开发回放系统 市场模拟(第 16 部分):新的类系统·进阶篇
(2/3)·随机游走只是前菜,把超大类拆成指针驱动的小类才让回放系统真正可扩展
回放引擎怎么把 CSV 柱推回图表
这段逻辑来自一个历史行情回放模块,核心是把离线 CSV 里的逐根 MqlRates 重新灌进自定义品种。LoadPreView 打开文件后进入 while 循环,只要 ReadBar 还能读出一根 K 线且脚本没被终止,就持续向 def_SymbolReplay 推送。 循环里先用 iAdjust 记录第二根与第一根的时间差,只在首次出现非零 dt 且 iAdjust 仍为 0 时赋值,相当于自动识别这根 CSV 的采样周期(比如 1 分钟档大概率是 60)。之后每根都走 CustomRatesUpdate 单根更新,图表也就一根一根地重画出历史。 函数收尾返回下一根预期时间:若用户点了停止或文件句柄失效就给 0,否则返回最后一根 time 加 iAdjust。你在 MT5 里接这段时,重点确认 def_SymbolReplay 已用 CustomSymbolCreate 建好,否则更新会静默失败。外汇与贵金属回放仅作技术验证,实盘高风险,参数错配可能画出失真走势。
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 验证访问权限,比看十遍表都直白。
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,调用方据此判断本次回放初始化是否成功。外汇与贵金属回放仅用于策略验证,实盘滑点与点差会让结果偏离,属高风险环节。
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 里重演。它先按当前利率计数与历史保留量之差重设本地数组尺寸,再整体拷贝并调用更新接口。
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;
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 里引发错位;新增变量时先确认下标再写配置文件。
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——本质是在清洗转录文本里的冗余空白。
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]);