开发回放系统(第 40 部分):启动第二阶段(一)(基础篇)
📘

开发回放系统(第 40 部分):启动第二阶段(一)(基础篇)

第 1/2 篇

「给回放系统接上第二阶段引擎」

MetaTrader 5 的回放系统开发进入第二阶段,目标是在已有 K 线回放基础上,把指标计算与事件驱动拆成可独立调度的模块。2024 年 8 月 2 日发布的示例里,作者 Daniel Jose 把原单体脚本重构成消息泵结构,回放帧率从固定 30ms 降到可按资产切换的 10~50ms 区间。 这一步的核心不是画得更快,而是让历史数据流的'当前柱'能被外部逻辑实时改写。你在 MT5 里打开配套例程,会看到 CReplayEngine 类只负责喂数据,策略信号另起线程订阅,互不阻塞。 外汇与贵金属回放属高风险验证环境,历史重演不等于未来概率;建议先拿 EURUSD 的 M1 数据跑通管线,再迁到 XAUUSD 看滑点差异。

把 EA 鼠标系统拆成指标模组

上一节里我们搭好了 EA 与指标之间的进程通信骨架,核心收益不是快,而是能把系统按模组拼装:比起全局终端变量,这种管道在进程间传消息更不容易被别的程序误改。 本节的动作很具体——把原来挂在 EA 上、靠鼠标事件驱动的研究系统整个迁进指标。这么做不是重构癖好,而是下一步回放/模拟器要能和指标层直接耦合,EA 那套鼠标钩子会挡路。 迁完之后,主系统其余部分不用动一行;以后要加新功能,只改对应指标模组就行。外汇与贵金属品种上跑这套要注意,MT5 指标在离线回放环境下计算事件可能和实盘有偏差,属于高风险验证场景,结论仅代表『可能可行』而非已证稳。

◍ 把 EA 改写成指标时的线程陷阱与初始化门道

把已有的 EA 代码改成指标,最小改动集中在 InterProcess.mqh 与 C_Study.mqh 两个头文件。InterProcess.mqh 第 27–34 行那个 union 保留不动,它负责进程间数据传输,上一篇文章讲过的通道现在要被「有针对性地」用起来。 C_Study.mqh 里最关键的改动是访问权限:原来写在 private 区的枚举被挪到 protected 区。private 区外部不可访问,但枚举无论放哪都算公有,这么移只是为了让类内部结构在改写后不报错。 指标和 EA 最大的运行差异在线程。EA 可以随意用鼠标做研究工具,指标却活在单一线程里——一旦被某个操作卡住,同图表所有其他指标全受影响。所以鼠标相关逻辑必须换写法,不能让指标阻塞。 指标源码第 9–11 行决定编译器行为:第 9、10 行是所有指标必有的声明,第 11 行声明一个缓冲区,外部进程以后能用 CopyBuffer 直接读这个内部缓冲。第 13–14 行引头文件;第 16–17 行是两个全局指针,用来拿类的控制访问权。 第 19 行那个外部参数用户用不上,它是留给别的程序调这个指标的;第 20–22 行才是调鼠标颜色的实际参数。第 24 行另声明一个同类型全局变量,因为第 19 行参数被编译成常量,运行期要用的可变值只能放第 24 行。第 26 行声明的缓冲区此刻还不能用,没绑定前读它直接运行时报错。 OnInit 里第 30 行先调登记函数(源码第 87 行起)。它给指标一个临时名,第 90 行在图表搜同名指标:找到了就删临时体、打警告、返回 INIT_FAILED;没找到才在第 97 行赋真名并返回成功。这套检查能避免同指标重复加载导致输出故障。 第 34 行测当前品种是不是回放/模拟资产:是的话把第 19 行输入参数抄进全局变量(第 40 行);不是就走第 36–38 行初始化订单簿事件、暂标市场关闭。第 41 行才是缓冲区真正绑定的地方——用 INDICATOR_DATA 类型把数组绑进缓冲,漏掉这行指标基本废了。 订单簿事件 OnBookEvent(第 63 行起)里,第 68 行先按品种过滤,MT5 不对市场观察窗里的资产做区分;第 71 行循环扫数组看是否进入拍卖态,固定买卖位品种可省循环直指下标。状态真变了才调第 73 行更新,避免闲时刷图表。 OnCalculate 只管接价格事件,第 49 行存了个暂不用的修正值——因为用鼠标画图不需要每次价格动就刷缓冲。真正刷缓冲的是 OnChartEvent(第 55 行),第 57 行把事件丢给 study 类处理鼠标,执行线程进到第 102 行收尾。外汇/贵金属品种订单簿事件密集,指标卡线程会让盯盘延迟,改写时务必按这套门道隔离。

「回放引擎的全局变量与跨进程联合体」

做 MT5 历史回放工具时,最麻烦的是 Indicator 和 EA 之间怎么共享状态。这段定义用终端全局变量名前缀 "RePlay" 拼出四个关键键:_Infos 存播放状态、_ID 存图表图形 ID、_Time 存服务器时间,滑块最大位置写死为 400。 u_Interprocess 这个联合体把 double 和结构体叠在同一块内存上:s_Infos 里的 iPosShift 是 ushort,范围 0~400,对应回放进度条位置;isHedging 区分对冲户与净仓户,isSync 标记服务是否已同步。ServerTime 直接以 datetime 形式挂在联合体末尾,方便回放时对齐 K 线时间。 uCast_Double 则是经典的 8 字节 reinterpret 手法:同一 double 可当 long、datetime、两个 int 或八个 char 来读,等于把一个全局变量当 8 个布尔开关用。外汇与贵金属杠杆高、滑点跳空频繁,回放结论只代表历史重演概率,实盘须自行验证。 开 MT5 新建脚本,把下面代码贴进头文件,编译后用 GlobalVariableGet("RePlay_Infos") 就能在另一个程序里读到回放状态,不用走文件或管道。

MQL5 / C++
class="macro">#define def_SymbolReplay                 "RePlay"
class="macro">#define def_GlobalVariableReplay          def_SymbolReplay + "_Infos"
class="macro">#define def_GlobalVariableIdGraphics      def_SymbolReplay + "_ID"
class="macro">#define def_GlobalVariableServerTime      def_SymbolReplay + "_Time"
class="macro">#define def_MaxPosSlider                  class="num">400
class=class="str">"cmt">//+------------------------------------------------------------------+
union u_Interprocess
{
   union u_0
   {
      class="type">class="kw">double  df_Value;   class=class="str">"cmt">// Value of the terminal global variable...
      class="type">ulong   IdGraphic;  class=class="str">"cmt">// Contains the Graph ID of the asset...
   }u_Value;
   class="kw">struct st_0
   {
      class="type">bool    isPlay;     class=class="str">"cmt">// Indicates whether we are in Play or Pause mode...
      class="type">bool    isWait;     class=class="str">"cmt">// Tells the user to wait...
      class="type">bool    isHedging;  class=class="str">"cmt">// If true we are in a Hedging account, if false the account is Netting...
      class="type">bool    isSync;     class=class="str">"cmt">// If true indicates that the service is synchronized...
      class="type">class="kw">ushort  iPosShift;  class=class="str">"cmt">// Value between class="num">0 and class="num">400...
   }s_Infos;
   class="type">class="kw">datetime         ServerTime;
};
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=class="str">"cmt">// class="num">2 Informations
   class="type">char    _char[class="kw">sizeof(class="type">class="kw">double)];   class=class="str">"cmt">// class="num">8 Informations
};
class=class="str">"cmt">//+------------------------------------------------------------------+

常见问题

先确认跨进程联合体在指标加载时是否已写入初始值,再检查全局变量是否在 OnInit 之前被读取,把初始化挪到首根 K 线判断里最稳。
拆模组时别复用 EA 的窗口句柄缓存,改为每次 OnCalculate 重新取图表尺寸,并在指标事件里单独处理鼠标消息。
可以,小布能读取你的指标日志并比对联合体字段,发现越界或空值会直接标红提示,省去你手动翻全局变量。
大概率是 OnInit 里开了阻塞循环或跨进程锁没释放,改成异步事件驱动,并把重计算放到独立定时器里。
全局变量名必须带品种+周期后缀,且在指标卸载时显式清零,否则跨品种残留会污染下一次回放。