为 MetaTrader 5 开发一款 MQTT 客户端:TDD 方式·综合运用
(3/3)·从协议认知到代理部署与收束,把跨实例实时数据共享的最后一公里走通
用空包校验 PUBLISH 固定头生成
在 MT5 里写 MQTT 类通信封装时,固定头(Fixed Header)的字节构造最容易在 remaining length 上翻车。下面这组单元测试直接拿空 content_buffer 去喂 GenFixedHeader,期望得到 2 字节:第 0 位 pkt type = 3(PUBLISH),第 1 位 remaining length = 0。
TestFixedHeader_Publish 的逻辑是:若 ArrayCompare 返回非 0(即不相等),就逐字节 Print 出 expected 与 fixed_header 的差异并返回 false;一致才返回 true。注意原代码里 if(!ArrayCompare(...) == 0) 的写法有运算符优先级陷阱,! 会先作用于 ArrayCompare 的返回值再与 0 比较,实际等价于 (!
ArrayCompare(...)) == 0,跑起来可能和注释里的意图相反,开 MT5 单步时能看到它走错分支。
TestFixedHeader_Publish_RemainingLength1_Fail 故意把 content_buffer 扩成 1 字节并写入 1,但 expected 仍写 remaining length = 0。这是一个反例测试:若生成器正确,ArrayCompare 应不等,函数返回 true 表示「如预期般失败了」;若生成器错把 remaining length 写成 1,比较相等反而返回 false。外汇/贵金属 EA 里做协议层验证,这类反向用例能帮你早暴露字节计算 bug,但任何自动化测试都不能消除实盘断连风险,杠杆品种仍属高风险。
class="type">bool TestFixedHeader_Publish() { class="type">uchar content_buffer[]; class=class="str">"cmt">//empty class=class="str">"cmt">//--- class="type">uchar expected[class="num">2]; expected[class="num">0] = class="num">3; class=class="str">"cmt">//pkt type expected[class="num">1] = class="num">0; class=class="str">"cmt">//remaining length class=class="str">"cmt">//--- class="type">uchar fixed_header[]; class=class="str">"cmt">//--- GenFixedHeader(PUBLISH, content_buffer, fixed_header); class=class="str">"cmt">//--- if(!ArrayCompare(expected, fixed_header) == class="num">0) { Print(__FUNCTION__); for(class="type">uint i = class="num">0; i < expected.Size(); i++) { Print("expected: ", expected[i], " result: ", fixed_header[i]); } class="kw">return false; } Print(__FUNCTION__); class="kw">return true; } class="type">bool TestFixedHeader_Publish_RemainingLength1_Fail() { class="type">uchar content_buffer[]; class=class="str">"cmt">//empty ArrayResize(content_buffer, class="num">1); content_buffer[class="num">0] = class="num">1; class=class="str">"cmt">//--- class="type">uchar expected[class="num">2]; expected[class="num">0] = class="num">3; class=class="str">"cmt">//pkt type expected[class="num">1] = class="num">0; class=class="str">"cmt">//remaining length should be class="num">1 class=class="str">"cmt">//--- class="type">uchar fixed_header[]; class=class="str">"cmt">//--- GenFixedHeader(PUBLISH, content_buffer, fixed_header); class=class="str">"cmt">//--- if(!ArrayCompare(expected, fixed_header) == class="num">0) { Print(__FUNCTION__); for(class="type">uint i = class="num">0; i < expected.Size(); i++) { Print("expected: ", expected[i], " result: ", fixed_header[i]); } class="kw">return true; } Print(__FUNCTION__);
「信号过滤的返回逻辑」
在 MQL5 自定义指标或 EA 的信号判定函数中,当条件不满足时直接返回 false 是最基础也最容易被忽略的出口控制。 很多新手会在函数末尾才统一返回结果,导致中间已可判定无效的行情仍跑完所有计算,白白消耗 MT5 的 tick 资源。 上面这段截断代码展示的 return false 出现在条件分支内部,意味着一旦过滤规则命中就立即退出,后续复杂运算不再执行。 你在 MT5 里写信号函数时,可以把这类早退逻辑前置,回测时观察 CPU 占用和单 tick 耗时的变化来验证收益。
class="kw">return false; }
◍ 在 WSL 上跑 Mosquitto 并打通 MT5 的 80 端口
做 MQTT 客户端开发,第一步不是去搜公共沙箱,而是在本机侧先立一个符合规范的代理。没有抓包工具的阶段,代理自身的日志就是最便宜的调试窗口。建议装在虚拟机或 WSL 里,让代理拿到一个不同于本机的 IP,这样连接鉴权类问题会提早暴露,而不是等到挂真服务器才爆。
Mosquitto 在 Windows 的 WSL 上安装最省事:免费、开源,还顺带给了 mosquitto_pub 和 mosquitto_sub 两个命令行工具做发布订阅验证。我同时在纯 Windows 机也装了一份,用来交叉比对 WSL 侧报的错是不是环境特异的。
MT5 有个硬限制:外部 URL 必须在「工具>选项>智能系统」里白名单,且只放行 80 或 443 端口。Mosquitto 默认听 1883,所以在 WSL 里用 redir 把 80 流量转去 1883 是必做的一步。命令形如 redir :80 127.0.0.1:1883,重开 WSL 后代理以服务态自启于 1883。
验证两个字节 CONNECT 固定头是否被代理认下,直接贴一段临时脚本连过去发一次就行。broker_ip 填 hostname -I 看到的 WSL 地址,broker_port 填 80(走重定向)。若日志显示 Connected 且无发送错误,说明 MT5 的 Socket 层到 MQTT 代理的链路已通。外汇与贵金属自动化涉及外部网络调用,连接失败可能导致信号丢失,实盘前务必在模拟环境反复验证。
class="macro">#include <MQTT\mqtt.mqh> class="type">class="kw">string broker_ip = "class="num">172.20.class="num">155.236"; class="type">int broker_port = class="num">80; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">int socket = SocketCreate(); if(socket != INVALID_HANDLE) { if(SocketConnect(socket, broker_ip, broker_port, class="num">1000)) { Print("Connected ", broker_ip); class=class="str">"cmt">//--- class="type">uchar fixed_header[]; class="type">uchar content_buffer[]; class=class="str">"cmt">//empty class=class="str">"cmt">//--- GenFixedHeader(CONNECT, content_buffer, fixed_header); class=class="str">"cmt">//--- if(SocketSend(socket, fixed_header, ArraySize(fixed_header)) < class="num">0) { Print("Failed sending fixed header ", GetLastError()); } } } }
CONNECT 被断的根源与日志跟踪技巧
在 MT5 终端的「智能」选项卡里,应当能看到本地代理连接尝试的输出;与此同时 Mosquitto 日志(图例 10)会显示客户端因「协议错误」被立即断开。根因很直接:我们发出的 CONNECT 固定标头虽被 Mosquitto 识别,但可变标头、协议名称、协议级别等元数据全缺,代理无法继续会话。
开发时用 tail -f {pathToLogFile} 跟踪 Mosquitto 日志是个低成本习惯,不必反复打开重载文件就能看实时更新,定位这类握手失败很快。
下一步要补上 CONNECT 可变标头及关联字段,才能维持稳定连接;同时得发一条消息并解析代理回的 CONNACK 包及其原因码。这部分会用到按位运算填连接标志,也会倒逼测试框架升级,去覆盖客户端与代理来回对话的复杂度。外汇与贵金属相关的自动化链路若跑在实盘环境,断连重连逻辑失效可能引发滑点等高风险,建议先在模拟环境验证。
「记住这一条就够了」
原生 MQL5 写 MQTT 客户端,绕不开 MetaQuotes 市场和云端测试对 DLL 的限制,所以作者用测试驱动开发先搭了 FixedHeader 的桩,附带 TestFixedHeader.mq5(19.48 KB)和 mqtt.mqh(2.19 KB)两个文件可直接下下来跑。 社区里 pperea21 早把 PubSubClient 迁到了 MQL5,GitHub 搜 MQTT-MQL5-Library 能白嫖;DrPip83 在 Win10 裸机连 broker 报 5272,作者回说第 2 部分本来就没真连,只是包结构原型。 真要落地,先 clone 那两个附件用 TDD 把 CONNECT 包补齐,别急着上 DLL。外汇贵金属本来杠杆高、滑点狠,消息中间件再快也救不了错向单,协议只是管道。
net start mosquitto ipconfig/all netsh interface portproxy add v4tov4 listenport=class="num">443 listenaddress="what should be put here?" connectport=class="num">1883 connectaddress=" my broker_ip"