开发回放系统 市场模拟(第 25 部分):为下一步做准备·综合运用
◍ 回放循环的进程间同步与逐笔推进
这段逻辑干的事,是在 MT5 回放引擎里用全局变量做进程间握手,再按 tick 时间差逐步把 K 线推出来。LoopEventOnTime 每次被定时器唤起时,先读 m_Infos.bInit,未初始化就补设图表线显示:外汇对开 Ask/Bid 线,交易所品种开 Last 线,并取 SYMBOL_TRADE_TICK_SIZE 填 PointsPerTick。 初始化块里有个细节,m_MountBar.Rate[0].time 被置 0,意味着挂载 K 线的首根时间锚点会在后续 CreateBarInReplay 里重算,而不是沿用旧会话残留值。 主推进段用 while 扫 m_Ticks.nTicks,iPos 累加相邻 tick 的 time_msc 差,超过 200 毫秒才进内部节流判断。也就是说,回放速度事实上受 tick 密度约束:若某段 tick 间隔普遍小于 200ms,图表刷新会明显快于墙钟时间。 内部 while 卡着 iPos>200 做三件事:图表被关就退出、读全局变量、若 isPlay 为假直接 return true 交还控制权。这给了「小布盯盘」类工具在用户暂停时零成本挂起回放的可能,不必杀掉整个 EA。 外汇与贵金属回放属高风险验证环境,行情重建仅反映历史 tick 序列,不预示未来概率走向,实盘前务必在模拟盘跑通。
for(iPos=(iPos>class="num">0?iPos-class="num">1:class="num">0);(m_ReplayCount<iPos)&&(!_StopFlag);) CreateBarInReplay(class="kw">false); CustomTicksAdd(def_SymbolReplay,m_Ticks.Info,m_ReplayCount); Info.u_Value.df_Value=GlobalVariableGet(def_GlobalVariableReplay); Info.s_Infos.isWait=class="kw">false; GlobalVariableSet(def_GlobalVariableReplay,Info.u_Value.df_Value); } class="type">bool LoopEventOnTime(class="kw">const class="type">bool bViewBuider) { u_Interprocess Info; class="type">int iPos,iTest; if(!m_Infos.bInit) ViewInfos(); if(!m_Infos.bInit) { ChartSetInteger(m_IdReplay,CHART_SHOW_ASK_LINE,m_Ticks.ModePlot==PRICE_FOREX); ChartSetInteger(m_IdReplay,CHART_SHOW_BID_LINE,m_Ticks.ModePlot==PRICE_FOREX); ChartSetInteger(m_IdReplay,CHART_SHOW_LAST_LINE,m_Ticks.ModePlot==PRICE_EXCHANGE); m_Infos.PointsPerTick=SymbolInfoDouble(def_SymbolReplay,SYMBOL_TRADE_TICK_SIZE); m_MountBar.Rate[class="num">0].time=class="num">0; m_Infos.bInit=true; ChartRedraw(m_IdReplay); } iTest=class="num">0; class="kw">while((iTest==class="num">0)&&(!_StopFlag)) { iTest=(ChartSymbol(m_IdReplay)!=""?iTest:-class="num">1); iTest=(GlobalVariableGet(def_GlobalVariableReplay,Info.u_Value.df_Value)?iTest:-class="num">1); iTest=(iTest==class="num">0?(Info.s_Infos.isPlay?class="num">1:iTest):iTest); if(iTest==class="num">0) Sleep(class="num">100); } if((iTest<class="num">0)||(_StopFlag)) class="kw">return class="kw">false; AdjustPositionToReplay(bViewBuider); iPos=class="num">0; class="kw">while((m_ReplayCount<m_Ticks.nTicks)&&(!_StopFlag)) { iPos+=(class="type">int)(m_ReplayCount<(m_Ticks.nTicks-class="num">1)?m_Ticks.Info[m_ReplayCount+class="num">1].time_msc-m_Ticks.Info[m_ReplayCount].time_msc:class="num">0); CreateBarInReplay(true); class="kw">while((iPos>class="num">200)&&(!_StopFlag)) { if(ChartSymbol(m_IdReplay)=="") class="kw">return class="kw">false; GlobalVariableGet(def_GlobalVariableReplay,Info.u_Value.df_Value); if(!Info.s_Infos.isPlay) class="kw">return true;
「回放引擎里的报价注入与首棒对齐」
回放循环里先把进度条位置按总 tick 数换算成滑块偏移:iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks),再把打包好的 u_Value 写进全局变量 def_GlobalVariableReplay,随后 Sleep(195) 并让 iPos 减 200,形成约 200ms 一帧的回放节奏。 ViewInfos() 负责把行情画到离线图上。外汇符号开 ASK/BID 线、交易所符号开 LAST 线;用 SymbolInfoDouble 取 SYMBOL_TRADE_TICK_SIZE 作为每跳点位。若 M1 的 Rate[0].close 大于 0,就按模式填 tick 结构:外汇算 ask = close + spread * PointsPerTick,交易所直接取 last,再把 time_msc 乘 1000 对齐毫秒。 交易所模式下若首棒 volume_real 为 0 会被 for 循环跳过,直到遇到有成交的 tick 才作为 c0 基准。FirstBarNULL() 里 rate[0] 的 open/high/low/close 全部压成同一价,避免首根棒出现畸形高低点。外汇与贵金属回放属高风险仿真,参数不对可能画出失真 K 线。 下面这段是回放帧与视图刷新的核心片段,可直接粘进 MT5 调试:
Info.s_Infos.iPosShift = (class="type">class="kw">ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks); GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); Sleep(class="num">195); iPos -= class="num">200; } } class="kw">return (m_ReplayCount == m_Ticks.nTicks); } class="type">void ViewInfos(class="type">void) { class="type">MqlRates Rate[class="num">1]; ChartSetInteger(m_IdReplay, CHART_SHOW_ASK_LINE, m_Ticks.ModePlot == PRICE_FOREX); ChartSetInteger(m_IdReplay, CHART_SHOW_BID_LINE, m_Ticks.ModePlot == PRICE_FOREX); ChartSetInteger(m_IdReplay, CHART_SHOW_LAST_LINE, m_Ticks.ModePlot == PRICE_EXCHANGE); m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); m_MountBar.Rate[class="num">0].time = class="num">0; m_Infos.bInit = true; CopyRates(def_SymbolReplay, PERIOD_M1, class="num">0, class="num">1, Rate); if ((m_ReplayCount == class="num">0) && (m_Ticks.ModePlot == PRICE_EXCHANGE)) for (; m_Ticks.Info[m_ReplayCount].volume_real == class="num">0; m_ReplayCount++); if (Rate[class="num">0].close > class="num">0) { if (m_Ticks.ModePlot == PRICE_EXCHANGE) m_Infos.tick[class="num">0].last = Rate[class="num">0].close; else { m_Infos.tick[class="num">0].bid = Rate[class="num">0].close; m_Infos.tick[class="num">0].ask = Rate[class="num">0].close + (Rate[class="num">0].spread * m_Infos.PointsPerTick); } m_Infos.tick[class="num">0].time = Rate[class="num">0].time; m_Infos.tick[class="num">0].time_msc = Rate[class="num">0].time * class="num">1000; }else m_Infos.tick[class="num">0] = m_Ticks.Info[m_ReplayCount]; CustomTicksAdd(def_SymbolReplay, m_Infos.tick); ChartRedraw(m_IdReplay); } class="kw">inline class="type">void FirstBarNULL(class="type">void) { class="type">MqlRates rate[class="num">1]; class="type">int c0 = class="num">0; for(; (m_Ticks.ModePlot == PRICE_EXCHANGE) && (m_Ticks.Info[c0].volume_real == class="num">0); c0++); rate[class="num">0].close = (m_Ticks.ModePlot == PRICE_EXCHANGE ? m_Ticks.Info[c0].last : m_Ticks.Info[c0].bid); rate[class="num">0].open = rate[class="num">0].high = rate[class="num">0].low = rate[class="num">0].close;
把昨日零点塞回重放序列
在回放引擎里,若要把一段历史行情重新喂给指标计算,得先伪造一根‘昨日此时’的柱。上面这段代码把当前 tick 对应时间减去 86400 秒(刚好一天),再去掉秒级余数,就得到昨天的零点时间戳。 tick_volume 与 real_volume 都置 0,是因为这根柱只是占位,不携带真实成交量;真正的数据靠后续 tick 逐步追加。CustomRatesUpdate 用 def_SymbolReplay 这个重放品种名把伪造 rate 推入,MT5 会把它当作一根已完成的历史柱处理。 m_ReplayCount 归零意味着重放计数器复位,下一根 tick 将从头累计。你在自写回测器时,若发现指标在跨日瞬间跳变,多半是这类占位柱的时间戳没对齐——直接开 MT5 用 Print(rate[0].time) 核对即可。外汇与贵金属回放含滑点及停牌风险,占位逻辑仅影响本地计算,不预示实盘走向。
rate[class="num">0].tick_volume = class="num">0; rate[class="num">0].real_volume = class="num">0; rate[class="num">0].time = macroRemoveSec(m_Ticks.Info[c0].time) - class="num">86400; CustomRatesUpdate(def_SymbolReplay, rate); m_ReplayCount = class="num">0; }
◍ 回放系统当前能跑什么
随文附带的 Market_Replay_-_25.zip(49.54 KB)已经能让你在 MT5 里直接跑通回放/模拟的基础链路,BOLSA、FOREX、FUTUROS 三组历史数据压缩包分别为 1358.24 KB、3743.96 KB 和 11397.51 KB,覆盖不同品种的样本量。系统骨架算立住了,但部分接口函数还没接进训练流程,想拿它做高效复盘还得自己动参数。 有读者在讨论里问过:能不能让重放服务每推一个 tick 就触发 EA 的 OnTick,并卡住后续画线等回调跑完,方便调试。作者给的答复是——能,但只差 EA 里加一行,只是多数人还没读到那篇。这行代码后续文章会高频出现,现在先吃透已发布的回放机制最划算。 外汇和贵金属模拟自带高杠杆与滑点风险,拿回放验证策略也只是概率层面的参考,别把历史重放当成实盘保命符。等下一篇讲外汇 Last 价模拟分离时,C_Replay 类里时间随机化的逻辑不用改,一种模式调优另一种跟着受益。