开发回放系统 市场模拟(第 19 部分):必要的调整(基础篇)
「回放系统里的市场模拟校准」
在 MT5 回放系统第 19 部分的开发中,作者 Daniel Jose 于 2024 年 3 月 13 日提交了一次必要调整,核心是把历史 tick 流重新映射为可控的模拟行情序列,避免回放时跳变。原始提交记录显示该补丁在测试者板块获得 764 次浏览、0 条评论,说明改动偏内部重构、未引发外围讨论。 这类调整对外汇与贵金属回放训练很关键:若模拟行情的时间戳和真实 tick 间隔不一致,价格行为学的形态识别会被误导。MT5 终端内打开回放插件后,建议先核对「每秒 tick 数」与历史样本是否同分布,再开始手动重演。 别把正态当圣经 历史 tick 的重采样不等于真实波动,贵金属夜盘流动性断层仍可能让回放失效,交易前请认清高风险。
多资产回放系统为什么必须先做结构化
如果你只用回放/模拟系统操控单一资产,很多组织层的代码可以完全不进配置文件,系统照样跑。但真实场景里,交易者往往要同时喂若干种甚至成规模的外汇、贵金属资产数据库,这时候源代码的隐形能力就不够用了,得把结构摆到明处。 当前这套回放框架还覆盖不了显著推进流程必需的逻辑。把数据组织方式先改好,后面往里加新函数、新参数(比如研究某品种跳价特征时)才能以最小工作量完成。前作第17、18篇已经演示了如何向市场观察窗口加跳价,算是打好了底,但还缺一些具体拼图,本文就从数据结构改进动手。 外汇与贵金属杠杆高、滑点跳空频繁,回放系统若结构混乱,多资产回测结果可能严重失真。先把架子搭成通用型,后续适应不同品种时才不会反复返工。
◍ 把数据目录收拢到一处配置
回放与模拟系统里,目录若散落在多处代码,一旦数据库变大,漏写或错写路径就会带来两类后果:轻则系统弹警告说找不到数据,重则悄悄加载了错误文件参与计算。把目录统一在单一配置项里指定,这类失误概率会明显下降,但不可能归零。 做法是在类里加一个私有字符串变量 m_szPath,仅类内可见,外部无法覆盖。这样能挡住无意中被内部流程改值的坑——很多「代码为啥不按预期跑」的诡异 bug 就来自这种暗改。变量声明后,必须在构造函数里给一个已知初值,没赋值时也有兜底,避免未初始化引发更严重的问题。 配置解析侧,要把标签 PATH 以全大写加进命令数组,链条第二环对应索引 1。配置文件里写 PATH=<NEW PATH> 即可,改目录不必重新编译系统。下面这段代码是类内配置解析的核心片段,黄色标记处在实际工程里常用宏展开复用。 宏的思路是:用构造里的初值比对运行时的 m_szPath,若相等说明用户没在配置里覆盖,就用默认路径;若等于配置文件读到的串,就采用新地址。这样同一套加载函数能无缝切目录。 涉及外汇与贵金属历史数据回放时,目录组织建议按品种和周期分桶,否则大库检索会拖慢模拟效率,且数据错配风险偏高。
<span class="keyword">class="kw">private</span> : <span class="keyword">enum</span> eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE}; <span class="keyword">class="type">class="kw">string</span> m_szPath; <span class="keyword">class="kw">inline</span> <span class="keyword">class="type">bool</span> Configs(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> szInfo) { <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> szList[] = { <span class="class="type">class="kw">string">"POINTSPERTICK"</span>, <span class="class="type">class="kw">string">"PATH"</span> }; <span class="keyword">class="type">class="kw">string</span> szRet[]; <span class="keyword">class="type">char</span> cWho; <span class="keyword">if</span> (<span class="functions">StringSplit</span>(szInfo, <span class="class="type">class="kw">string">&class="macro">#x27;=&class="macro">#x27;</span>, szRet) == <span class="number">class="num">2</span>) { <span class="functions">StringTrimRight</span>(szRet[<span class="number">class="num">0</span>]); <span class="functions">StringTrimLeft</span>(szRet[<span class="number">class="num">1</span>]); <span class="keyword">for</span> (cWho = <span class="number">class="num">0</span>; cWho < <span class="functions">ArraySize</span>(szList); cWho++) <span class="keyword">if</span> (szList[cWho] == szRet[<span class="number">class="num">0</span>]) <span class="keyword">class="kw">break</span>; <span class="keyword">class="kw">switch</span> (cWho) { <span class="keyword">case</span> <span class="number">class="num">0</span>: m_PointsPerTick = <span class="functions">StringToDouble</span>(szRet[<span class="number">class="num">1</span>]); <span class="keyword">class="kw">return</span> <span class="macro">true</span>; <span class="keyword">case</span> <span class="number">class="num">1</span>: m_szPath = szRet[<span class="number">class="num">1</span>];
「回放配置服务的文件读取与初始化」
在 MT5 里做行情回放,第一步是把 CSV 配置读进服务类。下面这段 C_ConfigService 的 SetSymbolReplay 方法,展示了从打开文件到预分配 tick 数组的完整路径,直接决定了后续回放能不能跑起来。
| 代码先拼出文件路径宏 macroFileName,若 m_szPath 为空则只用文件名,否则补上 '\\' 分隔。随后用 FileOpen 以 FILE_CSV | FILE_READ | FILE_ANSI 打开 Market Replay 目录下的配置,句柄等于 INVALID_HANDLE 就打印失败并 return false,这一步在外盘高波动时段可能因文件占用而失败,概率不低。 |
|---|
打开成功后立刻 ArrayResize(m_Ticks.Rate, def_BarsDiary) 预扩容量,并把 m_Ticks.nRate 置为 -1、首元素 time 清 0,iLine 从 1 计起。主循环靠 FileIsEnding 与 _StopFlag 双条件退出,cError 非 0 也断,说明解析出错即停。 循环内每行先 FileReadString 丢给 GetDefinition,返回 Transcription_DEFINE 走 WhatDefine 校验,返回 Transcription_INFO 则按 cStage 分支处理。这种状态机写法让你在 MT5 里改配置结构时,只需动 cStage 分支而不碰文件 IO。
class="kw">return true; } Print("Variable >>", szRet[class="num">0], "<< undefined."); }else Print("Definition of configuratoin >>", szInfo, "<< invalid."); class="kw">return false; } C_ConfigService() :m_szPath(NULL) { } class="type">bool SetSymbolReplay(class="kw">const class="type">class="kw">string szFileConfig) { class="macro">#define macroFileName((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo) class="type">int file, iLine; class="type">char cError, cStage; class="type">class="kw">string szInfo; class="type">bool bBarsPrev; C_FileBars *pFileBars; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { Print("Failed to open the configuration file [", szFileConfig, "]. Closing the service..."); class="kw">return false; } Print("Loading ticks 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; iLine = class="num">1; cError = cStage = class="num">0; bBarsPrev = false; class="kw">while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == class="num">0)) { class="kw">switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: cError = (WhatDefine(szInfo, cStage) ? class="num">0 : class="num">1); class="kw">break; case Transcription_INFO: if (szInfo != "") class="kw">switch (cStage) {
分模式加载的历史数据容错分支
这段 switch 落在文件解析主循环里,用 case 0~5 区分五种历史数据载入路径,每种失败都会把 cError 置成 2~7 的对应码,方便上层定位断点。 case 0 直接给 cError=2 并 break,一般代表前置模式判断未通过;case 1 用 C_FileBars 读预览,LoadPreView 返回 0 则 cError=3,否则 bBarsPrev=true 标记已有前 bars。 case 2 与 case 3 都调 LoadTicks,区别在后者传 false 且不强制覆盖,返回 0 分别置 cError=4/5;case 4 走 BarsToTicks 回退,失败 cError=6;case 5 校验 Configs,失败 cError=7。 循环末行 iLine += (cError>0 ? 0 : 1) 说明只有无错才推进行号,FileClose 后外层再按 cError 决定补读还是弃文件。外汇与贵金属历史 tick 常有缺口,这种分段错误码能在 MT5 里帮你快速看出是哪一类载入挂了。
case class="num">0: cError = class="num">2; class="kw">break; case class="num">1: pFileBars = new C_FileBars(macroFileName); if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == class="num">0) cError = class="num">3; else bBarsPrev = true; class="kw">delete pFileBars; class="kw">break; case class="num">2: if (LoadTicks(macroFileName) == class="num">0) cError = class="num">4; class="kw">break; case class="num">3: if ((m_dtPrevLoading = LoadTicks(macroFileName, false)) == class="num">0) cError = class="num">5; else bBarsPrev = true; class="kw">break; case class="num">4: if (!BarsToTicks(macroFileName)) cError = class="num">6; class="kw">break; case class="num">5: if (!Configs(szInfo)) cError = class="num">7; class="kw">break; } class="kw">break; }; iLine += (cError > class="num">0 ? class="num">0 : class="num">1); } FileClose(file); class="kw">switch(cError) { case class="num">0: if (m_Ticks.nTicks <= class="num">0) {