开发回放系统(第 67 部分):完善控制指标(基础篇)
📘

开发回放系统(第 67 部分):完善控制指标(基础篇)

第 1/3 篇

回放系统的控制指标从哪补

这套回放系统做到第 67 部分,核心缺口在控制指标——也就是让使用者能实时知道当前回放进度、速度与被测样本边界的显示层。原文明确写清:示例发布于 2025 年 7 月 4 日 07:52,截至发文被查看 476 次、评论 0 条,属于纯教学用途的演示工程。 作者在原声明里把使用范围卡得很死:内容仅用于教育,除学习和掌握提出的概念外,不应出于任何目的使用此应用程序。外汇与贵金属回测本身带高杠杆高风险,拿这类教学工程直接上实盘属于越界。 落地动作很直接:开 MT5 把该系列前 66 部分的控制面板代码调出来,对照本部分的指标补全逻辑,先在本机策略测试器里跑通回放,别急着接真实报价。

「低流动性模型下的柱形检测短板」

上篇实现的‘判断图表新柱何时出现’的方法,在流动性好的模型上表现稳定,但遇到低流动性或频繁竞价中止的模型就会失效,这类问题暂未处理。 对于只读应用、不碰代码的用户,底层机制改动几乎无感;但正在学 MQL5、想写稳系统的人,代码组织方式会直接改变你日后读写脚本的习惯。看别人怎么用代码拆问题,比死记语法收获更大,同一开发者隔段时间重写的版本也常判若两人。 上一篇为防程序在图表或终端关闭时删除报错,加了保护代码,bug 是堵住了,但那段逻辑还能大幅精简。重构后不仅运行更干净,后续维护源码的成本也会降下来。外汇与贵金属市场高波动、高杠杆,任何回放或检测逻辑都先在模拟环境验证再上实盘。

◍ 把绘图类改成依赖注入更稳

控制指标现在能跑,但 C_DrawImage 类直接攥着 C_Terminal 的继承链,扩展起来容易踩坑。原代码里 C_DrawImage 第 8 行公开继承 C_Terminal,而 C_Controls 在第 31 行是私有继承同一个类——控制指标根本不知道 C_DrawImage 存在,却通过这条隐式链路共享图表 ID,第 156 行那段「可能添加新指标」的检查实际从未被有意义地使用过。 更干净的做法是把 C_Terminal 以指针形式传给 C_DrawImage。改完后在构造函数第 61 行接收该指针,第 63 行调用其方法取图表 ID,第 64 行存进私有变量专供后续改 OBJ_BITMAP_LABEL 属性。第 65–66 行把初始化收进第 48–57 行的结构里,除了第 52 行的嵌套循环,整块基本全用 MQL5 标准库函数。 第 52 行外层循环只递增 Y,内层靠 PM(数组大小,不可改)和 PI(从 0 起)控终止;真正干活的是第 53 行循环:C0 跑满图像宽度,PF 的初值取决于 R180 参数——R180 为 true 时 PF = PI + 图像宽度且逐轮递减,生成镜像,否则直拷原图;第 54 行再按特定色做透明过滤。因为 MQL5 的指针语义和 C/C++ 不同,这些数组拷贝必须手动管内存,优先用库函数能少写坑。 C_Controls 那边也跟着改:第 7–17 行左右按钮只留 LEFT.BMP 和 LEFT_BLOCK.BMP,右按钮靠 C_DrawImage 镜像生成,不必重复定义。第 71、80–82 行创建对象时传 GetPointer(this)——C_Controls 私有继承 C_Terminal,this 指向的实例在继承域内能被 C_DrawImage 构造函数当 C_Terminal 指针用。这结构灵活但有风险:若 C_Terminal 封装不严,C_DrawImage 可能间接改到它的私有状态。 第 110–117 行给滑块销加了 Paint 字符串参数做悬停提示,第 114 行会显示滑块当前相对位置。外汇与贵金属模拟回放不允许直跳时间点,靠这个读数你能在点播放前判断引线是否过了兴趣点,过头就重开应用往前几步重定位,比盲调省力。MT5 里打开改完的 C_DrawImage.mqh 和 C_Controls.mqh 编译验证一下继承传递是否如预期。

回放控制面板的资源与宏定义

做行情回放工具时,控制按钮(播放、暂停、左右步进)最好用位图而不是系统自绘控件,这样在 MT5 图表上拖动不会闪、也不会被主题色干扰。下面这段前置代码就是先把 7 个按钮的 BMP 路径和编译期资源全部钉死。 路径统一收敛到 Images\Market Replay\Control\ 下,Play.bmp、Pause.bmp、Left.bmp、Left_Block.bmp、Right.bmp、Right_Block.bmp、Pin.bmp 共 7 个文件,通过 #define 拼接后再用 #resource 打进 ex5,运行时无需外部依赖。 按钮尺寸定死为 32 像素(def_SizeButtons 32),品红 0xFF00FF 作为透明滤色(def_ColorFilter),这是 BMP 不带 alpha 通道时的常用去背手段。控件 X 坐标基准 def_PosXObjects 120,意味着整排按钮默认贴在图表左侧 120 像素处,你改这个数就能整体平移。 对象命名用宏 def_ObjectCtrlName(A) 自动拼 MarketReplayCTRL_ 前缀,配合 EnumToString 把枚举转成字符串,避免手抖写错对象名。外汇与贵金属回放涉及真实历史 tick,复盘结果仅代表过去概率,实盘仍属高风险。

MQL5 / C++
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\\"
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_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=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_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,

「用矩形标签拼一个滑动条控件」

在 MT5 自定义面板里做滑块,不必依赖第三方库,用 OBJ_RECTANGLE_LABEL 就能叠出两层矩形:底层细条当轨道,上层色块当滑块主体。下面这段 CreteBarSlider 函数给出了直接可抄的坐标与尺寸参数。 轨道 B1 的 Y 偏移取 m_Section[ePin].y + 11,高度锁死 9 像素,背景色 clrLightSkyBlue,边框宽 3、类型 BORDER_FLAT;滑块块 B2 的 Y 偏移为 m_Section[ePin].y + 6,高度 19 像素,背景 clrRosyBrown。两者 X 都用 def_PosXObjects + x 定位,说明滑块是相对面板基准点横向排布的。 开 MT5 新建 EA 把这段结构补进类里,改 def_PosXObjects 和 x 就能看到滑块在图表左上角浮现;外汇与贵金属杠杆高,面板只是辅助,信号仍要以价格行为本身为准。

MQL5 / C++
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_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);
   ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSliderBlock = def_ObjectCtrlName("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);

◍ 播放暂停与滑条控件的回收逻辑

这段收尾代码把面板的交互控件收了口:SetPlay(bool state) 在 state 为假时才会调用 CreateCtrlSlider(),也就是说暂停态才允许出现进度滑条与左右步进按钮,播放态下这些对象不创建。CreateCtrlSlider() 里先判 ePin 按钮非空就直接 return,避免重复 new 出 C_DrawImage 实例导致句柄泄漏。 RemoveCtrlSlider() 用 ChartSetInteger 关掉 CHART_EVENT_OBJECT_DELETE,再 for 循环从 ePlay+1 到 eNull 逐个 delete 按钮指针并置 NULL,最后 ObjectsDeleteAll 按前缀 "B" 清掉残留图形对象。实盘加载这类面板时,若切周期后控件不消失,多半是没走这套删除流程。 开 MT5 把 082 行的提前 return 注释掉,连续点暂停/播放 20 次,用任务管理器看终端内存占用倾向上升,就能验证重复构建的代价。外汇与贵金属图表挂 EA 控件属于高风险操作,误删对象可能让面板失效。

MQL5 / C++
ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BORDER_TYPE, BORDER_RAISED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
   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_ObjectCtrlName(ePlay), def_ColorFilter, "::" + def_ButtonPause, "::" + def_ButtonPlay);
      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);
      if (!state) CreateCtrlSlider();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void CreateCtrlSlider(class="type">void)
   {
      if (m_Section[ePin].Btn != NULL) class="kw">return;
      CreteBarSlider(class="num">77, class="num">436);
      m_Section[eLeft].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_ObjectCtrlName(eLeft), def_ColorFilter, "::" + def_ButtonLeft, "::" + def_ButtonLeftBlock);
      m_Section[eRight].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_ObjectCtrlName(eRight), def_ColorFilter, "::" + def_ButtonRight, "::" + def_ButtonRightBlock);
      m_Section[ePin].Btn = new C_DrawImage(GetInfoTerminal().ID, class="num">0, def_ObjectCtrlName(ePin), def_ColorFilter, "::" + def_ButtonPin);
      PositionPinSlider(m_Slider.Minimal);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void RemoveCtrlSlider(class="type">void)
   {      
      ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false);
      for (eObjectControl c0 = ePlay + class="num">1; c0 < eNull; c0++)
      {
         class="kw">delete m_Section[c0].Btn;
         m_Section[c0].Btn = NULL;
      }
      ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("B"));

常见问题

优先在控制指标初始化阶段从回放核心拉取缺失序列,避免运行时动态申请导致卡顿。
当前模型对低流动性下的微柱识别有短板,可放宽最小变动阈值或单独标记异常柱供后续人工复核。
可以,小布能扫描回放面板资源与宏定义,提示缺失的控制指标并给出补齐建议,省去手动翻代码。
在暂停或退出回放时显式调用滑条与标签的回收函数,断开事件绑定,防止对象残留占用资源。
会,依赖注入让绘图类与回放逻辑解耦,面板崩溃概率下降,后续换肤或扩控件也更省事。