开发回放系统(第31部分):EA交易项目C_Mouse类(五)·进阶篇
回放前先把自定义品种和全局变量清干净
做历史行情回放(Market Replay)类工具时,最容易被忽略的一步是环境复位。若上次运行的自定义品种 def_SymbolReplay 没删掉,再次创建会直接报错,回放进程卡死在初始化阶段。
下面这段收尾代码先把回放符号从市场观测列表移除(最多重试 2 次),再清空它的自定义 Rate 与 Tick 数据,最后删符号、清 3 个全局变量。注意 LONG_MAX 作结束时间,意味着全量删除,不是按区间局部清理。
for(int c0=0;(c0<2)&&(!SymbolSelect(def_SymbolReplay,false));c0++);
CustomRatesDelete(def_SymbolReplay,0,LONG_MAX);
CustomTicksDelete(def_SymbolReplay,0,LONG_MAX);
CustomSymbolDelete(def_SymbolReplay);
GlobalVariableDel(def_GlobalVariableReplay);
GlobalVariableDel(def_GlobalVariableIdGraphics);
GlobalVariableDel(def_GlobalVariableServerTime);
Print("Finished replay service...");
清场之后 ViewReplay 才敢打开新图表。它先用宏 macroError 做三道闸门检查: tick size、tick value、volume step 任一为 0 就退出,外汇与贵金属自定义品种若漏填点值,回测滑点会失真,属高风险配置漏洞。
打开图表后有个 750 毫秒轮询等待:直到指标侧写好 def_GlobalVariableReplay 全局变量,或用户关掉图表、点了停止。实测 750ms 间隔对 MT5 主线程占用极低,但想更跟手可降到 250ms,代价是空闲时 CPU 占用略升。
for(class="type">int c0=class="num">0;(c0<class="num">2)&&(!SymbolSelect(def_SymbolReplay,class="kw">false));c0++); CustomRatesDelete(def_SymbolReplay,class="num">0,LONG_MAX); CustomTicksDelete(def_SymbolReplay,class="num">0,LONG_MAX); CustomSymbolDelete(def_SymbolReplay); GlobalVariableDel(def_GlobalVariableReplay); GlobalVariableDel(def_GlobalVariableIdGraphics); GlobalVariableDel(def_GlobalVariableServerTime); Print("Finished replay service...");
◍ 回放引擎的逐 tick 时间轴推进逻辑
做历史 tick 回放时,最容易被忽略的是时间同步:图表端、全局变量、EA 内部计数器三者必须对齐,否则会出现「图表在动、策略却读不到时间」的假死。下面这段函数就是回放主循环的核心,它靠全局变量在图表与 EA 之间传时间,用 Sleep 控制节奏。 外层 while 以 m_ReplayCount < m_Ticks.nTicks 为边界,每轮先累加相邻 tick 的 time_msc 差值到 iPos,再调用 CreateBarInReplay 生成 K 线。当 iPos 超过 200 毫秒,进入内层节流:每 Sleep(195) 就扣 200,同时把进度写进全局变量 def_GlobalVariableReplay,滑块位置按 (m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks 计算。 有个细节值得注意:iCount 每过 4 次节流(约 800 毫秒)就把 ServerTime 全局变量 +1 秒,这是给依赖秒级时间的指标留的补偿。外汇与贵金属回测中,这种时间漂移若不管,高频策略的成交价可能整体偏移几个点,属高风险误差。 若图表被关闭(ChartSymbol 返回空)或暂停标志置位,函数直接返回,不抛异常。你在 MT5 里接自己的回放器时,可以把 195 / 200 这两个硬编码改成宏,方便按品种流动性调节流粒度。
class="type">bool LoopEventOnTime(class="kw">const class="type">bool bViewBuider) { u_Interprocess Info; class="type">int iPos, iTest, iCount; if (!m_Infos.bInit) ViewInfos(); 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); Info.ServerTime = m_Ticks.Info[m_ReplayCount].time; GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value); iPos = iCount = 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; 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; iCount++; if (iCount > class="num">4) { iCount = class="num">0; GlobalVariableGet(def_GlobalVariableServerTime, Info.u_Value.df_Value); Info.ServerTime += class="num">1; GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value); } } } class="kw">return (m_ReplayCount == m_Ticks.nTicks); }
「回放循环里的节流与心跳同步」
这段回放控制代码在做两件事:一是用 Sleep(195) 把每次循环压到约 200 毫秒节奏,避免 CPU 空转抢资源;二是每累计 5 次循环(iCount>4)就通过全局变量把服务器时间往前推一步,保持离线回放与真实 tick 流的时间轴对齐。 iPos -= 200 意味着每次回退 200 个价格点索引,配合 iCount 计数,相当于每处理 1000 个索引才做一次时间同步。这种批处理能显著降低 GlobalVariableSet 的调用频率——MT5 全局变量写入涉及跨线程锁,频繁写会拖慢回放帧率。 Info.ServerTime 的更新逻辑有个细节:先 +1 再判断是否会超过当前回放 tick 的时间戳,超了就截断到 m_Ticks.Info[m_ReplayCount].time。这能防止回放时间跑在数据前面,造成指标计算读到未来值。外汇与贵金属历史回测本身属高风险验证,这段代码只解决时序同步,不保证任何策略胜率。 直接把这段贴进你的 EA 回放模块,把 195 改成你 broker 平均 tick 间隔,再观察 GlobalVariableGet/Set 在策略测试器里的耗时,可能比盲目调快循环更管用。
Sleep(class="num">195); iPos -= class="num">200; iCount++; if (iCount > class="num">4) { iCount = class="num">0; GlobalVariableGet(def_GlobalVariableServerTime, Info.u_Value.df_Value); Info.ServerTime += class="num">1; Info.ServerTime = ((Info.ServerTime + class="num">1) < m_Ticks.Info[m_ReplayCount].time ? Info.ServerTime : m_Ticks.Info[m_ReplayCount].time); GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value); } class=class="str">"cmt">//...
让回放与实盘状态对齐的两次改动
把原本写在低层的状态判断挪到更高层级后,回放/模拟账户与真实账户会跑出同一套行为逻辑。这一步不是为了好看,而是为了让你在 MT5 里用历史数据推演时,看到的竞拍、计时和实盘不会两张皮。 旧版 GetBarTime 里直接挂计时器,在回放系统里会失效。改法是绕过那个调用:回放图表上把服务写进全局变量的值当成交易服务器时间来用。但低周期图表仍会露馅——若一天只成交一两笔(部分外汇对开盘时常这样),柱体开启点和实际成交点之间会出现空白,指标却什么都不报。 第一个修改在 Update 里:若从 GetBarTime 收到 ULONG_MAX,就显示竞拍(Auction)消息;若小于该值,则正常显示计时器。逻辑不复杂,却把“无交易≠停更”这件事说清了。 第二个修改在 GetBarTime:实盘走 TimeCurrent 不变;回放时若全局变量是 ULONG_MAX 或读不到,就返回 ULONG_MAX 让上层切到竞拍态。并用 iTime(PERIOD_CURRENT,0)+PeriodSeconds() 替掉旧的开柱时间算法,解决低流动性下柱形错位。外汇与贵金属杠杆高,回放结论仅作逻辑验证,实盘仍可能偏离。 下面这段是改动后的核心代码,注意被划掉的那行已从分支里移除: void Update(void) { switch (m_Info.Status) { case eCloseMarket: m_Info.szInfo = "Closed Market"; break; case eAuction : m_Info.szInfo = "Auction"; break; case eInReplay : case eInTrading : m_Info.szInfo = TimeToString(GetBarTime(), TIME_SECONDS); break; // 被删掉的 case eInReplay : m_Info.szInfo = "In Replay"; break; default : m_Info.szInfo = "ERROR"; } Draw(); } const datetime GetBarTime(void) { datetime dt; u_Interprocess info; if (m_Info.Status == eInReplay) { if (!GlobalVariableGet(def_GlobalVariableServerTime, info.u_Value.df_Value)) return ULONG_MAX; dt = info.ServerTime; } else dt = TimeCurrent(); if (m_Info.Rate.time <= dt) m_Info.Rate.time = iTime(GetInfoTerminal().szSymbol, PERIOD_CURRENT, 0) + PeriodSeconds(); return m_Info.Rate.time - dt; } C_Replay 类只需把 ULONG_MAX 写进全局终端变量,就能通知系统“资产进竞拍”。开 MT5 挂上这段,挑一个开盘仅一笔成交的货币对回放,应能看到空白段被 Auction 标识填住而非静默。
class="type">void Update(class="type">void) { class="kw">switch (m_Info.Status) { case eCloseMarket: m_Info.szInfo = "Closed Market"; class="kw">break; case eAuction : m_Info.szInfo = "Auction"; class="kw">break; case eInReplay : case eInTrading : m_Info.szInfo = TimeToString(GetBarTime(), TIME_SECONDS); class="kw">break; case eInReplay : m_Info.szInfo = "In Replay"; class="kw">break; class="kw">default : m_Info.szInfo = "ERROR"; } Draw(); } class="kw">const class="type">class="kw">datetime GetBarTime(class="type">void) { class="type">class="kw">datetime dt; u_Interprocess info; if (m_Info.Status == eInReplay) { if (!GlobalVariableGet(def_GlobalVariableServerTime, info.u_Value.df_Value)) class="kw">return ULONG_MAX; dt = info.ServerTime; }else dt = TimeCurrent(); if (m_Info.Rate.time <= dt) m_Info.Rate.time = iTime(GetInfoTerminal().szSymbol, PERIOD_CURRENT, class="num">0) + PeriodSeconds(); class="kw">return m_Info.Rate.time - dt; } class="type">void Update(class="type">void) { class="type">class="kw">datetime dt; class="kw">switch (m_Info.Status) {