开发回放系统  市场模拟(第 17 部分):跳价和更多跳价(I)·综合运用

开发回放系统 市场模拟(第 17 部分):跳价和更多跳价(I)·综合运用

(3/3)·从市场观察窗到订单系统,跳价数据流如何在不拖慢回放的前提下逼近真实盘口

实战向进阶 第 3/3 篇
很多人以为回放系统只要把 K 线重画一遍就够像了,却漏掉了市场观察窗里那些细微的 Ask/Bid 跳动。缺了跳价注入,模拟盘和实盘的手感会差出一大截,订单簿类策略回测结论可能直接失真。

回放引擎里的新K线判定与耗时埋点

在 MT5 自定义回放框架中,CreateBarInReplay 负责把逐笔 tick 流重建成可显示的蜡烛。它先比对当前 tick 的时间戳与已挂载 K 线的分钟级时间(macroRemoveSec 去掉秒),不一致就置 bNew 为真,意味着要开一根新 Bar。 若调用时传入 bViewMetrics=true,函数会用静态变量 _mdt 记录上一次打点时刻,新 Bar 触发时算出间隔毫秒:_mdt = GetTickCount64() - _mdt。随后把毫秒转成「分:秒.毫秒」打印,例如 1234 ms 会输出 0:1.234,方便你量化重建效率。 外层循环用 for(iPos = iPos>0?iPos-1:0; m_ReplayCount<iPos && !_StopFlag; ) 持续喂数据,并用 GlobalVariableSet 回写进度变量。外汇与贵金属回放属高频重建,实盘模拟有滑点偏差高风险,数值仅作本地验证参考。 别把打印耗时当行情速度 GetTickCount64 量的是你本机回放循环的 CPU 耗时,不是经纪商真实 tick 到达间隔;同一段 EURUSD 1 分钟数据在 SSD 与 HDD 上可能差出 200~400 ms,调参前先固定硬件环境。

MQL5 / C++
CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount);
}
}
for (iPos = (iPos > class="num">0 ? iPos - class="num">1 : class="num">0); (m_ReplayCount < iPos) && (!_StopFlag);) CreateBarInReplay();
Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
Info.s_Infos.isWait = class="kw">false;
GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
}
class="kw">inline class="type">void CreateBarInReplay(class="kw">const class="type">bool bViewMetrics = class="kw">false)
{
class="macro">#define def_Rate m_MountBar.Rate[class="num">0]
class="kw">static class="type">class="kw">ulong _mdt = class="num">0;
class="type">int i;

if (m_MountBar.bNew = (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)))
{
if (bViewMetrics)
{
_mdt = (_mdt > class="num">0 ? GetTickCount64() - _mdt : _mdt);
i = (class="type">int) (_mdt / class="num">1000);
Print(TimeToString(m_Ticks.Info[m_ReplayCount].time, TIME_SECONDS), " - Metrica: ", i / class="num">60, ":", i % class="num">60, ".", (_mdt % class="num">1000));

「回放中每跳合成K线的关键赋值」

在 MT5 自定义回放引擎里,每来一跳都要把 Tick 数据灌进当前挂bar的结构体。下面这段逻辑决定了 open/high/low/close 以及两类成交量的更新方式,直接决定你回放出来的蜡烛是否和真实分笔一致。 新bar的判定靠 m_MountBar.bNew:若为 true,open 直接取当前 last,high/low 也以 close 重置;否则只在穿透极值时才更新 high/low。这样能避免旧bar被回放后期跳价错误拉伸。 real_volume 累加的是 volume_real 的 long 值,tick_volume 则按「该跳真实量>0 则计 1」来累计——这意味着零成交量跳价不会污染 tick 计数。最后用 CustomRatesUpdate 把单根 Rate 推送给图表,m_ReplayCount 自增进入下一跳。 别把正态当圣经:回放里 time 字段用的是 macroRemoveSec 处理后的秒级对齐时间,若你原始 tick 带亚秒精度,合成 bar 的时间轴会和实盘存在 0~59 秒偏移,验证时要在 MT5 里开「显示秒」对比。

MQL5 / C++
      _mdt = GetTickCount64();
      }
      m_MountBar.memDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
      def_Rate.real_volume = class="num">0;
      def_Rate.tick_volume = class="num">0;
      }
      def_Rate.close = m_Ticks.Info[m_ReplayCount].last;
      def_Rate.open = (m_MountBar.bNew ? def_Rate.close : def_Rate.open);
      def_Rate.high = (m_MountBar.bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
      def_Rate.low = (m_MountBar.bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low);
      def_Rate.real_volume += (class="type">long) m_Ticks.Info[m_ReplayCount].volume_real;
      def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > class="num">0 ? class="num">1 : class="num">0);
      def_Rate.time = m_MountBar.memDT;
      m_MountBar.bNew = class="kw">false;
      CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate, class="num">1);
      m_ReplayCount++;
      
class="macro">#undef def_Rate
      }
class="kw">inline class="type">void CreateBarInReplay(class="kw">const class="type">bool bViewMetrics = class="kw">false)
      {
class="macro">#define def_Rate m_MountBar.Rate[class="num">0]
      class="type">bool bNew;
      if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))

◍ 逐笔合成自定义K线的关键代码段

在 MT5 回放引擎里,把逐笔 tick 聚合成自定义周期 K 线,核心就在这一段条件更新逻辑。当 bViewMetrics 为真时先刷指标,再把当前挂载 Bar 的时间戳去掉秒级精度,并把实时成交量与 tick 计数清零,标记新 Bar 的起点。 bNewtick_volume == 0 判定:若为真,说明这是该 Bar 的第一笔,open 直接取当前 close;否则沿用已有 open。high / low 则只在新 Bar 或价格突破原极值时才被改写,避免无谓重算。 每来一个 tick,real_volume 累加 volume_real 的 long 值,tick_volume 仅在 volume_real > 0 时加 1——这意味着零成交量 tick 不计入笔数。最后用 CustomRatesUpdate 把合成后的 Rate 推给图表,并 m_ReplayCount++ 指向下一 tick。外汇与贵金属品种点差跳变频繁,这类自合成 Bar 在高波动时段可能偏离经纪商真实聚合结果,需自行比对验证。 下面这段是原文中实际参与挂载 Bar 更新的内联代码,逐行对应上述逻辑,可直接拷进 EA 的回放模块做断点观察。

MQL5 / C++
{
   if (bViewMetrics) Metrics();
   m_MountBar.memDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
   def_Rate.real_volume = class="num">0;
   def_Rate.tick_volume = class="num">0;
   }
   bNew = (def_Rate.tick_volume == class="num">0);
   def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > class="num">0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close);
   def_Rate.open = (bNew ? def_Rate.close : def_Rate.open);
   def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
   def_Rate.low = (bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low);
   def_Rate.real_volume += (class="type">long) m_Ticks.Info[m_ReplayCount].volume_real;
   def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > class="num">0 ? class="num">1 : class="num">0);
   def_Rate.time = m_MountBar.memDT;
   CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
   ViewTick();
   m_ReplayCount++;

class="macro">#undef def_Rate
   }
class="kw">inline class="type">void Metrics(class="type">void)
   {
   class="type">int i;
   class="kw">static class="type">class="kw">ulong _mdt = class="num">0;

把回放tick塞进自定义符号并打点耗时

这段逻辑负责在回放每笔 tick 时,把耗时统计和自定义符号写 tick 两件事接上。先算距上次打点的毫秒差:若 _mdt 大于 0 就取当前 GetTickCount64() 减去它,否则保持原值;再把毫秒转成秒赋给 i,用于后面打印分:秒.毫秒。 打印行会输出 tick 时间(精确到秒)、Metrica 耗时(i/60 为分、i%60 为秒、_mdt%1000 为毫秒),方便你直接看每笔回放的处理延迟分布。 ViewTick() 则是把当前回放 tick 拷进数组,把 time_msc 重算为「秒*1000+原毫秒」的绝对毫秒,再调 CustomTicksAdd 写进 def_SymbolReplay 自定义符号。开 MT5 把这段接进你的回放类,能在「摘要」窗口实时看到延迟,若发现 i/60 频繁大于 0 说明单 tick 处理已超 1 秒,回放速度可能已失真。外汇与贵金属回放仅作历史推演,实盘高风险,数据不代表未来。

MQL5 / C++
      _mdt = (_mdt > class="num">0 ? GetTickCount64() - _mdt : _mdt);
      i = (class="type">int) (_mdt / class="num">1000);
      Print(TimeToString(m_Ticks.Info[m_ReplayCount].time, TIME_SECONDS), " - Metrica: ", i / class="num">60, ":", i % class="num">60, ".", (_mdt % class="num">1000));
      _mdt = GetTickCount64();
      }
class="kw">inline class="type">void ViewTick(class="type">void)
      {
      class="type">MqlTick tick[class="num">1];
      tick[class="num">0] = m_Ticks.Info[m_ReplayCount];
      tick[class="num">0].time_msc = (m_Ticks.Info[m_ReplayCount].time * class="num">1000) + m_Ticks.Info[m_ReplayCount].time_msc;
      CustomTicksAdd(def_SymbolReplay, tick);
      }

「别急着下结论」

回放/模拟系统现在能跑,但快进功能先别碰。实测里只要时间步长大于 1 秒,市场观察窗口的跳价时间和系统输入值就对不上,同步直接失效。 迷你指数类合约在 1 分钟周期内表现还行,可一旦换成成交稀疏的资产,回放器经常卡住——柱线设计本身没问题,是调度逻辑漏了。这类毛病得等下一篇修,现在先用慢速回放验证策略。 外汇和贵金属波动大、流动性分层明显,拿这套东西测之前先确认点差和成交频率,否则模拟结果会骗人。真想往编程深里走,就得弄明白为什么同步会歪:躲着 bug 走的人写不出能用的工具。 附件里 Market_Replay_lvw_17.zip 约 9.8 MB,拆开看市场观察跳价处理那段代码,比看文档来得直接。修好之前,把它当半成品用,别拿去下实盘结论。

让小布替你跑这套
这些跳价注入与校验逻辑,小布盯盘已内置到对应品种回放页,打开即可加载真实盘口微结构,你只管调策略参数。

常见问题

多数自建回放只播成交 tick,未同步推送 Ask/Bid 变更事件;本篇把跳价作为独立数据流注入,才能对齐平台原生显示。
市场观察窗口、市场深度和订单系统三处都渲染跳价;指标方式亦可调用同一数据源,需引擎统一广播。
可以,小布盯盘的品种回放页已支持跳价层叠加,不需要自己写 C_Replay 派生类就能看到盘口微跳动。
原大类拆出传输与建模子模块,跳价采集归到行情源适配层,呈现归到图表渲染代理,降低单类耦合。
是跳价时序与成交 tick 的先后顺序错位,会导致深度图闪跳;需用同一时钟域排序再派发。