开发回放系统(第 61 部分):玩转服务(二)·综合运用
📘

开发回放系统(第 61 部分):玩转服务(二)·综合运用

第 3/3 篇

「直接读按钮对象状态替代缓冲区轮询」

回放/模拟服务里最吃 CPU 的是 LoopEventOnTime 定期调用检查控制按钮状态的函数。原方案每次都从控制指标缓冲区读数据判断播放/暂停,其实 OBJ_BITMAP_LABEL 对象自身就带状态变量,直接读对象属性更轻。 在 C_DrawImage.mqh 第 184–186 行做微小改动后,对象的 OBJPROP_STATE 会随播放(1)/暂停(0)实时更新。这样服务层可以绕过缓冲区,用 ObjectGetInteger 直接取状态。 修改点落在 C_Replay.mqh:构造函数里初始化 m_IndControl 指向控制指标创建的对象名;UpdateIndicatorControl 中先验证 handler 有效(第40行)、内存位置匹配(第41行),匹配后若静态变量为真则从图表对象读 OBJ_BITMAP_LABEL 当前值,否则退回缓冲区读。 实测这不一定让单次调用快很多,但每次调用省下的操作数在高频定时触发下会累积。外汇/贵金属回测环境开 MT5 按上面改完,用 Profiler 看 LoopEventOnTime 占用,可能能看到轻微下降。 别把缓冲读取妖魔化 缓冲区读取并非天生慢,只是比读对象直接属性多几步间接操作。只在暂停切播放的瞬间才需要回退缓冲区同步位置,日常轮询走对象属性就够了。

MQL5 / C++
class="num">174. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">175.    class="type">void Paint(class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">uchar cView, class="kw">const class="type">int what)
class="num">176.      {
class="num">177.       
class="num">178.       if ((m_szRecName == NULL) || (what < class="num">0) || (what >= def_MaxImages)) class="kw">return;
class="num">179.       ReSizeImage(w, h, cView, what);
class="num">180.       ObjectSetInteger(GetInfoTerminal().ID, m_szObjName, OBJPROP_XDISTANCE, x);
class="num">181.       ObjectSetInteger(GetInfoTerminal().ID, m_szObjName, OBJPROP_YDISTANCE, y);
class="num">182.       if (ResourceCreate(m_szRecName, m_Pixels, w, h, class="num">0, class="num">0, class="num">0, COLOR_FORMAT_ARGB_NORMALIZE))
class="num">183.        {
class="num">184.    ObjectSetString(GetInfoTerminal().ID, m_szObjName, OBJPROP_BMPFILE, m_szRecName);
class="num">185.         ObjectSetString(GetInfoTerminal().ID, m_szObjName, OBJPROP_BMPFILE, what, m_szRecName);
class="num">186.         ObjectSetInteger(GetInfoTerminal().ID, m_szObjName, OBJPROP_STATE, what == class="num">1);
class="num">187.         ChartRedraw(GetInfoTerminal().ID);
class="num">188.        }

◍ 用自定义品种搭回放沙盘

做历史行情回放,第一步不是读文件,而是在 MT5 里造一个干净的交易品种。上面这段类构造里,先删掉同名自定义品种再重建,路径写成 Custom\%s,等于在自定义分组下开了一块独立沙盘,和真实账户品种彻底隔离。 重建后立刻把 SYMBOL_TRADE_TICK_SIZESYMBOL_TRADE_TICK_VALUESYMBOL_VOLUME_STEP 三个字段全置 0(见 140–142 行)。这一步很关键:回放引擎不靠真实点值算盈亏,置 0 能避免 MT5 在绘图或测试时因点值异常报错。 srand(GetTickCount()) 放在 136 行,用系统节拍数播种子。回放若涉及随机偏移或噪声模拟,每次启动序列不同,可防止多实例回放撞出完全一样的伪随机轨迹。外汇与贵金属回放仅用于策略验证,实盘仍属高风险,置 0 参数不代表任何收益暗示。

MQL5 / C++
class="macro">#define def_IndicatorControl   "Indicators\\Market Replay.ex5"
class="macro">#resource "\\" + def_IndicatorControl
class="macro">#define def_CheckLoopService((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != ""))
class="macro">#define def_ShortNameIndControl "Market Replay Control"
class C_Replay : class="kw">public C_ConfigService
{
   class="kw">private  :
      class="kw">struct st00
      {
         C_Controls::eObjectControl Mode;
         uCast_Double                Memory;
         class="type">class="kw">ushort                     Position;
         class="type">int                        Handle;
      }m_IndControl;
   C_Replay()
      :C_ConfigService()
   {
       Print("************** Market Replay Service **************");
       srand(GetTickCount());
       SymbolSelect(def_SymbolReplay, class="kw">false);
       CustomSymbolDelete(def_SymbolReplay);
       CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay));
       CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, class="num">0);
       CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, class="num">0);
       CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, class="num">0);

回放符号初始化与控件状态同步

在 MT5 自定义回放环境里,先得把仿真品种的属性钉死。代码 143–144 行给重放符号写描述、设精度为 8 位小数,这种细粒度通常是为了让 tick 级回放不被报价舍入吃掉细节;145 行 SymbolSelect 置 true,否则图表里根本选不到这个自定义品种。 紧接着 146–150 行把回放计数、指标句柄、播放模式全部归零或置为暂停态,控件内存里的位置标记用 eTriState 占位,等于告诉主循环「还没收到有效定位」。外汇与贵金属回放涉及高杠杆与滑点模拟,实盘映射结果偏差可能很大,仅作策略逻辑验证用。 下面这段 UpdateIndicatorControl 是控件轮询核心。40 行先拦掉无效句柄;41 行比对内存中的位置标记和当前 Position,不一致才进分支。47–48 行用 CopyBuffer 取 1 根缓冲值写进 Memory.dValue,这是把指标面板上的按钮状态读回 C++ 结构体的关键一步。 49–51 行根据 CtrlStatus 决定播放还是暂停:若状态不是三态,就把 Mode 切过去,并在确为 ePlay 时把 Position 同步为内存位置。这样图表上的「播放/暂停」点击才能驱动回放指针往前走,开 MT5 把这段塞进 EA 的 OnTimer 里就能看到控件响应。

MQL5 / C++
CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, class="num">8);
SymbolSelect(def_SymbolReplay, true);
m_Infos.CountReplay = class="num">0;
m_IndControl.Handle = INVALID_HANDLE;
m_IndControl.Mode = C_Controls::ePause;
m_IndControl.Position = class="num">0;
m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState;
}

class="kw">inline class="type">void UpdateIndicatorControl(class="type">void)
  {
   class="kw">static class="type">bool bTest = class="kw">false;
   class="type">class="kw">double Buff[];

   if(m_IndControl.Handle == INVALID_HANDLE) class="kw">return;
   if(m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)
     {
      if(bTest)
        m_IndControl.Mode = (ObjectGetInteger(m_Infos.IdReplay, def_ObjectCtrlName((C_Controls::eObjectControl)C_Controls::ePlay), OBJPROP_STATE) == class="num">1 ? C_Controls::ePause : C_Controls::ePlay);
      else
        {
         if(CopyBuffer(m_IndControl.Handle, class="num">0, class="num">0, class="num">1, Buff) == class="num">1)
            m_IndControl.Memory.dValue = Buff[class="num">0];
         if((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState)
            if(bTest = ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay))
               m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition];
        }
     }else

「把回放控制状态塞进联合体内存」

这段逻辑出现在回放指标初始化分支里,核心是把控盘对象的状态打包进一个 16+8 字节的联合体,再抛给图表自定义事件。 第 55、56 行分别把持仓枚举和模式强转 ushort 写进 _16b 数组的对应槽位;57、58 行在 _8b 的高两位写入 'D' 和 'M' 字符标记,相当于给内存块打了一个可读指纹。 [CODE]

  • {
  • m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
  • m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode;
  • m_IndControl.Memory._8b[7] = 'D';
  • m_IndControl.Memory._8b[6] = 'M';
  • EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, "");
  • bTest = false;
  • }
  • }
  • //+------------------------------------------------------------------+

[/CODE] 逐行拆一下:55 行把当前仓位方向存到联合体前 16 位区的 eCtrlPosition 下标;56 行把运行模式转成无符号短整型塞进 eCtrlStatus 槽,避免符号位污染。57、58 行反向填字符,方便在内存观测器里肉眼区分块类型。59 行用 EventChartCustom 把整个 dValue(双精度视图)发到 IdReplay 标识的图表事件,参数 evCtrlReplayInit 告诉接收端这是初始化帧。60 行把测试开关关掉,防止重复触发。 在 MT5 里打开回放指标,把联合体定义成 _16b[2]_8b[8] 共享 dValue,断点跑这段,能在自定义事件日志里看到 dValue 随 Mode 切换而变化。外汇与贵金属回放测试属高风险验证环境,实盘前需在模拟盘充分跑通。

MQL5 / C++
class="num">54.     {
class="num">55.       m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
class="num">56.       m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (class="type">class="kw">ushort)m_IndControl.Mode;
class="num">57.       m_IndControl.Memory._8b[class="num">7] = &class="macro">#x27;D&class="macro">#x27;;
class="num">58.       m_IndControl.Memory._8b[class="num">6] = &class="macro">#x27;M&class="macro">#x27;;
class="num">59.       EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, class="num">0, m_IndControl.Memory.dValue, "");
class="num">60.       bTest = class="kw">false;
class="num">61.     }
class="num">62.   }
class="num">63. class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 别急着下结论

前面几轮重构把 C_Controls.mqh 里的定义搬进 C_Replay.mqh,可读性确实上来了,但回放服务的实测耗时几乎没有波动——至少在我的 MT5 环境里没看到明显变化。 真正能拉开差距的改动看起来有点反直觉:恢复按预期节奏生成一分钟柱的逻辑,而非在循环里做多余判断。把 C_Replay.mqh 整文件读一遍就能发现,几处直接调用生成函数的写法比原先绕一层封装快出一截。 你完全可以自己开 MT5 挂上 Anexo.zip 里的工程,用策略测试器跑同一段历史,对比修改前后从加载到首根回放柱的耗时。外汇与贵金属模拟本身高风险,任何性能优化都不构成胜率保证,只是让你验证想法时少等几秒。

常见问题

直接读取按钮图形对象的状态属性即可,比定时扫缓冲区更省资源,事件触发时同步更新逻辑。
先建自定义品种并写入历史 tick,再把回放控制面板的初始化和该符号绑定,确保控件状态与符号同步。
小布可按你的品种自动生成回放符号模板并监控按钮对象状态,你只需确认开始/暂停动作。
在符号创建完成后立即强制刷新一次控件值,并把控制状态写进联合体避免多副本冲突。
有必要,联合体避免重复缓冲占用,但别急着认定这是唯一解法,先在小数据量下验证稳定性。