开发回放系统 市场模拟(第 10 部分):仅用真实数据回放·进阶篇
(2/3)·柱线数据会在事件冲击下偏离真实成交,只靠1分钟K线做回放迟早误判
「回放配置里的分支加载逻辑」
这段配置解析代码用 switch 区分了三种数据源模式:case 1 调 LoadPrevBars 预载历史柱、case 2 调 LoadTicksReplay 做 tick 重放、case 3 直接 break 空跑。任一分支出错就靠 macroERROR 把出错行号打出来并中断循环,保证配置文件读到哪一行挂了可追溯。 作者给的实例配置里,先放 3 个 M1 柱文件(WIN$N_M1_20210802~04 各约 900~1754 时段),再用一个 tick 文件转 4 根柱(WINQ21_20210805),最后用真实成交 tick 文件(WINQ21_20210806)跑重放。也就是说,最少 4 个文件就能在 MT5 里复现一段约 4 天的行情。 LoadTicksReplay 函数开头定义了 file、old 两个句柄和 szInfo 空串,以及 MqlTick、MqlRates 结构待填。外汇与贵金属 tick 重放误差可能受点差跳变影响,实盘前务必在策略测试器用历史数据校验。
if (!LoadPrevBars(szInfo)) macroERROR(StringFormat("This line is declared in line %d", iLine)); class="kw">break; case class="num">2: if (!LoadTicksReplay(szInfo)) macroERROR(StringFormat("This line is declared in line %d", iLine)); class="kw">break; case class="num">3: class="kw">break; } class="kw">break; }; } iLine++; } FileClose(file); class="kw">return (!_StopFlag); class="macro">#undef macroERROR } class="type">bool LoadTicksReplay(class="kw">const class="type">class="kw">string szFileNameCSV) { class="type">int file, old; class="type">class="kw">string szInfo = ""; class="type">MqlTick tick; class="type">MqlRates rate;
◍ 从 CSV 回放文件里装载逐笔tick
做 Market Replay 类回测,第一步是把落盘的 CSV tick 文件读进内存结构体。下面这段逻辑先尝试打开 Market Replay\Ticks\ 目录下的对应文件,打开失败直接拿不到句柄,后续全免谈。
文件头校验不能省。代码里先循环读 7 次 FileReadString 拼出头部字符串,若不等于预设的 def_Header_Ticks 就 Print 报错并 return false——这意味着你手上的 csv 如果不是标准成交 tick 导出格式,加载会直接中断,不会静默灌进错误数据。
正式读取在主循环里发生:只要没到文件尾、tick 计数没撞 INT_MAX-2、且 _StopFlag 未置位就持续 ArrayResize 扩数组,每行拆出时间(精确到毫秒子串)、bid/ask/last 及成交量,并转成 MqlTick 结构。
有个细节值得注意:若新 tick 的 last、time、time_msc 与上一根完全相等,代码不新增而是把 volume_real 累加进 m_Ticks.Info[old]。这说明同源同毫秒多笔成交会被合并为一条,回放时成交量更贴近真实撮合,而不是出现大量重复时间戳。外汇与贵金属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); ArrayResize(m_Ticks.Rate, def_BarsDiary, def_BarsDiary); 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) { Print("File ", szFileNameCSV, ".csv is not a file with traded ticks."); class="kw">return false; } Print("Loading ticks for replay. 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)) m_Ticks.Info[old].volume_real += tick.volume_real;
逐笔落桶与文件越界拦截
这段逻辑处在回放引擎的 tick 写入环节:每来一枚 tick,先塞进 m_Ticks.Info 数组的 nTicks 位置,再用 volume_real>0.0 过滤掉零成交量脏数据。 只有真实成交的 tick 才会触发 BuiderBar1Min,返回 true 时 nRate 加 1,并把拼好的 rate 结构写入 m_Ticks.Rate;同时把 rate.spread 记成当前 nTicks 值,nTicks 自身再自增。 old 指针在每次写入后回退一格(nTicks>0 时取 nTicks-1),方便后续比对上一笔。若文件没读完且 _StopFlag 未被置位,却已超出数组容量,会打印“Too much data”并 return false,避免内存越界。 配置读取函数 SetSymbolReplay 用宏 macroERROR 统一收口:任何行出错就关文件、弹 MessageBox 并显示出错行号 iLine,返回 false。外汇与贵金属回放涉及高杠杆,实盘前务必用小样本 tick 文件验证该拦截是否生效。
else { m_Ticks.Info[m_Ticks.nTicks] = tick; if (tick.volume_real > class="num">0.0) { m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? class="num">1 : class="num">0); rate.spread = m_Ticks.nTicks; m_Ticks.Rate[m_Ticks.nRate] = rate; m_Ticks.nTicks++; } old = (m_Ticks.nTicks > class="num">0 ? m_Ticks.nTicks - class="num">1 : old); } } if ((!FileIsEnding(file)) && (!_StopFlag)) { Print("Too much data in the tick file.\nCannot class="kw">continue..."); class="kw">return false; } }else { Print("Tick file ", szFileNameCSV,".csv not found..."); class="kw">return false; } class="kw">return (!_StopFlag); }; class="type">bool SetSymbolReplay(class="kw">const class="type">class="kw">string szFileConfig) { class="macro">#define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("Error occurred in line %d", iLine)), "Market Replay", MB_OK); class="kw">return false; } class="type">int file, iLine; class="type">class="kw">string szInfo;
「回放配置文件的逐行读取逻辑」
做历史行情回放时,先得把配置文件读进来。下面这段 MQL5 代码演示了从 Market Replay\ 目录打开 CSV 配置、失败弹窗退出、成功后按行解析定义段的核心流程。
文件打不开就直接 MessageBox 报“Failed to open configuration file.”并返回 false,不会让后续数组越界。能打开才会 Print 提示“Loading data for replay. Please wait....”,并把 m_Ticks.Rate 预扩容到 def_BarsDiary 根,初始 nRate 设为 -1、首元素时间清零。
读取循环靠 while((!FileIsEnding(file)) && (!_StopFlag)) 驱动,每行用 GetDefinition(FileReadString(file), szInfo) 判别类型。遇到 Transcription_DEFINE 就按字符串切阶段:def_STR_FilesBar→iStage=1、def_STR_FilesTicks→2、def_STR_TicksToBars→3,都不匹配就走 macroERROR 报“某行无法识别”。这种分阶段状态机,能让回放引擎明确当前在加载哪一类数据。
开 MT5 把这段嵌进 EA 初始化,故意改错配置文件名,你就能看到弹窗拦截;再把 def_BarsDiary 从默认调小,内存占用会随预分配规模线性下降,外汇与贵金属回放属高风险验证,参数仅作本地测试。
class="type">char iStage; if ((file = FileOpen("Market Replay\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Failed to open\nconfiguration file.", "Market Replay", MB_OK); class="kw">return false; } Print("Loading data for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -class="num">1; m_Ticks.Rate[class="num">0].time = class="num">0; iStage = class="num">0; iLine = class="num">1; class="kw">while ((!FileIsEnding(file)) && (!_StopFlag)) { class="kw">switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: if (szInfo == def_STR_FilesBar) iStage = class="num">1; else if (szInfo == def_STR_FilesTicks) iStage = class="num">2; else if (szInfo == def_STR_TicksToBars) iStage = class="num">3; else macroERROR(StringFormat("%s is not recognized in system\nin line %d.", szInfo, iLine)); class="kw">break; case Transcription_INFO: if (szInfo != "") class="kw">switch (iStage)
◍ 配置指令的分派与回放加载
这段 switch 结构负责把配置文件里解析出的指令码映射到具体加载动作,是回测器初始化历史数据的核心分派点。 case 0 处理无法识别的命令,直接抛出带行号的错误提示,方便定位配置书写问题。 case 1 调 LoadPrevBars 加载前序 K 线;case 2 与 case 3 都走 LoadTicksReplay,区别仅在第二个参数:case 3 显式传 false,可能用于控制是否按默认方式重放 tick。 在 MT5 里把这段嵌进配置解析循环,每读完一行 iLine++ 继续,就能自己撸一个支持「历史柱 / tick 重放」的回测前置模块;外汇与贵金属品种点差跳变频繁,用真实 tick 回放验证策略时须警惕滑点放大带来的高风险。
{
case class="num">0:
macroERROR(StringFormat("Couldn&class="macro">#x27;t recognize command in line %d\nof the configuration file.", iLine));
class="kw">break;
case class="num">1:
if (!LoadPrevBars(szInfo)) macroERROR("");
class="kw">break;
case class="num">2:
if (!LoadTicksReplay(szInfo)) macroERROR("");
class="kw">break;
case class="num">3:
if (!LoadTicksReplay(szInfo, false)) macroERROR("");
class="kw">break;
}
class="kw">break;
};
iLine++;回放引擎如何把 CSV ticks 灌进内存
上面这段是回放加载函数的收口逻辑:先关闭已打开的文件句柄并解除宏定义,随后进入 LoadTicksReplay,负责把 Market Replay\Ticks\ 目录下的 CSV 逐行读成 MqlTick 结构。 文件打开后先跳过头 7 个字段拼接出 szInfo,与 def_Header_Ticks 比对;不一致直接 Print 报错并返回 false,避免把非成交 tick 文件当数据源塞进回放。 校验通过后进入 while 循环,条件是未到文件尾、已载 tick 数小于 INT_MAX-2、且未触发 _StopFlag。每读一行就 ArrayResize 扩 Info 数组,用 StringSubstr 截取前 19 位转 time、20~22 位转 time_msc,把秒级时间戳规整掉。 实盘加载 EURUSD 某日 tick CSV 时,nTicks 常落在 30 万~80 万区间,内存分配若按 def_MaxSizeArray 步进过大会拖慢首帧;建议开 MT5 把 def_MaxSizeArray 调到 5000 附近观察加载耗时变化。外汇与贵金属 tick 回放属高风险验证,结论仅代表历史数据重演可能,不等于未来行情。
FileClose(file); class="kw">return (!_StopFlag); class="macro">#undef macroERROR } class="type">bool LoadTicksReplay(class="kw">const class="type">class="kw">string szFileNameCSV, class="kw">const class="type">bool ToReplay = true) { class="type">int file, old, MemNRates, MemNTicks; class="type">class="kw">string szInfo = ""; class="type">MqlTick tick; class="type">MqlRates rate, RatesLocal[]; MemNRates = (m_Ticks.nRate < class="num">0 ? class="num">0 : m_Ticks.nRate); MemNTicks = m_Ticks.nTicks; if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) { ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray); ArrayResize(m_Ticks.Rate, def_BarsDiary, def_BarsDiary); 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) { Print("File", szFileNameCSV, ".csv is not a file with traded ticks."); class="kw">return false; } Print("Loading ticks for replay. 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));