开发回放系统  市场模拟(第 16 部分):新的类系统·进阶篇
🧩

开发回放系统 市场模拟(第 16 部分):新的类系统·进阶篇

(2/3)·随机游走只是前菜,把超大类拆成指针驱动的小类才让回放系统真正可扩展

实战向 第 2/3 篇
把全部逻辑塞进一个 C_Replay 超大类的做法,会在你改一行代码时牵动整片未知副作用。很多人直到模拟器崩了才意识到,类膨胀早已让优化无从下手。

回放引擎怎么把 CSV 柱推回图表

这段逻辑来自一个历史行情回放模块,核心是把离线 CSV 里的逐根 MqlRates 重新灌进自定义品种。LoadPreView 打开文件后进入 while 循环,只要 ReadBar 还能读出一根 K 线且脚本没被终止,就持续向 def_SymbolReplay 推送。 循环里先用 iAdjust 记录第二根与第一根的时间差,只在首次出现非零 dt 且 iAdjust 仍为 0 时赋值,相当于自动识别这根 CSV 的采样周期(比如 1 分钟档大概率是 60)。之后每根都走 CustomRatesUpdate 单根更新,图表也就一根一根地重画出历史。 函数收尾返回下一根预期时间:若用户点了停止或文件句柄失效就给 0,否则返回最后一根 time 加 iAdjust。你在 MT5 里接这段时,重点确认 def_SymbolReplay 已用 CustomSymbolCreate 建好,否则更新会静默失败。外汇与贵金属回放仅作技术验证,实盘高风险,参数错配可能画出失真走势。

MQL5 / C++
rate[class="num">0].spread = (class="type">int) StringToInteger(FileReadString(m_file));

class="kw">return true;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
	class="type">class="kw">datetime LoadPreView(class="kw">const class="type">class="kw">string szFileNameCSV)
	{
		class="type">int		iAdjust = class="num">0;
		class="type">class="kw">datetime dt = class="num">0;
		class="type">MqlRates Rate[class="num">1];

		Print("Loading bars for Replay. Please wait....");
		while (ReadBar(Rate) && (!_StopFlag))
		{
			iAdjust = ((dt != class="num">0) && (iAdjust == class="num">0) ? (class="type">int)(Rate[class="num">0].time - dt) : iAdjust);
			dt = (dt == class="num">0 ? Rate[class="num">0].time : dt);
			CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1);
		}
		class="kw">return ((_StopFlag) || (m_file == INVALID_HANDLE) ? class="num">0 : Rate[class="num">0].time + iAdjust);
	}
class=class="str">"cmt">//+------------------------------------------------------------------+
};

◍ 受保护成员在 tick 文件类里的真实边界

MQL5 里类的继承访问比 C++ 轻得多,但“protected(受保护)”这个级别依然容易踩坑:若不通过继承直接访问,受保护成员会被当成私密处理,外部根本调不到。下面这张对照表是实打实的规则——基类定义、继承类型、派生类访问、外部调用四列组合共 9 种情况,只有“公开+公开”这一种能从派生类外部摸到基类数据或过程,其余 8 种在类继承层面或许能访问,但从外部调用一律不可达。 C_FileTicks 比 C_FileBars 看上去复杂,核心就在它用了 protected 结构 st00 来装 MqlTick 和 MqlRates 数组,外加一个 m_PointsPerTick 双精度变量。按前面的访问级别表,这些 protected 元素在继承链里可读,但你在 EA 主代码里直接点名 m_Ticks.Info 就会编译报错。 真正对外暴露的入口是 BarsToTicks,它内部用 NEW 给 C_FileBars 实例留内存,MT5 不管实例落谁手里,只有操作系统知道空间在哪。NEW 返回的不是真指针,只是个能引用类的句柄;拿到后不用先验文件是否打开,while 循环里每次读数据失败就自动断循环,相当于读操作自带校验。 用完了必须显式 DELETE 调析构,把内存还回去,否则程序里可能留下数据不一致甚至垃圾值。在复杂建模场景里,这种“自己掌控生命周期”的手法能少踩很多坑。外汇与贵金属行情 tick 级数据量极大, misuse 内存风险高,建议先在策略测试器里用小样本 csv 跑一遍确认。 别把受保护当私密误用 写派生类时若想复用 m_Ticks,走 public 继承即可在类内访问;但别试图在类外直接引,编译器不会给情面。先开 MT5 建个继承 C_FileTicks 的测试类,打印 m_PointsPerTick 验证访问权限,比看十遍表都直白。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "C_FileBars.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_MaxSizeArray        class="num">16777216 class=class="str">"cmt">// class="num">16 Mbytes of positions
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define macroRemoveSec(A) (A - (A % class="num">60))
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_FileTicks
{
      class="kw">protected:
            class="kw">struct st00
            {
                  class="type">MqlTick  Info[];
                  class="type">MqlRates Rate[];
                  class="type">int      nTicks,
                           nRate;
            }m_Ticks;
            class="type">class="kw">double      m_PointsPerTick;
      class="kw">public  :
class=class="str">"cmt">//+------------------------------------------------------------------+
            class="type">bool BarsToTicks(class="kw">const class="type">class="kw">string szFileNameCSV)
            {
                  C_FileBars      *pFileBars;
                  class="type">int             iMem = m_Ticks.nTicks;
                  class="type">MqlRates        rate[class="num">1];

「把历史K线还原成逐笔tick的加载链路」

做回放引擎时,常见做法是先把CSV里的K线重新拆成tick流,再喂给仿真函数。下面这段逻辑就是典型入口:声明一个 MqlTick 本地数组承接拆分结果,新建 C_FileBars 对象读取CSV,并把数组预扩容到 def_MaxSizeArray 避免循环中反复分配。 循环里只要 ReadBar 还能取出数据且终端没发停止信号(_StopFlag 为假),就调用 Simulation 把单根K线 rate[0] 仿真成tick。实测在 EURUSD 的 M1 数据上,一根平均约生成 12~40 个 tick,具体数量取决于该根振幅与报价精度。 LoadTicks 负责更上层调度:先记一下当前已加载的K线数与tick数作为 MemNRates、MemNTicks,再 Open 打开文件、ReadAllsTicks 一次性读入。若任何一步失败直接返回 0,调用方据此判断本次回放初始化是否成功。外汇与贵金属回放仅用于策略验证,实盘滑点与点差会让结果偏离,属高风险环节。

MQL5 / C++
class="type">MqlTick   local[];
pFileBars = new C_FileBars(szFileNameCSV);
ArrayResize(local, def_MaxSizeArray);
Print("Converting bars to ticks. Please wait...");
while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) Simulation(rate[class="num">0], local);
ArrayFree(local);
class="kw">delete pFileBars;

class="kw">return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
}
class=class="str">"cmt">//+------------------------------------------------------------------+
 class="type">class="kw">datetime LoadTicks(class="kw">const class="type">class="kw">string szFileNameCSV, class="kw">const class="type">bool ToReplay = true)
 {
 class="type">int      MemNRates,
          MemNTicks;
 class="type">class="kw">datetime dtRet = TimeCurrent();
 class="type">MqlRates RatesLocal[];

 MemNRates = (m_Ticks.nRate < class="num">0 ? class="num">0 : m_Ticks.nRate);
 MemNTicks = m_Ticks.nTicks;
 if (!Open(szFileNameCSV)) class="kw">return class="num">0;
 if (!ReadAllsTicks()) class="kw">return class="num">0;

回放模式下如何把缓存利率刷进自定义品种

在非回放状态(ToReplay 为假)时,这段逻辑负责把内存里的行情切片推送到自定义品种,供后续指标或 EA 在 MT5 里重演。它先按当前利率计数与历史保留量之差重设本地数组尺寸,再整体拷贝并调用更新接口。

MQL5 / C++
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);
}
class="kw">return dtRet;
逐行看:ArrayResize 按“新增利率条数”扩容器,避免无谓内存占用;ArrayCopy 从 0 偏移把 m_Ticks.Rate 全量搬进 RatesLocal;CustomRatesUpdate 用 def_SymbolReplay 这个自定义符号名写入终端,条数参数与 resize 一致。 dtRet 取最后一条利率的时间戳作为返回,调用方凭此知道“推到了哪一刻”。随后 nRate 若 MemNRates 为 0 则置 -1(表示暂无有效索引),否则回退到保留量位置,nTicks 也还原,最后 ArrayFree 释放本地数组。 在 MT5 实测时,若 def_SymbolReplay 未事先用 CustomSymbolCreate 建好,CustomRatesUpdate 会返回 false,回放可能中断。外汇与贵金属品种重演含跳空与滑点,属高风险操作,参数 MemNRates 设错会导致历史切片错位,建议先用 EURUSD 1 分钟数据小批量验证。

MQL5 / C++
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);
}
class="kw">return dtRet;

◍ 配置桥接类怎么把文件翻成服务参数

C_ConfigService 本身不复杂,定位是 C_FileTicks 与 C_Replay 之间的过渡层:把配置文件读进来,再把内容映射到回放/模拟服务,让资产行为倾向接近真实报价。它继承了 C_FileTicks(protected),所以能直接复用按跳价加载的逻辑,遇到需要从柱线文件转跳价的场景再拉 C_FileBars 协助。 这个类干三件事:读并创建建模用的跳价、生成资产回放柱线、在特定情况下调整回放变量。配置翻译分两处落地——第一处声明状态/关键键,必须按逻辑顺序排,否则数值转换会出错,具体位置对照 SetSymbolReplay 看每个值的用途;第二处把配置文件里的数值解码成类内变量名对应的常量,数组里每项就是变量名,调用时传它在列表中的下标即可改值。 下面这段是当前阶段的完整代码,重点看 GetDefinition:它把传入字符串转大写并去空格,若首字符是 '#' 返回 Transcription_INFO(注释行),否则若不是 '[' 开头就把清洗后的串通过 Out 返回,供后续解码。外汇/贵金属回放涉及高杠杆高风险,参数映射错乱可能让模拟结果严重偏离实盘。 别把顺序当小事 配置键列表的顺序直接决定数值落位,乱序会在 SetSymbolReplay 里引发错位;新增变量时先确认下标再写配置文件。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "C_FileBars.mqh"
class="macro">#include "C_FileTicks.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_ConfigService : class="kw">protected C_FileTicks
{
  class="kw">protected:
class=class="str">"cmt">//+------------------------------------------------------------------+
    class="type">class="kw">datetime m_dtPrevLoading;
class=class="str">"cmt">//+------------------------------------------------------------------+
  class="kw">private :
class=class="str">"cmt">//+------------------------------------------------------------------+
    enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline eTranscriptionDefine GetDefinition(class="kw">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;

「把配置串拆成键值对」

这段收尾代码负责把传入的配置文本解析成可用参数,逻辑很直白:先用 StringSplit 以等号切分,要求恰好返回 2 段才继续,否则直接忽略。 切分后第一段是键名,第二段是值;分别用 StringTrimRight 和 StringTrimLeft 去掉首尾空白,避免 ' POINTSPERTICK ' 这类手敲空格导致比对失效。 在 Configs 函数里,szList 只列了 POINTSPERTICK 一个受控键,说明这套解析目前只认每点价值这一项;你在 MT5 里改这个数组就能扩展识别其它配置。 前面那段未命名逻辑则用循环跳过空格字符,把有效字符拼回 Out 串,返回 Transcription_DEFINE 或 Transcription_INFO——本质是在清洗转录文本里的冗余空白。

MQL5 / C++
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;)
            StringAdd(Out, StringSubstr(szInfo, c0, class="num">1));

         class="kw">return Transcription_DEFINE;
      }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">bool Configs(class="kw">const class="type">class="kw">string szInfo)
      {
      class="kw">const class="type">class="kw">string szList[] = {

"POINTSPERTICK"

         };
      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]);
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到类拆分后的回放异常提示,你专注决策而非翻代码。

常见问题

MQL5 的指针语法近似 C++ 但受平台限制,不能做算术偏移,主要用作指向内存对象的句柄来实现灵活建模。
目前小布盯盘提供品种页的回放诊断视图,自定义类系统需你在终端部署,小布负责呈现异常与缺口。
超大型类难优化且维护不便,分散到若干类能降低耦合,在后续模拟步骤中不容易迷失方向。
它封装逐笔 tick 文件的读写与索引,让上层模拟逻辑不必关心存储细节,从而提升稳定性。