用于时间序列挖掘的数据标签(第 5 部分):使用 Socket 在 EA 中进行应用和测试·进阶篇
(2/3)· 模型训好了却卡在回测环节?本文用 WebSocket 把 Python 推理搬进 MT5 历史测试
◍ 从模型回传文本到下单请求的衔接
拿到远端模型返回串之后,先别急着发单,要把可读字节数用 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 专家日志里丢包频率的变化。
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 客户端骨架,服务器掩码解包与握手响应另算。
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 两条路。
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) 清空结构,没有卖单逻辑,别以为它能双向交易。
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。
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=1、type=ORDER_TYPE_BUY、price=SymbolInfoDouble(Symbol(),SYMBOL_ASK)、deviation=5、type_filling=ORDER_FILLING_IOC,最后 OrderSend(request,result) 发出去,日志打 "Open buy order."。
pre 为其他值(如 "sell")的分支逻辑镜像对称:已有卖单就 return,有买单则撤单,无仓则按 SYMBOL_BID 开卖。外汇与贵金属杠杆高,这类反转逻辑若 deviation 设太小在跳空时可能拒单,建议先在策略测试器跑 2023 年 XAUUSD 的 M1 数据看成交率。
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;