开发回放系统 市场模拟(第 02 部分):首次实验(II)·进阶篇
◍ 回放引擎里逐笔 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 分支。外汇与贵金属回放含高杠杆风险,回测结果仅代表历史数据特征,实盘可能显著偏离。
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 重构是否完整。
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 推演下一帧,返回负值即退出。外汇与贵金属复盘仍存在滑点与外盘断连风险,结果仅作概率参考。
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 分钟对于训练与练习已经足够,不必追求严丝合缝。外汇与贵金属品种点差跳变频繁,这类回放加速仅用于离线演练,实盘高杠杆风险依旧不可控。
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 {