开发回放系统 市场模拟(第 17 部分):跳价和更多跳价(I)·综合运用
(3/3)·从市场观察窗到订单系统,跳价数据流如何在不拖慢回放的前提下逼近真实盘口
回放引擎里的新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,调参前先固定硬件环境。
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 里开「显示秒」对比。
_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 的起点。
bNew 由 tick_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 的回放模块做断点观察。
{
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 秒,回放速度可能已失真。外汇与贵金属回放仅作历史推演,实盘高风险,数据不代表未来。
_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,拆开看市场观察跳价处理那段代码,比看文档来得直接。修好之前,把它当半成品用,别拿去下实盘结论。