开发回放系统 市场模拟(第 21 部分):外汇(II)·进阶篇
(2/3)· 从配置文件崩溃到资产跳价锁定,手把手拆掉回放模拟里的隐藏坑
「回放引擎里的零成交量首棒处理」
在 MT5 做 tick 级回放时,真实行情文件开头往往有一串 volume_real 为 0 的 tick,直接灌进自定义品种会留下一根无开盘价的怪棒。C_ConfigService 里用 FirstBarNULL 专门兜底:从 m_Ticks.Info 头部扫到第一个有真实成交量的 tick 之前,把所有 last 价都塞进同一根 rate[0].close。 它把 open/high/low 全部等同 close,tick_volume 与 real_volume 置 0,time 设为首 tick 时间减 60 秒,再调 CustomRatesUpdate(def_SymbolReplay, rate) 强行写一根「空白首棒」。随后 m_ReplayCount 归零,回放计数从这根假棒之后重新开始。 外汇与贵金属回放属于高杠杆高风险操作,这种首棒修正只解决数据衔接,不预示任何方向。开 MT5 把这段逻辑挂到你的回放类里,能避免自定义品种图表最左端出现无法绘制的空洞。
class="kw">inline class="type">void FirstBarNULL(class="type">void) { class="type">MqlRates rate[class="num">1]; for(class="type">int c0 = class="num">0; m_Ticks.Info[c0].volume_real == class="num">0; c0++) rate[class="num">0].close = m_Ticks.Info[c0].last; rate[class="num">0].open = rate[class="num">0].high = rate[class="num">0].low = rate[class="num">0].close; rate[class="num">0].tick_volume = class="num">0; rate[class="num">0].real_volume = class="num">0; rate[class="num">0].time = m_Ticks.Info[class="num">0].time - class="num">60; CustomRatesUpdate(def_SymbolReplay, rate); m_ReplayCount = class="num">0; }
回放配置文件的读取与阶段分流
做行情回放服务时,第一步是把配置文件读进内存并判断当前处于哪个解析阶段。下面这段逻辑直接打开 Market Replay 目录下的 CSV 配置,读不到句柄就立刻退出,避免后续空跑。 文件句柄拿到后,代码把Tick数组预扩容到 def_BarsDiary 大小,并把 nRate 置为 -1、首元素时间清零,相当于给回放缓冲做了冷启动。cError 与 cStage 双零初始化,说明此时既无错误也未进入任何定义段。 主循环依靠 FileIsEnding 与全局 _StopFlag 双条件退出,只要 cError 不为 0 也会中断。每一行先过 GetDefinition 判定是 DEFINE 还是 INFO:DEFINE 走 WhatDefine 解析并回写 cError;INFO 则按 cStage 分流,stage 0 遇到非空信息直接标错 2,stage 1 往 pBarsToPrev 挂文件名与行号,stage 2 才建 pTicksToReplay 容器。 在 MT5 里跑这段,重点看 FileOpen 的路径拼接和 cStage 的递增时机——若配置里 INFO 出现在 DEFINE 之前,cStage 仍为 0,会稳定触发 cError=2 并停服。外汇与贵金属回测涉及高杠杆与滑点风险,该逻辑仅保证文件解析次序正确,不预示任何收益。
class="type">int file; class="type">char cError, cStage; class="type">class="kw">string szInfo; class="type">bool bBarsPrev; if ((file = FileOpen("Market Replay\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { Print("Failed to open 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; cError = cStage = class="num">0; bBarsPrev = false; m_GlPrivate.Line = class="num">1; m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL; 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); break; case Transcription_INFO: if (szInfo != "") class="kw">switch (cStage) { case class="num">0: cError = class="num">2; break; case class="num">1: if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new C_Array(); (*m_GlPrivate.pBarsToPrev).Add(macroFileName, m_GlPrivate.Line); break; case class="num">2: if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new C_Array();
◍ 回放指令的分流与报错出口
宏文件按行读取后,系统用 switch 把指令归到 5 类容器:case 2 把重放 tick 序列压进 pTicksToReplay,case 3 把 tick 转 bar 的任务挂到 pTicksToBars,case 4 则进 pBarsToTicks;三者都先做 NULL 判断再 new C_Array(),避免重复分配。 case 5 调用 Configs(szInfo) 做配置解析,失败就把 cError 置为 7,这条路径决定了后续服务能否起得来。 文件读完先 FileClose(file),接着依次跑 Cmd_TicksToReplay、Cmd_BarsToTicks,并用返回值更新 bBarsPrev——只要任一条返回 true,bBarsPrev 就被改写。 收尾的 switch(cError) 是硬出口:cError=0 时若 m_Ticks.nTicks<=0 直接打印 No ticks to use 并置 -1;等于 1 或 2 则打印具体行号的错误描述,方便你开 MT5 看日志定位是哪一行宏写崩了。
(*m_GlPrivate.pTicksToReplay).Add(macroFileName, m_GlPrivate.Line); break; case class="num">3: if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new C_Array(); (*m_GlPrivate.pTicksToBars).Add(macroFileName, m_GlPrivate.Line); break; case class="num">4: if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new C_Array(); (*m_GlPrivate.pBarsToTicks).Add(macroFileName, m_GlPrivate.Line); break; case class="num">5: if (!Configs(szInfo)) cError = class="num">7; break; } break; }; m_GlPrivate.Line += (cError > class="num">0 ? class="num">0 : class="num">1); } FileClose(file); Cmd_TicksToReplay(cError); Cmd_BarsToTicks(cError); bBarsPrev = (Cmd_TicksToBars(cError) ? true : bBarsPrev); bBarsPrev = (Cmd_BarsToPrev(cError) ? true : bBarsPrev); class="kw">switch(cError) { case class="num">0: if (m_Ticks.nTicks <= class="num">0) { Print("No ticks to use. Closing the service..."); cError = -class="num">1; }else if (!bBarsPrev) FirstBarNULL(); break; case class="num">1 : Print("Command in line ", m_GlPrivate.Line, " could not be recognized by the system..."); break; case class="num">2 : Print("The contents of the line is unexpected for the system: ", m_GlPrivate.Line); break;
「回放指令里的逐文件加载与错误码分支」
这段内联函数群负责把外部指定的 tick 或 bar 数据源逐个喂进回放引擎。Cmd_TicksToReplay 遍历 pTicksToReplay 指针容器,只要 At() 返回空串就 break,否则调用 LoadTicks;若返回 0 条记录则把 cError 置为 4,说明该文件可能为空或路径失效。 Cmd_BarsToTicks 逻辑类似,但失败分支的 cError 是 6,对应 BarsToTicks() 返回 false 的情形——通常是品种周期不匹配。两个函数都在循环结束后 delete 掉容器指针,避免重复释放。 Cmd_TicksToBars 多了一个 bBarsPrev 标记,LoadTicks 的第二个参数传 false 表示不立即建 bar,成功才把 m_dtPrevLoading 记为非零时间戳并翻转标记。错误码 5 即代表该 tick 文件加载条数为 0。外汇与贵金属回测中这类静默错误常导致样本量虚高,建议在 MT5 策略测试器里单步跑一遍看 cError 实际落点。
class="kw">default : Print("Access error occurred in the files indicated in line: ", m_GlPrivate.Line); } class="kw">return (cError == class="num">0 ? !_StopFlag : false); class="macro">#undef macroFileName } class="kw">inline class="type">void Cmd_TicksToReplay(class="type">char &cError) { class="type">class="kw">string szInfo; if (m_GlPrivate.pTicksToReplay != NULL) { for (class="type">int c0 = class="num">0; (c0 < INT_MAX) && (cError == class="num">0); c0++) { if ((szInfo = (*m_GlPrivate.pTicksToReplay).At(c0, m_GlPrivate.Line)) == "") break; if (LoadTicks(szInfo) == class="num">0) cError = class="num">4; } class="kw">delete m_GlPrivate.pTicksToReplay; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void Cmd_BarsToTicks(class="type">char &cError) { class="type">class="kw">string szInfo; if (m_GlPrivate.pBarsToTicks != NULL) { for (class="type">int c0 = class="num">0; (c0 < INT_MAX) && (cError == class="num">0); c0++) { if ((szInfo = (*m_GlPrivate.pBarsToTicks).At(c0, m_GlPrivate.Line)) == "") break; if (!BarsToTicks(szInfo)) cError = class="num">6; } class="kw">delete m_GlPrivate.pBarsToTicks; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">bool Cmd_TicksToBars(class="type">char &cError) { class="type">bool bBarsPrev = false; class="type">class="kw">string szInfo; if (m_GlPrivate.pTicksToBars != NULL) { for (class="type">int c0 = class="num">0; (c0 < INT_MAX) && (cError == class="num">0); c0++) { if ((szInfo = (*m_GlPrivate.pTicksToBars).At(c0, m_GlPrivate.Line)) == "") break; if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == class="num">0) cError = class="num">5; else bBarsPrev = true; } class="kw">delete m_GlPrivate.pTicksToBars; }
历史数据回放的内存回收与分支调度
这段内联函数负责把离线准备好的 tick 与 bar 序列灌进回测环境,同时把临时文件对象及时 delete 掉,避免 MT5 策略测试器里内存悄悄爬升。 Cmd_BarsToPrev 先判断 m_GlPrivate.pBarsToPrev 是否为空;非空就按行读取预览文件描述,用 C_FileBars 加载,LoadPreView 返回 0 则把 cError 置为 3,否则标记 bBarsPrev 为 true。循环上限写的是 INT_MAX,实际靠空字符串跳出,属于防御式写法。 CMD_Array 用 switch(e1) 把四种回放类型映射到不同指针:eTickReplay、eTickToBar、eBarToTick、eBarPrev。若指针非空,同样逐行取 szInfo,按类型分流——例如 eTickReplay 走 LoadTicks,返回 0 则 cError = 4。 开 MT5 把这段贴进 EA 的私有方法区,故意让 pBarsToPrev 指向一个损坏路径,能复现 cError=3 的静默中断;外汇与贵金属回测本身高风险,错误码不抛异常容易让策略在残缺数据上跑出失真结果。
class="kw">inline class="type">bool Cmd_BarsToPrev(class="type">char &cError) { class="type">bool bBarsPrev = false; class="type">class="kw">string szInfo; C_FileBars *pFileBars; if (m_GlPrivate.pBarsToPrev != NULL) { for (class="type">int c0 = class="num">0; (c0 < INT_MAX) && (cError == class="num">0); c0++) { if ((szInfo = (*m_GlPrivate.pBarsToPrev).At(c0, m_GlPrivate.Line)) == "") break; pFileBars = new C_FileBars(szInfo); if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == class="num">0) cError = class="num">3; else bBarsPrev = true; class="kw">delete pFileBars; } class="kw">delete m_GlPrivate.pBarsToPrev; } class="kw">return bBarsPrev; } class="kw">inline class="type">bool CMD_Array(class="type">char &cError, eWhatExec e1) { class="type">bool bBarsPrev = false; class="type">class="kw">string szInfo; C_FileBars *pFileBars; C_Array *ptr = NULL; class="kw">switch (e1) { case eTickReplay: ptr = m_GlPrivate.pTicksToReplay; break; case eTickToBar : ptr = m_GlPrivate.pTicksToBars; break; case eBarToTick : ptr = m_GlPrivate.pBarsToTicks; break; case eBarPrev : ptr = m_GlPrivate.pBarsToPrev; break; } if (ptr != NULL) { for (class="type">int c0 = class="num">0; (c0 < INT_MAX) && (cError == class="num">0); c0++) { if ((szInfo = ptr.At(c0, m_GlPrivate.Line)) == "") break; class="kw">switch (e1) { case eTickReplay: if (LoadTicks(szInfo) == class="num">0) cError = class="num">4;
◍ 配置解析与回放加载的收口逻辑
上面这段是历史数据回放服务的尾部:switch 分支处理四种加载模式,Tick 转 Bar、Bar 转 Tick、读取预览 Bar 各有错误码(5、6、3),任一失败即打断 bBarsPrev 的置位。 C_ConfigService 在构造时把 m_ModelLoading 默认设为 1,这意味着若配置文件不显式写 LOADMODEL,引擎会走第一种加载模型。 配置解析靠 StringSplit(szInfo, '=', szRet) == 2 保证键值成对,再拿 trim 后的左值去比对 PATH、POINTSPERTICK 等五个固定字段;字段不匹配就落到 switch 默认分支,参数被静默忽略。 开 MT5 把这段嵌进 EA 时,建议先打印 szRet[0] 确认配置文件没多空格——很多加载失败其实只是等号左边带了不可见字符。
break; case eTickToBar : if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == class="num">0) cError = class="num">5; else bBarsPrev = true; break; case eBarToTick : if (!BarsToTicks(szInfo)) cError = class="num">6; break; case eBarPrev : pFileBars = new C_FileBars(szInfo); if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == class="num">0) cError = class="num">3; else bBarsPrev = true; class="kw">delete pFileBars; break; } } class="kw">delete ptr; } class="kw">return bBarsPrev; } class C_ConfigService : class="kw">protected C_FileTicks { class="kw">protected: class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">datetime m_dtPrevLoading; class="type">int m_ReplayCount, m_ModelLoading; C_ConfigService() :m_szPath(NULL), m_ModelLoading(class="num">1) { } class="kw">inline class="type">bool Configs(const class="type">class="kw">string szInfo) { const class="type">class="kw">string szList[] = { "PATH", "POINTSPERTICK", "VALUEPERPOINTS", "VOLUMEMINIMAL", "LOADMODEL" }; class="type">class="kw">string szRet[]; class="type">char cWho; if (StringSplit(szInfo, &class="macro">#x27;=&class="macro">#x27;, szRet) == class="num">2) { StringTrimRight(szRet[class="num">0]); StringTrimLeft(szRet[class="num">1]); for (cWho = class="num">0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[class="num">0]) break; class="kw">switch (cWho) {