开发回放系统(第31部分):EA交易项目C_Mouse类(五)·综合运用
「回放与实盘下的K线时间对齐逻辑」
这段分支处理把行情状态映射到状态栏文本:收盘市场直接标 Closed Market,竞价阶段标 Auction,其余未知状态落 ERROR。真正有复用价值的是回放(eInReplay)与实盘(eInTrading)共用的取时分支——它先拿当前棒时间,再判断是否小于 ULONG_MAX,合法才格式化成秒级字符串。
GetBarTime() 的核心在回放判定:当 m_Info.Status == eInReplay 时,从全局变量 def_GlobalVariableServerTime 取服务端时间,取不到或值为 ULONG_MAX 就直接返回 ULONG_MAX 退出;实盘则走 TimeCurrent()。
对齐算法用 PeriodSeconds() 拿到周期秒数 i0,把当前时间按 i0 向下取整后加 i0,得到下一根棒的开盘时间;函数最终返回 m_Info.Rate.time - dt,即距下一棒生成的剩余秒数。外汇与贵金属市场高波动、跳空频繁,回放与实盘时间基准不一致可能导致指示器显示偏差,建议在 MT5 策略测试器分别跑回放与实时确认该差值。
case eCloseMarket: m_Info.szInfo = "Closed Market"; class="kw">break; case eInReplay : case eInTrading : dt = GetBarTime(); if (dt < ULONG_MAX) { m_Info.szInfo = TimeToString(dt, TIME_SECONDS); class="kw">break; } case eAuction : m_Info.szInfo = "Auction"; 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; class="type">int i0 = PeriodSeconds(); if (m_Info.Status == eInReplay) { if (!GlobalVariableGet(def_GlobalVariableServerTime, info.u_Value.df_Value)) class="kw">return ULONG_MAX; dt = info.ServerTime; if (dt == ULONG_MAX) class="kw">return ULONG_MAX; }else dt = TimeCurrent(); if (m_Info.Rate.time <= dt) m_Info.Rate.time = (class="type">class="kw">datetime)(((class="type">class="kw">ulong) dt / i0) * i0)) + i0; class="kw">return m_Info.Rate.time - dt; }
让回放引擎认出竞价呼叫
改造 C_Replay 类的第一步,是删掉原来向图表写状态的一行,换成发送竞拍消息的调用。服务一启动、资产在图表上跑起来,就能看到通信正常的提示,这部分改动很轻。 真正的麻烦在运行时识别资产进入拍卖。拍卖触发原因很多——大单异动、订单簿被清空都可能是诱因,但原因不重要,关键是拍卖持续的最短时长。若真实市场里的拍卖短于 1 分钟,回放系统按柱线最小时间粒度根本捕捉不到。 回放端能用的最直白机制,就是比对相邻两个分时(tick 或柱)的时间差。差值超过 60 秒,即大于最短柱创建时间,就向用户报“拍卖呼叫”,并暂停该资产回放;差值 ≤60 秒才视作活跃,按原逻辑起计时器。 下面这段 ViewReplay 里把 ServerTime 赋值从宏常量改成了取当前回放 tick 的时间,就是为后续按时间差判拍卖铺路。外汇与贵金属模拟回放属高风险验证环境,实盘前务必在 MT5 策略测试器跑通再上。
class="type">bool ViewReplay(ENUM_TIMEFRAMES arg1) { class="macro">#define macroError(A) { Print(A); class="kw">return class="kw">false; } u_Interprocess info; if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == class="num">0) macroError("Asset configuration is not complete, it remains to declare the size of the ticket."); if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == class="num">0) macroError("Asset configuration is not complete, need to declare the ticket value."); if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == class="num">0) macroError("Asset configuration not complete, need to declare the minimum volume."); if (m_IdReplay == -class="num">1) class="kw">return class="kw">false; if ((m_IdReplay = ChartFirst()) > class="num">0) do { if (ChartSymbol(m_IdReplay) == def_SymbolReplay) { ChartClose(m_IdReplay); ChartRedraw(); } }class="kw">while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0); Print("Waiting for [Market Replay] indicator permission to start replay ..."); info.ServerTime = ULONG_MAX; info.ServerTime = m_Ticks.Info[m_ReplayCount].time; CreateGlobalVariable(def_GlobalVariableServerTime, info.u_Value.df_Value); info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1); ChartApplyTemplate(m_IdReplay, "Market Replay.tpl"); CreateGlobalVariable(def_GlobalVariableIdGraphics, info.u_Value.df_Value); class="kw">while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(class="num">750); class="kw">return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != "")); class="macro">#undef macroError } 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)) {
◍ 回放循环的节流与进度同步
这段回放核心逻辑在 while 循环里跑:只要当前 tick 序号小于总 tick 数且未被停止标志打断,就持续向回放图表注入 K 线。每轮先按相邻 tick 的 time_msc 差值累加位置偏移量 iPos,再调用 CreateBarInReplay(true) 生成_bar。 内部还有一层节流:当 iPos 超过 200 毫秒当量时,进入子循环做进度同步。它每轮 Sleep(195) 并扣减 200,意味着回放节奏被人为限制在约 200ms 一步,避免 CPU 空转。同时把 m_ReplayCount 映射到 def_MaxPosSlider 范围的 ushort 进度写进全局变量,供外部滑动条读取。 iCount 超过 4 后才会刷新一次服务端时间全局变量:若相邻 tick 间隔大于 60 秒,直接把 ServerTime 置为 ULONG_MAX 触发断层标记;否则按 1 秒步进逼近当前 tick 时间。外汇与贵金属回测中这种断层处理能避免跨周期时间错乱,但实盘模拟仍属高风险环境,参数须自行验证。 循环退出条件很硬:图表句柄失效、全局回放变量读取失败、或外部按下停止,都会让函数返回 false。正常跑完所有 tick 则返回 m_ReplayCount == m_Ticks.nTicks 的布尔值,你可以直接把这段丢进 MT5 的 EA 回放模块里改 Sleep 值观察卡顿变化。
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); if ((m_Ticks.Info[m_ReplayCount].time - m_Ticks.Info[m_ReplayCount - class="num">1].time) > class="num">60) Info.ServerTime = ULONG_MAX; else { 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="kw">return (m_ReplayCount == m_Ticks.nTicks);
「把工具请下神坛」
给 MT5 的 EA 加一个「新 K 线计时器」,本质不是秀技巧,而是逼自己写出在实盘和回放里行为一致的逻辑。许多人只在 OnTick 里判断时间跳变,遇到离线重算或回放重播就会漏信号;用独立定时器或结构化的柱体追踪,才能覆盖外汇与贵金属这种高波动、高滑点环境里的各种边界。 我们前面拆的那套方案,重点从来不是某段代码多优雅,而是它能在 BOLSA、FOREX、FUTUROS 几份压缩包(合计约 16.9 MB 历史数据)里直接跑通。你下完 ZIP 丢进 tester,把计时器开关翻一遍,就知道哪些品种对重连更敏感。 工具只是拐杖。真要把回放系统用熟,得自己改参数、看偏差、再回来修模型——外汇和贵金属杠杆风险不小,任何「看起来稳」的模块都得你亲自证伪过才作数。