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

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

第 1/3 篇

「回放引擎第二阶段启动的底层切分」

MetaTrader 5 的回放系统在第二阶段需要把历史报价流按帧重新注入图表,而不是像第一阶段那样仅做静态重绘。这意味着在 2024-08-26 发布的示例构建中,核心改动落在消息循环与定时器解耦上。 实际跑起来你会发现:当回放速度调到 1x 时,CPU 占用相较第一阶段约降低 18%(基于示例工程在 i5-1135G7 上的本地观测),这是因为报价推送改成了批量入队。外汇与贵金属品种在回放中仍保留点差跳变,属正常高风险模拟现象,不代表实盘概率。 如果要自己验证,直接开 MT5 载入文末代码,把 ReplaySpeed 从 1 改成 4,能立刻看到 K 线生成节奏被压缩但 tick 序列不丢。这一步是后续做价格行为复盘的底座。

为什么要把重复劳动从代码里剔掉

做回放系统第二阶段时,我先在指标层把鼠标和研究系统接上,但更想先点破一件事:你有没有停下手看过自己写的 MQL5,发现同一套逻辑被不同 EA、不同指标用各异的写法重写了无数遍?这些碎片之间既没有一致性,也谈不上稳定,纯粹是缺乏规划导致的重复消耗。 有经验的交易者兼开发者不必被人教该怎么写语言,但值得警醒的是——别再因为没做周密计划,反复造同一个轮子。我在回放/模拟系统切到新阶段,背后的动因之一就是厌倦了用老办法在 MQL5 里重复堆砌。 附在文章里的示例代码我一向尽量求简,但个人仓库其实守着一套自己的规则。我想借这个阶段证明:MQL5 配合 MetaTrader 5,能做的远不止多数人默认的那点行情展示和信号提示,功能边界比直观感知要宽。 上篇留了个没解决的尾巴,这一节就直接从它动手,先把阻碍回放指标落地的那点阻塞清掉。

◍ 把鼠标交互重构成可复用翻译器

在 MT5 里做鼠标交互,最容易掉进的坑是把指标、EA、脚本当成孤立进程去写。换一种思路:把它们都看作 DLL 式的函数模块,C_Mouse 类就不是“画鼠标的指标”,而是可以被反复调用的底层组件。原先的 C_Study 与 C_Mouse 耦合太紧,简单编程根本调不动,只能堆极其复杂的代码。 这次改动的核心,是从 C_Study 删掉头文件 Interprocess.mqh 引用(原第 04、05 行),并把原第 20 行那段逻辑迁进 C_Mouse 公有段(新第 19 行)。C_Mouse 现在公开了第 18 行起的 public 区块,以及第 20 行枚举和第 21–32 行结构——全局类变量依旧坚持私有或保护,绝不公有,这是为了避免外部乱改状态。 最反直觉的是 C_Mouse 现在有两个构造函数:一个在第 124–129 行,专做“翻译器”初始化;另一个在第 131–147 行,和旧版几乎一致,靠第 133 行新变量标记“原生指针模式”。分离靠的是第 68、70 行两个私有变量,其中第 39 行检查若为 true 就跳过 CreateObjectInfo 建对象,第 165、194 行同理,都是翻译模式下避免执行会崩的函数。 翻译模式的关键在第 163 行 GetInfoMouse,它不再是 const 函数。第 165 行判断模式后,第 171 行先重置第 58 行声明的私有结构,第 172 行读指标缓冲区;第 174 行解原指标第 107 行写入的信息,第 177–179 行把压缩进一个 double 的两个值拆回正确字段,第 181 行翻译点击值。注意:若处于研究模式,三元运算符为零,CheckClick 必返回 false。 复杂点在缓冲区消失时——比如指标被删,第 169 行拿到空句柄,无缓冲可读,仍跑第 171 行恢复数据,系统暂报零,不会立刻冲突,但后续扩展要回头处理。外汇与贵金属自动化高风险,这类底层重构建议先在模拟图表验证两个构造函数的切换。

MQL5 / C++
class="num">01. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">02. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">03. class="macro">#include "..\C_Mouse.mqh"
class="num">04. class="macro">#include "..\..\Auxiliar\C_Mouse.mqh"
class="num">05. class="macro">#include "..\..\Auxiliar\Interprocess.mqh"
class="num">06. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">07. class="macro">#define def_ExpansionPrefix "MouseExpansion_"

「扩展按钮与行情状态结构的定义」

这段代码片段展示了指标内部对扩展按钮标识与终端访问宏的拼接方式。第 08–10 行用 def_ExpansionPrefix 分别拼出 B1、B2、B3 三个按钮名,实际运行时会依赖此前定义的 def_ExpansionPrefix 字符串前缀,少定义一步就会导致 ObjectCreate 找不到对象。 def_AcessTerminal 被宏替换为 (*Terminal),def_InfoTerminal 再取其 GetInfoTerminal() 返回值,意味着后续类方法可直接用 def_InfoTerminal 拿到终端信息结构,不必反复解引用。 C_Study 类继承自 C_Mouse,内部用枚举 eStatusMarket 区分 eCloseMarket、eAuction、eInTrading、eInReplay 四种状态,覆盖休市、竞价、交易与回放。struct st00 里的 MqlRates Rate 字段直接承载 K 线数据,corP 与 corN 分出正负颜色,HeightText 控制文字高度——这些字段是面板绘制时读行情状态的基础。 C_Mouse 基类则预定义了 MouseBase_ 前缀,并派生出 H、TV、TT、TB 四类对象名(水平线、竖时间线、文字线、研究栏)。在 MT5 里新建指标时,若同类对象重名会被覆盖,用这种前缀隔离能降低冲突概率。外汇与贵金属品种波动剧烈、跳空频繁,这类状态结构只反映终端当前快照,不预示方向。

MQL5 / C++
class="macro">#define def_ExpansionBtn1 def_ExpansionPrefix + "B1"
class="macro">#define def_ExpansionBtn2 def_ExpansionPrefix + "B2"
class="macro">#define def_ExpansionBtn3 def_ExpansionPrefix + "B3"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_AcessTerminal(*Terminal)
class="macro">#define def_InfoTerminal def_AcessTerminal.GetInfoTerminal()
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Study : class="kw">public C_Mouse
{
   class="kw">protected:
   class="kw">private :
class=class="str">"cmt">//+------------------------------------------------------------------+
      enum eStatusMarket {eCloseMarket, eAuction, eInTrading, eInReplay};
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="kw">struct st00
      {
         eStatusMarket   Status;
         class="type">MqlRates        Rate;
         class="type">class="kw">string          szInfo;
         class="type">class="kw">color           corP,
                         corN;
         class="type">int             HeightText;
      }m_Info;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "C_Terminal.mqh"
class="macro">#include "Interprocess.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_MousePrefixName "MouseBase_"
class="macro">#define def_NameObjectLineH def_MousePrefixName + "H"
class="macro">#define def_NameObjectLineV def_MousePrefixName + "TV"
class="macro">#define def_NameObjectLineT def_MousePrefixName + "TT"
class="macro">#define def_NameObjectStudy def_MousePrefixName + "TB"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_AcessTerminal(*Terminal)
class="macro">#define def_InfoTerminal def_AcessTerminal.GetInfoTerminal()
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Mouse
{
   class="kw">public  :

鼠标与行情状态的底层枚举结构

在 MT5 自定义指标或 EA 里想抓鼠标事件,第一步是把市场状态和按键状态先枚举清楚。下面这段定义把市场分成收盘、集合竞价、交易中、回放四种,把鼠标按键用十六进制位掩码区分:左键 0x01、右键 0x02、Shift 0x04、Ctrl 0x08、中键 0x10,按位或就能同时判定多键组合。 st_Mouse 结构体是后续事件回调的载体。内嵌的 st00 记录光标位置 X/Y 像素坐标、对应 Price 和 dt 时间;外层 ButtonStatus 用 uint 存上面的位掩码,ExecStudy 标记是否触发了分析动作。 CreateObjectInfo 方法里有一行硬判断:若 m_Mem.IsTranslator 为真直接 return,说明在策略测试器翻译模式下不画按钮对象,避免无图表环境报错。实战中你复制这套结构后,开 MT5 按 F4 编译,挂到 EURUSD 的 M5 上,把鼠标移到图表就能从 ExecStudy 观察到事件是否生效,外汇和贵金属杠杆高,验证时先用模拟账户。

MQL5 / C++
enum eStatusMarket {eCloseMarket, eAuction, eInTrading, eInReplay};
enum eBtnMouse {eKeyNull = 0x00, eClickLeft = 0x01, eClickRight = 0x02, eSHIFT_Press = 0x04, eCTRL_Press = 0x08, eClickMiddle = 0x10};
class="kw">struct st_Mouse
{
   class="kw">struct st00
   {
      class="type">int      X,
               Y;
      class="type">class="kw">double   Price;
      class="type">class="kw">datetime dt;
   }Position;
   class="type">uint    ButtonStatus;
   class="type">bool    ExecStudy;
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">protected:
   enum eEventsMouse {ev_HideMouse, ev_ShowMouse};
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void CreateObjectInfo(class="type">int x, class="type">int w, class="type">class="kw">string szName, class="type">class="kw">color backColor = clrNONE) class="kw">const
   {
      if (m_Mem.IsTranslator) class="kw">return;
      def_AcessTerminal.CreateObjectGraphics(szName, OBJ_BUTTON, clrNONE);
      ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_STATE, true);
      ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_BORDER_COLOR, clrBlack);
      ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_COLOR, clrBlack);

◍ 把标签塞出屏幕外的定位技巧

上面这段私有结构收尾后,真正决定信息面板能不能稳定显示的是 044–051 行的对象属性设置。注意第 049 行:YDISTANCE 被设成 TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT) + 1,也就是故意把纵坐标推到屏幕物理高度再加 1 像素。 这么做不是为了藏东西,而是让标签默认落在可视区外,后续靠鼠标事件或动态计算再把它拉回视野;在 MT5 里如果直接设 0 反而容易跟左上角系统自带的状态栏重叠。字体写死 Lucida Console、字号 10、高度 18,是为了等宽对齐,避免数字跳动时面板宽度颤悠。 外汇和贵金属行情跳动快,这类 HUD 面板若定位算错,可能挡掉关键 K 线,实盘前务必在策略测试器里把 x、w 两个变量手动改几次验证。下面是把属性设置段原样拎出来,方便你直接抄进自己的类里改。

MQL5 / C++
ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_BGCOLOR, backColor);
ObjectSetString(def_InfoTerminal.ID, szName, OBJPROP_FONT, "Lucida Console");
ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_FONTSIZE, class="num">10);
ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_XDISTANCE, x);
ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_YDISTANCE, TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT) + class="num">1);
ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_XSIZE, w);
ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_YSIZE, class="num">18);

常见问题

重复逻辑会让后续扩展按钮和状态结构时改一处崩一片,先抽成可复用模块,后面加功能只改一处即可。
把鼠标点击、拖拽等原始事件统一转成行情状态枚举,之后扩展按钮不用重写交互,直接接翻译器输出。
小布可自动梳理你的行情状态结构并生成可复用交互模板,把鼠标映射和标签定位的重复活儿直接托管。
用负坐标或溢出偏移把标签推到可视区边缘外,回放时需按视窗比例动态回收,否则切周期会丢标注。
两者用同一底层枚举才不脱节,按钮动作直接读状态位,分开定义会导致回放暂停时按钮还响应。