
简介面向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。选型可以按这张表来方案方向依赖多客户端断线恢复适用场景内置 SocketMT5 连外部无需自行实现需自行实现单机、原型、自用ZeroMQ DLL双向libzmq.dll原生支持自动多订阅方、跨机、生产WebRequestMT5 发 HTTP无服务端决定HTTP 层面低频拉取、REST 接口整体数据流推荐这样设计Python 端起 TCP 服务端或 SUB 订阅端MT5 端 OnTimer 每 500ms 推一次「最新 K 线 最新 tick」的 JSON外部程序若要历史数据走 REQ/REP 或另一个 TCP 连接发请求MT5 收到后调 CopyRates/CopyTicks 取数再序列化成 JSON 回发。实时推送和历史拉取最好不要走同一条连接不然推送数据量一大响应会被挤掉。3. JSON 封装与解析手写转义还是上库决定你会不会踩转义坑3.1 为什么不用 StringFormat 直接拼 JSONMQL5 没有原生的 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是全部 TickCOPY_TICKS_TRADE只取成交 TickCOPY_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 位数写死的问题。从那以后我每次换品种、换周期、改桥接代码都强制跑一遍对账任务再交给下游使用。这套桥接层不是写完就结束的东西校验环节花的时间远少于线上数据出问题后的排查时间。希望帮到你。本文还有配套的精品资源点击获取