在MQL中操作套接字,或者如何成为信号提供者·综合运用
(3/3)· 前两轮铺垫了理论与单机通信,这一篇把多实例同步、订单采集与服务器加固一次打通
◍ 用长连接让客户端实时跟单品种仓位
这一节把前面服务端和客户端的基础代码往上叠了一层:服务器发完数据不关连接,客户端也一直挂着。这样服务器上仓位一变,数据包立刻通过套接字推过去,客户端几乎实时和服务器同步持仓。外汇和贵金属杠杆高、滑点难测,这种同步只解决传输延迟,不替你过滤风险。
| 服务器每秒轮一次:连上来的客户端塞进总数组、把数据发出去,同时自查套接字还活着没;连接掉了就重建一个。EA 所在品种名和交易量会发给客户端,格式像 <<GBPUSD | 0.25>> 表示开 0.25 手,<<GBPUSD | 0.00>> 表示平掉。每个交易事件都发,新客户端连进来时也补发一遍。 |
|---|
客户端这次写成 EA 而不是脚本,因为必须维持活动连接。它收到数据追加到已有缓冲,再扫描 << 开头 >> 结尾的片段,解析后按服务器指定的品种交易量调自己的仓。下面这段服务器代码是整套逻辑的骨架,端口写死 8081,每秒定时器触发检测与推送。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| SignalServer | class=class="str">"cmt">//| programming & development - Alexey Sergeev | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "© class="num">2006-class="num">2016 Alexey Sergeev" class="macro">#class="kw">property link "profy.mql@gmail.com" class="macro">#class="kw">property version "class="num">1.00" class="macro">#include "SocketLib.mqh" class="kw">input class="type">class="kw">string Host="class="num">0.0.class="num">0.0"; class="kw">input class="type">class="kw">ushort Port=class="num">8081; class="type">bool bChangeTrades; class="type">uchar data[]; SOCKET server=INVALID_SOCKET; SOCKET conns[]; class=class="str">"cmt">//------------------------------------------------------------------ OnInit class="type">int OnInit() { OnTrade(); EventSetTimer(class="num">1); class="kw">return class="num">0; } class=class="str">"cmt">//------------------------------------------------------------------ OnDeinit class="type">void OnDeinit(class="kw">const class="type">int reason) { EventKillTimer(); CloseClean(); } class=class="str">"cmt">//------------------------------------------------------------------ OnTrade class="type">void OnTrade() { class="type">class="kw">double lot=GetSymbolLot(Symbol()); StringToCharArray("<<"+Symbol()+"|"+DoubleToString(lot,class="num">2)+">>",data); class=class="str">"cmt">// 把字符串转换为字节数组 bChangeTrades=true; } class=class="str">"cmt">//------------------------------------------------------------------ OnTimer class="type">void OnTimer() { if(server==INVALID_SOCKET) StartServer(Host,Port); else { AcceptClients(); class=class="str">"cmt">// 加上等待的客户端 if(bChangeTrades) { Print("向客户端发送新的仓位信息"); Send(); bChangeTrades=class="kw">false; } } } class=class="str">"cmt">//------------------------------------------------------------------ StartServer class="type">void StartServer(class="type">class="kw">string addr,class="type">class="kw">ushort port) { class=class="str">"cmt">// 初始化库 class="type">char wsaData[]; ArrayResize(wsaData,class="kw">sizeof(WSAData)); class="type">int res=WSAStartup(MAKEWORD(class="num">2,class="num">2), wsaData); if(res!=class="num">0) { Print("-WSAStartup 失败,错误: "+class="type">class="kw">string(res)); class="kw">return; } class=class="str">"cmt">// 创建一个套接字 server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(server==INVALID_SOCKET) { Print("-创建失败,错误: "+WSAErrorDescript(WSAGetLastError())); CloseClean(); class="kw">return; } class=class="str">"cmt">// 绑定地址和端口 Print("尝试绑定..."+addr+":"+class="type">class="kw">string(port));
服务端绑定端口与接收客户端连接
在 MT5 里用 MQL5 搭一个本地 socket 服务,第一步是把地址字符串转成字符数组再交给 inet_addr,bind 到指定端口后立刻切非阻塞。非阻塞模式下 listen 用 SOMAXCONN 挂队列,accept 不会卡死主线程,EA 仍能正常跑行情与下单逻辑。 accept 拿到客户端套接字后,同样要 ioctlsocket 设 FIONBIO=1,否则单客户掉线可能堵住整段接收循环。新连接进数组时 ArrayResize(conns,n+1) 把句柄存起来,并置 bChangeTrades=true,提示把当前仓位快照推给对面。 客户端 IP 用 inet_ntop 取,端口用 ntohs(aclient.sin_port) 还原,printf 打出的日志形如「接收新的客户端 127.0.0.1 : 54321」。外汇与贵金属行情转发走这套机制时,注意本机网络暴露面带来的高风险,建议仅限回环地址调试。
class="type">char ch[]; StringToCharArray(addr,ch); sockaddr_in addrin; addrin.sin_family=AF_INET; addrin.sin_addr=inet_addr(ch); addrin.sin_port=htons(port); ref_sockaddr ref=(ref_sockaddr)addrin; if(bind(server,ref.ref,class="kw">sizeof(addrin))==SOCKET_ERROR) { class="type">int err=WSAGetLastError(); if(err!=WSAEISCONN) { Print("-连接失败,错误: "+WSAErrorDescript(err)+". 清除套接字"); CloseClean(); class="kw">return; } } class=class="str">"cmt">// 设置为非阻塞模式 class="type">int non_block=class="num">1; res=ioctlsocket(server,(class="type">int)FIONBIO,non_block); if(res!=NO_ERROR) { Print("ioctlsocket 失败,错误: "+class="type">class="kw">string(res)); CloseClean(); class="kw">return; } class=class="str">"cmt">// 侦听端口,接受客户端的连接 if(listen(server,SOMAXCONN)==SOCKET_ERROR) { Print("侦听失败,错误: ",WSAErrorDescript(WSAGetLastError())); CloseClean(); class="kw">return; } Print("启动服务器成功"); } class=class="str">"cmt">//------------------------------------------------------------------ Accept class="type">void AcceptClients() class=class="str">"cmt">// 接受一个客户端套接字 { if(server==INVALID_SOCKET) class="kw">return; class=class="str">"cmt">// 加上所有等待的客户端 SOCKET client=INVALID_SOCKET; do { ref_sockaddr ch; class="type">int len=class="kw">sizeof(ref_sockaddr); client=accept(server,ch.ref,len); if(client==INVALID_SOCKET) { class="type">int err=WSAGetLastError(); if(err==WSAEWOULDBLOCK) Comment("\n等待客户端("+class="type">class="kw">string(TimeCurrent())+")"); else { Print("接受失败,错误: ",WSAErrorDescript(err)); CloseClean(); } class="kw">return; } class=class="str">"cmt">// 设置到非阻塞模式 class="type">int non_block=class="num">1; class="type">int res=ioctlsocket(client, (class="type">int)FIONBIO, non_block); if(res!=NO_ERROR) { Print("ioctlsocket 失败,错误: "+class="type">class="kw">string(res)); class="kw">continue; } class=class="str">"cmt">// 把客户端套接字加到数组中 class="type">int n=ArraySize(conns); ArrayResize(conns,n+class="num">1); conns[n]=client; bChangeTrades=true; class=class="str">"cmt">// 用于指示仓位信息必须要发送的标志 class=class="str">"cmt">// 显示客户端信息 class="type">char ipstr[class="num">23]={class="num">0}; sockaddr_in aclient=(sockaddr_in)ch; class=class="str">"cmt">// 转换为结构以取得连接的额外信息 inet_ntop(aclient.sin_family,aclient.sin_addr,ipstr,class="kw">sizeof(ipstr)); class=class="str">"cmt">// 取得地址 printf("接收新的客户端 %s : %d",CharArrayToString(ipstr),ntohs(aclient.sin_port)); } class="kw">while(client!=INVALID_SOCKET); } class=class="str">"cmt">//------------------------------------------------------------------ SendClient class="type">void Send() { class="type">int len=ArraySize(data); for(class="type">int i=ArraySize(conns)-class="num">1; i>=class="num">0; --i) class=class="str">"cmt">// 向客户端发送信息 {
「套接字清理与仓位净量的实盘写法」
在 MT5 的 EA 里做多端通信,断线回收比建连更考验细节。上面这段把服务器、客户端数组、单套接字三层关闭逻辑拆开写,能避免 WSA 句柄泄漏导致终端越跑越卡。 CloseClean 先打印当前连接数 ArraySize(conns),再逐个反向遍历调用 Close,最后 ArrayResize(conns,0) 清空并 WSACleanup 释放系统网络库。反向遍历(i 从 Size-1 到 0)是为了删除元素时不打乱下标,这个细节新手常写成正向漏关。 Close 函数里先 shutdown(asock,SD_BOTH) 双向切断,失败仅 Print 不阻断,随后 closesocket 并把入参引用置为 INVALID_SOCKET,保证调用方手里不再是野指针。 GetSymbolLot 给出一种净仓位算法:遍历 PositionsTotal,用 PositionGetString(POSITION_SYMBOL) 过滤品种,卖仓 volume 取负再加总。若某品种同时有 0.1 买和 0.05 卖,返回 +0.05,可直接作为信号端发给另一台终端的对冲参考。外汇与贵金属杠杆高,净仓位计算错一位可能触发超预期敞口,务必在策略测试器里先跑一遍。
if(conns[i]==INVALID_SOCKET) class="kw">continue; class=class="str">"cmt">// 跳过关闭的套接字 class="type">int res=send(conns[i],data,len,class="num">0); class=class="str">"cmt">// 发送 if(res==SOCKET_ERROR) { Print("-发送失败,错误: "+WSAErrorDescript(WSAGetLastError())+". 关闭套接字"); Close(conns[i]); } } } class=class="str">"cmt">//------------------------------------------------------------------ CloseClean class="type">void CloseClean() class=class="str">"cmt">// 关闭和清除操作 { printf("关闭服务器和%d个连接",ArraySize(conns)); if(server!=INVALID_SOCKET) { closesocket(server); server=INVALID_SOCKET; } class=class="str">"cmt">// 关闭服务器 for(class="type">int i=ArraySize(conns)-class="num">1; i>=class="num">0; --i) Close(conns[i]); class=class="str">"cmt">// 关闭客户端 ArrayResize(conns,class="num">0); WSACleanup(); } class=class="str">"cmt">//------------------------------------------------------------------ Close class="type">void Close(SOCKET &asock) class=class="str">"cmt">// 关闭一个套接字 { if(asock==INVALID_SOCKET) class="kw">return; if(shutdown(asock,SD_BOTH)==SOCKET_ERROR) Print("-关闭失败,错误: "+WSAErrorDescript(WSAGetLastError())); closesocket(asock); asock=INVALID_SOCKET; } class=class="str">"cmt">//------------------------------------------------------------------ GetSymbolLot class="type">class="kw">double GetSymbolLot(class="type">class="kw">string smb) { class="type">class="kw">double slot=class="num">0; class="type">int n=PositionsTotal(); for(class="type">int i=class="num">0; i<n;++i) { PositionSelectByTicket(PositionGetTicket(i)); if(PositionGetString(POSITION_SYMBOL)!=smb) class="kw">continue; class=class="str">"cmt">// 当服务器正在运行时,过滤掉当前交易品种的仓位 class="type">class="kw">double lot=PositionGetDouble(POSITION_VOLUME); class=class="str">"cmt">// 取得交易量 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) lot=-lot; class=class="str">"cmt">// 考虑方向 slot+=lot; class=class="str">"cmt">// 加到总和中 } class="kw">return slot; }
◍ EA 里的 TCP 长连接怎么搭
想在 MT5 的 EA 里直连外部信号源,得先卡掉一类账户。初始化时若检测到券商是零售对冲(净头寸)模式,直接弹窗报警并返回 INIT_FAILED,避免后续保证金计算逻辑和裸净额账户打架。 定时器每秒触发一次 OnTimer,连接断了就重拨 StartClient,连上了就轮询 recv。下面这段是套接字从建链到非阻塞的核心,逐行拆开看更清楚。 建链函数里 WSAStartup 用 MAKEWORD(2,2) 要 2.2 版 Winsock,socket 走 AF_INET + SOCK_STREAM + IPPROTO_TCP 标准 TCP。connect 之后若返回 SOCKET_ERROR 且不是 WSAEISCONN,就清场退出;过了这关再用 ioctlsocket 把套接字设成非阻塞,否则 EA 主线程会被 recv 卡死。 接收缓冲写死 512 字节,单次最多拿 512 字节,攒进 msg 字符串后由 CheckMessage 解析。外汇与贵金属杠杆高,这类常驻 socket 的 EA 若服务器掉线重连不及时,可能漏掉关键价位提醒,实盘前务必在模拟盘跑满 24 小时看掉线恢复表现。
if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) { Alert("客户端仅作用于净值账户"); class="kw">return INIT_FAILED; } EventSetTimer(class="num">1); class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//------------------------------------------------------------------ OnInit class="type">void OnDeinit(class="kw">const class="type">int reason) { EventKillTimer(); CloseClean(); } class=class="str">"cmt">//------------------------------------------------------------------ OnInit class="type">void OnTimer() { if(client==INVALID_SOCKET) StartClient(Host,Port); else { class="type">uchar data[]; if(Receive(data)>class="num">0) class=class="str">"cmt">// 接收数据 { msg+=CharArrayToString(data); class=class="str">"cmt">// 如果收到了一些信息,把它加到总字符串中 printf("从服务器收到了消息: %s",msg); } CheckMessage(); } } class=class="str">"cmt">//------------------------------------------------------------------ CloseClean class="type">void StartClient(class="type">class="kw">string addr,class="type">class="kw">ushort port) { class=class="str">"cmt">// 初始化库 class="type">int res=class="num">0; class="type">char wsaData[]; ArrayResize(wsaData, class="kw">sizeof(WSAData)); res=WSAStartup(MAKEWORD(class="num">2,class="num">2), wsaData); if (res!=class="num">0) { Print("-WSAStartup 失败,错误: "+class="type">class="kw">string(res)); class="kw">return; } class=class="str">"cmt">// 创建一个套接字 client=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(client==INVALID_SOCKET) { Print("-创建套接字失败,错误: "+WSAErrorDescript(WSAGetLastError())); CloseClean(); class="kw">return; } class=class="str">"cmt">// 连接到服务器 class="type">char ch[]; StringToCharArray(addr,ch); sockaddr_in addrin; addrin.sin_family=AF_INET; addrin.sin_addr=inet_addr(ch); addrin.sin_port=htons(port); ref_sockaddr ref=(ref_sockaddr)addrin; res=connect(client,ref.ref,class="kw">sizeof(addrin)); if(res==SOCKET_ERROR) { class="type">int err=WSAGetLastError(); if(err!=WSAEISCONN) { Print("-连接失败,错误: "+WSAErrorDescript(err)); CloseClean(); class="kw">return; } } class=class="str">"cmt">// 设置为非阻塞模式 class="type">int non_block=class="num">1; res=ioctlsocket(client,(class="type">int)FIONBIO,non_block); if(res!=NO_ERROR) { Print("ioctlsocket 失败,错误: "+class="type">class="kw">string(res)); CloseClean(); class="kw">return; } Print("连接成功"); } class=class="str">"cmt">//------------------------------------------------------------------ Receive class="type">int Receive(class="type">uchar &rdata[]) class=class="str">"cmt">// 接收直到对方关闭连接 { if(client==INVALID_SOCKET) class="kw">return class="num">0; class=class="str">"cmt">// 如果套接字没有打开 class="type">char rbuf[class="num">512]; class="type">int rlen=class="num">512; class="type">int r=class="num">0,res=class="num">0; do { res=recv(client,rbuf,rlen,class="num">0); if(res<class="num">0) { class="type">int err=WSAGetLastError();
把服务器仓位落地到本地账户
信号源通过 socket 推送的只是品种与手数字符串,真正下单动作发生在 SyncSymbolLot 里。它先取本地 GetSymbolLot,若与服务器手数一致就直接 return true,避免无谓的成交磨损。 当 nlot 为 0 且本地还有 clot 时,函数调用 trade.PositionClose(smb) 平掉该品种全部仓位;否则用 NormalizeDouble(nlot-clot,2) 算差值,dif 为正就 trade.Buy 补多,为负则 trade.Sell(-dif) 补空。这一层没有市价判断,纯粹做数量对齐。 CloseClean 负责释放连接:shutdown(client,SD_BOTH) 失败后打印 WSAErrorDescript(WSAGetLastError()),随后 closesocket 并把 client 置为 INVALID_SOCKET,最后 WSACleanup。断开不干净会让 MT5 终端残留句柄,下次重连可能报 10038。 CheckMessage 用 '|' 切分服务器消息,要求恰好两段(品种+手数),否则 printf 报错并返回;切出后 NormalizeDouble(StringToDouble(res[1]),2) 保留两位小数再交给同步函数。若 SyncSymbolLot 返回 false,原仓位信息会被包进 msg 回传,便于服务端知道哪一笔没对齐。
if(err!=WSAEWOULDBLOCK) { Print("-接收失败,错误: "+class="type">class="kw">string(err)+" "+WSAErrorDescript(err)); CloseClean(); class="kw">return -class="num">1; } class="kw">break; } if(res==class="num">0 && r==class="num">0) { Print("-接收. 连接关闭"); CloseClean(); class="kw">return -class="num">1; } r+=res; ArrayCopy(rdata,rbuf,ArraySize(rdata),class="num">0,res); } class="kw">while(res>class="num">0 && res>=rlen); class="kw">return r; } class=class="str">"cmt">//------------------------------------------------------------------ CloseClean class="type">void CloseClean() class=class="str">"cmt">// 关闭套接字 { if(client!=INVALID_SOCKET) { if(shutdown(client,SD_BOTH)==SOCKET_ERROR) Print("-关闭失败,错误: "+WSAErrorDescript(WSAGetLastError())); closesocket(client); client=INVALID_SOCKET; } WSACleanup(); Print("关闭套接字"); } class=class="str">"cmt">//------------------------------------------------------------------ CheckMessage class="type">void CheckMessage() { class="type">class="kw">string pos; class="kw">while(FindNextPos(pos)) { printf("server position: %s",pos); }; class=class="str">"cmt">// 从服务器取得最近变化 if(StringLen(pos)<=class="num">0) class="kw">return; class=class="str">"cmt">// 从消息中接收数据 class="type">class="kw">string res[]; if(StringSplit(pos,&class="macro">#x27;|&class="macro">#x27;,res)!=class="num">2) { printf("-错误的仓位信息: %s",pos); class="kw">return; } class="type">class="kw">string smb=res[class="num">0]; class="type">class="kw">double lot=NormalizeDouble(StringToDouble(res[class="num">1]),class="num">2); class=class="str">"cmt">// 同步交易量 if(!SyncSymbolLot(smb,lot)) msg="<<"+pos+">>"+msg; class=class="str">"cmt">// 如果有错,返回消息 } class=class="str">"cmt">//------------------------------------------------------------------ SyncSymbolLot class="type">bool SyncSymbolLot(class="type">class="kw">string smb,class="type">class="kw">double nlot) { class=class="str">"cmt">// 同步服务器和客户端的交易量 CTrade trade; class="type">class="kw">double clot=GetSymbolLot(smb); class=class="str">"cmt">// 取得交易品种的当前手数 if(clot==nlot) { Print("nothing change"); class="kw">return true; } class=class="str">"cmt">// 如果手数相同,就什么都不做 class=class="str">"cmt">// 首先检查特殊情况,服务器上没有仓位 if(nlot==class="num">0 && clot!=class="num">0) { Print("完全关闭仓位"); class="kw">return trade.PositionClose(smb); } class=class="str">"cmt">// 如果服务器上有仓位,在客户端上修改 class="type">class="kw">double dif=NormalizeDouble(nlot-clot,class="num">2); class=class="str">"cmt">// 根据差别进行买入或者卖出 if(dif>class="num">0) { Print("增加买入仓位"); class="kw">return trade.Buy(dif,smb); } else { Print("增加卖出仓位"); class="kw">return trade.Sell(-dif,smb); } } class=class="str">"cmt">//------------------------------------------------------------------ FindNextPos class="type">bool FindNextPos(class="type">class="kw">string &pos) {
「从消息里抠块、按品种算净仓」
做信号中转时,常用 << 和 >> 包裹一段指令块。上面那段逻辑先找开头标记,StringFind 返回 -1 就直接退出,说明这条消息里没有可用块;再找结尾标记,缺了也同样放弃。两个索引都拿到后,用 StringSubstr 切出中间内容赋给 pos,并把原消息从结尾后截掉,实现一边解析一边消费。 净仓统计则是另一回事。GetSymbolLot 遍历 PositionsTotal 返回的当前持仓数,用 PositionSelectByTicket 按单号切到具体仓。过滤条件很直接:PositionGetString(POSITION_SYMBOL) 不等于传入品种就 continue,只留目标品种。 方向处理上,空单的 POSITION_VOLUME 会被翻成负值再加进 slot,多单为正,最后 NormalizeDouble 到两位小数返回。这样你传一个 "XAUUSD" 进去,拿到的就是该品种净暴露手数,正为多净多、负为多净空。外汇与贵金属杠杆高,净仓读数仅反映当下,重算前价格一动就失真。 把这两段直接塞进 EA 的通用模块,开 MT5 接一条带 <<>> 的测试字符串和几个手动开的 XAUUSD 单,就能验证切块是否干净、净仓符号是否符合预期。
class="type">int b=StringFind(msg, "<<"); if(b<class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">// 没有消息开头 class="type">int e=StringFind(msg, ">>"); if(e<class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">// 没有消息结尾 pos=StringSubstr(msg,b+class="num">2,e-b-class="num">2); class=class="str">"cmt">// 取得信息块 msg=StringSubstr(msg,e+class="num">2); class=class="str">"cmt">// 从消息中删除 class="kw">return true; } class=class="str">"cmt">//------------------------------------------------------------------ GetSymbolLot class="type">class="kw">double GetSymbolLot(class="type">class="kw">string smb) { class="type">class="kw">double slot=class="num">0; class="type">int n=PositionsTotal(); for(class="type">int i=class="num">0; i<n;++i) { PositionSelectByTicket(PositionGetTicket(i)); if(PositionGetString(POSITION_SYMBOL)!=smb) class="kw">continue; class=class="str">"cmt">// 当服务器正在运行时,过滤掉当前交易品种的仓位 class="type">class="kw">double lot=PositionGetDouble(POSITION_VOLUME); class=class="str">"cmt">// 取得交易量 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) lot=-lot; class=class="str">"cmt">// 考虑方向 slot+=lot; class=class="str">"cmt">// 加到总和中 } class="kw">return NormalizeDouble(slot,class="num">2); }
◍ 用 UDP 套接字收订单流
想在不占用客户端连接资源的前提下,把多端交易品种的订单事件汇到一台机器上,UDP 套接字比 TCP 更轻。服务端不保存客户端状态,只管收;客户端在订单触发事件里把数据丢出来即可。 服务端用毫秒级计时器去轮询套接字,比默认的定时器更跟手。下面这段把轮询间隔设成 300 毫秒,意味着订单信息从发到收的延迟大概率落在 0.3 秒以内,对盘口不极端的外汇、贵金属品种足够用,但重波动时段仍可能丢包,外汇贵金属本身高风险,别拿它当成交兜底。 客户端逻辑更简单,全部处理压在订单来临的事件里,不另开线程。服务器代码里 WSAStartup 用 MAKEWORD(2,2) 初始化,绑定 0.0.0.0:8082 后切非阻塞模式,recvfrom 拿到 buf 就 Print 出来;若返回 WSAEWOULDBLOCK 说明暂无可读数据,继续等下一轮计时器即可。
class="kw">input class="type">class="kw">string Host="class="num">0.0.class="num">0.0"; class="kw">input class="type">class="kw">ushort Port=class="num">8082; SOCKET server=INVALID_SOCKET; class=class="str">"cmt">//------------------------------------------------------------------ OnInit class="type">int OnInit() { EventSetMillisecondTimer(class="num">300); class="kw">return class="num">0; } class=class="str">"cmt">//------------------------------------------------------------------ OnDeinit class="type">void OnDeinit(class="kw">const class="type">int reason) { EventKillTimer(); CloseClean(); } class=class="str">"cmt">//------------------------------------------------------------------ OnTimer class="type">void OnTimer() { if(server!=INVALID_SOCKET) { class="type">char buf[class="num">1024]={class="num">0}; ref_sockaddr ref={class="num">0}; class="type">int len=ArraySize(ref.ref); class="type">int res=recvfrom(server,buf,class="num">1024,class="num">0,ref.ref,len); if (res>=class="num">0) class=class="str">"cmt">// 接收并显示数据 Print("接收到来自客户端的订单: ", CharArrayToString(buf)); else { class="type">int err=WSAGetLastError(); if(err!=WSAEWOULDBLOCK) { Print("-接收失败,错误: "+WSAErrorDescript(err)+". 清除套接字"); CloseClean(); class="kw">return; } } } else class=class="str">"cmt">// 否则,启动服务器 { class=class="str">"cmt">// 初始化库 class="type">char wsaData[]; ArrayResize(wsaData,class="kw">sizeof(WSAData)); class="type">int res=WSAStartup(MAKEWORD(class="num">2,class="num">2), wsaData); if(res!=class="num">0) { Print("-WSAStartup 失败,错误: "+class="type">class="kw">string(res)); class="kw">return; } class=class="str">"cmt">// 创建一个套接字 server=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); if(server==INVALID_SOCKET) { Print("-创建失败,错误: "+WSAErrorDescript(WSAGetLastError())); CloseClean(); class="kw">return; } class=class="str">"cmt">// 绑定到地址和端口 Print("尝试绑定..."+Host+":"+class="type">class="kw">string(Port)); class="type">char ch[]; StringToCharArray(Host,ch); sockaddr_in addrin; addrin.sin_family=AF_INET; addrin.sin_addr=inet_addr(ch); addrin.sin_port=htons(Port); ref_sockaddr ref=(ref_sockaddr)addrin; if(bind(server,ref.ref,class="kw">sizeof(addrin))==SOCKET_ERROR) { class="type">int err=WSAGetLastError(); if(err!=WSAEISCONN) { Print("-连接失败,错误: "+WSAErrorDescript(err)+". 清除套接字"); CloseClean(); class="kw">return; } } class=class="str">"cmt">// 设置到非阻塞模式 class="type">int non_block=class="num">1; res=ioctlsocket(server,(class="type">int)FIONBIO,non_block); if(res!=NO_ERROR) { Print("ioctlsocket 失败,错误: "+class="type">class="kw">string(res)); CloseClean(); class="kw">return; }
用 UDP 把报价甩给本地服务
这段逻辑演示了 EA 在 MT5 里如何用 UDP 套接字,把当前品种的 Bid 价实时推到本机 127.0.0.1:8082。外汇和贵金属波动快,这种本地回环通信延迟极低,但涉及实时传输仍属高风险操作,参数填错可能丢包。 OnInit 里先把 Host 和 Port 填进 sockaddr_in 结构,Port 默认 8082 是 input 可控的。紧接着直接调 OnTick(),等于初始化阶段就先把客户端套接字建出来,不用等第一笔 tick。 OnTick 分两支:已有套接字就拼 "SYMBOL Bid" 字符串用 sendto 发出去;没有就走 WSAStartup → socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP) → ioctlsocket 设非阻塞。非阻塞模式让 sendto 返回 SOCKET_ERROR 时靠 WSAEWOULDBLOCK 判断,不是真失败就不清连接。 想验证,把 Host 留 127.0.0.1、Port 设 8082,本机起个 UDP 监听,挂 EA 后终端日志会打“成功创建套接字”和“发送 XXX 订单到服务器”,说明通道通了。
class="kw">input class="type">class="kw">string Host="class="num">127.0.class="num">0.1"; class="kw">input class="type">class="kw">ushort Port=class="num">8082; SOCKET client=INVALID_SOCKET; ref_sockaddr srvaddr={class="num">0}; class="type">int OnInit() { class="type">char ch[]; StringToCharArray(Host,ch); sockaddr_in addrin; addrin.sin_family=AF_INET; addrin.sin_addr=inet_addr(ch); addrin.sin_port=htons(Port); srvaddr=(ref_sockaddr)addrin; OnTick(); class="kw">return INIT_SUCCEEDED; } class="type">void OnDeinit(class="kw">const class="type">int reason) { CloseClean(); } class="type">void OnTick() { if(client!=INVALID_SOCKET) { class="type">uchar data[]; StringToCharArray(Symbol()+" "+DoubleToString(SymbolInfoDouble(Symbol(),SYMBOL_BID),Digits()),data); if(sendto(client,data,ArraySize(data),class="num">0,srvaddr.ref,ArraySize(srvaddr.ref))==SOCKET_ERROR) { class="type">int err=WSAGetLastError(); if(err!=WSAEWOULDBLOCK) { Print("-Send failed error: "+WSAErrorDescript(err)); CloseClean(); } } else Print("发送 "+Symbol()+" 订单到服务器"); } else { class="type">int res=class="num">0; class="type">char wsaData[]; ArrayResize(wsaData,class="kw">sizeof(WSAData)); res=WSAStartup(MAKEWORD(class="num">2,class="num">2),wsaData); if(res!=class="num">0) { Print("-WSAStartup 失败,错误: "+class="type">class="kw">string(res)); class="kw">return; } client=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); if(client==INVALID_SOCKET) { Print("-创建套接字失败,错误: "+WSAErrorDescript(WSAGetLastError())); CloseClean(); class="kw">return; } class="type">int non_block=class="num">1; res=ioctlsocket(client,(class="type">int)FIONBIO,non_block); if(res!=NO_ERROR) { Print("ioctlsocket 失败,错误: "+class="type">class="kw">string(res)); CloseClean(); class="kw">return; } Print("成功创建套接字"); } } class="type">void CloseClean()
「断开连接时的资源回收」
在 MQL5 里做 TCP 通信,最容易被忽略的不是建连,而是正常或异常退出时把系统资源还回去。上面这段收尾逻辑先判断 client 句柄是不是有效,有效才调用 closesocket 释放,随后不管连没连成都跑一次 WSACleanup 清理 Winsock 环境。 漏掉 WSACleanup 在反复重连的策略里会慢慢吃满句柄,MT5 终端可能越跑越卡甚至拒绝新建 socket。实盘跑之前,建议在策略测试器里手动触发几次断开,看 Experts 日志是否稳定打印出「关闭套接字」这一行。 外汇与贵金属品种本身波动剧烈、杠杆风险高,任何涉及网络异常的 EA 都应在断线后暂停下单而非盲目重连。
{
if(client!=INVALID_SOCKET) { closesocket(client); client=INVALID_SOCKET; }
WSACleanup();
Print("关闭套接字");
}◍ 给 MQL 服务器加认证与多线程缓冲
默认情况下,MQL 写的服务器会把消息广播给任意连上的客户端,这在实盘环境里等于裸奔。至少该加两道强制门槛:客户端用登录名加密码做认证,再对连续猜密码的行为做封锁——要么锁登录名,要么直接封来源 IP。 更隐蔽的瓶颈在单线程。原文示例里所有服务器逻辑都跑在一个 EA 的 OnTimer 里,连接数或下发数据量一大,计时器就会被拖死。折中办法是起一个 EA 池,让每个 EA 各自带计时器去分摊客户端互操作,相当于在 MQL 层硬造出多线程效果。 用不用纯 MQL 实现这套,取决于你手里的连接规模。MQL 的硬优势是直接读账户成交与报价,且无需依赖第三方 DLL——外汇和贵金属杠杆高、滑点猛,少一层外部依赖就少一处崩点。
套接字还能这么玩
MT5 里的 Socket 不只是拉行情。把客户端仓位量回传服务器,再拿到全体客户端总交易量,就能拼出一个去中心化的市场情绪指标;服务器也能只当二传手,在多个终端之间转发数据,或者把重计算甩给测试器代理网络跑分布式任务。 这类用法在官方示例里大多没写死,留给使用者自己接。实际踩坑的人不少:有用户在 2022–2024 的留言里提到,MT5 套接字若不在每个 tick 关闭再重连,发送就会阻塞或冻结;远程通信也常报 display 连接超时。 外汇和贵金属杠杆高、滑点狠,任何基于套接字的旁路数据通道都只是辅助信号,不代表方向确定性。真要落地,先在 MT5 用 demo 账户把连接—发送—关闭的节奏跑通,再谈实盘。