开发回放系统(第 58 部分):重返服务工作·进阶篇
📘

开发回放系统(第 58 部分):重返服务工作·进阶篇

第 2/3 篇

回放引擎的私有结构与首根空 bar 注入

这段私有段定义了回放系统的核心枚举与内存指针。eWhatExec 区分了四种执行模式:tick 重放、bar 转 tick、tick 转 bar、以及前一根 bar 引用;eTranscriptionDefine 则标记当前字符串是信息行还是定义行。 st001 结构体把四个 C_Array 指针收进 m_GlPrivate,分别承载不同粒度的数据序列,外加一个 Line 整型记录当前行号。外围的 m_szPath、m_AccountHedging、m_dtPrevLoading 等变量,控制文件路径、账户对冲模式与上次加载时间,这些都是实盘回放不可省略的状态。 FirstBarNULL 是冷启动关键:当交易所模式遇到 volume_real 为 0 的脏 tick,会跳过前置零量 tick,取首个有效 last 或 bid 价作为 close,并令 open/high/low 全部等于 close。 它把 time 字段减掉 86400 秒(正好一天)后写入 CustomRatesUpdate,相当于在重放符号上人为插了一根零量的昨日空 bar。m_ReplayCount 归零,意味着计数从这根占位 bar 之后才开始。外汇与贵金属回放含高杠杆风险,这种预处理只影响本地重放精度,不预示任何方向。

MQL5 / C++
class="kw">private :
   enum eWhatExec {eTickReplay, eBarToTick, eTickToBar, eBarPrev};
   enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE};
   class="kw">struct st001
   {
      C_Array *pTicksToReplay, *pBarsToTicks, *pTicksToBars, *pBarsToPrev;
      class="type">int      Line;
   }m_GlPrivate;
   class="type">class="kw">string   m_szPath;
   class="type">bool     m_AccountHedging;
   class="type">class="kw">datetime m_dtPrevLoading;
   class="type">int      m_ReplayCount,
            m_ModelLoading;
class=class="str">"cmt">//+------------------------------------------------------------------+
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;
             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;
            }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline eTranscriptionDefine GetDefinition(class="kw">const class="type">class="kw">string &In, class="type">class="kw">string &Out)

「C_Replay 重构:异步模板引发的图表竞态」

这一版 C_Replay 不是推倒重来,而是把回放服务拆进 C_ConfigService 的公共继承里(第13行),让数据封装和负载分担更干净。私有段从第15行拉到第58行,其中第21行把定位状态声明成结构体,意味着控制指标要嵌套携带额外状态,不再靠全局终端变量硬传。 第23行那个只干一件事的小函数——打印错误并返回 false——看着多余,实则消掉了每处报错都要写两行的冗余。看第79到80行就能明白:条件不满足时一行搞定报错加退出,不用再堆重复代码。 真正容易踩坑的是模板加载的异步性。第87行 ChartApplyTemplate 不会立刻把模板贴上图表,MT5 可能在几毫秒后才真正执行。服务代码第31行刚开图、第32行就要加控件,如果不在 C_Replay 第100行主动 Sleep 一小段等图表稳定,图表往往会在打开后瞬间被关掉。 实测里,鼠标指标必须等模板异步落定后,在第101行做存在性校验;循环一直卡到指标出现或用户关图。第105行再加控制指标时,若它已被打进模板反而会拒绝加载——所以后续改模板要刻意把控制指标剔出去。外汇与贵金属回放涉及实时行情重演,杠杆与滑点风险极高,任何图表初始化异常都可能让模拟脱离真实市况。 下面这段是 C_Replay.mqh 修订版头部,能直接看出资源内嵌和继承关系: 第002行 #property copyright 只是署名元数据,不影响逻辑; 第004行 #include 把 C_ConfigService 头文件拉进来,复用上节封装; 第006行 #define 把控制指标编译文件路径定为 Indicators\\Market Replay.ex5; 第007行 #resource 把上述 ex5 编进服务可执行体,省去手动拷文件; 第009行 #define 宏定义服务循环继续条件:未收到停止旗标且回放图表的交易品种非空; 第011行 #define 给控制指标短名 Market Replay Control,供后续按名检索; 第013行 class C_Replay : public C_ConfigService 确立公共继承,工作负载分流。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#include "C_ConfigService.mqh"
class="macro">#define def_IndicatorControl   "Indicators\\Market Replay.ex5"
class="macro">#resource "\\" + def_IndicatorControl
class="macro">#define def_CheckLoopService((!_StopFlag) && (ChartSymbol(m_IdReplay) != ""))
class="macro">#define def_ShortNameIndControl "Market Replay Control"
class C_Replay : class="kw">public C_ConfigService
{
class="kw">private  :
   class="type">long      m_IdReplay;
   class="kw">struct st00
   {
      class="type">class="kw">ushort Position;

◍ 用联合体在指标与控制结构间搬数据

这段逻辑解决一个很实际的问题:回放引擎的主图指标需要把「持仓方向」和「运行模式」两个 short 值实时回传给 EA 的控制结构 m_IndControl,但 ChartIndicatorGet 只能拿到 double 缓冲。作者用了一个 uCast_Double 联合体,把 8 字节 double 按 16 位数组 _16b[2] 重新解释,低位 _16b[0] 装 Position,高位 _16b[1] 装 Mode。 第 031 行先取指标句柄,取不到就直接 return;第 033–034 行用 CopyBuffer 读 1 根柱的缓冲到 Buff,成功才把 Buff[0] 赋给 info.dValue。随后 IndicatorRelease 释放句柄,避免句柄泄漏。 第 036–037 行判断:若 _16b[0] 不是 SHORT_MIN(即指标端写了有效仓位),就把 Mode 同步为 _16b[1]。第 038 行发现指标端 Position 与控制结构不一致时,若满足「有效仓位且 Mode==SHORT_MAX」才覆盖 m_IndControl.Position,再把当前 Position/Mode 写回联合体并 EventChartCustom 发自定义事件 evCtrlReplayInit,触发回放初始化。 别把 SHORT_MIN 当普通零值 SHORT_MIN(-32768)在这里被用作「未初始化」哨兵,不是数学上的最小仓位。若你抄这段代码去接自己的指标,务必保证指标端写 Position 时避开 -32768,否则同步分支永远不进。 第 048 行起的 SweepAndCloseChart 则是另一个清理入口:用 ChartFirst 拿到第一个图表 id 后进入 do 循环准备遍历,配合后续 ChartNext 能扫掉回放产生的孤儿图表。外汇与贵金属回放涉及高杠杆,实盘前请在模拟环境验证这套句柄与事件逻辑。

MQL5 / C++
class="type">class="kw">short   Mode;
 }m_IndControl;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">bool MsgError(class="type">class="kw">string sz0) { Print(sz0); class="kw">return class="kw">false; }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void UpdateIndicatorControl(class="type">void)
  {
   uCast_Double info;
   class="type">int handle;
   class="type">class="kw">double Buff[];
   
   if ((handle = ChartIndicatorGet(m_IdReplay, class="num">0, def_ShortNameIndControl)) == INVALID_HANDLE) class="kw">return;
   info.dValue = class="num">0;
   if (CopyBuffer(handle, class="num">0, class="num">0, class="num">1, Buff) == class="num">1)
      info.dValue = Buff[class="num">0];
   IndicatorRelease(handle);
   if ((class="type">class="kw">short)(info._16b[class="num">0]) != SHORT_MIN)
      m_IndControl.Mode = (class="type">class="kw">short)info._16b[class="num">1];
   if (info._16b[class="num">0] != m_IndControl.Position)
    {
     if (((class="type">class="kw">short)(info._16b[class="num">0]) != SHORT_MIN) && ((class="type">class="kw">short)(info._16b[class="num">1]) == SHORT_MAX))
         m_IndControl.Position = info._16b[class="num">0];
     info._16b[class="num">0] = m_IndControl.Position;
     info._16b[class="num">1] = (class="type">class="kw">ushort)m_IndControl.Mode;
     EventChartCustom(m_IdReplay, evCtrlReplayInit, class="num">0, info.dValue, "");
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void SweepAndCloseChart(class="type">void)
  {
   class="type">long id;
   
   if ((id = ChartFirst()) > class="num">0) do
    {

回放品种的初始化与图表清理

在 MT5 里做行情回放,第一步是造一个干净的自定义品种。构造函数里先把旧的 def_SymbolReplay 从市场观察里取消选择并删除,再用 CustomSymbolCreate 以 "Custom\\%s" 路径重建,避免历史残留干扰仿真。 重建后必须显式写入交易参数:tick size、tick value、volume step 都先置 0,描述字段标成 "Symbol for replay / simulation",价格精度设 8 位(SYMBOL_DIGITS=8),最后 SymbolSelect 置 true 让品种出现在观察列表。 OpenChartReplay 在开图前会反向校验那三个置 0 的字段——只要 SYMBOL_TRADE_TICK_SIZE / TICK_VALUE / VOLUME_STEP 任一仍为 0,就直接返回错误并拒绝开图。也就是说,若你复制这段代码却没在别处补上真实 tick 参数,回放图表永远打不开。 清理逻辑也值得留意:用 ChartNext 遍历所有图表 ID,遇到 ChartSymbol(id)==def_SymbolReplay 就 ChartClose 掉,直到 ID 归 0。外汇与贵金属回放属高杠杆仿真,参数错配可能导致 tick 计算失真,实盘前务必在策略测试器外先单品种验证。

MQL5 / C++
if (ChartSymbol(id) == def_SymbolReplay)
   ChartClose(id);
   }
class="kw">while ((id = ChartNext(id)) > class="num">0);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public   :
class=class="str">"cmt">//+------------------------------------------------------------------+
   C_Replay()
      :C_ConfigService()
   {
       Print("************** Market Replay Service **************");
       srand(GetTickCount());
       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);
       CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, class="num">0);
       CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, class="num">0);
       CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
       CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, class="num">8);
       SymbolSelect(def_SymbolReplay, true);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">bool OpenChartReplay(class="kw">const ENUM_TIMEFRAMES arg1, class="kw">const class="type">class="kw">string szNameTemplate)
   {
       if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == class="num">0)
         class="kw">return MsgError("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)
         class="kw">return MsgError("Asset configuration is not complete, need to declare the ticket value.");
       if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == class="num">0)

常见问题

在回放引擎初始化时主动注入一根空 bar 作为占位,避免后续数据写入越界;检查私有结构里的首根标记位即可确认是否已完成注入。
可能是 C_Replay 重构后异步模板与图表渲染产生竞态;用互斥锁约束图表写操作,或在单线程模式先验证回放逻辑。
可以,小布能扫描回放品种页的初始化状态和图表残留对象,提示未清理的指标与空 bar,你按提示一键复位即可。
联合体省内存且搬数快,但必须保证同一时刻只用一个成员;建议在写入前清联合体并加类型标记防误读。
在回放销毁流程里显式调用图表对象删除与缓存释放,并重置私有结构指针;别只隐藏窗口,否则下次回放会读到脏数据。