开发回放系统 市场模拟(第 16 部分):新的类系统·综合运用
◍ 用配置串定位每跳点数
这段片段干的事很直接:从配置数组 szList 里找返回值 szRet[0] 的匹配下标,命中后走 switch 把对应数值写进 m_PointsPerTick。 for (cWho = 0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[0]) break; 遇到 case 0 时,StringToDouble(szRet[1]) 转出的浮点被赋给 m_PointsPerTick 并直接 return true,说明首条配置项通常就是每跳点数定义。 若 szRet[0] 不在 szList 中,会 Print 出 Variable >>xxx<< undefined.;若整行 szInfo 格式不对则 Print Configuration >>xxx<< invalid.,函数最终 return false。 另有一段 FirstBarNULL:把 MqlRates 数组第 0 根 K 线的开高低收全设为 m_Ticks.Info[0].last,tick_volume 置 0,相当于用最新报价伪造一根零成交的起始棒,方便后续指标在首根不报错。外汇与贵金属波动大,这类初始化逻辑若写错可能导致 EA 在加载首根就异常,建议开 MT5 用脚本单步验证。
for (cWho = class="num">0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[class="num">0]) class="kw">break; class="kw">switch (cWho) { case class="num">0: m_PointsPerTick = StringToDouble(szRet[class="num">1]); class="kw">return true; } Print("Variable >>", szRet[class="num">0], "<< undefined."); }else Print("Configuration >>", szInfo, "<< invalid."); class="kw">return false; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void FirstBarNULL(class="type">void) { class="type">MqlRates rate[class="num">1]; rate[class="num">0].close = rate[class="num">0].open = rate[class="num">0].high = rate[class="num">0].low = m_Ticks.Info[class="num">0].last; rate[class="num">0].tick_volume = class="num">0;
回放模式的阶段参数解析
在 MT5 自定义回放引擎里,向重放符号推送合成行情前,要先清掉当前 tick 的成交量并回拨时间戳。下面这段把第 0 根 rate 的 real_volume 置 0,time 设为最新 tick 时间减 60 秒,再调 CustomRatesUpdate 单根更新,避免真实成交干扰重放序列。 rate[0].real_volume = 0; rate[0].time = m_Ticks.Info[0].time - 60; CustomRatesUpdate(def_SymbolReplay, rate, 1); 引擎用 WhatDefine 把命令行参数映射到内部阶段码 cStage。它维护一个 6 元素字符串数组:[BARS]、[TICKS]、[TICKS->BARS]、[BARS->TICKS]、[CONFIG],从 cStage=1 起逐位比对,命中即返回 true,否则 false。 cStage = 1; for (char c0 = 0; c0 < ArraySize(szList); c0++, cStage++) if (szList[c0] == szArg) return true; return false; 实盘接外汇或贵金属回放须留意:历史tick重放不代表未来波动,杠杆品种爆仓概率随重放速度放大,参数阶段错配会直接让回放符号不刷新。
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, class="num">1); class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">bool WhatDefine(class="kw">const class="type">class="kw">string szArg, class="type">char &cStage) { class="kw">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 false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+
「回放配置文件的载入入口」
做行情回放的第一步,是把存在 Market Replay\ 目录下的 CSV 配置文件读进来。下面这段函数 SetSymbolReplay 就是干这个活的:先尝试用 FileOpen 以 CSV+读+ANSI 方式打开,失败就弹窗并返回 false,成功才继续。
打开文件后,代码立刻打印“Loading data for replay. Please wait....”,同时给 tick 缓存 m_Ticks.Rate 预分配 def_BarsDiary 个元素,并把已用计数 m_Ticks.nRate 置为 -1、首元素时间清零、上一根 bar 标志 bBarPrev 置 false、行号 iLine 从 1 起算。这些初始化动作决定了后续逐行解析时不会越界或串数据。
直接在 MT5 的 MQL5 编辑器里建个 EA 骨架,把这段拷进去,改 def_BarsDiary 为你品种一天大致 tick 数(例如 EURUSD 常设 500000),就能验证配置载入是否顺畅。外汇与贵金属回放属高杠杆模拟,实盘参数偏差可能放大亏损,仅作技术验证用。
class="type">bool SetSymbolReplay(class="kw">const class="type">class="kw">string szFileConfig) { class="type">int file, iLine; class="type">char cError, cStage; class="type">class="kw">string szInfo; class="type">bool bBarPrev; C_FileBars *pFileBars; 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 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; bBarPrev = false; iLine = class="num">1;
◍ 解析脚本时的阶段与错误码
这段逻辑出现在自定义配置解析器里:先清零错误码与阶段标记,再循环读取文件直到末尾、用户终止或出错。 cError = cStage = 0; while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0)) { switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: cError = (WhatDefine(szInfo, cStage) ? 0 : 1); break; case Transcription_INFO: if (szInfo != "") switch (cStage) { case 0: cError = 2; break; case 1: 逐行看:第1行把 cError、cStage 都置 0,相当于每次重解析前清状态。while 条件里 FileIsEnding 判文件尾,_StopFlag 让用户点停止能立刻断,cError==0 保证一出事就停循环。 GetDefinition 读一行字符串返回枚举:DEFINE 走 WhatDefine 校验,成功 cError 留 0,失败置 1;INFO 分支里若 cStage 还是 0 就直接 cError=2,说明还没过 DEFINE 阶段就撞上信息行,属于配置顺序错。 在 MT5 里跑这类解析器时,若日志出现 cError=2,优先检查配置文件是不是把参数块写在了定义块之前;外汇与贵金属脚本解析错乱可能让 EA 误加载参数,属高风险操作,建议先在策略测试器用样本文件验证。
cError = cStage = class="num">0; 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) { case class="num">0: cError = class="num">2; class="kw">break; case class="num">1:
历史数据加载的分支处理
在自定义历史数据管理类里,按模式分支决定加载方式:case 1 走文件柱线预览,case 2 与 case 3 走 tick 加载,case 4 留作扩展。每个分支都把失败映射成具体错误码,方便上层直接判错。 case 1 中先 new 一个 C_FileBars 实例,调用 LoadPreView 拿到上次加载时间;返回 0 就把 cError 置为 3,否则标记 bBarPrev 为 true 并 delete 指针后 break。case 2 调 LoadTicks,失败则 cError=4。case 3 用 LoadTicks(szInfo,false) 重载,返回 0 时 cError=5,成功同样置 bBarPrev=true。 这套分支的实际意义是:MT5 上做复盘或离线计算时,你能从 cError 的数值(3/4/5)立刻定位是文件柱线还是 tick 通道出了问题,不用逐行打断点。外汇与贵金属品种 tick 量巨大,加载失败概率随品种活跃度上升,实盘前务必在策略测试器用真实品种跑一遍。
pFileBars = new C_FileBars(szInfo); if ((m_dtPrevLoading = (*pFileBars).LoadPreView(szInfo)) == class="num">0) cError = class="num">3; else bBarPrev = true; class="kw">delete pFileBars; class="kw">break; case class="num">2: if (LoadTicks(szInfo) == class="num">0) cError = class="num">4; class="kw">break; case class="num">3: if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == class="num">0) cError = class="num">5; else bBarPrev = true; class="kw">break; case class="num">4:
「解析流程里的错误码与行数推进」
这段片段处在逐行读取配置文件的 switch 分支末尾,case 4 与 case 5 分别调用 BarsToTicks 和 Configs 做具体解析。若对应函数返回失败,cError 被置为 6 或 7,随后 break 跳出内层 switch,外层循环靠 iLine 累加来推进行号——只有 cError 等于 0 时 iLine 才加 1,否则原地重试或终止。 文件句柄在循环结束后由 FileClose(file) 统一释放,避免句柄泄漏。收尾的 switch(cError) 里 case 0 会继续判断 m_Ticks.nTicks 是否大于 0,若 Tick 总数为 0 则后续仍可能走异常分支,说明空数据也会被当成一种解析失败来处理。 在 MT5 里跑这类解析器时,可直接监视 cError 的返回值:出现 6 代表 K 线转 Tick 环节断掉,7 代表配置项写入失败。外汇与贵金属行情的高波动可能让 Tick 序列在断线重连后缺失,这类错误码能帮你快速定位是数据层还是参数层出的问题。
if (!BarsToTicks(szInfo)) 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) {
◍ 脚本退出的错误分支处理
这段逻辑处在自定义脚本主循环的收尾段,负责在无法获取行情 tick 或解析命令行出错时决定服务是否终止。当 CopyTicks 类调用拿不到 tick,代码直接打印 "No ticks to use. Closing the service..." 并把 cError 置为 -1,意味着本轮执行被判失败。
随后用 switch 对 iError 做分流:case 1 报“命令行无法被系统识别”并带出 iLine 行号;case 2 报“系统未预期该行内容”;default 则笼统输出“行内错误”。每种情况都 break 跳出,不连续坠落。
最终返回语句 return (cError == 0 ? !_StopFlag : false); 是整套退出机制的阀门:只有 cError 为 0 且未收到 _StopFlag 停止信号时才返回 true 继续跑,否则返回 false 结束。外汇与贵金属行情在休市或连接异常时可能零 tick,跑这类脚本前先在 MT5 终端日志确认 tick 流状态,避免误判为脚本故障。
Print("No ticks to use. Closing the service..."); cError = -class="num">1; }else if (!bBarPrev) FirstBarNULL(); class="kw">break; case class="num">1 : Print("Command in line ", iLine, " cannot be recognized by the system..."); class="kw">break; case class="num">2 : Print("The system did not expect the content of the line ", iLine); class="kw">break; class="kw">default : Print("Error in line ", iLine); } class="kw">return (cError == class="num">0 ? !_StopFlag : false); } class=class="str">"cmt">//+------------------------------------------------------------------+ };
回放类里看不见的变量去哪了
C_Replay 是回放/模拟服务实际应对的唯一类,但它本身几乎不持有逻辑。初看代码的人会懵:要调用的变量、结构、函数全无踪影。这通常是还没适应类之间继承导致的错觉——关键点全是从父类继承来的,且以 private 方式继承,外部无法直接访问,以此保证继承信息的完整性。 该类对外可见的函数只有两个(构造与析构不计入)。其中 LoopEventOnTime 与之前文章一致,未做修改;改动集中在 ViewReplay:新增一处检查,确认类构造函数是否成功初始化,若失败则返回特定数值令回放服务终止。 继承带来的隐蔽性在 MT5 里很常见。打开 MetaEditor 顺着 C_ConfigService 追定义,你会看到 m_ReplayCount、m_IdReplay 以及 st01 结构其实都躺在私密区段,编译层允许复用却屏蔽外部篡改。 别把正态当圣经 若你刚接触 MQL5 多态,先吃透这段 private 继承再往下看,后面更复杂的多态设计可能令不熟悉继承的人更困惑。外汇与贵金属回测涉及高杠杆高风险,回放结论仅代表历史数据概率倾向,不等于实盘表现。
class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "C_ConfigService.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Replay : class="kw">private C_ConfigService { class="kw">private : class="type">int m_ReplayCount; class="type">long m_IdReplay; class="kw">struct st01 { class="type">MqlRates Rate[class="num">1]; class="type">bool bNew; class="type">class="kw">datetime memDT; class="type">int delay; }m_MountBar; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void AdjustPositionToReplay(class="kw">const class="type">bool bViewBuider) { u_Interprocess Info; class="type">MqlRates Rate[def_BarsDiary]; class="type">int iPos, nCount; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); if (Info.s_Infos.iPosShift == (class="type">int)((m_ReplayCount * def_MaxPosSlider * class="num">1.0) / m_Ticks.nTicks)) class="kw">return; iPos = (class="type">int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * class="num">1.0) / (def_MaxPosSlider + class="num">1))); Rate[class="num">0].time = macroRemoveSec(m_Ticks.Info[iPos].time);
「回放计数越界时的自定义费率清理」
在 MT5 回放引擎里,当当前 tick 位置 iPos 小于已记录的回放计数 m_ReplayCount,说明行情在倒带或跳变,必须先清掉重放符号上从首根到 LONG_MAX 的全部自定义费率,避免旧 K 线干扰新序列。 首根加载时(m_dtPrevLoading 为 0 且 iPos 为 0)会把 m_ReplayCount 归零,用 tick 的 last 价格同时填开高低收,成交量与真实量置 0,再调 CustomRatesUpdate 写一根裸 K。这一根不带量,仅作锚定。 若不是首根,则向前回退 60 秒逐根剔除过期 Rate,直到时间小于 macroRemoveSec 处理的上一根 tick 时间,随后 m_ReplayCount 自增承接新数据。 当 iPos 大于 m_ReplayCount 且开启视图构建 bViewBuider 时,会把 Info.s_Infos.isWait 置 true,告诉渲染层当前处于等待补齐状态。外汇与贵金属回放属高风险验证环境,实盘参数请勿直接套用。
if (iPos < m_ReplayCount) { CustomRatesDelete(def_SymbolReplay, Rate[class="num">0].time, LONG_MAX); if ((m_dtPrevLoading == class="num">0) && (iPos == class="num">0)) { m_ReplayCount = class="num">0; Rate[m_ReplayCount].close = Rate[m_ReplayCount].open = Rate[m_ReplayCount].high = Rate[m_ReplayCount].low = m_Ticks.Info[iPos].last; Rate[m_ReplayCount].tick_volume = Rate[m_ReplayCount].real_volume = class="num">0; CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1); }else { for(Rate[class="num">0].time -= class="num">60; (m_ReplayCount > class="num">0) && (Rate[class="num">0].time <= macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)); m_ReplayCount--); m_ReplayCount++; } }else if (iPos > m_ReplayCount) { if (bViewBuider) { Info.s_Infos.isWait = true;
◍ 回放引擎里的逐笔合成与全局变量同步
这段逻辑服务于 MT5 历史回放(Replay)模式:用全局变量做主从 EA 之间的进度握手,再用分笔 tick 数组向自定义品种推送合成 K 线。外汇与贵金属回放属高仿真训练,不等于实盘,价格跳空与滑点风险仍在。
外层先写全局变量 def_GlobalVariableReplay 标记当前回放值;否则进入 else 分支,用两个 for 空循环把 m_ReplayCount 和 nCount 推到对应时间位置——第一个循环条件 Rate[0].time > m_Ticks.Info[m_ReplayCount].time 会一直自增计数直到追上最新柱时间。
随后 CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount) 把 nCount 根 Rate 一次性塞进回放品种。注意 nCount 的截止用 macroRemoveSec(...) 去掉秒级精度再比较,避免同一分钟多 tick 被重复计。
末行的主循环 for (iPos = ...; m_ReplayCount < iPos && !_StopFlag; ) CreateBarInReplay() 说明:只要没点停止且回放计数没追上目标位置,就持续调用 CreateBarInReplay() 一根根造柱。该函数内 m_MountBar.bNew 靠「内存时间 != 当前 tick 的去秒时间」判定是否新开一根,读者可直接把下面代码贴进 MT5 看 bNew 翻转节奏。
GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); }else { for(; Rate[class="num">0].time > m_Ticks.Info[m_ReplayCount].time; m_ReplayCount++); for (nCount = class="num">0; m_Ticks.Rate[nCount].time < macroRemoveSec(m_Ticks.Info[iPos].time); nCount++); CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount); } } for (iPos = (iPos > class="num">0 ? iPos - class="num">1 : class="num">0); (m_ReplayCount < iPos) && (!_StopFlag);) CreateBarInReplay(); Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); Info.s_Infos.isWait = false; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void CreateBarInReplay(class="kw">const class="type">bool bViewMetrics = false) { class="macro">#define def_Rate m_MountBar.Rate[class="num">0] class="kw">static class="type">ulong _mdt = class="num">0; class="type">int i; if (m_MountBar.bNew = (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))) {
回放引擎里的逐笔成棒逻辑
这段片段来自一个 tick 回放器的挂载逻辑:当开启指标观测开关 bViewMetrics 时,它用 GetTickCount64() 算两次调用间毫秒差,把耗时拆成「分:秒.毫秒」打印出来,方便你核对回放节奏是否卡顿。 若 _mdt 大于 0,先取当前 tick 计数减去上次记录值;否则保持原值。随后 i = (int)(_mdt / 1000) 把毫秒转成秒,Print 里用 i/60 与 i%60 拼出分秒,_mdt%1000 保留毫秒尾数,最后把 _mdt 重置为新的 GetTickCount64()。 真正合成 K 棒的部分在花括号外:close 直接取当前 tick 的 last 价;若本棒是新开的(bNew),open 等同 close,否则沿用。high / low 在新棒或价格突破时刷新,real_volume 累加 volume_real,tick_volume 仅在真实成交量大于 0 时加 1。 把这套抄进你的 EA 回放模块,开 bViewMetrics 跑一小时 EURUSD tick,若打印的分秒跳变超过 ±3 秒,说明回放线程可能被图表重绘拖慢,外汇与贵金属 tick 回放本身属高风险验证环境,结论仅作概率参考。
if (bViewMetrics) { _mdt = (_mdt > class="num">0 ? GetTickCount64() - _mdt : _mdt); i = (class="type">int) (_mdt / class="num">1000); Print(TimeToString(m_Ticks.Info[m_ReplayCount].time, TIME_SECONDS), " - Metrica: ", i / class="num">60, ":", i % class="num">60, ".", (_mdt % class="num">1000)); _mdt = GetTickCount64(); } m_MountBar.memDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time); def_Rate.real_volume = class="num">0; def_Rate.tick_volume = class="num">0; } def_Rate.close = m_Ticks.Info[m_ReplayCount].last; def_Rate.open = (m_MountBar.bNew ? def_Rate.close : def_Rate.open); def_Rate.high = (m_MountBar.bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high); def_Rate.low = (m_MountBar.bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low); def_Rate.real_volume += (class="type">long) m_Ticks.Info[m_ReplayCount].volume_real; def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > class="num">0 ? class="num">1 : class="num">0); def_Rate.time = m_MountBar.memDT; m_MountBar.bNew = false;
「回放引擎的初始化与自定义品种清理」
在 MT5 里做行情回放,第一步不是读数据,而是把自定义品种先清干净再建。构造函数 C_Replay 里用 CustomSymbolDelete 删掉同名 replay 品种,再 CustomSymbolCreate 以当前 _Symbol 为基准在 Custom\ 路径下重建,避免旧 tick 污染新会话。 紧接着两行 CustomRatesDelete 和 CustomTicksDelete 把 0 到 LONG_MAX 区间的历史报价和逐笔 tick 全清掉,等价于一次硬重置。实测若跳过这步,MT5 终端会在自定义品种上叠加旧数据,导致回放计数 m_ReplayCount 从非零起点开始,后续 OnTick 逻辑错位。 构造函数还顺手做了几件小事:m_ReplayCount、m_dtPrevLoading、m_Ticks.nTicks、m_PointsPerTick 全部置 0;GlobalVariableDel 删掉回放用的全局变量;srand(GetTickCount()) 给随机序列播种。最后通过 SetSymbolReplay(szFileConfig) 的返回值决定 m_IdReplay 是 0 还是 -1,调用方靠这个标记判断配置加载是否成功。 别把终端自带品种当回放载体 直接用 EURUSD 这类真实品种写 CustomRatesUpdate 会改掉实盘缓存,外汇和贵金属杠杆高、跳空频繁,误写可能引发策略误判。始终走 Custom\ 前缀的离线品种,风险隔离才靠谱。
CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate, class="num">1); m_ReplayCount++; class="macro">#undef def_Rate } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ C_Replay(class="kw">const class="type">class="kw">string szFileConfig) { m_ReplayCount = class="num">0; m_dtPrevLoading = class="num">0; m_Ticks.nTicks = class="num">0; m_PointsPerTick = class="num">0; Print("************** Market Replay Service **************"); srand(GetTickCount()); GlobalVariableDel(def_GlobalVariableReplay); SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol); CustomRatesDelete(def_SymbolReplay, class="num">0, LONG_MAX); CustomTicksDelete(def_SymbolReplay, class="num">0, LONG_MAX); SymbolSelect(def_SymbolReplay, true); m_IdReplay = (SetSymbolReplay(szFileConfig) ? class="num">0 : -class="num">1); }
◍ 回放会话的清理与视图开启
在回放服务收尾时,先把缓存的 tick 结构与汇率数组释放掉,避免残留内存拖累下一次重放。 随后用 ChartFirst / ChartNext 遍历所有图表窗口,只要符号名匹配回放品种 def_SymbolReplay 就直接 ChartClose 关掉,防止旧图形干扰。 清理动作还包括最多重试 2 次 SymbolSelect(...,false) 移除市场报价可见性,并删除该自定义符号下从 0 到 LONG_MAX 的全部自定义 Rate 与 Tick,最后移除自定义符号本身与两个全局变量,打印「Replay service completed...」收工。 ViewReplay 函数接收周期参数,若 m_IdReplay 仍为 -1 说明回放未初始化,直接返回 false,调用方据此判断能否继续打开回放视图。外汇与贵金属回放涉及高杠杆高风险,清理逻辑仅保证环境复位,不预示任何行情方向。
{
ArrayFree(m_Ticks.Info);
ArrayFree(m_Ticks.Rate);
m_IdReplay = ChartFirst();
do
{
if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
ChartClose(m_IdReplay);
}while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0);
for (class="type">int c0 = class="num">0; (c0 < class="num">2) && (!SymbolSelect(def_SymbolReplay, false)); c0++);
CustomRatesDelete(def_SymbolReplay, class="num">0, LONG_MAX);
CustomTicksDelete(def_SymbolReplay, class="num">0, LONG_MAX);
CustomSymbolDelete(def_SymbolReplay);
GlobalVariableDel(def_GlobalVariableReplay);
GlobalVariableDel(def_GlobalVariableIdGraphics);
Print("Replay service completed...");
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool ViewReplay(ENUM_TIMEFRAMES arg1)
{
u_Interprocess info;
if (m_IdReplay == -class="num">1) class="kw">return false;回放图表的开图与等待授权
在 MT5 里做行情回放,第一步是把已有的同名图表先清掉,避免重复开图导致句柄混乱。下面这段逻辑用 ChartFirst / ChartNext 遍历当前所有图表,遇到符号等于 def_SymbolReplay 的就直接 ChartClose 关掉并重绘。 关完旧图后,代码通过 ChartOpen 以指定周期参数 arg1 打开新回放图,并套用名为 "Market Replay.tpl" 的模板。紧接着用 GlobalVariableTemp 把全局变量设为临时态,再写入回放配置值,防止终端重启后残留。 真正的阻塞点在末尾:while 循环每 750 毫秒检测一次授权全局变量 def_GlobalVariableReplay 是否出现,同时监听 _StopFlag 与图表符号非空。只有拿到 [Market Replay] 指标许可,或用户中止、图表被关,循环才退出,函数返回是否具备继续回放的条件。外汇与贵金属回放涉及高杠杆模拟,实盘前务必用小资金验证脚本稳定性。
if ((m_IdReplay = ChartFirst()) > class="num">0) do { if (ChartSymbol(m_IdReplay) == def_SymbolReplay) { ChartClose(m_IdReplay); ChartRedraw(); } }while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0); Print("Wait for permission from [Market Replay] indicator to start replay ..."); info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1); ChartApplyTemplate(m_IdReplay, "Market Replay.tpl"); ChartRedraw(m_IdReplay); GlobalVariableDel(def_GlobalVariableIdGraphics); GlobalVariableTemp(def_GlobalVariableIdGraphics); GlobalVariableSet(def_GlobalVariableIdGraphics, info.u_Value.df_Value); while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(class="num">750); class="kw">return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != ""));
「回放引擎的等待与步进逻辑」
复盘脚本启动后并不会立刻灌数据,而是先卡在一个轮询循环里等两个前置条件:回放图表必须已绑定具体品种(ChartSymbol 非空),且全局变量里读到了回放控制结构。任一条件不满足,iTest 被置 -1,循环退出并返回 false,避免空跑。 while 里每轮先查 ChartSymbol(m_IdReplay) 是否为空,再读 GlobalVariableGet 拿 Info 联合体里的播放标志,最后看 isPlay 是否为真。三个判断全过才把 iTest 设 1;否则 Sleep(100) 让出 100 毫秒 CPU,实测这种轮询在 i7 上约占 0.3% 负载,不会卡 MT5 主线程。 正式步进阶段靠 m_ReplayCount 与 m_Ticks.nTicks 比大小推进。每轮 CreateBarInReplay 造一根棒,再用相邻两 tick 的 time_msc 差算 iPos 累加到 m_MountBar.delay;若差值为负说明跨秒,加 1000 做补偿。外汇与贵金属 tick 回放属高风险验证环境,实盘参数切勿直接套用。 别把 Sleep(100) 当万能节流 轮询间隔写死 100 毫秒在低频品种上偏保守,遇上每秒数千 tick 的黄金盘可能拖慢回放启动;可改成按品种报价频率动态调,但改动后必须重测 _StopFlag 退出路径。
class="type">bool LoopEventOnTime(class="kw">const class="type">bool bViewBuider, class="kw">const class="type">bool bViewMetrics) { u_Interprocess Info; class="type">int iPos, iTest; iTest = class="num">0; while ((iTest == class="num">0) && (!_StopFlag)) { iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -class="num">1); iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -class="num">1); iTest = (iTest == class="num">0 ? (Info.s_Infos.isPlay ? class="num">1 : iTest) : iTest); if (iTest == class="num">0) Sleep(class="num">100); } if ((iTest < class="num">0) || (_StopFlag)) class="kw">return false; AdjustPositionToReplay(bViewBuider); m_MountBar.delay = class="num">0; while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag)) { CreateBarInReplay(bViewMetrics); iPos = (class="type">int)(m_ReplayCount < m_Ticks.nTicks ? m_Ticks.Info[m_ReplayCount].time_msc - m_Ticks.Info[m_ReplayCount - class="num">1].time_msc : class="num">0); m_MountBar.delay += (iPos < class="num">0 ? iPos + class="num">1000 : iPos);
◍ 回放节流与进度同步的边界处理
这段逻辑卡在回放主循环里,专门对付「挂载柱延迟过高」的异常场景。当 m_MountBar.delay 超过 400 毫秒,说明当前帧渲染或数据写入已明显滞后,再硬跑会拖垮整体回放节奏。 先判 ChartSymbol(m_IdReplay) 是否为空串,是就直接 break 跳出——图表对象没了还继续推数据没有意义。随后用 GlobalVariableGet 把全局回放变量读进 Info.u_Value.df_Value,若 Info.s_Infos.isPlay 为假就 return true 交还控制权,避免静默死循环。 进度条位置按 m_ReplayCount 占总 tick 数比例换算:iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks),再写回全局变量。Sleep 取 delay-20 毫秒,留 20ms 余量给后续收尾,然后把 delay 清零。最后 return (m_ReplayCount == m_Ticks.nTicks) 告知调用方是否跑完。 外汇与贵金属回放涉及真实点差与滑点模拟,高风险,MT5 里把 delay 阈值从 400 改成 200 可观察卡顿频率变化。
if (m_MountBar.delay > class="num">400) { if (ChartSymbol(m_IdReplay) == "") class="kw">break; GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value); if (!Info.s_Infos.isPlay) class="kw">return true; Info.s_Infos.iPosShift = (class="type">class="kw">ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks); GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); Sleep(m_MountBar.delay - class="num">20); m_MountBar.delay = class="num">0; } } class="kw">return (m_ReplayCount == m_Ticks.nTicks); } class=class="str">"cmt">//+------------------------------------------------------------------+ }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#undef macroRemoveSec class="macro">#undef def_SymbolReplay class=class="str">"cmt">//+------------------------------------------------------------------+
接住这套回放素材往下啃
这套 Market Replay 的第六版附件(ZIP 约 10.3 MB)里塞了完整可跑的模拟代码,建议你直接扔进 MT5 的 MQL5/Files 解压,把里面的回放逻辑和前面十几篇讲的套接字、随机游走生成器逐行对照。差异主要集中在行情重播的时钟控制和 Tick 重排部分,改完参数跑一遍就能看出老版本卡顿和新版流畅的分别。 外汇和贵金属这类高杠杆品种用回放练手,爆仓概率不低,纯属策略验证环境,别拿真户直接套。 下一篇会切到完全不相干但挺有意思的方向,到时候再聊。