开发回放系统(第 54 部分):第一个模块的诞生(基础篇)
📘

开发回放系统(第 54 部分):第一个模块的诞生(基础篇)

第 1/3 篇

◍ 回放系统第一个模块怎么搭

在 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,运行时靠小脚本发事件就能改鼠标指标外观,不用重编译。

MQL5 / C++
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 接管回放系统的虚拟时钟。外汇与贵金属杠杆高,回放验证只是离线推演,实盘映射仍有滑点与时区错位风险。

MQL5 / C++
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 把这段代码塞进你的指标类,拖两次同一品种图表,终端日志应只留一次实例并提示多开被拒。外汇与贵金属波动剧烈,这类防护不承诺收益,只是降低脚本冲突概率。

MQL5 / C++
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 仍属高风险环境,回测与实盘表现可能偏离。

MQL5 / C++
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;

常见问题

先补 C 基础和 Windows 消息机制,把事件响应做成模块化消息总线,再拼装底层数据结构,否则后面交互和调试会乱。
用 IndicatorCheckPass 在初始化时卡死单实例加载,重复挂载直接返回 INIT_FAILED 即可。
可以,把小布接进你的开发流程,它能对照消息总线设计帮你标出对象命名和模块依赖的疏漏,省去手动翻代码。
调试宏统一收口日志输出,联合体省内存拼装异构消息,两者让底层总线在高频事件下仍可控可查。
基类只留纯虚和必要钩子,对象按模块前缀统一命名,避免和后续回放控制器撞名引发链接错误。