简介MQL5-JSON-API是一套基于MQL5语言的JSON接口实现面向MetaTrader 5平台的中高级开发者用于解决MT5与外部系统行情源、交易服务器或自研工具之间的报价及历史数据交换问题。资源以zip压缩包发布共11个文件、约49KB包含mqh头文件、mq5示例程序、md文档与clean.sh清理脚本等其中json.mqh与JsonAPI.mq5构成核心通信与解析逻辑另附错误控制、字符串枚举转换等辅助模块README和changelog提供使用说明与更新记录结构紧凑适合快速集成到MT5脚本或EA中。已有368人学习下载。通过这套接口可了解JSON编解码、套接字通信、ZeroMQ消息模式在MQL5中的落地方式并借助现成代码获取实时bid/ask、历史K线数据对涉及实时报价与历史数据回测的交易场景可直接替换或调用这些封装函数。同时项目按Experts、Indicators、Include组织目录适合作为量化交易开发的基础工程。1. 把 MT5 变成可编程的 JSON 服务MQL5-JSON-API 到底是什么做量化或者写自动化脚本的人迟早会遇到一个尴尬场景策略逻辑跑在 Python 或 Node 里但报价和下单都在某个交易终端里两边像隔着一条河。MQL5-JSON-API 就是在这种场景下被反复提到的一个桥接层它在 MetaTrader 5 里跑一个 MQL5 编写的服务端通过 ZeroMQ sockets 对外开放 JSON 格式的接口让外部程序可以拉实时报价、抓历史数据甚至下发交易动作。换句话说你的脚本不再需要去解析终端日志也不用依赖人工导出的 CSV直接把交易终端当做一个本地服务来调用。这套东西适合两类人一类是刚刚开始把策略外置、不想写一堆文件交换逻辑的新手另一类是已经在用外部语言做分析和风控、需要一个稳定数据通道的老手。它的价值不在于单个接口多强大而在于把“交易终端”和“程序世界”之间的边界用 JSON 统一掉了。读完这篇文章你能搞清它的架构、能复现一个数据请求和下单的最小闭环也能避开那些文档里几乎不会写的坑。2. 先搞清架构ZeroMQ socket 与 JSON 请求-响应的三条路径2.1 为什么桥接层选 ZeroMQ 而不是 REST我最早接触这类方案时第一反应是为什么不在 MT5 里直接开一个 HTTP 服务后来试过才发现MetaTrader 5 的脚本环境本身不擅长做长连接和并发监听用内置函数硬写 HTTP 解析既脆弱又难以调试。而 ZeroMQ 是一个轻量消息库它不要求你架设一个完整的 Web 服务只需要绑定一个端口然后按“请求-应答”模式收发消息。这个思路和交易终端的运行方式很匹配EA 本身就在一个事件循环里跑收到一条 socket 消息解析、处理、回一条 JSON开销很小。另一个选它的理由是跨语言。ZeroMQ 的客户端库在 Python、JavaScript、C# 里都有你不需要为了接入这套 API 而去学一套专属的 IPC 协议。MQL5 这一侧只需要把收到的字节流按 JSON 解析再把结果序列化回去。整个握手、心跳、数据请求、指令下发本质上都在同一个 socket 连接上完成区别只是消息体里的 “action” 字段不同。网上流传的这类资源包通常会把核心逻辑拆成几个部分一个负责 socket 监听和消息分发的主模块一个负责 JSON 编解码的辅助模块再加上一组封装好的接口函数。你拿到的 .mqh 头文件就是拿来 include 的.mq5 文件则是可以直接编译成 EA 或脚本的入口。我一般会把它们放在一个统一目录下而不是散落在终端的默认文件夹里后面编译和升级都省事。2.2 把库拖进 MetaEditor文件布局、include 与编译拿到资源包第一步不是急着看代码而是把文件放进正确的位置。MetaEditor 的 include 搜索路径是 MQL5\Include而可编译的 EA 要在 MQL5\Experts 下脚本在 MQL5\Scripts 下。每个版本略有差异但结构大致不变。我习惯先建一个项目文件夹比如 json_api_server把 .mqh 和 .mq5 放一起然后在主文件里用相对路径 include这样整包移动也不会断。典型的主文件开头长这样#include json_api/JsonParser.mqh #include json_api/ZeroMQSender.mqh input string bind_address tcp://127.0.0.1:5555; input int max_requests 1000; CJsonParser jsonParser; CZeroMQSender zmqSender; int OnInit() { if(!zmqSender.Init(bind_address)) { Print(ZeroMQ init failed at , bind_address); return INIT_FAILED; } Print(MQL5-JSON-API listening on , bind_address); return INIT_SUCCEEDED; }这里有两个关键设计一是把 bind_address 做成 input 参数这样不用改代码就能切换端口二是把初始化结果用返回值上报给终端初始化失败时 EA 不会继续运行。很多翻车现场就是漏了这一步日志里什么都没有等调试半天才发现是地址绑定失败。Init 返回 INIT_FAILED 时终端的“自动交易”按钮旁边会给出提示这是第一时间定位问题的最快路径。编译通过之后把 EA 拖到图表上或者用策略测试器跑起来日志里能看到监听地址。这一步的意义是确认 ZeroMQ 库已经正确加载。如果日志里出现 DLL 相关的报错说明终端限制了外部库调用这个在第 5 章会专门展开。2.3 握手与心跳确认管道通没通服务端起来之后第一件要验证的事是客户端和服务端能不能正常对话。与其上来就拉行情我一般先发一个最简的 ping 请求确认整条链路通。请求体保持最小的 JSON 结构{action: ping, id: 1}正常的返回应该是类似这样的{ok: true, id: 1, msg: pong}在 Python 端对应的发送代码是这样import zmq, json context zmq.Context() socket context.socket(zmq.REQ) socket.connect(tcp://127.0.0.1:5555) socket.send_json({action: ping, id: 1}) reply socket.recv_json() print(reply)注意 REQ-REP 模式有一个硬性约束必须先 send 再 recv顺序不能乱。如果你在同一个 socket 上连续 send 两次而没有 recvZeroMQ 会直接报错并关闭连接。这是新手最常见的第一个坎。connect 用 127.0.0.1 表示本机回环服务端和客户端都在同一台机器时不要用局域网 IP免得引入防火墙干扰。send_json 和 recv_json 是 PyZMQ 提供的便捷方法底层自动完成了 JSON 序列化省去手动 dumps 和 loads。拿到 “pong” 之后管道就算通了接下来可以放心地去测真实接口。3. 取行情与历史数据接口、参数与数据格式解析3.1 实时报价与 Symbol 参数行情接口是这类 API 里调用频率最高的。常见的设计是客户端发一个订阅或查询请求服务端返回当前报价快照。请求里至少要包含 symbol 参数比如{action: quote, symbol: EURUSD, id: 2}返回数据大概长这样{ ok: true, id: 2, symbol: EURUSD, bid: 1.08543, ask: 1.08546, time: 1720000000 }在封装的 .mqh 实现里这个动作通常会走一遍这样的逻辑解析 symbol 字符串将其转换成终端内部的枚举或字符串格式然后调用 MarketInfo 系列函数取 Bid、Ask 和最后更新时间。这里值得注意的一点是symbol 的写法必须和终端“市场报价”窗口里的完全一致。EURUSD 写成 eurusd 或 EURUSD.f 都可能在 MarketInfo 里拿不到值返回 0。这个看似简单的问题实际调试时常让人一头雾水。另一个容易踩的细节是返回的 time 字段。MQL5 里时间戳默认是服务器时间而不是本地时区。如果你的客户端脚本要拿这个时间和本地时间做对比记得处理时区偏移。我自己习惯在 API 层统一返回 UTC 时间戳偏移逻辑全部放在客户端做避免两边各偏一次。3.2 拉历史 K 线的请求结构与时间帧映射历史数据的请求比实时报价复杂一些因为要从终端的数据缓冲区里拷贝数组并做转换。一个常见的历史数据请求长这样{ action: history, symbol: EURUSD, timeframe: M15, start: 2024-01-01, end: 2024-02-01, limit: 5000 }timeframe 字段在客户端和 MQL5 之间需要做一次映射。M1、M5、M15、H1、D1 这些字符串要对应到终端内部的周期枚举值。资源包一般会在转换函数里维护一个静态字典把字符串映射成枚举。这里最容易出问题的是 “M” 和 “H” 的大小写有的实现要求大写有的要求首字母大写不一致时会返回空数组。服务端处理逻辑分三步先按时间范围计算需要的柱子数量再调用 CopyRates 把数据填充进数组最后遍历数组逐条转成 JSON 对象。如果 start 和 end 跨度太大一次拉十几万根 K 线CopyRates 会非常慢而且可能超出数组内存限制。所以我在设计请求参数时一定会带上 limit作为保护阀值。超过限定额度的请求直接返回错误码而不是硬拉。3.3 返回数据怎么用Python 端解包示例拿到历史数据之后外部程序要做的事情通常是转成 DataFrame 或者直接落库。这里给一个 Python 端的完整示例包括发送请求和解析返回import zmq, json def fetch_history(symbol, timeframe, start, end): context zmq.Context() socket context.socket(zmq.REQ) socket.setsockopt(zmq.RCVTIMEO, 5000) socket.setsockopt(zmq.SNDTIMEO, 5000) socket.connect(tcp://127.0.0.1:5555) req { action: history, symbol: symbol, timeframe: timeframe, start: start, end: end, limit: 5000 } socket.send_json(req) reply socket.recv_json() socket.close() return reply reply fetch_history(EURUSD, M15, 2024-01-01, 2024-02-01) bars reply.get(data, []) print(fgot {len(bars)} bars) if bars: print(bars[-1])这段代码里有几个容易忽视的细节RCVTIMEO 和 SNDTIMEO 设置成 5 秒避免服务端卡住时客户端无限等下去。这是实际运行中非常重要的习惯因为 MQL5 端在极端行情下可能忙于处理其他请求没有及时应答客户端如果没有超时就表现为“假死”。每次请求单独建一个 socket 并立刻关闭是为了避免 REQ 模式下的状态错乱如果复用同一个 socket 而中途有一次超时后续请求大概率会全部失败。数据量大的请求回复可能接近 1MBZeroMQ 默认能处理但建议在客户端把接收缓冲区调大一些比如 setsockopt(zmq.RCVHWM, 10000)。4. 把交易动作暴露成 JSON下单、改单、平仓与仓位查询4.1 订单请求的参数表与必填项比拉数据更进一步的是把交易动作也变成 JSON 请求。资源的交易接口通常设计成一套通用的 order_send 动作把所有下单参数塞进一个 JSON 对象里服务端解析后映射到 MQL5 的订单结构。一个典型的下单请求{ action: order_send, id: 100, symbol: EURUSD, type: buy, volume: 0.1, sl: 1.08000, tp: 1.09000, comment: from python }对应到 MQL5 端服务端要填充一个订单结构体。这里最核心的一个认知是MQL5 的订单系统里没有“平仓”这个原子操作它只有“反向开仓”的概念。你在客户端发一个 sell服务端如果当前仓位方向相反且数量相等交易所会把它当成平仓处理但底层逻辑仍然是开一笔反向单。理解了这一点你写的客户端代码就不会迷惑于“为什么平仓之后还多了一个仓位记录”。4.2 封装 trade_passthrough把 JSON 映射到订单对象服务端收到订单请求后典型的处理函数是这样bool ProcessOrderSend(CObject* obj) { string symbol obj.GetString(symbol); string type obj.GetString(type); double volume obj.GetDouble(volume); double sl obj.GetDouble(sl); double tp obj.GetDouble(tp); MqlTradeRequest request {}; request.action TRADE_ACTION_DEAL; request.symbol symbol; request.volume volume; request.sl sl; request.tp tp; request.type (type buy) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; request.deviation 20; request.magic 123456; MqlTradeResult result {}; if(!OrderSend(request, result)) { return BuildErrorResponse(result.retcode); } return BuildSuccessResponse(result.order); }注意这里的 request 结构体用了大括号初始化把所有字段先清零这是 MQL5 里避免脏数据的标准做法。任何结构体字段没有被显式赋值都可能带入上一次的值。另一个关键字段是 deviation它表示允许的滑点范围单位是点数。如果你的策略对滑点敏感这个值不要设 0否则在高波动行情下订单会频繁被拒。前一个版本我还犯过一个错误忘记设置 magic。结果所有从 API 进来的订单都归到默认魔法数 0 下面和手动下单混在一起做风控对账时完全分不清哪个是程序单。后来我把 magic 写成可配置参数并且单独用一个数字段区分不同的策略实例这个习惯一直保留到现在。4.3 同步结果回执与常见字段解读OrderSend 是同步调用函数返回时结果已经确定。服务端需要把结果转发回客户端。常见的回执结构{ ok: true, id: 100, order_id: 58001234, retcode: 10009 }retcode 是 MQL5 的交易服务器返回码10009 表示订单已成交10008 表示请求已受理但可能未成交10004 表示请求被拒绝。很多人在客户端只判断 ok 字段不看 retcode导致“订单没成交但脚本认为成功”的情况。我一般在客户端做双重判断ok 为真且 retcode 为期望值时才认为动作有效。这里要特别强调一个交易动作的边界这类 API 暴露的接口本质上是把终端本地的交易能力透传出去服务端本身不校验风控。也就是说外部程序可以随时下任何数量的单。如果你是拿来做自动化一定在客户端这一侧做好仓位上限、单笔风险、最大下单频率的控制不要把风控逻辑寄托在桥接层上。这不是资源的问题而是所有这类透明通道的共同边界。5. 避坑与排查五个让桥接层翻车的细节5.1 坑一DLL 调用权限导致的静默失败现象EA 编译通过日志里显示初始化成功但客户端一连接就超时或者完全没有任何响应。原因MQL5 桥接层几乎都要依赖外部库来实现 ZeroMQ socket。MetaTrader 终端默认禁止 EA 调用 DLL需要在“工具 — 选项 — EA 交易”里勾选“允许 DLL 导入”并且该 EA 自身的属性里也要开启允许。很多资源包自述文件里提了这一点但实际部署时总有人漏掉。解决检查两个开关终端的全局设置以及 EA 属性对话框里“允许 DLL 导入”的勾选。改完之后重启终端或重新加载 EA。这个坑的特点是没有报错提示就是纯超时排查起来最迷惑。5.2 坑二端口被占用或 bind 地址写死现象第一次启动正常第二次启动时日志提示 bind 失败或者换了机器之后无论如何连不上。原因资源包默认监听 5555 端口如果你的其他程序占用该端口或者绑定的地址是一个不存在的回环接口初始化就会失败。有的版本把 bind_address 写死在代码里而不是 input 参数导致换机器后必须改代码重新编译。解决优先选择把 bind_address 作为 input 参数的版本并且在启动前检查端口占用。在 Linux 或 Windows 下可以用命令行快速检查端口是否被占确认可用后再启动。我通常习惯用一个专门的高位端口比如 17017降低被随机程序占用的概率。5.3 坑三历史数据时间戳与服务器时区偏移现象从接口拉到的 K 线开盘时间和图表上看到的不一致甚至整段数据看起来偏移了几个小时。原因终端的时间基准是经纪商服务器时区通常是 UTC2 或 UTC3而你客户端默认按本地时区解析时间戳于是产生了偏移。更隐蔽的是不同品种的交易时段不一样黄金和原油的 K 线起点时间和外汇不同按同一套偏移去校准会出错。解决在请求历史数据时明确约定返回时间戳的时区语义。最稳妥的做法是服务端统一返回 UTC 秒级时间戳客户端拿到后自行转换。同时对比 K 线数据时不要用某一个品种的经验偏移去套所有品种。5.4 坑四同步阻塞把 EA 的策略循环卡死现象当数据请求或订单回执频繁时EA 的 tick 处理明显变慢图表卡顿或者出现主循环不响应。原因socket 的 recv 操作是同步阻塞的如果在事件处理函数里长时间等待客户端消息会阻碍新的 tick 事件处理。订单回执如果客户端迟迟不接收服务端的发送操作也会被阻塞。解决不要在主图表 EA 里做长时间的同步 recv。把 socket 收发放到独立的线程中执行或者在主循环里每次只尝试接收一条消息处理完立刻返回。如果你拿到的资源包没有做线程隔离实际部署时就要注意调用频率不要让外部程序发大量并发请求。5.5 坑五JSON 浮点精度与科学计数法现象客户端收到的价格从 1.08550 变成了 1.0855或者大额浮点数被显示成科学计数法导致后续计算产生微小但烦人的误差。原因JSON 的标准实现里浮点数没有固定精度表达序列化端默认会省略末尾的 0并且对于极大或极小的数值可能切换成科学计数法。解决在客户端解析时把价格统一视为浮点类型不要用字符串直接拼接生成订单价格。在下单请求时价格字段做一次显式的 round 处理保留到产品的最小报价精度。这个精度值可以在初始化时用 SymbolInfoDouble 查询得到不要硬编码成 4 位或 5 位小数。6. 进阶把它做成本地行情与交易网关当数据请求和交易动作都跑通之后我通常会把这一套桥接层再封装一层变成一个常驻的本地网关进程。这个进程负责维护与 MT5 的长连接对外提供更稳定的接口同时把验证、超时、重试的逻辑集中到一个地方。import zmq, json, time class MT5Gateway: def __init__(self, endpointtcp://127.0.0.1:5555): self.endpoint endpoint self.context zmq.Context() def call(self, action, **params): socket self.context.socket(zmq.REQ) socket.setsockopt(zmq.RCVTIMEO, 10000) socket.setsockopt(zmq.SNDTIMEO, 10000) socket.connect(self.endpoint) request {action: action, id: int(time.time() * 1000)} request.update(params) socket.send_json(request) reply socket.recv_json() socket.close() if not reply.get(ok, False): raise Exception(reply.get(msg, unknown error)) return reply gateway MT5Gateway() for symbol in [EURUSD, GBPUSD, XAUUSD]: try: data gateway.call(history, symbolsymbol, timeframeH1, start2024-01-01, end2024-03-01, limit2000) print(symbol, len(data.get(data, [])), bars loaded) except Exception as e: print(symbol, failed:, e)这个网关的生命周期管理是有讲究的。每次调用都新建 socket、用后即关可以避免 REQ 模式下因为一次超时导致的永久状态错乱代价是多几次握手开销但对于本地低频请求完全够用。如果你要高频轮询报价可以改为常驻连接并设计心跳重连机制。资源包本身通常只提供 MQL5 端的能力外部进程的稳定性全靠你自己把握。我用这个模式跑了很长时间最大的教训就是一定要给所有请求加超时而且要做重试不然凌晨数据断了一次策略就可能基于不完整的行情跑几个小时。从那以后我每次上新的品种或策略前都会强制走一遍全品种批量拉数、核对数据条数的流程确认没有缺口再上线。这个习惯帮我排查掉了不少潜在问题希望帮到你。本文还有配套的精品资源点击获取