MQL5-RPC来自 MQL5 的远程过程调用:针对乐趣及获利的网络服务访问及 XML-RPC 自动交易锦标赛分析程序·进阶篇
(2/3)· 搞懂 XML-RPC 数据模型与 MQL5 实现骨架,才能把外部接口真正接进 EA
◍ 拆解 XML-RPC 回包的双层结构
服务端回包和请求一样分两截:前面是纯文本报头,后面是 XML 有效负荷。报头首行返回 200 即代表服务端认得这次调用,同时必须带 Content-Type: text/xml 与 Content-Length,后者要正好等于后面 XML 的字节数,差一个字节客户端都可能解析失败。 有效负荷的树根固定叫 methodResponse,里面二选一:成功走 params,失败走 fault。params 只允许嵌套一层 param,再里面裹一个 value;也就是说一次成功响应只回一个值,想批量返数据得自己序列化进这个值里。 下面这段实例里,成功响应 Content-Length 标了 124,value 里是 double 类型的 62606001.94;失败响应则把 fault 的 value 写成 string 'No such method!'。注意 XML-RPC 协议本身没统一错误码,报什么错全看对方服务怎么实现,你写客户端时别硬编码别人的英文描述。 开 MT5 用 WebRequest 打自家或第三方 XML-RPC 接口时,先按字节数校验 Content-Length,再判 methodResponse 第一层子节点是 params 还是 fault,能省掉一大半诡异断连的排查时间。
HTTP/class="num">1.1 class="num">200 OK Date: Tue, class="num">08 Nov class="num">2011 class="num">23:class="num">00:class="num">01 GMT Server: Unix Connection: close Content-Type: text/xml Content-Length: class="num">124 <?xml version="class="num">1.0"?> <methodResponse> <params> <param> <value><class="type">class="kw">double>class="num">62606001.94</class="type">class="kw">double></value> </param> </params> </methodResponse> <?xml version="class="num">1.0"?> <methodResponse> <fault> <value><class="type">class="kw">string>No such method!</class="type">class="kw">string></value> </fault> </methodResponse>
在 MT5 里跑起 XML-RPC 客户端
有交易者翻过 Alex Sergeev 关于 WinInet.dll 在终端间传数据的两篇旧文,顺着思路给 MetaTrader 5 从零撸了一个 XML-RPC 客户端。规范吃透后自研实现,不依赖现成封装。 目前这是个滚动迭代的项目,还没把规范全吞下——base64 支持明确待补,但已经能覆盖 XML-RPC 调用的一个大子集。你在 MT5 里发远程调用、跨进程要数据,现在就能直接挂上用。 外汇与贵金属杠杆高、滑点跳空频繁,这类远程调用只解决通道问题,不替你过滤行情风险,实盘前先在模拟盘压一遍延迟。
「把 MQL5 类型塞进 RPC 参数数组」
做 MQL5 远程调用最绕的一步,是给请求参数设计一套能兼容多种基础类型的数据模型。框架侧如果直接暴露裸结构,使用者很容易在类型转换上踩坑,所以这里用 CObject* 指针数组统一收口——凡是继承自 CObject 的数组,都能当作一个 param 挂进请求。 标准库已有 CArrayInt、CArrayDouble、CArrayString,在此之上补了 CArrayBool、CArrayDatetime 覆盖布尔与日期时间,又写了 CArrayMqlRates 承接 K 线结构数组。若某个数组只装了 1 个元素,编码时会降维成 XML 里的单个 value 节点,而不是数组节点。 下面这段脚本实测往 CArrayObj 里塞了 6 类子数组:整数、双精度、字符串、datetime、布尔、MqlRates,各加 4 条样本(datetime 里还混了 TimeCurrent 与 TimeTradeServer 加 3600 秒偏移)。跑完你能在调试里看到 params 指针下挂了 6 个独立数组对象。 CArrayBool 和 CArrayDatetime 直接以 CArrayInt 做底层存储,但 CArrayMqlRates 不能这么偷懒——结构得按引用传递,而且系统没定义 TYPE_MQLRATES 枚举,必须自己处理序列化。所有数组最终都要由 CXMLRPCEncoder 转成 XML 文本才能发出去,这个 helper 类对 bool、string、datetime 分别提供了返回合规 XML 的方法,标记里的 _B 和 _E 后缀只是起止标签,写进头文件是为了让编码过程更透明。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ArrayObjTest.mq5 | class=class="str">"cmt">//| Copyright class="num">2011, Investeo.pl | class=class="str">"cmt">//| http://Investeo.pl | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2011, Investeo.pl" class="macro">#class="kw">property link "http:class=class="str">"cmt">//Investeo.pl" class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include <Arrays\ArrayInt.mqh> class="macro">#include <Arrays\ArrayDouble.mqh> class="macro">#include <Arrays\ArrayString.mqh> class="macro">#include <Arrays\ArrayBool.mqh> class="macro">#include <Arrays\ArrayMqlRates.mqh> class="macro">#include <Arrays\ArrayDatetime.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- CArrayObj* params = new CArrayObj; CArrayInt* arrInt = new CArrayInt; arrInt.Add(class="num">1001); arrInt.Add(class="num">1002); arrInt.Add(class="num">1003); arrInt.Add(class="num">1004); CArrayDouble* arrDouble = new CArrayDouble; arrDouble.Add(class="num">1001.0); arrDouble.Add(class="num">1002.0); arrDouble.Add(class="num">1003.0); arrDouble.Add(class="num">1004.0); CArrayString* arrString = new CArrayString; arrString.Add("s1001.class="num">0"); arrString.Add("s1002.class="num">0"); arrString.Add("s1003.class="num">0"); arrString.Add("s1004.class="num">0"); CArrayDatetime* arrDatetime = new CArrayDatetime; arrDatetime.Add(TimeCurrent()); arrDatetime.Add(TimeTradeServer()+class="num">3600); arrDatetime.Add(TimeCurrent()+class="num">3600*class="num">24); arrDatetime.Add(TimeTradeServer()+class="num">3600*class="num">24*class="num">7); CArrayBool* arrBool = new CArrayBool; arrBool.Add(false); arrBool.Add(true); arrBool.Add(true); arrBool.Add(false); CArrayMqlRates* arrRates = new CArrayMqlRates; class="type">MqlRates rates[];
◍ 把多类型数组塞进一个容器再统一打印
MQL5 的标准库提供了 CArrayObj 这种对象数组容器,可以混装不同基础类型的数组对象。下面这段逻辑先取了最近 4 根 K 线存进 rates,并将序列方向设为从新到旧(ArraySetAsSeries(rates,true)),再把 rates[0]~rates[3] 逐个 Add 进 CArrayMqlRates,同时把 int / double / string / datetime / bool 的数组也各加进一个 CArrayObj,最终 params.Total() 会返回 6,即容器里正好挂了 6 个数组对象。 遍历时用 params.At(p).Type() 拿到每个元素的类型枚举,再 switch 到对应子类指针(如 CArrayInt*、CArrayDouble*)做向下转型,随后按各自格式 PrintFormat。比如 TYPE_DOUBLE 分支用 %f 打出浮点,TYPE_BOOL 分支手动判断 true/false 再输出字符串,TYPE_DATETIME 则用 TimeToString 带日期和分钟格式化。 开 MT5 新建脚本粘入下方代码,跑完看 Experts 日志:应能见到 6 行数组头标识以及各数组内部值。若某类型数组你没预先 Fill,Total() 为 0 就不会打印内部项,这是排查「容器有但内容空」的直接现象。外汇与贵金属杠杆品种请先上模拟盘验证,实盘波动可能引发 CopyRates 返回少于 4 根。
ArraySetAsSeries(rates,true); class="type">int copied=CopyRates(Symbol(),class="num">0,class="num">0,class="num">4,rates); arrRates.Add(rates[class="num">0]); arrRates.Add(rates[class="num">1]); arrRates.Add(rates[class="num">2]); arrRates.Add(rates[class="num">3]); params.Add(arrInt); params.Add(arrDouble); params.Add(arrString); params.Add(arrDatetime); params.Add(arrBool); params.Add(arrRates); Print("params has " + IntegerToString(params.Total()) + " arrays."); for (class="type">int p=class="num">0; p<params.Total(); p++) { class="type">int type = params.At(p).Type(); class="kw">switch (type) { case TYPE_INT: { CArrayInt *arr = params.At(p); for (class="type">int i=class="num">0; i<arr.Total(); i++) PrintFormat("%d %d %d", p, i, arr.At(i)); class="kw">break; } case TYPE_DOUBLE: { CArrayDouble *arr = params.At(p); for (class="type">int i=class="num">0; i<arr.Total(); i++) PrintFormat("%d %d %f", p, i, arr.At(i)); class="kw">break; } case TYPE_STRING: { CArrayString *arr = params.At(p); for (class="type">int i=class="num">0; i<arr.Total(); i++) PrintFormat("%d %d %s", p, i, arr.At(i)); class="kw">break; } case TYPE_BOOL: { CArrayBool *arr = params.At(p); for (class="type">int i=class="num">0; i<arr.Total(); i++) if (arr.At(i) == true) PrintFormat("%d %d true", p, i); else PrintFormat("%d %d false", p, i); class="kw">break; } case TYPE_DATETIME: { CArrayDatetime *arr = params.At(p); for (class="type">int i=class="num">0; i<arr.Total(); i++) PrintFormat("%d %d %s", p, i, TimeToString(arr.At(i), TIME_DATE|TIME_MINUTES)); class="kw">break; } case TYPE_MQLRATES: { class=class="str">"cmt">// CArrayMqlRates *arr = params.At(p);
遍历对象数组时只取首行就跳出
在 MT5 里用 CArrayObj 这类容器装 K 线结构时,常会写循环逐个 PrintFormat 出来核对。下面这段循环从 i=0 跑起,但体内第一圈就 break,实际只dump了第 0 根柱子,用来快速看数组头部的 open/high/low/close 字段是否正常挂上。 for(int i=0; i<arr.Total(); i++) PrintFormat("%d %d %f %f %f %f", p, i, arr.At(i).open, arr.At(i).high, arr.At(i).low, arr.At(i).close); break; } 实跑 EURUSD H1 的日志显示:params 一共挂了 6 个数组,索引 0~3 分别对应整型序列(1001~1004)、浮点序列(1001.0~1004.0)、字符串序列(s1001.0~s1004.0)和时间戳(2011.11.11 23:00 起每小时一跳)。 注意 break 没有包在花括号的 if 里,它只终结 for 的第一轮,所以 p 维度若外层还有循环,每个 p 也只印首行。外汇与贵金属行情高波动,这类快照仅用于结构自检,不代表任何方向判断。
for (class="type">int i=class="num">0; i<arr.Total(); i++) PrintFormat("%d %d %f %f %f %f", p, i, arr.At(i).open, arr.At(i).high, arr.At(i).low, arr.At(i).close); class="kw">break; }
「从日志看对象数组的落地形态」
把一段 EURUSD H1 上的 ArrayObjTest 日志摊开,能直接看到对象数组在运行期吐了什么。前三行是索引 3 的整型字段:3 2 对应 2011.11.12 23:00,3 3 对应 2011.11.18 23:01,说明数组里塞的是带时间戳的 K 线片段而非裸价格。 索引 4 那四行是布尔标记:0=false、1=true、2=true、3=false,等于在告诉你哪些柱体被策略判定为有效信号、哪些被过滤掉。做价格行为的人可以照这个思路,把「pin bar 是否成立」写进对象数组的布尔位,回看时一眼分辨。 索引 5 的四条是 OHLC 实数:例如 5 3 给出 1.375270 / 1.377530 / 1.374360 / 1.374690,高点比开收高出约 22 点,低点低于开盘约 9 点,是一根偏多头的上影测试线。外汇与贵金属杠杆高,这类形态只代表当时概率倾斜,不代表后续必延。 尾部代码引用了 ArrayMqlRates.mqh,用 #include "Array.mqh" 承接动态结构体数组。你开 MT5 把这类头文件挂上,自己 Print 一遍同周期日志,就能确认对象数组和原生 MqlRates 数组在内存布局上的差异。
class="macro">#include "Array.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class CArrayMqlRates. | class=class="str">"cmt">//| Purpose: Class of dynamic array of structs |