开发回放系统  市场模拟(第 05 部分):加入预览·综合运用
📘

开发回放系统 市场模拟(第 05 部分):加入预览·综合运用

第 3/3 篇

回放引擎的 Tick 聚合与图表生命周期

做 MT5 市场回放,核心是把逐笔 tick 重组成 1 分钟 K 线并推到独立图表。下面这段逻辑直接操作自定义品种图表:开图、套模板、退场清场,都在几个函数里闭环。 ViewReplay 用 ChartOpen 以 PERIOD_M1 打开名为 def_SymbolReplay 的自定义品种,再 ApplyTemplate 载入 "Market Replay.tpl" 并 Redraw,返回图表 ID 供后续写数据。CloseReplay 则反向操作:关图、SymbolSelect(false) 移除市场报价可见性、CustomSymbolDelete 删品种、GlobalVariableDel 清全局变量,避免残留。 Event_OnTime 是回放节拍器。静态 MqlRates Rate[1] 只存一根 Bar;用 _dt 记录上一根时间,m_ReplayCount 遍历 m_Ticks.Info[]。当检测到新分钟(_dt != time),重置 vol 为 0;否则在同日毫秒内 while 循环把同毫秒 tick 全部累进 close/open/high/low 与 real_volume。 外汇与贵金属回放涉及高杠杆与滑点风险,实盘前务必用历史数据验证聚合是否丢失 tick。开 MT5 把这段接进 EA,先跑 2023 年 EURUSD 的 1 分钟样本,看 Rate[0].real_volume 累加是否与源 tick 数一致。

MQL5 / C++
class="kw">return false;
}
class="kw">return true;
}
class="type">long ViewReplay(class="type">void)
{
    m_IdReplay = ChartOpen(def_SymbolReplay, PERIOD_M1);
    ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
    ChartRedraw(m_IdReplay);
    class="kw">return m_IdReplay;
}
class="type">void CloseReplay(class="type">void)
{
    ChartClose(m_IdReplay);
    SymbolSelect(def_SymbolReplay, false);
    CustomSymbolDelete(def_SymbolReplay);
    GlobalVariableDel(def_GlobalVariableReplay);
}
class="kw">inline class="type">int Event_OnTime(class="type">void)
{
    class="type">bool    bNew;
    class="type">int     mili, iPos;
    u_Interprocess Info;
    class="kw">static class="type">MqlRates Rate[class="num">1];
    class="kw">static class="type">class="kw">datetime _dt = class="num">0;

    if (m_ReplayCount >= m_Ticks.nTicks) class="kw">return -class="num">1;
    if (bNew = (_dt != m_Ticks.Info[m_ReplayCount].time))
    {
        _dt = m_Ticks.Info[m_ReplayCount].time;
        Rate[class="num">0].real_volume = class="num">0;
        Rate[class="num">0].tick_volume = class="num">0;
    }
    mili = (class="type">int) m_Ticks.Info[m_ReplayCount].time_msc;
    do
    {
        while (mili == m_Ticks.Info[m_ReplayCount].time_msc)
        {
            Rate[class="num">0].close = m_Ticks.Info[m_ReplayCount].last;
            Rate[class="num">0].open = (bNew ? Rate[class="num">0].close : Rate[class="num">0].open);
            Rate[class="num">0].high = (bNew || (Rate[class="num">0].close > Rate[class="num">0].high) ? Rate[class="num">0].close : Rate[class="num">0].high);
            Rate[class="num">0].low = (bNew || (Rate[class="num">0].close < Rate[class="num">0].low) ? Rate[class="num">0].close : Rate[class="num">0].low);
            Rate[class="num">0].real_volume += (class="type">long) m_Ticks.Info[m_ReplayCount].volume_real;
            bNew = false;
            m_ReplayCount++;
        }

「回放进度与仓位重算的底层循环」

这段逻辑干了两件事:一边按毫秒级 tick 推进虚拟品种行情,一边根据滑块位置重算回放游标。mili 自增后和当前 tick 的 time_msc 做 do-while 比对,保证同一毫秒内的多笔 tick 被全部吃进,再借 CustomRatesUpdate 把合成 K 线推给 def_SymbolReplay。 进度同步靠全局变量传递。iPos 用 (m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks 算百分比位置,只有发生偏移才写 GlobalVariableSet,避免每 tick 无谓落盘。 AdjustPositionReplay 里若检测到滑块对应的 iPos 小于已回放计数,就 CustomRatesDelete 清掉从 m_dtPrevLoading 到 LONG_MAX 的历史,并把 m_ReplayCount 归零重跑——这意味着拖滑块往回拉会触发整段重放,而不是局部修补。 return 里的延时算法值得注意:time_msc 小于 mili 时补 (1000 - mili),否则减 mili,实质是把当前 tick 相对整秒起点的偏移换算成下次定时器间隔。外汇与贵金属回测用这类机制时波动放大,属高风险操作,参数不对可能跑出和实盘偏差极大的曲线。

MQL5 / C++
         mili++;
      }while (mili == m_Ticks.Info[m_ReplayCount].time_msc);
         Rate[class="num">0].time = m_Ticks.Info[m_ReplayCount].time;
         CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1);
         iPos = (class="type">int)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
         GlobalVariableGet(def_GlobalVariableReplay, Info.Value);
         if (Info.s_Infos.iPosShift != iPos)
         {
            Info.s_Infos.iPosShift = iPos;
            GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
         }
         class="kw">return (class="type">int)(m_Ticks.Info[m_ReplayCount].time_msc < mili ? m_Ticks.Info[m_ReplayCount].time_msc + (class="num">1000 - mili) : m_Ticks.Info[m_ReplayCount].time_msc - mili);
}
class="type">int AdjustPositionReplay()
{
      u_Interprocess Info;
      class="type">MqlRates Rate[class="num">1];
      class="type">int iPos = (class="type">int)((m_ReplayCount * def_MaxPosSlider * class="num">1.0) / m_Ticks.nTicks);
         
      Info.Value = GlobalVariableGet(def_GlobalVariableReplay);
      if (Info.s_Infos.iPosShift == iPos) class="kw">return class="num">0;
      iPos = (class="type">int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * class="num">1.0) / def_MaxPosSlider));
      if (iPos < m_ReplayCount)
      {
         CustomRatesDelete(def_SymbolReplay, m_dtPrevLoading, LONG_MAX);
         m_ReplayCount = class="num">0;
         if (m_dtPrevLoading == class="num">0)
         {
            Rate[class="num">0].close = Rate[class="num">0].open = Rate[class="num">0].high = Rate[class="num">0].low = m_Ticks.Info[m_ReplayCount].last;
            Rate[class="num">0].tick_volume = class="num">0;
            Rate[class="num">0].time = m_Ticks.Info[m_ReplayCount].time - class="num">60;
            CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1);
         }
      };
      for (iPos = (iPos > class="num">0 ? iPos - class="num">1 : class="num">0); m_ReplayCount < iPos; m_ReplayCount++) Event_OnTime();
      class="kw">return Event_OnTime();
}

◍ 最后说句实在话

回放系统的核心配置与指标挂载逻辑在前文已拆透,这一节只补一个社区里真实踩过的坑。有用户在自建 EA 时照搬指标写法,用 IndicatorSetDouble 给 INDICATOR_CALCULATIONS 塞 OHLC 的 double 值,编译器报「必须是 double」,即便变量已显式转型、屏幕打印也显示 33573.43 / 33573.44 / 33569.98 / 33569.99 正常,依然编不过。 根因不在类型转换,而在运行时上下文:INDICATOR_CALCULATIONS 这类宏只服务于指标缓冲区,EA 里没有对应的指标环境,硬调必然失败。作者让他去翻 MQL5 的 running 文档才点醒——环境不对,代码再对也没用。 外汇与贵金属市场高杠杆、滑点无常,这类回放工具仅用于历史逻辑验证,实盘前务必在 MT5 策略测试器跑通再上真金。下回你抄片段时,先确认自己是写指标还是 EA,能省掉大半编译夜。

MQL5 / C++
IndicatorSetDouble(INDICATOR_LEVELVALUE, class="num">0, class="num">100);
IndicatorSetDouble(INDICATOR_LEVELVALUE, class="num">0, class="num">100);
mas nessa parte da erro dizendo que tem que ser class="type">class="kw">double
    IndicatorSetDouble(INDICATOR_CALCULATIONS, class="num">1, (class="type">class="kw">double) open_price);
    IndicatorSetDouble(INDICATOR_CALCULATIONS, class="num">2, (class="type">class="kw">double) high_price);
    IndicatorSetDouble(INDICATOR_CALCULATIONS, class="num">3, (class="type">class="kw">double) low_price);
    IndicatorSetDouble(INDICATOR_CALCULATIONS, class="num">4, (class="type">class="kw">double) close_price);

常见问题

按时间桶或成交量桶做轻量聚合,只在可视窗口内重建对象,移出视窗即销毁,避免全量重绘。
在回放主循环里每次推进都重算净值与占用保证金,以当前回放时间戳为基准重估,不依赖实时账户。
小布可加载回放任务并按你设定的品种与周期自动推进,标记你关心的价格行为,省去手动拖进度。
内存与句柄泄漏,MT终端变慢甚至崩溃;必须在生命周期结束或移出视窗时显式释放。
回放若用近似聚合会丢失原始tick微结构,结论仅作概率参考,外汇贵金属高风险,需实盘验证。