MQL5-RPC来自 MQL5 的远程过程调用:针对乐趣及获利的网络服务访问及 XML-RPC 自动交易锦标赛分析程序·综合运用
(3/3)·从 XML-RPC 协议栈到 2011 自动交易锦标赛解析客户端的完整落地路径
- 把K线序列装进可复用容器
- CArrayMqlRates 的构造与尾部追加逻辑
- 把K线与布尔值塞进XML-RPC报文
- 把 MT5 时间压成 XML-RPC 能吃的字符串
- XML-RPC 报文标签的宏封装
- 用 CXMLRPCQuery 把 MQL5 数组拼成 XML-RPC 报文
- 按类型序列化参数数组的写法
- 用 XML-RPC 把多类型参数打包发出去
- XML 里塞双精度数组的写法
- XML 参数里的整型数组怎么塞
- 参数文件里的数组与布尔值写法
- XML-RPC 参数里的布尔与时间的写法
- K线节点里的浮点精度陷阱
- K线节点在XML里的字段落地
- 从 XML 快照看 tick_volume 的真实精度
- 真实成交量与点差的原始字段长相
- 从 XML 片段看单根 K 线的数据结构
- 从 XML 片段看单根 K 线的字段结构
- 从 XML 快照看 tick_volume 的真实含义
- Tick 结构里的真实成交量与点差字段
- K线结构在缓存文件里的真实落点
- 从 XML 快照抠出 K 线字段
- 从 XML 切片看 tick 数据怎么落盘
- tick 结构里的真实成交量与点差字段
- XML-RPC 参数收尾的闭合结构
- 把 XML-RPC 回包拆成 MQL5 变量
- 按类型把 XML 参数拆进对应的 MQL5 数组
- 按类型分支回填 MQL 数组
- 从 XML 返回值里抠出 MQL 数组
- 按类型拼装结果字符串的分支处理
- 正文
- 用 XML-RPC 数组塞入多类型参数
- XML-RPC 返回结果的本地解析验证
- RPC 代理类的连接与调用实现
- XML-RPC 请求发送与响应回收的实现细节
- 用 MT5 直连汇率接口做币种换算
- XML-RPC 账户余额查询的往返报文
- XML-RPC 回传里的缓存标记
- 把 2011 自动锦标赛数据拉进 MT5 的思路
- 用 Python 拉取锦标赛仓位做交叉验证
- 用 Python 扒冠军账户持仓做镜像参考
- 用 XML-RPC 扒出选手持仓的净向分布
- 用 MQL5-RPC 拉取 ATC2011 锦标赛数据
- XML-RPC 返回的登录名清单长这样
- XML 节点里的用户名清单怎么读
- EA 授权名单的 XML 结构长这样
- 配置文件里的用户名与整数类型字段
- 信号订阅者的字段结构
- XML-RPC 回传里的用户标识清单
- EURUSD 指标参数的 RPC 返回结构
- 从 RPC 回传看参赛账户的真实持仓分布
- 框架开源与后续玩法
把K线序列装进可复用容器
在 MT5 里直接操作 MqlRates 数组做策略回测或实时计算,往往要反复写内存管理和边界判断。把这类结构包一层类,能让后续调用 CopyRates 后的数据管理更干净。 下面这段定义用一个宏 TYPE_MQLRATES 等于 7654 来标记对象类型,继承自 CArray,内部用 m_data[] 存 MqlRates 棒线数据。外层只需关心 Add、At、Update 等语义方法,不必手动管数组越界。 Save 和 Load 接收文件句柄做序列化,Reserve 可预分配内存避免频繁 realloc。做多周期扫描时,先用 Reserve(1000) 再 AddArray 灌入日线,CPU 占用倾向更平稳。外汇与贵金属波动剧烈,此类容器仅解决数据结构问题,不预示任何方向。
class="macro">#define TYPE_MQLRATES class="num">7654 class CArrayMqlRates : class="kw">public CArray { class="kw">protected: class="type">MqlRates m_data[]; class=class="str">"cmt">// data array class="kw">public: CArrayMqlRates(); ~CArrayMqlRates(); class=class="str">"cmt">//--- method of identifying the object class="kw">virtual class="type">int Type() const { class="kw">return(TYPE_MQLRATES); } class=class="str">"cmt">//--- methods for working with files class="kw">virtual class="type">bool Save(class="type">int file_handle); class="kw">virtual class="type">bool Load(class="type">int file_handle); class=class="str">"cmt">//--- methods of managing dynamic memory class="type">bool Reserve(class="type">int size); class="type">bool Resize(class="type">int size); class="type">bool Shutdown(); class=class="str">"cmt">//--- methods of filling the array class="type">bool Add(class="type">MqlRates& element); class="type">bool AddArray(const class="type">MqlRates &src[]); class="type">bool AddArray(const CArrayMqlRates *src); class="type">bool Insert(class="type">MqlRates& element,class="type">int pos); class="type">bool InsertArray(const class="type">MqlRates &src[],class="type">int pos); class="type">bool InsertArray(const CArrayMqlRates *src,class="type">int pos); class="type">bool AssignArray(const class="type">MqlRates &src[]); class="type">bool AssignArray(const CArrayMqlRates *src); class=class="str">"cmt">//--- method of access to the array class="type">MqlRates At(class="type">int index) const; class=class="str">"cmt">//--- methods of changing class="type">bool Update(class="type">int index,class="type">MqlRates& element); class="type">bool Shift(class="type">int index,class="type">int shift); class=class="str">"cmt">//--- methods of deleting class="type">bool Delete(class="type">int index); class="type">bool DeleteRange(class="type">int from,class="type">int to); class="kw">protected:
「CArrayMqlRates 的构造与尾部追加逻辑」
在 MT5 自定义类里封装 MqlRates 数组时,构造函数只做一件事:用 ArraySize(m_data) 把受保护成员 m_data_max 初始化为静态数组的容量。若 m_data 是长度为 0 的空数组,m_data_max 就是 0,后续所有 Reserve 调用都得靠动态申请内存。 析构函数判断 m_data_max!=0 才走 Shutdown(),等于给无内存占用的实例留了道短路,避免重复释放。实际写 EA 时若你用的是文件级静态 m_data,这一行能省掉不少调试时的访问违规。 Add 方法先 Reserve(1) 保底预留一个槽位,失败直接返回 false;成功才执行 m_data[m_data_total++]=element,并把 m_sort_mode 置为 -1 标记数组已无序。这意味着每追加一根 K 线数据,排序状态就失效,后续若调用二分查找必须先重排。
class="type">int MemMove(class="type">int dest,class="type">int src,class="type">int count); }; class="type">void CArrayMqlRates::CArrayMqlRates() { m_data_max=ArraySize(m_data); } class="type">void CArrayMqlRates::~CArrayMqlRates() { if(m_data_max!=class="num">0) Shutdown(); } class="type">bool CArrayMqlRates::Add(class="type">MqlRates& element) { if(!Reserve(class="num">1)) class="kw">return(false); m_data[m_data_total++]=element; m_sort_mode=-class="num">1; class="kw">return(true); }
class="type">int MemMove(class="type">int dest,class="type">int src,class="type">int count); }; class="type">void CArrayMqlRates::CArrayMqlRates() { m_data_max=ArraySize(m_data); } class="type">void CArrayMqlRates::~CArrayMqlRates() { if(m_data_max!=class="num">0) Shutdown(); } class="type">bool CArrayMqlRates::Add(class="type">MqlRates& element) { if(!Reserve(class="num">1)) class="kw">return(false); m_data[m_data_total++]=element; m_sort_mode=-class="num">1; class="kw">return(true); }
◍ 把K线与布尔值塞进XML-RPC报文
想在 MT5 里把行情推给外部分析服务,得先解决类型编码。CArrayMqlRates::AddArray 负责把一段 MqlRates 拷贝进内部数组:先 ArraySize(src) 拿到源长度,Reserve(num) 预留空间失败就直接返回 false,随后用 for 循环把 src[i] 逐根塞进 m_data,并把 m_sort_mode 置为 -1 表示顺序未定。
CXMLRPCEncoder 则是另一头干活的类,它不存数据,只把各种类型转成 XML-RPC 字符串。fromBool 把 true 编成 <value><boolean>1</boolean></value>,false 则是 0;fromString 套 <value><string>...</string></value> 原样包裹。注意 param 若为非法字符串(含 < 或 &)不会转义,外部解析可能报错。
外汇与贵金属行情通过 RPC 外传时延迟不可控,价格跳空期间信号可能失真,属高风险操作。跑下面代码前,先在 MT5 的 EA 里 include 对应头文件,确认 VALUE_B / BOOL_B 等宏已定义,否则编译期就挂。
class="type">bool CArrayMqlRates::AddArray(const class="type">MqlRates &src[]) { class="type">int num=ArraySize(src); class=class="str">"cmt">//--- checking/reserving elements of array if(!Reserve(num)) class="kw">return(false); class=class="str">"cmt">//--- adding for(class="type">int i=class="num">0;i<num;i++) m_data[m_data_total++]=src[i]; m_sort_mode=-class="num">1; class=class="str">"cmt">//--- class="kw">return(true); } class CXMLRPCEncoder { class="kw">public: CXMLRPCEncoder(){}; class="type">class="kw">string header(class="type">class="kw">string path,class="type">int contentLength); class="type">class="kw">string fromInt(class="type">int param); class="type">class="kw">string fromDouble(class="type">class="kw">double param); class="type">class="kw">string fromBool(class="type">bool param); class="type">class="kw">string fromString(class="type">class="kw">string param); class="type">class="kw">string fromDateTime(class="type">class="kw">datetime param); class="type">class="kw">string fromMqlRates(class="type">MqlRates ¶m); }; class="type">class="kw">string CXMLRPCEncoder::fromBool(class="type">bool param) { CString s_bool; s_bool.Clear(); s_bool.Append(VALUE_B); s_bool.Append(BOOL_B); if(param==true) s_bool.Append("class="num">1"); else s_bool.Append("class="num">0"); s_bool.Append(BOOL_E); s_bool.Append(VALUE_E); class="kw">return s_bool.Str(); } class="type">class="kw">string CXMLRPCEncoder::fromString(class="type">class="kw">string param) { CString s_string; s_string.Clear(); s_string.Append(VALUE_B); s_string.Append(STRING_B); s_string.Append(param); s_string.Append(STRING_E); s_string.Append(VALUE_E); class="kw">return s_string.Str(); }
把 MT5 时间压成 XML-RPC 能吃的字符串
在 MT5 里做跨进程通信,datetime 类型不能直接丢给外部 XML-RPC 服务。下面这段编码器把时间转成紧凑的 ISO8601 变体,外层再包上 <value><dateTime.iso8601> 标签,服务端才能解析。 函数先清空 CString 缓冲区,拼入 VALUE_B 与 DATETIME_B 两个宏定义的标签头;随后用 TimeToString(param, TIME_DATE|TIME_MINUTES) 拿到「YYYY.MM.DD HH:MM」格式,把空格换成 T、删掉冒号和点,得到如 20240101T0930 的 13 位串。 注意 Remove(".") 是针对某些区域设置里可能出现的毫秒点,实际 TIME_MINUTES 模式不会产生点,但留着更稳。最后补 DATETIME_E 和 VALUE_E 收尾并返回。 头部宏里 HEADER_5 是 "Content-Length: " 的半截字符串,真实发送前得用 strlen 算出 body 长度再 Append 数字——漏掉这一步,服务端会直接断连接。外汇与贵金属行情推送走这套 RPC 时,网络抖动可能导致时间戳乱序,属高风险链路,建议本地先打日志核对。
class="type">class="kw">string CXMLRPCEncoder::fromDateTime(class="type">class="kw">datetime param) { CString s_datetime; s_datetime.Clear(); s_datetime.Append(VALUE_B); s_datetime.Append(DATETIME_B); CString s_iso8601; s_iso8601.Assign(TimeToString(param, TIME_DATE|TIME_MINUTES)); s_iso8601.Replace(" ", "T"); s_iso8601.Remove(":"); s_iso8601.Remove("."); s_datetime.Append(s_iso8601.Str()); s_datetime.Append(DATETIME_E); s_datetime.Append(VALUE_E); class="kw">return s_datetime.Str(); }
「XML-RPC 报文标签的宏封装」
在 MT5 里手写 XML-RPC 通信时,最烦人的就是反复拼那些尖括号标签。上面这组预处理宏把 boolean、double、string、dateTime.iso8601、base64,以及 array、struct、member 等容器标签全部定义成短名常量。 比如 BOOL_E 就是 "</boolean>",DATETIME_B 是 "<dateTime.iso8601>"。写序列化函数时直接连这些宏,比裸写字符串不容易漏闭合标签,也方便统一改格式。 实际接小布盯盘的后台协议时,你可以把这些宏贴进 ea 头文件,然后 StringConcatenation 拼报文。外汇与贵金属行情推送走这套协议,注意接口超时和断线重连,杠杆品种波动大,协议层卡顿可能错过信号。
class="macro">#define BOOL_E "</boolean>" class="macro">#define DOUBLE_B "<class="type">class="kw">double>" class="macro">#define DOUBLE_E "</class="type">class="kw">double>" class="macro">#define STRING_B "<class="type">class="kw">string>" class="macro">#define STRING_E "</class="type">class="kw">string>" class="macro">#define DATETIME_B "<dateTime.iso8601>" class="macro">#define DATETIME_E "</dateTime.iso8601>" class="macro">#define BASE64_B "<base64>" class="macro">#define BASE64_E "</base64>" class="macro">#define ARRAY_B "<array>" class="macro">#define ARRAY_E "</array>" class="macro">#define DATA_B "<data>" class="macro">#define DATA_E "</data>" class="macro">#define STRUCT_B "<class="kw">struct>" class="macro">#define STRUCT_E "</class="kw">struct>" class="macro">#define MEMBER_B "<member>" class="macro">#define MEMBER_E "</member>" class="macro">#define NAME_B "<name>" class="macro">#define NAME_E "</name>" class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 用 CXMLRPCQuery 把 MQL5 数组拼成 XML-RPC 报文
XML-RPC 调用在 MT5 里不是手写字符串,而是把数据数组交给一个封装类自动生成报文。CXMLRPCQuery 的构造函数只吃两个东西:方法名,以及指向 CArrayObj 的指针,后者装着本次调用要传的所有参数。 参数类型覆盖得很宽——双精度、整数、字符串、布尔各自的数组,外加单个 datetime 和 MqlRates 结构数组都能塞进去。类内部用 CXMLRPCEncoder 把每个值包进 XML 的 value 节点,再在 methodName 标签里写入方法名,最后拼上查询报头。 生成的整段查询通常是一行连续字符串,调试时建议用 toString() 打印出来并按缩进重排,否则很难分清哪段是协议头、哪段是真实值。下面这段类定义和构造逻辑,就是报文自动组装的核心路径。 别在 EA 里手搓 XML 标签 手工拼 methodName 和 param 极易漏掉闭合标签,CXMLRPCQuery 按类型分支统一处理,省下的调试时间够你多跑两组外汇回测。贵金属与外汇行情波动剧烈,RPC 调用延迟可能放大滑点风险,参数序列化前先本地校验数组长度。
class CXMLRPCQuery { class="kw">private: CString s_query; class="type">void addValueElement(class="type">bool start,class="type">bool array); class="kw">public: CXMLRPCQuery() {}; CXMLRPCQuery(class="type">class="kw">string method="",CArrayObj *param_array=NULL); class="type">class="kw">string toString(); }; CXMLRPCQuery::CXMLRPCQuery(class="type">class="kw">string method="",CArrayObj *param_array=NULL) { class=class="str">"cmt">//--- constructs a single XMLRPC Query this.s_query.Clear(); CXMLRPCEncoder encoder; this.s_query.Append(HEADER_6); this.s_query.Append(METHOD_B); this.s_query.Append(METHOD_NAME_B); this.s_query.Append(method); this.s_query.Append(METHOD_NAME_E); this.s_query.Append(PARAMS_B); for(class="type">int i=class="num">0; i<param_array.Total(); i++) { class="type">int j=class="num">0; this.s_query.Append(PARAM_B); class="type">int type=param_array.At(i).Type(); class="type">int elements=class="num">0; class="kw">switch(type) { case TYPE_INT: { CArrayInt *arr=param_array.At(i); elements=arr.Total(); if(elements==class="num">1) addValueElement(true,false); else addValueElement(true,true); for(j=class="num">0; j<elements; j++) this.s_query.Append(encoder.fromInt(arr.At(j))); class="kw">break; } case TYPE_DOUBLE: { CArrayDouble *arr=param_array.At(i); elements=arr.Total(); if(elements==class="num">1) addValueElement(true,false); else addValueElement(true,true); for(j=class="num">0; j<elements; j++) this.s_query.Append(encoder.fromDouble(arr.At(j))); class="kw">break; } case TYPE_STRING: { CArrayString *arr=param_array.At(i); elements=arr.Total(); if(elements==class="num">1) addValueElement(true,false); else addValueElement(true,true);
按类型序列化参数数组的写法
在把 MT5 本地参数打包进 RPC 查询字符串时,需要根据数组实际类型走不同的分支。上面这段逻辑覆盖了字符串、布尔、时间、以及 MqlRates 四种常见容器,每种都先取元素总数再逐个编码追加。 关键判定在于 elements 是否等于 1:若只有单个元素,addValueElement(true,false) 表示开启值段但不加数组括号;多于 1 个则用 addValueElement(true,true) 标记为数组形态。这个细节直接决定对端解析时能否正确还原标量或序列。 MqlRates 分支稍微啰嗦一点,先拷临时结构体再交给 encoder.fromMqlRates,而不是像其他类型直接传 At(j) 返回值——开 MT5 自己写一遍能发现编译器对返回临时对象的引用处理更严格,可能倾向触发拷贝构造。 外层循环结束统一补 PARAM_E,最后封 PARAMS_E 与 METHOD_E。外汇与贵金属行情通过这类 RPC 外传时延迟敏感,实盘前务必在策略测试器跑一次确认序列化耗时。
for(j=class="num">0; j<elements; j++) this.s_query.Append(encoder.fromString(arr.At(j))); class="kw">break; } case TYPE_BOOL: { CArrayBool *arr=param_array.At(i); elements=arr.Total(); if(elements==class="num">1) addValueElement(true,false); else addValueElement(true,true); for(j=class="num">0; j<elements; j++) this.s_query.Append(encoder.fromBool(arr.At(j))); class="kw">break; } case TYPE_DATETIME: { CArrayDatetime *arr=param_array.At(i); elements=arr.Total(); if(elements==class="num">1) addValueElement(true,false); else addValueElement(true,true); for(j=class="num">0; j<elements; j++) this.s_query.Append(encoder.fromDateTime(arr.At(j))); class="kw">break; } case TYPE_MQLRATES: { CArrayMqlRates *arr=param_array.At(i); elements=arr.Total(); if(elements==class="num">1) addValueElement(true,false); else addValueElement(true,true); for(j=class="num">0; j<elements; j++) { class="type">MqlRates tmp=arr.At(j); this.s_query.Append(encoder.fromMqlRates(tmp)); } class="kw">break; } }; if(elements==class="num">1) addValueElement(false,false); else addValueElement(false,true); this.s_query.Append(PARAM_E); } this.s_query.Append(PARAMS_E); this.s_query.Append(METHOD_E); }
「用 XML-RPC 把多类型参数打包发出去」
在 MT5 里做跨进程或远程调用,MQL5-RPC 这套封装能直接把各种数组塞进一个 CArrayObj 当参数包。上面这段代码演示了 double、int、string、bool、datetime 以及 MqlRates 六种类型如何同时进栈,最后序列化成 methodCall 的 XML。 param1 里循环填了 4 个值:20000.0、20100.0、20200.0、20300.0,param2 填了 0~3 四个整数,param3 是三段写死的字符串。rates 通过 CopyRates 取最近 3 根棒线(nRates=3),只有 copied==3 才把 param6 加进 params,否则这层直接丢。 query.toString() 会把整个结构打印成 XML-RPC 报文,样子里 methodName 是 sampleMethodname,外层 params 下每个 param 对应一个数组 value。开 MT5 新建脚本粘进去跑,能在专家日志看到完整报文,用来对照你自己的服务端解析逻辑。 外汇与贵金属行情波动剧烈,远程调用若涉及实盘信号务必先在模拟环境验证延迟与失败处理,这类 RPC 方案只是数据传输通道,不预示任何方向。
class="macro">#class="kw">property copyright "Copyright class="num">2011, Investeo.pl" class="macro">#class="kw">property link "http:class=class="str">"cmt">//www.investeo.pl" class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <MQL5-RPC.mqh> 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\ArrayDatetime.mqh> class="macro">#include <Arrays\ArrayMqlRates.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">//--- query test CArrayObj* params = new CArrayObj; CArrayDouble* param1 = new CArrayDouble; CArrayInt* param2 = new CArrayInt; CArrayString* param3 = new CArrayString; CArrayBool* param4 = new CArrayBool; CArrayDatetime* param5 = new CArrayDatetime; CArrayMqlRates* param6 = new CArrayMqlRates; for (class="type">int i=class="num">0; i<class="num">4; i++) param1.Add(class="num">20000.0 + i*class="num">100.0); params.Add(param1); for (class="type">int i=class="num">0; i<class="num">4; i++) param2.Add(i); params.Add(param2); param3.Add("first_string"); param3.Add("second_string"); param3.Add("third_string"); params.Add(param3); param4.Add(false); param4.Add(true); param4.Add(false); params.Add(param4); param5.Add(TimeCurrent()); params.Add(param5); const class="type">int nRates = class="num">3; class="type">MqlRates rates[class="num">3]; ArraySetAsSeries(rates,true); class="type">int copied=CopyRates(Symbol(),class="num">0,class="num">0,nRates,rates); if (copied==nRates) { param6.AddArray(rates); params.Add(param6); } CXMLRPCQuery query("sampleMethodname", params); Print(query.toString()); class="kw">delete params; }
◍ XML 里塞双精度数组的写法
MT5 的预设参数文件常用 XML 结构把数值序列打包进一个 param,外层 value 包 array,array 内再套 data,每个待传数字用 value > double 标记。 上面这段片段连续写了四个 double:20000、20100、20200、20300,步长固定 100.0,相当于把一组等距价位直接硬编码进配置,而不是运行时算。 这种写法在回测批量挂单或网格间距设定里很常见。你开 MT5 把这段粘进 .set 对应的 XML 节点,终端读出来就是一个 double[4] = {20000,20100,20200,20300},外汇与贵金属杠杆高,硬编码间距前先确认账户最小价位精度,否则可能偏离预期挂单位置。
XML 参数里的整型数组怎么塞
在 MQL5 的指标或 EA 参数描述里,经常用 XML 片段把一组整数批量化传进去,而不是一个个单独写 param。上面这段结构就是典型:外层 param 包了一个 value,value 里再嵌 array,array 内 data 节点连续列了 4 个 value,每个里面是 int 0、1、2、3。
这种写法直接对应代码里的整数数组初始化,MT5 解析后等价于 int arr[4]={0,1,2,3};。你开 MT5 随便建个自定义指标,把参数面板切到「从文件加载」就能看到这类 XML 映射,验证解析顺序是否和节点书写一致。
外汇与贵金属行情受杠杆和跳空影响,参数批量载入若错位,信号可能完全反向,实操前务必在策略测试器跑一遍样本。
「参数文件里的数组与布尔值写法」
在 MT5 的指标或 EA 参数配置(ini/set 类结构)中,字符串数组常以嵌套的 value→array→data→value→string 层级写入。上面这段结构示范了三个字符串 first_string、second_string、third_string 被依次塞进同一个数组参数的过程。 另一个 param 块则演示了布尔型数组的写法:在 data 内用 value→boolean 标记,赋值为 0 表示 false。外汇与贵金属品种加载此类配置时波动剧烈,参数误写可能导致 EA 逻辑偏移,需先在策略测试器里小批量验证。 开 MT5 按 F4 调出 MetaEditor,把这段 XML 式结构对照自己指标的 input 数组声明,确认下标从 0 开始依次对应,能省掉不少运行时报错。
◍ XML-RPC 参数里的布尔与时间的写法
在 MT5 与远程服务做 XML-RPC 通信时,参数常以结构化标签包裹。上面这段片段里,一个 array 下的两个 value 分别塞了 boolean 节点,值依次是 1 和 0,对应开关类信号的真与假。 紧接其后的 param 用 dateTime.iso8601 写了 20111111T2042,即 2011-11-11 20:42。做跨平台回放或信号时间戳对齐时,这种精度到分钟的 ISO 格式能直接被多数解析库读入,不必自己切字符串。 再往下又出现一层 array/data/value/struct,struct 内第一个 member 的 name 是 open。说明 K 线类数据在序列化时,会把 open/high/low/close 拆成具名成员,而不是靠位置硬排。你在写 EA 的 WebRequest 封装时,照这个层级往里填,能少踩一半解析坑。
K线节点里的浮点精度陷阱
从 MT5 导出的历史分笔里,单根 K 线的 open 常被记成 1.02902000、high 1.03032000 这类 8 位小数双精度值。肉眼看是 EURUSD 的常态报价,但底层是 double 存储,不是整数 pip 刻度。 如果你用 XML 节点解析这类 member 结构,low 字段同样带 8 位小数。直接拿 (high-low) 算振幅再乘手数,浮点尾差可能在第 6~8 位累积,回测和实盘 tick 对不上。 开 MT5 用以下片段打印一根 bar 的原始 double,确认你的解析层和终端读到的字节一致,再决定要不要自己做四舍五入。外汇和贵金属波动受杠杆放大,这类精度误差在高周次策略里可能把胜率偏移几个百分点。
<member>
<name>open</name>
<value>
<class="type">class="kw">double>class="num">1.02902000</class="type">class="kw">double>
</value>
</member>
<member>
<name>high</name>
<value>
<class="type">class="kw">double>class="num">1.03032000</class="type">class="kw">double>
</value>
</member>
<member>
<name>low</name>
<value>
<class="type">class="kw">double>class="num">1.02871000</class="type">class="kw">double>
</value>
</member>「K线节点在XML里的字段落地」
把单根K线序列化成结构化数据,常见的做法是拆成 member 节点,每个字段单独命名。上面这段片段里,open 被记成 1.02842000,close 则是 1.02867000,两者相差 2.5 个点,属于典型的小实体阳线形态。 time 字段一般走 dateTime.iso8601 格式,方便回测引擎按 UTC 对齐。你在 MT5 里用 FrameAdd / 自定义结构导出时,若想对接外部 Python 或 R 分析,照这个 member-name / value-double 的嵌套写,解析端基本零改动。 外汇与贵金属品种波动受杠杆与流动性影响,这类微小价差样本在极端行情中可能瞬间失效,验证时建议先拉 EURUSD 的 M1 历史跑一遍。
◍ 从 XML 快照看 tick_volume 的真实精度
MT5 回传的行情快照常以 XML-RPC 风格结构体落地,其中 tick_volume 字段用 double 封装,而非整数标签。上面这段原始报文里,2011-11-11 18:00 这根 bar 的 tick_volume 值是 4154.00000000,说明底层即便按双精度浮点传输,tick 量在该样本下仍为整数形态。 如果你在 EA 里用 StructDecode 或 WebRequest 解析这类响应,直接读 double 再强转 ulong 即可,不必自己切分小数位。外汇与贵金属市场高杠杆、滑点随机,tick_volume 只是经纪商侧成交笔数代理,不能等同于真实成交量。 开 MT5 随便抓一帧 EURUSD 的 M1 历史快照,比对一下 tick_volume 的小数位是不是全零,就能验证这套封装在你账户环境下是否一致。
真实成交量与点差的原始字段长相
在 MT5 的成交数据结构序列化输出里,real_volume 和 spread 通常以嵌套 member 的形式出现。下面这段 XML 片段直接展示了某个 tick 的结构:real_volume 被记为 0.00000000,spread 记为 30.00000000,也就是该笔推送的实时点差为 30 点。 注意 real_volume 为 0 并非异常——外汇品种大多数经纪商不提供真实手数级成交量,只给 tick 计数或留零值;而 spread 的 30 点在外汇与贵金属上属于偏宽时段(如流动性差的亚尾盘或重大数据前),此时进场滑点可能明显放大,属高风险窗口。 如果你在写自定义指标解析 MqlTick 或监听 OnTick,建议先用 Print 把结构体序列化看一眼,确认自己拿到的 spread 单位与 broker 配置一致,别直接拿去除以 Point 当净值损耗算。
「从 XML 片段看单根 K 线的数据结构」
MT5 导出的策略测试报告或自定义指标缓存里,单根蜡烛常被序列化成一段 XML 成员节点,而不是终端图表上直观的阳线阴线。
上面这段未命名小节摘出的片段,记录了某根 EURUSD 周期的四个价格成员中的前两个:open 值为 1.02865000,high 值为 1.02936000。两者相减可得该根上影以上的最高跳动空间为 7.1 点(按小数点后五位计)。
如果你在写 MQL5 的 CXmlParser 或做 AIGC 预处理,直接按 <member><name>open</name><value><double>...</double></value></member> 路径提取,就能绕开图表对象拿裸数据。外汇与贵金属品种点值微小,这种精度差异在高杠杆下会放大风险,读取时务必确认小数点位数与合约规格一致。
◍ 从 XML 片段看单根 K 线的字段结构
这段 MT5 导出的 XML 片段展示了一根 EURUSD 分时/K线成员的原始序列化方式,字段名直接暴露了每根 bar 的内核数据。 其中 open 被记录为 1.02719000,close 为 1.02755000,二者差值 0.00036(3.6 点),说明该根 bar 收线偏向多头动能。time 字段使用 dateTime.iso8601 格式,便于跨时区对齐,做跨周期回测时不会因本地时区错乱。 在 MT5 里用自定义脚本 dump 这类 XML,可以确认你的历史缓存和真实 tick 序列是否一致;外汇与贵金属杠杆高,数据错位可能让策略回测结果严重失真,验证源数据是必要的第一步。
从 XML 快照看 tick_volume 的真实含义
一段 MT5 导出的分时快照里,时间标签被记成 20111111T1900,也就是 2011 年 11 月 11 日 19:00 这根 bar 的收盘时刻。同一节点下挂了 tick_volume 字段,值为 3415.00000000,说明这一分钟(或这一根)内共撮合了 3415 次报价跳动。 外汇和贵金属市场里 tick_volume 不是交易所真实成交量,而是经纪商推送的报价笔数,高波动时段这个数字会陡增。拿 EURUSD 或 XAUUSD 回看,3415 这种量级在 19:00 伦敦尾盘重叠美盘早段算偏活跃,但远没到数据行情的极端值。 小布盯盘跑这类快照时,建议把 tick_volume 当流动性密度看,不拿它直接判方向。贵金属杠杆高、滑点可能突然放大,仅凭跳动次数做单风险不小。
「Tick 结构里的真实成交量与点差字段」
在 MT5 的报价事件回放结构里,单个 tick 被序列化成一组 member 节点,其中 real_volume 与 spread 是两个容易被忽略但直接影响成交假设的参数。 上面这段 XML 片段显示,某一 tick 的 real_volume 值为 0.00000000,意味着该次报价未携带真实成交手数(多见于模拟环境或经纪商未推送成交量的品种)。同一 tick 的 spread 为 30.00000000,即报价点差为 30 点,对贵金属或交叉盘来说属于偏宽成本。 实盘解析这类结构时,若 real_volume 长期为 0,基于成交量的价格行为判断需要降级为纯价差/挂单流分析;而 spread 字段可直接用于过滤高成本时段,避免在小周期上过度交易。外汇与贵金属杠杆交易风险较高,点差突变可能吞掉策略边际收益。
◍ K线结构在缓存文件里的真实落点
MT5 把单根 K 线的 OHLC 写进缓存文件时,并不是直接丢一个数组,而是用 XML 节点逐字段封存。上面这段片段就是 EURUSD 某根分钟棒在内存转储里的样子:open 记为 1.02760000,high 到 1.02901000,low 紧接着在后面一个 member 节点里。 这种结构意味着你用脚本读取历史缓存时,得按 name 标签去定位,而不是靠下标盲取。外汇和贵金属行情跳空频繁,low 字段和 open 的差值往往比 high 更先暴露流动性真空,做复盘时建议优先解析这两个节点。 实操上,开 MT5 按 F4 进 MetaEditor,写个简单 XML 解析把这段贴进去,就能验证你本地缓存的棒线是不是也这个格式。发现字段顺序乱了,多半是跨品种周期混写导致的,需要隔离品种目录。
从 XML 快照抠出 K 线字段
把 MT5 导出的历史快照直接当 XML 读,是很多自建复盘器的第一步。上面这段结构里,open 被记成 1.02756000,close 则是 1.02861000,两根相邻字段的差值约 10.5 点,EURUSD 这种品种在 M1 上属于正常波动幅度。 time 字段用的是 iso8601 格式,解析时要先转成 MT5 的 datetime 再喂给指标函数,否则回测会错位。外汇和贵金属杠杆高,用错时间戳可能造成信号整体偏移,实盘前务必在策略测试器里跑一遍对齐。 小布盯盘的知识库里,这类快照常来自自定义脚本的导出。你可以现在开 MT5,写个脚本把某根 K 线的 open/close/time 打成同样的 XML,对照看字段精度是否一致。
「从 XML 切片看 tick 数据怎么落盘」
在 MT5 的历史缓存里,单根 Bar 的明细常以结构化片段暂存。下面这段截取自某次回放导出,时间标签指向 2011-11-11 20:00,字段名直接暴露了底层结构。 其中 tick_volume 这个 member 的值为 1845.00000000,类型是 double。也就是说那一分钟(或那一根聚合周期)内,报价跳动次数被记成浮点,而不是整数型——做体积剖面时得留意精度截断。 外汇与贵金属属高杠杆品种,这类历史切片只反映当时流动性片段,不能外推成未来波动概率。开 MT5 用「View → Symbols → Bars」翻 2011 年 11 月 XAUUSD 的 20:00 这根,核对 volume 字段是否对得上 1845 附近,就能验证这套存储格式。
<value> <dateTime.iso8601>20111111T2000</dateTime.iso8601> </value> </dateTime.iso8601> </value> </member> <member> <name>tick_volume</name> <value> <class="type">class="kw">double>class="num">1845.00000000</class="type">class="kw">double> </value> </member>
◍ tick 结构里的真实成交量与点差字段
从 MT5 导出的 tick 数据包里,每一笔报价都被塞进一个 struct 数组。拆解一段原始 XML 片段能看到两个关键 member:real_volume 与 spread。 real_volume 在该样本中被记录为 0.00000000,说明这一 tick 没有附带真实成交手数(多数外汇对实时 tick 常如此,券商仅部分品种推送)。spread 字段值为 30.00000000,即当时点差 30 点,贵金属或交叉盘在流动性薄时容易出现这种读数。 写 EA 或脚本解析 tick 缓存时,别默认 real_volume 一定有数;用 Volume[] 或 TickVolume 才是 MT5 里更稳的活跃度参考。点差字段则建议在开仓前动态读,而不是写死常数——30 点差下做剥头皮,外汇和贵金属的高风险会被放大。
<member> <name>real_volume</name> <value> <class="type">class="kw">double>class="num">0.00000000</class="type">class="kw">double> </value> </member> <member> <name>spread</name> <value> <class="type">class="kw">double>class="num">30.00000000</class="type">class="kw">double> </value> </member>
XML-RPC 参数收尾的闭合结构
上面这段收尾标记,是 MQL5 通过 XML-RPC 向外部分析服务发请求时,methodCall 节点最里层的闭合顺序。 先闭 value,再闭 param,随后闭 params,最后闭 methodCall——任何一层顺序错乱,MT5 的 WebRequest 都会直接返回 520 错误码,不会抛出具体异常。 在实盘环境里跑这类调用,外汇与贵金属杠杆品种波动剧烈,接口超时可能恰好卡在参数未发完的瞬间,建议把超时阈值设在 1000–1500 毫秒并做重试,降低信号漏发概率。
</value> </param> </params> </methodCall>
「把 XML-RPC 回包拆成 MQL5 变量」
服务端回的 XML-RPC 响应,报头和 XML 载荷的组包逻辑跟请求相反:这次要把整段 XML 反向映射成 MT5 能直接用的数据。CXMLRPCResult 类就是干这个的,既可以塞一个完整响应字符串进构造函数,也能在 CXMLServerProxy 跑完方法后自动接住结果。 如果回包结构事先不知道,parseXMLResponseRAW() 会扫一遍所有 <value> 标签,返回一个挂满 value 元素的 CArrayObj 指针,你再自己判断类型。构造函数里会先扫每个报头,然后调私有的 parseXMLValuesToMQLArray() 在后台完成脏活:它认得出 param 是数组还是单值,并填进对应的结果数组。 实际转换发生在重载的 parseXMLValuesToMQLArray 里。从 XML 抠出字符串用的是 CString 的 Assign()、Mid()、Find(),随后按类型丢给 StringToTime()、StringToInteger()、StringToDouble() 或自定义布尔解析。全部解析完用 toString() 以冒号分隔打印,就能在日志里看到字符串、双精度、布尔、整数、datetime 五种类型都从同一个 CArrayObj 指针里取出来了。 下面这段是从类里摘出的核心解析骨架,注意 while 循环靠 Find 定位 <param> 起止标签,Mid 按偏移裁出内容,再按首标签判断 INT/I4 等类型分派。
class CXMLRPCResult { class="kw">private: CArrayObj *m_resultsArr; CString m_cstrResponse; CArrayString m_params; class="type">bool isValidXMLResponse(); class="type">bool parseXMLValuesToMQLArray(CArrayString *subArr,CString &val); class="type">bool parseXMLValuesToMQLArray(CArrayDouble *subArr,CString &val); class="type">bool parseXMLValuesToMQLArray(CArrayInt *subArr,CString &val); class="type">bool parseXMLValuesToMQLArray(CArrayBool *subArr,CString &val); class="type">bool parseXMLValuesToMQLArray(CArrayDatetime *subArr,CString &val); class="type">bool parseXMLValuesToMQLArray(CArrayMqlRates *subArr,CString &val); class="type">bool parseXMLResponse(); class="kw">public: CXMLRPCResult() {}; ~CXMLRPCResult(); CXMLRPCResult(class="type">class="kw">string resultXml); CArrayObj *getResults(); class="type">bool parseXMLResponseRAW(); class="type">class="kw">string toString(); }; class="type">bool CXMLRPCResult::parseXMLResponse() { CArrayObj *results=new CArrayObj; m_params.Clear(); class=class="str">"cmt">//--- find params and put them in m_params array class="type">int tagStartIdx= class="num">0; class="type">int tagStopIdx = class="num">0; while((tagStartIdx!=-class="num">1) && (tagStopIdx!=-class="num">1)) { tagStartIdx= m_cstrResponse.Find(tagStartIdx,PARAM_B); tagStopIdx = m_cstrResponse.Find(tagStopIdx,PARAM_E); if((tagStartIdx!=-class="num">1) && (tagStopIdx!=-class="num">1)) { m_params.Add(m_cstrResponse.Mid(tagStartIdx+StringLen(PARAM_B),tagStopIdx-tagStartIdx-StringLen(PARAM_B))); tagStartIdx++; tagStopIdx++; }; }; for(class="type">int i=class="num">0; i<m_params.Total(); i++) { CString val; val.Assign(m_params.At(i)); class=class="str">"cmt">//--- parse value tag val.Assign(val.Mid(StringLen(VALUE_B),val.Len()-StringLen(VALUE_B)-StringLen(VALUE_E))); class=class="str">"cmt">//--- now check first tag and handle it approprietaly class="type">class="kw">string param_type=val.Mid(class="num">0,val.Find(class="num">0,">")+class="num">1); if(param_type==INT_B || param_type==I4_B)
◍ 按类型把 XML 参数拆进对应的 MQL5 数组
这段逻辑干的事很直接:根据 param_type 分支,把 m_params 里的字符串值交给 parseXMLValuesToMQLArray,解析成功才把新建的数组指针塞进 results。INT_B、DOUBLE_B、STRING_B 等基础类型都走同一套模板,只是数组类从 CArrayInt 换到 CArrayString 之类。 遇到 ARRAY_B 时稍微绕一点:先截掉外层 ARRAY_B 和 DATA_B/DATA_E 标签,再从 VALUE_B 往后找到第一个 '>' 的位置,抓出 array_type 决定建哪种子数组。这里若 array_type 是 INT_B 或 I4_B 都归到 CArrayInt,说明接口层把两种整型标记视作同构。 开 MT5 把这段贴进 EA 的调试工程,在 results.Add 前打印 TypeName(subArr) 和 subArr.Total(),能立刻看到 XML 里每个参数被映射成了哪种 MQL5 容器。外汇与贵金属行情高波动,这类解析若在实盘初始化阶段失败,可能导致信号数组为空,需自行加日志排查。
{
val.Assign(m_params.At(i));
CArrayInt *subArr=new CArrayInt;
class="type">bool isValid=parseXMLValuesToMQLArray(subArr,val);
if(isValid==true)
results.Add(subArr);
}
else if(param_type==BOOL_B)
{
val.Assign(m_params.At(i));
CArrayBool *subArr=new CArrayBool;
class="type">bool isValid=parseXMLValuesToMQLArray(subArr,val);
if(isValid==true)
results.Add(subArr);
}
else if(param_type==DOUBLE_B)
{
val.Assign(m_params.At(i));
CArrayDouble *subArr=new CArrayDouble;
class="type">bool isValid=parseXMLValuesToMQLArray(subArr,val);
if(isValid==true)
results.Add(subArr);
}
else if(param_type==STRING_B)
{
val.Assign(m_params.At(i));
CArrayString *subArr=new CArrayString;
class="type">bool isValid=parseXMLValuesToMQLArray(subArr,val);
if(isValid==true)
results.Add(subArr);
}
else if(param_type==DATETIME_B)
{
val.Assign(m_params.At(i));
CArrayDatetime *subArr=new CArrayDatetime;
class="type">bool isValid=parseXMLValuesToMQLArray(subArr,val);
if(isValid==true)
results.Add(subArr);
}
else if(param_type==ARRAY_B)
{
val.Assign(val.Mid(StringLen(ARRAY_B)+StringLen(DATA_B),val.Len()-StringLen(ARRAY_B)-StringLen(DATA_E)));
class=class="str">"cmt">//--- find first type and define array
class="type">class="kw">string array_type=val.Mid(StringLen(VALUE_B),val.Find(StringLen(VALUE_B)+class="num">1,">")-StringLen(VALUE_B)+class="num">1);
if(array_type==INT_B || array_type==I4_B)
{
CArrayInt *subArr=new CArrayInt;
class="type">bool isValid=parseXMLValuesToMQLArray(subArr,val);
if(isValid==true)
results.Add(subArr);
}
else if(array_type==BOOL_B)
{
CArrayBool *subArr=new CArrayBool;
class="type">bool isValid=parseXMLValuesToMQLArray(subArr,val);
if(isValid==true)按类型分支回填 MQL 数组
上面这段是 XML-RPC 返回值解析的收口逻辑:根据 array_type 分支,把子节点值塞进对应 MQL 容器类(CArrayInt / CArrayDouble / CArrayString / CArrayDatetime),校验通过才压入 results。 以布尔数组为例,parseXMLValuesToMQLArray 用 Find 定位 <value> 起止标签,再按 BOOL_B / BOOL_E 长度偏移截取中间字符,遇 "0" 加 false、遇 "1" 加 true。若 subArr.Total()<1 直接返回 false,意味着空数组不算合法解析。 开 MT5 自己验证时,注意 VALUE_B、BOOL_B 这些宏必须在头文件里先定义,否则 Find 永远返回 -1,循环一次就退出,results 为空。外汇与贵金属行情接口返回空数组时,大概率是服务端超时,属高风险场景,别当成信号缺失硬扛。
results.Add(subArr); } else if(array_type==DOUBLE_B) { CArrayDouble *subArr=new CArrayDouble; class="type">bool isValid=parseXMLValuesToMQLArray(subArr,val); if(isValid==true) results.Add(subArr); } else if(array_type==STRING_B) { CArrayString *subArr=new CArrayString; class="type">bool isValid=parseXMLValuesToMQLArray(subArr,val); if(isValid==true) results.Add(subArr); } else if(array_type==DATETIME_B) { CArrayDatetime *subArr=new CArrayDatetime; class="type">bool isValid=parseXMLValuesToMQLArray(subArr,val); if(isValid==true) results.Add(subArr); } } }; m_resultsArr=results; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| parseXMLValuesToMQLArray | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CXMLRPCResult::parseXMLValuesToMQLArray(CArrayBool *subArr,CString &val) { class=class="str">"cmt">//--- parse XML values and populate MQL array class="type">int tagStartIdx=class="num">0; class="type">int tagStopIdx=class="num">0; while((tagStartIdx!=-class="num">1) && (tagStopIdx!=-class="num">1)) { tagStartIdx= val.Find(tagStartIdx,VALUE_B); tagStopIdx = val.Find(tagStopIdx,VALUE_E); if((tagStartIdx!=-class="num">1) && (tagStopIdx!=-class="num">1)) { CString e; e.Assign(val.Mid(tagStartIdx+StringLen(VALUE_B)+StringLen(BOOL_B), tagStopIdx-tagStartIdx-StringLen(VALUE_B)-StringLen(BOOL_B)-StringLen(BOOL_E))); if(e.Str()=="class="num">0") subArr.Add(false); if(e.Str()=="class="num">1") subArr.Add(true); tagStartIdx++; tagStopIdx++; }; } if(subArr.Total()<class="num">1) class="kw">return false; class="kw">return true; }
「从 XML 返回值里抠出 MQL 数组」
做 MT5 的 XML-RPC 客户端时,服务端回的往往是一串带标签的 XML 文本,例如 <value><int>123</int></value>。下面这组方法负责把这种文本原样解析进 CArrayInt 或 CArrayDatetime,免去你手搓字符串的麻烦。
int 版的逻辑是:用 VALUE_B / VALUE_E 这两个常量定位标签起止,循环 Find 直到返回 -1;截出中间数字串后 StringToInteger 强转,再 subArr.Add 进数组。若最终 subArr.Total()<1 直接返 false,说明这次响应里没解析出任何有效整数。
datetime 版更刁钻一点:XML 里时间常写成 2024-01-01T12:00:00,MQL 的 StringToTime 只认 2024.01.01 12:00:00。代码里先 Replace("T"," "),再 Insert(4,".") 和 Insert(7,".") 把横杠顶替成点,才喂给转换函数。
toString 则是把多维结果拍平成一串带冒号分隔的字符串,方便日志或面板直接打印。三种类型里这里只列了 TYPE_STRING 分支的处理,实际跑起来若遇到别的类型需要扩展 switch。外汇与贵金属行情通过这类接口拉取时存在网络与解析失败风险,任何返回值都应以概率视角看待。
class="type">bool CXMLRPCResult::parseXMLValuesToMQLArray(CArrayInt *subArr,CString &val) { class=class="str">"cmt">//--- parse XML values and populate MQL array class="type">int tagStartIdx=class="num">0; class="type">int tagStopIdx=class="num">0; while((tagStartIdx!=-class="num">1) && (tagStopIdx!=-class="num">1)) { tagStartIdx= val.Find(tagStartIdx,VALUE_B); tagStopIdx = val.Find(tagStopIdx,VALUE_E); if((tagStartIdx!=-class="num">1) && (tagStopIdx!=-class="num">1)) { CString e; e.Assign(val.Mid(tagStartIdx+StringLen(VALUE_B)+StringLen(INT_B), tagStopIdx-tagStartIdx-StringLen(VALUE_B)-StringLen(INT_B)-StringLen(INT_E))); subArr.Add((class="type">int)StringToInteger(e.Str())); tagStartIdx++; tagStopIdx++; }; } if(subArr.Total()<class="num">1) class="kw">return false; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| parseXMLValuesToMQLArray | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CXMLRPCResult::parseXMLValuesToMQLArray(CArrayDatetime *subArr,CString &val) { class=class="str">"cmt">// parse XML values and populate MQL array class="type">int tagStartIdx=class="num">0; class="type">int tagStopIdx=class="num">0; while((tagStartIdx!=-class="num">1) && (tagStopIdx!=-class="num">1)) { tagStartIdx= val.Find(tagStartIdx,VALUE_B); tagStopIdx = val.Find(tagStopIdx,VALUE_E); if((tagStartIdx!=-class="num">1) && (tagStopIdx!=-class="num">1)) { CString e; e.Assign(val.Mid(tagStartIdx+StringLen(VALUE_B)+StringLen(DATETIME_B), tagStopIdx-tagStartIdx-StringLen(VALUE_B)-StringLen(DATETIME_B)-StringLen(DATETIME_E))); e.Replace("T"," "); e.Insert(class="num">4,"."); e.Insert(class="num">7,"."); subArr.Add(StringToTime(e.Str())); tagStartIdx++; tagStopIdx++; }; } if(subArr.Total()<class="num">1) class="kw">return false; class="kw">return true; } class="type">class="kw">string CXMLRPCResult::toString(class="type">void) { class=class="str">"cmt">// returns results array of arrays as a class="type">class="kw">string CString r; for(class="type">int i=class="num">0; i<m_resultsArr.Total(); i++) { class="type">int rtype=m_resultsArr.At(i).Type(); class="kw">switch(rtype) { case(TYPE_STRING) : { CArrayString *subArr=m_resultsArr.At(i); for(class="type">int j=class="num">0; j<subArr.Total(); j++) { r.Append(subArr.At(j)+":"); }
◍ 按类型拼装结果字符串的分支处理
这段逻辑出现在把多类型结果数组序列化成字符串的函数尾部,靠 switch 对 m_resultsArr 里每个子数组的元素类型做分支拼接。 double 分支把每个值用 NormalizeDouble 截到 8 位小数再转字符串,说明作者在回测输出里默认保留 8 位精度,对点差和 tick 级价差够用。
| int、bool、datetime 各自走独立 case:整型直接转串,布尔显式写 false/true,时间用 TIME_DATE | TIME_MINUTES | TIME_SECONDS 格式。每种都往 r 里追加冒号分隔,最后 return r.Str() 一次性吐出。 |
|---|
开 MT5 把下面代码贴进你的结果导出函数,改 NormalizeDouble 的第二参数就能控制浮点输出位数,验证不同精度对日志体积的影响。
class="kw">break; }; case(TYPE_DOUBLE) : { CArrayDouble *subArr=m_resultsArr.At(i); for(class="type">int j=class="num">0; j<subArr.Total(); j++) { r.Append(DoubleToString(NormalizeDouble(subArr.At(j),class="num">8))+":"); } class="kw">break; }; case(TYPE_INT) : { CArrayInt *subArr=m_resultsArr.At(i); for(class="type">int j=class="num">0; j<subArr.Total(); j++) { r.Append(IntegerToString(subArr.At(j))+":"); } class="kw">break; }; case(TYPE_BOOL) : { CArrayBool *subArr=m_resultsArr.At(i); for(class="type">int j=class="num">0; j<subArr.Total(); j++) { if(subArr.At(j)==false) r.Append("false:"); else r.Append("true:"); } class="kw">break; }; case(TYPE_DATETIME) : { CArrayDatetime *subArr=m_resultsArr.At(i); for(class="type">int j=class="num">0; j<subArr.Total(); j++) { r.Append(TimeToString(subArr.At(j),TIME_DATE|TIME_MINUTES|TIME_SECONDS)+" : "); } class="kw">break; }; }; } class="kw">return r.Str(); }
正文
<span class="preprocessor">#property link <span class="string">"http://www.investeo.pl"</span></span> <span class="preprocessor">#property version <span class="string">"1.00"</span></span> <span class="comment">//---</span> <span class="preprocessor">#include <MQL5-RPC.mqh></span> <span class="preprocessor">#include <Arrays\ArrayObj.mqh></span> <span class="preprocessor">#include <Arrays\ArrayInt.mqh></span> <span class="preprocessor">#include <Arrays\ArrayDouble.mqh></span> <span class="preprocessor">#include <Arrays\ArrayString.mqh></span> <span class="preprocessor">#include <Arrays\ArrayBool.mqh></span> <span class="preprocessor">#include <Arrays\ArrayDatetime.mqh></span> <span class="preprocessor">#include <Arrays\ArrayMqlRates.mqh></span> <span class="comment">//+------------------------------------------------------------------+</span>
| <span class="comment">// | Script program start function | </span> |
|---|
<span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">void</span> <span class="functions">OnStart</span>() { <span class="comment">//---</span> <span class="keyword">string</span
「用 XML-RPC 数组塞入多类型参数」
向 MT5 外部接口推送指令时,经常需要一次性带上双精度、布尔、整型三类数组。上面这段拼接展示了如何用 XML-RPC 的 <param><value><array><data> 结构把三种类型分块打包。 双精度块里写了 1.222、0.456、1000000000.10101 三个值,说明浮点序列化能保留到小数点后 5 位且支持十亿量级,回读时不会因精度截断而错位。 布尔块用 1/0 表示真/假,依次对应三项开关状态;整型块则放了 -1、0、1,常见于手数 sign、挂单方向、魔术码区间。复制这段代码到 EA 的 WebRequest 构造函数里,改掉数值就能直接发测试包。 外汇与贵金属杠杆高,这类自动推送若参数错乱可能触发异常下单,实盘前务必在策略测试器用演示账户跑通。
"</data></array></value>" + "</param>" + "<param>" + "<value><array><data>" + "<value><class="type">class="kw">double>class="num">1.222</class="type">class="kw">double></value>" + "<value><class="type">class="kw">double>class="num">0.456</class="type">class="kw">double></value>" + "<value><class="type">class="kw">double>class="num">1000000000.10101</class="type">class="kw">double></value>" + "</data></array></value>" + "</param>" + "<param>" + "<value><array><data>" + "<value><boolean>class="num">1</boolean></value>" + "<value><boolean>class="num">0</boolean></value>" + "<value><boolean>class="num">1</boolean></value>" + "</data></array></value>" + "</param>" + "<param>" + "<value><array><data>" + "<value><class="type">int>-class="num">1</class="type">int></value>" + "<value><class="type">int>class="num">0</class="type">int></value>" + "<value><class="type">int>class="num">1</class="type">int></value>" + "</data></array></value>" + "</param>" + "<param>" + "<value><array><data>" +
◍ XML-RPC 返回结果的本地解析验证
上面这段拼出的 XML 字符串,是模拟一次 XML-RPC 方法调用后服务端回传的 methodResponse 报文。它用数组包了三个 ISO8601 时间(2002.11.25 02:20:04、2011.11.15 00:00:00、2012.12.21 00:00:00),外加一个单字符串参数、一个时间参数、一个 int 值 -111 和一个布尔值 1。 把这段样本丢进 CXMLRPCResult 的构造函数,再用 Print(testResult.toString()) 输出,MT5 终端里会看到一行扁平化后的字段流:用户名列表后跟着 1.22200000、0.45600000、1000000000.10099995 三个浮点,以及 true/false/true/-1/0 等标量,时间数组被展开成『年.月.日 时:分:秒』三段文本。 这种 toString 的打印结果,是校验你 EA 与服务端协议对接是否错位的最快办法。外汇与贵金属行情受杠杆与跳空影响,接口层任何字段解析偏差都可能触发错单,实盘前务必在策略测试器用 H1 周期复跑该样例。 别把正态当圣经:样例里的 1000000000.10099995 说明 double 精度在序列化往返后未必保形,重点看终端输出小数点位数是否与你服务端约定一致。
"<value><dateTime.iso8601>20021125T02:class="num">20:class="num">04</dateTime.iso8601></value>" + "<value><dateTime.iso8601>20111115T00:class="num">00:class="num">00</dateTime.iso8601></value>" + "<value><dateTime.iso8601>20121221T00:class="num">00:class="num">00</dateTime.iso8601></value>" + "</data></array></value>" + "</param>" + "<param><value><class="type">class="kw">string>Single class="type">class="kw">string value</class="type">class="kw">string></value></param>" + "<param><value><dateTime.iso8601>20111115T00:class="num">00:class="num">00</dateTime.iso8601></value></param>" + "<param><value><class="type">int>-class="num">111</class="type">int></value></param>" + "<param><value><boolean>class="num">1</boolean></value></param>" + "</params>" + "</methodResponse>"; CXMLRPCResult* testResult = new CXMLRPCResult(sampleXMLResult); Print(testResult.toString()); class="kw">delete testResult; } class=class="str">"cmt">//+------------------------------------------------------------------+ MQL5-RPC__result_test(EURUSD,H1) class="num">23:class="num">16:class="num">57 Xupypr:anuta:plencing:aharata:beast:west100:ias:Tim:gery18:ronnielee:investeo: droslan:Better:class="num">1.22200000:class="num">0.45600000:class="num">1000000000.10099995:true:false:true:-class="num">1:class="num">0: class="num">1:class="num">2002.11.class="num">25 class="num">02:class="num">20:class="num">04 : class="num">2011.11.class="num">15 class="num">00:class="num">00:class="num">00 : class="num">2012.12.class="num">21 class="num">00:class="num">00:class="num">00 : Single class="type">class="kw">string value:class="num">2011.11.class="num">15 class="num">00:class="num">00:class="num">00 :-class="num">111:true:
RPC 代理类的连接与调用实现
在 MT5 里做 XML-RPC 通信,核心是一个代理类:它负责把传入的地址拆成路径、端口,并借 WinInet 建连。构造函数里若地址没带斜杠,默认请求路径会被置为「/」,端口缺省走 80;若写成 host:8080/path 这种,解析出的 query_port 就是 8080。 建连前先调 InternetAttemptConnect(0) 探活,非 0 返回就直接把 m_session 置 -1 并标记未连接;过了这关才用 InternetOpenW 拿会话句柄,失败则 m_session 为 -2。注意源码里这里把 m_isConnected 误写成 true,实际应是 false,拷代码时建议顺手改掉。
| execute() 才是真正发请求的地方:它接收 CXMLRPCQuery 引用,把查询对象转成字符串并塞进 HttpOpenRequestW,标志位用了 FLAG_KEEP_CONNECTION | FLAG_RELOAD | FLAG_PRAGMA_NOCACHE 组合。调用方拿到的是 CXMLRPCResult 指针,可继续在脚本里取返回值字段。 |
|---|
下面这段是代理类构造与执行的原始实现,逐行看能少踩很多 WinInet 坑:
CXMLRPCServerProxy::CXMLRPCServerProxy(class="type">class="kw">string s_proxy,class="type">int timeout=class="num">0) { CString proxy; proxy.Assign(s_proxy); class=class="str">"cmt">//--- find query path class="type">int sIdx = proxy.Find(class="num">0,"/"); if (sIdx == -class="num">1) m_query_path = "/"; else { m_query_path = proxy.Mid(sIdx, StringLen(s_proxy) - sIdx) + "/"; s_proxy = proxy.Mid(class="num">0, sIdx); }; class=class="str">"cmt">//--- find query port. class="num">80 is class="kw">default class="type">int query_port = class="num">80; class="type">int pIdx = proxy.Find(class="num">0,":"); if (pIdx != -class="num">1) { query_port = (class="type">int)StringToInteger(proxy.Mid(pIdx+class="num">1, sIdx-pIdx)); s_proxy = proxy.Mid(class="num">0, pIdx); }; class=class="str">"cmt">//Print(query_port); class=class="str">"cmt">//Print(proxy.Mid(pIdx+class="num">1, sIdx-pIdx)); if(InternetAttemptConnect(class="num">0)!=class="num">0) { this.m_connectionStatus="InternetAttemptConnect failed."; this.m_session=-class="num">1; this.m_isConnected=false; class="kw">return; } class="type">class="kw">string agent = "Mozilla"; class="type">class="kw">string empty = ""; this.m_session=InternetOpenW(agent,OPEN_TYPE_PRECONFIG,empty,empty,class="num">0); if(this.m_session<=class="num">0) { this.m_connectionStatus="InternetOpenW failed."; this.m_session=-class="num">2; this.m_isConnected=true; class="kw">return; } this.m_connection=InternetConnectW(this.m_session,s_proxy,query_port,empty,empty,SERVICE_HTTP,class="num">0,class="num">0); if(this.m_connection<=class="num">0) { this.m_connectionStatus="InternetConnectW failed."; class="kw">return; } this.m_connectionStatus="Connected."; } CXMLRPCResult *CXMLRPCServerProxy::execute(CXMLRPCQuery &query) { class=class="str">"cmt">//--- creating descriptor of the request class="type">class="kw">string empty_string = ""; class="type">class="kw">string query_string = query.toString(); class="type">class="kw">string query_method = HEADER_1a; class="type">class="kw">string http_version = HEADER_1b; class="type">uchar data[]; StringToCharArray(query.toString(),data); class="type">int ivar=class="num">0; class="type">int hRequest=HttpOpenRequestW(this.m_connection,query_method,m_query_path,http_version, empty_string,class="num">0,FLAG_KEEP_CONNECTION|FLAG_RELOAD|FLAG_PRAGMA_NOCACHE,class="num">0); if(hRequest<=class="num">0) { Print("-Err OpenRequest"); InternetCloseHandle(this.m_connection);
「XML-RPC 请求发送与响应回收的实现细节」
在 MT5 的 MQL5 环境里做 XML-RPC 调用,发送环节最容易漏掉的是请求头构造与句柄清理。下面这段代码把编码、发送、读回、关句柄串在了一个函数尾部,可直接复制到自己的网络类里验证。
class=class="str">"cmt">//-- sending the request CXMLRPCEncoder encoder; class="type">class="kw">string header=encoder.header(m_query_path,ArraySize(data)); class="type">int aH=HttpAddRequestHeadersW(hRequest,header,StringLen(header),HTTP_ADDREQ_FLAG_ADD|HTTP_ADDREQ_FLAG_REPLACE); class="type">bool hSend=HttpSendRequestW(hRequest,empty_string,class="num">0,data,ArraySize(data)-class="num">1); if(hSend!=true) { class="type">int err=class="num">0; err=GetLastError(); Print("-Err SendRequest= ",err); } class="type">class="kw">string res; ReadPage(hRequest,res,false); CString out; out.Assign(res); out.Remove("\n"); class=class="str">"cmt">//--- closing all handles InternetCloseHandle(hRequest); InternetCloseHandle(hSend); CXMLRPCResult* result = new CXMLRPCResult(out.Str()); class="kw">return result; }
CXMLRPCEncoder 生成符合 XML-RPC 规范的 HTTP 头,ArraySize(data) 是 payload 字节数;第 3 行 HttpAddRequestHeadersW 把头塞进请求句柄,标志位用了 ADD|REPLACE 防止重复头。第 4 行发请求时长度传 ArraySize(data)-1,这是刻意去掉末尾的结束符,否则服务端可能解析失败。
发送失败分支里用 GetLastError() 抓错误码并 Print 出来,实际调试时常见返回 5200 左右的 WinINet 错误,对应证书或代理问题。第 9~12 行把响应读进 CString 并 Remove("\n"),因为 XML-RPC 响应常带换行,不剥掉会让后续 XML 解析器报错。
最后两行关句柄有个坑:InternetCloseHandle(hSend) 关的其实是 bool 变量而非句柄,真句柄是 hRequest,这行在官方示例里也这么写,但跑起来不会崩只是无效调用。外汇与贵金属 EA 调用外部 API 本身有断网重连风险,建议把发送封装成带重试次数的函数,单次失败不直接丢结果。
class=class="str">"cmt">//-- sending the request CXMLRPCEncoder encoder; class="type">class="kw">string header=encoder.header(m_query_path,ArraySize(data)); class="type">int aH=HttpAddRequestHeadersW(hRequest,header,StringLen(header),HTTP_ADDREQ_FLAG_ADD|HTTP_ADDREQ_FLAG_REPLACE); class="type">bool hSend=HttpSendRequestW(hRequest,empty_string,class="num">0,data,ArraySize(data)-class="num">1); if(hSend!=true) { class="type">int err=class="num">0; err=GetLastError(); Print("-Err SendRequest= ",err); } class="type">class="kw">string res; ReadPage(hRequest,res,false); CString out; out.Assign(res); out.Remove("\n"); class=class="str">"cmt">//--- closing all handles InternetCloseHandle(hRequest); InternetCloseHandle(hSend); CXMLRPCResult* result = new CXMLRPCResult(out.Str()); class="kw">return result; }
◍ 用 MT5 直连汇率接口做币种换算
在 MQL5 里调用外部网络服务,最轻量的练手案例是把一种货币按实时汇率换算成另一种。这里用的公开接口是 foxrate 的 currencyConvert 方法,它吃三个参数:源币种字符串、目标币种字符串、待换算的浮点金额。 因为返回的是包在 XML 里的复合结构,代码里用 parseXMLResponseRAW() 把原始响应拆开。Print 出来的结果通常含三项:第一项 0 表示无错误,第二项是换算后的基础货币数额,第三项是缓存里的最新汇率时间戳。 下面这段脚本把 10000.0 的 GBP 换成 USD,跑完你能在 MT5 专家日志里直接看到发出的 XML-RPC 请求和回包。外汇与贵金属本身波动剧烈、杠杆风险高,这类接口汇率只作参考,别拿它当成交价。 别把外部接口当永远在线 foxrate.org 这类免费 RPC 服务可能限流或停更,实盘前先手动跑一次确认返回结构没变,否则解析会直接越界报错。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MQL5-RPC_ex1_ExternalWebService.mq5 | class=class="str">"cmt">//| Copyright class="num">2011, Investeo.pl | class=class="str">"cmt">//| http://www.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">//www.investeo.pl" class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- class="macro">#include <MQL5-RPC.mqh> class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include <Arrays\ArrayDouble.mqh> class="macro">#include <Arrays\ArrayString.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">//--- class=class="str">"cmt">//--- web service test CArrayObj* params = new CArrayObj; CArrayString* from = new CArrayString; CArrayString* to = new CArrayString; CArrayDouble* amount = new CArrayDouble; from.Add("GBP"); to.Add("USD"); amount.Add(class="num">10000.0); params.Add(from); params.Add(to); params.Add(amount); CXMLRPCQuery query("foxrate.currencyConvert", params); Print(query.toString()); CXMLRPCServerProxy s("foxrate.org/rpc"); CXMLRPCResult* result; result = s.execute(query); result.parseXMLResponseRAW(); Print(result.toString()); class="kw">delete params; class="kw">delete result; } class=class="str">"cmt">//+------------------------------------------------------------------+ <?xml version="class="num">1.0" ?> <methodCall> <methodName>foxrate.currencyConvert</methodName> <params> <param> <value> <class="type">class="kw">string>GBP</class="type">class="kw">string> </value> </param> <param> <value>
XML-RPC 账户余额查询的往返报文
MT5 终端与外部分析工具之间常用 XML-RPC 传递账户状态。一次典型的 methodCall 会把币种符号和数值参数打进 <param> 节点,例如下方请求里塞了 USD 字符串与 10000.00000000 的双精度数值,代表以美元计、初始权益 1 万的外汇账户查询。 服务端回的 methodResponse 用 struct 承载字段:flerror 为 0 表示调用无报错,amount 返回 15773 这个 double 值,即该账户在某一时刻的权益读数约 15773(单位与请求币种一致)。外汇与贵金属杠杆品种权益随报价跳变,该数值仅反映快照,不预示后续走向。 抓这段报文的意义在于:你可以拿它对照自己 EA 或 AIGC 脚本发出的请求,确认字段名(flerror / amount)和类型(int / double)没拼错,否则终端会静默拿到错值。开 MT5 接一个本地 XML-RPC 回环,把 10000 改成你实盘净值,看回包 amount 是否对得上。
「XML-RPC 回传里的缓存标记」
上面这段是 MT5 终端通过 XML-RPC 与本地分析桥接服务通信时,methodResponse 节点下的原始回包片段。它用 struct 嵌套 member 的方式,把一次指标查询的结果序列化回来。 注意第二个 member 的 name 是 message,其 value 里的 string 写死为 cached,说明这次响应并非实时计算,而是命中了服务端缓存。外汇与贵金属行情高频跳动,缓存数据可能滞后于盘口 1~3 个 tick,用于低频策略尚可,做 scalping 需警惕。 回包末尾的 0:15773.00000000:cached: 是桥接层打的纯文本尾行,15773.00000000 即该次查询返回的数值(例如某品种的点值或缓存时间戳换算值),冒号后的 cached 与 XML 内 message 字段互证。开 MT5 接上同款 EA,抓包看尾行就能判断你的调用是否走了缓存。
<value> </member> <member> <name>message</name> <value> <class="type">class="kw">string>cached</class="type">class="kw">string> </value> </member> </class="kw">struct> </value> </param> </params> </methodResponse> class="num">0:class="num">15773.00000000:cached:
◍ 把 2011 自动锦标赛数据拉进 MT5 的思路
想从 2011 年自动交易锦标赛里取统计资料,并在 MetaTrader 5 客户端内直接使用,关键不在重算历史,而是把外部服务返回的数据接进终端。当时 MetaQuotes 正在准备信号跟踪服务,它允许订阅具体 EA 的信号,这给外部数据和分析程序打通了通道。 把信号订阅能力和你自己的分析脚本组合起来,就有可能做更复杂的横截面分析,比如跨来源比对表现、筛选特定风格的参赛 EA。实际落地时,你是用 XML-RPC 从外部拿到数据,再在 MT5 里按自己的逻辑重构策略或监控面板。 外汇与贵金属市场高杠杆、滑点和断连都会扭曲这类统计结论,任何从历史锦标赛得出的倾向都只是概率,必须在实盘前用 MT5 策略测试器复核。
用 Python 拉取锦标赛仓位做交叉验证
想看清自动交易锦标赛里领先者的真实动作,第一步是把网页上的参赛者数据抓下来结构化。比如只挑净资产过某一门槛的人,统计他们当前 EURUSD 多空仓位的分布,这类样本在 2011 年赛事中约有 2/3 的领先者挂着 EURUSD 买单。 环境搭起来不难:装 Python 2.7.1 加 setup_tools,进 C:\Python27\Scripts 跑 easy_install BeautifulSoup 即可。装完在 Python 控制台敲 import BeautifulSoup 不报错,就说明解析库就位。 下面这段就是当时用的轻量爬虫核心,直接连锦标赛用户页,从 HTML 里抠出余额、净值和未平仓位表。 跑 ContestantParser.py 后,控制台会吐出姓名、余额、净值和持仓。把这层数据再包成 XML-RPC 服务(SimpleXMLRPCServer 提供 listContestants 和 getStats),MT5 端就能按需拉统计。 用 getStats('eurusd') 返回三个数:过门槛总人数、持 EURUSD 未平仓人数、其中持 EURUSD 买单人数。若你的 EA 或指标也发出“EURUSD 买入持仓”信号,而样本里多数领先者同样暴露该仓位,这个信号得到交叉确认的概率更高,但外汇/贵金属本身高风险,别当成必然。
C:\Python27\Scripts>easy_install BeautifulSoup Searching for BeautifulSoup Reading http:class=class="str">"cmt">//pypi.python.org/simple/BeautifulSoup/ Reading http:class=class="str">"cmt">//www.crummy.com/software/BeautifulSoup/ Reading http:class=class="str">"cmt">//www.crummy.com/software/BeautifulSoup/download/ Best match: BeautifulSoup class="num">3.2.class="num">0 Downloading http:class=class="str">"cmt">//www.crummy.com/software/BeautifulSoup/download/class="num">3.x/BeautifulS oup-class="num">3.2.class="num">0.tar.gz Processing BeautifulSoup-class="num">3.2.class="num">0.tar.gz Running BeautifulSoup-class="num">3.2.class="num">0\setup.py -q bdist_egg --dist-dir c:\users\przemek\ap pdata\local\temp\easy_install-zc1s2v\BeautifulSoup-class="num">3.2.class="num">0\egg-dist-tmp-hnpwoo zip_safe flag not set; analyzing archive contents... C:\Python27\lib\site-packages\setuptools\command\bdist_egg.py:class="num">422: UnicodeWarnin g: Unicode equal comparison failed to convert both arguments to Unicode - interp reting them as being unequal symbols = dict.fromkeys(iter_symbols(code)) Adding beautifulsoup class="num">3.2.class="num">0 to easy-install.pth file Installed c:\python27\lib\site-packages\beautifulsoup-class="num">3.2.class="num">0-py2.class="num">7.egg Processing dependencies for BeautifulSoup Finished processing dependencies for BeautifulSoup C:\Python27\Scripts> Python class="num">2.7.class="num">1 (r271:class="num">86832, Nov class="num">27 class="num">2010, class="num">18:class="num">30:class="num">46) [MSC v.class="num">1500 class="num">32 bit(Intel)] on win32 Type "copyright", "credits" or "license()" for more information. >>> class="kw">import BeautifulSoup >>> class="kw">import re class="kw">import urllib from BeautifulSoup class="kw">import BeautifulSoup class ContestantParser(object): URL_PREFIX = &class="macro">#x27;[MQL5官方文档] def __init__(self, name): print "User:", name url = ContestantParser.URL_PREFIX + name feed = urllib.urlopen(url) s = BeautifulSoup(feed.read()) account = s.findAll(&class="macro">#x27;td&class="macro">#x27;, attrs={&class="macro">#x27;class&class="macro">#x27; : &class="macro">#x27;alignRight&class="macro">#x27;}) self.balance = class="type">float(account[class="num">0].contents[class="num">0].replace(&class="macro">#x27; &class="macro">#x27;, &class="macro">#x27;&class="macro">#x27;)) self.equity = class="type">float(account[class="num">2].contents[class="num">0].replace(&class="macro">#x27; &class="macro">#x27;, &class="macro">#x27;&class="macro">#x27;)) terminals = s.findAll(&class="macro">#x27;table&class="macro">#x27;, attrs={&class="macro">#x27;class&class="macro">#x27;: &class="macro">#x27;terminal&class="macro">#x27;}) terminal = terminals[class="num">0] trs = terminal.findAll(&class="macro">#x27;tr&class="macro">#x27;) pairs = terminal.findAll(&class="macro">#x27;span&class="macro">#x27;, attrs={&class="macro">#x27;class&class="macro">#x27;:&class="macro">#x27;stateText&class="macro">#x27;}) self.stats = {} for i in range(len(pairs)-class="num">1): if len(pairs[i].class="type">class="kw">string)> class="num">6:
「用 Python 扒冠军账户持仓做镜像参考」
这段脚本不是 MQL5,而是配套的 Python 爬虫,用来批量抓取某届交易赛排行榜里权益高于阈值的账户,并解析其持仓品种与方向。核心类是 ContestantsFinder,它按权益降序翻页,直到单页出现权益低于 20000.0 的选手才停。 初始化时传入 min_equity=20000.0,脚本会拼接带 ?orderby=equity&dir=down 的列表 URL,用 BeautifulSoup 捞 class 匹配 row.* 的表格行,从 contents[19] 取权益数值并去空格转 float。 另一个 ContestantParser 类负责读单人页:position('eurusd') 返回该账户在欧美上的方向,示例跑出来 Tim 无欧美持仓(none),enivid 则是 eurusd sell。外汇与贵金属自带高杠杆高风险,这类榜单持仓只可作反向样本或相关性观察,复制前先在 MT5 策略测试器验证自己的风控边界。 想落地就改一行:把 ContestantsFinder(20000.0) 的阈值调成你关心的权益门槛,或直接把 'eurusd' 换成 'xauusd' 看黄金持仓分布倾向。
class="kw">break self.stats[str(pairs[i].class="type">class="kw">string)] = str(trs[i+class="num">1].contents[class="num">7].class="type">class="kw">string) def __str__(self): for k in self.stats.keys(): print k, self.stats[k] class="kw">return "Bal " + str(self.balance) + " Equ " + str(self.equity) def position(self, pair): if pair in self.stats.keys(): class="kw">return self.stats[pair] else: class="kw">return "none" class ContestantsFinder(object): URL_PREFIX = "[MQL5官方文档] URL_SUFFIX = "?orderby=equity&dir=down" def __init__(self, min_equity): self.min_equity = min_equity self.user_list = [] self.__find() def __find(self): isLastPage = False pageCnt = class="num">1 while isLastPage == False: url = ContestantsFinder.URL_PREFIX + str(pageCnt) + \ ContestantsFinder.URL_SUFFIX feed = urllib.urlopen(url) s = BeautifulSoup(feed.read()) urows = s.findAll(&class="macro">#x27;tr&class="macro">#x27;, attrs={&class="macro">#x27;class&class="macro">#x27; : re.compile(&class="macro">#x27;row.*&class="macro">#x27;)}) for row in urows: user_name = row.contents[class="num">5].a.class="type">class="kw">string equity = class="type">float(row.contents[class="num">19].class="type">class="kw">string.replace(&class="macro">#x27; &class="macro">#x27;,&class="macro">#x27;&class="macro">#x27;)) if equity <= self.min_equity: isLastPage = True class="kw">break self.user_list.append(str(user_name)) print user_name, equity pageCnt += class="num">1 def list(self): class="kw">return self.user_list if __name__ == "__main__": # find all contestants with equity larger than threshold contestants = ContestantsFinder(class="num">20000.0) print contestants.list() # display statistics print "* Statistics *" for contestant in contestants.list(): u = ContestantParser(contestant) print u print u.position(&class="macro">#x27;eurusd&class="macro">#x27;) print &class="macro">#x27;-&class="macro">#x27; * class="num">60 * Statistics * User: Tim Bal class="num">31459.2 Equ class="num">31459.2 none ------------------------------------------------------------ User: enivid eurusd sell
◍ 用 XML-RPC 扒出选手持仓的净向分布
上面这段日志是某次交易竞赛后台的实况切片:用户 ias 在 EURAUD 空单后切换成多币种网格(EURJPY 空、USDCHF 空、GBPUSD 多…),其余额从 26179.98 跌到 15670.0,权益同步从 29779.89 滑到 29345.66;而 west100 在 EURUSD 多单下余额 27980.5 却权益仅 26255.5,说明浮亏已吃掉近 1700 点账户面值。这类逐人快照若靠肉眼扫,十几个账号就够呛。 工程上更干净的做法是把持仓抓取做成服务。下面这段 Python 2 的 XML-RPC 服务端绑在 192.168.235.168:6666,对外只暴露 listContestants 与 getStats 两个方法;客户端一行 ServerProxy 就能拉数据。
from SimpleXMLRPCServer class="kw">import SimpleXMLRPCServer from SimpleXMLRPCServer class="kw">import SimpleXMLRPCRequestHandler from PositionGrabberTest class="kw">import ContestantParser, ContestantsFinder class RequestHandler( SimpleXMLRPCRequestHandler ): rpc_path = ( &class="macro">#x27;/RPC2&class="macro">#x27;, ) class ATC2011XMLRPCServer(SimpleXMLRPCServer): def __init__(self): SimpleXMLRPCServer.__init__( self, ("class="num">192.168.class="num">235.168", class="num">6666), requestHandler=RequestHandler, logRequests = False) self.register_introspection_functions() self.register_function( self.xmlrpc_contestants, "listContestants" ) self.register_function( self.xmlrpc_get_position_stats, "getStats" ) def xmlrpc_contestants(self, min_equity): try: min_equity = class="type">float(min_equity) except ValueError as error: print error class="kw">return [] self.contestants = ContestantsFinder(min_equity) class="kw">return self.contestants.list() def xmlrpc_get_position_stats(self, pair): total_users = len(self.contestants.list()) holding_pair = class="num">0 buy_positions = class="num">0 for username in self.contestants.list(): u = ContestantParser(username) position = u.position(pair) if position != "none": holding_pair += class="num">1 if position == "buy": buy_positions += class="num">1 class="kw">return [ total_users, holding_pair, buy_positions ] if __name__ == &class="macro">#x27;__main__&class="macro">#x27;: server = ATC2011XMLRPCServer() server.serve_forever()
from SimpleXMLRPCServer class="kw">import SimpleXMLRPCServer from SimpleXMLRPCServer class="kw">import SimpleXMLRPCRequestHandler from PositionGrabberTest class="kw">import ContestantParser, ContestantsFinder class RequestHandler( SimpleXMLRPCRequestHandler ): rpc_path = ( &class="macro">#x27;/RPC2&class="macro">#x27;, ) class ATC2011XMLRPCServer(SimpleXMLRPCServer): def __init__(self): SimpleXMLRPCServer.__init__( self, ("class="num">192.168.class="num">235.168", class="num">6666), requestHandler=RequestHandler, logRequests = False) self.register_introspection_functions() self.register_function( self.xmlrpc_contestants, "listContestants" ) self.register_function( self.xmlrpc_get_position_stats, "getStats" ) def xmlrpc_contestants(self, min_equity): try: min_equity = class="type">float(min_equity) except ValueError as error: print error class="kw">return [] self.contestants = ContestantsFinder(min_equity) class="kw">return self.contestants.list() def xmlrpc_get_position_stats(self, pair): total_users = len(self.contestants.list()) holding_pair = class="num">0 buy_positions = class="num">0 for username in self.contestants.list(): u = ContestantParser(username) position = u.position(pair) if position != "none": holding_pair += class="num">1 if position == "buy": buy_positions += class="num">1 class="kw">return [ total_users, holding_pair, buy_positions ] if __name__ == &class="macro">#x27;__main__&class="macro">#x27;: server = ATC2011XMLRPCServer() server.serve_forever()
用 MQL5-RPC 拉取 ATC2011 锦标赛数据
想在 MT5 里直接拿到自动锦标赛的参赛与持仓统计,不必手动导表,MQL5-RPC 框架可以把查询封装成 XML 发到远端服务。实际部署时,Linux 主机跑 Python 的 XML-RPC 服务,Windows 虚拟机里的 MT5 做客户端调用,跨系统打通很轻量。 脚本启动后先建一个指向 192.168.235.168:6666 的代理对象,第一个查询 listContestants 带参数 20000.0,意为筛选初始资金 2 万的那批参赛者;Print(query.toString()) 会把封装好的 XML 打到日志,方便确认报文格式。 第二个查询 getStats 传 eurusd 字符串,远端只回三个整数:参赛人数、EURUSD 总持仓数、其中 BUY 方向持仓数。代码里用 CArrayInt 接结果并拼成一句可读输出,例如「Contestants = X. EURUSD positions = Y. BUY positions = Z」,具体数值取决于你服务端统计时刻的实况。 下面这段是客户端核心,含两次查询与结果解析,行内已按中文逐行拆清逻辑: //+------------------------------------------------------------------+
| // | MQL5-RPC_ex2_ATC2011AnalyzerClient.mq5 |
|---|---|
| // | Copyright 2011, Investeo.pl |
| // | http://www.investeo.pl |
//+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" // 脚本版权声明 #property link "http://www.investeo.pl" // 作者链接 #property version "1.00" // 版本号 #include <MQL5-RPC.mqh> // RPC 框架头文件 #include <Arrays\ArrayObj.mqh> // 对象数组 #include <Arrays\ArrayInt.mqh> // 整型数组 #include <Arrays\ArrayDouble.mqh> // 双精度数组 #include <Arrays\ArrayString.mqh> // 字符串数组 //+------------------------------------------------------------------+
| // | Script program start function |
|---|
//+------------------------------------------------------------------+ void OnStart() // 脚本入口 { //--- //--- ATC 2011 analyzer test CXMLRPCServerProxy s("192.168.235.168:6666"); // 建远端代理,填你服务IP端口 CXMLRPCResult* result; // 响应指针 //--- Get list of contestants CArrayObj* params1 = new CArrayObj; // 参数容器 CArrayDouble* amount = new CArrayDouble; // 双精度参数 amount.Add(20000.0); params1.Add(amount); // 加2万阈值 CXMLRPCQuery query("listContestants", params1); // 封装查询 Print(query.toString()); // 打印请求XML result = s.execute(query); // 执行并拿结果 Print(result.toString()); // 打印响应 delete result; // 释放 //--- Get position statistics CArrayObj* params2 = new CArrayObj; // 第二查询参数 CArrayString* pair = new CArrayString; // 字符串参数 pair.Add("eurusd"); params2.Add(pair); // 传 EurUsd CXMLRPCQuery query2("getStats", params2); // 统计查询 Print(query2.toString()); // 打印请求 result = s.execute(query2); // 执行 CArrayObj* resultArray = result.getResults(); // 取结果数组 CArrayInt* stats = resultArray.At(0); // 首个整型组 Print("Contestants = " + IntegerToString(stats.At(0)) + // 人数 ". EURUSD positions = " + IntegerToString(stats.At(1)) + // 总仓 ". BUY positions = " + IntegerToString(stats.At(2))); // 买仓 delete params1; // 清参数1 delete params2; // 清参数2 delete result; // 清响应 } //+------------------------------------------------------------------+ <?xml version="1.0" ?> <methodCall> <methodName>listContestants</methodName> <params> <param> <value>
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MQL5-RPC_ex2_ATC2011AnalyzerClient.mq5 | class=class="str">"cmt">//| Copyright class="num">2011, Investeo.pl | class=class="str">"cmt">//| http://www.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">//www.investeo.pl" class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <MQL5-RPC.mqh> 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- class=class="str">"cmt">//--- ATC class="num">2011 analyzer test CXMLRPCServerProxy s("class="num">192.168.class="num">235.168:class="num">6666"); CXMLRPCResult* result; class=class="str">"cmt">//--- Get list of contestants CArrayObj* params1 = new CArrayObj; CArrayDouble* amount = new CArrayDouble; amount.Add(class="num">20000.0); params1.Add(amount); CXMLRPCQuery query("listContestants", params1); Print(query.toString()); result = s.execute(query); Print(result.toString()); class="kw">delete result; class=class="str">"cmt">//--- Get position statistics CArrayObj* params2 = new CArrayObj; CArrayString* pair = new CArrayString; pair.Add("eurusd"); params2.Add(pair); CXMLRPCQuery query2("getStats", params2); Print(query2.toString()); result = s.execute(query2); CArrayObj* resultArray = result.getResults(); CArrayInt* stats = resultArray.At(class="num">0); Print("Contestants = " + IntegerToString(stats.At(class="num">0)) + ". EURUSD positions = " + IntegerToString(stats.At(class="num">1)) + ". BUY positions = " + IntegerToString(stats.At(class="num">2))); class="kw">delete params1; class="kw">delete params2; class="kw">delete result; } class=class="str">"cmt">//+------------------------------------------------------------------+ <?xml version="class="num">1.0" ?> <methodCall> <methodName>listContestants</methodName> <params> <param> <value>
「XML-RPC 返回的登录名清单长这样」
MT5 终端跟信号服务后端走的是 XML-RPC 协议,请求里把参数用 <param> 节点塞进去,比如某次调用传了 double 类型的 20000.00000000 作为数值参数。 服务端回包是 methodResponse 结构,数据装在 array/data 下的多个 <value><string> 节点里。实测一次返回拿到了 4 个字符串:lf8749、ias、Xupypr、aharata,这通常是关联账号或信号提供者的登录标识。 在 MT5 里抓这类包,可以用本机回环监听或者路由器镜像,重点看 data 节点数量——如果返回少于 1 个 value,说明账号授权可能已失效,外汇和贵金属信号订阅本身带高风险,标识核对错就可能跟到假源。
◍ XML 节点里的用户名清单怎么读
这段配置片段用嵌套的 value / string 标签列出了若干用户标识,是 EA 或指标里常见的白名单写法。 外层每个 <value> 包一个 <string>,字符串内容即具体用户名:beast、Tim、tmt0086、yyy999、bobsley、Diubakin,共 6 个条目。 在 MT5 里若看到这类结构,通常是用于限制脚本仅对特定账户名生效;直接改 <string> 里的文本就能增删许可对象,无需动逻辑代码。外汇与贵金属交易杠杆高,这类权限控制只防误用,不降低市场风险。
EA 授权名单的 XML 结构长这样
在 MT5 的 EA 配置文件里,授权用户清单常以嵌套 XML 片段出现,外层是多个 <value> 节点,每个节点内包一个 <string> 存具体账户名。
从这段原始片段能看到 6 个 <value>:依次装了 Pirat、AAA777、notused、ronnielee、samclider-2010,以及最后一个空 <value> 待填。
空节点不是笔误,多半是预留位——你往里补 <string>你的账户ID</string> 再存盘,EA 就可能放行该账户运行。外汇与贵金属杠杆品种波动剧烈,改配置前先在模拟盘验证授权逻辑,避免实盘误锁。
「配置文件里的用户名与整数类型字段」
在 MQL5 程序的 XML 风格配置段中,value 节点常用来承载具体参数,其内部再以 string 标记写入字面量。上面这段配置就连续列出了 gery18、Integer、GradyLee、p96900、skarkalakos、Loky 这些字符串,彼此互无类型关联,只是顺序入表。 其中第二个 value 写入的「Integer」并非关键字声明,而是当作普通字符串存进配置,真正决定变量类型的逻辑在 EA 源码里另行处理。若你在 MT5 里导入类似配置,看到 Integer 出现在 string 标签内,不必误以为系统已自动转型。 这类配置段在外汇与贵金属 EA 中多用于社区信号订阅者白名单,字符串顺序即读取顺序。高杠杆品种下误改此处可能导致信号过滤失效,调参前先备份原文件。
◍ 信号订阅者的字段结构
在 MQL5 信号数据库的 XML 输出里,每个订阅者记录并非扁平字符串,而是被包在 <value> 节点下、内部再以 <string> 承载用户名。上面这段原始片段列出了 6 个连续条目:zephyrrr、Medvedev、Gans-deGlucker、InvestProm、zioliberato、QuarkSpark。
这种嵌套写法意味着,如果你用 MQL5 的 XML 解析接口(或外部脚本)抓取信号跟随者列表,必须向下钻两层才能拿到可读的登录名,而不是在根节点直接遍历文本。
实盘外接分析时注意:外汇与贵金属杠杆交易风险较高,订阅信号不代表历史收益可复制,解析出的名单仅反映某一时刻的跟随关系,可能随服务器快照变化而失效。
XML-RPC 回传里的用户标识清单
上面这段 XML-RPC 响应片段里,methodResponse 下的 params 结构通过 array 嵌套了多组 value/string,实际回传了 4 个论坛用户名:rho2011、ld73、enivid、botaxuper。 紧随其后的纯文本行又列了一长串标识,例如 lf8749、ias、Xupypr、aharata、beast、Tim、tmt0086、yyy999、bobsley、Diubakin、Pirat、AAA777、notused、ronnielee、samclider-2010、gery18、Integer、GradyLee、p96900、skarkalakos、Loky、zephyrrr、Medvedev、Gans-deGlucker、InvestProm、zioliberato、QuarkSpark 等,合计约 34 个,其中 rho2011、ld73、enivid、botaxuper 与前面 XML 块完全重复。 末尾另起一段以 <?xml version="1.0" ?> 开头,发起了一个新的 methodCall,methodName 明确为 getStats——说明这套通信在拉取统计前,先用响应把可用用户集抛了出来。开 MT5 接这类 XML-RPC 时,可先解析 methodResponse 里的 array 拿到核心 4 个 ID,再对照后续文本行做差集校验。
「EURUSD 指标参数的 RPC 返回结构」
通过 XML-RPC 向 MT5 网关查询 EURUSD 的某组指标参数时,请求体只需在 methodCall 的 params 里塞一个 string 类型的值 eurusd,服务端据此回吐对应品种的配置。 响应被包在 methodResponse/params/param/value 下的 array/data 中,实际返回了三个整型值:31、10、3。这三个数大概率对应均线周期、偏移量或平滑层数中的某组组合,具体含义取决于你本地 EA 的解析约定。 外汇与贵金属品种的高杠杆特性意味着,任何参数回读结果都只是当前环境的快照,不能直接当作开仓依据;拿到 31/10/3 后应在策略测试器里跑一遍验证稳定性。 别把回读数值当默认最优 RPC 返回的整型数组是服务端当前状态,不是历史胜率背书。换品种或重连后同样请求可能给出不同序列,务必在调用层做边界判断。
<?xml version="class="num">1.0" ?> <methodCall> <params> <param> <value> <class="type">class="kw">string>eurusd</class="type">class="kw">string> </value> </param> </params> </methodCall> <?xml version="class="num">1.0" ?> <methodResponse> <params> <param> <value> <array> <data> <value><class="type">int>class="num">31</class="type">int></value> <value><class="type">int>class="num">10</class="type">int></value> <value><class="type">int>class="num">3</class="type">int></value> </data> </array> </value> </param> </params> </methodResponse>
◍ 从 RPC 回传看参赛账户的真实持仓分布
上面这段是 MQL5-RPC 客户端在 AUDUSD H1 图表上跑出来的原始回传尾段,时间戳停在 12:39:23 到 12:39:29 之间,中间列了 31 个参赛者代号,说明这次分析拉取的是 2011 年自动交易大赛的选手样本。 紧接着的汇总行给出硬数据:Contestants = 31,也就是实际纳入统计的账户为 31 个;其中 EURUSD 持仓共 10 个,BUY 方向仅 3 个。也就是说在那一瞬间,这批选手里做欧美的不到三分之一,而多单占比只有 30%,净倾向偏空或观望。 把这种回传接进你自己的 MT5 脚本,就能在盘中对任意品种复刻「选手情绪快照」。外汇与贵金属杠杆高、跳空频繁,这类群体仓位只是概率参考,不能单独作为下单依据,真要跟得先在小周期验证离散度。
</value> </param> </params> </methodResponse> MQL5-RPC_ex2_ATC2011AnalyzerClient(AUDUSD,H1) class="num">12:class="num">39:class="num">23 lf8749:ias:Xupypr:aharata:beast:Tim:tmt0086:yyy999:bobsley:Diubakin:Pirat: AAA777:notused:ronnielee:samclider-class="num">2010:gery18:Integer:GradyLee:p96900: skarkalakos:Loky:zephyrrr:Medvedev:Gans-deGlucker:InvestProm:zioliberato: QuarkSpark:rho2011:ld73:enivid:botaxuper: MQL5-RPC_ex2_ATC2011AnalyzerClient(AUDUSD,H1) class="num">12:class="num">39:class="num">29 Contestants = class="num">31. EURUSD positions = class="num">10. BUY positions = class="num">3
框架开源与后续玩法
把 MT5 的 MQL5 脚本通过 XML-RPC 协议甩到外部服务去跑,这套思路在 2011 年自动交易锦标赛的分析程序里已经验证过可行性。作者把整套 MQL5-RPC 框架按 GPL 放了出来,附件 mql5-rpc.zip 约 22.18 KB,里面含两个例程:一个是抓网络服务数据,一个是锦标赛自定义分析。 想动手的人直接下 ZIP 解压,用 MT5 把例子里的 EA 挂上,再把 XML-RPC 服务端地址改成你自己的局域网或云主机,就能把行情计算挪到 Python 或 C# 侧。外汇与贵金属杠杆高,远程调用一旦延迟或断连,订单逻辑可能失真,先拿模拟盘测链路稳定性。 代码后续维护靠社区,原作者点名欢迎做补丁和重构的人进项目;MT4 用户问过兼容,目前只见过 MT5 版,别在四系终端里硬塞。