开发回放系统(第 59 部分):新的未来(基础篇)
📘

开发回放系统(第 59 部分):新的未来(基础篇)

第 1/3 篇

给回放系统接上未来数据通道

MetaTrader 5 的回放系统做到第 59 版,核心变化是把「未来」也纳入了可重放的时间轴。传统回放只重播历史,这一版允许在模拟中注入尚未发生的行情片段,用来检验策略在未知波动下的应激表现。 作者在 2025 年 2 月 28 日发布的示例里,回放引擎版本号对应 Build 735 之后的内部迭代,说明官方仍在持续扩展回放 API。对交易者而言,这意味着可以在 MT5 里用更接近实盘演化路径的方式压测 EA,而不只是看历史 K 线倒带。 外汇与贵金属属高杠杆品种,这类未来注入测试只能提高策略鲁棒性概率,不等于实盘免亏。开 MT5 切到回放模式,载入最近一次非农前后的 EURUSD 分钟数据,手动注入一段放大三倍波动的模拟未来,观察 EA 止损逻辑是否提前触发。

「服务被强制提前关闭的来龙去脉」

上一篇文章里提到,系统在模板应用到日程表、再到服务更新日程表这两个动作之间,需要插入一个小延迟。若不插入,模块会强制把服务提前关掉,这不是偶发,而是时序竞争导致的必然结果。 当时有读者问:明明只是两张表先后写数据,怎么会牵连到服务生命周期?本节能把这条链路拆开。先看一段未剪辑的实际演示,它完整记录了失败发生的那一刻——服务在预期时间之前就被终止,日志里没有用户主动关闭的指令。 要解释清楚根因,得回到前文的一个要点:日程表写入和服务状态轮询跑在同一个工作线程里。模板提交后,若立刻触发服务更新,轮询会在旧状态未刷新时读到“空窗”,从而误判为停止信号。下面这段代码就是当时用于复现的最小逻辑片段。

◍ 模板加载后图表自关的异步陷阱

在 MT5 里套用模板后回放/模拟服务提前退场,根子不在模板本身,而在控制模块被异步卸除。很多人以为第 58 行会打印“Modified template”从而证明是模板变更触发关闭,实际终端消息窗口里这条从不出现。 看下面这段 OnDeinit 逻辑:当 reason 为 REASON_TEMPLATE 时才走打印分支,但真正发生的是 MetaTrader 5 在套模板过程中主动删除控制指标,派发的退出原因是 REASON_REMOVE,于是直接落进第 61–63 行执行 ChartClose(user00)。也就是说,事件顺序和“同步套完模板再关”的直觉相反。 为什么平台不把 ChartApplyTemplate 做成同步?答案在性能。一个模板可能挂多个指标甚至 EA,后者要求图表有足够 K 线才能算数;若同步等完,平台可能卡死几秒,极端情况直接崩。异步化换来了流畅,却把“哪些调用立刻返回、哪些后台慢慢跑”的坑留给了写代码的人。 别把正态当圣经:ChartRedraw 强制重绘并不能逼模板立即生效,所以得换阻塞机制而不是把控制指标写进模板。旧版用全局终端变量防指标串图,现在改了路子,模板里再塞控制模块反而会引出更难缠的依赖问题。

MQL5 / C++
class="num">53. class="type">void OnDeinit(const class="type">int reason)
class="num">54. {
class="num">55.    class="kw">switch (reason)
class="num">56.    {
class="num">57.      case REASON_TEMPLATE:
class="num">58.         Print("Modified class="kw">template. Replay class=class="str">"cmt">// simulation system shutting down.");
class="num">59.      case REASON_INITFAILED:
class="num">60.      case REASON_PARAMETERS:
class="num">61.      case REASON_REMOVE:
class="num">62.      case REASON_CHARTCLOSE:
class="num">63.         ChartClose(user00);
class="num">64.         class="kw">break;
class="num">65.    }
class="num">66.    class="kw">delete control;
class="num">67. }

别把图表模板当免死金牌

控制指标源码这次只动了第 26 行:初始化时同时校验 _LastError 与 user00 两个量。user00 在第 16 行声明,类型是 long,默认 0,它负责接收回放/模拟服务派发过来的图表 ID,正常由系统自动填入,用户手动改它基本没意义。 有人会想:等服务把图表完全加载、MT5 稳定后,把整张图存成模板,以后直接调模板不就省事?实测不行。模板文件里第 3 行记了指标位置,第 24~26 行包裹着输入参数段,其中第 25 行把 user00 写死成一个值,本意是让第 26 行条件为 false 好让指标正常跑。 但回放服务自己往图表挂控制指标时,指标代码会侦测到“已有另一个实例在跑”。这时第 24 行逻辑触发、改写第 25 行结果,_LastError 不再是第 26 行预期的 ERR_SUCCESS。于是 OnInit 返回 INIT_FAILED,MT5 抛 Deinit 事件,OnDeinit 收到 REASON_INITFAILED,直接关图表并停掉回放服务。外汇与贵金属回放环境本就高波动高风险,这种静默崩图会让你丢掉正在复盘的会话。 鼠标指标那边反而宽松些:它不独占流程,只抢先启动。所以模块内要做些增删,具体改动留到下一段讲。

MQL5 / C++
<span class="number">class="num">01</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">02</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Daniel Jose"</span>
<span class="number">class="num">03</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">icon</span> <span class="class="type">class="kw">string">"/Images/Market Replay/Icons/Replay - Device.ico"</span>
<span class="number">class="num">04</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">description</span> <span class="class="type">class="kw">string">"Control indicator for the Replay-Simulator service."</span>
<span class="number">class="num">05</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">description</span> <span class="class="type">class="kw">string">"This one doesn&class="macro">#x27;t work without the service loaded."</span>
<span class="number">class="num">06</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">version</span>&nbsp;&nbsp; <span class="class="type">class="kw">string">"class="num">1.59"</span>
<span class="number">class="num">07</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span> <span class="class="type">class="kw">string">"[MQL5官方文档]
<span class="number">class="num">08</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">indicator_chart_window</span>
<span class="number">class="num">09</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">indicator_plots</span> <span class="number">class="num">0</span>
<span class="number">class="num">10</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">indicator_buffers</span> <span class="number">class="num">1</span>
<span class="number">class="num">11</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">12</span>. <span class="preprocessor">class="macro">#include </span>&lt;Market Replay\Service Graphics\C_Controls.mqh&gt;
<span class="number">class="num">13</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">14</span>. C_Controls *control = <span class="macro">NULL</span>;
<span class="number">class="num">15</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">16</span>. <span class="keyword">input</span> <span class="keyword">class="type">long</span> user00 = <span class="number">class="num">0</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//ID</span>
<span class="number">class="num">17</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">18</span>. <span class="keyword">class="type">class="kw">double</span> m_Buff[];
<span class="number">class="num">19</span>. <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;m_RatesTotal = <span class="number">class="num">0</span>;
<span class="number">class="num">20</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">21</span>. <span class="keyword">class="type">int</span> <span class="functions">OnInit</span>()
<span class="number">class="num">22</span>. {
<span class="number">class="num">23</span>.&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ResetLastError</span>();&nbsp;&nbsp;
<span class="number">class="num">24</span>.&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (<span class="functions">CheckPointer</span>(control = <span class="keyword">new</span> C_Controls(user00, <span class="class="type">class="kw">string">"Market Replay Control"</span>, <span class="keyword">new</span> C_Mouse(user00, <span class="class="type">class="kw">string">"Indicator Mouse Study"</span>))) == <span class="macro">POINTER_INVALID</span>)
<span class="number">class="num">25</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">SetUserError</span>(C_Terminal::ERR_PointerInvalid);
<span class="number">class="num">26</span>.&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> ((<span class="predefines">_LastError</span> != <span class="macro">ERR_SUCCESS</span>) || (user00 == <span class="number">class="num">0</span>))
<span class="number">class="num">27</span>.&nbsp;&nbsp;&nbsp;&nbsp;{
<span class="number">class="num">28</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">Print</span>(<span class="class="type">class="kw">string">"Control indicator failed on initialization."</span>);
<span class="number">class="num">29</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">return</span> <span class="macro">INIT_FAILED</span>;
<span class="number">class="num">30</span>.&nbsp;&nbsp;&nbsp;&nbsp;}
<span class="number">class="num">31</span>.&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">SetIndexBuffer</span>(<span class="number">class="num">0</span>, m_Buff, <span class="macro">INDICATOR_DATA</span>);
<span class="number">class="num">32</span>.&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayInitialize</span>(m_Buff, <span class="macro">EMPTY_VALUE</span>);
<span class="number">class="num">33</span>.&nbsp;&nbsp;&nbsp;&nbsp;
<span class="number">class="num">34</span>.&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> <span class="macro">INIT_SUCCEEDED</span>;
<span class="number">class="num">35</span>. }

「回放指标的回调与卸载钩子」

把回放系统挂到图表上之后,真正决定它能不能安静跑起来的,是那几个事件函数怎么写。OnCalculate 只做一件事:把当前 K 线总数存进 m_RatesTotal 并返回,第 39 行直接赋值返回,意味着指标每次刷新都同步最新数据量。 OnChartEvent 负责把图表交互事件丢给 control 对象处理,第 44 行 DispatchMessage 之后立刻用 _LastError 判断内部消息系统是否崩了。错误码一旦大于等于 ERR_USER_ERROR_FIRST + C_Terminal::ERR_Unknown,就打印一句内部失败并强制关掉 user00 对应的图表(第 48 行)。随后第 50 行把缓冲区和 K 线数喂回 control,保证绘制不脱节。 OnDeinit 是清理现场的关键。reason 命中 REASON_TEMPLATE 会打印模板修改提示,但注意第 57–58 行后面没写 break,它会顺着落到 REASON_INITFAILED、REASON_PARAMETERS、REASON_REMOVE、REASON_CHARTCLOSE 这一组,统一执行 ChartClose(user00) 再 break。最后第 66 行 delete control,避免指针悬空。 那段指标配置里,inputs 下的 user00=130652731570824061 是一个 18 位整数句柄,用来绑定回放服务创建的隐藏图表。expertmode=1610613824 则说明该指标走的是服务化加载路径,普通拖拽指标不会带这个标记。开 MT5 把这段挂上,改一下 user00 的值就能验证不同回放实例的隔离效果,外汇与贵金属回放涉及高杠杆高风险,参数跑偏可能让你误判真实波动。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[])
{
  class="kw">return m_RatesTotal = rates_total;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam)
{
  (*control).DispatchMessage(id, lparam, dparam, sparam);
  if (_LastError >= ERR_USER_ERROR_FIRST + C_Terminal::ERR_Unknown)
  {
     Print("Internal failure in the messaging system...");
     ChartClose(user00);
  }
  (*control).SetBuffer(m_RatesTotal, m_Buff);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
{
  class="kw">switch (reason)
  {
     case REASON_TEMPLATE:
        Print("Modified class="kw">template. Replay class=class="str">"cmt">// simulation system shutting down.");
     case REASON_INITFAILED:
     case REASON_PARAMETERS:
     case REASON_REMOVE:
     case REASON_CHARTCLOSE:
        ChartClose(user00);
        class="kw">break;
  }
  class="kw">delete control;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

<indicator>
name=Custom Indicator
path=Services\Market Replay.ex5::Indicators\Market Replay.ex5
apply=class="num">0
show_data=class="num">1
scale_inherit=class="num">0
scale_line=class="num">0
scale_line_percent=class="num">50
scale_line_value=class="num">0.000000
scale_fix_min=class="num">0
scale_fix_min_val=class="num">0.000000
scale_fix_max=class="num">0
scale_fix_max_val=class="num">0.000000
expertmode=class="num">1610613824
fixed_height=-class="num">1

<graph>
name=
draw=class="num">0
style=class="num">0
width=class="num">1
class="type">color=
</graph>
<inputs>
user00=class="num">130652731570824061
</inputs>
</indicator>

◍ 让鼠标模块钻进任意子窗口

MetaTrader 5 的鼠标指标模块原本只打算蹲在主窗口(索引 0),但图表子窗口一多,就得改类结构把它指到指定窗口。C_Terminal 类第 22 行新增了一个子窗口变量,类外可访问,所以第 61 行构造函数多收一个 0~255 的参数——实际图表很少超过两三个子窗口,255 的上限绰绰有余。 第 65 行做了显式类型转换而不是把变量直接声明成 uchar,原因是 MQL5 需要有符号整数保向后兼容,用 uchar 当参数只是顺手把子窗口数卡在 255 以内。第 149 行把这个值交给 MetaTrader 5 决定对象画在哪个窗口;第 164~166 行的 IndicadorCheckPass 里先用临时指标名配合 ChartWindowFind 反查窗口索引,查不到就掉回主窗口,同时保证指标单实例,避免图形对象画错层。 C_Mouse 头文件第 6~7 行换了对象命名约定,名字绑定窗口和对象数防冲突;第 118 行起的 DecodeAlls 函数在非主窗口就露怯。关键在第 128 行:子窗口的 Y 坐标相对主窗口有偏移,MetaTrader 5 会把鼠标位置修正进工作区(标题栏以上 Y 变负),所以要用 ChartGetInteger(CHART_WINDOW_YDISTANCE, 子窗口号) 取偏移并从转换值里减掉,否则水平线会指错位置。 第 145 行存了指标短名,到第 164 行用来判断能不能关鼠标事件——MT5 每次鼠标移动都触发事件,没人用还开着只会增加事件队列垃圾,所以停用时得靠名字确认没有别的指标还在依赖它。 鼠标指标源码第 36 行现在会自动适配回放/模拟器服务的资产图表,老版本要手填图表 ID 的输入参数全删了。下面贴出 C_Terminal 类片段,注意第 22、61、65、149 行就是上面说的改动落点。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "Macros.mqh"
class="macro">#include "..\Defines.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Terminal
{
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">protected:
    enum eErrUser {ERR_Unknown, ERR_FileAcess, ERR_PointerInvalid, ERR_NoMoreInstance};
class=class="str">"cmt">//+------------------------------------------------------------------+
    class="kw">struct st_Terminal
    {
        ENUM_SYMBOL_CHART_MODE    ChartMode;
        ENUM_ACCOUNT_MARGIN_MODE  TypeAccount;
        class="type">long                      ID;
        class="type">class="kw">string                    szSymbol;

常见问题

多为后台线程未正确释放或主循环超时触发保护机制,需在关闭前显式终止数据拉取任务并清空缓冲。
大概率是模板加载走了异步路径,图表对象未就绪就触发了卸载钩子,应在回调里加就绪判断再执行关闭逻辑。
可以,小布能扫描你的回放配置并标记未挂载卸载钩子的指标,直接在品种页提示你补上清理代码。
需按子窗口索引遍历窗体句柄并绑定事件,不能只认主图,否则副图上的拖拽和点击会全部失效。
模板只管样式不管生命周期,异步加载竞态下仍会自关,必须自己写好回调与资源回收才稳。