MQL5-RPC来自 MQL5 的远程过程调用:针对乐趣及获利的网络服务访问及 XML-RPC 自动交易锦标赛分析程序·进阶篇
🔌

MQL5-RPC来自 MQL5 的远程过程调用:针对乐趣及获利的网络服务访问及 XML-RPC 自动交易锦标赛分析程序·进阶篇

(2/3)· 搞懂 XML-RPC 数据模型与 MQL5 实现骨架,才能把外部接口真正接进 EA

偏理论 第 2/3 篇
很多交易者把 EA 困在终端内,忽略了对接外部网络服务可拿到盘面外的统计维度。XML-RPC 作为轻量协议,能让 MQL5 直接调用远程方法,但类型映射错一处就全段失效。本文承接基础定位,聚焦数据模型与框架内部构造。

◍ 拆解 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,能省掉一大半诡异断连的排查时间。

MQL5 / C++
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 后缀只是起止标签,写进头文件是为了让编码过程更透明。

MQL5 / C++
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 根。

MQL5 / C++
  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 也只印首行。外汇与贵金属行情高波动,这类快照仅用于结构自检,不代表任何方向判断。

MQL5 / C++
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 数组在内存布局上的差异。

MQL5 / C++
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                        |
把接口诊断交给小布盯盘
这些协议层的字段映射与调用异常,小布盯盘的 AIGC 已内置核对逻辑,打开对应品种页即可看到调用状态,你只需判断信号是否有效。

常见问题

多用于传输二进制或需转义的序列化数据,例如远程返回的压缩统计包,解码后可由 MQL5 结构解析。
可以,小布盯盘内置了接口层诊断,能提示超时与类型不匹配,减少你手动查日志的时间。
它统一了远程服务器回传的成交与排名时间戳,避免本地时区错位导致统计偏移。
array 可装载多个 struct,每个 struct 以名称-值对表达一条记录,适合批量回传锦标赛选手成绩。