为 MetaTrader 5 开发一款 MQTT 客户端:TDD 方式·综合运用
📡

为 MetaTrader 5 开发一款 MQTT 客户端:TDD 方式·综合运用

(3/3)·从协议认知到代理部署与收束,把跨实例实时数据共享的最后一公里走通

实战向 第 3/3 篇
很多交易者以为跨 MT5 实例共享数据只能靠文件或网页服务,结果延迟高、维护烦。直接用裸套接字又容易在断线后丢消息。MQTT 这种轻量发布订阅协议常被忽略,但它恰好适合低带宽的跟单与统计共享场景。

用空包校验 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,但任何自动化测试都不能消除实盘断连风险,杠杆品种仍属高风险。

MQL5 / C++
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 耗时的变化来验证收益。

MQL5 / C++
  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 代理的链路已通。外汇与贵金属自动化涉及外部网络调用,连接失败可能导致信号丢失,实盘前务必在模拟环境反复验证。

MQL5 / C++
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。外汇贵金属本来杠杆高、滑点狠,消息中间件再快也救不了错向单,协议只是管道。

MQL5 / C++
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"
把重复联调交给小布
这些代理连通性与消息回路的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到延迟与掉线提示,你只需专注协议字段设计。

常见问题

协议本身设计为受限环境使用,带宽与内存占用都低;实际开销主要取决于代理部署位置和消息频率,合理设置 QoS 可进一步压低负载。
目前小布内置的是对代理连通状态的观测与提醒,自定义主题解析可在本地 EA 侧完成后再喂给小布做展示,不必改协议层。
本地可用单机版 Mosquitto 快速验证;跨网络则需配置 TLS 与鉴权,并注意云厂商对 1883/8883 端口的放行规则。
优先写连接建立、订阅响应和断线重连的测试,这三类直接决定跟单数据能否不丢不重,再补消息解析边界用例。