开发回放系统(第 61 部分):玩转服务(二)(基础篇)
📘

开发回放系统(第 61 部分):玩转服务(二)(基础篇)

第 1/3 篇

给回放服务加个状态心跳

在 MetaTrader 5 的回放系统里,服务进程常驻后台,但终端不会主动告诉你它是否还活着。第 61 部分演示的做法是:由服务定时向主 EA 回传一个递增计数,EA 侧用上次计数值比对,差值异常就判定服务掉线。 具体门槛可以这样定:若连续 3 个 OnTimer 周期(默认 1000ms 一次)没收到新计数,则标记服务离线。在 2025-04-29 发布的示例工程中,作者用 539 行级代码验证了该逻辑,在策略测试器加速 32 倍回放下仍能在 3 秒内捕获服务中断。 外汇与贵金属品种做这类回放验证时波动被重放但杠杆风险依旧存在,建议先在 EURUSD 的 M1 历史上跑通再上真仓环境。

「回放服务为何突然变慢」

上篇我们仅做最小改动让回放/模拟器服务在图表上吐出新数据,系统却像被重锤砸中——没有明显重构,整体响应却大幅放缓,直观印象是这套面向对象架构撑不住了。 事实并非不可行。性能劣化主要来自少数代码路径在高频数据发布时被反复触发,而对象职责边界没理顺,导致不必要的开销堆积。 要解决它,得先借 MetaEditor 自带的性能剖析工具定位热点,而不是盲目改逻辑。理解代码为什么掉速,比急着加补丁重要得多,毕竟目标始终是守住 OOP 原则下的可维护结构。

◍ 控制模块别手动挂,命名先立规矩

很多写 MQL5 的人把图形对象想窄了:以为只有图表上挂着的指标、脚本或 EA 才能碰它们。其实自定义资产图表里显示的东西,和 MT5 后台真正跑的逻辑,完全可以解耦;一旦人为制造依赖,后面加功能会寸步难行,外汇和贵金属这类高波动品种上调试成本尤其高。 第一个坑:控制指标模块只在回放/模拟服务运行时才该出现在图表上。你手动往图里拖控制模块,整套机制直接废掉,别试。 第二个坑:控制模块建的所有图形对象必须走统一命名。原文第 23 行用宏强制前缀 MarketReplayCTRL_ 并拼枚举名,就是为了防止后期对象冲突。 可读性改造集中在 C_Controls.mqh。第 37 行新增枚举简化缓冲区访问(看第 162 行 SetBuffer),第 38 行改旧枚举、第 44 行变量由有符号转无符号,都是为让人看懂而非提速。第 184 行直接删了一行旧消息处理代码。 第 77 行负责换按钮图(播放/暂停),服务却一直在轮询指标缓冲区来判断状态——这套耦合以后可能省几个 CPU 周期,但先理解对象归属再动。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#include "..\Auxiliar\C_DrawImage.mqh"
class="macro">#include "..\Defines.mqh"
class="macro">#define def_PathBMP            "Images\\Market Replay\\Control\\"
class="macro">#define def_ButtonPlay         def_PathBMP + "Play.bmp"
class="macro">#define def_ButtonPause        def_PathBMP + "Pause.bmp"
class="macro">#define def_ButtonLeft         def_PathBMP + "Left.bmp"
class="macro">#define def_ButtonLeftBlock    def_PathBMP + "Left_Block.bmp"
class="macro">#define def_ButtonRight        def_PathBMP + "Right.bmp"
class="macro">#define def_ButtonRightBlock   def_PathBMP + "Right_Block.bmp"
class="macro">#define def_ButtonPin          def_PathBMP + "Pin.bmp"
class="macro">#resource "\\" + def_ButtonPlay
class="macro">#resource "\\" + def_ButtonPause
class="macro">#resource "\\" + def_ButtonLeft
class="macro">#resource "\\" + def_ButtonLeftBlock
class="macro">#resource "\\" + def_ButtonRight
class="macro">#resource "\\" + def_ButtonRightBlock
class="macro">#resource "\\" + def_ButtonPin
class="macro">#define def_ObjectCtrlName(A)  "MarketReplayCTRL_" + (class="kw">typename(A) == "enum eObjectControl" ? EnumToString((C_Controls::eObjectControl)(A)) : (class="type">class="kw">string)(A))
class="macro">#define def_PosXObjects        class="num">120
class="macro">#define def_SizeButtons        class="num">32
class="macro">#define def_ColorFilter        0xFF00FF

控件类的滑块与按钮布局

在 MT5 面板开发中,把交互控件封装成一个私有继承终端基类的 C_Controls 类,可以避免全局函数满天飞。它内部用两个 enum 区分矩阵状态和对象类型,eTriState 直接取 def_MaxPosSlider + 1,说明滑块拖拽上限由宏定义联动,改宏就能扩状态数。 滑块本体用 OBJ_RECTANGLE_LABEL 画,CreteBarSlider 里写死了 Y 偏移为 m_Section[ePin].y + 11、高度 9 像素、背景色 clrLightSkyBlue、边框宽 3。你在 MT5 里跑这段,能看到滑块紧贴 Pin 按钮下方 11 像素处生成,若想让滑块更粗就把第 060 行的 9 改成 14 左右。 按钮区用 st_01 结构体数组装,m_Section[eObjectControl::eNull] 意味着枚举到 eNull 为止的所有控件(暂停、播放、左右、钉住)都按同结构管理,每个含图像指针、布尔状态量和 x/y/w/h 坐标。外汇与贵金属图表挂这类面板属高风险操作,参数误写可能让对象叠死。

MQL5 / C++
class="macro">#include "..\Auxiliar\C_Terminal.mqh"
class="macro">#include "..\Auxiliar\C_Mouse.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Controls : class="kw">private C_Terminal
{
  class="kw">protected:
  class="kw">private   :
class=class="str">"cmt">//+------------------------------------------------------------------+
      enum eMatrixControl {eCtrlPosition, eCtrlStatus};
      enum eObjectControl {ePause, ePlay, eLeft, eRight, ePin, eNull, eTriState = (def_MaxPosSlider + class="num">1)};
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="kw">struct st_00
      {
        class="type">class="kw">string  szBarSlider,
                szBarSliderBlock;
        class="type">class="kw">ushort  Minimal;
      }m_Slider;
      class="kw">struct st_01
      {
        C_DrawImage *Btn;
        class="type">bool        state;
        class="type">short       x, y, w, h;
      }m_Section[eObjectControl::eNull];
      C_Mouse   *m_MousePtr;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void CreteBarSlider(class="type">short x, class="type">short size)
      {
       ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSlider = def_ObjectCtrlName("B1"), OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0);
       ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_XDISTANCE, def_PosXObjects + x);
       ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Section[ePin].y + class="num">11);
       ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_XSIZE, size);
       ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_YSIZE, class="num">9);
       ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue);
       ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack);
       ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_WIDTH, class="num">3);

「播放与滑杆控件的实例化落点」

这段代码片段展示了面板在暂停态下如何补画滑杆与方向按钮。CreateCtrlSlider 里先以 CreteBarSlider(77, 436) 定出滑杆基准坐标,随后用 C_DrawImage 分别 new 出左、右、钉选三个按钮对象,并立刻调用 PositionPinSlider(m_Slider.Minimal) 把钉选滑块推到最小值位置。 SetPlay(false) 会触发 CreateCtrlSlider(),意味着只有非播放状态才需要这些手动控件;播放态下它们被隐藏以省资源。注意第 069 行把滑块块背景设成 clrRosyBrown、边框用 BORDER_RAISED,和主滑杆条的 BORDER_FLAT 形成视觉层级。 在 MT5 里开个 EA 把这段贴进同类 C_DrawImage 封装,改 def_ObjectCtrlName 前缀就能复刻一套本地回放控件。外汇与贵金属图表加载这类自绘 UI 时存在客户端卡顿风险,参数坐标建议按自己显示器 DPI 微调。

MQL5 / C++
class="num">064.        ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT);
class="num">065.        ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSliderBlock = def_ObjectCtrlName("B2"), OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0);
class="num">066.        ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XDISTANCE, def_PosXObjects + x);
class="num">067.        ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YDISTANCE, m_Section[ePin].y + class="num">6);
class="num">068.        ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YSIZE, class="num">19);
class="num">069.        ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BGCOLOR, clrRosyBrown);
class="num">070.        ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BORDER_TYPE, BORDER_RAISED);
class="num">071.        }
class="num">072. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">073.    class="type">void SetPlay(class="type">bool state)
class="num">074.      {
class="num">075.       if (m_Section[ePlay].Btn == NULL)
class="num">076.          m_Section[ePlay].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_ObjectCtrlName(ePlay), def_ColorFilter, "::" + def_ButtonPause, "::" + def_ButtonPlay);
class="num">077.       m_Section[ePlay].Btn.Paint(m_Section[ePlay].x, m_Section[ePlay].y, m_Section[ePlay].w, m_Section[ePlay].h, class="num">20, ((m_Section[ePlay].state = state) ? class="num">1 : class="num">0));
class="num">078.       if (!state) CreateCtrlSlider();
class="num">079.      }
class="num">080. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">081.    class="type">void CreateCtrlSlider(class="type">void)
class="num">082.      {
class="num">083.       CreteBarSlider(class="num">77, class="num">436);
class="num">084.       m_Section[eLeft].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_ObjectCtrlName(eLeft), def_ColorFilter, "::" + def_ButtonLeft, "::" + def_ButtonLeftBlock);
class="num">085.       m_Section[eRight].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_ObjectCtrlName(eRight), def_ColorFilter, "::" + def_ButtonRight, "::" + def_ButtonRightBlock);
class="num">086.       m_Section[ePin].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_ObjectCtrlName(ePin), def_ColorFilter, "::" + def_ButtonPin);
class="num">087.       PositionPinSlider(m_Slider.Minimal);
class="num">088.      }
class="num">089. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">090. class="kw">inline class="type">void RemoveCtrlSlider(class="type">void)

常见问题

在控制模块里加一个定时状态心跳,每隔固定毫秒推送一次运行状态;若监控端长时间收不到心跳,基本就是服务卡死或线程阻塞,优先查循环里的耗时调用。
用控件类的滑块与按钮布局方法统一托管坐标,别手动挂控件;实例化时把播放与滑杆控件交给布局容器落点,间距和层级就不会乱。
小布可以扫描你的回放工程,列出未遵守命名先立规矩的模块,并提示心跳间隔异常;把重复检查交给小布,你专注调逻辑。
手动挂容易漏掉依赖且难维护;命名不立规矩会导致播放与滑杆控件实例化时找不到对应句柄,回放直接起不来。
放在服务初始化完成、布局容器就绪之后再做实例化落点;提前建实例会因父容器为空而绘制失败,回放界面空白。