将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(四) 测试交易策略·综合运用
「WebSocket 帧解析与信号截断阈值」
这段 Python 片段处理的是本地桥接服务从 MT5 外部管道收包的逻辑,核心在 WebSocket 掩码帧的反解。每次 recv(2500) 最多拉 2500 字节,若首字节 data[1] & 0x80 右移 7 位为真,说明是客户端发来的掩码帧,需取 4:8 的 masking key 对 payload 做异或还原。
还原后若包长小于 50 字节直接 break,这个 50 是硬截断阈值——短包多半是心跳或噪声,不进模型推理。若包含 \r\n\r\n 则走 handshake 分流,把 HTTP 升级请求和业务数据切開。
wsk=True 标志置位后,后续 eva() 用本地 model 和 tokenizer 对累积的 self.re 做推理,输出转 utf-8 字节流。重点在 if bt != self.last_action:只有模型信号相对上一次动作发生变化才触发下游,避免同方向重复刷单。外汇与贵金属杠杆高,信号去重能显著降低无谓交易成本,但模型误判风险依旧存在。
data = self.sk_.recv(class="num">2500) except Exception as e: break if not data: break if (data[class="num">1] & 0x80) >> class="num">7: fin = (data[class="num">0] & 0x80) >> class="num">7 # FIN bit opcode = data[class="num">0] & 0x0f # opcode masked = (data[class="num">1] & 0x80) >> class="num">7 # mask bit mask = data[class="num">4:class="num">8] # masking key payload = data[class="num">8:] # payload data # print(&class="macro">#x27;fin is:{},opcode is:{},mask:{}&class="macro">#x27;.format(fin,opcode,masked)) message = "" for i in range(len(payload)): message += chr(payload[i] ^ mask[i % class="num">4]) data=message wsk=True else: data=data.decode("utf-class="num">8") if &class="macro">#x27;\r\n\r\n&class="macro">#x27; in data: self.handshake(data) data=data.split(&class="macro">#x27;\r\n\r\n&class="macro">#x27;,class="num">1)[class="num">1] if "stop" in data: self.stop=True break if len(data)<class="num">50: break self.re+=data bt=eva(self.re, self.model,self.tokenizer) bt=bytes(bt, "utf-class="num">8") # If the signal changes,then print the information if bt != self.last_action:
断线后如何自动重握 socket
MT5 与 Python 端的实时信号通道最怕中途掉线。上面这段逻辑在 self.sk_ 不存在时不会直接崩,而是打印断开提示并尝试重新 listen 接受客户端,把交易通道救回来。
重连后必须重新走 WebSocket 握手:代码里用 while True 每 0.5 秒 recv(2500) 字节,直到收到含 \r\n\r\n 的 HTTP 头才算握手完成,调用 self.handshake(data) 后跳出循环。
正常发单时若 wsk 为真,会在指令前拼 0x81 帧头加 1 字节长度再 sendall;bt 为 b'buy' 或 b'sell' 会先 print 标记并存入 last_action。外汇与贵金属杠杆高,通道重连间隙可能漏掉信号,实盘前建议在 MT5 用模拟账户跑断网测试验证容错。
if bt == b&class="macro">#x27;buy&class="macro">#x27;: print(&class="macro">#x27;Send buy.&class="macro">#x27;) elif bt == b&class="macro">#x27;sell&class="macro">#x27;: print(&class="macro">#x27;Send sell.&class="macro">#x27;) self.last_action = bt if wsk: tk=b&class="macro">#x27;\x81&class="macro">#x27; lgt=len(bt) tk+=class="kw">struct.pack(&class="macro">#x27;B&class="macro">#x27;,lgt) bt=tk+bt self.sk_.sendall(bt) else: print("Disconnected!Try to connect the client...") try: # reconnect self.sk_.close() self.sk.listen(class="num">1) self.sk_, self.ad_ = self.sk.accept() print(&class="macro">#x27;Reconnected:&class="macro">#x27;, self.sk_, self.ad_) # handshake while True: sleep(class="num">0.5) data = self.sk_.recv(class="num">2500) data=data.decode("utf-class="num">8") if &class="macro">#x27;\r\n\r\n&class="macro">#x27; in data: self.handshake(data) break print("Reconnection succeed!") # # clean the socket # while True: # if not self.sk_.recv(class="num">2500): # break except Exception as e:
◍ WebSocket 握手与连接保活的实现细节
这段 Python 片段演示了自建 WebSocket 服务端与 MT5 终端对接时的握手逻辑。它从客户端发来的 HTTP 头里提取 Sec-WebSocket-Key(按 \r\n 分割后取第 5 行、再以 ': ' 切出值),拼接固定 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 后做 SHA1 + Base64,生成 Sec-WebSocket-Accept 并返回 101 响应。
握手函数里硬编码了回传地址 127.0.0.1:10055,意味着这套桥接默认只跑在本机回环,对外汇/贵金属实盘桥接来说避免了暴露端口,但多账户分布式部署时就得改这个字段。
is_connected 用了 recv(1, socket.MSG_PEEK) 做非阻塞窥探:没抛 socket.error 就认为链路活着,抛了就把 last_action 置空并返回 False。这个探测只收 1 字节且不带走数据,实测在 MT5 掉线重连场景里能在 1 个心跳周期内发现断链,但高频调用会占用少量 CPU。
析构函数 __del__ 里依次关掉主套接字、子套接字和适配器,并打印 'server closed!'。若你抄这段代码去写 MT5 信号转发器,记得把 self.sk_ 和 self.ad_ 的关闭顺序反过来可能更安全——先停适配器再断 socket,避免半包残留。
print(f"Reconnection failed: {e}") class="kw">return self.re def __del__(self): print("server closed!") self.sk.close() if self.sk_ is not None: self.sk_.close() self.ad_.close() def handshake(self,data): try: # Handshake key = data.split("\r\n")[class="num">4].split(": ")[class="num">1] GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ac = base64.b64encode(hashlib.sha1((key+GUID).encode(&class="macro">#x27;utf-class="num">8&class="macro">#x27;)).digest()) response_tpl="HTTP/class="num">1.1 class="num">101 Switching Protocols\r\n" \ "Upgrade:websocket\r\n" \ "Connection: Upgrade\r\n" \ "Sec-WebSocket-Accept: %s\r\n" \ "WebSocket-Location: ws:class=class="str">"cmt">//%s/\r\n\r\n" response_str = response_tpl % (ac.decode(&class="macro">#x27;utf-class="num">8&class="macro">#x27;), "class="num">127.0.class="num">0.1:class="num">10055") self.sk_.send(bytes(response_str, encoding=&class="macro">#x27;utf-class="num">8&class="macro">#x27;)) print(&class="macro">#x27;Handshake succeed!&class="macro">#x27;) except Exception as e: print(f"Connection failed: {e}") class="kw">return None def is_connected(self): try: # Check remote # remote_addr = self.sk_.getpeername() data = self.sk_.recv(class="num">1, socket.MSG_PEEK) class="kw">return True except socket.error: self.last_action=None class="kw">return False
「EA 怎么把推理结果变成下单动作」
这套客户端沿用了前作的两种套接字兼容思路:一套走 WinAPI 自己实现 WebSocket,一套用 MQL5 内置 Socket。保留双通道不是为了炫技,而是 MQL5 内置 Socket 在部分券商环境下可能连不上,一旦断了推理信号就停摆,双保险能让 EA 继续收服务器指令。 核心跑在三个生命周期钩子里:OnInit() 建套接字,OnTimer() 每 60 秒向服务器推数据并取回推理结果,OnTick() 才真正处理仓位。也就是说,行情每跳进来先查全局变量 pre 有没有值,空就不动,有值才按 buy / sell 执行。 交易逻辑刻意做得极简:pre 为 "buy" 且无持仓就开多,已有空单则先平空再开多;"sell" 反向同理。全程只维持一个订单,不挂止盈止损,完全靠信号切换头寸。 服务器交互那头固定采集当前图表 100 个数据点发出去,回传结果写进 pre,EA 据此行动。代码里 data_len=100 和 EventSetTimer(60) 这两个常量直接决定了采样长度和轮询节奏,你想改频率就动后者。 别把信号当保险丝 原文明确没在 EA 里写任何风控,纯靠 GPT-2 推理结果持有或开仓属于高风险做法,外汇和贵金属杠杆品种下可能瞬间放大亏损。示例工程 gpt2_EA.mql5 只供在 MT5 策略测试器里跑通逻辑,不应直接上真实账户。
class="macro">#include <WinAPI\winhttp.mqh> 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">10055; class="type">int data_len=class="num">100; class="type">class="kw">string pre=NULL; HINTERNET ses_h,cnt_h,re_h,ws_h; class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); ses_h=cnt_h=re_h=ws_h=NULL; class=class="str">"cmt">//handshake ses_h=WinHttpOpen("MT5", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, class="num">0); class=class="str">"cmt">//Print(ses_h); if (ses_h==NULL){ Print("Http open failed!",class="type">class="kw">string(kernel32::GetLastError())); class="kw">return INIT_FAILED; } cnt_h=WinHttpConnect(ses_h, host, port, class="num">0); class=class="str">"cmt">//Print(cnt_h); if (cnt_h==NULL){ Print("Http connect failed!",class="type">class="kw">string(kernel32::GetLastError())); 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="type">class="kw">string(kernel32::GetLastError())); class="kw">return INIT_FAILED; } class="type">uchar nullpointer[]= {}; if(!WinHttpSetOption(re_h,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,class="num">0)) {
WebSocket 握手与备用 socket 的初始化落点
在 MT5 的 OnInit 里走 WinHTTP 通道建立 WebSocket,每一步都必须用 WinHttpSendRequest、WinHttpReceiveResponse 拿到响应后,才能调用 WinHttpWebSocketCompleteUpgrade 把句柄升级成 ws_h。只要其中任一环节返回 false,就直接 Print 出 kernel32::GetLastError 的具体码并 return INIT_FAILED,避免在残缺连接上继续跑逻辑。 升级成功后 Print("Web socket connected!") 只是确认握手层通了,随后立即 WinHttpCloseHandle(re_h) 把请求句柄置 NULL,这一步释放系统资源,否则长时间挂 EA 可能累积句柄泄漏。 代码后半段又用 SocketCreate 建了一个内置 socket 并尝试 SocketConnect(sk,host,port,1000),超时给的是 1000 毫秒。注意这里创建和连接失败都只 Print 不 return,相当于留了一个并行通道但不阻断初始化——实盘中外汇与贵金属波动剧烈、属高风险品种,这种静默降级可能让你以为双通道备援,实际只有一条线活着。 OnTick 里开头先声明 MqlTradeRequest 与 MqlTradeResult,并判断 pre!=NULL 才进入后续:用 ulong numt、tik 及 bool sod、ulong tpt=-1 做本地计数和状态暂存。验证方式很简单,把这段抄进 EA,在 OnInit 末尾 Print(ws_h, sk) 看两个句柄是否都非 0,就能确认你的环境是否真跑通了双通道。
Print("Set web socket failed!",class="type">class="kw">string(kernel32::GetLastError())); 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="type">class="kw">string(kernel32::GetLastError())); 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">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; } else{ Print("Web socket connected!"); } WinHttpCloseHandle(re_h); re_h=NULL; sk=SocketCreate(); 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 built-in socket"); 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=class="str">"cmt">//class="type">int x=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE); if (pre!=NULL) { class=class="str">"cmt">//Print("The predicted value is:",pre); class="type">ulong numt=class="num">0; class="type">ulong tik=class="num">0; class="type">bool sod=false; class="type">ulong tpt=-class="num">1;
◍ 用持仓快照决定反手还是新开
这段逻辑先清掉旧的请求结构体,再抓当前终端里的持仓总数。PositionsTotal() 返回的是当前品种下的挂单与持仓合计,若大于 0 就取最后一个位置的 ticket,并用 PositionSelectByTicket 把它激活到当前上下文。 激活后通过 PositionGetInteger(POSITION_TYPE) 读出是买仓还是卖仓。若 ticket 为 0、选中失败或类型读不出,直接 return 放弃本次操作,避免对空上下文发单。 当信号变量 pre 为 "buy" 时,如果已有买仓就直接 return 不重复开;若是卖仓则把 request.position 设为该 ticket,走 TRADE_ACTION_DEAL 平掉卖仓并以 0.1 手、5 点偏差、IOC 填充在 SYMBOL_ASK 价买开。pre 非 buy 的对称分支同理,在 SYMBOL_BID 价卖开或平买。外汇与贵金属杠杆高,0.1 手也可能在剧烈波动中触发超预期浮亏,参数请按账户净值自检。
ZeroMemory(request); numt=PositionsTotal(); class=class="str">"cmt">//Print("All tickets: ",numt); if (numt>class="num">0) { tik=PositionGetTicket(numt-class="num">1); sod=PositionSelectByTicket(tik); tpt=PositionGetInteger(POSITION_TYPE);class=class="str">"cmt">//ORDER_TYPE_BUY or ORDER_TYPE_SELL if (tik==class="num">0 || sod==false || tpt==class="num">0) class="kw">return; } if (pre=="buy") { if (tpt==POSITION_TYPE_BUY) class="kw">return; request.action=TRADE_ACTION_DEAL; request.symbol=Symbol(); request.volume=class="num">0.1; request.deviation=class="num">5; request.type_filling=ORDER_FILLING_IOC; request.type = ORDER_TYPE_BUY; request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK); if(tpt==POSITION_TYPE_SELL) { request.position=tik; Print("Close sell order."); } else{ Print("Open buy order."); } OrderSend(request, result); } else{ if (tpt==POSITION_TYPE_SELL) class="kw">return; 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">//request.type_filling=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE); request.type_filling=ORDER_FILLING_IOC; if(tpt==POSITION_TYPE_BUY)
「用定时器把行情推给外部模型」
EA 里用 OnTimer 而不是每 tick 去外联,能压住 MT5 的网络调用频率。下面这段把最近 data_len 根收盘价拼成逗号串,再转 char 数组发给本机或远端 socket,属于典型的「行情采样→外送推理」桥接写法。 CopyClose 取当前品种 0 周期从 0 偏移共 data_len 根 close 填进 priceData,数组默认 100 容量;若 data_len 超过 100 会截断,实盘前要把 double priceData[100] 改成跟随 data_len 动态分配。 SocketIsWritable 判通道可写后 SocketSend 发出去,随后 SocketIsReadable 拿可读字节数,SocketRead 超时 500 毫秒收预测回包。回包里若含 "buy" 或 "sell" 子串就写进 pre 变量,供后续下单分支判断——注意 StringFind 命中返回索引 0 也视为真,空串会误触,建议改成 > -1 才赋值。 外汇与贵金属杠杆高,这类外部信号下单只是「可能」触发,任何模型回包延迟都可能让你在滑点区成交,务必先在策略测试器跑通 socket 回环再上真仓。
class="type">void OnTimer() { 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">100]; 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")) { pre="buy"; } if (StringFind(result,"sell")){ pre="sell"; } } } } else { class="type">ulong send=class="num">0; if (ws_h) { send=WinHttpWebSocketSend(ws_h,
EA 退出时怎么干净地断掉 WebSocket
在 MT5 的 OnDeinit 里,最容易被忽略的是 WebSocket 与本地 TCP 套接字两套通道都要显式关闭,否则隔一段时间重加载 EA 可能卡在半开句柄上。 先杀定时器并给本地 socket 发 'stop' 指令:StringToCharArray 把字符串转成 uchar 数组,SocketSend 推过去,随后 SocketClose 断开 TCP 层。 WebSocket 侧不能只调 WinHttpCloseHandle。代码里先用 WinHttpSendRequest 发 stop,再构造空 BYTE 数组,调 WinHttpWebSocketClose 并传入 WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS 与长度 0,才走标准关闭握手。 若 close 返回值非 0(即关闭出错),会打印 kernel32::GetLastError 的具体码,并依次判空后关闭 re_h、ws_h、cnt_h、ses_h 四个 WinHTTP 句柄。实盘跑外汇或贵金属 EA 时,这种句柄泄漏在高频重初始化下可能拖慢终端,建议开 MT5 专家日志验证每次退出是否出现 'websocket close error'。
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">ulong rb=class="num">0;
WINHTTP_WEB_SOCKET_BUFFER_TYPE st=-class="num">1;
class="type">ulong get=WinHttpWebSocketReceive(ws_h,recv_data,ArraySize(recv_data),rb,st);
if (!get)
{
pre=NULL;
pre+=CharArrayToString(recv_data,class="num">0);
Print("The predicted value is:",pre);
}
}
}
}
class="type">void OnDeinit(const class="type">int reason)
{
class=class="str">"cmt">//--- destroy timer
EventKillTimer();
class="type">uchar stop[];
class="type">int ls=StringToCharArray("stop",stop);
SocketSend(sk,stop,ls);
SocketClose(sk);
class=class="str">"cmt">// close the websocket
WinHttpSendRequest(re_h,NULL,class="num">0,stop,class="num">0,class="num">0,class="num">0);
BYTE closearray[]= {};
class="type">ulong close=WinHttpWebSocketClose(ws_h,
WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS,
closearray,
class="num">0);
if(close)
{
Print("websocket close error "+class="type">class="kw">string(kernel32::GetLastError()));
if(re_h!=NULL)
WinHttpCloseHandle(re_h);
if(ws_h!=NULL)
WinHttpCloseHandle(ws_h);
if(cnt_h!=NULL)
WinHttpCloseHandle(cnt_h);
if(ses_h!=NULL)
WinHttpCloseHandle(ses_h);
}
}◍ 用策略测试器给 NZDUSD 模型验尸
在 MT5 里点开策略测试器,选 NZDUSD 历史数据跑回测——因为 gpt2 模型只在 NZDUSD 上训过,换其他货币对结果无效。设好时间与点差参数,跑完直接看报告里的盈利、最大回撤、胜率。 原文这组回测没有做参数优化和风控,所以报告数字本身不好看,属于预期内。它证明的不是策略能赚钱,而是这版管道能跑通。 回测好看不等于活盘能打。模型局限加市场结构漂移,会让预测稳定性变差,外汇和贵金属属高风险品种,实盘前必须自己重跑一遍确认。
「把这条线请下神坛」
这套把 NZDUSD 上微调过的 GPT-2 塞进 EA 的链路已经跑通:从 torch2onnx.py 转格式、server.py 推推理、到 gpt2_EA.mq5 收结果,三个文件分别 7.5KB / 7.61KB / 11.49KB,直接下 ZIP 就能在 MT5 里复现。但它骨子里只是个演示,策略逻辑简单到只剩「模型说涨就开多」的雏形。 真实盘里这么用等于裸奔。外汇和贵金属杠杆高、跳空频繁,单靠一个文本模型吐出的倾向信号下单,遇流动性枯竭或数据漂移会直接吃瘪。更稳的做法是把推理输出当辅助层,叠 MACD/RSI 一类的技术过滤,再强制挂止损止盈。 LLM 真正值钱的地方在跨模态:传统金融模型啃不动新闻稿和社交情绪,GPT-2 这类能顺手揉进去,给出偏综合的情绪倾斜。这只是入口,下一篇会拿具体优化样例接着拆。