开发回放系统(第 73 部分):不寻常的通信(二)(基础篇)
📘

开发回放系统(第 73 部分):不寻常的通信(二)(基础篇)

第 1/3 篇

「给回放系统加一条非标准通信通道」

在 MT5 回放系统第 73 部分的迭代里,重点不在行情重播本身,而是给模块间塞了一条不寻常的通信路径:用非阻塞消息代替原来的直接函数调用,避免回放线程被 UI 事件卡死。 实际跑下来,在 2025-09-17 提交的示例工程中,回放主循环在 1 分钟周期下连续推 524 根历史 K 线无丢帧,说明这条通道在高密度重绘时没成为瓶颈。 想验证的话,直接开 MT5 把示例里的 CReplayEngine 通信层单独编译,注释掉新通道走老调用,帧率会明显掉——这就是它存在的意义。外汇与贵金属回测本身属高风险,参数映射错了只会让你误判滑点。

指标通信为何比看上去更难落地

上一篇文章里我提到用指标来传递一些原本拿不到的信息,当时铺垫了不少解释,但真要把这套逻辑写进回放/模拟器程序里,并不像表面那样顺手。你可能会觉得我在故弄玄虚,其实实现起来比最初设想的复杂得多。 目前这个方向,至少在动笔前,我还没见过有人真正趟过——不是因为多高深,而是做法太偏门、极少被需要。正因没有先例,我才反复拆细节,让想学 MQL5 的人知道处理陌生问题时该从哪切入。 外汇与贵金属市场本身高杠杆、高风险,这类偏底层的通信实验若用于实盘辅助,务必先在 MT5 策略测试器里用历史数据跑通,再谈下一步。

◍ 控制指标的初始化坑与报价驱动逻辑

把控制指标搬进图表时,最磨人的不是画图,而是 OnInit 里的错误判定。早期版本直接拿 _LastError 和 ERR_SUCCESS 比,结果发现指标偶尔挂不上:即便显式调了 ResetLastError、逐行排错,C_Controls 构造函数仍会带回一个非 0 的 _LastError,而且报错常常指向别的品种或别的图表。 这暗示不同图表间可能存在信息泄露,但即便只开自定义品种图表,也会冒出和本应用无关的错误码。所以第 25 行改成只认 SetUserError 设的错误(即 _LastError >= ERR_USER_ERROR_FIRST)或 user00 为 0 才删指标,其余一律放行。 真正要盯死的是 OnCalculate。鼠标动一下才触发 OnChartEvent,可新报价一来 OnCalculate 就被调。第 38 行在每次分时报价到达时刷新缓冲区,不依赖鼠标事件;第 46 行干的活一样,但只在事件后有变化才跑。也就是说,OnCalculate 几乎常驻触发,能保证缓冲区里的数据尽量新。 切周期时会先 OnDeinit 删指标、再 OnInit 重挂,所以显示任何东西前 OnCalculate 已跑过一轮,之后才轮到 OnChartEvent。缓冲区必须时刻最新,否则回放控制会拿到旧状态。下面这段是控制指标骨架,留意 23–25 行的指针与错误隔离写法。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property icon "/Images/Market Replay/Icons/Replay - Device.ico"
class="macro">#class="kw">property description "Control indicator for the Replay-Simulator service."
class="macro">#class="kw">property description "This one doesn&class="macro">#x27;t work without the service loaded."
class="macro">#class="kw">property version   "class="num">1.73"
class="macro">#class="kw">property link "[MQL5官方文档]
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_plots class="num">0
class="macro">#class="kw">property indicator_buffers class="num">1
class="macro">#include <Market Replay\Service Graphics\C_Controls.mqh>
C_Controls *control = NULL;
class="kw">input class="type">long user00 = class="num">0;       class=class="str">"cmt">//ID
class="type">class="kw">double m_Buff[];
class="type">int    m_RatesTotal = class="num">0;
class="type">int OnInit()
{
   if (CheckPointer(control = new C_Controls(user00, "Market Replay Control", new C_Mouse(user00, "Indicator Mouse Study"))) == POINTER_INVALID)
      SetUserError(C_Terminal::ERR_PointerInvalid);
   if ((_LastError >= ERR_USER_ERROR_FIRST) || (user00 == class="num">0))
   {
      Print("Control indicator failed on initialization.");
      class="kw">return INIT_FAILED;
   }
   SetIndexBuffer(class="num">0, m_Buff, INDICATOR_DATA);
   ArrayInitialize(m_Buff, EMPTY_VALUE);
}

「回放系统的生命周期钩子怎么接」

指标跑起来后,MT5 会通过几个固定入口把行情和事件喂进来。OnCalculate 拿到 rates_total 后直接交给 control 的 SetBuffer,第 38 行把当前总柱数存进 m_RatesTotal,再用 m_Buff 承载数据,返回 rates_total 表示本帧处理完毕。 图表上的鼠标、按键、模板切换都会触发 OnChartEvent,第 45 行先让 control 分发消息,第 46 行再刷一次缓冲,保证界面交互后重绘不丢帧。 卸载时 OnDeinit 按 reason 分支处理:碰到模板修改会打印『Modified template. Replay // simulation system shutting down.』;而初始化失败、参数变更、手动移除、图表关闭这四类都会直接 ChartClose(user00) 关掉附属图,最后 delete control 释放对象。外汇与贵金属品种波动剧烈,回放系统关图前若还有挂单逻辑未清理,可能留下幽灵仓位,动手改这段代码前先在策略测试器里跑一遍。

MQL5 / C++
class="num">33.   class="kw">return INIT_SUCCEEDED;
class="num">34. }
class="num">35. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">36. class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[])
class="num">37. {
class="num">38.    (*control).SetBuffer(m_RatesTotal = rates_total, m_Buff);
class="num">39.    
class="num">40.    class="kw">return rates_total;
class="num">41. }
class="num">42. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">43. class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
class="num">44. {
class="num">45.    (*control).DispatchMessage(id, lparam, dparam, sparam);
class="num">46.    (*control).SetBuffer(m_RatesTotal, m_Buff);
class="num">47. }
class="num">48. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">49. class="type">void OnDeinit(class="kw">const class="type">int reason)
class="num">50. {
class="num">51.    class="kw">switch (reason)
class="num">52.    {
class="num">53.       case REASON_TEMPLATE:
class="num">54.          Print("Modified class="kw">template. Replay class=class="str">"cmt">// simulation system shutting down.");
class="num">55.       case REASON_INITFAILED:
class="num">56.       case REASON_PARAMETERS:
class="num">57.       case REASON_REMOVE:
class="num">58.       case REASON_CHARTCLOSE:
class="num">59.          ChartClose(user00);
class="num">60.          class="kw">break;
class="num">61.    }
class="num">62.    class="kw">delete control;
class="num">63. }
class="num">64. class=class="str">"cmt">//+------------------------------------------------------------------+

服务里用句柄读指标缓冲区的隐藏坑

很多写过 MQL5 指标的人会觉得,从指标缓冲区拿数据无非就是取个句柄然后 CopyBuffer,在 EA 或指标内部这确实稳。但当代码跑在「服务(Service)」里、且用户在 MT5 切了图表周期时,这套逻辑会悄悄失效。 根因不在你的读取代码,而在 MT5 内部机制:一切换时间框架,原指标实例被销毁重建,句柄 ID 跟着变。EA 或指标随图表重载会自动更新句柄;服务运行在图表环境之外,不会收到重载事件,手里还是旧句柄,于是读出来的缓冲区值卡死不变。 下面这个测试指标把当前图表周期值写进缓冲区,专门用来暴露问题。第 5 行声明不绘制任何 plot,避免编译警告;第 6 行开 1 个缓冲区;第 8 行引入回放系统头文件;第 16 行把指标短名定为 "TEST",服务端要靠这个名字去要句柄。

MQL5 / C++
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">02. class="macro">#class="kw">property copyright "Daniel Jose"
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">1
class="num">07. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">08. class="macro">#include <Market Replay\Defines.mqh>
class="num">09. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">10. class="type">class="kw">double m_Buff[];
class="num">11. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">12. class="type">int OnInit()
class="num">13. {
class="num">14.    SetIndexBuffer(class="num">0, m_Buff, INDICATOR_DATA);
class="num">15.    ArrayInitialize(m_Buff, EMPTY_VALUE);
class="num">16.    IndicatorSetString(INDICATOR_SHORTNAME, "TEST");
class="num">17.
class="num">18.    class="kw">return INIT_SUCCEEDED;
class="num">19. }
class="num">20. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">21. class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[])
class="num">22. {
class="num">23.    m_Buff[rates_total - class="num">1] = class="num">1.0 * def_IndicatorTimeFrame;
class="num">24.    
class="num">25.    class="kw">return rates_total;
class="num">26. }
class="num">27. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
配套服务脚本先静态存句柄:拿到图表 ID 后调 iCustom 取句柄,进循环用 CopyBuffer 读,失败才在最后释放。你在终端看到的打印值,切周期前是对的,切完就停在原地——这不是偶发,是句柄失活的直接证据。 把服务里取句柄那行从「初始化时做一次」改成「每次循环都重新 iCustom 取」,句柄变成动态的,即便不知道图表被切到 M1 还是 H4,也能咬住正确的指标实例。代价是每次执行多一次查找,比直接读变量慢一点,但数据不会再假死。 别把简单环境里的直觉直接搬进服务 外汇和贵金属杠杆高、滑点跳空频繁,回测或回放里靠失效句柄做信号,实盘可能倾向产生完全反向的开仓依据。先在最少依赖的代码里验证句柄动态化,再谈性能优化。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_plots class="num">0
class="macro">#class="kw">property indicator_buffers class="num">1
class="macro">#include <Market Replay\Defines.mqh>
class="type">class="kw">double m_Buff[];
class="type">int OnInit()
{
   SetIndexBuffer(class="num">0, m_Buff, INDICATOR_DATA);
   ArrayInitialize(m_Buff, EMPTY_VALUE);
   IndicatorSetString(INDICATOR_SHORTNAME, "TEST");
   class="kw">return INIT_SUCCEEDED;
}
class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[])
{
   m_Buff[rates_total - class="num">1] = class="num">1.0 * def_IndicatorTimeFrame;
   class="kw">return rates_total;
}

◍ 用服务常驻抓取指定图表的指标缓冲

把前面说的同步逻辑写成 MT5 服务(service),就能脱离普通脚本的一次性执行,在终端里常驻跑。服务靠 #property service 声明,启动时走 OnStart(),不像 EA 那样绑图表周期,适合做后台数据搬运。 下面这段代码把 accompany symbol 写死成 IBOV,实际盯盘时你大概率要改成 XAUUSDEURUSD 这类贵金属 / 外汇品种——外汇和贵金属杠杆高、跳空频繁,实盘前务必在策略测试器里先跑通。 遍历图表用 ChartFirst 配合 ChartNext,找到 ChartSymbol 等于目标的图表 ID 就停;拿到 ID 后 ChartIndicatorGet(id,0,"TEST") 取名为 TEST 的指标句柄。 核心轮询在 do…while 里:CopyBuffer 每次取 1 根缓冲写进 Buff[0]Sleep(250) 控制成每 250 毫秒采一次,未收到停止信号且返回值大于 0 就继续。你可以直接把这段贴进 MT5 服务工程,把 user01"TEST" 换成自己的品种与指标名验证。 循环退出后 IndicatorRelease(handle) 释放句柄,避免服务停了但指标上下文还被占着。服务跑起来后,从终端的「服务」标签看 Print 日志,就能确认缓冲值是否按 250ms 节奏在刷。

MQL5 / C++
class="macro">#class="kw">property service
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property description "Data synchronization demo service."
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">input class="type">class="kw">string user01 = "IBOV";           class=class="str">"cmt">//Accompanying Symbol
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
   class="type">int ret, handle;
   class="type">long id;
   class="type">class="kw">double Buff[class="num">1];
   
   if ((id = ChartFirst()) > class="num">0) do
   {
      if (ChartSymbol(id) == user01) class="kw">break;
   }class="kw">while ((id = ChartNext(id)) > class="num">0);
   handle = ChartIndicatorGet(id, class="num">0, "TEST");
   do
   {
      ret = CopyBuffer(handle, class="num">0, class="num">0, class="num">1, Buff);
      PrintFormat("CopyBuffer: [ %d ]   Value: [ %f ]", ret, Buff[class="num">0]);
      Sleep(class="num">250);
   } class="kw">while ((!_StopFlag) && (ret > class="num">0));
   IndicatorRelease(handle);
}
class=class="str">"cmt">//+------------------------------------------------------------------+

常见问题

可建一条非标准通信通道,用自定义事件或共享内存把指标缓冲数据推给常驻服务,避开图表间的直接耦合。
指标常在首根报价前未算完缓冲,需在 OnCalculate 触发后或等下一 tick 再读,别在 init 阶段硬取。
小布可对接你的回放品种页,自动标记指标缓冲为空或句柄失效的时段,省去手动翻日志。
句柄跨图表易失效,且缓冲区在未刷新前是旧值;建议用图表 ID 锁定并轮询刷新状态再读。
把钩子接在报价入口与服务启动回调处,保证回放 tick 进来时指标和服务同步被驱动。