开发回放系统(第 58 部分):重返服务工作·进阶篇
回放引擎的私有结构与首根空 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 之后才开始。外汇与贵金属回放含高杠杆风险,这种预处理只影响本地重放精度,不预示任何方向。
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 确立公共继承,工作负载分流。
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 能扫掉回放产生的孤儿图表。外汇与贵金属回放涉及高杠杆,实盘前请在模拟环境验证这套句柄与事件逻辑。
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 计算失真,实盘前务必在策略测试器外先单品种验证。
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)