MQL5-JSON-API 实战:用 ZeroMQ 打通 MetaTrader 与外部系统的数据通道
发布时间:2026/10/12 3:32:46 锦皓数字建站

简介这份资源是面向量化交易开发者与MT5平台进阶用户的MQL5-JSON-API集成方案重点解决MetaTrader 5与外部系统之间通过JSON格式交换行情报价与交易指令的问题。它借助套接字与ZeroMQ实现轻量级跨平台通信可用于实时获取买卖价、最高最低价等引号数据并支撑自动化策略与历史数据回测场景。资源包共11个文件约49KB以mqh头文件、mq5源码为主辅以md说明文档、license许可、sh脚本及git配置涵盖JSON编解码、套接字通信、错误控制与指标、EA示例等模块目录结构清晰便于按功能查阅与二次开发。目前已有368人学习下载。读者可从中获得JSON API设计、Sockets通信、ZeroMQ应用、MQL5编程及交易接口调用等关键知识点的可运行参考代码与排错思路适合需要将MT5接入自有系统或构建高效交易数据流的中高级开发者研读。1. 拆开 MQL5-JSON-API把行情数据从终端里“捞”出来的那条路如果你写过 MQL5 的 EA 或者指标大概率遇到过这个场景策略逻辑在终端里跑得好好的但你想把实时报价、历史 K 线、账户状态同步到外部的 Python 或 Node.js 服务里做二次分析、落库或者对接自己的风控面板结果发现 MetaTrader 那边根本没有一个开箱即用的 HTTP 接口。你只能靠文件读写、命名管道或者 DLL 去凑写出来的东西又脆又难维护。MQL5-JSON-API 这个资源包解决的就是这件事——它在 MQL5 侧封装了一套基于 JSON 的通信层配合 ZeroMQ 的 sockets 机制把终端里的数据以结构化格式推给外部程序反过来也能接收外部指令。适合谁适合已经能写基础 MQL5 脚本、但被跨进程通信折磨过的量化开发者以及需要把 MetaTrader 数据接入自有系统的后端工程师。它不是官方 API是一层社区风格的桥接实现理解它的边界比会用更重要。2. JSON 通信层与 ZeroMQ 的选型逻辑为什么不用 HTTP 和文件2.1 终端内进程通信的三种常见做法对比在 MQL5 里往外传数据常见做法无非三类文件轮询、命名管道、socket 通信。文件轮询最简单EA 把数据写成 CSV 或 JSON 文件外部程序定时读。问题是延迟不可控高频场景下磁盘 IO 直接成为瓶颈而且文件锁和读写竞争会带来一堆玄学问题。命名管道比文件快但 Windows 下的管道实现跨平台性差Linux 那边跑 MetaTrader 得靠 Wine管道路径映射经常翻车。socket 通信是延迟和通用性平衡得最好的方案MQL5 原生支持 Socket 函数集但原生 socket 只传字节流你得自己定协议、处理粘包、做序列化。MQL5-JSON-API 的思路是在 socket 之上再包一层用 ZeroMQ 作为消息传输层用 JSON 作为数据格式。ZeroMQ 不是传统意义上的消息队列中间件它是一个嵌入式网络库不需要单独部署 broker在 MQL5 侧通过 DLL 调用在外部程序侧用对应语言的 ZeroMQ binding 即可。这样做的直接好处是消息边界由 ZeroMQ 保证你不用自己处理粘包JSON 格式让两端的数据结构自描述调试时直接打印就能看懂。提示ZeroMQ 的 DLL 需要和 MQL5 的位数匹配64 位终端配 64 位 DLL放错目录会导致加载失败但报错信息很模糊。2.2 资源包里的核心文件与职责划分这个资源包的结构不复杂但每个文件都有明确分工。下面这张表是我拆包后整理的职责对照方便你定位改哪里。文件/目录职责改动频率Include/JSON/JSON 序列化与反序列化库低除非要支持新类型Include/ZeroMQ/ZeroMQ 的 MQL5 封装头文件低Libraries/ZeroMQ 动态库文件极低跟终端位数走Scripts/或Experts/示例发布端与订阅端高业务逻辑都在这配置示例端口、地址、主题等参数中按环境改JSON 库负责把 MQL5 的结构体、数组转成字符串ZeroMQ 封装负责把字符串通过 socket 发出去。示例脚本里通常包含一个 publisher 和一个 subscriberpublisher 挂在 EA 的OnTick里推送报价subscriber 用来验证通道是否打通。我一般会先把 subscriber 跑起来确认能收到心跳再去改 publisher 的业务字段。2.3 通信模式选择PUB/SUB 还是 REQ/REPZeroMQ 支持多种套接字模式这个资源包里最常见的是 PUB/SUB 和 REQ/REP 两种。PUB/SUB 适合行情推送publisher 只管发subscriber 只管收一对多天然支持新订阅者上线不需要 publisher 做任何改动。缺点是 PUB 端不关心有没有人收消息发出去就丢不适合需要确认的场景。REQ/REP 适合指令交互外部程序发一个请求EA 处理完返回结果一问一答。缺点是同步阻塞高频请求下吞吐上不去。实际项目里我一般混用行情走 PUB/SUB下单和查询走 REQ/REP两个端口分开。资源包的示例可能只演示了一种但 ZeroMQ 的套接字类型在初始化时改一个枚举值就能切换不需要动传输层代码。// MQL5 侧初始化 PUB 套接字的典型写法 #include ZeroMQ/zmq.mqh int ctx; // 上下文句柄 int publisher; // 套接字句柄 int OnInit() { // 创建 ZeroMQ 上下文1 表示内部 IO 线程数 ctx zmq_ctx_new(); if(ctx 0) { Print(上下文创建失败检查 DLL 是否加载); return INIT_FAILED; } // 创建 PUB 类型套接字 publisher zmq_socket(ctx, ZMQ_PUB); if(publisher 0) { Print(套接字创建失败); return INIT_FAILED; } // 绑定到本地端口 5555外部程序连这个地址 // 注意用 tcp://* 而不是 tcp://127.0.0.1否则外部机器连不上 int rc zmq_bind(publisher, tcp://*:5555); if(rc ! 0) { Print(绑定端口失败可能被占用); return INIT_FAILED; } Print(PUB 端已就绪端口 5555); return INIT_SUCCEEDED; }这段代码的逻辑很直白先建上下文再建套接字最后绑定端口。参数上zmq_ctx_new()的参数是 IO 线程数一般给 1 就够给多了反而增加上下文切换开销。zmq_bind的地址里*表示监听所有网卡如果你只在本机通信改成127.0.0.1更安全。失败时不要只看返回值ZeroMQ 的错误码要用zmq_errno()取常见的是端口被占用和 DLL 位数不匹配。2.4 数据发布端的实现步骤发布端的核心工作是把 MQL5 里的行情数据组装成 JSON然后通过套接字发出去。步骤拆开是取数据、组 JSON、发送、异常处理。取数据用SymbolInfoDouble和CopyRates组 JSON 用资源包里的 JSON 库发送用zmq_send。// 在 OnTick 中发布当前报价 void OnTick() { // 组装一个 JSON 对象 JSONObject obj; obj.Set(symbol, _Symbol); obj.Set(bid, SymbolInfoDouble(_Symbol, SYMBOL_BID)); obj.Set(ask, SymbolInfoDouble(_Symbol, SYMBOL_ASK)); obj.Set(time, (long)TimeCurrent()); obj.Set(volume, (long)SymbolInfoInteger(_Symbol, SYMBOL_VOLUME)); // 序列化成字符串 string payload obj.ToString(); // 发送ZMQ_SNDMORE 不设置表示这是最后一条消息 // 返回值是发送字节数-1 表示失败 int sent zmq_send(publisher, payload, StringLen(payload), 0); if(sent 0) { Print(发送失败错误码: , zmq_errno()); } }这里有几个参数值得说清楚。zmq_send的第三个参数是数据长度MQL5 里字符串按字节算用StringLen在纯 ASCII 下没问题但如果 JSON 里有中文或特殊字符得用StringToCharArray转成字节数组再取长度。第四个参数是标志位0 表示普通发送ZMQ_SNDMORE表示后面还有消息帧做多段消息时才用。发送失败最常见的原因是上下文或套接字已经失效比如终端在OnDeinit里没正确关闭就重载了 EA。2.5 外部接收端的对接方式外部程序这边Python 用pyzmq是最顺手的。安装完pip install pyzmq之后几行代码就能订阅。import zmq import json # 创建上下文 context zmq.Context() # 创建 SUB 类型套接字 subscriber context.socket(zmq.SUB) # 连接发布端地址注意这里是 connect 不是 bind subscriber.connect(tcp://127.0.0.1:5555) # 订阅所有主题空字符串表示不过滤 subscriber.setsockopt_string(zmq.SUBSCRIBE, ) print(SUB 端已连接等待数据...) while True: # 接收消息阻塞等待 message subscriber.recv_string() try: data json.loads(message) print(f收到 {data[symbol]} 报价: bid{data[bid]} ask{data[ask]}) except json.JSONDecodeError: print(JSON 解析失败原始内容:, message)connect和bind的方向不要搞反发布端 bind订阅端 connect。setsockopt_string设置订阅过滤空字符串表示全收如果发布端按品种分了主题这里可以填EURUSD只收对应品种。接收循环里加 JSON 解析的异常捕获是血泪经验——发布端如果发了半截消息或者格式不对没有捕获的话整个循环直接崩掉。3. 历史数据批量拉取从终端到外部存储的完整链路3.1 历史数据接口的设计约束实时报价走 PUB/SUB 很顺但历史数据不一样。历史 K 线数据量大、请求频率低、需要按条件筛选用 REQ/REP 更合适。外部程序发一个请求带上品种、周期、起止时间EA 收到后用CopyRates取数据组 JSON 返回。这里有个约束MQL5 的CopyRates一次能取的 K 线数量有限制具体上限跟终端设置有关常见做法是分批取每批几千根取完再合并。资源包里如果有历史数据相关的示例通常会包含一个请求解析函数和一个数据组装函数。请求格式我一般约定成 JSON字段包括action、symbol、timeframe、start、count。EA 侧解析action字段决定走哪个分支这样一套 REQ/REP 通道能同时处理多种请求类型。3.2 请求与响应的 JSON 结构约定两端约定好结构是避免后期扯皮的关键。下面这张表是我在项目里常用的字段约定你可以直接抄也可以按自己习惯改但改完两端必须同步。字段方向类型说明action请求string操作类型如history、accountsymbol请求string品种名称如EURUSDtimeframe请求string周期如M1、H1、D1start请求long起始时间戳秒级count请求int请求 K 线数量status响应stringok或errordata响应arrayK 线数组每项含 OHLCVmessage响应string出错时的描述timeframe用字符串而不是数字是为了可读性。EA 侧收到后用一个映射函数转成 MQL5 的ENUM_TIMEFRAMES比如H1转PERIOD_H1。这个映射函数要写全漏一个周期就会在运行时返回空数据而且不报错排查起来很费时间。3.3 EA 侧的历史数据组装代码// 处理历史数据请求 string HandleHistoryRequest(JSONObject request) { string symbol request.GetString(symbol); string tfStr request.GetString(timeframe); long start request.GetLong(start); int count request.GetInt(count); // 周期字符串转枚举 ENUM_TIMEFRAMES tf StringToTimeframe(tfStr); if(tf PERIOD_CURRENT) { return BuildError(不支持的周期: tfStr); } // 分批取数据每批 5000 根 MqlRates rates[]; int total 0; int batchSize 5000; datetime cursor (datetime)start; while(total count) { int toFetch MathMin(batchSize, count - total); int copied CopyRates(symbol, tf, cursor, toFetch, rates); if(copied 0) { // 取不到就停可能是历史数据没下载 break; } total copied; // 游标后移注意时间戳单位 cursor rates[copied - 1].time PeriodSeconds(tf); } // 组装 JSON 数组 JSONArray arr; for(int i 0; i total; i) { JSONObject bar; bar.Set(t, (long)rates[i].time); bar.Set(o, rates[i].open); bar.Set(h, rates[i].high); bar.Set(l, rates[i].low); bar.Set(c, rates[i].close); bar.Set(v, (long)rates[i].tick_volume); arr.Add(bar); } JSONObject resp; resp.Set(status, ok); resp.Set(data, arr); return resp.ToString(); }这段代码里最关键的是游标推进逻辑。CopyRates的第三个参数是起始时间取完之后要把游标推到这批数据的最后一根之后否则下一批会重复取到相同数据。PeriodSeconds把周期转成秒数M1 是 60H1 是 3600。如果CopyRates返回 0 或负数说明终端本地没有这段历史数据需要先在图表上加载对应品种和周期让终端去下载这个前置动作经常被忽略。注意CopyRates在策略测试器里的行为和实盘不同测试器里历史数据是预加载的取数逻辑不会暴露“数据没下载”的问题实盘跑的时候才发现取不到这个坑我踩过不止一次。3.4 外部程序的分批接收与落库外部程序收到响应后解析data数组逐条写入数据库或文件。数据量大的时候一次性json.loads整个响应会占不少内存但一般几万根 K 线还在可接受范围。落库用 SQLite 最省事建一张表按品种和周期分区。import sqlite3 def save_bars(conn, symbol, timeframe, bars): cursor conn.cursor() # 建表语句主键是品种周期时间戳避免重复插入 cursor.execute( CREATE TABLE IF NOT EXISTS bars ( symbol TEXT, timeframe TEXT, ts INTEGER, open REAL, high REAL, low REAL, close REAL, volume INTEGER, PRIMARY KEY (symbol, timeframe, ts) ) ) # 批量插入用 INSERT OR REPLACE 处理重复 cursor.executemany( INSERT OR REPLACE INTO bars VALUES (?, ?, ?, ?, ?, ?, ?, ?) , [(symbol, timeframe, b[t], b[o], b[h], b[l], b[c], b[v]) for b in bars]) conn.commit()INSERT OR REPLACE配合主键重复拉取同一时间段的数据不会产生冗余行这个设计在增量更新时很实用。批量插入用executemany比循环单条插入快一个数量级几万条数据秒级完成。4. 避坑与排查那些让通道“假死”的细节4.1 现象SUB 端连上了但收不到任何数据原因通常有两个。一是发布端还没开始发SUB 端连接后 ZeroMQ 需要一点时间完成握手如果 publisher 在 subscriber 连接之前就发完了消息这些消息直接丢弃SUB 端永远等不到。二是订阅过滤设置不对setsockopt_string(zmq.SUBSCRIBE, )必须调用不调用的话 SUB 套接字默认不订阅任何主题收不到消息也不报错。解决发布端加一个定时心跳比如每秒发一条{type:heartbeat}这样新订阅者上线后最多等一秒就能确认通道通了。SUB 端确认SUBSCRIBE设置在所有connect之前或之后都行但一定要设。4.2 现象EA 重载后端口被占用绑定失败原因是在OnDeinit里没有正确关闭套接字和上下文。MQL5 的 EA 在参数变更或图表切换时会走OnDeinit再走OnInit如果旧套接字没关新套接字绑同一个端口就会失败。ZeroMQ 的套接字关闭需要先zmq_close再zmq_ctx_term顺序反了会卡住。解决在OnDeinit里按顺序关闭并且加一个短暂的等待。void OnDeinit(const int reason) { if(publisher ! 0) { zmq_close(publisher); // 先关套接字 publisher 0; } if(ctx ! 0) { zmq_ctx_term(ctx); // 再关上下文这个调用会阻塞直到所有套接字关闭 ctx 0; } Print(资源已释放); }zmq_ctx_term是阻塞的如果还有套接字没关它会一直等。所以顺序不能反也不能漏关套接字。4.3 现象JSON 里的浮点数精度丢失MQL5 的double是 64 位但 JSON 库在序列化时如果默认保留位数不够价格数据会丢精度。比如 1.23456789 变成 1.23457对于需要精确价格的下游系统来说不可接受。解决检查 JSON 库的浮点序列化配置通常有一个精度参数。如果没有可以在组 JSON 之前手动用DoubleToString格式化指定足够的小数位再以字符串形式存入 JSON。代价是下游解析时要再转回浮点但精度可控。4.4 现象历史数据请求返回空数组但不报错原因在 3.3 节提过终端本地没有对应品种和周期的历史数据。CopyRates返回 0代码里如果只是break然后返回空数组调用方看到status: ok但data: []会以为是请求参数问题。解决在返回空数组时把status设成errormessage里写明“本地无历史数据请先在图表加载”。另外可以在 EA 初始化时用SymbolSelect确保品种在 Market Watch 里用CopyRates取一根数据触发下载。4.5 现象跨机器通信时连不上原因通常是绑定地址用了127.0.0.1只监听本机回环外部机器连不进来。另一个可能是防火墙拦了端口。解决绑定用tcp://*:端口或指定本机对外 IP。防火墙方面Windows Defender 默认会拦入站连接需要给终端进程放行或者换一个已放行的端口。跨机器场景下还要注意两台机器的时间同步时间戳对不上会导致数据关联出错。5. 进阶用 REQ/REP 做指令通道与连接健康检查PUB/SUB 通道跑通之后下一步我一般会加一条 REQ/REP 通道做指令交互和健康检查。REQ/REP 是同步的外部程序发一个{action:ping}EA 回一个{status:ok,time:...}收到回复就说明 EA 还活着、套接字还通。这个比看日志直观也比等心跳超时快。实现上EA 侧在OnTimer里轮询 REQ 套接字有请求就处理没请求就返回。注意 REQ/REP 模式下REP 端必须收一条才能发一条收发的顺序不能乱否则 ZeroMQ 的状态机会卡死。我一般用zmq_recv的非阻塞模式设一个短超时避免OnTimer被阻塞。// 在 OnTimer 中轮询 REQ 请求 void OnTimer() { char buffer[4096]; // 非阻塞接收ZMQ_DONTWAIT 表示没数据立即返回 int received zmq_recv(responder, buffer, 4096, ZMQ_DONTWAIT); if(received 0) { return; // 没有请求直接返回 } string request CharArrayToString(buffer, 0, received); string response ProcessRequest(request); // REP 端必须回复否则下一个请求会卡住 zmq_send(responder, response, StringLen(response), 0); }ZMQ_DONTWAIT是关键参数没有它zmq_recv会一直阻塞到有数据为止OnTimer就废了。ProcessRequest里根据action字段分发到不同处理函数返回 JSON 字符串。发送回复时如果失败REQ 端会一直等所以发送失败要记日志。验证方法上我习惯先用一个最简单的 Python 脚本发 ping确认收到 pong再逐步加业务指令。每加一个指令先在 EA 侧用Print打出收到的原始请求确认解析没问题再看响应。这个顺序能快速定位是解析问题还是业务逻辑问题。从那以后我每次接新的通信通道都强制先跑通 ping-pong 再写业务这个习惯帮我省了很多来回排查的时间。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。