开发回放系统(第31部分):EA交易项目C_Mouse类(五)·综合运用
📘

开发回放系统(第31部分):EA交易项目C_Mouse类(五)·综合运用

第 3/3 篇

「回放与实盘下的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 策略测试器分别跑回放与实时确认该差值。

MQL5 / C++
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 策略测试器跑通再上。

MQL5 / C++
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 值观察卡顿变化。

MQL5 / C++
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,把计时器开关翻一遍,就知道哪些品种对重连更敏感。 工具只是拐杖。真要把回放系统用熟,得自己改参数、看偏差、再回来修模型——外汇和贵金属杠杆风险不小,任何「看起来稳」的模块都得你亲自证伪过才作数。

常见问题

先确认回放引擎注入的柱体时间与经纪商时区一致,再检查EA是否用了本地时间而非服务器时间,统一用TimeCurrent()取服务器时间即可避免错位。
在回放数据生成时给竞价时段打独立标记,EA端用C_Mouse类监听该标记触发对应逻辑,否则回放会把它当普通休市跳过。
可以。小布盯盘能直接加载回放与实盘同源EA的差异日志,自动标出时间对齐异常和节流丢帧,你只需看结论不用自己跑脚本。
给回放循环加固定步长休眠(如Sleep(10))做节流,并把进度按已处理Tick数除总数来刷新,别每Tick都重绘界面。
不是。它主要解决回放环境缺鼠标/事件模拟的问题,实盘有真实终端事件,投产前把相关依赖剥掉更稳。