开发回放系统(第 72 部分):异常通信(一)·进阶篇
🔌

开发回放系统(第 72 部分):异常通信(一)·进阶篇

(2/3)· 切换周期后鼠标指标变『市场关闭』、控制指标消失,这套回放架构的通信断点到底卡在哪

偏理论进阶 第 2/3 篇

很多人以为回放系统里指标消失只是 MT5 正常重载,其实当你切到别的周期,鼠标指标从竞价活动变成市场关闭、控制指标直接蒸发,根本原因是 OnInit 被立即调用而服务端的自定义事件还没跟上。更麻烦的是服务不在播放模式时,这个事件永远不触发,你只能手动重启。

「用最简指标摸清周期变量的边界」

做新功能前先写个能跑的测试版,是 MT5 程序里省时间的习惯。不用一上来就动主程序,单独建个极简指标,把要验证的机制跑通,再决定怎么并进去。 第一个测试指标只干一件事:在 OnInit 里把内置变量 _Period 打印出来。切换图表周期时,终端消息栏会跟着刷出不同数字。MT5 支持到月线周期,实测 _Period 最大值到了 49153,也就是说要标记周期变化,粗算得用 16 位空间。 改一行就能压扁数值范围。把 _Period 按阈值映射后,最大值掉到 96,只需 7 位就能装下。底层其实是平台把周期枚举合并了,映射后的值对「检测周期是否切换」完全够用。我们盯的不是周期本身,而是它变没变。 下面两段是前后对照的 MQL5 源码,重点看第 9 行从直接打印到三元表达式映射的变化。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
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=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
   Print(_Period);
   class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
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="kw">return rates_total;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
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=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
   Print(_Period < class="num">60 ? _Period : (_Period < PERIOD_D1 ? _Period - class="num">16325 : (_Period == PERIOD_D1 ? class="num">84 : (_Period == PERIOD_W1 ? class="num">91 : class="num">96))));
   class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
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="kw">return rates_total;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

把时间框架塞进指标缓冲区的 8 字节里

要把时间框架变化通知给回放/模拟服务,最干净的办法是借用指标缓冲区——对用户完全不可见,还顺带做了信息封装。MT5 里指标数据缓冲区固定是 double 类型,占 8 字节(64 位 QWORD),我们和服务的全部交换内容必须压进这 8 字节,多一个字节都负担不起。 目前控制指标已经用掉了低 4 字节(索引 0~3),结构里通过 union 把 32 位拆成两个 16 位数组值来用。时间框架用单字节编码就够了,于是占用扩展到索引 4,也就是 8 字节里的第 5 字节。写错索引会直接覆盖已有有效数据,这是新手用 union 最常翻车的地方。 Defines.mqh 第 16 行那段宏把 _Period 映射成紧凑编码:低于 60 的周期原样走,小时级以上用偏移减法(如 D1 映射为 84、W1 为 91、MN1 为 96),保证一个字节装得下。第 17 行则补了 def_IndicatorTimeFrame 的引用定义,让后续写缓冲区时能用安全索引。 C_Controls.mqh 里第 175 行附近才真正决定往缓冲区哪写、写什么;构造函数(第 146 行)改成只在我们显式抛出的错误下提前退出,其余错误暂忽略。图片资源也改了名(Ctrl.bmp 即原 LEFT.BMP),附件里已带,直接拖进项目即可。外汇与贵金属回放测试属高风险环境,实盘前务必在模拟端跑通字节布局。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Defines.mqh 片段(含时间框架编码与索引定义)                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_IndicatorTimeFrame(_Period < class="num">60 ? _Period : (_Period < PERIOD_D1 ? _Period - class="num">16325 : (_Period == PERIOD_D1 ? class="num">84 : (_Period == PERIOD_W1 ? class="num">91 : class="num">96))))
class=class="str">"cmt">// 第 class="num">17 行新增安全索引(示意,基于原文描述)
class="macro">#define def_TimeFrameIndex class="num">4
union uCast_Double {
   class="type">class="kw">double   dValue;
   class="type">long     _long;        class=class="str">"cmt">// 已用低4字节信息
   class="type">class="kw">datetime _datetime;
};

◍ 用联合体把控件状态塞进一个 double

想在同一个指标缓冲里同时存滑块位置和播放状态,又不想开两个数组占内存,可以借一个 union 把 double 按位拆成不同宽度的整数看。double 在 MT5 里占 8 字节,按 uint(4字节)拆得到 2 段,按 ushort(2字节)拆得到 4 段,按 uchar(1字节)拆得到 8 段,这就是下面这段声明的由来。 [CODE] uint _32b[sizeof(double)/sizeof(uint)]; // 2 段信息 ushort _16b[sizeof(double)/sizeof(ushort)]; // 4 段信息 uchar _8b [sizeof(double)/sizeof(uchar)]; // 8 段信息 [/CODE] 实际写回缓冲时,只用了 _16b 里的两个下标:eCtrlPosition 放滑块最小值,eCtrlStatus 放播放或暂停标记。SetBuffer 里先构造 uCast_Double info,填好两个 ushort,再把 info.dValue 整体写进 Buff[rates_total-1],等于一次赋值就打包了两种控制信息。 [CODE] void SetBuffer(const int rates_total, double &Buff[]) { uCast_Double info; info._16b[eCtrlPosition] = m_Slider.Minimal; info._16b[eCtrlStatus] = (ushort)(m_Slider.Minimal > def_MaxPosSlider ? m_Slider.Minimal : (m_Section[ePlay].state ? ePlay : ePause)); if (rates_total > 0) Buff[rates_total - 1] = info.dValue; } [/CODE] EnumEvents 里那串 evHide/evShow 开头的枚举,是给回放面板用的显隐开关:鼠标价格线、K线时间、日内波幅、瞬时波幅各自一对,最后加一个 evCtrlReplayInit 做初始化。外汇和贵金属行情跳变快,这种把多状态压进单缓冲的做法能减少重绘开销,但联合体内存布局依赖平台字节序,换环境前最好在 MT5 里跑一段打印 _16b 各下标的脚本验证。

MQL5 / C++
class="type">uint   _32b[class="kw">sizeof(class="type">class="kw">double)/class="kw">sizeof(class="type">uint)];   class=class="str">"cmt">// class="num">2 Informations
class="type">class="kw">ushort  _16b[class="kw">sizeof(class="type">class="kw">double)/class="kw">sizeof(class="type">class="kw">ushort)]; class=class="str">"cmt">// class="num">4 Informations
class="type">uchar   _8b [class="kw">sizeof(class="type">class="kw">double)/class="kw">sizeof(class="type">uchar)];  class=class="str">"cmt">// class="num">8 Informations
};
class=class="str">"cmt">//+------------------------------------------------------------------+
enum EnumEvents      {
        evHideMouse,                class=class="str">"cmt">//Hide mouse price line
        evShowMouse,                class=class="str">"cmt">//Show mouse price line
        evHideBarTime,              class=class="str">"cmt">//Hide bar time
        evShowBarTime,              class=class="str">"cmt">//Show bar time
        evHideDailyVar,             class=class="str">"cmt">//Hide daily variation
        evShowDailyVar,             class=class="str">"cmt">//Show daily variation
        evHidePriceVar,             class=class="str">"cmt">//Hide instantaneous variation
        evShowPriceVar,             class=class="str">"cmt">//Show instantaneous variation
        evCtrlReplayInit            class=class="str">"cmt">//Initialize replay control
                 };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="type">void SetBuffer(class="kw">const class="type">int rates_total, class="type">class="kw">double &Buff[])
      {
       uCast_Double info;
       
       info._16b[eCtrlPosition] = m_Slider.Minimal;
       info._16b[eCtrlStatus] = (class="type">class="kw">ushort)(m_Slider.Minimal > def_MaxPosSlider ? m_Slider.Minimal : (m_Section[ePlay].state ? ePlay : ePause));
       if (rates_total > class="num">0)
        Buff[rates_total - class="num">1] = info.dValue;
      }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_VERSION_DEBUG
class=class="str">"cmt">//+------------------------------------------------------------------+

「用宏与联合体给回放系统打底」

做 MT5 回放(RePlay)类工具时,先靠预编译宏把调试开销切干净。def_VERSION_DEBUG 开启时,macro_DEBUG_MODE(A) 会把文件名、行号、函数名和变量值一起 Print 出来;关闭时宏直接展开为空,实盘加载不占一丝日志带宽。 几个全局常量值得直接抄:def_SymbolReplay 写死 "RePlay" 作虚拟品种名,def_MaxPosSlider 给 400 根 K 线滑块上限,def_MaskTimeService 用 0xFED00000 屏蔽时间服务高位。周期映射那段最绕——_Period 小于 60 用本身,小于日线减 16325,日线落 84,周线 91,月线 96,复制前先在脚本里 Print(_Period) 核对你的图表周期落点。 uCast_Double 联合体是字节级读数的关键:同一个 double 能按 long、datetime、uint[2]、ushort[4]、uchar[8] 五种视角拆开,做价格位掩码或时间戳注入时不用反复类型强转。 EnumEvents 把鼠标线、Bar 时间的显隐和日波幅开关列成事件枚举,后续 ChartEvent 分发直接 switch 即可,比满屏 bool 标志清爽。

MQL5 / C++
class="macro">#ifdef def_VERSION_DEBUG
class="macro">#define macro_DEBUG_MODE(A) \
      Print(__FILE__, " ", __LINE__, " ", __FUNCTION__ + " " + class="macro">#A + " = " + (class="type">class="kw">string)(A));
class="macro">#else
class="macro">#define macro_DEBUG_MODE(A)
class="macro">#endif
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_SymbolReplay        "RePlay"
class="macro">#define def_MaxPosSlider        class="num">400
class="macro">#define def_MaskTimeService     0xFED00000
class="macro">#define def_IndicatorTimeFrame(_Period < class="num">60 ? _Period : (_Period < PERIOD_D1 ? _Period - class="num">16325 : (_Period == PERIOD_D1 ? class="num">84 : (_Period == PERIOD_W1 ? class="num">91 : class="num">96))))
class="macro">#define def_IndexTimeFrame      class="num">4
class=class="str">"cmt">//+------------------------------------------------------------------+
union uCast_Double
{
   class="type">class="kw">double   dValue;
   class="type">long     _long;                                          class=class="str">"cmt">// class="num">1 Information
   class="type">class="kw">datetime _datetime;                                     class=class="str">"cmt">// class="num">1 Information
   class="type">uint     _32b[class="kw">sizeof(class="type">class="kw">double) / class="kw">sizeof(class="type">uint)];           class=class="str">"cmt">// class="num">2 Informations
   class="type">class="kw">ushort   _16b[class="kw">sizeof(class="type">class="kw">double) / class="kw">sizeof(class="type">class="kw">ushort)];         class=class="str">"cmt">// class="num">4 Informations
   class="type">uchar    _8b [class="kw">sizeof(class="type">class="kw">double) / class="kw">sizeof(class="type">uchar)];          class=class="str">"cmt">// class="num">8 Informations
};
class=class="str">"cmt">//+------------------------------------------------------------------+
enum EnumEvents   {
         evHideMouse,         class=class="str">"cmt">//Hide mouse price line
         evShowMouse,         class=class="str">"cmt">//Show mouse price line
         evHideBarTime,       class=class="str">"cmt">//Hide bar time
         evShowBarTime,       class=class="str">"cmt">//Show bar time
         evHideDailyVar,      class=class="str">"cmt">//Hide daily variation
};

回放控件枚举与位图资源定义

在 Market Replay 控件的实现里,先通过枚举把交互事件收敛成几个明确状态:显示日波动、隐藏瞬时波动、显示瞬时波动、初始化回放控制。这样后续事件分发不用散落一堆布尔开关,直接比对枚举值即可。 代码前半段用 #define 把按钮位图路径拼出来,Play.bmp / Pause.bmp / Ctrl.bmp 等五张图统一放在 Images\Market Replay\Control\ 下,并用 #resource 预编译指令打进 ex5,运行时零磁盘依赖。 控件类 C_Controls 私有继承 C_Terminal,内部又定义了 eMatrixControl 二级枚举(eCtrlPosition、eCtrlStatus),把「位置矩阵」和「状态矩阵」分开索引。def_SizeButtons 写死 32 像素、def_ColorFilter 用 0xFF00FF 品红做透明滤色——开 MT5 把这两值改掉,按钮大小和抠图边缘会立刻变样。 def_PosXObjects 设为 120,意味着所有控制对象默认锚在图表左侧 120 像素处;若你的图表左边要给均线或成交量留空间,把这个宏调大即可。外汇与贵金属行情受杠杆影响波动剧烈,回测与可视化仅辅助判断,实盘仍属高风险。

MQL5 / C++
class="num">35.    evShowDailyVar,          class=class="str">"cmt">//Show daily variation
class="num">36.    evHidePriceVar,          class=class="str">"cmt">//Hide instantaneous variation
class="num">37.    evShowPriceVar,          class=class="str">"cmt">//Show instantaneous variation
class="num">38.    evCtrlReplayInit        class=class="str">"cmt">//Initialize replay control
class="num">39.           };
class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">001. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">002. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">003. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">004. class="macro">#include "..\Auxiliar\C_DrawImage.mqh"
class="num">005. class="macro">#include "..\Defines.mqh"
class="num">006. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">007. class="macro">#define def_PathBMP          "Images\\Market Replay\\Control\\"
class="num">008. class="macro">#define def_ButtonPlay       def_PathBMP + "Play.bmp"
class="num">009. class="macro">#define def_ButtonPause      def_PathBMP + "Pause.bmp"
class="num">010. class="macro">#define def_ButtonCtrl       def_PathBMP + "Ctrl.bmp"
class="num">011. class="macro">#define def_ButtonCtrlBlock  def_PathBMP + "Ctrl_Block.bmp"
class="num">012. class="macro">#define def_ButtonPin        def_PathBMP + "Pin.bmp"
class="num">013. class="macro">#resource "\\" + def_ButtonPlay
class="num">014. class="macro">#resource "\\" + def_ButtonPause
class="num">015. class="macro">#resource "\\" + def_ButtonCtrl
class="num">016. class="macro">#resource "\\" + def_ButtonCtrlBlock
class="num">017. class="macro">#resource "\\" + def_ButtonPin
class="num">018. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">019. class="macro">#define def_ObjectCtrlName(A)  "MarketReplayCTRL_" + (class="kw">typename(A) == "enum eObjectControl" ? EnumToString((C_Controls::eObjectControl)(A)) : (class="type">class="kw">string)(A))
class="num">020. class="macro">#define def_PosXObjects        class="num">120
class="num">021. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">022. class="macro">#define def_SizeButtons        class="num">32
class="num">023. class="macro">#define def_ColorFilter        0xFF00FF
class="num">024. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">025. class="macro">#include "..\Auxiliar\C_Mouse.mqh"
class="num">026. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">027. class C_Controls : class="kw">private C_Terminal
class="num">028. {
class="num">029.   class="kw">protected:
class="num">030.   class="kw">private   :
class="num">031. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">032.      enum eMatrixControl {eCtrlPosition, eCtrlStatus};

◍ 滑条控件的结构与绘制落点

自定义 GUI 里做时间轴滑条,先把控件状态收进枚举和结构体最省事。eObjectControl 把暂停、播放、左右移、固定针都列成枚举值,eTriState 故意取 def_MaxPosSlider + 1,说明三态切换的边界是跟着滑条最大位移走的,改最大格数时这里也得同步。 st_00 只装滑条本体名和滑块名两个字符串加一个 ushort 下限,st_01 则把每个按钮的绘图指针、状态、坐标宽高打包成数组,长度按 eNull 截断,避免越界写对象。实际挂 MT5 看,m_Section 数组下标直接拿枚举当索引,比用魔法数字可读性强太多。 CreteBarSlider 里两个 OBJ_RECTANGLE_LABEL 才是肉眼可见的部分:底层条 Y 偏移取 m_Section[ePin].y + 11,高 9 像素、宽由入参 size 定;上层滑块块 Y 偏移 +6,比条高重心往上提 5 像素,视觉上卡在条中间。把 x 和 size 随便传个不同值,能在图表左上角立即看出滑条长度和针位参照关系。 外汇和贵金属图表加载这类自绘控件时波动刷新频繁,对象创建失败可能静默丢图,建议先在回测器外的实盘样本符号试一遍坐标。

MQL5 / C++
enum eObjectControl {ePause, ePlay, eLeft, eRight, ePin, eNull, eTriState = (def_MaxPosSlider + class="num">1)};
class=class="str">"cmt">//+------------------------------------------------------------------+
  class="kw">struct st_00
  {
    class="type">class="kw">string  szBarSlider,
            szBarSliderBlock;
    class="type">class="kw">ushort  Minimal;
  }m_Slider;
  class="kw">struct st_01
  {
    C_DrawImage *Btn;
    class="type">bool        state;
    class="type">class="kw">short       x, y, w, h;
  }m_Section[eObjectControl::eNull];
  C_Mouse  *m_MousePtr;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void CreteBarSlider(class="type">class="kw">short x, class="type">class="kw">short size)
        {
         ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSlider = def_ObjectCtrlName("B1"), OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0);
         ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_XDISTANCE, def_PosXObjects + x);
         ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Section[ePin].y + class="num">11);
         ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_XSIZE, size);
         ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_YSIZE, class="num">9);
         ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue);
         ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack);
         ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_WIDTH, class="num">3);
         ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT);
         ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSliderBlock = def_ObjectCtrlName("B2"), OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0);
         ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XDISTANCE, def_PosXObjects + x);
         ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YDISTANCE, m_Section[ePin].y + class="num">6);
让小布替你跑这套
这类周期切换后的通信异常诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到事件链路提示,把重复劳动交给小布,你专注决策。

常见问题

MT5 在周期切换时会重载图表指标并立即调用 OnInit,而我们的回放服务依赖自定义事件重新初始化,事件未到达前控制指标无法重建,因此从图表移除。
概率上很难自行恢复,因为触发控制指标重初始化的自定义事件只在播放模式下由服务发出,此时只能停止并重启服务。
引入自定义订单簿事件后,大于一分钟周期也能直接从 OnCalculate 参数读价差,不必走 iSpread,流程更简化但依赖前述订单簿机制。
可以,小布盯盘对应品种页会展示事件链路状态,当检测不到服务自定义事件时会提示通信断点,便于快速定位是否卡在 OnInit 之后。
时间处理决定了订单簿事件与柱体对齐方式,若时间轴错乱,周期切换后服务更难发出正确自定义事件,异常通信会被放大。