用于时间序列挖掘的数据标签(第 5 部分):使用 Socket 在 EA 中进行应用和测试·进阶篇
🔌

用于时间序列挖掘的数据标签(第 5 部分):使用 Socket 在 EA 中进行应用和测试·进阶篇

(2/3)· 模型训好了却卡在回测环节?本文用 WebSocket 把 Python 推理搬进 MT5 历史测试

实战向 第 2/3 篇
很多交易者把模型在 Python 里跑通就以为完事了,真到 MT5 回测才发现两边语言不通、数据传不过去。其实只要架一层 Socket 服务,EA 发柱数、服务器返信号,验证流程就能跑起来。

◍ 从模型回传文本到下单请求的衔接

拿到远端模型返回串之后,先别急着发单,要把可读字节数用 SocketIsReadable 探出来,再按实际长度收口。下面这段把 recv_data 转成字符串后,只做了 Print 输出预测值,真正的 OrderSend 被注释掉了——这是故意留的手动确认位。 判断方向靠 StringFind(result,"buy") 做字符串匹配:命中就填 ORDER_TYPE_BUY,否则走 ORDER_TYPE_SELL。两个分支都写死 volume=0.1、deviation=5,价格分别取 SYMBOL_ASK / SYMBOL_BID。外汇与贵金属杠杆高,写死手数在极端滑点下可能放大回撤,实盘前建议改成变量。 注意 rsp_len>0 才解析,SocketRead 的 500 是超时毫秒数;若模型端延迟超过这个值,这一轮就不会触发任何交易动作。你可以把 500 调到 2000 对比 MT5 专家日志里丢包频率的变化。

MQL5 / C++
class="type">int ssl=SocketSend(sk,ds,dsl);
}
class="type">uint len=SocketIsReadable(sk);
if (len)
{
  class="type">int rsp_len=SocketRead(sk,recv_data,len,class="num">500);
  if(rsp_len>class="num">0)
  {
    class="type">class="kw">string result;
    result+=CharArrayToString(recv_data,class="num">0,rsp_len);
    Print("The predicted value is:",result);
    if (StringFind(result,"buy"))
    {
      ZeroMemory(request);
      request.action = TRADE_ACTION_DEAL;      
      request.symbol = Symbol();  
      request.volume = class="num">0.1;  
      request.type = ORDER_TYPE_BUY;  
      request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK);  
      request.deviation = class="num">5;
      class=class="str">"cmt">//OrderSend(request, result);
    }
    else{
      ZeroMemory(request);
      request.action = TRADE_ACTION_DEAL;      
      request.symbol = Symbol();  
      request.volume = class="num">0.1;  
      request.type = ORDER_TYPE_SELL;  
      request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID);  
      request.deviation = class="num">5;
      class=class="str">"cmt">//OrderSend(request, result);
      }
  }
}
}

用 WinHTTP 在 MT5 回测里跑通 WebSocket

MQL5 原生套接字在回测环境里基本失效,想让 EA 在策略测试器里跟 Python 模型通信,得换 WinHTTP 的 WebSocket 通道。先把微软的 winhttp.mqh 丢进客户端数据文件夹的 Include/WinAPI/ 路径下,否则编译阶段就卡死。 握手有严格顺序:WinHttpOpen → WinHttpConnect → WinHttpOpenRequest → 设升级选项 → SendRequest → ReceiveResponse → CompleteUpgrade,最后关掉原请求句柄。任一步返回 NULL 都代表初始化失败,且回测时那些「初始化失败就 return」的默认语句必须注释掉,否则流程走不到 WebSocket 逻辑。 OnTick 里用 sk!=-1 区分实盘套接字与回测 WebSocket:若 sk==-1 就走 WinHttpWebSocketSend 发数据,返回 0 才算成功;接着 ZeroMemory 清接收缓冲,WinHttpWebSocketReceive 拿数据,返回 0 接收成功。服务器推 "buy" 就开多,"sell" 开空,已有反向单先 PositionTotal 查持仓再 OrderSend 平仓反手。 端口写死 8989、host 为 127.0.0.1、发单手数 0.1、deviation=5、填充 IOC——这些数直接抄进代码就能在 Ctrl+F5 回测里看到成交。外汇与贵金属杠杆高,回测通过不等于实盘能复现,信号延迟或滑点可能让概率倾斜。 不想每次报价都发数据,可把主逻辑挪到 OnTimer 里,比如每 15 分钟触发一次,能压低频繁请求带来的开销。代码层只演示 WebSocket 客户端骨架,服务器掩码解包与握手响应另算。

MQL5 / C++
class="type">int sk=-class="num">1;
class="type">class="kw">string host="class="num">127.0.class="num">0.1";
class="type">int port= class="num">8989;
class="type">int data_len=class="num">300;
HINTERNET ses_h,cnt_h,re_h,ws_h;
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   ses_h=cnt_h=re_h=ws_h=NULL;
   ses_h=WinHttpOpen("MT5",
                      WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                      NULL,
                      NULL,
                      class="num">0);
   Print(ses_h);
   if (ses_h==NULL){
      Print("Http open failed!");
      class="kw">return INIT_FAILED;
      }
   cnt_h=WinHttpConnect(ses_h,

「从 HTTP 握手切到 WebSocket 通道」

这段逻辑跑在 EA 的初始化阶段,目标不是发普通 REST 请求,而是把一条 WinHTTP 连接升级成 WebSocket,用来持续收报价流。外汇与贵金属行情刷新快、跳空多,用 WebSocket 推流比轮询更省资源,但连接失败会直接让 INIT_FAILED,EA 起不来。 先 WinHttpOpen 拿到连接句柄 cnt_h,若返回 -1 说明连不上 host:port,Print 后直接退出。接着 WinHttpOpenRequest 用 GET 方法建请求句柄 re_h,所有 header 与 flag 填 NULL/0 即可,失败同样回 INIT_FAILED。 升级动作靠 WinHttpSetOption 带 WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,入参是空 uchar 数组 nullpointer 和长度 0;这一步不成功就断 init。随后 WinHttpSendRequest 与 WinHttpReceiveResponse 连续调用,任一返回 false 就打印 GetLastError 的具体码并退出。 收尾用 WinHttpWebSocketCompleteUpgrade 把 re_h 换成 WebSocket 句柄 ws_h,升级失败打印错误码。最后 WinHttpCloseHandle 关掉原请求句柄、置 re_h 为 NULL,并调 SocketCreate 建一个备用 socket 句柄 sk——这一步说明代码同时保留了 WebSocket 与底层 socket 两条路。

MQL5 / C++
                              host,
                              port,
                              class="num">0);
   Print(cnt_h);
   if (cnt_h==-class="num">1){
      Print("Http connect failed!");
      class="kw">return INIT_FAILED;
      }
   re_h=WinHttpOpenRequest(cnt_h,
                              "GET",
                              NULL,
                              NULL,
                              NULL,
                              NULL,
                              class="num">0);
   if(re_h==NULL){
      Print("Request open failed!");
      class="kw">return INIT_FAILED;
   }
   class="type">uchar nullpointer[]= {};
   if(!WinHttpSetOption(re_h,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,class="num">0))
      {
            Print("Set web socket failed!");
            class="kw">return INIT_FAILED;
         }
   class="type">bool br;   
   br = WinHttpSendRequest( re_h,
                              NULL,
                              class="num">0,
                              nullpointer,
                              class="num">0,
                              class="num">0,
                              class="num">0);
   if (!br)
      {
         Print("send request failed!");
         class="kw">return INIT_FAILED;
         }
   br=WinHttpReceiveResponse(re_h,nullpointer);            
   if (!br)
    {
      Print("receive response failed!",class="type">class="kw">string(kernel32::GetLastError()));
      class="kw">return INIT_FAILED;
      }
   class="type">class="kw">ulong nv=class="num">0;
   ws_h=WinHttpWebSocketCompleteUpgrade(re_h,nv);  
   if (!ws_h)
   {
      Print("Web socket upgrade failed!",class="type">class="kw">string(kernel32::GetLastError()));
      class="kw">return INIT_FAILED;
         }


   WinHttpCloseHandle(re_h);
   re_h=NULL;
   sk=SocketCreate();

◍ EA 通过 socket 把行情甩给外部模型

这段逻辑演示了 MT5 的 EA 如何借助 Socket 把本地收盘价数组送往外部 Python 服务,再读回预测信号。外汇与贵金属杠杆高、滑点大,这种自动链路只是技术验证,实盘触发订单前必须人工复核。 初始化阶段先打印句柄与错误码,若 sk 等于 INVALID_HANDLE 或 SocketConnect 在 1000 毫秒内连不上 host:port,就打印失败信息(return 被注释,方便继续跑后面)。OnTick 里用 CopyClose 取当前品种 0 号周期最近 data_len 根收盘,装进 priceData[300],再拼成逗号分隔字符串经 StringToCharArray 转成字节数组 ds。 sk 不等于 -1 时,SocketIsWritable 为真就用 SocketSend 把 ds 发过去,Print 出发送长度 dsl;随后 SocketIsReadable 拿到可读字节数 len,若大于 0 则 SocketRead 进 recv_data[5]、超时 500 毫秒。读到的内容转成字符串,若包含 "buy" 就填 MqlTradeRequest 准备以 EURUSD 市价买 0.1 手、偏差 5 点——但 OrderSend 被注释,意味着默认只打印 "The predicted value is:" 不真下单。 想自己验证,把代码里两处 //return INIT_FAILED 和 OrderSend 的注释按需打开,先在策略测试器跑 EURUSD 观察 Print 的预测值,再决定是否接真账户。外接模型若返回非 buy 分支,代码仅 ZeroMemory(request) 清空结构,没有卖单逻辑,别以为它能双向交易。

MQL5 / C++
  Print(sk);
  Print(GetLastError());
  if (sk==INVALID_HANDLE) {
    Print("Failed to create socket");
    class=class="str">"cmt">//class="kw">return INIT_FAILED;
  }
  if (!SocketConnect(sk,host, port,class="num">1000))
  {
    Print("Failed to connect to server");
    class=class="str">"cmt">//class="kw">return INIT_FAILED;
  }
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   class="type">MqlTradeRequest request;
   class="type">MqlTradeResult result;
   class="type">char recv_data[class="num">5];
   class="type">class="kw">double priceData[class="num">300];
   class="type">class="kw">string dataToSend;
   class="type">char ds[];
   class="type">int nc=CopyClose(Symbol(),class="num">0,class="num">0,data_len,priceData);
   for(class="type">int i=class="num">0;i<ArraySize(priceData);i++) dataToSend+=(class="type">class="kw">string)priceData[i]+",";
   class="type">int dsl=StringToCharArray(dataToSend,ds);


   if (sk!=-class="num">1)
   {
     if (SocketIsWritable(sk))
       {
       Print("Send data:",dsl);
       class="type">int ssl=SocketSend(sk,ds,dsl);
       }
     class="type">uint len=SocketIsReadable(sk);
     if (len)
     {
       class="type">int rsp_len=SocketRead(sk,recv_data,len,class="num">500);
       if(rsp_len>class="num">0)
       {
         class="type">class="kw">string result=NULL;
         result+=CharArrayToString(recv_data,class="num">0,rsp_len);
         Print("The predicted value is:",result);
         if (StringFind(result,"buy"))
         {
           ZeroMemory(request);
           request.action = TRADE_ACTION_DEAL;
           request.symbol = "EURUSD";
           request.volume = class="num">0.1;
           request.type = ORDER_TYPE_BUY;
           request.price = SymbolInfoDouble("EURUSD", SYMBOL_ASK);
           request.deviation = class="num">5;
           class=class="str">"cmt">//OrderSend(request, result);
         }
         else{
           ZeroMemory(request);

把模型预测值接回 MT5 下单流

这段逻辑展示了一个典型闭环:本地通过 WebSocket 拿到外部模型的预测字符串后,并不只是打印,而是准备回写进交易请求结构。request 里把 action 设成 TRADE_ACTION_DEAL、symbol 写死 EURUSD、volume 给 0.1 手、type 用 ORDER_TYPE_SELL,price 直接取 SYMBOL_BID,deviation 留 5 点容差——这是最朴素的市价空单模板。 注意 OrderSend 那行被注释掉了,说明作者当时在调试,没真发单。你在 MT5 里跑类似代码时,务必先确认账户杠杆与 0.1 手保证金是否匹配,外汇和贵金属杠杆交易风险极高,滑点可能让实际成交偏离 BID 数个点。 WebSocket 接收段里,WinHttpWebSocketReceive 拿到服务端返回的二进制包,用 CharArrayToString 转成可读预测值并打印「The predicted value is:」前缀。随后 PositionsTotal 查当前持仓,若大于 0 就用 OrderGetTicket(numt-1) 取最后一单 ticket 做 OrderSelect——这一步是为后续跟单或对冲做准备,不是预测即跟。 别把预测字符串当信号直接梭 模型回传的 pre 只是文本,没有经过阈值判断就进持仓逻辑,实盘可能把噪声当指令。建议你在 Print 之后加一层 double 转换与边界判断,再决定是否填充 request。

MQL5 / C++
request.action = TRADE_ACTION_DEAL;
request.symbol = "EURUSD";
request.volume = class="num">0.1;
request.type = ORDER_TYPE_SELL;
request.price = SymbolInfoDouble("EURUSD", SYMBOL_BID);
request.deviation = class="num">5;
class=class="str">"cmt">//OrderSend(request, result);
}
}
}
}
else
{
class="type">class="kw">ulong send=class="num">0;
if (ws_h)
{
send=WinHttpWebSocketSend(ws_h,
WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE,
ds,
dsl);
class=class="str">"cmt">//Print("Send data failed!",class="type">class="kw">string(kernel32::GetLastError()));
if(!send)
{
ZeroMemory(recv_data);
class="type">class="kw">ulong rb=class="num">0;
WINHTTP_WEB_SOCKET_BUFFER_TYPE st=-class="num">1;
class="type">class="kw">ulong get=WinHttpWebSocketReceive(ws_h,recv_data,ArraySize(recv_data),rb,st);
if (!get)
{
class="type">class="kw">string pre=NULL;
pre+=CharArrayToString(recv_data,class="num">0);
Print("The predicted value is:",pre);
class="type">class="kw">ulong numt=class="num">0;
class="type">class="kw">ulong tik=class="num">0;
class="type">bool sod=class="kw">false;
class="type">class="kw">ulong tpt=-class="num">1;
numt=PositionsTotal();
if (numt>class="num">0)
{ tik=OrderGetTicket(numt-class="num">1);
sod=OrderSelect(tik);

「信号反转时怎么平旧仓开新仓」

当外部信号字符串 pre 为 "buy" 时,先读取当前挂单或成交单的方向 tpt:若是 ORDER_TYPE_BUY 就直接 return 不重复操作,若是 ORDER_TYPE_SELL 则把 request.action 设为 TRADE_ACTION_REMOVE 并填好 request.order=tik 来撤掉卖单,日志打 "Close sell order."。 若账户里根本没有同向或反向单(即 tpt 既不是买也不是卖),就走 ZeroMemory(request) 重置结构体,然后以市价买单参数填充:volume=1type=ORDER_TYPE_BUYprice=SymbolInfoDouble(Symbol(),SYMBOL_ASK)deviation=5type_filling=ORDER_FILLING_IOC,最后 OrderSend(request,result) 发出去,日志打 "Open buy order."。 pre 为其他值(如 "sell")的分支逻辑镜像对称:已有卖单就 return,有买单则撤单,无仓则按 SYMBOL_BID 开卖。外汇与贵金属杠杆高,这类反转逻辑若 deviation 设太小在跳空时可能拒单,建议先在策略测试器跑 2023 年 XAUUSD 的 M1 数据看成交率。

MQL5 / C++
tpt=OrderGetInteger(ORDER_TYPE);class=class="str">"cmt">//ORDER_TYPE_BUY or ORDER_TYPE_SELL
 }
 if (pre=="buy")
 {
 if (tpt==ORDER_TYPE_BUY)
 class="kw">return;
 else if(tpt==ORDER_TYPE_SELL)
 {
 request.order=tik;
 request.action=TRADE_ACTION_REMOVE;
 Print("Close sell order.");
 }
 else{
 ZeroMemory(request);
 request.action = TRADE_ACTION_DEAL;
 request.symbol = Symbol();
 request.volume = class="num">1;
 request.type = ORDER_TYPE_BUY;
 request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
 request.deviation = class="num">5;
 request.type_filling=ORDER_FILLING_IOC;
 Print("Open buy order.");

 OrderSend(request, result);
 }
 else{
 if (tpt==ORDER_TYPE_SELL)
 class="kw">return;
让小布替你盯回测通道
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 Socket 延迟与推理返回状态,你专注调模型而非查日志。

常见问题

大型模型推理常带跨语言优化,硬转 ONNX 迁移成本极高;Socket 方案可让服务器跑在远程或 WSL/Docker,不绑单一系统。
可以,小布盯盘内置了通道状态观测,能直接显示 Socket 推理服务的响应与异常,省去手动翻日志。
可能带来轻微延迟,倾向用异步发送或非阻塞客户端,并在 tick 频率低的品种上先验证。
WebSocket 自带帧协议更易对接 Python 库,裸 TCP 更轻量但需自己处理分包,按模型输入输出复杂度选。
不能,本文只是演示实例,外汇贵金属高风险,实盘需独立理论支撑与大量稳健性验证。