开发回放系统(第 62 部分):玩转服务(三)·综合运用
◍ 回放模式下逐笔合成与内存回收的细节
下面这段逻辑负责把采样 tick 按 1 分钟粒度重建成柱,并在非回放模式下把增量推给自定义品种。循环从 MemNTicks 起步,只处理本次新到的 tick,避免重复计算历史。 for (int c0 = MemNTicks; c0 < m_Ticks.nTicks; c0++) { if (!BuildBar1Min(c0, rate, bNew)) continue; if (bNew) ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary); m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate; } 第15行:从 MemNTicks 遍历到当前 tick 总数,只跑新增部分。 第17行:调用 BuildBar1Min 尝试把该 tick 并入 1 分钟柱,失败就跳过。 第18行:若生成新柱,按当前已存柱数 +2 或默认 def_BarsDiary 扩容 Rate 数组。 第19行:新柱才让 nRate 加 1,并把 rate 写进数组对应位。 if (!ToReplay) { ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0); CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); dtRet = m_Ticks.Rate[m_Ticks.nRate].time; m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates); m_Ticks.nTicks = MemNTicks; ArrayFree(RatesLocal); }else SetSymbolInfos(); 第21–30行:非回放时,把增量柱拷到 RatesLocal,用 CustomRatesUpdate 刷到 def_SymbolReplay;随后把 nRate 回退到 MemNRates、nTicks 回退到 MemNTicks,释放本地数组。若走回放分支,只调 SetSymbolInfos 同步品种属性。 m_Ticks.bTickReal = true; return dtRet; 第31–33行:标记 tick 为真实来源,返回最后一根柱的时间。外汇与贵金属价差跳变频繁,这种增量回退机制能压住内存增长,但自定义品种刷新仍有滑点错乱的可能,建议在 MT5 用 EURUSD 的盯盘品种先单步验证 ArrayResize 的扩容步长。
for (class="type">int c0 = MemNTicks; c0 < m_Ticks.nTicks; c0++) { if (!BuildBar1Min(c0, rate, bNew)) class="kw">continue; if (bNew) ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > class="num">0 ? m_Ticks.nRate + class="num">2 : def_BarsDiary), def_BarsDiary); 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 SetSymbolInfos(); m_Ticks.bTickReal = true; class="kw">return dtRet;
「tick异常放量时切入模拟补点」
回放历史 tick 时,若某根已成型分钟的 tick_volume 超过 MaxTickVolume,这段逻辑会放弃真实聚合、改走模拟器补点。 第27行先判断 m_Ticks.nRate>=0 且 ToReplay 为真,再比对当前 Rate 的 tick_volume 与阈值;一旦越界,第29行把 nShift 回退到 MemShift,保证写入位置不偏移。 第30~34行新建 C_Simulation 对象,用 Simulation() 返回补点数量 c1;ArrayCopy 把 TicksLocal 从0拷贝 c1 个到 m_Ticks.Info 的 nShift 处,随后 nShift+=c1 并 delete 对象,避免 MT5 终端内存泄漏。 第37行对 m_Ticks.Rate 做 ArrayResize,当 nRate>0 时预留 +2 缓冲,否则按 def_BarsDiary 初始化。外汇与贵金属 tick 流在重大数据行情可能瞬间放量数倍,这种分支能让 1 分钟 bar 重建倾向更平滑,但模拟数据终归非真实成交,验证时需在 MT5 用极端行情样本比对。
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(); ArrayResize(TicksLocal, def_MaxSizeArray); m_Ticks.bTickReal = true; for (class="type">int c0 = MemNTicks, c1, MemShift = nShift; c0 < m_Ticks.nTicks; c0++, nShift++) { 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; C_Simulation *pSimulator = new C_Simulation(nDigits); if ((c1 = (*pSimulator).Simulation(m_Ticks.Rate[m_Ticks.nRate], TicksLocal, MaxTickVolume)) < class="num">0) class="kw">return class="num">0; ArrayCopy(m_Ticks.Info, TicksLocal, nShift, class="num">0, c1); nShift += c1; class="kw">delete pSimulator; } MemShift = nShift; ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > class="num">0 ? m_Ticks.nRate + class="num">2 : def_BarsDiary), def_BarsDiary); }; m_Ticks.Rate[(m_Ticks.nRate += (bNew ? class="num">1 : class="num">0))] = rate; } ArrayFree(TicksLocal); if (!ToReplay) {
回放引擎里的速率数组回收逻辑
在 MT5 自定义回放(Replay)场景里,RatesLocal 只是临时承载一段行情速率的本地数组,用完后必须释放,否则长时间跑复盘会缓慢吃内存。上面这段代码段展示了每次推送新速率前如何重设大小、拷贝并交由 CustomRatesUpdate 注入到 def_SymbolReplay 这个虚拟品种。 第 44 行先用 ArrayResize 把 RatesLocal 缩到「当前累计速率数减去已记忆的旧速率数」,也就是只装增量部分;第 45 行 ArrayCopy 从 m_Ticks.Rate 全量拷到本地数组起点。注意这里第二个 0 是目标起始、第三个 0 是源起始,拷的是整段而非偏移。 第 46 行 CustomRatesUpdate 只更新增量长度,避免重复写全盘历史。第 47 行把返回时间锚定到最新一条速率的 time 字段,供上层判断回放时钟走到哪。 第 48–49 行重置 nRate 与 nTicks 游标:若 MemNRates 为 0 则 nRate 置 -1,代表从头开始;否则保留记忆位置。第 50 行 ArrayFree 立刻释放 RatesLocal,这一行不能省。外汇与贵金属复盘本身带高杠杆高风险,回放结论只用于验证策略逻辑,不等于实盘表现。
class="num">44. ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); class="num">45. ArrayCopy(RatesLocal, m_Ticks.Rate, class="num">0, class="num">0); class="num">46. CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); class="num">47. dtRet = m_Ticks.Rate[m_Ticks.nRate].time; class="num">48. m_Ticks.nRate = (MemNRates == class="num">0 ? -class="num">1 : MemNRates); class="num">49. m_Ticks.nTicks = MemNTicks; class="num">50. ArrayFree(RatesLocal); class="num">51. }else m_Ticks.nTicks = nShift; class="num">52. class="num">53. class="kw">return dtRet; class="num">54. }; class="num">55. class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 画得少,看得清
限制每分钟柱形的分时报价数之后,回放系统确实清爽了,但两个坑也跟着浮出来。其一是最小分时报价数的模拟边界没兜住——一旦计数超过给定值才去模拟报价,某些本该存在的 tick 就会漏掉,柱形生成逻辑随之失真。 其二是把模拟值拷进分时报价数组那一步,会直接伤到回放柱形生成系统:偶尔吐出不合逻辑的价格序列,甚至某个时段报价整段消失,导致回放不再可信。外汇与贵金属回测本就高波动、高风险,用这种带缺陷的回放做验证须格外谨慎。 下一篇会给出修法,但建议你先自己排一遍雷:打开 MT5 把 CopyTicks 相关的数组填充段单跑几次,观察报价断层出现在第几根柱。能定位,才算真吃透了这套模拟思路。