开发回放系统  市场模拟(第 19 部分):必要的调整(基础篇)
📘

开发回放系统 市场模拟(第 19 部分):必要的调整(基础篇)

第 1/3 篇

「回放系统里的市场模拟校准」

在 MT5 回放系统第 19 部分的开发中,作者 Daniel Jose 于 2024 年 3 月 13 日提交了一次必要调整,核心是把历史 tick 流重新映射为可控的模拟行情序列,避免回放时跳变。原始提交记录显示该补丁在测试者板块获得 764 次浏览、0 条评论,说明改动偏内部重构、未引发外围讨论。 这类调整对外汇与贵金属回放训练很关键:若模拟行情的时间戳和真实 tick 间隔不一致,价格行为学的形态识别会被误导。MT5 终端内打开回放插件后,建议先核对「每秒 tick 数」与历史样本是否同分布,再开始手动重演。 别把正态当圣经 历史 tick 的重采样不等于真实波动,贵金属夜盘流动性断层仍可能让回放失效,交易前请认清高风险。

多资产回放系统为什么必须先做结构化

如果你只用回放/模拟系统操控单一资产,很多组织层的代码可以完全不进配置文件,系统照样跑。但真实场景里,交易者往往要同时喂若干种甚至成规模的外汇、贵金属资产数据库,这时候源代码的隐形能力就不够用了,得把结构摆到明处。 当前这套回放框架还覆盖不了显著推进流程必需的逻辑。把数据组织方式先改好,后面往里加新函数、新参数(比如研究某品种跳价特征时)才能以最小工作量完成。前作第17、18篇已经演示了如何向市场观察窗口加跳价,算是打好了底,但还缺一些具体拼图,本文就从数据结构改进动手。 外汇与贵金属杠杆高、滑点跳空频繁,回放系统若结构混乱,多资产回测结果可能严重失真。先把架子搭成通用型,后续适应不同品种时才不会反复返工。

◍ 把数据目录收拢到一处配置

回放与模拟系统里,目录若散落在多处代码,一旦数据库变大,漏写或错写路径就会带来两类后果:轻则系统弹警告说找不到数据,重则悄悄加载了错误文件参与计算。把目录统一在单一配置项里指定,这类失误概率会明显下降,但不可能归零。 做法是在类里加一个私有字符串变量 m_szPath,仅类内可见,外部无法覆盖。这样能挡住无意中被内部流程改值的坑——很多「代码为啥不按预期跑」的诡异 bug 就来自这种暗改。变量声明后,必须在构造函数里给一个已知初值,没赋值时也有兜底,避免未初始化引发更严重的问题。 配置解析侧,要把标签 PATH 以全大写加进命令数组,链条第二环对应索引 1。配置文件里写 PATH=<NEW PATH> 即可,改目录不必重新编译系统。下面这段代码是类内配置解析的核心片段,黄色标记处在实际工程里常用宏展开复用。 宏的思路是:用构造里的初值比对运行时的 m_szPath,若相等说明用户没在配置里覆盖,就用默认路径;若等于配置文件读到的串,就采用新地址。这样同一套加载函数能无缝切目录。 涉及外汇与贵金属历史数据回放时,目录组织建议按品种和周期分桶,否则大库检索会拖慢模拟效率,且数据错配风险偏高。

MQL5 / C++
<span class="keyword">class="kw">private</span> :
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">enum</span> eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE};
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">string</span> m_szPath;
<span class="keyword">class="kw">inline</span> <span class="keyword">class="type">bool</span> Configs(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> szInfo)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> szList[] = {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"POINTSPERTICK"</span>,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"PATH"</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;};
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;szRet[];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">char</span>&nbsp;&nbsp;&nbsp;&nbsp;cWho;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (<span class="functions">StringSplit</span>(szInfo, <span class="class="type">class="kw">string">&class="macro">#x27;=&class="macro">#x27;</span>, szRet) == <span class="number">class="num">2</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">StringTrimRight</span>(szRet[<span class="number">class="num">0</span>]);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">StringTrimLeft</span>(szRet[<span class="number">class="num">1</span>]);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (cWho = <span class="number">class="num">0</span>; cWho &lt; <span class="functions">ArraySize</span>(szList); cWho++) <span class="keyword">if</span> (szList[cWho] == szRet[<span class="number">class="num">0</span>]) <span class="keyword">class="kw">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">switch</span> (cWho)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">case</span> <span class="number">class="num">0</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_PointsPerTick = <span class="functions">StringToDouble</span>(szRet[<span class="number">class="num">1</span>]);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> <span class="macro">true</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">case</span> <span class="number">class="num">1</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_szPath = szRet[<span class="number">class="num">1</span>];

「回放配置服务的文件读取与初始化」

在 MT5 里做行情回放,第一步是把 CSV 配置读进服务类。下面这段 C_ConfigService 的 SetSymbolReplay 方法,展示了从打开文件到预分配 tick 数组的完整路径,直接决定了后续回放能不能跑起来。

代码先拼出文件路径宏 macroFileName,若 m_szPath 为空则只用文件名,否则补上 '\\' 分隔。随后用 FileOpen 以 FILE_CSVFILE_READFILE_ANSI 打开 Market Replay 目录下的配置,句柄等于 INVALID_HANDLE 就打印失败并 return false,这一步在外盘高波动时段可能因文件占用而失败,概率不低。

打开成功后立刻 ArrayResize(m_Ticks.Rate, def_BarsDiary) 预扩容量,并把 m_Ticks.nRate 置为 -1、首元素 time 清 0,iLine 从 1 计起。主循环靠 FileIsEnding 与 _StopFlag 双条件退出,cError 非 0 也断,说明解析出错即停。 循环内每行先 FileReadString 丢给 GetDefinition,返回 Transcription_DEFINE 走 WhatDefine 校验,返回 Transcription_INFO 则按 cStage 分支处理。这种状态机写法让你在 MT5 里改配置结构时,只需动 cStage 分支而不碰文件 IO。

MQL5 / C++
         class="kw">return true;
            }
         Print("Variable >>", szRet[class="num">0], "<< undefined.");
         }else
            Print("Definition of configuratoin >>", szInfo, "<< invalid.");

            class="kw">return false;
   }
C_ConfigService()
	:m_szPath(NULL)
	{
	}
class="type">bool SetSymbolReplay(class="kw">const class="type">class="kw">string szFileConfig)
   {
      class="macro">#define macroFileName((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)
      class="type">int      file,
               iLine;
      class="type">char   cError,
               cStage;
      class="type">class="kw">string   szInfo;
      class="type">bool   bBarsPrev;
      C_FileBars *pFileBars;
      
      if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
      {
         Print("Failed to open the 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;
      iLine = class="num">1;
      cError = cStage = class="num">0;
      bBarsPrev = false;
      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);
               class="kw">break;
            case Transcription_INFO:
               if (szInfo != "") class="kw">switch (cStage)
               {

分模式加载的历史数据容错分支

这段 switch 落在文件解析主循环里,用 case 0~5 区分五种历史数据载入路径,每种失败都会把 cError 置成 2~7 的对应码,方便上层定位断点。 case 0 直接给 cError=2 并 break,一般代表前置模式判断未通过;case 1 用 C_FileBars 读预览,LoadPreView 返回 0 则 cError=3,否则 bBarsPrev=true 标记已有前 bars。 case 2 与 case 3 都调 LoadTicks,区别在后者传 false 且不强制覆盖,返回 0 分别置 cError=4/5;case 4 走 BarsToTicks 回退,失败 cError=6;case 5 校验 Configs,失败 cError=7。 循环末行 iLine += (cError>0 ? 0 : 1) 说明只有无错才推进行号,FileClose 后外层再按 cError 决定补读还是弃文件。外汇与贵金属历史 tick 常有缺口,这种分段错误码能在 MT5 里帮你快速看出是哪一类载入挂了。

MQL5 / C++
      case class="num">0:
            cError = class="num">2;
            class="kw">break;
      case class="num">1:
            pFileBars = new C_FileBars(macroFileName);
            if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == class="num">0) cError = class="num">3; else bBarsPrev = true;
            class="kw">delete pFileBars;
            class="kw">break;
      case class="num">2:
            if (LoadTicks(macroFileName) == class="num">0) cError = class="num">4;
            class="kw">break;
      case class="num">3:
            if ((m_dtPrevLoading = LoadTicks(macroFileName, false)) == class="num">0) cError = class="num">5; else bBarsPrev = true;
            class="kw">break;
      case class="num">4:
            if (!BarsToTicks(macroFileName)) 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)
      {

常见问题

不先收拢目录,后面每种资产各写各的路径,加载历史数据时会频繁找不到文件;统一到一处配置能省掉大量容错代码。
常见是初始化顺序错了,先调了数据加载再读配置;正确做法是先完成文件读取与参数初始化,再进历史数据分支。
可以。小布能按你设定的目录结构扫描各资产回放文件,并提示配置缺失或路径错乱,省去手动核对。
在加载分支里加容错:缺数据就跳到下一可用片段并打标记,回放继续跑,事后统一补录即可。
会出现价格跳变、时间戳错位和资产混读,导致模拟信号完全不可信,必须返工重调结构。