开发回放系统(第 39 部分):铺平道路(三)(基础篇)
「给回放系统打地基的第三块砖」
在 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,确保吃到最新代码。 这套骨架搭完,双向通信仍差最后一步:从指标读数据回来。
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,保证卸载时图表干净,不会留下孤儿指标对象。
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)