开发回放系统 市场模拟(第 06 部分):首次改进(I)·进阶篇
◍ 回放配置文件的读取与分流
做 MT5 市场回放(Market Replay)时,配置 CSV 是入口。文件里用 [BARS] 和 [TICKS] 两个标记段来区分接下来列出的是分钟 Bar 文件还是 Tick 文件,例如 WIN$N_M1_202108020900_202108021754 属于前者,WINQ21_202108050900_202108051759 属于后者。 SetSymbolReplay 这个函数负责把配置读进来。它先尝试打开 Market Replay\\ 目录下的 CSV,失败就弹窗并返回 false;成功则用 FileReadString 逐行读,把每行转大写后判断:碰到 [BARS] 就把 isBars 置 true,碰到 [TICKS] 就置 false,其余非空行按当前标记交给 LoadPrevBars 或 LoadTicksReplay。 这种分流写法意味着你改配置时,段标记写错一个字符(大小写已被 ToUpper 归一,但拼写必须完全匹配 def_STR_FilesBar / def_STR_FilesTicks)就会导致整段数据被误判。外汇与贵金属回放含杠杆与跳空,属高风险验证环境,结论仅代表历史数据可被重放,不预示未来。
class="type">bool SetSymbolReplay(const class="type">class="kw">string szFileConfig) { class="type">int file; class="type">class="kw">string szInfo; class="type">bool isBars = true; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Failed to read the configuration file.", "Market Replay", MB_OK); class="kw">return false; } Print("Loading data for replay.\nPlease wait...."); class="kw">while ((!FileIsEnding(file)) && (!_StopFlag)) { szInfo = FileReadString(file); StringToUpper(szInfo); if (szInfo == def_STR_FilesBar) isBars = true; else if (szInfo == def_STR_FilesTicks) isBars = false; else if (szInfo != "") if (!(isBars ? LoadPrevBars(szInfo) : LoadTicksReplay(szInfo))) { if (!_StopFlag) MessageBox(StringFormat("File %s of %s\ncould not be loaded.", szInfo, (isBars ? def_STR_FilesBar : def_STR_FilesTicks), "Market Replay", MB_OK)); FileClose(file);
从历史CSV回填重放K线
| 做行情回放的第一步,是把已导出的 CSV 历史棒线重新灌回自定义序列。下面这段 LoadPrevBars 函数就干了这件事:它先按固定路径 Market Replay\Bars\ 打开指定名称的 CSV,用 FILE_CSV | FILE_READ | FILE_ANSI 模式读取。 |
|---|
打开后先跳过表头——代码里用 for 循环读 9 次 FileReadString 拼成 szInfo,再和预设的 def_Header_Bar 比对。若对不上就 Print 报错并返回 false,避免把错误格式当历史数据吃进去。 表头校验通过后进入 while 循环,只要文件没读完且 _StopFlag 未被置位,就逐行把时间、开高低收、tick_volume、real_volume、spread 用 StringToTime / StringToDouble / StringToInteger 转成 MqlRates 结构。注意时间字段是日期串加时间串拼接后再转,说明 CSV 里这两项是分开存的。 外汇与贵金属品种点差跳变频繁,real_volume 在多数零售平台常为 0,回放时若依赖成交量过滤信号,需先确认源 CSV 是否含真实成交数据,否则重放结果可能偏离实盘概率。
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; class="type">MqlRates Rate[class="num">1]; class="type">class="kw">string szInfo = ""; 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++) szInfo += FileReadString(file); if (szInfo != def_Header_Bar) { Print("Файл ", szFileNameCSV, ".csv это не файл предыдущих баров."); class="kw">return false; } Print("Loading bars for the replay. Please wait...."); class="kw">while ((!FileIsEnding(file)) && (!_StopFlag)) { 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));
「回放引擎里的偏移修正与逐笔 tick 装载」
在回放历史行情时,首根 K 线时间与文件起始时间往往不对齐。下面这行用 iAdjust 把差值算出来并固化:若 dt 非零且 iAdjust 还是 0,就取当前 Rate[0].time 减 dt 强转成 int 存进 iAdjust,之后 dt 自身若为零也补成 Rate[0].time,保证后续每根都按同一偏移续接。 iAdjust = ((dt != 0) && (iAdjust == 0) ? (int)(Rate[0].time - dt) : iAdjust); dt = (dt == 0 ? Rate[0].time : dt); CustomRatesUpdate(def_SymbolReplay, Rate, 1); m_dtPrevLoading = Rate[0].time + iAdjust; 文件打不开时直接 Print 报错并把 m_dtPrevLoading 归零返回 false,这意味着回放会话中断,MT5 终端不会注入任何假棒。外汇与贵金属回放仅用于策略验证,实盘高杠杆风险依旧存在,切勿把回测顺滑度等同于实盘胜率。 装载 tick 走另一条路:LoadTicksReplay 打开 Market Replay\Ticks\ 下的 CSV,先跳 7 列读表头字符串拼成 szInfo,与 def_Header_Ticks 比对。宏 macroRemoveSec(A) 定义为 A - (A % 60),可把时间戳截到分钟级,便于和棒线时间对齐。若表头不符,后面的 for 循环体不会执行,数组不扩容,老 tick 数 old 保留原值。
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("Could not access the bars data file."); m_dtPrevLoading = class="num">0; class="kw">return false; } class="kw">return (!_StopFlag); } 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; if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) { ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray); old = m_Ticks.nTicks; for (class="type">int c0 = class="num">0; c0 < class="num">7; c0++) szInfo += FileReadString(file); if (szInfo != def_Header_Ticks) {
◍ 回放 tick 的逐行装载逻辑
把 CSV 里的成交 tick 灌回结构体,核心在 while 循环里逐字段读取。循环终止条件有三个:文件读完、已装载 tick 数逼近 INT_MAX-2、或终端发出 _StopFlag 停止信号,任一满足就跳出,避免历史数据过大时卡死 MT5。 下面这段是实际落盘读取的主干,建议直接开 MT5 新建脚本粘进去跑一份自己导出的 tick CSV 验证字段顺序: ArrayResize 每次多开一个 MqlTick 槽位,def_MaxSizeArray 作为预留扩容步长;szInfo 拼的是日期串加毫秒串,StringSubstr 取前 19 位做秒级时间、再取 20 位起 3 位做毫秒。bid/ask/last/volume_real/flags 全部按 FileReadString 顺序转类型写入。 装载后会做一条去重判断:若上一条 old 的 last、time、time_msc 与当前 tick 三者全同,则视为重复 tick 跳过。实盘导出的 EURUSD tick 在秒级重叠时可能出现这种冗余,过滤后回测重放更接近真实撮合。外汇与贵金属 tick 回放属高杠杆环境,重放结果仅代表历史数据概率特征,不等于未来行情倾向。
Print("File ", szFileNameCSV, ".csv is not a file with traded ticks."); class="kw">return false; Print("Loading replay ticks. Please wait..."); class="kw">while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - class="num">2)) && (!_StopFlag)) { ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + class="num">1), 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.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))