开发回放系统  市场模拟(第 16 部分):新的类系统·综合运用
📘

开发回放系统 市场模拟(第 16 部分):新的类系统·综合运用

第 3/3 篇

◍ 用配置串定位每跳点数

这段片段干的事很直接:从配置数组 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 用脚本单步验证。

MQL5 / C++
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重放不代表未来波动,杠杆品种爆仓概率随重放速度放大,参数阶段错配会直接让回放符号不刷新。

MQL5 / C++
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),就能验证配置载入是否顺畅。外汇与贵金属回放属高杠杆模拟,实盘参数偏差可能放大亏损,仅作技术验证用。

MQL5 / C++
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 误加载参数,属高风险操作,建议先在策略测试器用样本文件验证。

MQL5 / C++
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 量巨大,加载失败概率随品种活跃度上升,实盘前务必在策略测试器用真实品种跑一遍。

MQL5 / C++
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 序列在断线重连后缺失,这类错误码能帮你快速定位是数据层还是参数层出的问题。

MQL5 / C++
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 流状态,避免误判为脚本故障。

MQL5 / C++
         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 继承再往下看,后面更复杂的多态设计可能令不熟悉继承的人更困惑。外汇与贵金属回测涉及高杠杆高风险,回放结论仅代表历史数据概率倾向,不等于实盘表现。

MQL5 / C++
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,告诉渲染层当前处于等待补齐状态。外汇与贵金属回放属高风险验证环境,实盘参数请勿直接套用。

MQL5 / C++
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_ReplayCountnCount 推到对应时间位置——第一个循环条件 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 翻转节奏。

MQL5 / C++
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 回放本身属高风险验证环境,结论仅作概率参考。

MQL5 / C++
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\ 前缀的离线品种,风险隔离才靠谱。

MQL5 / C++
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,调用方据此判断能否继续打开回放视图。外汇与贵金属回放涉及高杠杆高风险,清理逻辑仅保证环境复位,不预示任何行情方向。

MQL5 / C++
{
   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] 指标许可,或用户中止、图表被关,循环才退出,函数返回是否具备继续回放的条件。外汇与贵金属回放涉及高杠杆模拟,实盘前务必用小资金验证脚本稳定性。

MQL5 / C++
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 退出路径。

MQL5 / C++
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 可观察卡顿频率变化。

MQL5 / C++
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 重排部分,改完参数跑一遍就能看出老版本卡顿和新版流畅的分别。 外汇和贵金属这类高杠杆品种用回放练手,爆仓概率不低,纯属策略验证环境,别拿真户直接套。 下一篇会切到完全不相干但挺有意思的方向,到时候再聊。

常见问题

在配置串里按约定字段写入每跳点数映射,解析时直接按字段读取即可,避免手动换算出错。
阶段参数在载入配置后按序解析,每个阶段对应固定参数块,看解析脚本时的阶段标记就能确认当前生效参数。
可以,小布能读取你的配置串并对照阶段规则做静态校验,有错直接标出字段和行数,省去手动排查。
分支处理看配置里是否声明了替代数据源或区间裁剪,有声明就走分支加载,无声明则按默认全量读取。
是的,错误码配合行数推进指向配置文件中的具体出错行,按行数回看配置串对应字段即可定位问题。