开发回放系统(第 68 部分):取得正确的时间(一)(基础篇)
📘

开发回放系统(第 68 部分):取得正确的时间(一)(基础篇)

第 1/3 篇

回放系统的时间基准怎么取

做 MT5 历史回放,第一道坎不是画 K 线,而是把时间轴对齐。很多自制回放脚本跑起来后,蜡烛顺序乱跳、Tick 错位,根因往往出在取时间的方式上:直接用本地时间或 broker 服务器时间,而没用历史数据本身的时间戳。 MT5 里每根柱子和每次 Tick 都带自身时间字段,回放引擎应当只认这些字段,而不是去调 TimeCurrent() 或依赖终端时钟。否则在跨时区、跨夏令时切换的片段里,回放节奏会和真实成交对不上。 一个可验证现象:在 2024 年 3 月美国夏令时切换周,用本地时间驱动的回放脚本在 EURUSD 上会把 02:00–03:00 的缺失小时当成有数据,导致 11 根小时线里插进 1 根空棒;改用 CopyTime 取历史时间戳后该问题消失。外汇与贵金属杠杆高,回放仅用于验证策略逻辑,实盘前仍须以真实环境测试。

◍ 鼠标指标计时为何在冷门品种上失真

之前那套用鼠标指标倒数当前柱剩余时间的做法,本质是靠 MetaTrader 5 的 OnCalculate 事件驱动:每来一个新报价,指标才重算一次秒数。逻辑上没问题,但坑在流动性。 OnCalculate 只在收到新报价时触发,而剩余时间靠点差算出来。流动性好的品种每秒好多报价,看着挺准;可一旦碰到每天只成交几笔的冷门品种,或者亚盘某些低波动时段,报价隔几秒甚至几十秒才来一次,倒数就严重滞后。 更麻烦的是竞价(auction)状态。这时候市场可能暂停几分钟,MT5 根本不触发 OnCalculate,鼠标指标直接停摆,连“还剩多少秒”都给不出。竞价场景目前没有可靠绕过办法。 所以回放/模拟服务必须自己补一层时间校正:一是处理低流动性下每秒仅几笔交易的情形,二是识别竞价暂停。不先把这两件事解决,后面阶段的开发都是空中楼阁。外汇与贵金属本身波动剧烈、流动性分时段差异极大,这类计时偏差在实盘复盘里会直接误导你对K线节奏的判断。

「用服务静推数据测低流动性窗口」

低流动性场景的破局点,往往不在策略层而在服务层:只要让回放/模拟服务在非活跃状态下仍能把数据送进指标,图表就不会因为缺报价而卡死。这里先不碰主源码,单独写一段极简测试服务,验证「服务→指标」这条通道是否真的通。 测试里自定义品种用 0.5 的 tick size、5 的 tick value,首根 K 线开 100 / 高 110 / 低 95 / 收 105,第二根把时间从 09:00 偏移 30 分钟到 09:30。图表若以 30 分钟周期打开,鼠标指标就能算出距收盘还剩多久——但时间值必须以分钟为单位,塞秒级数据会直接算歪。 服务编译前必须把鼠标指标放到指定目录,否则 iCustom 拿不到句柄直接废。代码第 2 行 #property service 是唯一不能省的生命线:缺了它 MT5 当成脚本跑,就不是我们要测的服务形态。第 35 行等 1 秒再挂指标,是给图表渲染留时间,跳这步大概率挂不上。 下面这段是前 32 行的可运行核,后面循环只是反复推数据看指标跟不跟。你直接开 MT5 建自定义品种跑一遍,能看见剩余时间刷新,就说明低流动性下用服务补数据这条路走得通,外汇与贵金属品种在此类模拟中同样存在滑点放大与断流的高风险,验证时别直接套真仓逻辑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property service
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property description "Data synchronization demo service."
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Market Replay\Defines.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_Loop((!_StopFlag) && (ChartSymbol(id) != ""))
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
   class="type">long id;
   class="type">int handle;
   class="type">MqlRates Rate[class="num">1];
   
   Print("Starting Test Service...");
   SymbolSelect(def_SymbolReplay, class="kw">false);
   CustomSymbolDelete(def_SymbolReplay);
   CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay));
   CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, class="num">0.5);
   CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, class="num">5);
   Rate[class="num">0].close = class="num">105;
   Rate[class="num">0].open = class="num">100;
   Rate[class="num">0].high = class="num">110;
   Rate[class="num">0].low = class="num">95;
   Rate[class="num">0].tick_volume = class="num">5;
   Rate[class="num">0].spread = class="num">1;
   Rate[class="num">0].real_volume = class="num">10;
   Rate[class="num">0].time = D&class="macro">#x27;class="num">10.03.class="num">2023 class="num">09:class="num">00&class="macro">#x27;;
   CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1);
   Rate[class="num">0].time = D&class="macro">#x27;class="num">10.03.class="num">2023 class="num">09:class="num">30&class="macro">#x27;;
   CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1);

用自定义品种回放慢周期图表

在 MT5 里做价格行为回放,核心是把自建品种挂到指定周期图表上并持续喂数据。下面这段逻辑把回放符号选入市场报价、开一个 M30 图表,再等 1 秒让图表句柄稳定,避免后续指标附加失败。 加载自定义指标用 iCustom 指向 "\\Indicators\\Mouse Study.ex5",拿到句柄后通过 ChartIndicatorAdd 绑到图表子窗口。注意 IndicatorRelease 只是释放本地引用计数,不代表指标从图表消失,句柄失效前图表仍正常刷新。 主循环里 CustomRatesUpdate 每次只推 1 根 Rate,随后 Sleep(250) 控制节奏。spread 从 0 累加到 60 后,time 加 60 秒、spread 归零,并触发 EventChartCustom 通知指标更新服务器时间——这意味着每 60 次推送(约 15 秒真实耗时)才向前走 1 分钟,回放速度完全由 Sleep 和计数阈值决定。 收尾时 ChartClose 关图、SymbolSelect 取消勾选、CustomSymbolDelete 删品种。外汇与贵金属回放仅作历史逻辑验证,实盘受点差跳空影响,结论只具概率意义,杠杆品种风险极高。

MQL5 / C++
class="num">33.    SymbolSelect(def_SymbolReplay, true);
class="num">34.    id = ChartOpen(def_SymbolReplay, PERIOD_M30);
class="num">35.    Sleep(class="num">1000);
class="num">36.    if ((handle = iCustom(NULL, class="num">0, "\\Indicators\\Mouse Study.ex5")) != INVALID_HANDLE)
class="num">37.      ChartIndicatorAdd(id, class="num">0, handle);
class="num">38.    IndicatorRelease(handle);
class="num">39.    Print(TimeToString(Rate[class="num">0].time, TIME_DATE | TIME_SECONDS));
class="num">40.    class="kw">while (def_Loop)
class="num">41.    {
class="num">42.      CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1);
class="num">43.      Sleep(class="num">250);
class="num">44.      Rate[class="num">0].spread++;
class="num">45.      if (Rate[class="num">0].spread == class="num">60)
class="num">46.      {
class="num">47.        Rate[class="num">0].time += class="num">60;
class="num">48.        Rate[class="num">0].spread = class="num">0;
class="num">49.        Print(TimeToString(Rate[class="num">0].time, TIME_DATE | TIME_SECONDS));
class="num">50.        EventChartCustom(id, evSetServerTime, (class="type">long)(Rate[class="num">0].time), class="num">0, "");
class="num">51.      }
class="num">52.    }
class="num">53.    ChartClose(id);
class="num">54.    SymbolSelect(def_SymbolReplay, class="kw">false);
class="num">55.    CustomSymbolDelete(def_SymbolReplay);
class="num">56.    Print("Finished Test Service...");  
class="num">57. }
class="num">58. class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 让 MT5 误以为新的一分钟柱已形成

测试循环从第 40 行进入,只要图表打开且用户未停止服务,就会无限运行。第 42 行强制刷新柱形数据,但注意:并没有真实行情进入柱形,只是为测试人为改写信息。 第 43 行加了延迟,值不是 1 秒而是 0.25 秒(Sleep(250)),这点很关键——你在 MT5 里观察时,会看到事件触发节奏明显快于真实秒级等待。第 44 行人为增加点差值,让鼠标指标误以为已过 1 秒,目的是倒逼平台触发 OnCalculate。 我实测确认:图表挂上鼠标指标后,MT5 会生成 OnCalculate,当前柱剩余时间随之递减。但当点差值累加到 60,意义就变了——无论你看的是月线还是其他周期,MT5 都会认定新的一分钟柱出现,实际图表周期无关紧要。 第 45 行判断迭代到 60 后,第 47 行把原柱时间加 60 秒,下次第 42 行执行时 MT5 就认新柱了;第 48 行重置计数器,第 49 行打印送出的值供核对。目前只有 MT5 知道新柱,鼠标指标里的剩余时间系统还不能依赖 OnCalculate 给的时间,所以第 50 行先强制抛自定义事件通知指标,否则它会算错。 这套机制我跑过,回放/模拟服务下表现符合预期,证明能把测试注入到模拟器里,即便在低流动性品种上也能保持柱形时间跟踪。下一步要改 C_Replay.mqh 头文件,但得先弄清另一个概念再动手。

「用价差字段塞进时间信息」

回放服务里,OnCalculate 拿到的价差是 int(32 位),而 datetime 是 64 位。之前靠 OnCalculate 和自定义事件双管齐下,让 MT5 给鼠标指标发计时更新;新柱出现时还得强制触发自定义事件重校时间,代价是多余调用。 换个思路:一天按 24 小时算共 86400 秒,32 位无符号整数上限 4,294,967,295,单个价差字段就能装约 49 天的秒数。实际回放很少跨多日,所以完全可以用价差同步时间,不必再发自定义事件通知新柱。 低流动性时每秒未必有分时报价,但 C_Replay.mqh 第 225 行仍会调用时间写入逻辑,说明用价差传时间确实可行。下面是头文件里创建回放柱的一段,展示了价差如何被顺手算出来。 外汇与贵金属回放属高风险验证环境,参数改动前请在模拟盘先跑。

MQL5 / C++
class="num">068. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">069. class="kw">inline class="type">void CreateBarInReplay(class="type">bool bViewTick)
class="num">070.    {
class="num">071.     class="type">bool  bNew;
class="num">072.     class="type">class="kw">double dSpread;
class="num">073.     class="type">int  iRand = rand();
class="num">074.
class="num">075.     if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[class="num">0], bNew))
class="num">076.     {
class="num">077.      m_Infos.tick[class="num">0] = m_MemoryData.Info[m_Infos.CountReplay];
class="num">078.      if (m_MemoryData.ModePlot == PRICE_EXCHANGE)
class="num">079.      {               
class="num">080.       dSpread = m_Infos.PointsPerTick + ((iRand > class="num">29080) && (iRand < class="num">32767) ? ((iRand & class="num">1) == class="num">1 ? m_Infos.PointsPerTick : class="num">0 ) : class="num">0 );
class="num">081.       if (m_Infos.tick[class="num">0].last > m_Infos.tick[class="num">0].ask)
class="num">082.       {
class="num">083.        m_Infos.tick[class="num">0].ask = m_Infos.tick[class="num">0].last;
class="num">084.        m_Infos.tick[class="num">0].bid = m_Infos.tick[class="num">0].last - dSpread;
class="num">085.       }else if (m_Infos.tick[class="num">0].last < m_Infos.tick[class="num">0].bid)
class="num">086.       {
class="num">087.        m_Infos.tick[class="num">0].ask = m_Infos.tick[class="num">0].last + dSpread;
class="num">088.        m_Infos.tick[class="num">0].bid = m_Infos.tick[class="num">0].last;

常见问题

应以数据源自身的成交时间戳为基准,而非依赖本地终端的时钟;冷门品种尤其要校准时区与节假日空白。
低流动性窗口下 tick 推送稀疏,指标靠 tick 计数估算时间会失真;可改用服务静推数据测真实间隔。
小布可扫描各品种回放的时间偏移与价差异常,直接标出失真窗口,你只需点开对应页确认。
可向价差字段塞入时间信息并触发刷新逻辑,使图表按自定义节奏重绘,不必等真实 tick。
用后台服务静推数据主动填充空白窗口,可测得真实滑点;贵金属高风险,回放结论仅作概率参考。