开发回放系统  市场模拟(第 02 部分):首次实验(II)·进阶篇
📘

开发回放系统 市场模拟(第 02 部分):首次实验(II)·进阶篇

第 2/3 篇

◍ 回放引擎里逐笔 tick 如何并成一根 K

在做 MT5 历史回放时,核心动作是把逐笔 tick 按毫秒对齐后塞进当前 0 号柱。下面这段逻辑就是回放器每收到一批同毫秒 tick 时的处理骨架。 代码里先用 isNew 判断是不是新的一秒(或新柱),决定 time 和 open 是否重置;close 永远取最新 tick 的 Last 价。high / low 只在创极值或新柱时才改写,避免无谓刷新。 同毫秒的 tick 会被 while 循环一口吞掉:tick_volume 在新柱时直接赋值,否则累加 Vol 字段。循环结束立刻用 CustomRatesUpdate 把 1 根 m_Rate 推给 def_SymbolReplay 品种——这意味着回放品种的最小更新粒度就是同毫秒 batch,而不是单 tick。 mili 变量在循环后重算下一批的毫秒间隔,若跨秒则补 (1000 - mili) 做回绕。你在自写回放 EA 时,若发现回放品种成交量偏小,优先查这里 Vol 累加是否漏了非 isNew 分支。外汇与贵金属回放含高杠杆风险,回测结果仅代表历史数据特征,实盘可能显著偏离。

MQL5 / C++
m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt);
mili = m_ArrayInfoTicks[m_ReplayCount].milisec;
while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec)
{
   m_Rate[class="num">0].close = m_ArrayInfoTicks[m_ReplayCount].Last;
   m_Rate[class="num">0].open = (isNew ? m_Rate[class="num">0].close : m_Rate[class="num">0].open);
   m_Rate[class="num">0].high = (isNew || (m_Rate[class="num">0].close > m_Rate[class="num">0].high) ? m_Rate[class="num">0].close : m_Rate[class="num">0].high);
   m_Rate[class="num">0].low = (isNew || (m_Rate[class="num">0].close < m_Rate[class="num">0].low) ? m_Rate[class="num">0].close : m_Rate[class="num">0].low);
   m_Rate[class="num">0].tick_volume = (isNew ? m_ArrayInfoTicks[m_ReplayCount].Vol : m_Rate[class="num">0].tick_volume + m_ArrayInfoTicks[m_ReplayCount].Vol);
   isNew = class="kw">false;
   m_ReplayCount++;
}
m_Rate[class="num">0].time = m_dt;
CustomRatesUpdate(def_SymbolReplay, m_Rate, class="num">1);
mili = (m_ArrayInfoTicks[m_ReplayCount].milisec < mili ? m_ArrayInfoTicks[m_ReplayCount].milisec + (class="num">1000 - mili) : m_ArrayInfoTicks[m_ReplayCount].milisec - mili);
if ((macroGetMin(m_dt) == class="num">1) && (_dt > class="num">0))
{

回放服务的计时与事件驱动骨架

这段代码片段展示了行情回放服务(Market Replay)的核心驱动逻辑:用 GetTickCount64() 做毫秒级计时,按设定间隔向回放引擎推送事件。delay 变量初始为 3,代表每 3 毫秒尝试触发一次 Replay.Event_OnTime(),实际节奏会受 Sleep 与系统调度影响,MT5 Tick 回放不是严格实时。 代码里先通过 Replay.CreateSymbolReplay(user01) 载入指定 tick 文件(示例名 WINZ21_202110220900_202110221759,覆盖 2021.10.22 09:00–17:59 共约 8 小时数据),再用全局变量 def_SymbolReplay 作为“回放许可”信号,while 循环阻塞等待外部授权后才进入主回放循环。 主循环中 t1 记录上次事件时间戳,当 GetTickCount64()-t1 >= delay 时调用 Event_OnTime(),若返回负值则 break 退出。外汇与贵金属品种用这套机制回放历史 tick 时,需注意点差与滑点在高波动段可能被低估,属高风险验证行为,结论仅具概率性。 让小布替你跑这套:把 delay 从 3 改成 100 再编译,能明显降低 CPU 占用,便于观察逐笔 tick 重构是否完整。

MQL5 / C++
class="type">class="kw">ulong t1;
class="type">int delay = class="num">3;

if (!Replay.CreateSymbolReplay(user01)) class="kw">return;
Print("Waiting for permission to start replay ...");
GlobalVariableTemp(def_GlobalVariable01);
while (!GlobalVariableCheck(def_SymbolReplay)) Sleep(class="num">750);
Print("Replay service started ...");
t1 = GetTickCount64();
while (GlobalVariableCheck(def_SymbolReplay))
{
   if ((GetTickCount64() - t1) >= (class="type">uint)(delay))
   {
      if ((delay = Replay.Event_OnTime()) < class="num">0) break;
      t1 = GetTickCount64();
   }
}
GlobalVariableDel(def_GlobalVariable01);
Print("Replay service finished ...");

「用脚本驱动历史重播的启动与收尾」

想在 MT5 里做价格行为复盘,不一定手动点图表。下面这段脚本逻辑能把「Market Replay」类库接起来,自动等系统就绪再开重播。 核心是一个 OnStart 函数:先打印等待提示,然后用 while 循环每 500 毫秒查一次全局变量 def_GlobalVariable01 是否就位,同时监听 IsStopped 防止手动终止。若用户中途停止则直接 return,不进重播。 重播期间用 GlobalVariableTemp 写入品种标记,主循环持续 Sleep(500) 保活;结束删掉标记、打印完成、调 CloseReplay 收口。回测或复盘时这种结构能把人工操作压缩成一次脚本运行。 类库内部还有一层计时驱动:用 GetTickCount64 记 t1,只要全局变量 def_SymbolReplay 还在,就循环判断流逝时间是否 ≥ delay,到了就调 Event_OnTime 推演下一帧,返回负值即退出。外汇与贵金属复盘仍存在滑点与外盘断连风险,结果仅作概率参考。

MQL5 / C++
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Market Replay\C_Replay.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Replay Replay;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
      Print("Waiting for the Replay System ...");
      while((!GlobalVariableCheck(def_GlobalVariable01)) && (!IsStopped())) Sleep(class="num">500);
      if (IsStopped()) class="kw">return;
      Replay.ViewReplay();
      GlobalVariableTemp(def_SymbolReplay);
      while ((!IsStopped()) && (GlobalVariableCheck(def_GlobalVariable01))) Sleep(class="num">500);
      GlobalVariableDel(def_SymbolReplay);
      Print("Replay Script finished...");
      Replay.CloseReplay();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
      t1 = GetTickCount64();
      while (GlobalVariableCheck(def_SymbolReplay))
      {
class=class="str">"cmt">// ...  COMMENT ...  if ((GetTickCount64() - t1) >= (class="type">uint)(delay))
        {
          if ((delay = Replay.Event_OnTime()) < class="num">0) break;
          t1 = GetTickCount64();
        }
      }
      GlobalVariableDel(def_GlobalVariable01);

◍ 把回放压进一秒内的外循环代价

想让回放系统跑得比 1 分钟更快,最后这版改动只动了一处:塞进一个外循环去做 1 毫秒级的判定。毫秒字段里常出现相邻两行只差 1ms 的情况,但系统仍按同一秒归并处理,所以这一层循环可能对整体延迟产生肉眼可见的影响。 代码里用 macroGetMin 宏从时间戳抠出分钟数,Event_OnTime 中先用 m_ReplayCount 与 m_ArrayCount 比大小决定退出,再用静态 _dt 记录本地时间起点。新加的 do 循环(高亮部分)就是用来反复吃掉那 1 毫秒间隙的——系统内很难做精确调校,索性让这一毫秒从播放流里被剥离。 实测生成的 1 分钟柱线已经够用。再往下压榨到精确 1 分钟会抬高系统延迟,但本意只是搭一套能跑模拟研究的架子,接近 1 分钟对于训练与练习已经足够,不必追求严丝合缝。外汇与贵金属品种点差跳变频繁,这类回放加速仅用于离线演练,实盘高杠杆风险依旧不可控。

MQL5 / C++
class="macro">#define macroGetMin(A)  (class="type">int)((A - (A - ((A % class="num">3600) - (A % class="num">60)))) / class="num">60)
class="kw">inline class="type">int Event_OnTime(class="type">void)
                  {
                              class="type">bool isNew;
                              class="type">int mili;
                              class="kw">static class="type">class="kw">datetime _dt = class="num">0;
                              
                              if (m_ReplayCount >= m_ArrayCount) class="kw">return -class="num">1;
                              if (m_dt == class="num">0)
                              {
                                          m_Rate[class="num">0].close = m_Rate[class="num">0].open =  m_Rate[class="num">0].high = m_Rate[class="num">0].low = m_ArrayInfoTicks[m_ReplayCount].Last;
                                          m_Rate[class="num">0].tick_volume = class="num">0;
                                          m_Rate[class="num">0].time = m_ArrayInfoTicks[m_ReplayCount].dt - class="num">60;
                                          CustomRatesUpdate(def_SymbolReplay, m_Rate, class="num">1);
                                          _dt = TimeLocal();
                              }
                              isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt;
                              m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt);
                              mili = m_ArrayInfoTicks[m_ReplayCount].milisec;
                              do
                              {

常见问题

按时间戳落在同一根 K 的周期区间内聚合,超过周期就收线换新柱;回放只是用历史 tick 喂给同一套逻辑,合并规则与实盘一致,差别在数据源是否实时。
先搭一个按固定步长推进的外部循环,在每步里派发‘新 tick’‘新柱’‘计时到’三类事件,业务逻辑只订阅事件,不自己死循环读时间。
可以,小布能按你给的品种和时段跑回放,并直接标出形态与异常点,你只管看结论不用自己搭引擎。
启动前清空缓存并定位首 tick 时间,结尾强制收最后一根未闭合 K 并写回状态文件,否则重开时会丢尾段。
代价是 CPU 空转和计时抖动,若每步无休眠硬跑,低频品种浪费资源、高频品种可能丢事件,建议按数据密度动态步长。