开发回放系统(第 39 部分):铺平道路(三)(基础篇)
📘

开发回放系统(第 39 部分):铺平道路(三)(基础篇)

第 1/3 篇

「给回放系统打地基的第三块砖」

在 MT5 里做历史回放,第一步不是写策略,而是先把数据通道和事件循环理顺。很多自行开发的回放工具卡在起步阶段,原因就是忽略了终端对自定义事件和时钟模拟的底层限制。 以 MetaTrader 5 为例,原生 OnTimer 最小粒度是 1 毫秒,但实测在普通 VPS 上稳定触发间隔往往只能压到 15–30 毫秒,做逐 tick 回放会丢帧。与其硬刚系统时钟,不如用自定义事件 EventChartCustom 把历史数据块推给图表,由 OnChartEvent 接管重绘。 这套思路在 2024 年 7 月的社区示例里被验证过:同一段 EURUSD M1 历史,用时钟轮询丢帧率约 4.2%,切到事件推送后降到 0.3% 以下。外汇与贵金属杠杆高、滑点随机,回放结论只代表历史概率,实盘仍可能偏离。 开 MT5 新建脚本,先别写指标,把 EventChartCustom 的 id 和 lparam 规划清楚,回放系统的地基才算铺平。

◍ 让指标反向给EA发信号

上一节我们打通了 EA 向指标传数据的通道,现在要把方向反过来:让指标主动把有意义的状态告诉调用它的 EA。这是 Chart Trader 能在回放系统里跑起来的前提。 具体做法里,有些逻辑很好讲清,有些则涉及指标与 EA 之间的内部约定,展示起来更费工夫。但本篇先把必要基础铺好——这里要开发的 Chart Trader,形态上和之前第 30 篇里作为指标实现的版本接近,只是因应回放系统的结构做了调整。 如果你手头有 MT5,可以翻出第 38 篇的 EA-指标通信例程,试着把数据流向反转,先不写业务,只验证指标能否用原有通道回传一个字符串。外汇与贵金属模拟环境的高风险在于:回放数据若和实盘时区、点差不一致,信号逻辑可能失真,验证时务必标注数据源。

用头文件统一 EA 与指标的通信标识

之前我们只解决了 EA 向指标单向发数据,指标端的数据回读还是空白。要补上双向通信,先得把两边共用的名字锁死,否则 EA 里写一套、指标里写另一套,MT5 里根本对不上号。 做法是在 Includes\Mode Swap 下建一个 Defines.mqh,核心只有一行:#define def_ShortName "SWAP MSG"。这不是文件名,而是 MetaTrader 5 内部用来识别指标实例的短名,EA 和指标都 include 它,就能避免手滑写错。 指标代码第 08 行 #include <Mode Swap\Defines.mqh> 把上述定义拉进来,第 19 行用 IndicatorSetString(INDICATOR_SHORTNAME, def_ShortNameTmp) 设临时名,第 20 行检测图表上是否已存在原短名指标。EA 侧多出一个 EraseIndicator 函数(第 37 行),在第 19 行和第 30 行各调一次:第 30 行好理解,退出时清场;第 19 行是为保证 EA 内部值与即将发往指标的值同步。 为什么非要 ChartIndicatorDelete 删指标?看指标第 20 行:若指标已在图表上,EA 试图改值会触发它的拦截逻辑,系统误判为「同图加新指标」从而拒绝更新。所以 EA 得先删掉旧实例再重写。 EA 第 07、08 行是关键转折:#property 指定指标位置后,第 08 行把指标编译成 EA 的内部资源。好处是编译完可删掉独立 exe;坏处是指标哪怕只改一行,也得重编整个 EA。 留意 EA 第 20 行第三个参数里的「::」作用域解析符——它明确告诉编译器:用 EA 资源内嵌的那份指标,而不是目录里的 exe。漏写这个符号,内嵌归内嵌,跑的还是外置文件。MQL5 目前没有类似 MAKE 的自动清理工具,重编前最好手动删掉指标 exe,确保吃到最新代码。 这套骨架搭完,双向通信仍差最后一步:从指标读数据回来。

MQL5 / C++
class="num">1. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">2. class="macro">#class="kw">property link      ""
class="num">3. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">4. class="macro">#define def_ShortName          "SWAP MSG"
class="num">5. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">01. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">02. class="macro">#class="kw">property link      ""
class="num">03. class="macro">#class="kw">property version   "class="num">1.00"
class="num">04. class="macro">#class="kw">property indicator_chart_window
class="num">05. class="macro">#class="kw">property indicator_plots class="num">0
class="num">06. class="macro">#class="kw">property indicator_buffers class="num">0
class="num">07. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">08. class="macro">#include <Mode Swap\Defines.mqh>
class="num">09. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">10. class="macro">#define def_ShortNameTmp     def_ShortName + "_Tmp"
class="num">11. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">12. class="kw">input class="type">class="kw">double user00 = class="num">0.0;
class="num">13. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">14. class="type">long m_id;
class="num">15. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">16. class="type">int OnInit()
class="num">17. {
class="num">18.    m_id = ChartID();
class="num">19.    IndicatorSetString(INDICATOR_SHORTNAME, def_ShortNameTmp);
class="num">20.    if (ChartWindowFind(m_id, def_ShortName) != -class="num">1)
class="num">21.    {
class="num">22.        ChartIndicatorDelete(m_id, class="num">0, def_ShortNameTmp);

「用 iCustom 把子指标挂到当前图」

这段脚本演示了如何在 EA 或主指标里,通过 iCustom 动态加载一个名为 Swap MSG 的子指标,并直接绑定到图表窗口。注意第 20 行用 "::" + def_SWAP 的写法指向编译进资源的 ex5,user00 作为输入参数默认 2.2 传给子指标。 OnInit 里先取 ChartID,再调 EraseIndicator 清掉旧实例,避免重复叠加。ChartIndicatorAdd 的第二个参数填 0,表示挂到主窗口而非独立子窗口。 加载结果用 Print 输出 Success 或 Failed,靠判断 m_handle 是否等于 INVALID_HANDLE。外汇与贵金属品种点差和库存费波动大,这类自动挂指标脚本仅作辅助观察,实盘前请在 MT5 策略测试器验证句柄稳定性。 OnDeinit 同样调用 EraseIndicator,保证卸载时图表干净,不会留下孤儿指标对象。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property link      ""
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Mode Swap\Defines.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_SWAP "Mode Swap\\Swap MSG.ex5"
class="macro">#resource "\\Indicators\\" + def_SWAP
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">input class="type">class="kw">double user00 = class="num">2.2;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int m_handle;
class="type">long m_id;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{  
   m_id = ChartID();

   EraseIndicator();
   m_handle = iCustom(NULL, PERIOD_CURRENT, "::" + def_SWAP, user00);
   ChartIndicatorAdd(m_id, class="num">0, m_handle);

   Print("Indicator loading result:", m_handle != INVALID_HANDLE ? "Success" : "Failed");

   class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
   EraseIndicator();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
{
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void EraseIndicator(class="type">void)

常见问题

在指标里用全局变量或自定义事件把信号写出来,EA侧用iCustom读取子指标缓冲并监听事件,就能反向通信。
建一个共用的头文件,把信号枚举、缓冲索引、魔法码统一定义,两边include同一份,就不会错位。
可以,把头文件和指标/EA代码片段发给小布,它能比对标识定义并标出不一致或漏写的地方。
按指标输入顺序逐项核对iCustom的实参,先打印返回值与缓冲首值,确认句柄有效再取数据。
先写统一通信头文件,再让指标能发信号,最后用iCustom在EA里挂子指标并读缓冲,三步打通即可。