开发回放系统(第 63 部分):玩转服务(四)·综合运用
「回放引擎里的真实量判定与数组扩容」
在 MT5 自定义回放器初始化阶段,代码通过 real_volume 是否大于 0 来区分交易所品种与外汇品种:当最新一根 K 线的真实成交量非零时,绘图模式切到 PRICE_EXCHANGE,否则落到 PRICE_FOREX。这一行直接决定后续 tick 模拟走哪套报价逻辑,外汇/贵金属盘后无真实成交量时大概率走 PRICE_FOREX 分支。 行情数组的预留空间也有讲究:若已缓存 nRate 根 K 线,则 ArrayResize 按 nRate+3 扩,否则用 def_BarsDiary 常量兜底。每来一根新 rate[0] 就先 ++nRate 再写入,保证索引连续不越界。 模拟器对象 pSimulator 在首次初始化时 new 出来,之后每个 tick 调用 Simulation() 拿返回条数 iRet,再用 for 循环把 local[] 里的逐笔 tick 塞进 m_Ticks.Info,容量按 nTicks+1 动态扩。循环末尾统一 ArrayFree(local)、delete 指针,并把 bTickReal 置 false——说明这批 tick 是合成的,不是交易所原始流。 函数最终返回判定依赖三个条件:未触发 _StopFlag、tick 总数相比进入前确有增加、且 iRet>0。三者同时成立才认为本批加载有效,任一不满足就中断回放。外汇与贵金属合成回放仅用于策略验证,实盘价差与滑点风险仍偏高。
m_Ticks.ModePlot = (rate[class="num">0].real_volume > class="num">0 ? PRICE_EXCHANGE : PRICE_FOREX); pSimulator = new C_Simulation(SetSymbolInfos()); bInit = true; ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > class="num">0 ? m_Ticks.nRate + class="num">3 : def_BarsDiary), def_BarsDiary); m_Ticks.Rate[++m_Ticks.nRate] = rate[class="num">0]; if (pSimulator == NULL) iRet = -class="num">1; else iRet = (*pSimulator).Simulation(rate[class="num">0], local, MaxTickVolume); if (iRet < class="num">0) break; for (class="type">int c0 = class="num">0; c0 <= iRet; c0++) { ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + class="num">1), def_MaxSizeArray); m_Ticks.Info[m_Ticks.nTicks++] = local[c0]; } ArrayFree(local); class="kw">delete pFileBars; class="kw">delete pSimulator; m_Ticks.bTickReal = class="kw">false; class="kw">return ((!_StopFlag) && (iMem != m_Ticks.nTicks) && (iRet > class="num">0));
◍ tick超限时的回放重采样
把真实 tick 流直接聚合成 1 分钟 bar,遇到单根 bar 的 tick_volume 超过 MaxTickVolume 时,原始聚合会失真。代码里用 ToReplay 开关配合 m_Ticks.Rate[m_Ticks.nRate].tick_volume > MaxTickVolume 判断,触发后不走普通追加,而是进模拟重采样分支。 重采样由 C_Simulation 实例接管:先 ArrayResize(TicksLocal, def_MaxSizeArray) 开一块本地 tick 缓冲,再调 (*pSimulator).Simulation(...) 按 nDigits 精度把该根 bar 拆回成不超过 MaxTickVolume 的细分 tick,返回有效条数 c1。c1>0 才用 ArrayCopy 把 TicksLocal 写回 m_Ticks.Info 的 nShift 位置,并顺推 nShift。 注意 nShift 与循环变量 c0 分离:正常情况 nShift==c0 时直接搬移 Info;一旦重采样插入了 c1 条,后续偏移就由 MemShift 暂存再续上。外汇与贵金属 tick 在流动性突变时可能瞬间爆量,这种重采样逻辑能降低回测与实盘偏差,但高频重采样本身也会放大 CPU 开销,属于概率层面的修复而非保证。 开 MT5 把 def_MaxSizeArray 和 MaxTickVolume 打印出来,看你的品种在新闻秒里单 bar tick 数是否经常触顶,再决定要不要开 ToReplay。
class="type">MqlTick TicksLocal[]; class="type">bool bNew; MemNRates = (m_Ticks.nRate < class="num">0 ? class="num">0 : m_Ticks.nRate); nShift = MemNTicks = m_Ticks.nTicks; if (!Open(szFileNameCSV)) class="kw">return class="num">0; if (!ReadAllsTicks()) class="kw">return class="num">0; rate.time = class="num">0; nDigits = SetSymbolInfos(); m_Ticks.bTickReal = true; for (class="type">int c0 = MemNTicks, c1, MemShift = nShift; c0 < m_Ticks.nTicks; c0++, nShift++) { if (nShift != c0) m_Ticks.Info[nShift] = m_Ticks.Info[c0]; if (!BuildBar1Min(c0, rate, bNew)) class="kw">continue; if (bNew) { if ((m_Ticks.nRate >= class="num">0) && (ToReplay)) if (m_Ticks.Rate[m_Ticks.nRate].tick_volume > MaxTickVolume) { nShift = MemShift; ArrayResize(TicksLocal, def_MaxSizeArray); C_Simulation *pSimulator = new C_Simulation(nDigits); if ((c1 = (*pSimulator).Simulation(m_Ticks.Rate[m_Ticks.nRate], TicksLocal, MaxTickVolume)) > class="num">0) nShift += ArrayCopy(m_Ticks.Info, TicksLocal, nShift, class="num">0, c1); class="kw">delete pSimulator; ArrayFree(TicksLocal); if (c1 < class="num">0) class="kw">return class="num">0; } MemShift = nShift; ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > class="num">0 ? m_Ticks.nRate + class="num">2 : def_BarsDiary), def_BarsDiary);
回放模式下如何把分笔落进自定义序列
这段收尾逻辑只在不回放(!ToReplay)时触发,负责把内存里累积的 rate 刷到自定义品种。第 209 行先用 ArrayResize 把 RatesLocal 缩到 (m_Ticks.nRate - MemNRates) 的长度,也就是本次新增的 K 线数量;若 MemNRates 为 0,则 nRate 在 213 行被重置为 -1,表示缓冲清空。
- 行 ArrayCopy 从 m_Ticks.Rate 全量拷进本地数组,211 行调 CustomRatesUpdate 按 def_SymbolReplay 推给图表。212 行把返回时间设为当前最后一根 rate 的 time,供上层判断推进到哪一刻。
- 行是回放分支:若 ToReplay 为真,不写盘,只把 nTicks 设为 nShift,留待下一轮继续喂 tick。218 行统一返回 dtRet,调用方拿它决定下次抓取的起始时间。
在 MT5 里接这段时,重点看 211 行的 def_SymbolReplay 是否已在 SymbolCreate 中建好,否则 CustomRatesUpdate 会静默失败,图表不动。外汇与贵金属tick回放属高杠杆高风险操作,缓冲逻辑出错可能重复发单,建议先用 EURUSD 极小周期验证 nRate 计数。
};
m_Ticks.Rate[(m_Ticks.nRate += (bNew ? class="num">1 : class="num">0))] = rate;
}
if (!ToReplay)
{
ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
ArrayCopy(RatesLocal, m_Ticks.Rate, class="num">0, class="num">0);
CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
dtRet = m_Ticks.Rate[m_Ticks.nRate].time;
m_Ticks.nRate = (MemNRates == class="num">0 ? -class="num">1 : MemNRates);
m_Ticks.nTicks = MemNTicks;
ArrayFree(RatesLocal);
}else m_Ticks.nTicks = nShift;
class="kw">return dtRet;
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline stInfoTicks GetInfoTicks(class="type">void) const
{
class="kw">return m_Ticks;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef def_MaxSizeArray
class=class="str">"cmt">//+------------------------------------------------------------------+「把最大分时报价数交还给配置文件」
回放/模拟器早期版本只能靠硬编码决定一根分钟柱里塞多少分时报价,现在改成读配置文件,用户不写就用编译时默认值,写了就覆盖。示例配置第七行 MaxTicksPerBar = 2800 就是新增键:老版程序会报“无法识别的参数”,新版能直接解释。
这个设置完全可选。每个品种的配置文件相互独立,你可以给 WDO 写 2800,给 XAUUSD 写别的,直到 MT5 图表渲染流畅、回放不卡。外汇和贵金属点差跳变频繁,分时报价数设太高会拖慢渲染,设太低可能丢失价格细节,都属于高风险调参。
代码层面改动极小。C_ConfigService 类第 19 行声明 MaxTickVolume 变量,第 185 行构造函数里赋默认值;第 63 行那个键数组里加一项,第 70 行定义新键 MAXTICKSPERBAR(必须全大写),第 75 行用库函数按等号拆键值。第 79 行在数组里搜索引,本例新键索引是 6,所以第 107 行用整数转换函数把配置值接进来。
别把硬编码当护身符
原来 Defines.mqh 里的 def_MaxTicksVolume 已删掉,因为配置接管了来源。你若留着也能编过,但后续版本可能消失,别奇怪。第 157、160、163 行把值传给真正干活的底层类,全程没加新逻辑,只是透传。
第 108 行转成整数后只检查是否为正,不验证上限。若你填个离谱值,模拟过程可能直接崩,不会提前报错。动手前先开 MT5 用 2800 跑一遍 WDO 的 M1 回放,看图表是否顺滑再决定加减。
[Config] Path = WDO PointsPerTick = class="num">0.5 ValuePerPoints = class="num">5.0 VolumeMinimal = class="num">1.0 Account = NETTING MaxTicksPerBar = class="num">2800 [Bars] WDON22_M1_202206140900_202206141759 [ Ticks -> Bars] [ Bars -> Ticks ] [Ticks] WDON22_202206150900_202206151759 class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Support\C_FileBars.mqh" class="macro">#include "Support\C_FileTicks.mqh" class="macro">#include "Support\C_Array.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class C_ConfigService : class="kw">protected C_FileTicks { class="kw">protected: class="kw">private : enum eWhatExec {eTickReplay, eBarToTick, eTickToBar, eBarPrev}; enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE}; class="kw">struct st001 { C_Array *pTicksToReplay, *pBarsToTicks, *pTicksToBars, *pBarsToPrev; class="type">int Line, MaxTickVolume; class="type">bool AccountHedging; class="type">char ModelLoading;
◍ 回放首根 K 线的零成交量补点
在 MT5 自建回放品种时,交易所行情(PRICE_EXCHANGE)的开盘首 tick 常常 real_volume 为 0,直接喂给 CustomRatesUpdate 会让首根 bar 缺失或形态异常。下面这段内联函数专门在回放前补一根「昨天的同级收盘」虚拟 bar,把 open/high/low/close 全压成同一价、tick_volume 与 real_volume 置 0。 FirstBarNULL 先按 ModePlot 判断行情类型,再用 for 循环跳过 volume_real == 0 的前导 tick,定位到第一个有真实成交的 Info[c0]。随后把 rate[0].close 取 last(交易所)或 bid(做市),其余 OHLC 全等,time 则取 macroRemoveSec 去秒后的时间再减 86400 秒(即往前推一整天)。 最后一行 CustomRatesUpdate(def_SymbolReplay, rate) 把这根零量 bar 写进回放符号。实盘接外汇或贵金属时需注意:这类回放仅还原历史 tick 流,杠杆与滑点未建模,重放结果不代表未来概率,品种本身仍属高风险。 GetDefinition 则处理配置行解析:以 # 或 [ 开头的行被识别为 Transcription_INFO 注释或段标记,其余去空格转大写后原样返回。两者都放在私有结构后的 inline 区,编译期直接展开,不占调用栈。
class="type">class="kw">string szPath; } m_GlPrivate; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void FirstBarNULL(class="type">void) { class="type">MqlRates rate[class="num">1]; class="type">int c0 = class="num">0; for(; (GetInfoTicks().ModePlot == PRICE_EXCHANGE) && (GetInfoTicks().Info[c0].volume_real == class="num">0); c0++); rate[class="num">0].close = (GetInfoTicks().ModePlot == PRICE_EXCHANGE ? GetInfoTicks().Info[c0].last : GetInfoTicks().Info[c0].bid); 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 = macroRemoveSec(GetInfoTicks().Info[c0].time) - class="num">86400; CustomRatesUpdate(def_SymbolReplay, rate); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline eTranscriptionDefine GetDefinition(const class="type">class="kw">string &In, class="type">class="kw">string &Out) { class="type">class="kw">string szInfo; szInfo = In; Out = ""; StringToUpper(szInfo); StringTrimLeft(szInfo); StringTrimRight(szInfo); if (StringSubstr(szInfo, class="num">0, class="num">1) == "#") class="kw">return Transcription_INFO; if (StringSubstr(szInfo, class="num">0, class="num">1) != "[") { Out = szInfo; class="kw">return Transcription_INFO; } for (class="type">int c0 = class="num">0; c0 < StringLen(szInfo); c0++) if (StringGetCharacter(szInfo, c0) > &class="macro">#x27; &class="macro">#x27;)
配置文件里的键值怎么落进全局结构
这段内联函数 Configs 负责把外部传入的一行配置字符串写进全局私有结构。它先定义了一个固定白名单 szList,包含 7 个键:PATH、POINTSPERTICK、VALUEPERPOINTS、VOLUMEMINIMAL、LOADMODEL、ACCOUNT、MAXTICKSPERBAR,任何不在表中的键都会被忽略。 函数用 StringSplit 按 '=' 切分,要求切出来的数组长度严格等于 2,否则直接不处理。随后对左右两侧分别做 StringTrimRight 和 StringTrimLeft,去掉首尾空格,避免 ' PATH = C:/' 这类写法污染路径。 匹配逻辑是用 for 循环遍历 szList,找到对应下标 cWho 后进入 switch。case 0 即 PATH 键,把右侧值赋给 m_GlPrivate.szPath 并返回 true。其余 6 个键在后续 case 中按同样方式落位,写错键名时 cWho 会等于 ArraySize 结果(此处为 7),switch 不命中,函数默认不返回 true。 在 MT5 里验证时,可故意传 'VOLUMEMINIMAL=0.01' 和 'volumeminimal=0.01' 两行,后者因大小写不符白名单会静默失效,这是调试配置加载时容易踩的坑。
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", "ACCOUNT", "MAXTICKSPERBAR" }; 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) { case class="num">0: m_GlPrivate.szPath = szRet[class="num">1]; class="kw">return true; case class="num">1:
「回放品种的交易参数落地」
上面这段是回放引擎里解析配置字符串后,往自定义品种写交易属性的核心分支。case 1 到 3 分别把 tick size、tick value、volume step 从文本转成 double 再塞进 CustomSymbolSetDouble,直接决定回测时最小报价单位和手数步长。 case 4 处理模型加载模式:先把字符串转成整型,再用一行三元表达式把越界值(小于 1 或大于 4)强制夹回 1,避免后面加载历史数据时用错模式导致静默失败。 case 5 是账户类型识别,只认 HEDGING 和 NETTING 两种;遇到其他字符串会 Print 报错并返回 false,这里原代码写的 'is not invalid' 其实是笔误,语义上应是无效输入被拒。外汇与贵金属回放测试本身属高风险验证,参数错配可能让成交逻辑完全偏离实盘。 开 MT5 把这段贴进 EA 的 config 解析函数,故意传一个 'ABC' 给 case 5,能立刻看到终端报错并中断初始化,比读文档直观。
CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, StringToDouble(szRet[class="num">1])); class="kw">return true; case class="num">2: CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, StringToDouble(szRet[class="num">1])); class="kw">return true; case class="num">3: CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, StringToDouble(szRet[class="num">1])); class="kw">return true; case class="num">4: m_GlPrivate.ModelLoading = StringInit(szRet[class="num">1]); m_GlPrivate.ModelLoading = ((m_GlPrivate.ModelLoading < class="num">1) && (m_GlPrivate.ModelLoading > class="num">4) ? class="num">1 : m_GlPrivate.ModelLoading); class="kw">return true; case class="num">5: if (szRet[class="num">1] == "HEDGING") m_GlPrivate.AccountHedging = true; else if (szRet[class="num">1] == "NETTING") m_GlPrivate.AccountHedging = class="kw">false; else { Print("Entered account type is not invalid."); class="kw">return class="kw">false; } class="kw">return true; case class="num">6:
◍ 配置段解析与执行分支判定
这段逻辑负责把外部传入的配置串拆成键值对,并据此决定引擎跑在哪种数据模式上。第108行把切分后索引1的字符串转成整数再取绝对值,写进 MaxTickVolume 私有成员,意味着单根 K 线允许的最大 tick 量是可外部定义的,解析成功直接 return true 交差。 若 szRet[0] 这个变量名没在预设表里命中,第111行会打印『Variable >>xxx<< not defined.』;若是整段配置串本身格式非法,则走第113行报『Configuration definition >>xxx<< invalidates.』,两种异常都返回 false,调用方据此跳过该配置。 WhatDefine 函数用字符串数组枚举了五种模式:[BARS]、[TICKS]、[TICKS->BARS]、[BARS->TICKS]、[CONFIG],cStage 从1起算,for 循环里 c0 每加一 cStage 也跟着加,命中即返回 true。你在 MT5 里改这个数组顺序,cStage 的编号就会整体偏移,下游 switch 分支得同步改。 CMD_Array 是总入口,形参 cError 和 eWhatExec 枚举决定它内部走哪个子流程;第137行先给 bBarsPrev 置 false,第138行留 szInfo 做日志载体。外汇与贵金属波动大、滑点频繁,MaxTickVolume 设太小可能丢 tick,设太大则内存与回测耗时飙升,建议先用历史数据跑一遍再定。
m_GlPrivate.MaxTickVolume = (class="type">int) MathAbs(StringToInteger(szRet[class="num">1])); class="kw">return true; } Print("Variable >>", szRet[class="num">0], "<< not defined."); }else Print("Configuration definition >>", szInfo, "<< invalidates."); class="kw">return class="kw">false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">bool WhatDefine(const class="type">class="kw">string szArg, class="type">char &cStage) { const class="type">class="kw">string szList[] = { "[BARS]", "[TICKS]", "[TICKS->BARS]", "[BARS->TICKS]", "[CONFIG]" }; cStage = class="num">1; for (class="type">char c0 = class="num">0; c0 < ArraySize(szList); c0++, cStage++) if (szList[c0] == szArg) class="kw">return true; class="kw">return class="kw">false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">bool CMD_Array(class="type">char &cError, eWhatExec e1) { class="type">bool bBarsPrev = class="kw">false; class="type">class="kw">string szInfo;
回放模式下的数据路由与装载
历史数据回放模块用枚举 e1 决定走哪条装载路径:eTickReplay 重放逐笔 tick,eTickToBar 把 tick 聚合成 bar,eBarToTick 反向拆 bar 为 tick,eBarPrev 则补前置 bar。代码里先用 switch 把对应数组指针 ptr 从 m_GlPrivate 里取出来,空指针直接跳过后续循环。 循环上限写成 INT_MAX 只是防御式写法,真正退出靠 At() 取到空字符串就 break,实际行数由文件内容决定。四种分支各自调不同函数:LoadTicks 带 true/false 区分是否重放,BarsToTicks 返回 bool,任一环节失败就写 cError(4~6)并停转。 在 MT5 里验证时,把 m_GlPrivate.MaxTickVolume 调小到比如 1000,能明显看到大样本回放耗时下降,但可能丢失极端成交量 tick。外汇与贵金属回放属高杠杆环境,参数误设会导致历史重构偏差,实盘前务必用策略测试器交叉核对。
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, true, m_GlPrivate.MaxTickVolume) == class="num">0) cError = class="num">4; break; case eTickToBar : if (LoadTicks(szInfo, class="kw">false, m_GlPrivate.MaxTickVolume) == class="num">0) cError = class="num">5; else bBarsPrev = true; break; case eBarToTick : if (!BarsToTicks(szInfo, m_GlPrivate.MaxTickVolume)) cError = class="num">6; break; case eBarPrev :
「配置服务里的回放与账户初始化」
这段 C_ConfigService 的构造与几个内联方法,决定了回放引擎启动前的基础状态。构造函数里把 AccountHedging 置为 false、ModelLoading 设为 1、MaxTickVolume 给到 2000,相当于默认按净头寸账户、标准加载模式、单 tick 最大成交量 2000 手的上限来跑。 LoadPreView 那段逻辑很直接:用 szInfo 拼出文件名后 new 一个 C_FileBars,调 LoadPreView(),返回 0 就把 cError 记成 3 并跳出,非 0 才把 bBarsPrev 置 true,随后立刻 delete 指针。说明历史 bars 预览加载失败会被静默标记,不会中断整体流程,但后续依赖 bBarsPrev 的判断要留意这个坑。 TypeAccountIsHedging() 只是把私有成员原样返回,外部调用来确认当前是不是锁仓账户——外汇和贵金属品种在 MT5 上开 hedging 模式的概率不低,回测前最好先打印确认,否则仓位逻辑可能和真实账户错位。 SetSymbolReplay 用宏把路径和 szInfo 拼成完整文件名,file / cError / cStage 等局部变量已声明但未在这几行展开读写,真正读配置的动作在后面分段。想验证的话,直接把 MaxTickVolume 改小到 500 再跑同一份 tick 数据,能看出成交量截断后重采样的偏差。
pFileBars = new C_FileBars(szInfo); if ((*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=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ C_ConfigService() :C_FileTicks() { m_GlPrivate.AccountHedging = class="kw">false; m_GlPrivate.ModelLoading = class="num">1; m_GlPrivate.MaxTickVolume = class="num">2000; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline const class="type">bool TypeAccountIsHedging(class="type">void) const { class="kw">return m_GlPrivate.AccountHedging; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool SetSymbolReplay(const class="type">class="kw">string szFileConfig) { class="macro">#define macroFileName((m_GlPrivate.szPath != NULL ? m_GlPrivate.szPath + "\\" : "") + szInfo) class="type">int file; class="type">char cError, cStage; class="type">class="kw">string szInfo; class="type">bool bBarsPrev;
◍ 回放配置载入与逐行解析的容错逻辑
行情回放模块启动后,第一道关卡是打开 Market Replay\ 目录下的配置文件。若句柄返回 INVALID_HANDLE,说明文件缺失或路径错,程序直接打印终止信息并 return false,不会带着空配置往下跑。
打开成功后先打印等待提示,并把错误计数器 cError、阶段计数器 cStage 清零,同时把 bBarsPrev 置 false、m_GlPrivate.Line 设为 1,四个指针数组全部置 NULL,这是典型的「重初始化」动作,避免上一次回放残留数据污染本次会话。
主循环以 FileIsEnding 未结束、_StopFlag 未被用户终止、cError == 0 三者同时满足为条件。每轮用 FileReadString 读一行,交给 GetDefinition 判定是 DEFINE 还是 INFO 类型,再分流处理。
当行类型为 INFO 且内容非空时,按 cStage 分支:阶段 0 下遇到 INFO 直接把 cError 设为 2(配置顺序错乱);阶段 1 下则懒初始化 pBarsToPrev 数组并 Add 宏文件名与当前行号。外汇与贵金属回放涉及杠杆与跳空,实盘模拟误差风险高,建议先在策略测试器用历史数据验证该载入路径。
if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { Print("Failed to open configuration file [", szFileConfig, "]. Service being terminated..."); class="kw">return class="kw">false; } Print("Loading data for playback. Please wait...."); cError = cStage = class="num">0; bBarsPrev = class="kw">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 2~5 区分了四种不同的数据映射任务。case 2 到 case 4 都遵循同一套路:先判空再 new 一个 C_Array,然后把当前文件名和行号写进对应的指针数组,分别服务于 ticks 重放、ticks 转 bars、bars 转 ticks 三类队列。 case 5 走的是 Configs(szInfo) 配置解析,若返回失败就把 cError 置为 7,这是整个 switch 里唯一显式抛错误码的出口。行 247 的 Line 自增很有讲究:只有 cError 为 0 才 +1,解析出错时停在原行,方便定位脏配置。 收尾部分(250~252)按 ModelLoading 的值决定向 CMD_Array 注册哪套回放模型:≤2 时主注册 eTickReplay、辅注册 eBarToTick,否则反过来。最后一行用位与 1 判断是否需要保留上一根 bar 的快照,决定 bBarsPrev 的布尔态。外汇与贵金属回测走这套队列时波动放大,属于高风险验证场景,参数配错可能让回放完全失真。
case class="num">2: if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new C_Array(); (*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_Array(cError, (m_GlPrivate.ModelLoading <= class="num">2 ? eTickReplay : eBarToTick)); CMD_Array(cError, (m_GlPrivate.ModelLoading <= class="num">2 ? eBarToTick : eTickReplay)); bBarsPrev = (CMD_Array(cError, ((m_GlPrivate.ModelLoading & class="num">1) == class="num">1 ? eTickToBar : eBarPrev)) ? true : bBarsPrev);
「命令行解析失败时的退出分支」
这段逻辑处在 EA 初始化读取配置文件的收尾阶段,核心是根据 cError 的值决定服务是否继续。cError 为 0 表示命令被系统识别,此时若 GetInfoTicks().nTicks <= 0 就直接打印无 tick 可用并终止,把 cError 置为 -1。 若 cError 为 0 且已有 tick 数据,但 bBarsPrev 仍为 false,则调用 FirstBarNULL() 补首根 bar 的空值处理;其他 case 1/2/default 分别打印「命令行未识别」「系统不期望该行内容」「文件访问错误」并 break,不会进入正常运行。 最后 return 写得很直白:cError == 0 时返回 !_StopFlag(未被强制停止才继续),否则返回 false 直接退出。你在 MT5 里接这段时,重点看 257 行 nTicks 的判断——若历史数据没加载满,EA 会静默终止而非报错卡死。 下面把关键几行拆开:
- 行根据模型加载模式位选 eBarPrev 或 eTickToBar 决定 bBarsPrev 是否延续;
257–261 行是 0 号错误的唯一实质拦截点;
- 行用 _StopFlag 做外部停止信号的兜底。
class="num">253. bBarsPrev = (CMD_Array(cError, ((m_GlPrivate.ModelLoading & class="num">1) == class="num">1 ? eBarPrev : eTickToBar)) ? true : bBarsPrev); class="num">254. class="kw">switch(cError) class="num">255. { class="num">256. case class="num">0: class="num">257. if (GetInfoTicks().nTicks <= class="num">0) class="num">258. { class="num">259. Print("There are no ticks to use. Service is being terminated..."); class="num">260. cError = -class="num">1; class="num">261. }else if (!bBarsPrev) FirstBarNULL(); class="num">262. break; class="num">263. case class="num">1 : Print("The command on the line ", m_GlPrivate.Line, " not recognized by the system..."); break; class="num">264. case class="num">2 : Print("The system did not expect the contents of the line: ", m_GlPrivate.Line); break; class="num">265. class="kw">default : Print("Error accessing the file indicated in the line: ", m_GlPrivate.Line); class="num">266. } class="num">267. class="num">268. class="kw">return (cError == class="num">0 ? !_StopFlag : class="kw">false); class="num">269. class="macro">#undef macroFileName class="num">270. } class="num">271. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">272. }; class="num">273. class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 回放服务的两个待修瑕疵
上一节实现的回放分时报价限制系统能跑,但视频里暴露的两个小毛病还没清掉。关闭图表时极少数情况下图形对象没被正确删除,不影响 MT5 稳定性也不崩平台,只是脏对象残留;另一个更烦人的是模拟服务会随机自己暂停,得手动再点播放。 这两个问题都不致命,但下一篇会专门修:图形对象销毁逻辑补一道强制清理,回放服务加状态锁避免误暂停,同时把暂时禁用的功能重新打开。 视频里随机游走填的 tick 和真实账户 tick 走势差异非常明显,拿模拟数据校验完务必接实盘符号跑一遍,外汇和贵金属杠杆高、滑点跳空频繁,回放结果仅作逻辑验证、不代表实盘表现。