开发回放系统 市场模拟(第 05 部分):加入预览·进阶篇
(2/3)· 只播当根柱的回放为何让均线和信号全部失效?本篇给出补数据的实操路径
「回放类内部重构:变量与初始化逻辑」
重构后的 C_Replay 类第一处可见变化是成员变量集合。原先的字段被替换成一组新的内部状态,其中 m_ReplayCount 记录已加载跳价数,m_dtPrevLoading 存最近一次数据载入时间,m_IdReplay 作为回放品种的唯一 ID,另用 st00 结构承接跳价数组与计数。这套变量足以支撑后续回放控制,但其它组件也必须同步改。 初始化函数 InitSymbolReplay 现在先做清理:若市场观察里已存在回放品种且不同于当前图表品种,先 SymbolSelect 取消选中,再 CustomSymbolDelete 删掉,然后用 CustomSymbolCreate 以当前品种为基准建一个 Custom\ 下的自定义品种,最后重新选入市场观察。这么做是为了把回放品种完全置于代码控制之下,方便后续往图表上挂。 别急着加功能,先把这套初始化在 MT5 里跑通。自定义品种路径写错会直接导致后面 LoadTicksReplay 打开 CSV 失败,而该函数目前没有任何文件名校验,文件必须手动放在 MQL5 指定目录、且不用写 .csv 后缀(默认补 CSV)。若找不到文件,回放服务不起动,只会弹窗报错。
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_CSV | FILE_READ | FILE_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重放存在点差跳变与滑点风险,回测结果仅代表历史概率。
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 策略测试器先跑一遍。
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 看自定义品种是否按预期出现历史棒,能省掉手动导入的麻烦。
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; }