开发回放系统 市场模拟(第 11 部分):模拟器的诞生(I)·进阶篇
(2/3)·只用真实票据回放已到极限,如何用 1 分钟柱线反推锯齿走位造出可信模拟器
◍ 回放配置文件的读取入口
做行情回放前,先要把配置文件读进内存。下面这段逻辑负责打开 Market Replay 目录下的 CSV 配置,打不开就直接弹窗并报错退出,避免后续空跑。 文件句柄校验是最容易漏的一步:若 FileOpen 返回 INVALID_HANDLE,说明路径或编码不对,此时应当立刻 return false,而不是继续往下走。 读取阶段用一个 iStage 变量标记当前解析到哪一段,初始置 0;m_Ticks.Rate 数组先按 def_BarsDiary 预扩容,nRate 设 -1 表示尚未装入任何 tick。循环条件同时判断 FileIsEnding 与 _StopFlag,保证用户点停止时能马上断流。
class="type">char iStage; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Failed to open the\nconfiguration file.", "Market Replay", MB_OK); class="kw">return class="kw">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
配置指令如何被解析成阶段状态
这段逻辑处理配置文件中每行指令的识别:系统用 szInfo 字符串比对预设宏,命中 def_STR_FilesTicks 就把 iStage 置为 2,命中 def_STR_TicksToBars 置 3,命中 def_STR_BarsToTicks 置 4,三者皆不中则抛错并 break,说明配置写错一个词就会直接中断转录流程。 在 Transcription_INFO 分支里,只有当 szInfo 非空才进入 switch(iStage)。若 iStage 还是 0,代表前面没认出任何命令,直接报“配置文件中第 N 行命令无法识别”;若 iStage 已是 1,则调用 LoadPrevBars(szInfo) 去加载历史 Bar,返回 false 同样触发 macroERROR。 实盘意义在于:你在 MT5 里改配置文件时,阶段关键字必须和代码里的 def_STR_* 宏一字不差,否则脚本连历史数据都载不进来。外汇与贵金属杠杆高,脚本加载异常可能让你漏掉一段关键行情,建议先拿模拟盘跑通再上真仓。
if (szInfo == def_STR_FilesTicks) iStage = class="num">2; else if (szInfo == def_STR_TicksToBars) iStage = class="num">3; else if (szInfo == def_STR_BarsToTicks) iStage = class="num">4; else macroERROR(StringFormat("%s is not recognized in the system\nin line %d.", szInfo, iLine)); class="kw">break; case Transcription_INFO: if (szInfo != "") class="kw">switch (iStage) { case class="num">0: macroERROR(StringFormat("Command not recognized in line %d\nof the configuration file.", iLine)); class="kw">break; case class="num">1: if (!LoadPrevBars(szInfo)) macroERROR("");
「回放模式的四种分支处理」
在 MT5 历史回放引擎里,按模式编号走不同加载逻辑:case 1 直接 break 不加载;case 2 调 LoadTicksReplay(szInfo) 载入标准 tick 回放;case 3 带 false 参数调同一函数,可能用于跳过某些校验;case 4 走 LoadBarsToTicksReplay 把柱线重构成 tick 流。 任一分支加载失败就触发 macroERROR("") 中断,避免脏数据进回放。实盘外接这套逻辑时,外汇与贵金属品种的高波动可能在重构 tick 阶段放大滑点偏差,需自行在策略测试器里核对。 把下面这段直接贴进 EA 的 switch 块,改 szInfo 指向你的符号,就能在本地重跑对应模式。
class="kw">break; case class="num">2: if (!LoadTicksReplay(szInfo)) macroERROR(""); class="kw">break; case class="num">3: if (!LoadTicksReplay(szInfo, class="kw">false)) macroERROR(""); class="kw">break; case class="num">4: if (!LoadBarsToTicksReplay(szInfo)) macroERROR(""); class="kw">break; } class="kw">break;
◍ 文件收尾与循环退出判定
这段片段处在逐行读取逻辑之后,负责把文件句柄关掉并决定本轮处理是否继续。 FileClose(file) 释放了前面用 FileOpen 拿到的句柄;若忘了关,MT5 在高频重读脚本里可能很快堆到 64 个打开文件上限从而报错。 return (!_StopFlag) 把退出条件交还给调用层:当用户点停止或 EA 被移除时,_StopFlag 置真,取反后返回 false,循环自然断掉。 #undef macroERROR 只在函数域内撤掉之前定义的宏,避免污染后续编译单元,这种写法在多个指标共用报错宏时尤其必要。
iLine++;
}
FileClose(file);
class="kw">return (!_StopFlag);
class="macro">#undef macroERROR
}柱线配置只有四类,但票据最少要三次
动手写转换系统前,先弄清一件事:市面所有柱线形态看似无穷,其实可压缩成 4 种基本配置。明白这点,才不会在实现时漏掉分支,也不会造出多余的无用案例。 这里说的「票据(ticket)」是指一次真实的买卖事件,而「基差(tick)」是价格相对成交价的最小变动单位。股票 1 基差 = 0.01 点,美元期货 1 基差 = 0.5 点,指数期货 1 基差 = 5 点——别把俩词混了。 只发 1 次票据不现实,那仅代表买或卖,不是走位;2 次票据(开+收)也够不着真实路径。最小可用单位是 3 次票据,才能勉强映射出前文那 4 类配置里的某种走位。但 3 次不等于「涨 1 基差、跌 1 基差、再涨 1 基差」这种对称假设,流动性缺失时跳价路径完全可能歪掉。 举个实在例子:某 1 分钟柱线开盘后,因流动性薄,价格跳 3 基差、回 1 基差、尾盘收在别处。左边短横是真实基差步长,圆圈是两次票据间实际停顿价,绿线是真涨。仅用 OHLC 烛条去模拟,蓝线会扫过全部基差——和真实发生的根本不是一回事。 建模永远替代不了真实数据。用 1 分钟以上周期做回放或策略测试,外汇与贵金属这种高波动品种失真会更狠,所以能取的最短周期就是 1 分钟,别往上妥协。
「把逐笔报价压成可见的分钟柱」
回放系统要跑起来,第一步是让程序认得配置文件里多出来的段落标记。C_Replay 类得先定义几个字符串常量,比如 [BARS]、[TICKS]、[TICKS->BARS],它们就是配置数据分区的识别符,缺了这步系统根本不知道哪段该按什么规则解析。 解析发生在分析的第 5 阶段:捕获到信息项后,把变量名和数值拆开存进内部变量。用户写配置时不用管类型是 string、double 还是 int,代码里会自动选对应类型,对用户完全透明。 拆完先清掉和基本信息无关的内容,比如内部格式包装。任何无法理解或应用的东西都会触发返回 false,服务直接终止,MetaTrader 5 里会弹出原因弹窗。下面这段宏就是出错时干这事用的: #define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("An error occurred in line %d", iLine)), "Market Replay", MB_OK); return false; } 一个实用细节:可以关掉秒级数值的可视化,让每个模拟票据在图表上直接变成一根柱线。做法是删除票据里对应秒数的字段,否则每张 tick 都生成柱线,而你其实只想看 1 分钟柱像真的一样走。def_BarsDiary 设为 540 时,系统按这个日记容量缓冲柱线数据,方便离线回放观测。 配置里加上 [TICKS->BARS] 段并指定 POINTSPERTICK,再在 SetSymbolReplay 里用 FileOpen 读 Market Replay\\ 下的 CSV,就能在 MT5 里验证票据到柱线的转换是否如预期。外汇与贵金属回放仅作历史机制验证,实盘杠杆风险极高,转换模型不代表任何未来价格倾向。
class="macro">#define def_STR_FilesBar "[BARS]" class="macro">#define def_STR_FilesTicks "[TICKS]" class="macro">#define def_STR_TicksToBars "[TICKS->BARS]" class="macro">#define def_STR_BarsToTicks "[BARS->TICKS]" class="macro">#define def_STR_ConfigSymbol "[CONFIG]" class="macro">#define def_STR_PointsPerTicks "POINTSPERTICK" class="macro">#define def_Header_Bar "<DATE><TIME><OPEN><HIGH><LOW><CLOSE><TICKVOL><VOL><SPREAD>" class="macro">#define def_Header_Ticks "<DATE><TIME><BID><ASK><LAST><VOLUME><FLAGS>" class="macro">#define def_BarsDiary class="num">540 class="type">bool SetSymbolReplay(class="kw">const class="type">class="kw">string szFileConfig) { class="macro">#define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("An error occurred in line %d", iLine)), "Market Replay", MB_OK); class="kw">return class="kw">false; } class="type">int file, iLine; class="type">class="kw">string szInfo; class="type">char iStage; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Failed to open the\nconfiguration file.", "Market Replay", MB_OK);