开发回放系统  市场模拟(第 21 部分):外汇(II)·综合运用
📘

开发回放系统 市场模拟(第 21 部分):外汇(II)·综合运用

第 3/3 篇

◍ 回放品种的参数注入分支

这段 switch 结构负责把配置文件里的字段映射到自定义回放品种 def_SymbolReplay 的属性上。case 0 只记路径不碰品种属性,case 1~3 分别写 tick 最小变动价位、每跳价值和成交量步长,全部用 StringToDouble 把 CSV 文本转成浮点再落库。 case 4 处理模型加载模式 m_ModelLoading,先用 StringInit 转整型,再用一行条件表达式把越界值夹回 1。注意原代码写的是 (m_ModelLoading < 1) && (m_ModelLoading > 4),逻辑上永远为假,实际任何不在 1~4 的值都不会被修正,这是移植时容易踩的坑。 SetSymbolReplay 入口先拼宏 macroFileName,再尝试用 FileOpen 以 CSV+ANSI 只读方式打开 Market Replay 目录下的配置。若句柄等于 INVALID_HANDLE,会打印失败并退出服务,说明路径或文件名错一个字符就直接中断,没有兜底。 开 MT5 把这段粘进 EA 初始化,故意改错配置名看 Print 输出,能确认你的回放品种到底卡在哪一行。外汇与贵金属回放含高杠杆风险,参数错配可能让历史重演失真。

MQL5 / C++
case class="num">0:
   m_szPath = szRet[class="num">1];
   class="kw">return true;
case class="num">1:
   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_ModelLoading = StringInit(szRet[class="num">1]);
   m_ModelLoading = ((m_ModelLoading < class="num">1) && (m_ModelLoading > class="num">4) ? class="num">1 : m_ModelLoading);
   class="kw">return true;
}
Print("Variable >>", szRet[class="num">0], "<< undefined.");
}else
Print("Set-up configuration >>", szInfo, "<< invalid.");
class="kw">return false;
}
class="type">bool SetSymbolReplay(const class="type">class="kw">string szFileConfig)
{
class="macro">#define macroFileName((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)
   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...");

「回放引擎的初始化与文件解析循环」

这段逻辑出现在 tick 回放类模块的加载函数里,负责把历史转录文件读进内存并切分阶段。先打印等待提示,把 m_Ticks.Rate 按 def_BarsDiary 预扩容,再把 nRate 置 -1、首元素时间归零,相当于清空上一轮回放残留。 cError 与 cStage 都清 0,bBarsPrev 设 false,私有指针 pTicksToReplay / pTicksToBars / pBarsToTicks / pBarsToPrev 全部 NULL,Line 从 1 计起。这样后续 while 循环才能在文件未尾、未点停止、无错误三个条件下持续读取。 循环体里用 FileReadString 取行、GetDefinition 判别是 DEFINE 还是 INFO。DEFINE 走 WhatDefine 解析;INFO 非空时按 cStage 分流:0 阶段收到 INFO 直接 cError=2 报错,1 阶段挂 pBarsToPrev 数组,2 阶段挂 pTicksToReplay,3 阶段挂 pTicksToBars。外汇与贵金属 tick 回放属高风险验证,参数错配可能导致回测失真。 开 MT5 把这段贴进 EA 的加载段,改 def_BarsDiary 看内存占用变化,能直接确认你的转录文件是否被按阶段正确捕获。

MQL5 / C++
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();
                     (*m_GlPrivate.pTicksToReplay).Add(macroFileName, m_GlPrivate.Line);
                     break;
                  case class="num">3:
                     if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new C_Array();

回放脚本的指令解析与报错分支

这段逻辑处在历史数据回放器的配置加载尾部,负责把宏文件里读到的指令分派到不同数组,并依据错误码决定后续建栏方式。case 4 与 case 5 分别把文件名塞进 BarsToTicks 映射和走 Configs 解析,若 Configs 失败则把 cError 置为 7,意味着该行的配置项直接被丢弃。 加载收尾时连续调用 CMD_Array 四次,根据 m_ModelLoading 的值(≤2 走 TickReplay/BarToTick,否则反向)以及最低位比特切换 eTickToBar 与 eBarPrev 的启用标记,任一返回真都会把 bBarsPrev 翻成 true。 最后的 switch(cError) 是硬约束:cError 为 0 时若 m_Ticks.nTicks ≤ 0 直接打印 No ticks to use 并置 -1 退出;若 nTicks 正常但 bBarsPrev 仍假则补 FirstBarNULL()。其余 1、2 及 default 分别打印无法识别指令、行内容异常、文件访问错误并带出 m_GlPrivate.Line 行号,方便在 MT5 终端里定位宏文件哪一行写崩了。外汇与贵金属历史回放涉及杠杆与滑点,实盘前务必用策略测试器小周期验证该加载路径。

MQL5 / C++
         (*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_ModelLoading <= class="num">2 ? eTickReplay : eBarToTick));
      CMD_Array(cError, (m_ModelLoading <= class="num">2 ? eBarToTick : eTickReplay));
      bBarsPrev = (CMD_Array(cError, ((m_ModelLoading & class="num">1) == class="num">1 ? eTickToBar : eBarPrev)) ? true : bBarsPrev);
      bBarsPrev = (CMD_Array(cError, ((m_ModelLoading & class="num">1) == class="num">1 ? eBarPrev : eTickToBar)) ? 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;
         class="kw">default : Print("Access error occurred in the files indicated in line: ", m_GlPrivate.Line);
      }

◍ 退出逻辑里的停止旗标判定

这段收尾代码把错误码和全局停止旗标做了二值收敛:当 cError 等于 0 时,返回「非 _StopFlag」状态,否则直接返回 false。 宏 macroFileName 在作用域末尾用 #undef 撤掉,避免污染后续编译单元。 在 MT5 里跑 EA 时,若你在策略测试器手动点停止,_StopFlag 会被置 true,此时即便无报错也会令函数返回 false,循环自然断掉。 验证方式:把这段塞进 OnTick 尾部的退出分支,打印返回值,观察点停止后第一帧是否翻 false。

MQL5 / C++
    class="kw">return (cError == class="num">0 ? !_StopFlag : false);
class="macro">#undef macroFileName
    }

「配置文件先搁一边,外汇回放接着调」

这一阶段的配置文件工作算告一段落,用户目前能指定早期阶段所需的全部参数,暂时不用再回头折腾配置结构。无论配置文件怎么搭,按现有逻辑都会按预期加载,后续开发可以放心往上叠功能。 下一篇会把回放/模拟系统往外汇和贵金属市场推,股票那套已经更成熟,所以重点补外汇侧的等价实现。外汇和贵金属杠杆高、波动猛,模拟回放只能帮你验证逻辑,真上实盘仍可能因滑点和流动性吃亏。 附带工程包 Market_Replay_ev221.zip 约 14.4 MB,开 MT5 直接丢进对应目录就能跑起当前版本,先熟悉配置字段再等下一篇的回放改造。

常见问题

通常卡在文件解析循环的开头,检查文件路径与格式头是否匹配,先用绝对路径跑通再改相对路径。
点值偏差会让每根K线的波动被放大或压缩,回放出来的买卖盘口和真实外汇节奏对不上,需按经纪商规格重填。
可以,小布能读取你的回放脚本日志,标出参数注入异常和指令解析报错位置,并给出修正建议省去手动排查。
先看循环内是否漏写旗标判定,再确认外部中断事件有没有正确传入,补上状态轮询即可正常退出。
把品种参数硬编码进脚本先跑通流程,但记得给后续接入配置留好接口,避免回头重构成本过高。