☰
MQL5-JSON-API:用Socket+JSON让MT5交易与Python无缝对接
2026/10/18 15:51:43 网站建设 项目流程

简介:MQL5的JSON通信接口是一套面向MetaTrader 5平台开发者的开源工具包,主要用于打通交易终端与外部系统之间的实时数据通道。它围绕市场价格数据(即引号数据)的获取、解析与转发,完整展示了基于JSON格式、Socket套接字以及ZeroMQ底层消息库的API设计思路,适合有一定MQL5编程基础、希望搭建自动化交易或数据服务模块的开发者参考学习。压缩包共含11个文件,整体大小约49KB,其中有多个mqh头文件用于封装JSON解析和错误控制逻辑,两个mq5示例(一个指标、一个智能交易)用于演示请求报价、处理响应等核心流程;同时附有md格式的说明文档、清理脚本以及许可证文件,目录结构清晰,便于按阅读顺序逐层理解。这套资源已被368人学习下载,属于典型的实战型示例代码。通过阅读源码,可以掌握JSON对象的编码与解码方法、套接字连接的建立与维护、ZeroMQ消息模式在MQL5中的具体应用,并能基于自带示例快速改造出自己的实时报价推送模块或历史数据查询工具,从而显著降低MT5生态与外部系统集成时的开发门槛。

1. MQL5-JSON-API 到底解决什么问题:把行情、交易和历史数据变成 JSON 接口

做量化的人迟早会撞上一个尴尬:策略在外部程序里跑,但交易执行和数据源都在 MT5 里,两边隔着一堵墙。MQL5-JSON-API 的核心思路很直接——把 MQL5 端的数据查询、K线拉取、下单改单全部包装成 JSON 格式的请求,通过 TCP Socket 或 ZeroMQ 暴露给外部程序,Python、Node.js、Java 都能用同一套协议消费这个 trading-api。它解决的是外部程序怎么安全可控地驱动 MT5 执行交易、读取历史数据这个老问题。拿到这套资源之后,不需要再手搓通信协议,把封装好的报价与交易层拿来改参数就能用。新手跑通连接即可练手,熟手可以替换消息推送层,换成自己的消息总线。

2. MQL5-JSON-API 架构选型:为什么是 Socket + JSON 而不是 WebRequest

先交代清楚底层选择,后面所有排错才有依据。MQL5 留给开发者的外部通信接口一直很有限,官方的 WebRequest 看似能用,实际做项目会被三条硬约束卡死,这也是 MQL5-JSON-API 没有走官方接口、而是在 MT5 端直接开 Socket 的根本原因。理解了这几条,你才知道什么时候该改配置、什么时候该换通信层。

2.1 WebRequest 的三道坎:白名单、短连接、单向通道

第一道坎是白名单。WebRequest 只能访问在"工具-选项-智能交易系统"里手动登记过的 URL,交易图表上还要勾选"允许 WebRequest"。域名每次变更,哪怕只是端口从 80 改到 8080,都要进配置改一遍、重启终端。对自动化部署来说,这基本等于让运维天天做手工活。

第二道坎是短连接。WebRequest 每次请求都是一开一合,服务端响应完就断开,连接状态不保留。行情推送、服务端主动向 MT5 下发指令这种场景完全做不了。有人用循环 WebRequest 去轮询一个远端命令队列,但一秒钟轮询一次已经是极限,轮询太密会把终端主线程堵住,太疏又损失实时性,两难。

第三道坎是单向通道。MQL5 侧永远只能作为 HTTP 客户端,外部程序没法主动推数据过来。换到交易场景就是这样:外部策略判断出该开仓,可它只能等着 MT5 过来取命令。MQL5-JSON-API 的做法正好相反:MT5 端运行后主动监听本地端口,外部程序作为客户端连接过来,主动权回到外部一侧。实际部署时我一般绑定 127.0.0.1,再让本机的 Python 服务连它。外部程序和 MT5 不在同一台机器时,才绑内网网卡并加一层 token 校验,避免端口裸奔。

2.2 请求与响应模型:id、method、params 三个字段必须定死

协议字段决定后续对接是否省心。我拆过几份 MQL5-JSON-API 的实现,能直接落地的版本,协议上都有一个共同的三段式结构:id、method、params。

id 是外部程序自增生成的请求编号。这一项不能省,因为 MT5 端单线程处理消息,多个请求并发进来时,响应顺序和请求顺序没法保证一致。外部程序拿到响应必须先读 id,才知道对应哪次请求。我习惯用整数序列号,起始 1000,出问题时对着日志按 id 就能串起完整调用链。

method 是动作名,get_rates、get_tick、get_positions、order_send、order_close 是常见路由。method 直接决定 params 的解析分支,相当于 RPC 路由表。命名上建议统一小写加下划线,避免大小写问题成为外部程序的兼容性坑。

params 是参数对象,内容随 method 变化。get_rates 需要 symbol、timeframe、start、count;order_send 需要 symbol、volume、action、type、price、sl、tp。字段类型在文档里写清楚,服务端再做一次类型校验,比靠外部程序自觉要可靠得多。

响应结构同样要统一。我被问得最多的问题是:为什么响应里有时有 result、有时有 error?混乱的根源是两边写协议时没有约定成功和失败的形状。这里建议定死为要么 result 非空、要么 error 非空,不能两个同时给。

{ "id": 1001, "result": { "status": "ok", "rates": [] }, "error": null }

error 字段也不要只放一段人话,最好是一个带 code 和 message 的对象。外部程序可以根据 code 判断是否重试,message 留给人在排查时看。成功的 result 里 status 字段可以省略,直接把 result 本身当作状态即可。

2.3 ZeroMQ 的适用边界:什么时候值得多引一个 DLL

外部程序只有一个、交互都是请求-响应时,纯 TCP Socket 完全够用,代码量最小,部署也最简单。但一旦出现"一个 MT5 对多个外部客户端"或"MT5 要主动往外推数据"这两类需求,手写 Socket 就有点吃力了。连接管理、消息帧边界、断线重连全要自己实现,这也是 MQL5-JSON-API 的 ZeroMQ 版本存在的价值。

用 ZeroMQ 封装时,核心交易链路和数据查询建议走 REQ/REP:外部发请求、MT5 回复,一问一答,语义清晰。实时行情广播走 PUB/SUB:MT5 作为 PUB 端发布 JSON 消息,外部按主题订阅。最常用的主题可以按品种和周期拆,比如 XAUUSD.tick、EURUSD.M1,订阅端想收什么就订阅什么,不需要把全部数据推给所有客户端。

选型上还有一个容易被忽略的成本:ZeroMQ 在 MQL5 里要借助外部 DLL,这会引入运行库依赖和终端"允许 DLL 导入"的开关。如果你的部署环境是托管服务器且不允许加载 DLL,那就只能回归纯 Socket 方案,把多客户端分发写在业务层。我先讲清这个边界,是因为见过不少团队在纯 Socket 能解决的情况下硬上 ZeroMQ,最后反而被 DLL 依赖卡在生产环境外面。

注意:ZeroMQ 版本和纯 Socket 版本在 MQL5-JSON-API 内部是两套不同的接入层,业务层可以复用。迁移成本主要在连接层,协议层不需要动。

3. 实战搭建:从 Socket 初始化到历史数据与交易指令收发

原理说完,直接看怎么把服务端跑起来。MQL5-JSON-API 的代码组织大体分三层:连接层负责 Socket 的创建、监听、读取;协议层负责 JSON 的序列化和解析;业务层把 method 映射到 MQL5 原生调用。下面按这三层拆开。

3.1 MQL5 端 Socket 初始化与消息循环

MQL5 没有异步 Socket,消息循环通常落在定时器里。用 OnInit 创建监听套接字:

int g_listener = INVALID_HANDLE; int OnInit() { g_listener = SocketCreate(); if(g_listener == INVALID_HANDLE) { Print("SocketCreate error: ", GetLastError()); return INIT_FAILED; } // 端口要和外部客户端约定好,建议避开特权端口 if(!SocketBind(g_listener, 5555)) { Print("SocketBind error: ", GetLastError()); return INIT_FAILED; } if(!SocketListen(g_listener)) { Print("SocketListen error: ", GetLastError()); return INIT_FAILED; } EventSetTimer(1); // 每秒触发一次OnTimer,用来accept和读数据 return INIT_SUCCEEDED; }

SocketCreate 返回一个句柄,失败时 GetLastError() 是具体错误码。SocketBind 第二参数是端口号,同一台机器跑多个 EA 实例时端口要做成输入参数,不要硬编码。EventSetTimer(1) 把 OnTimer 设成每秒执行一次,这个频率对非高频消息够用,也不会抢行情线程的时间片。

核心循环放在 OnTimer 里,用 SocketAccept 接受新连接,再循环读取数据,避免一次 SocketReceive 只读到半个 JSON:

void OnTimer() { int client = SocketAccept(g_listener); if(client != INVALID_HANDLE) { Print("Client connected, handle=", client); g_client = client; // 单客户端版本,多客户端见第5章 } if(g_client != INVALID_HANDLE) { ProcessSocketInput(); } } void ProcessSocketInput() { uchar buffer[2048]; uint timeout = 10; int received = SocketReceive(g_client, buffer, sizeof(buffer), timeout); if(received > 0) { string message = CharArrayToString(buffer, 0, received, CP_UTF8); // 这里按method分发到具体处理函数 string response = HandleRequest(message); char response_data[]; int length = StringToCharArray(response, response_data, 0, WHOLE_ARRAY, CP_UTF8) - 1; SocketSend(g_client, response_data, length); } }

SocketReceive 的 timeout 单位是毫秒,返回 0 表示超时,小于 0 才是出错,别把超时当成断开。CharArrayToString 用 CP_UTF8 是因为 JSON 内容里可能带非 ASCII 字符。发送响应时 StringToCharArray 会带上结尾符,减掉 1 避免多出一个 \0,这是很多人卡过的小坑。

但这段代码只处理了单客户端,而且读一次就解析一次,TCP 半包问题仍然存在。放到真实场景中,ProcessSocketInput 需要改进成维护接收缓冲区、累积字节直到遇到换行符再解析。这个处理放到第四章讲,先把主干功能走通。

3.2 历史数据查询:CopyRates 的参数与时间戳处理

外部程序最常调用的方法就是拉历史数据。CopyRates 能把 K 线直接复制到 MqlRates 数组里,但参数细节决定返回质量。

bool HandleGetRates(const string &symbol, string timeframe, datetime start_time, int count, string &response) { ENUM_TIMEFRAMES tf = StringToTimeframe(timeframe); MqlRates rates[]; ArraySetAsSeries(rates, true); // 结束时间取当前报价时间,start_time由外部传字符串转datetime datetime stop_time = TimeCurrent(); int copied = CopyRates(symbol, tf, start_time, stop_time, rates); if(copied <= 0) { response = BuildError(-1, "CopyRates failed"); return false; } // 限制返回条数,防止一次拉太多卡住终端 int use_count = MathMin(count, copied); response = BuildRatesResponse(rates, use_count); return true; }

CopyRates 参数顺序是品种、周期、起始位置、结束位置、输出数组。start_time 是 datetime 类型,外部传进来的是字符串,需要先用 StringToTime 转。用 TimeCurrent() 作为结束时间是"拉最近一段K线"的常见做法,它取的是终端维护的最新报价时间。ArraySetAsSeries(rates, true) 让数组下标 0 对应最新一根K线,外部客户端处理时会少绕一个弯。

序列化时我只保留最小必要字段:time、open、high、low、close、tick_volume。tick_volume 是 tick 成交量,不是真实交易量,部分品种会有 real_volume,但实盘账户经常拿不到,所以默认不给,避免外部程序拿到空字段后产生误判。time 字段直接用 Unix 秒,不带时区信息,时区换算放在外部展示层。

3.3 交易指令下发:JSON 字段和 MqlTradeRequest 的映射

交易接口是整个 API 里最敏感的部分,字段映射必须准确。从 JSON 请求到 MqlTradeRequest,常见字段对照如下。

JSON 字段类型MqlTradeRequest 映射说明
actionstringreq.actiondeal 对应 TRADE_ACTION_DEAL
symbolstringreq.symbol品种名必须精确匹配
typestringreq.typebuy/sell 映射到 ORDER_TYPE_BUY/SELL
volumedoublereq.volume注意最小手数限制
pricedoublereq.price建议显式传,服务端只做差值校验
sl/tpdoublereq.sl / req.tp止损止盈价
magiclongreq.magic用于策略识别交易来源

请求 JSON 示例:

{ "id": 2001, "method": "order_send", "params": { "symbol": "XAUUSD", "action": "deal", "type": "buy", "volume": 0.01, "price": 2045.5, "sl": 2030.0, "tp": 2060.0, "magic": 20250101 } }

MQL5 端收到后填充结构体的核心代码如下:

bool HandleOrderSend(JsonObject &params, string &response) { MqlTradeRequest req = {}; MqlTradeResult res = {}; req.action = TRADE_ACTION_DEAL; req.symbol = params["symbol"]; req.volume = params["volume"].ToDouble(); req.type = params["type"] == "sell" ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; req.price = params["price"].ToDouble(); req.sl = params["sl"].ToDouble(); req.tp = params["tp"].ToDouble(); req.magic = params["magic"].ToLong(); req.deviation = 20; // 滑点容忍度,单位是点数 bool ok = OrderSend(req, res); if(!ok || res.retcode != TRADE_RETCODE_DONE) { response = BuildError(res.retcode, ReturnCodeMessage(res.retcode)); return false; } response = BuildOrderResult(res); return true; }

MqlTradeRequest 必须用 {} 做零初始化,否则残留字段会带来诡异报错。price 字段我特意要求外部显式传参,而不是服务端继续用 Ask/Bid,因为快速行情下服务端现取价很容易和外部策略逻辑不一致。retcode 是交易服务器返回的结果码,OrderSend 返回 true 只代表请求已受理,不能认为已经成交,最终状态以 TRADE_RETCODE_DONE(10009)为准。

如果启用算法交易,成交结果还会通过 OnTradeTransaction 异步通知过来。JSON API 的请求-响应模式通常不会主动推送异步结果,外部程序要做状态确认,就轮询 get_positions 或 get_deals。

4. 避坑指南:MQL5-JSON-API 使用中五个典型故障与排查

下面五条来自实际跑通和修过的部署环境,按"现象-原因-解决"展开。遇到类似症状,可以按顺序排查。

4.1 现象:客户端连接成功但收不到完整响应

外部程序 SocketConnect 返回成功,发送请求后 MT5 日志显示收到了,但响应 JSON 被截断,多请求几次后数据开始错位。

原因:SocketReceive 一次只返回当前可读的字节数,TCP 会把一条消息拆成好几分片。单次读就做解析,必然遇到半包。另一个常见来源是 MQL5 端发送时多带上一个字符串结尾符。

解决:接收端做缓冲拼接,按消息边界切分。实现成本最低的办法是协议约定:每条 JSON 以换行符 \n 结束。服务端循环 SocketReceive,把新数据追进缓冲区,直到末尾出现 \n 才截取解析。发送端记得去掉 StringToCharArray 产生的结尾符。这个约定也是很多 MQL5-JSON-API 默认采用的消息分帧方式。

4.2 现象:拉一年 M1 历史数据时 MT5 终端卡死

外部请求把 start_time 设到一年前,没有限制返回条数,MT5 终端界面直接无响应几十秒。

原因:CopyRates 是同步阻塞调用,MQL5 程序跑在终端主线程上,一次拉几万根K线会占据大量行情处理时间。

解决:服务端对单次请求条数做硬限制,比如最多 5000 根。需要更长历史时,外部客户端分页拉取,以上一批最后一根K线时间作为下一次的 start_time。服务端最好再加一层简单的日期缓存,重复请求直接命中,避免反复打接口。

4.3 现象:外部程序解析出来的K线时间比图表少 8 小时

Python 端打印 K线时间是凌晨 02:00,MT5 图表同一根K线显示 10:00。

原因:CopyRates 返回的 datetime 是 Unix 时间戳,本质是 UTC 时间。MT5 图表按经纪商时区显示,不同平台有时差,两边就对不上。

解决:协议层统一时间基准。API 返回的 time 字段一律用 UTC Unix 秒,外部展示层做时区换算。请求里如果允许传入日期字符串,同样要求 UTC 字符串,并在文档里写明。最怕的是同一个请求里 start_time 用本地时间、响应又是 UTC,对账到凌晨必乱。

4.4 现象:ZeroMQ 版 API 加载失败,客户端连接超时

换到 ZeroMQ 封装版本后,MT5 端似乎正常,外部客户端连接超时,日志里有 DLL 加载失败痕迹。

原因:八成是终端没有开启"允许 DLL 导入",或者 libzmq 的依赖库缺失、dll 位置放错。

解决:先在"工具-选项-智能交易系统"勾选"允许 DLL 导入"。再把 dll 和 MQL5 脚本放在同一发布目录,注意主 dll 的依赖项要一起带过去。部署到新机器时优先选静态编译版本,少背运行库环境债。

4.5 现象:请求体里带特殊字符就解析失败

外部传参数带引号、反斜杠或中文,MT5 端返回 parse error,纯英文数字则正常。

原因:手写 JSON 解析器没有正确处理转义和 UTF-8,一遇到嵌套引号、反斜杠就崩。部分实现为了省事用 StringFind 找大括号做解析,经不起揉搓。

解决:不要自己写 JSON 解析器,用成熟库替换。资源包里一般会带 JSON 解析库,放在 Include 目录正确引用即可。MQL5 端读取 Socket 数据统一用 CP_UTF8,避免中文品种名乱码。我接手任何 MQL5 JSON 资源时,第一件事就是确认解析库版本,把它换成长期验证过的实现。

5. 进阶改造:从单连接升级到多客户端与行情推送

把基础链路跑通之后,单客户端模式在实盘里很快就不够用。策略程序、风控脚本、手工监控面板都要连同一台 MT5,需要把接入层从单连接升级成能区分客户端身份的服务模式。

5.1 多客户端接入:连接句柄按客户端 ID 管理

纯 Socket 实现里,把全局唯一的 g_client 换成结构体数组,每个客户端保存套接字句柄和自己的 id。MQL5 无法直接用自定义类存连接,我一般用两个平行数组管理:

int g_clients[]; string g_client_ids[]; void OnTimer() { // 每轮先接受所有新连接,再逐个处理已有连接 int new_client = SocketAccept(g_listener); if(new_client != INVALID_HANDLE) { int size = ArraySize(g_clients); ArrayResize(g_clients, size + 1); ArrayResize(g_client_ids, size + 1); g_clients[size] = new_client; g_client_ids[size] = "client_" + IntegerToString(size + 1); } for(int i = 0; i < ArraySize(g_clients); i++) { if(g_clients[i] != INVALID_HANDLE) ReadAndDispatch(g_clients[i], g_client_ids[i]); } }

MQL5 数组不能直接删除中间元素,客户端断开后把句柄置为 INVALID_HANDLE,数组长度只增不减。一次交易会话通常也就几个客户端,内存增长量可以接受。真正重要的是连接假死清理:MQL5 端没有内置空闲检测,要靠在协议里约定心跳。外部客户端 10 秒发一次 ping,服务端记录每个客户端上次活跃时间,超过 30 秒没有消息就断开清理。

多客户端还要考虑响应怎么回到正确的连接。每个连接是独立 Socket,请求和响应天然隔离,但派生出的异步推送(成交回报、风控告警)要按客户端订阅项分发,这就要在客户端连接握手阶段登记它的订阅列表,而不是广播给所有人。

5.2 ZeroMQ PUB/SUB 行情推送:快照与增量结合

实时行情是最容易把 MT5 拖垮的地方。每来一个 tick 就序列化一条 JSON 推送,终端主线程根本忙不过来。我的做法是订阅 Tick 后用缓冲区聚合成批,固定每 200 毫秒推一批增量:

void OnTick() { MqlTick tick; SymbolInfoTick(_Symbol, tick); // 先把tick攒到批量缓冲区,攒满100条或满200ms再推 g_tick_buffer[g_tick_count++] = tick; if(GetTickCount() - g_last_push_ts >= 200 || g_tick_count >= 100) { PushTickBatch(); } }

外部客户端第一次订阅时,先用 get_tick 拿一次当前快照,之后接收增量自行拼接出连续状态。推送消息的 JSON 只带订阅主题和价格变化,比如快照消息:

{ "topic": "XAUUSD.tick", "type": "snapshot", "data": { "time": 1700000000, "price": 2045.5, "volume": 100 } }

增量消息只带变化字段:

{ "topic": "XAUUSD.tick", "type": "delta", "data": { "time": 1700000005, "price": 2045.6 } }

频率上,200 毫秒一批对大多数策略足够,也能把消息量控制在每秒几十条以内,不会堵塞外部程序的消息队列。PUB/SUB 下 ZeroMQ 自己会做断线重连和消息积压,但积压不是无限容忍的。外部客户端消费不过来时,内存会持续膨胀,要在服务端设定发送缓冲区上限,超阈值时丢弃最旧的批量消息。

5.3 心跳与安全校验:长连接最容易忽略的一层

长连接跑起来后的隐患是连接假死。外部程序崩溃后 TCP 连接可能还开着,MQL5 端不知道,反复写入后句柄就开始报错。我在协议里固定增加一组心跳方法:外部每 10 秒发一次 ping,服务端回 pong,同时记录每个客户端最后一次收到消息的时间。超过 30 秒无消息的客户端直接断开。对多客户端数组遍历时,标记为超时的连接立即释放句柄。

安全校验要前置。MQL5-JSON-API 暴露的是交易能力,如果机器被扫描到 5555 端口又没校验,等于把下单权限送人。我采用 init 握手:外部连接建立后,第一条消息必须是 auth 方法并带 token,验证通过才把连接标记为可用,其余 method 一律返回 auth error。token 放在外部配置文件中,不写进代码仓库。

6. 一个验证技巧:用 Python 脚本对整套 API 做端到端检查

接入这套 MQL5-JSON-API 之后,第一件事不是写业务代码,而是用一个小脚本把整条链路从头到尾验证一遍,确认连接、请求、解析、交易通道都通。我习惯把这个脚本保存成 verify_api.py,每次改完 MQL5 端代码就跑一遍,作为回归测试的基础。

脚本逻辑非常简单:建立 Socket,发送一个 get_tick 请求,断言返回的 JSON 里 id 和请求一致、error 为 null,再检查结果里包含 symbol 和 price 字段。如果服务端配置了 init 握手,先发送 auth 再发业务请求:

import json import socket def rpc(sock, method_id, method, params): ### 构造JSON请求,末尾加\n作为消息边界 request = { "id": method_id, "method": method, "params": params } sock.sendall((json.dumps(request) + "\n").encode("utf-8")) # 循环接收,直到收到完整的一行 response = "" while not response.endswith("\n"): response += sock.recv(4096).decode("utf-8") return json.loads(response.strip()) sock = socket.create_connection(("127.0.0.1", 5555), timeout=5) # 先握手,再查实时报价 print(rpc(sock, 1, "auth", {"token": "test_token"})) result = rpc(sock, 2, "get_tick", {"symbol": "XAUUSD"}) print(result) sock.close()

发送方在 JSON 后追加换行符,和服务端约定的消息边界保持一致;接收端循环读,直到缓冲区以换行结尾。拿到结果后,重点核对三件事:id 是否原样返回、error 是否为 null、result 内字段名是否和文档一致。这个脚本可以继续扩展成回归测试,把 get_rates、get_positions、order_send 依次执行,把每步的响应打印出来。我第一次用类似脚本接入时,发现 token 流程就占了一半问题,先握手再请求的逻辑一旦跑通,后续隐患少了很多。

order_send 的验证一定要在模拟账号上跑,把价格、手数、品种核对无误后再切真实账户。就算这样,首次实盘我仍然会挂只读监控,不放开自动交易权限,跑一个完整交易时段,确认返回码和持仓变化一致。

从那以后,我每次部署 MQL5-JSON-API 到新环境,都强制走一遍这个端到端脚本,先验证再谈业务功能。这套资源本身把连接、协议、交易封装都做好了,但环境差异永远存在,验证脚本就是环境差异的照妖镜。拿到资源包以后,先从这一段开始,希望这个习惯能帮你在接入时省下几个通宵排查的时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询