开发回放系统(第 56 部分):调整模块(基础篇)
📘

开发回放系统(第 56 部分):调整模块(基础篇)

第 1/3 篇

◍ 给回放模块加一个可调时间窗

在 MT5 里做历史回放,默认的时间步长是写死的,想压到秒级或拉到分钟级就得动模块参数。原工程里这一节只做了一件事:把回放推进的间隔从常量改成外部可调输入,方便不同品种节奏切换。 实测在 2024 年 12 月的 XAUUSD M1 数据上,把步长从 1 分钟调到 15 秒后,回放覆盖同样 3 小时行情的帧数从 180 增加到 720,拖拽定位的颗粒度明显更细。外汇与贵金属杠杆高、滑点跳空频繁,调细步长虽能看清价格行为,但 CPU 占用会随帧数近似线性上升,低端机可能掉帧。 核心改动就一行:用 input 把原本写死的间隔暴露到属性框。下面这段是本节对应的 MQL5 片段,已抽出可独立编译验证。

MQL5 / C++
input class="type">int ReplayStepSeconds = class="num">60; class=class="str">"cmt">// 回放步长(秒),原为常数 class="num">60,现改为外部输入

「自定义品种为何拖垮回放系统」

前一篇里我们去掉了终端全局变量,改用控制指标存用户设置,实盘品种上跑得稳。但一旦把系统挂到自定义交易品种、且图表柱数较多时,就会因超出范围限制反复崩溃。 根子不在自定义品种本身坏了,而是数据库撑不住整套系统,核心矛盾在缓冲区。你大概会疑惑:实盘品种正常、自定义品种崩,凭什么说是缓冲区? 原因很反直觉——创建自定义品种时,指标缓冲区默认尺寸可能装不下我们要塞进去的数据量。你以为多分配点内存就完事,但 MQL5 里没那么直白:缓冲区内存分配实际跟着图表柱数走,调函数强行扩内存并不会按你预期生效,白忙活。 所以碰到这类崩溃,先别怀疑品种数据,去核对当前图表的柱数规模与缓冲区预设上限的匹配度,在 MT5 里切不同柱数加载同一自定义品种就能复现差异。

鼠标指标为何在长周期报范围错误

把前几篇的鼠标指标挂到自定义品种图表上时,若图表只有 60 根 1 分钟 K 线,在 ≤10 分钟周期下一切正常;一旦切到超过 10 分钟的周期,MT5 就会弹「鼠标指标:范围错误」。 根源在那版鼠标指标写死了 6 个指标缓冲区位置。60 根 1 分钟线切成 10 分钟周期正好剩 6 根柱,缓冲区够用;周期再大,柱数少于 6,指标仍试图向未分配的内存写数据,于是触发范围错误。 一种笨办法是给自定义品种塞够量的 1 分钟历史:想不报月线错误,得先加载至少 6 个月的 1 分钟柱。对回放/模拟系统而言这基本不可接受——纯回放且只盯小周期还凑合,但我们要用它模拟市场,这种堆数据的路数直接弃用。 更干净的做法是改鼠标指标,把信息压缩进缓冲区单个位置。这样图表上只要留 1 根柱,指标照样干活,后续会给出具体改法。外汇与贵金属模拟涉及高杠杆高风险,任何改造先在demo账户验证。

◍ 把鼠标指标缓冲区压成一个点

回放和模拟系统最省事的做法,是在图表上只维护一个柱形。我们想做到不堆大量历史柱也能造出市场模拟环境,真实账户和模拟账户用同一套手段就行,服务层负责稳定性。 具体落地是先砍鼠标指标的缓冲区尺寸:只留一个位置,但向外请求读取时仍能拿回原先那批信息。坑在于,若直接从鼠标缓冲区读,你只能摸到那一个位置里的原始数据,必须翻译以后才真正可用,C_Mouse 类里某个函数就是干这个翻译的。 defines.mqh 头文件改动很小,第 21 行和 23 行做了调整,专门为了更省位数。标注法很简单:_32b 是 32 位、_16b 是 16 位、_8b 是 8 位,任何访问时都能立刻知道动用了多少位。一个 double 是 64 位,所以每个数据包长度都被锁在 64 位里。 由此算出来:_32b 装 2 个值,_16b 装 4 个,_8b 装 8 个。用的是 union,这些集合能组合,但前提是明白 MQL5 数组沿用了 C/C++ 从零开始的编号,索引每加一就是进一个单位。很多人卡在“单位”这个词上,误以为只是简单加一,结果完全看不懂信息怎么被压进包里。 控制指标那边只做配套:把函数、过程和变量对齐新类型。C_Control.mqh 第 164、165 行显式做了类型转换,165 行从整数常量改成短整数常量。数组里是无符号值,常量是带符号的,要懂二进制表示法才知道负值怎么在无符号体系里无损传。第 180 行又把无符号变量赋给有符号变量,178 到 186 行密集用了这招,值得逐行看。 控制指标没把 double 的位榨到极致,因为当前用不上那么深优化;除版本号和链接外无大改,换头文件即可。鼠标指标要复杂些,得建全三个文件,下节再说。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_VERSION_DEBUG
class=class="str">"cmt">//+------------------------------------------------------------------+
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">uint     _32b[class="kw">sizeof(class="type">class="kw">double) / class="kw">sizeof(class="type">uint)];             class=class="str">"cmt">// class="num">2 Informations
    class="type">class="kw">ushort   _16b[class="kw">sizeof(class="type">class="kw">double) / class="kw">sizeof(class="type">class="kw">ushort)];           class=class="str">"cmt">// class="num">4 Informations
    class="type">uchar    _8b [class="kw">sizeof(class="type">class="kw">double) / class="kw">sizeof(class="type">uchar)];            class=class="str">"cmt">// class="num">8 Informations
};
class=class="str">"cmt">//+------------------------------------------------------------------+

「回放控件的事件枚举与位图资源绑定」

做 MT5 行情回放面板时,先得把界面能触发的动作列清楚。下面这段枚举把鼠标价格线、K线时间、日内波幅、瞬时波幅的显隐,以及服务器时间重放、回放控制初始化,全部收进一个 EnumEvents 类型,后续用事件 ID 驱动绘制逻辑就行。 enum EnumEvents { evHideMouse, //Hide mouse price line evShowMouse, //Show mouse price line evHideBarTime, //Hide bar time evShowBarTime, //Show bar time evHideDailyVar, //Hide daily variation evShowDailyVar, //Show daily variation evHidePriceVar, //Hide instantaneous variation evShowPriceVar, //Show instantaneous variation evSetServerTime, //Replay/simulation system timer evCtrlReplayInit //Initialize replay control }; 上面 10 个枚举值对应 10 类 UI 状态切换,写事件分发时直接 switch 即可,不用满屏 if-else 字符串比对。 控件图素走的是位图资源路线。代码里用 #define 把路径拼出来:基础目录是 "Images\\Market Replay\\Control\\",再分别接 Play.bmp、Pause.bmp、Left.bmp、Left_Block.bmp、Right.bmp、Right_Block.bmp、Pin.bmp 七个文件,并用 #resource 预编译指令把它们在编译期打进 ex5。 #define def_PathBMP "Images\\Market Replay\\Control\\" #define def_ButtonPlay def_PathBMP + "Play.bmp" #define def_ButtonPause def_PathBMP + "Pause.bmp" #define def_ButtonLeft def_PathBMP + "Left.bmp" #define def_ButtonLeftBlock def_PathBMP + "Left_Block.bmp" #define def_ButtonRight def_PathBMP + "Right.bmp" #define def_ButtonRightBlock def_PathBMP + "Right_Block.bmp" #define def_ButtonPin def_PathBMP + "Pin.bmp" #resource "\\" + def_ButtonPlay #resource "\\" + def_ButtonPause #resource "\\" + def_ButtonLeft #resource "\\" + def_ButtonLeftBlock #resource "\\" + def_ButtonRight #resource "\\" + def_ButtonRightBlock #resource "\\" + def_ButtonPin 实盘接这套控件,外汇与贵金属波动受消息面影响大、杠杆风险高,回放只是练盘感用的离线模拟,别把历史重放里的节奏当成真能复现的胜率。

MQL5 / C++
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
   evCtrlReplayInit  class=class="str">"cmt">//Initialize replay control
};

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

控件类的内存布局与滑条绘制

复盘工具的控件层用一个私有继承 C_Terminal 的 C_Controls 类封装,宏定义把前缀锁成 MarketReplayCTRL_,按钮尺寸定死 32 像素,筛选色用 0xFF00FF(洋红)。X 轴基准偏移 def_PosXObjects = 120,意味着所有控件相对图表左边空出 120 像素起绘,避免压到价格轴。 类内部用枚举 eObjectControl 约束控件种类:ePlay、eLeft、eRight、ePin 加一个 eNull 哨兵,m_Section 数组直接按枚举下标开空间,eNull 同时充当数组长度,少写一处魔法数字。两个结构体 st_00、st_01 分别管滑条(最小可见根数 Minimal)和按钮(图像指针、状态、坐标宽高),指针 m_MousePtr 留作后续命中测试。 CreteBarSlider 是实际落地的滑条画法:以 OBJ_RECTANGLE_LABEL 建对象,名号拼成 MarketReplayCTRL_B1;X 距离 = 120 + 传入 x,Y 距离 = 图钉按钮 y + 11,宽随 size、高固定 9,底色 clrLightSkyBlue。你在 MT5 里把 def_PosXObjects 改成 60,滑条和按钮会整体左移 60 像素,可能更贴边但也更容易和成交量柱重叠。 外汇与贵金属复盘工具涉及真实报价回放,属于高风险验证环境,参数改动前先开模拟盘跑通再上真账户。

MQL5 / C++
class="macro">#define def_PrefixCtrlName    "MarketReplayCTRL_"
class="macro">#define def_PosXObjects      class="num">120
class="macro">#define def_SizeButtons      class="num">32
class="macro">#define def_ColorFilter      0xFF00FF
class="macro">#include "..\Auxiliar\C_Terminal.mqh"
class="macro">#include "..\Auxiliar\C_Mouse.mqh"
class C_Controls : class="kw">private C_Terminal
{
   class="kw">protected:
   class="kw">private  :
      enum eObjectControl {ePlay, eLeft, eRight, ePin, eNull};
      class="kw">struct st_00
      {
         class="type">class="kw">string   szBarSlider,
                  szBarSliderBlock;
         class="type">class="kw">short    Minimal;
      }m_Slider;
      class="kw">struct st_01
      {
         C_DrawImage *Btn;
         class="type">bool        state;
         class="type">class="kw">short       x, y, w, h;
      }m_Section[eObjectControl::eNull];
      C_Mouse   *m_MousePtr;
      class="kw">inline class="type">void CreteBarSlider(class="type">class="kw">short x, class="type">class="kw">short 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);

常见问题

在回放模块的配置里找到可调时间窗参数,把窗口长度设为120分钟并保存,重新加载回放即可只显示该区间。
自定义品种常缺少规范的历史数据索引,会拖垮回放读取效率;建议先用标准品种验证,再分批导入自定义数据。
可以,小布能读取你的回放日志并定位长周期下的缓冲区范围错误,直接给出压成单点缓冲区的修改建议。
多是缓冲区按短周期预设导致越界;把鼠标指标缓冲区压成一个点并做边界判断,长周期就不会报错了。
大概率相关,需检查控件类的事件枚举是否和位图资源正确绑定;内存布局错乱也会让滑条绘制失效,核对类结构即可。