简介:面向MetaTrader 5开发者与量化交易学习者的MQL5-JSON-API开源实现,用于打通MT5与外部系统之间的实时报价与历史数据传输。资源聚焦引号数据获取,涵盖JSON API设计、套接字通信、ZeroMQ消息模式、MQL5编码解码、交易接口安全处理等核心知识点,适合需要构建自动化交易管道或自研数据服务的进阶用户。压缩包仅49KB,共11个文件,包含3个mqh头文件(JSON封装、错误控制、枚举转换)、2个mq5示例(JsonAPI指标与EA)、2个md文档,以及许可证、清理脚本与Git配置;文件类型覆盖源码、说明与工程配置,目录结构清晰,便于直接集成或按需改造。该资源目前已有368人学习,示例代码与文档可帮助开发者快速掌握JSON请求构建、响应解析及ZeroMQ socket通信实现思路,减少从零排查网络与编码问题的成本,是结合MT5平台搭建轻量级数据接口的实用参考。
1. MQL5-JSON-API:先把 MT5 行情喂给外部程序
做量化的人迟早会撞上这样一个需求:MT5 里跑着 EA 或指标,但实时行情、历史 K 线得交给 Python、C# 或者别的程序去算。MQL5-JSON-API 这套接口解决的就是这个中间层——把 MetaTrader 5 里的报价行情、K 线、Tick 历史统一转成 JSON,再通过 Socket 或 ZeroMQ 送出去。它不碰交易单,也不替你写策略,只做一件事:让 MT5 变成下游程序的数据源。适合谁:已经在用 MT5,又不想在 MQL5 里重写一套数据处理框架的人;要给团队搭统一行情接口的人;以及需要把 MT5 历史数据导出来做分析的人。下面从通道选型开始讲,把 DLL 配置、JSON 转义、历史数据边界几个最容易翻车的细节一次说透。
2. 通道选型:内置 Socket、ZeroMQ 与 WebRequest,三条路怎么选
2.1 MQL5 内置 Socket 的边界:它只能当客户端
MQL5 从很早的版本就提供了 SocketCreate、SocketConnect、SocketSend、SocketReceive 这一组函数,不需要任何第三方 DLL,配置成本最低。但有一个边界必须认清:这组函数只能让 MT5 作为客户端主动去连别人,MT5 端没有 listen 能力,当不了服务端。所以想用内置 Socket,正确姿势是外部程序(比如 Python)先起一个 TCP 服务端,MT5 在 OnTimer 里定时连接、发送、再断开,或者保持长连接持续推。
import socket, json server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(("127.0.0.1", 9100)) server.listen(1) print("waiting for MT5...") conn, addr = server.accept() print("MT5 connected from", addr) buffer = b"" while True: data = conn.recv(65536) if not data: break buffer += data while b"\n" in buffer: # 以换行符作为消息边界 line, buffer = buffer.split(b"\n", 1) if line.strip(): msg = json.loads(line.decode("utf-8")) print("got:", msg)这段代码里有两个关键点:一是SO_REUSEADDR,否则 MT5 频繁重连时端口会进入 TIME_WAIT,导致连不上;二是按\n切分消息而不是按 recv 次数切。TCP 是流协议,一条 JSON 可能被拆成多个包到达,也可能多个 JSON 挤在一个包里,必须自己做粘包处理,这是新手最常见的翻车点。
MQL5 端对应的发送代码:
int sock = SocketCreate(); if(sock != INVALID_HANDLE) { if(SocketConnect(sock, "127.0.0.1", 9100, 1000)) { string msg = "{\"type\":\"heartbeat\",\"ts\":" + (string)TimeCurrent() + "}\n"; uchar bytes[]; int len = StringToCharArray(msg, bytes, 0, StringLen(msg), CP_UTF8); SocketSend(sock, bytes, len); } SocketClose(sock); }注意StringToCharArray的第四个参数必须传StringLen(msg),否则它会在数组末尾追加一个\0终止符,把\0也发给 Python。Python 侧 json.loads 一旦遇到\0直接抛异常,这类问题排查半天往往就卡在一个多余的 0 字节上。
2.2 ZeroMQ:用 DLL 把 MT5 变成 PUB/REP 服务端
如果外部程序不只有一个,或者 MT5 要跨机器推送,内置 Socket 就不够用了。业内普遍做法是给 MT5 挂一个 ZeroMQ 的 DLL 封装,让 MT5 拥有 PUB/SUB 和 REQ/REP 能力。PUB/SUB 适合实时行情广播,多个下游程序各自订阅自己关心的品种;REQ/REP 适合外部程序主动向 MT5 要历史数据,一问一答,天然对得上。
// 以下函数名以你下载的 ZeroMQ 封装头文件为准,不同版本差异较大 #import "ZeroMQ.dll" long zmq_ctx_new(); long zmq_socket(long context, int type); int zmq_bind(long socket, string endpoint); int zmq_send(long socket, uchar &data[], uint len, int flags); int zmq_recv(long socket, uchar &data[], uint len, int flags); #import #define ZMQ_PUB 1 #define ZMQ_SUB 2 #define ZMQ_REP 4 #define ZMQ_REQ 3 long ctx = zmq_ctx_new(); long pub = zmq_socket(ctx, ZMQ_PUB); zmq_bind(pub, "tcp://*:9200");DLL 的放置位置是MQL5\Libraries目录,同时要在 MT5 终端设置里勾选「允许 DLL 导入」。这里要特别提醒位数问题:MT5 终端是 64 位还是 32 位,必须用对应的 libzmq.dll,装反了日志里只会出现一个笼统的“cannot load library”。还有杀毒软件会把 Socket DLL 当风险程序拦掉,解压后先加白名单再拷贝进 Libraries。
ZeroMQ 这套方案的真实价值是它的队列和重连机制。PUB/SUB 模式下订阅端掉线再回来,只要主题前缀对得上,数据流自动恢复,不用像内置 Socket 那样自己维护心跳和重连。代价是你得先接受「MT5 主线程不能阻塞」这个现实,zmq_recv 不能像 Python 里那样同步等消息,只能放到 OnTimer 里轮询。
2.3 我的选型倾向与整体数据流
我自己接模拟项目X的时候,一个自用分析工具,数据量小、单机跑,直接用内置 Socket,零依赖最省心;后来要给三个下游程序同时供数,才换成了 ZeroMQ。选型可以按这张表来:
| 方案 | 方向 | 依赖 | 多客户端 | 断线恢复 | 适用场景 |
|---|---|---|---|---|---|
| 内置 Socket | MT5 连外部 | 无 | 需自行实现 | 需自行实现 | 单机、原型、自用 |
| ZeroMQ DLL | 双向 | libzmq.dll | 原生支持 | 自动 | 多订阅方、跨机、生产 |
| WebRequest | MT5 发 HTTP | 无 | 服务端决定 | HTTP 层面 | 低频拉取、REST 接口 |
整体数据流推荐这样设计:Python 端起 TCP 服务端或 SUB 订阅端,MT5 端 OnTimer 每 500ms 推一次「最新 K 线 + 最新 tick」的 JSON;外部程序若要历史数据,走 REQ/REP 或另一个 TCP 连接发请求,MT5 收到后调 CopyRates/CopyTicks 取数,再序列化成 JSON 回发。实时推送和历史拉取最好不要走同一条连接,不然推送数据量一大,响应会被挤掉。
3. JSON 封装与解析:手写转义还是上库,决定你会不会踩转义坑
3.1 为什么不用 StringFormat 直接拼 JSON
MQL5 没有原生的 JSON 对象,很多人第一反应是用 StringFormat 直接拼字符串。拼简单的{"type":"heartbeat"}没问题,但一旦 K 线数组里有字符串、有换行、有特殊符号,就开始出幺蛾子。尤其是品种名称、注释字段里如果带了双引号或反斜杠,拼出来的 JSON 直接坏掉,下游解析时要么报语法错误,要么解析出完全错误的字段。
另一个坑是 Unicode。MT5 里字符串是 UTF-16 存储,StringFormat 拼出来的中文或特殊符号,发到 Python 那边解码后经常变成乱码。所以我的习惯是:凡是走网络传输的字符串,一律先过一遍转义函数再拼 JSON,不在拼接处偷懒。
3.2 轻量转义函数:顺序比你想的重要
string EncodeJSONString(const string s) { string res = s; StringReplace(res, "\\", "\\\\"); // 先转义反斜杠 StringReplace(res, "\"", "\\\""); // 再转义双引号 StringReplace(res, "\n", "\\n"); StringReplace(res, "\r", "\\r"); StringReplace(res, "\t", "\\t"); return res; }顺序必须固定:先反斜杠,后双引号。如果先替换双引号,把"变成\",再回头替换反斜杠,就会把刚才加的\"里的反斜杠又转义一次,最终变成\\\",下游解析出来字符串内容完全对不上。这个坑我见过不止一次,很多人以为是下游解析库的问题,其实是自己转义顺序错了。
然后是字符集问题。发送前统一用StringToCharArray(msg, bytes, 0, StringLen(msg), CP_UTF8)转成 UTF-8 字节流,接收端统一按 UTF-8 解码。不要用终端的默认 ANSI 编码去推,跨平台时早晚出错。
3.3 把 MqlRates 数组转成 JSON 数组字符串
历史 K 线要转成 JSON 数组,核心是把 MqlRates 结构体里的 time、open、high、low、close 这些字段映射成 JSON 键值。时间戳我强烈建议直接用整数秒,不要转成"2024-01-05 08:00:00"这种字符串。原因有两个:一是整数秒不涉及时区显示问题,下游想显示哪个时区自己转;二是省流量,每根 K 线省十几个字节,拉 10 万根 K 线差距就很明显了。
string RowsToJSONArray(const MqlRates &rates[], int count) { string json = "["; for(int i = 0; i < count; i++) { if(i > 0) json += ","; json += StringFormat("{\"t\":%d,\"o\":%s,\"h\":%s,\"l\":%s,\"c\":%s,\"v\":%d}", (int)rates[i].time, DoubleToString(rates[i].open, 5), DoubleToString(rates[i].high, 5), DoubleToString(rates[i].low, 5), DoubleToString(rates[i].close, 5), (int)rates[i].tick_volume); } json += "]"; return json; }注意DoubleToString的小数位数不要写死。EURUSD 五位小数没问题,但 JPY 系品种要三位,XAUUSD 要两位,最好的做法是从品品种的Digits()动态取,或者直接在 JSON 里输出原始 double,由下游自己格式化。写死位数是这道工序里最常见的隐藏 bug,换一个品种就露馅。
3.4 外部请求进来时的解析器
如果外部程序要主动问 MT5 要数据,就得在 MQL5 侧解析收到的 JSON 请求。MQL5 没有内置 JSON 解析器,拿到的字节流要么自己按字符串匹配硬拆,要么引入社区开源的 JSON 解析库。我一般把解析做在一个独立的 Include 文件里,暴露JsonGetString、JsonGetInt这类函数出来,OnTimer 里收到完整消息后调用,避免 EA 主逻辑堆满字符串处理的脏代码。
解析器有一个界面要约定好:收到的请求统一走{"action":"...","params":{...}}结构。比如历史数据请求就是{"action":"history","symbol":"EURUSD","timeframe":"H1","start":1704067200,"end":1706745600}。用 action 字段区分心跳、历史请求、实时订阅,比让下游自己发明协议要省心得多,也方便以后加新的数据接口。
4. 历史数据与实时推送:CopyTicks、CopyRates 的边界参数与推送节奏
4.1 CopyRates 按时间范围取K线:两个重载的方向差异
CopyRates 有两个重载,一个是按位置取CopyRates(symbol, tf, start_pos, count, rates),一个是按时间范围取CopyRates(symbol, tf, start_time, stop_time, rates)。新手最容易搞混的是返回数组的方向:按位置取,索引 0 是离当前最近的那根还是最早的那根,取决于你是否调用了ArraySetAsSeries;按时间范围取,返回数组默认按时间升序,索引 0 是 start_time 那根。
bool GetRatesByTime(const string symbol, const ENUM_TIMEFRAMES tf, const datetime start_time, const datetime stop_time, MqlRates &rates[]) { int count = CopyRates(symbol, tf, start_time, stop_time, rates); if(count <= 0) { Print("CopyRates failed, error=", GetLastError()); return false; } ArraySetAsSeries(rates, false); return true; }ArraySetAsSeries(rates, false)这句很多人不理解。MT5 复制函数返回的数组方向其实受数组的 AS_SERIES 标志影响,如果你之前对同一个数组调用过ArraySetAsSeries(rates, true),那复制结果就是索引 0 对应最新时间。为了不让方向问题在上下游之间传递,我统一强制设为 false,也就是按时间升序,然后在 JSON 序列化时按顺序输出。要不要从旧到新,由下游自己反转,MQL5 侧不掺和。
时间边界还有一个细节:范围重载是含端点的,请求start_time到stop_time,返回的第一根和最后一根可能恰好落在边界上。如果你的业务要求「不含 start_time」,就得在序列化时往数组里加过滤条件,不要依赖 CopyRates 帮你截断。
4.2 CopyTicks 拉历史 Tick:毫秒时间戳的坑
Tick 数据和 K 线不一样,它的时间精度是毫秒,字段里有 time 和 time_msc 两个时间。正常情况 time_msc 是完整毫秒时间戳,但如果终端刚启动、历史 Tick 还没完全从服务器同步完,部分 Tick 的 time_msc 可能为 0,这时候只能从 time 字段取秒再推断毫秒。代码里必须做这个兜底:
long GetTickTimeMs(const MqlTick &tick) { if(tick.time_msc > 0) return tick.time_msc; return (long)tick.time * 1000; }CopyTicks 的 flags 参数也要说清楚。COPY_TICKS_ALL是全部 Tick,COPY_TICKS_TRADE只取成交 Tick,COPY_TICKS_BID只取买价变动。做历史回放和做实时信号,用的 flags 不一样。实战中我一般不用 ALL,因为真实盘 ALL 数据量很大,一次拉几万条还好,拉几百万条 MT5 终端内存直接飙起来。尽量按需拉取,下游要算量价关系就用 TRADE,要算买卖价差就用 BID/ASK。
还有一个限制是单次拉取数量不能超过终端限定的最大值,超了 CopyTicks 返回 -1,而且不是报错,只是结果放不下。规范做法是先探测再分批拉:第一次调用只取 1 条,从返回结果里拿服务器可用数据的边界,再按边界分批次循环拉。
4.3 推送节奏与心跳设计:OnTick 太密,OnTimer 刚好
实时推送的节奏很多人无脑放在 OnTick 里,结果 Tick 密集时段每秒推送几百条,下游处理不过来,Socket 缓冲区直接打爆。我的做法是所有对外推送统一走 OnTimer,频率一般设在 300 到 1000 毫秒。OnTick 只负责更新内存里的最新行情快照,OnTimer 到了才把快照序列化推出去。
void OnTimer() { MqlTick tick; if(SymbolInfoTick(_Symbol, tick)) { string msg = StringFormat("{\"type\":\"tick\",\"symbol\":\"%s\",\"bid\":%s,\"ask\":%s,\"t\":%I64d}\n", _Symbol, DoubleToString(tick.bid, _Digits), DoubleToString(tick.ask, _Digits), tick.time_msc); SendToPeer(msg); } }心跳和业务数据分开处理。心跳固定每 5 秒一条,只带 type 和时间戳;业务数据按实际需求发。下游拿心跳当「MT5 还活着」的信号,连续丢了 N 次心跳就报警,这样可以区分「终端卡死」和「网络中断」。不要把所有事情都塞进一个消息里,出问题的时候很难定位。
另外要注意:OnTimer 的触发间隔在 MT5 里最短是 10 毫秒,但实际稳定性受终端主线程影响很大。如果你的 EA 里还有大量图表对象操作或繁重计算,OnTimer 的实际触发频率会远低于设定值。所以推送代码里不要做重量级序列化,尽量先把 JSON 字符串拼好缓存起来,OnTimer 到了直接发。
5. 避坑排查:DLL 权限、订阅时机与时间边界的五个高频问题
5.1 DLL 导入被拦:日志只说 cannot load library
现象:终端日志出现cannot load library,或者 EA 图标变灰,没有任何业务输出。原因通常是三类:终端设置里没勾选「允许 DLL 导入」;DLL 放错目录;DLL 位数和终端不匹配。还有一类隐蔽原因是被杀毒软件拦截,拷进 Libraries 目录的 DLL 还没加载就被隔离了。解决:先在终端设置里勾选允许导入并重启终端;确认 DLL 在MQL5\Libraries下;64 位终端必须用 64 位 libzmq.dll;杀毒软件加白名单后再放行。排查顺序按这个来,五分钟内能定位。
5.2 JSON 转义翻车:下游多收到反斜杠和 0 字节
现象:Python 端 json.loads 抛Invalid control character,或者解析出来的字符串里带着\,再或者字符串末尾多了一个看不见的\x00。原因:转义顺序写反,以及 StringToCharArray 没有指定长度,把终止符也发过去了。解决:转义时先替换反斜杠再替换双引号;发送前StringToCharArray(msg, bytes, 0, StringLen(msg), CP_UTF8)明确指定长度,不要省略。这类问题属于肉眼极难发现、一查一个准的黑匣子,建议在 MQL5 侧加一个调试日志函数,把最终发送的字节长度打印出来,和接收端收到的字节数对比。
5.3 ZeroMQ 订阅不到消息:PUB 发太快,SUB 还没连上
现象:MT5 端 PUB 已经 bind 并开始发送,Python 端 SUB 启动后一条消息都收不到,等十几分钟还是空的。原因:ZeroMQ 的 PUB/SUB 有慢连接问题,PUB 启动后立刻发送时,SUB 还没完成连接握手,消息直接丢弃。另一个常见原因是 SUB 端没有设置订阅前缀,zmq 默认不订阅任何主题。解决:SUB 端显式调用sub.setsockopt(zmq.SUBSCRIBE, b"")订阅所有主题;MT5 端启动后先延时 3 到 5 秒再开始推数据,或者周期性重发最近一次快照,覆盖掉连接窗口期的丢失。
import zmq ctx = zmq.Context() sub = ctx.socket(zmq.SUB) sub.setsockopt(zmq.SUBSCRIBE, b"") # 空前缀订阅全部 sub.connect("tcp://127.0.0.1:9200") while True: msg = sub.recv_string() print(msg)5.4 历史数据时间边界:首尾 K 线多一根、方向整体反转
现象:请求 2024.01.01 到 2024.01.31 的 H1 K 线,结果返回的第一根是 2023.12.31 23:00,最后一根跑到了 2024.02.01 00:00,或者数组索引 0 变成了最新一根。原因:CopyRates 范围重载包含两端边界,且不同版本对边界 K 线处理有差异;数组方向受 AS_SERIES 标志影响。解决:在序列化时统一过滤rates[i].time >= start_time && rates[i].time <= stop_time,不要相信函数返回的边界;取数后统一ArraySetAsSeries(rates, false)固定方向。还有一个相关坑是服务器时区——MT5 的 K 线时间基于服务器时区,不是本地时区,下游按本地时间对齐数据会造成整体偏移几十小时。
5.5 策略测试器里桥接静默失败:不报错、没数据
现象:同一个 EA 挂到图表上通信正常,放进策略测试器里跑,下游什么都收不到,终端日志也没有任何报错。原因:策略测试器为了隔离环境,网络相关调用会被模拟或者直接静默失败,SocketCreate 返回的值看起来正常,但实际数据发不出去。解决:桥接逻辑必须显式排除测试器环境,在 OnInit 里加一句判断,测试器里直接禁用推送并打印日志。
if(MQLInfoInteger(MQL_TESTER)) { Print("桥接层在策略测试器中禁用"); return INIT_SUCCEEDED; }这类问题最气人的是「看起来一切正常但就是没数据」,不加这层判断,你会在测试器里浪费一整个下午。
6. 进阶验证:用对账任务确认桥接层没有悄悄丢数据
6.1 校验内容与判定标准
桥接层跑起来之后,最难回答的问题是「数据到底有没有丢」。我的做法是加一个定时对账任务:外部程序每隔 5 分钟向 MT5 请求最近 200 根 H1 K 线,拿到之后和本地缓存做一致性校验。校验项不必多,盯住四个指标就够。
| 校验项 | 通过标准 | 失败排查方向 |
|---|---|---|
| 时间连续性 | 相邻 K 线时间间隔等于 3600 秒,无缺口 | 服务器时区、周末跳空、CopyRates 范围遗漏 |
| 数量一致 | 返回 200 根,无缺根 | 时间边界过滤条件写错 |
| 前后衔接 | 最后一根 K 线的 close 与下一根 open 一致 | 推送频率太低,漏了最新未完成 K 线 |
| 数值有效性 | open/high/low/close 满足 h >= max(o,c),l <= min(o,c) | 小数位数写死导致精度丢失 |
6.2 把对账做成常态
不要等出了问题再去查日志,对账任务如果发现不一致,直接输出告警并附上两端数据的差异摘要。比如时间缺口,就把缺口前一根和后一根的时间戳都打出来,定位范围比全量比对快得多。还可以把对账结果写成一个状态文件,下游程序每次启动都先检查这个文件,数据没对齐就不允许进入计算流程。
有一次改了个品种的小数位配置,忘了同步对账脚本里的精度参数,结果 high/low 校验一直失败,我花了不少时间才定位到是 DoubleToString 位数写死的问题。从那以后我每次换品种、换周期、改桥接代码,都强制跑一遍对账任务再交给下游使用。这套桥接层不是写完就结束的东西,校验环节花的时间,远少于线上数据出问题后的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取