开发回放系统  市场模拟(第 05 部分):加入预览·进阶篇

开发回放系统 市场模拟(第 05 部分):加入预览·进阶篇

(2/3)· 只播当根柱的回放为何让均线和信号全部失效?本篇给出补数据的实操路径

案例拆解新手友好 第 2/3 篇
很多人在回放里刚跑出第一根 1 分钟柱就想看指标信号,结果屏幕空空如也。没有前置历史柱线,3 周期均线要等三根走完才显示,所谓预分析根本无从谈起。把回放当实时行情用,是这套系统最常见的误用。

「回放类内部重构:变量与初始化逻辑」

重构后的 C_Replay 类第一处可见变化是成员变量集合。原先的字段被替换成一组新的内部状态,其中 m_ReplayCount 记录已加载跳价数,m_dtPrevLoading 存最近一次数据载入时间,m_IdReplay 作为回放品种的唯一 ID,另用 st00 结构承接跳价数组与计数。这套变量足以支撑后续回放控制,但其它组件也必须同步改。 初始化函数 InitSymbolReplay 现在先做清理:若市场观察里已存在回放品种且不同于当前图表品种,先 SymbolSelect 取消选中,再 CustomSymbolDelete 删掉,然后用 CustomSymbolCreate 以当前品种为基准建一个 Custom\ 下的自定义品种,最后重新选入市场观察。这么做是为了把回放品种完全置于代码控制之下,方便后续往图表上挂。 别急着加功能,先把这套初始化在 MT5 里跑通。自定义品种路径写错会直接导致后面 LoadTicksReplay 打开 CSV 失败,而该函数目前没有任何文件名校验,文件必须手动放在 MQL5 指定目录、且不用写 .csv 后缀(默认补 CSV)。若找不到文件,回放服务不起动,只会弹窗报错。

MQL5 / C++
class="type">int        m_ReplayCount;
class="type">class="kw">datetime   m_dtPrevLoading;
class="type">long       m_IdReplay;
class="kw">struct st00
{
    class="type">MqlTick Info[];
    class="type">int     nTicks;
}m_Ticks;
class="type">void InitSymbolReplay(class="type">void)
{
    SymbolSelect(def_SymbolReplay, false);
    CustomSymbolDelete(def_SymbolReplay);
    CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\%s", def_SymbolReplay), _Symbol);
    SymbolSelect(def_SymbolReplay, true);
}
class="macro">#define macroRemoveSec(A) (A - (A % class="num">60))
            class="type">bool LoadTicksReplay(const class="type">class="kw">string szFileNameCSV)
            {
                class="type">int     file,
                        old;
                class="type">class="kw">string  szInfo;
                class="type">MqlTick tick;

从CSV回放历史tick的读取逻辑

做市场重放(Market Replay)时,核心是把提前落盘的 tick CSV 按原始时序灌回内存数组,MT5 里用 FileOpen 配合 FILE_CSVFILE_READFILE_ANSI 打开位于 Market Replay\Ticks\ 下的文件即可。

代码先跳过表头:for 循环里连续 FileReadString 七次,把前 7 行非数据内容(通常是标题与字段说明)丢弃,再从第 8 行开始逐条读 tick。 每条 tick 由时间、毫秒、bid、ask、last、volume_real 六个字段拼成;其中秒级时间用 macroRemoveSec 抹掉秒数,毫秒单独从子串 [20,3] 取整数。数组上限由 def_MaxSizeArray 控制,读到文件尾或触顶就停,避免内存爆掉。 开 MT5 把这段接进 EA 的初始化函数,改掉 szFileNameCSV 的命名规则,就能验证自己导出的 tick 是否被正确加载——外汇与贵金属tick重放存在点差跳变与滑点风险,回测结果仅代表历史概率。

MQL5 / C++
if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
{
   Print("Loading replay ticks. Wait...");
   ArrayResize(m_Ticks.Info, def_MaxSizeArray);
   old = m_Ticks.nTicks = class="num">0;
   for (class="type">int c0 = class="num">0; c0 < class="num">7; c0++) FileReadString(file);
   while ((!FileIsEnding(file)) && (m_Ticks.nTicks < def_MaxSizeArray))
   {
      szInfo = FileReadString(file) + " " + FileReadString(file);
      tick.time = macroRemoveSec(StringToTime(StringSubstr(szInfo, class="num">0, class="num">19)));
      tick.time_msc = (class="type">int)StringToInteger(StringSubstr(szInfo, class="num">20, class="num">3));
      tick.bid = StringToDouble(FileReadString(file));
      tick.ask = StringToDouble(FileReadString(file));
      tick.last = StringToDouble(FileReadString(file));
      tick.volume_real = StringToDouble(FileReadString(file));

◍ 逐笔 tick 的合并与缺失文件兜底

回放历史 tick 时,同一毫秒内可能连续推来多笔相同 last、time、time_msc 的报价。上面这段逻辑的做法是:先比对上一条记录的三个字段,若完全一致,就只把 volume_real 累加进去,不新增数组元素。 若三项不全等,才把当前 tick 写进 m_Ticks.Info 并把 nTicks 加 1——但仅当 volume_real > 0.0 才计数,零成交量 tick 不会污染样本长度。old 指针随后回退到最新一条,供下一笔继续比对。 文件层有个硬兜底:当 FileReadString 取不到目标 csv 时,直接 Print("Tick file ...csv not found...") 并返回 false,调用方必须判返回值,否则后续基于空 tick 池的计算会全错。外汇与贵金属 tick 重放属高频数据操作,实盘与回测偏差风险高,参数改动前建议在 MT5 策略测试器先跑一遍。

MQL5 / C++
tick.flags = (class="type">uchar)StringToInteger(FileReadString(file));
if ((m_Ticks.Info[old].last == tick.last) && (m_Ticks.Info[old].time == tick.time) && (m_Ticks.Info[old].time_msc == tick.time_msc))
   m_Ticks.Info[old].volume_real += tick.volume_real;
else
   {
   m_Ticks.Info[m_Ticks.nTicks] = tick;
   m_Ticks.nTicks += (tick.volume_real > class="num">0.0 ? class="num">1 : class="num">0);
   old = (m_Ticks.nTicks > class="num">0 ? m_Ticks.nTicks - class="num">1 : old);
   }
}
else
   {
   Print("Tick file ", szFileNameCSV,".csv not found...");
   class="kw">return false;
   }
class="kw">return true;
class="macro">#undef macroRemoveSec
class="type">bool LoadPrevBars(const class="type">class="kw">string szFileNameCSV)
{
   class="type">int      file,
            iAdjust = class="num">0;
   class="type">class="kw">datetime dt = class="num">0;

「把 CSV 历史棒线灌进回放品种」

做行情回放时,常需要把已经导出的历史 K 线先喂给自定义回放品种,而不是从零开始跑。下面这段逻辑就是打开 Market Replay\Bars 目录下的 CSV,逐行读出 OHLC 与成交量后,用 CustomRatesUpdate 推给回放符号。 文件打开后先跳过前 9 行表头或注释,这是写死在 for 循环里的常数,改文件格式时要同步改这个数。随后进入按行读取:时间由日期串加时间串拼成,open/high/low/close 走 StringToDouble,tick_volume 与 real_volume 走 StringToInteger,spread 强转成 int。 iAdjust 这段用来修正时区或偏移:仅在第一根有效棒时计算与基准 dt 的差值,之后保持不动。最后 m_dtPrevLoading 记的是最后一根棒时间加偏移,供后续增量加载判断起点;若文件打不开则置 0 并打印失败。外汇与贵金属回放含高杠杆风险,加载数据仅供策略验证,不预示实盘走向。 让小布替你跑这套 直接把这段塞进 EA 的初始化或按钮回调,CSV 路径和 def_SymbolReplay 换成你自己的,开 MT5 看自定义品种是否按预期出现历史棒,能省掉手动导入的麻烦。

MQL5 / C++
class="type">MqlRates Rate[class="num">1];

if ((file = FileOpen("Market Replay\\Bars\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
{
   for (class="type">int c0 = class="num">0; c0 < class="num">9; c0++) FileReadString(file);
   Print("Loading preview bars to Replay. Wait ....");
   while (!FileIsEnding(file))
   {
      Rate[class="num">0].time = StringToTime(FileReadString(file) + " " + FileReadString(file));
      Rate[class="num">0].open = StringToDouble(FileReadString(file));
      Rate[class="num">0].high = StringToDouble(FileReadString(file));
      Rate[class="num">0].low = StringToDouble(FileReadString(file));
      Rate[class="num">0].close = StringToDouble(FileReadString(file));
      Rate[class="num">0].tick_volume = StringToInteger(FileReadString(file));
      Rate[class="num">0].real_volume = StringToInteger(FileReadString(file));
      Rate[class="num">0].spread = (class="type">int) StringToInteger(FileReadString(file));
      iAdjust = ((dt != class="num">0) && (iAdjust == class="num">0) ? (class="type">int)(Rate[class="num">0].time - dt) : iAdjust);
      dt = (dt == class="num">0 ? Rate[class="num">0].time : dt);
      CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1);
   }
   m_dtPrevLoading = Rate[class="num">0].time + iAdjust;
   FileClose(file);
}else
{
   Print("Failed to access the previous bars data file.");
   m_dtPrevLoading = class="num">0;
}
交给小布盯盘看盘口
这些历史柱线补齐与回放诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到模拟前的上下文是否充足,你只管专注策略验证。

常见问题

指标需要最少样本数才能计算,例如 3 周期均线必须累积 3 根完整柱才会绘制,回放若只生成当前柱就必然延迟显示。
回放使用真实成交跳价数据,模拟器则基于数学或任意模型合成数据,二者操作原则相同但数据可信度与用途不同。
没有标准模型,取决于你分析的目标时间帧与所用指标的最小计算窗口,先明确这两点再补历史数据。
可以,小布盯盘的 AIGC 模块支持识别回放上下文并提示缺失的前置柱线,省去手动拼接数据的麻烦。
多由数学模型或纯任意方式合成走势,用来预测极端情况下的潜在波动,本篇仅铺垫,后续会展开。