开发回放系统(第 54 部分):第一个模块的诞生(基础篇)
◍ 回放系统第一个模块怎么搭
在 MT5 里做价格回放,第一步不是写策略,而是先把历史数据按帧读出来。2025 年 1 月社区里一篇 532 浏览的实例帖,给出了最轻量的模块雏形:只做数据切片,不碰任何指标计算。 外汇与贵金属行情高频跳变,回放模块若直接吃裸 Tick,内存和渲染都会卡死;正确做法是按固定步长把 M1 或 M5 切成“帧”,再用定时器推进。这类模块本身不预测方向,仅还原盘口序列,属高风险的复盘辅助,不代表任何胜率保证。 打开 MT5 按 F4 进 MetaEditor,新建一个 Expert Advisor 空壳,先只留 OnInit 与 OnTimer,就能验证这个思路跑不跑得通。
为什么先补 C 和 Windows 消息机制
这一节承接前一篇「开发回放系统(第 53 部分)」里引入的底层概念。那些概念之后会直接写进 MQL5 和 MT5 的程序逻辑里,但对多数交易者而言是全新的。 有 Windows 系统编程背景的人会一眼认出:我们后续要用的是进程间消息传递那一套。想在 MT5 里真正搞懂「为什么这么写、数据怎么在模块间跑」,先去补一点 Windows 编程和 C 语言更划算。 在 Windows 下找用消息传递通信的程序不难,但没 C 基础很容易误读。建议路线很直接:先学 C,再看 Windows 程序怎么互发消息,之后回来啃 MQL5 回放系统会顺很多。外汇和贵金属杠杆高、回测不等于实盘,这套底层功夫只是降低试错成本,不保证收益。
「把 MT5 事件响应做成模块化消息总线」
早先几周卡在一个认知误区里:把 MetaTrader 5 当成普通宿主程序,而不是自带事件系统的平台。结果多个功能模块各自跑、互不通信,放大到系统层级就冲突或失效。MT5 的灵活性比多数人探到的深,忽略它对图表事件的响应,是拖慢开发的主因。 先从公共定义文件 Defines.mqh 切入口。第 4 行留了 _DEBUG_MODE 宏,运行时想抓变量状态就打开它,不需要大改结构;第 13–14 行搬迁了原 InterProcess.mqh 里的自定义品种名与回放服务名;真正可扩展的是第 25 行起的事件枚举列表。 枚举只能在列表末尾追加,这是硬约束。末尾加新事件,旧代码不用重编;若在中间插,所有依赖代码必须重新编译,否则消息错位。外汇/贵金属自动化本就高波动高风险,这种重编遗漏会直接让图表上多个指标互相误触发。 C_Terminal.mqh 只做两处小改:第 5 行指向新的 Defines.mqh 相对路径,第 6 行删掉旧头文件引用;第 12 行枚举换新值,用来检测图表上是否已有同指标实例。配合片段 2 的替换,重复加载时 _LastError 会被正确置位。 C_Mouse.mqh 到第 168 行才出现分水岭:缓冲区从 5 个变 6 个,第 30 行新变量专供回放服务传“下一根 K 线剩余时间”。妙处在 MT5 强制指标切周期时清零缓冲区——服务侦测到值失效,反向发事件让鼠标指标重填。平台规则反而成了模块间握手信号。 消息处理函数从第 189 行起吃前三条自定义事件:第 194 行先确认调用方是指标自身;第 199 行隐藏价格线;第 202 行恢复显示;第 205 行接收 double 型 dparam 里塞的 8 字节时间值(datetime 强转 double,CPU 只搬字节)。后续 C_Study.mqh 第 150 行把未处理事件透传给 C_Mouse,运行时靠小脚本发事件就能改鼠标指标外观,不用重编译。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_VERSION_DEBUG class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 调试宏与联合体在回放系统里的底层拼装
在 MT5 回放类指标里,先用条件编译把调试输出关进开关:定义了 def_VERSION_DEBUG 才展开 macro_DEBUG_MODE(A),否则宏体为空,实盘加载时不产生任何 Print 开销。 调试版宏会把文件名、行号、函数名和变量名拼成一条日志,例如 Print(__FILE__," ",__LINE__," ",__FUNCTION__+" "+#A+" = "+(string)(A)),方便你定位某次报价解析出错在第几行。 回放标的写死成 "RePlay",最大持仓滑杆上限给到 400,这两个常量直接决定面板能拖多远、仿真品种叫什么,改之前先想清楚会不会冲掉历史缓存。 uCast_Double 这个联合体是位级读数的关键:double 和 long、datetime 共享 8 字节,_int[2] 拆成 2 个 4 字节,_char[8] 拆成 8 个字节。做价格重放时,直接按字节改 _char 数组就能在不触发浮点舍入的情况下注入特定尾数。 事件枚举 EnumEvents 管的是界面显隐:鼠标价线、K线时间、日内波幅、瞬时波幅各自一对 hide/show,另有 evSetServerTime 接管回放系统的虚拟时钟。外汇与贵金属杠杆高,回放验证只是离线推演,实盘映射仍有滑点与时区错位风险。
class="macro">#ifdef def_VERSION_DEBUG class="macro">#define macro_DEBUG_MODE(A) \ Print(__FILE__, " ", __LINE__, " ", __FUNCTION__ + " " + class="macro">#A + " = " + (class="type">class="kw">string)(A)); class="macro">#else class="macro">#define macro_DEBUG_MODE(A) class="macro">#endif class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_SymbolReplay "RePlay" class="macro">#define def_MaxPosSlider class="num">400 class=class="str">"cmt">//+------------------------------------------------------------------+ union uCast_Double { class="type">class="kw">double dValue; class="type">long _long; class=class="str">"cmt">// class="num">1 Information class="type">class="kw">datetime _datetime; class=class="str">"cmt">// class="num">1 Information class="type">int _int[class="kw">sizeof(class="type">class="kw">double) / class="kw">sizeof(class="type">int)]; class=class="str">"cmt">// class="num">2 Informations class="type">char _char[class="kw">sizeof(class="type">class="kw">double) / class="kw">sizeof(class="type">char)]; class=class="str">"cmt">// class="num">8 Informations }; class=class="str">"cmt">//+------------------------------------------------------------------+ enum EnumEvents { evHideMouse, class=class="str">"cmt">//Hide mouse price line evShowMouse, class=class="str">"cmt">//Show mouse price line evHideBarTime, class=class="str">"cmt">//Hide bar time evShowBarTime, class=class="str">"cmt">//Show bar time evHideDailyVar, class=class="str">"cmt">//Hide daily variation evShowDailyVar, class=class="str">"cmt">//Show daily variation evHidePriceVar, class=class="str">"cmt">//Hide instantaneous variation evShowPriceVar, class=class="str">"cmt">//Show instantaneous variation evSetServerTime, class=class="str">"cmt">//Replay/simulation system timer
用 IndicatorCheckPass 卡死单实例加载
在 MT5 自定义指标里,重复把同一个指标拖到同一图表会互相干扰信号。上面这段 C_Terminal 类的成员函数,就是用来在初始化阶段拦掉多实例的。 它在第 160 行先拼一个带 _TMP 后缀的临时名,第 162 行检查 _LastError 是否为零,非零直接返回 false 终止加载。随后第 163 行把临时名写进 INDICATOR_SHORTNAME,第 164 行用 ChartWindowFind 找原短名是否已在图表窗口 0。 若找到(返回不等于 -1),第 166 行删掉刚写的临时指标,第 167 行向日志打 'Only one instance is allowed...',第 168 行通过 SetUserError 抛 ERR_NoMoreInstance。整套逻辑保证同图表只允许一个实例运行,避免金价或欧元美元行情里出现重复画线。 开 MT5 把这段代码塞进你的指标类,拖两次同一品种图表,终端日志应只留一次实例并提示多开被拒。外汇与贵金属波动剧烈,这类防护不承诺收益,只是降低脚本冲突概率。
class="type">bool IndicatorCheckPass(class="kw">const class="type">class="kw">string szShortName) { class="type">class="kw">string szTmp = szShortName + "_TMP"; if (_LastError != ERR_SUCCESS) class="kw">return false; IndicatorSetString(INDICATOR_SHORTNAME, szTmp); if (ChartWindowFind(m_Infos.ID, szShortName) != -class="num">1) { ChartIndicatorDelete(m_Infos.ID, class="num">0, szTmp); Print("Only one instance is allowed..."); SetUserError(C_Terminal::ERR_NoMoreInstance); }
「鼠标交互基类的收尾与对象命名」
指标初始化收尾处,第 172 行用 IndicatorSetString(INDICATOR_SHORTNAME, szShortName) 把缩写名写进指标属性,第 173 行 ResetLastError() 清掉之前可能遗留的错误码,第 175 行 return true 表示初始化成功,若前面校验不过则在第 170 行 return false 中止。 类定义从 C_Mouse 继承 C_Terminal 开始,第 006–010 行用宏拼出四类鼠标辅助对象的名称前缀:水平线 MouseBase_H、竖线 MouseBase_TV、时间线 MouseBase_TT、研究框 MouseBase_TB。改这些前缀能避免和多指标同图时的对象冲突。 第 015 行枚举了市场状态 eCloseMarket / eAuction / eInTrading / eInReplay,第 016 行把鼠标按键和修饰键做成位掩码(eClickLeft=0x01、eSHIFT_Press=0x04、eCTRL_Press=0x08 等),按位或即可同时判多键。外汇与贵金属图表加载此类 EA 仍属高风险环境,回测与实盘表现可能偏离。
class="num">170. class="kw">return false; class="num">171. } class="num">172. IndicatorSetString(INDICATOR_SHORTNAME, szShortName); class="num">173. ResetLastError(); class="num">174. class="num">175. class="kw">return true; class="num">176. } class="num">177. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">001. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">002. class="macro">#class="kw">property copyright "Daniel Jose" class="num">003. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">004. class="macro">#include "C_Terminal.mqh" class="num">005. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">006. class="macro">#define def_MousePrefixName "MouseBase_" class="num">007. class="macro">#define def_NameObjectLineH def_MousePrefixName + "H" class="num">008. class="macro">#define def_NameObjectLineV def_MousePrefixName + "TV" class="num">009. class="macro">#define def_NameObjectLineT def_MousePrefixName + "TT" class="num">010. class="macro">#define def_NameObjectStudy def_MousePrefixName + "TB" class="num">011. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">012. class C_Mouse : class="kw">public C_Terminal class="num">013. { class="num">014. class="kw">public : class="num">015. enum eStatusMarket {eCloseMarket, eAuction, eInTrading, eInReplay}; class="num">016. enum eBtnMouse {eKeyNull = 0x00, eClickLeft = 0x01, eClickRight = 0x02, eSHIFT_Press = 0x04, eCTRL_Press = 0x08, eClickMiddle = 0x10}; class="num">017. class="kw">struct st_Mouse class="num">018. { class="num">019. class="kw">struct st00 class="num">020. { class="num">021. class="type">int X_Adjusted, class="num">022. Y_Adjusted, class="num">023. X_Graphics, class="num">024. Y_Graphics;