为 MetaTrader 5 开发一款 MQTT 客户端:TDD 方式 - 第2部分(基础篇)
📘

为 MetaTrader 5 开发一款 MQTT 客户端:TDD 方式 - 第2部分(基础篇)

第 1/3 篇

◍ 在 MT5 里跑通 MQTT 的第一道关

想在 MT5 上接 MQTT,先得确认客户端能连上 broker 并收发消息,而不是等 EA 跑飞了才查网络。用测试驱动(TDD)把连接逻辑先写成可跑的用例,能在编译期就把大多数低级错误掐掉。 MQL5 没有原生 MQTT 库,常规做法是封装系统 DLL 或走 WebSocket 桥接。下面这段是最小连接骨架,先在 OnStart 里把客户端实例化并打日志,确认上下文创建成功再往下写发布/订阅。 高风险提示:外汇与贵金属杠杆交易可能迅速亏损本金,以下代码仅作技术验证,不构成任何交易建议。 别把打印成功当连通 很多新手看到 Print('context ok') 就以为链路通了,其实 broker 握手还没完成。务必在回调里确认 CONNACK 返回码为 0,否则后续 publish 都是空转。

MQL5 / C++
class="type">int OnInit()
{
   MqttClient client = new MqttClient(&class="macro">#x27;tcp:class=class="str">"cmt">//broker.hivemq.com:class="num">1883&class="macro">#x27;);
   if(client == NULL) { Print(&class="macro">#x27;client init failed&class="macro">#x27;); class="kw">return INIT_FAILED; }
   Print(&class="macro">#x27;client created&class="macro">#x27;);
   class="kw">return INIT_SUCCEEDED;
}

「先写测试再补协议头」

上一节里本地客户端已经能连上 Mosquitto,但代理立刻以协议错误重置了连接。日志里那句“<unknown> 客户端立即断开连接”不是网络问题,而是 CONNECT 包只发了固定头,缺了协议名、协议级别和变长头这些元数据。 我们走的是测试驱动开发路线,所以这步不急着写生成代码,而是先给 CONNECT 数据包生成器写一个单元测试:它必须产出带完整元数据的包,且不再被代理以协议错误拒掉。对很多交易者出身的量化新手来说,还没写实现就先写测试像是在绕路,但把测试当成需求的客观描述,它其实就是开发要达到的硬指标。 Robert Martin 在《整洁编码》里说过,单元测试就是最底层设计文档,毫不含糊、可被执行。对外汇和贵金属这类高波动、高杠杆品种做自动化接入,先把协议边界用测试钉死,后面改 broker 或换代理时才不会 silently 发错包。

用 OOP 拆解 MQTT 数据包比硬写字节数组靠谱

第一次连本地代理时,我们故意把固定头字节数组写死发出去,结果 MT5 日志直接报“协议错误”断连。这次失败不是白费——它验证了开发环境能跑测试,也暴露出硬编码方式撑不起复杂协议。 构建一致的 MQTT 数据包只是写健壮客户端的第一步,后面服务器响应类型、应用状态一多,写死的数组立刻不够用。MQL5 跑在 PC 上,没有当年 MQTT 设计时的内存和 CPU 硬约束,完全可以放开用面向对象。 选对抽象层级后,协议逻辑能直接映射成类和方法,读代码的人(多数时候是隔周的你自己)不用数字节。MQL5 官方参考里整章讲面向对象编程,类、继承、多态都齐,拿来组织连接和收发状态机正合适。 外汇和贵金属 EA 跑这套有实时断连风险,建议先在本地代理用日志断点验证类结构,再上真实账户。

◍ 把协议常量锁进两个头文件

消息共享协议的本质是一组有状态规则:设备端代码在下发下一步动作前,必须先评估应用当前状态。但在这些状态机逻辑之前,还躺着一批与运行状态无关的底层定义——协议名、控制包类型、剩余长度字节值,它们通常以常量和枚举形式存在,几乎不会随业务逻辑改动。 工程上把这两类东西拆进两个不同的 .mqh 头文件:Defines.mqh 只收协议术语和定值,写完后基本不动;MQTT.mqh 收枚举、结构和函数,在首个版本开发期内就会频繁演进,后续优化和 bug 修复也主要落在这里。这种用头文件集中共享定义的做法,在 K&R 的《C 编程语言》里就被强调过——程序膨胀后头文件拆分能保住唯一副本的正确性。 命名上所有协议相关标识符都强制加 MQTT_ 前缀,跟自研定义做物理隔离;CONNECT 包目前独占了协议名与版本常量,但故意不塞进 CPktConnect 类,而是留在 Defines 里等以后复用。标识符拼写到字节级明确,是刻意迎合现代 IDE 的自动补全和全局搜索,调试时少翻层。 下面这段 Defines.mqh 的节选能直接拖进 MT5 的 MQL5 编辑器编译验证:协议名长度两字节 0x00/0x04,随后四个字符 M Q T T,协议版本固定 0x05;属性字段里 0x01 是单字节的载荷格式指示,0x02 是四字节消息过期间隔,0x03 和 0x08 都是 UTF-8 字符串类型。外汇或贵金属 EA 跑 MQTT 桥接时,这类常量错一个字节就可能连不上 broker,属于高风险的底层对接。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                          Defines.mqh |
class=class="str">"cmt">//|     ********* WORK IN PROGRESS **********                        |
class=class="str">"cmt">//| **** PART OF ARTICLE [MQL5官方文档] **** |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|             PROTOCOL NAME AND VERSION                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define MQTT_PROTOCOL_NAME_LENGTH_MSB        0x00
class="macro">#define MQTT_PROTOCOL_NAME_LENGTH_LSB        0x04
class="macro">#define MQTT_PROTOCOL_NAME_BYTE_3            &class="macro">#x27;M&class="macro">#x27;
class="macro">#define MQTT_PROTOCOL_NAME_BYTE_4            &class="macro">#x27;Q&class="macro">#x27;
class="macro">#define MQTT_PROTOCOL_NAME_BYTE_5            &class="macro">#x27;T&class="macro">#x27;
class="macro">#define MQTT_PROTOCOL_NAME_BYTE_6            &class="macro">#x27;T&class="macro">#x27;
class="macro">#define MQTT_PROTOCOL_VERSION                0x05
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|             PROPERTIES                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">/*
The last field in the Variable Header of the CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC,
PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, DISCONNECT, and
AUTH packet is a set of Properties. In the CONNECT packet there is also an optional set of Properties in
the Will Properties field with the Payload
*/
class="macro">#define MQTT_PROPERTY_PAYLOAD_FORMAT_INDICATOR     0x01 class=class="str">"cmt">// (class="num">1) Byte
class="macro">#define MQTT_PROPERTY_MESSAGE_EXPIRY_INTERVAL      0x02 class=class="str">"cmt">// (class="num">2) Four Byte Integer
class="macro">#define MQTT_PROPERTY_CONTENT_TYPE                 0x03 class=class="str">"cmt">// (class="num">3) UTF-class="num">8 Encoded String
class="macro">#define MQTT_PROPERTY_RESPONSE_TOPIC               0x08 class=class="str">"cmt">// (class="num">8) UTF-class="num">8 Encoded String

「MQTT 属性标识符的硬编码清单」

在 MT5 里用 MQL5 对接 MQTT broker 做行情转发或 AIGC 信号订阅时,属性类型必须用十六进制标识符明确标注,否则握手阶段直接被服务端断连。下面这批宏定义把 MQTT 5.0 协议里常用的 21 个属性映射成了整数常量,覆盖二进制、字符串、整数等多种载荷形态。 例如 0x09 对应 Correlation Data(二进制),0x11 对应 Session Expiry Interval(四字节整数),0x21 对应 Receive Maximum(双字节整数)。开 MT5 新建 include 头文件粘贴这套定义,编译不会报错,但注意 0x26 的 User Property 是字符串对,发送时需成对写入键值。 外汇与贵金属品种通过外部通道推送信号时,网络抖动可能导致属性丢包,这类桥接方案本身属于高风险辅助手段,仅建议用于复盘或离线分析,不要直接挂实盘自动下单。

MQL5 / C++
class="macro">#define MQTT_PROPERTY_CORRELATION_DATA                   0x09 class=class="str">"cmt">// (class="num">9) Binary Data
class="macro">#define MQTT_PROPERTY_SUBSCRIPTION_IDENTIFIER           0x0B class=class="str">"cmt">// (class="num">11) Variable Byte Integer
class="macro">#define MQTT_PROPERTY_SESSION_EXPIRY_INTERVAL           0x11 class=class="str">"cmt">// (class="num">17) Four Byte Integer
class="macro">#define MQTT_PROPERTY_ASSIGNED_CLIENT_IDENTIFIER        0x12 class=class="str">"cmt">// (class="num">18) UTF-class="num">8 Encoded String
class="macro">#define MQTT_PROPERTY_SERVER_KEEP_ALIVE                 0x13 class=class="str">"cmt">// (class="num">19) Two Byte Integer
class="macro">#define MQTT_PROPERTY_AUTHENTICATION_METHOD             0x15 class=class="str">"cmt">// (class="num">21) UTF-class="num">8 Encoded String
class="macro">#define MQTT_PROPERTY_AUTHENTICATION_DATA               0x16 class=class="str">"cmt">// (class="num">22) Binary Data
class="macro">#define MQTT_PROPERTY_REQUEST_PROBLEM_INFORMATION       0x17 class=class="str">"cmt">// (class="num">23) Byte
class="macro">#define MQTT_PROPERTY_WILL_DELAY_INTERVAL               0x18 class=class="str">"cmt">// (class="num">24) Four Byte Integer
class="macro">#define MQTT_PROPERTY_REQUEST_RESPONSE_INFORMATION      0x19 class=class="str">"cmt">// (class="num">25) Byte
class="macro">#define MQTT_PROPERTY_RESPONSE_INFORMATION              0x1A class=class="str">"cmt">// (class="num">26) UTF-class="num">8 Encoded String
class="macro">#define MQTT_PROPERTY_SERVER_REFERENCE                  0x1C class=class="str">"cmt">// (class="num">28) UTF-class="num">8 Encoded String
class="macro">#define MQTT_PROPERTY_REASON_STRING                     0x1F class=class="str">"cmt">// (class="num">31) UTF-class="num">8 Encoded String
class="macro">#define MQTT_PROPERTY_RECEIVE_MAXIMUM                   0x21 class=class="str">"cmt">// (class="num">33) Two Byte Integer
class="macro">#define MQTT_PROPERTY_TOPIC_ALIAS_MAXIMUM               0x22 class=class="str">"cmt">// (class="num">34) Two Byte Integer
class="macro">#define MQTT_PROPERTY_TOPIC_ALIAS                       0x23 class=class="str">"cmt">// (class="num">35) Two Byte Integer
class="macro">#define MQTT_PROPERTY_MAXIMUM_QOS                       0x24 class=class="str">"cmt">// (class="num">36) Byte
class="macro">#define MQTT_PROPERTY_RETAIN_AVAILABLE                  0x25 class=class="str">"cmt">// (class="num">37) Byte
class="macro">#define MQTT_PROPERTY_USER_PROPERTY                     0x26 class=class="str">"cmt">// (class="num">38) UTF-class="num">8 String Pair
class="macro">#define MQTT_PROPERTY_MAXIMUM_PACKET_SIZE               0x27 class=class="str">"cmt">// (class="num">39) Four Byte Integer
class="macro">#define MQTT_PROPERTY_WILDCARD_SUBSCRIPTION_AVAILABLE   0x28 class=class="str">"cmt">// (class="num">40) Byte

MQTT 返回码与可用属性字节定义

在 MT5 里用 MQL5 写 MQTT 客户端对接行情中继时,先认清楚两个服务端能力标识字节:0x29(十进制 41)表示支持订阅标识符,0x2A(十进制 42)表示支持共享订阅。这两个宏若不在握手包里返回,客户端就别硬发对应特性,否则会被服务端按协议错误踢掉。 Reason Code 是单字节无符号值,用来告知某次操作结果。小于 0x80 都算成功,其中 0x00 最常用——CONNACK、PUBACK 以及 SUBACK 里 granted QoS0 都靠它;0x01、0x02 分别对应授权 QoS1、QoS2。大于等于 0x80 即失败,例如 0x80 是未指定错误,0x82 是协议错误,0x84 是协议版本不支持。 SUBACK 和 UNSUBACK 的 payload 里可以塞一组返回码,而不像 CONNACK 那样只有一个。下面这段代码把常用定义落到了编译层,开 MT5 直接贴进 ea 头文件就能用: 别把 0x00 当万能码 虽然 0x00 在成功类里出现频率最高,但 NORMAL_DISCONNECTION 也是 0x00,断连和授权成功同值,解析包时必须结合控制报文类型区分,不能只比对一个字节就下结论。

MQL5 / C++
class="macro">#define MQTT_PROPERTY_SUBSCRIPTION_IDENTIFIER_AVAILABLE 0x29 class=class="str">"cmt">// (class="num">41) Byte
class="macro">#define MQTT_PROPERTY_SHARED_SUBSCRIPTION_AVAILABLE    0x2A class=class="str">"cmt">// (class="num">42) Byte
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                        REASON CODES                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">/*
A Reason Code is a one byte unsigned value that indicates the result of an operation. Reason Codes less
than 0x80 indicate successful completion of an operation. The normal Reason Code for success is class="num">0.
Reason Code values of 0x80 or greater indicate failure.
The CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, DISCONNECT and AUTH Control Packets
have a single Reason Code as part of the Variable Header. The SUBACK and UNSUBACK packets
contain a list of one or more Reason Codes in the Payload.
*/
class="macro">#define MQTT_REASON_CODE_SUCCESS                      0x00 class=class="str">"cmt">// (class="num">0)
class="macro">#define MQTT_REASON_CODE_NORMAL_DISCONNECTION         0x00 class=class="str">"cmt">// (class="num">0)
class="macro">#define MQTT_REASON_CODE_GRANTED_QOS_0                0x00 class=class="str">"cmt">// (class="num">0)
class="macro">#define MQTT_REASON_CODE_GRANTED_QOS_1                0x01 class=class="str">"cmt">// (class="num">1)
class="macro">#define MQTT_REASON_CODE_GRANTED_QOS_2                0x02 class=class="str">"cmt">// (class="num">2)
class="macro">#define MQTT_REASON_CODE_DISCONNECT_WITH_WILL_MESSAGE 0x04 class=class="str">"cmt">// (class="num">4)
class="macro">#define MQTT_REASON_CODE_NO_MATCHING_SUBSCRIBERS      0x10 class=class="str">"cmt">// (class="num">16)
class="macro">#define MQTT_REASON_CODE_NO_SUBSCRIPTION_EXISTED      0x11 class=class="str">"cmt">// (class="num">17)
class="macro">#define MQTT_REASON_CODE_CONTINUE_AUTHENTICATION      0x18 class=class="str">"cmt">// (class="num">24)
class="macro">#define MQTT_REASON_CODE_RE_AUTHENTICATE              0x19 class=class="str">"cmt">// (class="num">25)
class="macro">#define MQTT_REASON_CODE_UNSPECIFIED_ERROR            0x80 class=class="str">"cmt">// (class="num">128)
class="macro">#define MQTT_REASON_CODE_MALFORMED_PACKET             0x81 class=class="str">"cmt">// (class="num">129)
class="macro">#define MQTT_REASON_CODE_PROTOCOL_ERROR               0x82 class=class="str">"cmt">// (class="num">130)
class="macro">#define MQTT_REASON_CODE_IMPLEMENTATION_SPECIFIC_ERROR 0x83 class=class="str">"cmt">// (class="num">131)
class="macro">#define MQTT_REASON_CODE_UNSUPPORTED_PROTOCOL_VERSION 0x84 class=class="str">"cmt">// (class="num">132)
class="macro">#define MQTT_REASON_CODE_CLIENT_IDENTIFIER_NOT_VALID  0x85 class=class="str">"cmt">// (class="num">133)

常见问题

先别写连接逻辑,用测试驱动把协议头结构跑通,确认数据包能正确序列化再谈网络层。
用面向对象把数据包拆成类,每个字段独立读写,比裸字节数组更不容易越界和漏字段。
可以,小布能基于你贴的代码梳理常量清单与属性标识符,标出可能硬编码缺失或返回码未覆盖的地方。
是的,建议锁进独立头文件统一管理,避免散落在多处导致改一处漏一片。
建一份硬编码清单放单独文件,起好语义化名字,调试时直接查表不用翻协议文档。