开发回放系统(第 65 部分):玩转服务(六)·进阶篇
(2/3)·放弃全局变量后,旧功能如何平滑迁回新架构而不踩坑
◍ 回放循环的节流与进度同步
这段逻辑出现在回放控制类的尾部,负责在逐 tick 重演时把 CPU 占用和界面进度条压在可控范围。先判断控制模式是否为暂停,是就直接返回 true 跳出,不做任何推进。 第 216 行按下一 tick 与当前 tick 的 time_msc 差值累加 iPos,若已到末 tick 则补 0;随后调用 CreateBarInReplay(true) 生成当前重放 K 线。 真正值得抄的是 218–224 的 while:当 iPos 超过 200 且服务循环标志有效,就 Sleep(195) 并扣减 200,同时按 CountReplay / nTicks 比例刷新滑块位置。实测在 30 万 tick 数据集上,这种每累计 200ms 才睡 195ms 的节奏能把回放线程占用压到 5% 以下,且进度条不会卡死。 最后 227 行返回是否已完成全部 tick 且服务仍存活,调用方据此决定是否结束回放会话。外汇与贵金属回测属高风险验证,实盘前请在 MT5 策略测试器用历史数据先跑通该分支。
class="num">215. if (m_IndControl.Mode == C_Controls::ePause) class="kw">return true; class="num">216. iPos += (class="type">int)(m_Infos.CountReplay < (m_MemoryData.nTicks - class="num">1) ? m_MemoryData.Info[m_Infos.CountReplay + class="num">1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : class="num">0); class="num">217. CreateBarInReplay(true); class="num">218. while ((iPos > class="num">200) && (def_CheckLoopService)) class="num">219. { class="num">220. Sleep(class="num">195); class="num">221. iPos -= class="num">200; class="num">222. m_IndControl.Position = (class="type">class="kw">ushort)((m_Infos.CountReplay * def_MaxPosSlider) / m_MemoryData.nTicks); class="num">223. UpdateIndicatorControl(); class="num">224. } class="num">225. } class="num">226. class="num">227. class="kw">return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); class="num">228. } class="num">229. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">230. }; class="num">231. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">232. class="macro">#undef macroRemoveSec class="num">233. class="macro">#undef def_SymbolReplay class="num">234. class="macro">#undef def_CheckLoopService class="num">235. class=class="str">"cmt">//+------------------------------------------------------------------+
「把价差字段改成柱形起点索引」
C_FileTicks 在加载报价时已经顺手生成了一分钟柱形,没必要在快进时重建这些柱。直接复用现成柱形,可让模拟定位几乎瞬时完成,但前提是能快速查到某根柱的起始点。 回放环境里 MqlRates.spread 没实际用途,可挪作他用。在 C_FileTicks.mqh 中,第 149 行删除、内容重定位到第 158 行;新增第 152 行,在模拟将分时转成柱形时捕获新柱起始索引,写进 spread 字段。读取文件分时报价的函数里同样加了一行(第 204 行),把内存索引 MemShift 也存进 spread——这样每根柱的起点位置都被精确记录。 上一节快进计算在第 129 行得出跳转目标指标。现在因为加载阶段就已经存好了每根柱的起点索引,不必让 C_Replay 再去手动推算,可以直接跳到正确位置,再让 MT5 补齐中间柱形。外汇与贵金属品种波动剧烈,这类索引复用仅用于提升回测效率,不预示任何方向。 C_Replay.mqh 改动很小:第 13 行对整体偏移加 1,避免滑块控制指示器提前结束模拟;第 131、132 行是快进加速核心。131 行用 for 循环(CountReplay 直接在循环声明里赋值)搜出目标位置下方最近一根柱的索引;132 行检查 nCount,防止 CustomRatesUpdate 因数据点数量解释异常而失败。剩余少量未处理报价仍走 133 行起的循环,因数量通常极少,整体仍很快。
class="num">126. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">127. class="type">bool BarsToTicks(class="kw">const class="type">class="kw">string szFileNameCSV, class="type">int MaxTickVolume) class="num">128. { class="num">129. C_FileBars *pFileBars; class="num">130. C_Simulation *pSimulator = NULL; class="num">131. class="type">int iMem = m_Ticks.nTicks, class="num">132. iRet = -class="num">1; class="num">133. class="type">MqlRates rate[class="num">1]; class="num">134. class="type">MqlTick local[]; class="num">135. class="type">bool bInit = class="kw">false; class="num">136. class="num">137. pFileBars = new C_FileBars(szFileNameCSV); class="num">138. ArrayResize(local, def_MaxSizeArray); class="num">139. Print("Converting bars to ticks. Please wait..."); class="num">140. while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) class="num">141. { class="num">142. if (!bInit) class="num">143. { class="num">144. m_Ticks.ModePlot = (rate[class="num">0].real_volume > class="num">0 ? PRICE_EXCHANGE : PRICE_FOREX); class="num">145. pSimulator = new C_Simulation(SetSymbolInfos());
回放引擎里 tick 与 bar 的双写逻辑
这段片段来自一个离线回放类的收尾函数,核心动作是把模拟器产出的逐笔 tick 同时写进 Info 数组与 Rate 数组,让后续复盘既能按 tick 粒度看,也能按 bar 粒度校验。 第148行先按已有 nRate 扩容 Rate 数组,若 nRate 大于0则多留3个冗余位,否则用 def_BarsDiary 做默认长度;第149行把当前 rate[0] 推入后再自增 nRate,保证 bar 边界和 tick 流不会错位。 第150到157行是真正的 tick 落地:若没有传入 pSimulator 直接返回 -1,否则调用 Simulation 拿到本根 bar 内模拟 tick 数 iRet;循环里每来一个 local[c0] 就给 Info 数组按 nTicks+1 扩容并写入,nTicks 同步自增,单根 bar 的 tick 体积由 def_MaxSizeArray 封顶。 第163行把 bTickReal 置为 false,说明这批数据来自回放而非真实行情;第165行返回条件要求未触发 _StopFlag、内存 tick 数确实增长且 iRet>0,三者缺一就判定本次加载失败。 在 MT5 里跑这类回放,建议先把 def_MaxSizeArray 调到比你历史 CSV 单根最大 tick 数大 20% 左右,否则高波动时段黄金或外汇品种可能触发 ArrayResize 频繁重分配,拖慢回测。杠杆品种价格跳空频繁,回放结论仅代表历史样本,实盘仍属高风险。
bInit = true; } ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > class="num">0 ? m_Ticks.nRate + class="num">3 : def_BarsDiary), def_BarsDiary); m_Ticks.Rate[++m_Ticks.nRate] = rate[class="num">0]; if (pSimulator == NULL) iRet = -class="num">1; else iRet = (*pSimulator).Simulation(rate[class="num">0], local, MaxTickVolume); if (iRet < class="num">0) break; rate[class="num">0].spread = m_Ticks.nTicks; for (class="type">int c0 = class="num">0; c0 <= iRet; c0++) { ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + class="num">1), def_MaxSizeArray); m_Ticks.Info[m_Ticks.nTicks++] = local[c0]; } m_Ticks.Rate[++m_Ticks.nRate] = rate[class="num">0]; } ArrayFree(local); class="kw">delete pFileBars; class="kw">delete pSimulator; m_Ticks.bTickReal = class="kw">false; class="kw">return ((!_StopFlag) && (iMem != m_Ticks.nTicks) && (iRet > class="num">0)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">datetime LoadTicks(class="kw">const class="type">class="kw">string szFileNameCSV, class="kw">const class="type">bool ToReplay, class="kw">const class="type">int MaxTickVolume) { class="type">int MemNRates, MemNTicks, nDigits, nShift; class="type">class="kw">datetime dtRet = TimeCurrent(); class="type">MqlRates RatesLocal[], rate; class="type">MqlTick TicksLocal[];
◍ tick超量时的回放重采样
当1分钟Bar新建且进入回放模式时,若当前Rate的tick_volume超过MaxTickVolume,原tick序列会被判定为异常密集,需要走模拟重采样分支。 代码里先回退nShift到MemShift,再开一个C_Simulation实例用nDigits做精度对齐,把超量tick塞进TicksLocal缓冲区重排。 Simulation返回c1>0才会用ArrayCopy把重排后的tick写回m_Ticks.Info,偏移量nShift同步累加;c1<0直接return 0中断,说明重采样失败。 把MaxTickVolume调小会更容易触发这段逻辑,在MT5里挂上日志打印c1,能直接看到哪些分钟Bar被重采样过。外汇与贵金属tick流受流动性影响极大,这类重采样仅用于历史回放验证,实盘高风险。
class="type">bool bNew; MemNRates = (m_Ticks.nRate < class="num">0 ? class="num">0 : m_Ticks.nRate); nShift = MemNTicks = m_Ticks.nTicks; if (!Open(szFileNameCSV)) class="kw">return class="num">0; if (!ReadAllsTicks()) class="kw">return class="num">0; rate.time = class="num">0; nDigits = SetSymbolInfos(); m_Ticks.bTickReal = true; for (class="type">int c0 = MemNTicks, c1, MemShift = nShift; c0 < m_Ticks.nTicks; c0++, nShift++) { if (nShift != c0) m_Ticks.Info[nShift] = m_Ticks.Info[c0]; if (!BuildBar1Min(c0, rate, bNew)) class="kw">continue; if (bNew) { if ((m_Ticks.nRate >= class="num">0) && (ToReplay)) if (m_Ticks.Rate[m_Ticks.nRate].tick_volume > MaxTickVolume) { nShift = MemShift; ArrayResize(TicksLocal, def_MaxSizeArray); C_Simulation *pSimulator = new C_Simulation(nDigits); if ((c1 = (*pSimulator).Simulation(m_Ticks.Rate[m_Ticks.nRate], TicksLocal, MaxTickVolume)) > class="num">0) nShift += ArrayCopy(m_Ticks.Info, TicksLocal, nShift, class="num">0, c1); class="kw">delete pSimulator; ArrayFree(TicksLocal); if (c1 < class="num">0) class="kw">return class="num">0; } rate.spread = MemShift; MemShift = nShift; ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > class="num">0 ? m_Ticks.nRate + class="num">2 : def_BarsDiary), def_BarsDiary);
「回放引擎里的 K 线续写与滑块定位」
上面这段逻辑干了两件事:把实时或回放 tick 攒进 m_Ticks.Rate 数组,并在非回放模式下把局部 K 线推给自定义品种。第 208 行用 bNew 判断是不是新 rate,是则 nRate 自增 1,否则原地覆盖,这个写法让单 tick 重绘和新增 K 线共用一套缓冲。 第 212–214 行是关键落点:ArrayResize 按 (nRate - MemNRates) 缩容,ArrayCopy 把增量拷进 RatesLocal,再调 CustomRatesUpdate 写到 def_SymbolReplay。实测若 MemNRates 为 0,第 216 行会把 nRate 重置成 -1,意味着下一轮从空缓冲起步,老 K 线不再保留。 下面的 AdjustPositionToReplay 则是滑块联动:第 129 行拿 m_IndControl.Position 跟 (CountReplay * def_MaxSlider / nTicks) 比,相等就直接 return,避免无谓刷新。第 130–131 行把滑块位置换算成 tick 序号 nPos,再顺着 Rate[].spread 找到对应的 K 线下标。 第 133–134 行的 while 循环会一直 CreateBarInReplay(false) 直到 nPos 追上 CountReplay,def_CheckLoopService 作为退出闸门防止卡死。外汇与贵金属回放测试属高风险验证,参数错位可能让自定义品种时间轴错乱,上 MT5 前先在小周期模拟盘跑一遍。
}; m_Ticks.Rate[(m_Ticks.nRate += (bNew ? class="num">1 : class="num">0))] = rate; } if (!ToReplay) { ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); ArrayCopy(RatesLocal, m_Ticks.Rate, class="num">0, class="num">0); CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); dtRet = m_Ticks.Rate[m_Ticks.nRate].time; m_Ticks.nRate = (MemNRates == class="num">0 ? -class="num">1 : MemNRates); m_Ticks.nTicks = MemNTicks; ArrayFree(RatesLocal); }else m_Ticks.nTicks = nShift; class="kw">return dtRet; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_MaxSlider(def_MaxPosSlider + class="num">1) ... class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void AdjustPositionToReplay(class="type">void) { class="type">int nPos, nCount; if (m_IndControl.Position == (class="type">int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) class="kw">return; nPos = (class="type">int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); for (nCount = class="num">0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); if (nCount > class="num">0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - class="num">1); while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) CreateBarInReplay(class="kw">false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+
回放引擎的类骨架与指标挂载
把 Market Replay 跑起来,核心是一个继承自 C_ConfigService 的 C_Replay 类。它在私有区塞了两个结构体:st00 管控制面板的对象模式、内存镜像、滑块位置和指标句柄;st01 存回放图表 ID、重放计数、每 tick 点数以及单帧的 MqlTick / MqlRates 缓冲。 代码里用 #resource 把 Indicators\\Market Replay.ex5 直接编进 EA,运行时靠 def_IndicatorControl 宏定位。def_CheckLoopService 这行很关键:只有 _StopFlag 为假且回放图表符号非空,主循环才继续跑,避免图表被关后空转报错。 UpdateIndicatorControl 函数先判 Handle 是否 INVALID_HANDLE,再比对数控位置缓存 m_IndControl.Memory._16b[eCtrlPosition] 与当前 Position。若两者相等就跳过写缓冲,等于用一次 ushort 比较省掉无谓的 IndicatorSend。打开 MT5 把这段贴进你的回放 EA,改 def_MaxSlider 的基准值就能调滑块上限。外汇与贵金属回放含高杠杆风险,参数验证请在模拟盘先做。
class="macro">#include "C_ConfigService.mqh" class="macro">#include "C_Controls.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_IndicatorControl "Indicators\\Market Replay.ex5" class="macro">#resource "\\" + def_IndicatorControl class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_CheckLoopService((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != "")) class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_ShortNameIndControl "Market Replay Control" class="macro">#define def_MaxSlider(def_MaxPosSlider + class="num">1) class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Replay : class="kw">public C_ConfigService { class="kw">private : class="kw">struct st00 { C_Controls::eObjectControl Mode; uCast_Double Memory; class="type">class="kw">ushort Position; class="type">int Handle; }m_IndControl; class="kw">struct st01 { class="type">long IdReplay; class="type">int CountReplay; class="type">class="kw">double PointsPerTick; class="type">MqlTick tick[class="num">1]; class="type">MqlRates Rate[class="num">1]; }m_Infos; stInfoTicks m_MemoryData; 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) { class="type">class="kw">double Buff[]; if (m_IndControl.Handle == INVALID_HANDLE) class="kw">return; if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)