开发回放系统(第 62 部分):玩转服务(三)·综合运用
📘

开发回放系统(第 62 部分):玩转服务(三)·综合运用

第 3/3 篇

◍ 回放模式下逐笔合成与内存回收的细节

下面这段逻辑负责把采样 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 的扩容步长。

MQL5 / C++
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 用极端行情样本比对。

MQL5 / C++
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,这一行不能省。外汇与贵金属复盘本身带高杠杆高风险,回放结论只用于验证策略逻辑,不等于实盘表现。

MQL5 / C++
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 相关的数组填充段单跑几次,观察报价断层出现在第几根柱。能定位,才算真吃透了这套模拟思路。

常见问题

可在tick异常放量时切入模拟补点逻辑,用插值补齐缺失笔数,避免回放引擎阻塞。先确认放量阈值再开模拟,别无脑补。
重点看速率数组的回收逻辑,每轮回放结束应显式释放不再用的数组引用。建议打印每阶段内存峰值定位泄漏点。
小布可接管回放任务调度,自动标记速率数组未回收的时段并推送提醒,你只需看结论不用盯日志。
回放模式逐笔合成需做唯一ID去重,并在内存回收时清掉已合成缓存。建议加一个本地哈希表做过滤。
遵循画得少看得清原则,只留关键价位和异常tick标记。把次要线交给后台统计,不在主图堆砌。