在MQL中操作套接字,或者如何成为信号提供者(基础篇)
「用 MQL5 套接字把自己变成信号源」
MT5 客户端原生支持通过 Socket 接口对外通信,这意味着你写的 EA 不只能下单,还能把账户状态、信号标记实时推到外部服务,变相成为信号提供者。
- 年 9 月 9 日官方示例帖发布后,该主题在站内累计获得约 7045 次浏览与 94 条讨论,说明用 MQL 直接玩套接字的需求一直有真实交易者埋单。
想在 MT5 里验证这条路,先确认终端设置中「允许 WebRequest」与「允许 DLL 导入」已开启,否则 Socket 函数会直接返回无效句柄。外汇与贵金属杠杆高、滑点无常,实盘前务必在模拟环境把断线重连逻辑跑满。
套接字是 MT5 联网的底层命脉
早上你点开 MetaTrader 5,终端第一时间就建了套接字连上交易服务器;你开浏览器、收邮件、校时,背后也都是套接字在反复建连和断连。从 1982 年 BSD 套接字问世到现在,它一直是网络的神经末梢,MT5 的实时报价和分布式计算全靠这套机制。 外汇和贵金属交易靠网络连接行情与订单,套接字异常会直接断流,这类依赖外部网络的环境本身高风险,参数和超时设置要留余量。 想动手验证很简单:开 MT5 按 F4 进 MetaEditor,新建 EA 后在 OnInit 里写 SocketCreate 调一次,编译挂到图表,看日志是否返回句柄。本文后续会把维基百科和 MSDN 里的标准结构逐一翻成 MQL5 可调用的代码。
◍ 把 WinAPI 套接字函数搬进 MQL5
MQL5 在内存模型上和 C 系 WinAPI 是近亲,所以把 TCP/UDP 所需的那几个 Winsock 函数原样移植成 .mqh 并不绕。TCP 客户端只要走完 WSAStartup 初始化、socket 建套接字、ioctlsocket 设非阻塞、connect 连服务器、recv/send 收发、closesocket 加 WSACleanup 收尾这六步;服务器端多一个 bind 和 listen,再用 accept 接连接。UDP 更短,没有握手环节,客户端用 sendto/recvfrom 直接跑,服务端仅多 bind 一个端口。
移植的核心技巧是用字节数组冒充结构指针。例如 WSAStartup 在 MQL 里声明成 int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData[]),其中 LPWSADATA 被定义为 char&,实际传进去的是一块大小等于 WSAData 结构的字节数组。调用前 ArrayResize(wsaData, sizeof(WSAData)) 把数组撑到结构体积,函数写回的数据再靠类型转换抠出来。
MAKEWORD(2,2) 在代码里直接展开成 ((WORD)(((BYTE)(2)&0xff))|((WORD)((BYTE)(2)&0xff))<<8),对应请求 Winsock 2.2 版——这是 MT5 里跑网络通讯的最低可用基线。socket 函数常数也提前钉死:AF_INET=2、SOCK_STREAM=1、IPPROTO_TCP=6,少查一次 MSDN 就少一处笔误。
别把结构当玄学
任何结构本质就是定长内存块,MQL 里用同尺寸 char 数组承接,WinAPI 侧照常读写。connect 要的 sockaddr 也是同样套路:从 MSDN 抄结构,指针全换成数组地址,sin_addr 的 union 在 MQL 里落为 8 字节整数表达。
下面这段是移植完可直接存成 WinAPI_Sockets.mqh 的骨架,开 MT5 建个脚本 include 它就能验证编译通过:
WINAPI: class="type">int WSAAPI WSAStartup(_In_ WORD wVersionRequested, _Out_ LPWSADATA lpWSAData); class="macro">#define MAKEWORD(a, b) ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << class="num">8)) class="macro">#define BYTE class="type">uchar class="macro">#define WORD class="type">class="kw">ushort class="macro">#define DWORD class="type">int class="macro">#define DWORD_PTR class="type">class="kw">ulong class="kw">struct WSAData { WORD wVersion; WORD wHighVersion; class="type">char szDescription[WSADESCRIPTION_LEN+class="num">1]; class="type">char szSystemStatus[WSASYS_STATUS_LEN+class="num">1]; class="type">class="kw">ushort iMaxSockets; class="type">class="kw">ushort iMaxUdpDg; class="type">char lpVendorInfo[]; } class="macro">#define LPWSADATA class="type">char& MQL: class="type">int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData[]); class="type">char wsaData[]; class=class="str">"cmt">// 未来结构的字节数组 ArrayResize(wsaData, class="kw">sizeof(WSAData)); class=class="str">"cmt">// 把它的大小设置为结构的大小 WSAStartup(MAKEWORD(class="num">2,class="num">2), wsaData); class=class="str">"cmt">// 调用函数 WINAPI: SOCKET WSAAPI socket(_In_ class="type">int af, _In_ class="type">int type, _In_ class="type">int protocol); class="macro">#define SOCKET class="type">uint class="macro">#define INVALID_SOCKET(SOCKET)(~class="num">0) class="macro">#define SOCKET_ERROR(-class="num">1) class="macro">#define NO_ERROR class="num">0 class="macro">#define AF_INET class="num">2 class=class="str">"cmt">// 互联网家族: UDP, TCP, 等等. class="macro">#define SOCK_STREAM class="num">1 class="macro">#define IPPROTO_TCP class="num">6 MQL: class="type">int ioctlsocket(SOCKET s, class="type">int cmd, class="type">int &argp); WINAPI:
「MT5 套接字函数与底层宏定义对照」
在 MT5 里做行情外联或自建数据中转,绕不开 WinAPI 的 socket 接口封装。MQL5 把 connect、send、recv、sendto、recvfrom、closesocket、WSACleanup 都重写成可直接调用的形式,参数里用 LPSOCKADDR(即 char& 别名)替代原生指针结构,降低脚本层踩内存的概率。 原生 sockaddr 只占 16 字节:sa_family 用 ushort 标记地址家族,sa_data 是 14 字节原始地址。实际跑 TCP 时多用 sockaddr_in,其中 sin_zero 固定留 8 字节补齐,sin_port 为 ushort,sin_addr 内 s_addr 是 ulong 型 IPv4 地址——这套布局跟 POSIX 标准一致,开 MT5 用 sizeof(sockaddr_in) 验证会得到 16 字节。 下面的宏是硬骨头:AF_INET 值是 2,SOCK_STREAM 是 1,IPPROTO_TCP 是 6,SOMAXCONN 限制 128 个挂起连接。INVALID_SOCKET 被定义成 uint 的全 1(~0),SOCKET_ERROR 为 -1,写重连逻辑时要用这两个常量判断返回值,别自己写 -1 魔法数。 MAKEWORD(a,b) 把两个字节拼成 ushort,低位 a 高位 b 左移 8 位,WSAStartup 传版本号就靠它。WSADESCRIPTION_LEN 256 与 WSASYS_STATUS_LEN 128 决定你缓冲区数组该开多大,开小了 recv 回来会被截断。
class="type">int connect(_In_ SOCKET s, _In_ class="kw">const class="kw">struct sockaddr *name, _In_ class="type">int namelen); class="kw">struct sockaddr { class="type">class="kw">ushort sa_family; class=class="str">"cmt">// 地址家族. class="type">char sa_data[class="num">14]; class=class="str">"cmt">// 一共14个字节的直接地址. }; class="macro">#define LPSOCKADDR class="type">char& class="kw">struct sockaddr_in { class="type">short sin_family; class="type">class="kw">ushort sin_port; class="kw">struct in_addr sin_addr; class="type">char sin_zero[class="num">8]; }; class="kw">struct in_addr { class="type">class="kw">ulong s_addr; }; MQL: class="type">int connect(SOCKET s, LPSOCKADDR name[], class="type">int namelen); WINAPI: class="type">int send(_In_ SOCKET s, _In_ class="kw">const class="type">char* buf, _In_ class="type">int len, _In_ class="type">int flags); class="type">int recv(_In_ SOCKET s, _Out_ class="type">char* buf, _In_ class="type">int len, _In_ class="type">int flags); MQL: class="type">int send(SOCKET s, class="type">char& buf[], class="type">int len, class="type">int flags); class="type">int recv(SOCKET s, class="type">char& buf[], class="type">int len, class="type">int flags); WINAPI: class="type">int recvfrom(_In_ SOCKET s, _Out_ class="type">char* buf, _In_ class="type">int len, _In_ class="type">int flags, _Out_ class="kw">struct sockaddr *from, _Inout_opt_ class="type">int *fromlen); class="type">int sendto(_In_ SOCKET s, _In_ class="kw">const class="type">char* buf, _In_ class="type">int len, _In_ class="type">int flags, _In_ class="kw">const class="kw">struct sockaddr *to, _In_ class="type">int tolen); MQL: class="type">int recvfrom(SOCKET s,class="type">char &buf[],class="type">int len,class="type">int flags,LPSOCKADDR from[],class="type">int &fromlen); class="type">int sendto(SOCKET s,class="kw">const class="type">char &buf[],class="type">int len,class="type">int flags,LPSOCKADDR to[],class="type">int tolen); MQL: class="type">int closesocket(SOCKET s); class="type">int WSACleanup(); class="macro">#define BYTE class="type">uchar class="macro">#define WORD class="type">class="kw">ushort class="macro">#define DWORD class="type">int class="macro">#define DWORD_PTR class="type">class="kw">ulong class="macro">#define SOCKET class="type">uint class="macro">#define MAKEWORD(a, b) ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << class="num">8)) class="macro">#define WSADESCRIPTION_LEN class="num">256 class="macro">#define WSASYS_STATUS_LEN class="num">128 class="macro">#define INVALID_SOCKET(SOCKET)(~class="num">0) class="macro">#define SOCKET_ERROR(-class="num">1) class="macro">#define NO_ERROR class="num">0 class="macro">#define SOMAXCONN class="num">128 class="macro">#define AF_INET class="num">2 class=class="str">"cmt">// 互联网家族: UDP, TCP, 等等. class="macro">#define SOCK_STREAM class="num">1 class="macro">#define IPPROTO_TCP class="num">6 class="macro">#define SD_RECEIVE 0x00
在 MT5 里直接撸原生 socket 结构
想在 MQL5 里做低延迟行情中转或私有信号通道,绕不开 Windows 的 Ws2_32.dll。上面这组宏和结构体就是把 Berkeley socket 的底层契约搬进 MT5 编译环境,不依赖任何第三方库。 #define 里 IOCPARM_MASK 是 0x7f,意味着 ioctl 类的参数长度必须压在 128 字节以内;_IOW('f',126,int) 生成的 FIONBIO 命令码用来切换阻塞与非阻塞模式,做高频轮询时这行直接决定线程会不会卡死在 recv 上。 WSAData 和 sockaddr_in 两个 struct 必须按字段宽度对齐:sockaddr_in 里 sin_zero 固定 8 字节填充,sa_data 在通用 sockaddr 里是 14 字节,ref_sockaddr 用 2+14 的 char 数组做等价映射,错一个字节 socket 函数就会返回 10014(WSAEFAULT)。 import 段列出了 WSAStartup 到 listen 的 15 个原生函数声明。开 MT5 新建一个 EA,把这段原样贴进 include 头文件,编译后调一次 WSAStartup(MAKEWORD(2,2), wsa) 即可验证环境是否通——外汇与贵金属杠杆交易本身高风险,这类底层网络代码若写错还会引发终端崩溃,务必先在模拟账户跑。
class="macro">#define SD_SEND 0x01 class="macro">#define SD_BOTH 0x02 class="macro">#define IOCPARM_MASK 0x7f class=class="str">"cmt">/* 参数必须 < class="num">128 个字节 */ class="macro">#define IOC_IN 0x80000000 class=class="str">"cmt">/* 复制输入参数 */ class="macro">#define _IOW(x,y,t) (IOC_IN|(((class="type">int)class="kw">sizeof(t)&IOCPARM_MASK)<<class="num">16)|((x)<<class="num">8)|(y)) class="macro">#define FIONBIO _IOW(&class="macro">#x27;f&class="macro">#x27;, class="num">126, class="type">int) class=class="str">"cmt">/* 设置/清除 非阻塞 i/o */ class=class="str">"cmt">//------------------------------------------------------------------ class="kw">struct WSAData class="kw">struct WSAData { WORD wVersion; WORD wHighVersion; class="type">char szDescription[WSADESCRIPTION_LEN+class="num">1]; class="type">char szSystemStatus[WSASYS_STATUS_LEN+class="num">1]; class="type">class="kw">ushort iMaxSockets; class="type">class="kw">ushort iMaxUdpDg; class="type">char lpVendorInfo[]; }; class="macro">#define LPWSADATA class="type">char& class=class="str">"cmt">//------------------------------------------------------------------ class="kw">struct sockaddr_in class="kw">struct sockaddr_in { class="type">class="kw">ushort sin_family; class="type">class="kw">ushort sin_port; class="type">class="kw">ulong sin_addr; class=class="str">"cmt">//class="kw">struct in_addr { class="type">class="kw">ulong s_addr; }; class="type">char sin_zero[class="num">8]; }; class=class="str">"cmt">//------------------------------------------------------------------ class="kw">struct sockaddr class="kw">struct sockaddr { class="type">class="kw">ushort sa_family; class=class="str">"cmt">// 地址家族. class="type">char sa_data[class="num">14]; class=class="str">"cmt">// 14个字节大小的直接地址. }; class="macro">#define LPSOCKADDR class="type">char& class="kw">struct ref_sockaddr { class="type">char ref[class="num">2+class="num">14]; }; class=class="str">"cmt">//------------------------------------------------------------------ class="kw">import Ws2_32.dll class="macro">#class="kw">import "Ws2_32.dll" class="type">int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData[]); class="type">int WSACleanup(); class="type">int WSAGetLastError(); class="type">class="kw">ushort htons(class="type">class="kw">ushort hostshort); class="type">class="kw">ulong inet_addr(class="type">char& cp[]); class="type">class="kw">string inet_ntop(class="type">int Family,class="type">class="kw">ulong &pAddr,class="type">char &pStringBuf[],class="type">uint StringBufSize); class="type">class="kw">ushort ntohs(class="type">class="kw">ushort netshort); SOCKET socket(class="type">int af,class="type">int type,class="type">int protocol); class="type">int ioctlsocket(SOCKET s,class="type">int cmd,class="type">int &argp); class="type">int shutdown(SOCKET s,class="type">int how); class="type">int closesocket(SOCKET s); class=class="str">"cmt">// 服务器端函数 class="type">int bind(SOCKET s,LPSOCKADDR name[],class="type">int namelen); class="type">int listen(SOCKET s,class="type">int backlog);
◍ 把 socket 接口接回 EA 里跑通
上面这组声明把底层 socket 函数重新暴露给 MQL5 的 EA 或脚本,省去自己写 DLL 的麻烦。accept 用于服务端套接字等待客户端连入,addr 与 addrlen 以引用方式回传对端地址,connect 则由客户端发起三次握手,name 与 namelen 描述目标地址结构。 send 和 recv 是实际收发数据的两个调用,buf 用 char 数组引用传递,len 指明本次字节数,flags 一般填 0。在 MT5 里直接 #import 进这些符号后,你可以在 OnTimer 里轮询 recv,把行情外的外部信号接进策略。 外汇与贵金属杠杆高、滑点突变频繁,用 socket 引外部数据时要先在小周期回测确认延迟不击穿止损逻辑。开 MT5 新建脚本,把这几行贴进 #import 段,编译无报错即说明本机 socket 库可用。
SOCKET accept(SOCKET s,LPSOCKADDR addr[],class="type">int &addrlen); class=class="str">"cmt">// 客户端函数 class="type">int connect(SOCKET s,LPSOCKADDR name[],class="type">int namelen); class="type">int send(SOCKET s,class="type">char &buf[],class="type">int len,class="type">int flags); class="type">int recv(SOCKET s,class="type">char &buf[],class="type">int len,class="type">int flags); class="macro">#class="kw">import