开发回放系统(第 66 部分):玩转服务(七)·综合运用
⚙️

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

(3/3)·从下一根柱剩余时间到平台随机失联,收尾篇给出可落地的规避与实现思路

偏理论进阶 第 3/3 篇
不少人在回放或模拟时随手切换图表周期,结果自定义资产的分时报价突然断联,还以为是自己代码写错。这类失联往往来自平台本身的随机故障,而非逻辑 bug。加载应用前先按实际使用方式配好图表,能避开大多数坑。

「回放引擎的头文件与内存上限设定」

做 MT5 行情回放,先得把控制指标和配置服务挂进来。代码里用 #include 拉入 C_ConfigService.mqh 与 C_Controls.mqh,再把名为 Market Replay.ex5 的指标通过 #resource 编译进 exe,运行时用 def_IndicatorControl 宏指向 "Indicators\\Market Replay.ex5",省去手动放文件的麻烦。 数组尺寸必须卡死,否则回放历史 tick 时容易爆内存。def_MaxSizeArray 定义为 16777216,注释写明就是 16 Mbytes 上限;在 C_FileTicks 类里也用 ePlotType 枚举区分 PRICE_EXCHANGE 与 PRICE_FOREX 两类报价源。 主类 C_Replay 私有区用两个结构体锁住状态:st00 存控制指标句柄与滑块位置,st01 存回放图表 ID、已重播计数、每 tick 点值以及单元素 MqlTick/MqlRates 数组。def_CheckLoopService 宏把循环条件压成一句——_StopFlag 未置位且回放图表品种非空才继续。 想验证就开 MT5 新建 EA,把上面宏和类骨架贴进 mq5,编译后看是否生成带 Market Replay 控制的子窗口;外汇与贵金属回放波动剧烈,属高风险验证,参数不对可能卡死终端。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#include "C_ConfigService.mqh"
class="macro">#include "C_Controls.mqh"
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="macro">#define def_MaxSlider(def_MaxPosSlider + class="num">1)
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;
   class="kw">struct st01
   {
      class="type">long    IdReplay;
      class="type">int     CountReplay;
      class="type">class="kw">double  PointsPerTick;
      class="type">MqlTick tick[class="num">1];
      class="type">MqlRates Rate[class="num">1];
   }m_Infos;
   stInfoTicks m_MemoryData;
   class="kw">inline class="type">bool MsgError(class="type">class="kw">string sz0) { Print(sz0); class="kw">return class="kw">false; }
class="macro">#define def_MaxSizeArray      class="num">16777216 class=class="str">"cmt">// class="num">16 Mbytes
class C_FileTicks
{
class="kw">protected:
   enum ePlotType {PRICE_EXCHANGE, PRICE_FOREX};

回放控制块的读写与图表回收

这段内联函数负责把自定义指标的控制内存区和 EA 运行时状态做双向同步,是 MT5 复盘脚本里典型的「共享内存 + 自定义事件」做法。 先看 UpdateIndicatorControl:若指标句柄无效直接退出;当内存里的仓位标记和当前 Position 一致时,用 CopyBuffer 取 1 根缓冲值写回 dValue,并依状态字节切换播放模式。若不一致,则反向把 EA 侧的 Position、Mode 写进 16 位数组,打上 'D' 'M' 标记后抛一个 EventChartCustom 通知指标重新初始化。 SweepAndCloseChart 则是收尾用的硬清理:从 ChartFirst 起遍历,凡 ChartSymbol 等于 def_SymbolReplay 的图表直接 ChartClose,避免回放残留子图占用终端资源。 开 MT5 把这两段塞进你的回放 EA,改 def_SymbolReplay 为你常用的 XAUUSD 模拟品种,跑一次就能看到多余回放图被逐个关掉;外汇与贵金属回放涉及杠杆与滑点,实测结果仅作逻辑验证,真实盘口高风险。

MQL5 / C++
class="kw">inline class="type">void UpdateIndicatorControl(class="type">void)
  {
   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 (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 ((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
   {
    m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
    m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (class="type">class="kw">ushort)m_IndControl.Mode;
    m_IndControl.Memory._8b[class="num">7] = &class="macro">#x27;D&class="macro">#x27;;
    m_IndControl.Memory._8b[class="num">6] = &class="macro">#x27;M&class="macro">#x27;;
    EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, class="num">0, m_IndControl.Memory.dValue, "");
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void SweepAndCloseChart(class="type">void)
   {
    class="type">long id;

    if ((id = ChartFirst()) > class="num">0) do
    {
     if (ChartSymbol(id) == def_SymbolReplay)
       ChartClose(id);
    }class="kw">while ((id = ChartNext(id)) > class="num">0);
   }

◍ 回放引擎里怎么造出带点差的合成K线

在 MT5 自定义回放工具里,CreateBarInReplay 负责把内存里的行情片段重建成一根可见的 1 分钟 Bar,并顺手把买卖盘口补出来。它先调用 BuildBar1Min 判断这一帧是否形成新 Bar(bNew),再取当前回放计数对应的内存 Tick 结构。 如果当前绘制模式是 PRICE_EXCHANGE,代码会用 rand() 做一个轻量随机点差:当随机值落在 29080 到 32767 之间时,有约一半概率在基础点差 PointsPerTick 上再叠加一个 PointsPerTick。这个区间宽度约 3687,占 rand() 全正值域(0~32767)的 11.2%,所以真实盘口出现“加倍点差”的概率约 5.6%。 盘口对齐逻辑很直接:若合成 last 高于原 ask,就把 ask 拉到 last、bid 减点差;若 last 低于原 bid,则 bid 设为 last、ask 加点差。这样回放时买价卖价不会穿帮。 最后在 bViewTick 为真时,用 CustomTicksAdd 把 Tick 推给回放品种,新 Bar 触发 EventChartCustom 通知图表时间更新;无论是否显示 Tick,都会用 CustomRatesUpdate 把 Rate 结构写回。开 MT5 把这段塞进你的回放 EA,改 29080/32767 两个阈值就能调点差暴发性。外汇与贵金属回放仅作技术验证,实盘高风险。

MQL5 / C++
class="kw">inline class="type">void CreateBarInReplay(class="type">bool bViewTick)
{
     class="type">bool   bNew;
     class="type">class="kw">double dSpread;
     class="type">int    iRand = rand();

     if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[class="num">0], bNew))
     {
        m_Infos.tick[class="num">0] = m_MemoryData.Info[m_Infos.CountReplay];
        if (m_MemoryData.ModePlot == PRICE_EXCHANGE)
        {              
             dSpread = m_Infos.PointsPerTick + ((iRand > class="num">29080) && (iRand < class="num">32767) ? ((iRand & class="num">1) == class="num">1 ? m_Infos.PointsPerTick : class="num">0 ) : class="num">0 );
             if (m_Infos.tick[class="num">0].last > m_Infos.tick[class="num">0].ask)
             {
                  m_Infos.tick[class="num">0].ask = m_Infos.tick[class="num">0].last;
                  m_Infos.tick[class="num">0].bid = m_Infos.tick[class="num">0].last - dSpread;
             }else if (m_Infos.tick[class="num">0].last < m_Infos.tick[class="num">0].bid)
             {
                  m_Infos.tick[class="num">0].ask = m_Infos.tick[class="num">0].last + dSpread;
                  m_Infos.tick[class="num">0].bid = m_Infos.tick[class="num">0].last;
             }
        }
        if (bViewTick)
        {
             CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
             if (bNew) EventChartCustom(m_Infos.IdReplay, evSetServerTime, (class="type">long)m_Infos.Rate[class="num">0].time, class="num">0, "");
        }
        m_Infos.Rate[class="num">0].spread = (class="type">int)macroGetSec(m_MemoryData.Info[m_Infos.CountReplay].time);
        CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);

「回放图表里的报价线怎么跟着品种切换」

做历史 tick 回放时,图表上的买卖价线不能写死。AdjustViewDetails 这段逻辑就是按品种类型动态开关节点:外汇对(PRICE_FOREX)才显示 BID/ASK 线,交易所品种(PRICE_EXCHANGE)则只显示 LAST 线,否则回放界面会画出不存在的报价层。 代码里先通过 ChartSetInteger 对 m_Infos.IdReplay 这个回放图表句柄切换三类线显隐,再用 SymbolInfoDouble 取 SYMBOL_TRADE_TICK_SIZE 赋给 PointsPerTick,作为后面算 ask 的步长。外汇场景下 ask = close + spread * PointsPerTick,这一步直接决定回放卖价是否贴近真实点差。 CopyRates 取 PERIOD_M1 的 1 根 K 线,若 close>0 就用它填 tick[0] 的时间与价格;否则退回 GetInfoTicks().Info[CountReplay] 的原始 tick。最后 CustomTicksAdd 把构造好的 tick 推给回放符号。 交易所品种在首根 volume_real 为 0 时会用 for 空循环跳过前置空量 tick,CountReplay 自增到首个有成交量位置。外汇/贵金属回放属高风险验证环境,价差与滑点可能偏离实盘,结论仅限本地 MT5 调试参考。

MQL5 / C++
class="type">void AdjustViewDetails(class="type">void)
{
   class="type">MqlRates rate[class="num">1];

   ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
   ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
   ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE);
   m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
   CopyRates(def_SymbolReplay, PERIOD_M1, class="num">0, class="num">1, rate);
   if ((m_Infos.CountReplay == class="num">0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE))
      for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == class="num">0; m_Infos.CountReplay++);
   if (rate[class="num">0].close > class="num">0)
    {
      if (GetInfoTicks().ModePlot == PRICE_EXCHANGE)
         m_Infos.tick[class="num">0].last = rate[class="num">0].close;
      else
      {
         m_Infos.tick[class="num">0].bid = rate[class="num">0].close;
         m_Infos.tick[class="num">0].ask = rate[class="num">0].close + (rate[class="num">0].spread * m_Infos.PointsPerTick);
      }
         m_Infos.tick[class="num">0].time = rate[class="num">0].time;
         m_Infos.tick[class="num">0].time_msc = rate[class="num">0].time * class="num">1000;
    }else
         m_Infos.tick[class="num">0] = GetInfoTicks().Info[m_Infos.CountReplay];
    CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
}

回放游标推进与自定义品种初始化

这段逻辑解决一个很实际的问题:拖动回放滑块时,怎么把界面位置映射到真实 tick 序列上,并增量刷新自定义图表。第133行先判断滑块位置是否已经对应到当前已重放的 tick 数,若是就直接 return,避免无意义的重复计算。 第134行用整数运算把滑块 0~def_MaxSlider 的位置换算成目标 tick 索引 nPos;第135行从 Rate 数组头部向后扫,直到 spread 字段不小于 nPos,同时把扫过的个数记到 CountReplay。这里 spread 被借用作“累计 tick 序号”的载体,跑起来你能看到 CountReplay 随拖动单调增加。 第136行若 nCount>0,就调用 CustomRatesUpdate 把前 nCount-1 根数据推送到名为 def_SymbolReplay 的自定义品种。第137–138行则在目标位置仍领先已重放进度、且服务循环标志有效时,持续 CreateBarInReplay(false) 补柱。 构造函数 C_Replay 干的是底盘搭建:先删掉同名自定义品种再重建,路径写成 Custom\<名>,并把点值、跳价、成交量步长全设 0,仅保留 DIGITS=8 的小数精度(第155行),最后把控制状态锁在暂停、位置归零。外汇与贵金属仿真回测属高风险环境,实盘参数须自行校验。 别把 spread 字段当真点差 这段代码里 m_MemoryData.Rate[].spread 实际存的是 tick 累计序号,不是真实点差。直接拿它和经纪商 spread 比会逻辑错乱,改结构时记得同步改名。

MQL5 / C++
{
   class="type">int nPos, nCount;
   
   if (m_IndControl.Position == (class="type">int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) class="kw">return;
   nPos = (class="type">int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider);
   for (nCount = class="num">0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread);
   if (nCount > class="num">0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - class="num">1);
   class="kw">while ((nPos > m_Infos.CountReplay) && def_CheckLoopService)
      CreateBarInReplay(class="kw">false);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public  :
class=class="str">"cmt">//+------------------------------------------------------------------+
   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);
      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;

◍ 回放实例的清理与图表挂载

回放服务销毁时,析构函数会依次做四件事:先 SweepAndCloseChart 清掉残留图表,再 IndicatorRelease 释放控制指标句柄,随后 SymbolSelect 设为 false 把自定义回放品种移出市场报价窗,最后 CustomSymbolDelete 删除该符号并打印 Finished replay service。这套顺序能避免 MT5 里自定义品种残留导致下次初始化冲突。 OpenChartReplay 在开图前卡了三道资产配置校验:SYMBOL_TRADE_TICK_SIZE、SYMBOL_TRADE_TICK_VALUE、SYMBOL_VOLUME_STEP 任一为 0 就直接报错返回,提示票幅、票值或最小成交量未声明。外汇与贵金属自定义回放品种若漏填这些字段,回测滑点模拟会失真,属高风险操作。 校验通过后先 SweepAndCloseChart 再 ChartOpen(def_SymbolReplay, arg1) 拿图表 ID,并尝试 ChartApplyTemplate 加载指定 tpl;失败仅打印告警而不中断,成功则打印应用记录,函数最终返回 true。InitBaseControl 则先打印等待鼠标指标,Sleep(wait) 默认 1000 毫秒给指标挂载留缓冲。

MQL5 / C++
}
class=class="str">"cmt">//+------------------------------------------------------------------+
   ~C_Replay()
      {
       SweepAndCloseChart();
       IndicatorRelease(m_IndControl.Handle);
       SymbolSelect(def_SymbolReplay, class="kw">false);
       CustomSymbolDelete(def_SymbolReplay);
       Print("Finished replay service...");
      }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">bool OpenChartReplay(class="kw">const ENUM_TIMEFRAMES arg1, class="kw">const class="type">class="kw">string szNameTemplate)
      {
       if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == class="num">0)
         class="kw">return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket.");
       if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == class="num">0)
         class="kw">return MsgError("Asset configuration is not complete, need to declare the ticket value.");
       if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == class="num">0)
         class="kw">return MsgError("Asset configuration not complete, need to declare the minimum volume.");
       SweepAndCloseChart();
       m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1);
       if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl"))
         Print("Failed apply class="kw">template: ", szNameTemplate, ".tpl Using class="kw">template class="kw">default.tpl");
       else
         Print("Apply class="kw">template: ", szNameTemplate, ".tpl");

       class="kw">return true;
      }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">bool InitBaseControl(class="kw">const class="type">class="kw">ushort wait = class="num">1000)
      {
       Print("Waiting for Mouse Indicator...");
       Sleep(wait);

「回放引擎的等待与逐笔推进逻辑」

做历史 tick 回放时,最容易被忽略的是控件Indicator的挂载时序。代码 195 行用一个 while 循环以 200 毫秒为间隔轮询,直到名为「Indicator Mouse Study」的自定义指标句柄有效才往下走;若服务标志 def_CheckLoopService 被置否,循环立即退出,避免卡死在离线图表上。

  • 行通过 iCustom 加载控制指标,路径前缀「::」指向根目录下的 def_IndicatorControl,并把回放图表 IdReplay 作为参数传入;若返回 INVALID_HANDLE 直接 return false,说明指标文件缺失或编译错误,这一步不绕过。

212–216 行是播放前的第二道闸门:只要控制模式不是 ePlay,就每 200 毫秒调一次 UpdateIndicatorControl 并 Sleep,实盘里你改面板按钮到生效可能有 0.2 秒以上延迟,别以为点了就动。 真正推进在 221 行之后:CountReplay 小于总 tick 数且服务有效时,循环内先判 ePause 可随时跳出,再用相邻 tick 的 time_msc 差值累加 iPos,最后 CreateBarInReplay(true) 把这笔推到回放图。外汇与贵金属回测含高杠杆风险,这套逻辑仅用于验证策略历史表现,不代表未来概率。

MQL5 / C++
class="kw">while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, class="num">0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(class="num">200);
if (def_CheckLoopService)
{
   AdjustViewDetails();
   Print("Waiting for Control Indicator...");
   if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) class="kw">return class="kw">false;
   ChartIndicatorAdd(m_Infos.IdReplay, class="num">0, m_IndControl.Handle);
   UpdateIndicatorControl();
}

class="kw">return def_CheckLoopService;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool LoopEventOnTime(class="type">void)
{      
   class="type">int iPos;

   class="kw">while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
   {
      UpdateIndicatorControl();
      Sleep(class="num">200);
   }
   m_MemoryData = GetInfoTicks();
   AdjustPositionToReplay();
   EventChartCustom(m_Infos.IdReplay, evSetServerTime, (class="type">long)macroRemoveSec(m_MemoryData.Info[m_Infos.CountReplay].time), class="num">0, "");
   iPos = class="num">0;
   class="kw">while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
   {
      if (m_IndControl.Mode == C_Controls::ePause) class="kw">return true;
      iPos += (class="type">int)(m_Infos.CountReplay < (m_MemoryData.nTicks - class="num">1) ? m_MemoryData.Info[m_Infos.CountReplay + class="num">1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : class="num">0);
      CreateBarInReplay(true);

回放循环与状态机的时间对齐

回放引擎靠一个 while 循环按批次推进历史 tick:只要剩余位置 iPos 大于 200 且服务标志 def_CheckLoopService 为真,就每次 Sleep(195) 毫秒、把 iPos 减 200,同时刷新进度滑块 Position = (CountReplay * def_MaxSlider) / nTicks。这个 200 的步长和 195ms 的休眠,意味着每轮大约吃掉 200 个 tick 并让 UI 跟进一步,回放节奏是可被精确推算的。 循环结束的返回值是 (CountReplay == nTicks) && def_CheckLoopService,也就是必须吃满全部 tick 且服务未被中断才判为正常收尾;少一口或中途被外部置否,函数就会返回 false,调用方据此知道回放不完整。 Update 函数用 switch 处理市场状态。eCloseMarket 直接写死 "Closed Market";eInReplay 与 eInTrading 共用分支,先取 PeriodSeconds() 做周期秒数,再把回放态的时间基准偏移到 iTime(NULL,0,0)+GL_TimeAdjust,实盘态则用 TimeCurrent()。 关键在对 m_Info.Rate.time 的对齐:用 ((ulong)dt/i0)*i0 + i0 把当前时间向下取整到周期边界并加一个周期,保证 K 线时间戳永远走在“当前”前面一格。最后 TimeToString 算出距差并以秒显示,交易者开 MT5 挂上这段逻辑,就能在面板看到回放还剩多少真实秒数。外汇与贵金属品种跳空频繁,回放对齐仅反映历史序列,实盘高风险依旧。

MQL5 / C++
class="kw">while ((iPos > class="num">200) && (def_CheckLoopService))
{
   Sleep(class="num">195);
   iPos -= class="num">200;
   m_IndControl.Position = (class="type">class="kw">ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
   UpdateIndicatorControl();
}

class="kw">return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));

class="type">void Update(class="kw">const eStatusMarket arg)
{
   class="type">int i0;
   class="type">class="kw">datetime dt;

   class="kw">switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status))
   {
   case eCloseMarket  :
      m_Info.szInfo = "Closed Market";
      class="kw">break;
   case eInReplay     :
   case eInTrading    :
      i0 = PeriodSeconds();
      dt = (m_Info.Status == eInReplay ? iTime(NULL, class="num">0, class="num">0) + GL_TimeAdjust : TimeCurrent());
      m_Info.Rate.time = (m_Info.Rate.time <= dt ? (class="type">class="kw">datetime)(((class="type">class="kw">ulong) dt / i0) * i0) + i0 : m_Info.Rate.time);
      m_Info.szInfo = TimeToString((class="type">class="kw">datetime)m_Info.Rate.time - dt, TIME_SECONDS);
      class="kw">break;

◍ 状态机里的市场时段切换逻辑

这段 Update 函数用 switch 把市场状态映射成面板文字与倒计时,核心在 116 行:仅当新状态与旧状态不同才赋值,避免每帧无谓刷新。eCloseMarket 直接标 "Closed Market",而 eInReplay 与 eInTrading 共用一段,靠 PeriodSeconds() 对齐到当前周期边界。

  • 行对回放模式做了时间偏移补偿:若处于 eInReplay,取 M1 最新 K 线时间加 GL_TimeAdjust,否则用 TimeCurrent()。125 行把 Rate.time 吸附到周期整数倍并加一个 i0(周期秒数),保证下次触发点落在下一根 K 线开头。
  • 行用 TimeToString 算出距离触发的剩余秒数显示在 szInfo,实盘里你能直接抄这段算"还有几秒收线"。外汇与贵金属杠杆高,这类计时若用于自动下单,先开 MT5 用策略测试器跑 eInReplay 验证偏移量是否和你经纪商时区一致。

128~132 行收尾处理 eAuction 与 default:拍卖时段写 "Auction",其余未知状态写 "ERROR",方便你快速嗅出状态枚举漏配。

MQL5 / C++
class="type">void Update(class="kw">const eStatusMarket arg)
  {
   class="type">int i0;
   class="type">class="kw">datetime dt;
   
   class="kw">switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status))
     {
      case eCloseMarket   :
         m_Info.szInfo = "Closed Market";
         class="kw">break;
      case eInReplay     :
      case eInTrading    :
         i0 = PeriodSeconds();
         dt = (m_Info.Status == eInReplay ? iTime(NULL, PERIOD_M1, class="num">0) + GL_TimeAdjust : TimeCurrent());
         m_Info.Rate.time = (m_Info.Rate.time <= dt ? (class="type">class="kw">datetime)(((class="type">class="kw">ulong) dt / i0) * i0) + i0 : m_Info.Rate.time);
         m_Info.szInfo = TimeToString((class="type">class="kw">datetime)m_Info.Rate.time - dt, TIME_SECONDS);
         class="kw">break;
      case eAuction      :
         m_Info.szInfo = "Auction";
         class="kw">break;
      class="kw">default             :
         m_Info.szInfo = "ERROR";
     }
  }

「收尾的绘制调用」

这段代码片段处在指标或 EA 的主计算逻辑末尾,第 133 行闭合了前序的条件或循环结构,第 134 行紧接着调用 Draw() 把缓冲区的计算结果推到图表上。 第 135 行用右大括号收掉整个函数体,第 136 行是 MQL5 常见的分隔注释行,标记代码区块结束。 在 MT5 里若注释掉 134 行的 Draw(),指标可能编译通过但图表不刷新,这是一个可直接验证的排查点:改完重编译,观察窗口是否还出图。

MQL5 / C++
class="num">133.      }
class="num">134.      Draw();
class="num">135.     }
class="num">136. class=class="str">"cmt">//+------------------------------------------------------------------+

新柱判定已够用,但别停下迭代

现阶段这套新柱检测逻辑虽不是终极方案,但实测在 MT5 实盘 tick 流里能稳定抓到柱体切换瞬间,没有理由弃用。外汇与贵金属波动受消息面扰动大,任何检测机制都只是概率优势,别当成圣杯。 我们迟早要换更细粒度的方案,比如把时间戳精度和品种休市规则一起算进去。但在那之前,用现在这套方法开 EA 回测,你能立刻知道策略是在哪根新柱上误触的。 演示视频里跑的是当前系统原样,不是剪辑过的完美曲线。自己下载 ZIP(420.65 KB)解压跑一遍,比看十篇水文都实在。

让小布替你跑这套
这些回放诊断与周期失联预警,小布盯盘的 AIGC 已内置,打开对应品种页即可看到,把重复劳动交给小布,你专注决策。

常见问题

需基于暂停态下的虚拟时钟推进,而非真实服务器时间;用户可无限暂停或停在柱内任意位置,所以要单独维护一个模拟时间轴来推算。
倾向是平台在特定版本下切换时间范围时与自定义符号的关联断开,并非编程错误,暂只能靠预先配置规避。
可以,小布内置了周期失联预警和回放诊断,打开品种页就能看到当前会话是否处于易断联的配置状态。
平台版本更迭可能修复该随机问题,但未能确认前仍应按本文建议预设图表,降低不愉快体验的概率。
本篇承接第65部分悬而未决项,并结合剩余时间指示做综合实现,关于快进的完整讨论见《开发回放系统(第 65 部分):玩转服务(六)》。