开发回放系统(第 55 部分):控制模块(基础篇)
📘

开发回放系统(第 55 部分):控制模块(基础篇)

第 1/3 篇

◍ 回放系统的控制模块怎么搭

在 MT5 里做历史行情回放,控制模块决定暂停、步进和速度切换何时生效。没有它,回放只能一路跑到尾,没法定点观察某根 K 线形成时的挂单与指标状态。 核心思路是用一个全局标志位记录运行状态,主循环每帧先读标志再决定要不要推进 CopyTicksRange 取到的数据。下面这段是最小可用骨架,直接丢进 EA 的 OnTick 旁就能改。 外汇与贵金属回放属高风险验证环境,结论仅代表历史数据重演可能,不预示实盘走向。把速度挡位从 1x 调到 10x 后,同样 1 小时行情在回放里可能 6 分钟跑完,方便快速定位异动段。

MQL5 / C++
class="kw">input class="type">int ReplaySpeed = class="num">1; class=class="str">"cmt">// 回放速度倍率
class="type">int replayState = class="num">0; class=class="str">"cmt">// 0停 1播
class="type">void ControlTick() {
   if(replayState==class="num">1) {
      CopyTicksRange(Symbol(), ticks, COPY_TICKS_ALL, start, start+ReplaySpeed*class="num">1000);
      start += ReplaySpeed*class="num">1000;
   }
}

回放系统为何改用模块消息而非全局变量

上一篇文章里我们已经拼出了回放/模拟器系统的第一个真实模块。这个模块除了嵌进整套系统,也能单独拿出来用,省得每次都为个性化需求重写大量代码。改模块行为很简单:发一条用户事件消息即可,甚至用个轻量脚本就能完成。 新模型的关键转变在于通信机制。旧版回放系统依赖 MT5 的全局终端变量做程序间交互;现在改成通过用户事件(Custom Event)传消息,全局终端变量可以全部删掉。但代价是:自定义事件若在被接收方不在图表时发出,信息直接丢失,无法补读。 所以必须设计重发机制——同一条控制指令要反复投递,直到目标程序回确认包。按这个标准,回放/模拟器系统收敛为三个核心程序:服务程序、鼠标指标(这两个用户可见),以及作为服务程序隐藏资源的控制指标。不挂服务,控制指标就起不来。 外汇与贵金属品种用这套做训练或实盘都属高风险,模拟回放不等于真实滑点环境。下一步就是改控制指标,让它以模块化方式接管回放服务的管理。

「用缓冲区替掉全局变量传状态」

控制指标这边的改动不大,因为删终端全局变量的动作前面已经做掉一大半。但消息怎么走不通讲清楚,后面读模块会直接迷路。核心变化是:不再用全局终端变量递数据,改走指标缓冲区当通道。 C_Control.mqh 第 150 行用 MQL5 常量 INT_MIN 设滑块最小偏移,是整数变量可能的最小值。现在看不出用意,得等压缩和回放状态那套逻辑铺完才串得起来。第 160 行起把数据写进控制指标缓冲区,目前只压两个值:用户拖滑块的位置(164 行)和指标状态——播放还是暂停(165 行)。这两个值用整数极值区分,避开了空值歧义,后续测试更稳。 重点:信息不是从控制指标直送服务,必须经过缓冲区。第 166 行把联合压缩后的双精度值写进特定缓冲位。消息处理从 169 行开始,第 177 行有个自定义事件负责初始化滑块定位;收到的打包双精度值要翻译且校验,第 180 行测状态值是否为零,为零就 SetUserError 报错了。第 209 行把滑块值存进 m_Slider.Minimal,只在用户点过播放后才存,省掉到处传值的重复检查。 指标本体第 9 行声明不在图表显示,第 10 行要一个内部缓冲区(18、31 行声明,32 行清空前确保为空)。MT5 换周期会清掉图表并重跑 OnInit,指标内状态全丢。过去靠全局变量同步,现在靠缓冲区:服务能看到缓冲,指标刚挂上时缓冲是零或异常,服务侦测到就发特殊事件让指标用正确值重画按钮和滑块。这比让用户可改输入参数安全,也避开了输入参数在指标挂上后没法再交互的坑。 演示服务第 25 行把控制指标挂到新开图表,它不进指标列表;第 29 行加鼠标指标做全套测试。第 36 行循环等控制指标真正挂上(代码跑得比实际快),第 40 行查是否初始化,首次未初始化就 42–44 行发自定义事件去初始化。换周期时第 40 行回 true,服务把存的值塞回指标,状态不丢。外汇/贵金属模拟回放涉及高频重绘与参数误触,实盘外接有高风险控制需求,建议先开 MT5 用附带的演示服务跑通再改自己逻辑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "..\Auxiliar\C_DrawImage.mqh"
class="macro">#include "..\Defines.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_PathBMP        "Images\\Market Replay\\Control\\"

◍ 回放控制面板的位图与常量定义

在 MT5 里做行情回放器,第一步是把按钮图标编译进 EX5。上面这组 #define 把 7 个 BMP 文件名拼到 def_PathBMP 路径后面,再用 #resource 逐条声明,编译器会把这些位图嵌进程序资源段,运行时直接按名调用,不依赖外部文件。 按钮尺寸锁死在 32 像素(def_SizeButtons),拾色滤镜用 0xFF00FF 洋红做透明键——这是 BMP 无 Alpha 通道时的常规做法。控件前缀统一成 MarketReplayCTRL_,对象初始 X 坐标给 120,避免和主图左侧价格轴叠在一起。 类 C_Controls 私有继承 C_Terminal,内部用枚举 eObjectControl 管播放、左移、右移、钉住四个状态,加 eNull 收尾方便数组越界保护。结构体 st_01 里每个按钮挂一个 C_DrawImage 指针和 state 标志,坐标宽高 x/y/w/h 全留着运行时填。 开 MT5 验证时,把 BMP 丢进 def_PathBMP 指向的文件夹,改一行 def_SizeButtons 就能让按钮变大或变小,界面手感立刻不一样。

MQL5 / C++
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=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_PrefixCtrlName       "MarketReplayCTRL_"
class="macro">#define def_PosXObjects          class="num">120
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_SizeButtons          class="num">32
class="macro">#define def_ColorFilter          0xFF00FF
class=class="str">"cmt">//+------------------------------------------------------------------+
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 eObjectControl {ePlay, eLeft, eRight, ePin, eNull};
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="kw">struct st_00
      {
         class="type">class="kw">string   szBarSlider,
                  szBarSliderBlock;
         class="type">int      Minimal;
      }m_Slider;
      class="kw">struct st_01
      {
         C_DrawImage *Btn;
         class="type">bool         state;
         class="type">int          x, y, w, h;
      }m_Section[eObjectControl::eNull];
      C_Mouse   *m_MousePtr;

用矩形标签拼出可拖动的滑条

在 MT5 自定义控件里,滑块不一定非要用 OBJ_SLIDER,用两个 OBJ_RECTANGLE_LABEL 叠起来就能做出视觉可辨的滑条区。下面这段 CreteBarSlider 就是典型做法:先建一个浅蓝底、黑边、宽 3 像素的细条当轨道,再建一个 RosyBrown 底、凸起边框的方块当滑块把手。 轨道的 Y 坐标取 m_Section[ePin].y + 11,高度锁死 9 像素;把手的 Y 取 m_Section[ePin].y + 6,高度 19 像素,比轨道高出 10 像素形成包裹感。两者 X 都从 def_PosXObjects + x 起算,size 参数直接决定轨道长度,把手初始宽度跟随建对象时的默认。 SetPlay 里能看到另一个细节:播放按钮用 C_DrawImage 封装,首次调用时若 m_Section[ePlay].Btn 为 NULL 才 new 一个,并传入 Play / Pause 两套位图资源。这种懒加载避免在面板初始化时把用不到的按钮全建出来。 开 MT5 把这段代码塞进你的面板类,改 def_PosXObjects 和 ePin 的 y 值,就能直观看到滑条落在图表左上区的相对位置,外汇与贵金属图表叠加此类控件时需注意高风险下的视觉遮挡问题。

MQL5 / C++
class="kw">inline class="type">void CreteBarSlider(class="type">int x, class="type">int size)
    {
     ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSlider = def_PrefixCtrlName + "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);
     ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT);
     ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSliderBlock = def_PrefixCtrlName + "B2", OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0);
     ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XDISTANCE, def_PosXObjects + x);
     ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YDISTANCE, m_Section[ePin].y + class="num">6);
     ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YSIZE, class="num">19);
     ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BGCOLOR, clrRosyBrown);
     ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BORDER_TYPE, BORDER_RAISED);
    }
    class="type">void SetPlay(class="type">bool state)
    {
     if (m_Section[ePlay].Btn == NULL)
        m_Section[ePlay].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_PrefixCtrlName + EnumToString(ePlay), def_ColorFilter, "::" + def_ButtonPlay, "::" + def_ButtonPause);

常见问题

全局变量在多线程回放里容易互相覆盖、难追踪,模块消息把状态变更封装成事件,谁改的、改了啥一目了然,调试更省事。
建一个固定长度的数组当环形缓冲区,控制模块往里写当前帧号和速度,渲染模块按序读,避免直接共享变量导致的竞态。
可以,小布能根据你的品种页参数直接生成矩形标签拼滑条的代码骨架,并把位图常量定义好,你只调坐标和配色。
把按钮、滑道、滑块分别指定独立位图ID和枚举常量,集中放一个头文件,改样式时只动常量不碰逻辑。
只在鼠标移动事件里重绘被拖动的那个标签,其余标签设不可选,并把坐标换算限制在滑道像素范围内,掉帧会少很多。