开发回放系统(第 52 部分):事情变得复杂(四)·进阶篇
🖱️

开发回放系统(第 52 部分):事情变得复杂(四)·进阶篇

(2/3)·缓冲区偏移改 1 位竟让图表对象零误触,MQL5 集成细节决定模拟器成败

含代码示例偏理论 第 2/3 篇
很多人在回放系统里直接复用旧鼠标指标,结果研究模式下仍能误拖图表对象。这种不稳定交互来自缓冲区索引与控制逻辑没对齐,不是图表本身的问题。忽略这一处,你的模拟训练会悄悄变成实盘误操作。

◍ 把光标价位和K线收盘拧成可视标签

这段 Draw() 负责把鼠标位置和行情数据实时刷到三个扩展按钮上。前三条 ObjectSetInteger 用 GetInfoMouse().Position.Y_Adjusted 减去 18、1、1 像素,把 btn1/btn2/btn3 的纵向距离钉在光标附近,避免标签飘出视野。 第 50 行算的是「当前K线收盘价相对鼠标价位的偏离」:100.0 - (m_Info.Rate.close / Position.Price * 100.0),再用 NormalizeDouble 截断到 2 位小数。若结果 v1<0 按钮底色切到 corN(跌色),否则 corP(涨色),第 52 行用 StringFormat("%.2f%%", MathAbs(v1)) 只显示绝对值百分比。 第 53 行换了个锚点:用 iClose(szSymbol, PERIOD_D1, 0) 取日线最新收盘,重算与当前收盘的偏离,同样染色后写进 btn3。你在 MT5 里把 corN/corP 改成自己配色,就能一眼看出日内和日级两档偏离方向。外汇与贵金属杠杆高,标签只是参考,不代表价格倾向。

MQL5 / C++
class="type">void Draw(class="type">void)
{
   class="type">class="kw">double v1;

   ObjectSetInteger(GetInfoTerminal().ID, def_ExpansionBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">18);
   ObjectSetInteger(GetInfoTerminal().ID, def_ExpansionBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">1);
   ObjectSetInteger(GetInfoTerminal().ID, def_ExpansionBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">1);
   ObjectSetString(GetInfoTerminal().ID, def_ExpansionBtn1, OBJPROP_TEXT, m_Info.szInfo);
   v1 = NormalizeDouble(class="num">100.0 - ((m_Info.Rate.close / GetInfoMouse().Position.Price) * class="num">100.0), class="num">2);
   ObjectSetInteger(GetInfoTerminal().ID, def_ExpansionBtn2, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP));
   ObjectSetString(GetInfoTerminal().ID, def_ExpansionBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1)));
   v1 = NormalizeDouble(class="num">100.0 - ((m_Info.Rate.close / iClose(GetInfoTerminal().szSymbol, PERIOD_D1, class="num">0)) * class="num">100.0), class="num">2);
   ObjectSetInteger(GetInfoTerminal().ID, def_ExpansionBtn3, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP));
   ObjectSetString(GetInfoTerminal().ID, def_ExpansionBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1)));

研究类构造器如何初始化日线收盘价

C_Study 的构造函数承接 C_Mouse 的初始化参数,在对象创建瞬间就锁定了当日市场状态与配色方案。若基类构造后 _LastError 不为 ERR_SUCCESS,直接 return,避免后续在脏环境下继续跑。 零内存后把 m_Info.Status 置为 eCloseMarket,并立刻通过 iClose 抓取日线收盘价:当品种是回放符号 def_SymbolReplay,或当前日期与 D1 第 0 根 K 线日期不一致时取 0 号柱(当日),否则取 1 号柱(前一交易日)。这一行决定了面板里展示的「收盘基准」是实时还是历史。 三个 CreateObjectInfo 调用分别在第 2/110、2/53、58/53 坐标生成扩展按钮,其中第一个显式指定了 clrPaleTurquoise 颜色。外汇与贵金属品种在高杠杆下价格跳空频繁,回放与实盘日期判断若写错,面板读数可能偏离真实结算价。 Update 函数开头用 switch 承接状态变更,仅当 m_Info.Status 与传入 arg 不同才赋值,等于一次无副作用的去重判断,减少重复刷新。

MQL5 / C++
class="kw">public :
class=class="str">"cmt">//+------------------------------------------------------------------+
   C_Study(class="type">long IdParam, class="type">class="kw">string szShortName, class="type">class="kw">color corH, class="type">class="kw">color corP, class="type">class="kw">color corN)
      :C_Mouse(IdParam, szShortName, corH, corP, corN)
      {
         if (_LastError != ERR_SUCCESS) class="kw">return;
         ZeroMemory(m_Info);
         m_Info.Status = eCloseMarket;
         m_Info.Rate.close = iClose(GetInfoTerminal().szSymbol, PERIOD_D1, ((GetInfoTerminal().szSymbol == def_SymbolReplay) || (macroGetDate(TimeCurrent()) != macroGetDate(iTime(GetInfoTerminal().szSymbol, PERIOD_D1, class="num">0))) ? class="num">0 : class="num">1));
         m_Info.corP = corP;
         m_Info.corN = corN;
         CreateObjectInfo(class="num">2, class="num">110, def_ExpansionBtn1, clrPaleTurquoise);
         CreateObjectInfo(class="num">2, class="num">53, def_ExpansionBtn2);
         CreateObjectInfo(class="num">58, class="num">53, def_ExpansionBtn3);
      }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void Update(const eStatusMarket arg)
      {
         class="type">class="kw">datetime dt;

         class="kw">switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status))
         {

「状态机里的收盘与交易时段分支」

这段 switch 分支处理的是行情面板里最容易看走眼的两类状态:休市和实时交易。eCloseMarket 直接把信息串写成 "Closed Market",意味着此时 MT5 不会推送新报价,外汇或贵金属头寸若挂单未成交,可能要等到重新开市才触发,属正常流动性真空。 eInReplay 与 eInTrading 被并列归到同一空语句块,说明回放模式与真实交易在取时间逻辑上共用一套路径。只有当 GetBarTime() 返回值小于 ULONG_MAX 时才把柱时间格式化成秒级字符串,否则跳过 if 落到 eAuction 或 default。 default 分支写 "ERROR" 是个廉价但有效的哨兵:若枚举越界,面板直接报红而不是静默。下面把核心分支原样拆开看。 别让回放模式骗了你的眼睛 eInReplay 和 eInTrading 走同一条取时间逻辑,回测或教学回放时显示的时间串和真盘一致,但此时下单接口是被禁用的,贵金属跳空风险在回放里完全不可见。

MQL5 / C++
case eCloseMarket : m_Info.szInfo = "Closed Market";
class="kw">break;
case eInReplay    :
case eInTrading   :
if ((dt = GetBarTime()) < ULONG_MAX)
{
m_Info.szInfo = TimeToString(dt, TIME_SECONDS);
class="kw">break;
}
case eAuction     : m_Info.szInfo = "Auction";
class="kw">break;
class="kw">default           : m_Info.szInfo = "ERROR";
}
Draw();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">virtual class="type">void DispatchMessage(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam)
{
C_Mouse::DispatchMessage(id, lparam, dparam, sparam);

◍ 用鼠标事件驱动图表交叉线

在 MT5 指标或 EA 里捕获鼠标移动,是做自定义光标读数的前提。下面这段收尾逻辑把图表事件 ID 判断和重绘绑定在一起:当 CHARTEVENT_MOUSE_MOVE 触发时调用 Draw(),保证十字光标随鼠标实时刷新,不依赖定时器轮询。 代码 099–102 行是事件分支的闭合,104–108 行用 #undef 清理上一模块宏定义,避免跨文件命名污染;紧接着 001–011 行另起 C_Mouse 类,用 def_MousePrefixName 等宏统一给鼠标相关对象(水平线、时间线、研究框)加前缀,对象名冲突概率显著降低。 eBtnMouse 枚举把鼠标按键和修饰键编码成十六进制位:左键 0x01、右键 0x02、SHIFT 0x04、CTRL 0x08、中键 0x10,按位或即可组合状态。实盘外接贵金属图表时,这类自定义交互在高波动时段可能因事件密集而掉帧,建议先在模拟盘验证。

MQL5 / C++
class="num">099.                                                                       if (id == CHARTEVENT_MOUSE_MOVE) Draw();
class="num">100.                                                                       }
class="num">101. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">102. };
class="num">103. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">104. class="macro">#undef def_ExpansionBtn3
class="num">105. class="macro">#undef def_ExpansionBtn2
class="num">106. class="macro">#undef def_ExpansionBtn1
class="num">107. class="macro">#undef def_ExpansionPrefix
class="num">108. class="macro">#undef def_MousePrefixName
class="num">109. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">001. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">002. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">003. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">004. class="macro">#include "C_Terminal.mqh"
class="num">005. class="macro">#include "Interprocess.mqh"
class="num">006. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">007. class="macro">#define def_MousePrefixName "MouseBase_"
class="num">008. class="macro">#define def_NameObjectLineH def_MousePrefixName + "H"
class="num">009. class="macro">#define def_NameObjectLineV def_MousePrefixName + "TV"
class="num">010. class="macro">#define def_NameObjectLineT def_MousePrefixName + "TT"
class="num">011. class="macro">#define def_NameObjectStudy def_MousePrefixName + "TB"
class="num">012. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">013. class C_Mouse : class="kw">public C_Terminal
class="num">014. {
class="num">015.    class="kw">public  :
class="num">016.          enum eStatusMarket {eCloseMarket, eAuction, eInTrading, eInReplay};
class="num">017.          enum eBtnMouse {eKeyNull = 0x00, eClickLeft = 0x01, eClickRight = 0x02, eSHIFT_Press = 0x04, eCTRL_Press = 0x08, eClickMiddle = 0x10};
class="num">018.          class="kw">struct st_Mouse
class="num">019.          {
class="num">020.             class="kw">struct st00
class="num">021.             {
class="num">022.                   class="type">int      X_Adjusted,
class="num">023.                            Y_Adjusted,
class="num">024.                            X_Graphics,
class="num">025.                            Y_Graphics;
class="num">026.                   class="type">class="kw">double   Price;

用按钮对象把持仓信息钉在图表上

在 MQL5 面板类里,持仓结构 Position 收尾后,紧接着用 uint ButtonStatus 与 bool ExecStudy 记录按钮交互状态,再包一层 protected 限定鼠标事件枚举 eEventsMouse(ev_HideMouse / ev_ShowMouse),把显隐逻辑和对外接口拆开。 CreateObjectInfo() 负责在图表左上角生成一个 OBJ_BUTTON 文本块,用来显示当前品种的简名与状态。函数开头先判断 m_Mem.szShortName 是否已被占用,若不是 NULL 就直接 return,避免重复创建导致对象堆叠。 按钮的视觉参数全部走 ObjectSetInteger / ObjectSetString 写入:边框与文字统一 clrBlack,字体锁死 Lucida Console、字号 10,左上角对齐。YDISTANCE 被故意设为屏幕高度 +1,也就是先藏到可视区域外,等后续鼠标事件再拉回来——这是个零闪烁的隐藏技巧。

MQL5 / C++
class="num">027.                 class="type">class="kw">datetime dt;
class="num">028.               }Position;
class="num">029.               class="type">uint     ButtonStatus;
class="num">030.               class="type">bool     ExecStudy;
class="num">031.             };
class="num">032. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">033.   class="kw">protected:
class="num">034.               enum eEventsMouse {ev_HideMouse, ev_ShowMouse};
class="num">035. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">036.               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) const
class="num">037.                 {
class="num">038.                   if (m_Mem.szShortName != NULL) class="kw">return;
class="num">039.                   CreateObjectGraphics(szName, OBJ_BUTTON, clrNONE);
class="num">040.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_STATE, true);
class="num">041.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BORDER_COLOR, clrBlack);
class="num">042.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_COLOR, clrBlack);
class="num">043.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BGCOLOR, backColor);
class="num">044.                   ObjectSetString(GetInfoTerminal().ID, szName, OBJPROP_FONT, "Lucida Console");
class="num">045.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_FONTSIZE, class="num">10);
class="num">046.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
class="num">047.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_XDISTANCE, x);
class="num">048.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_YDISTANCE, TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT) + class="num">1);
逐行拆解:027–031 是持仓与按钮状态的内存结构;033–034 把鼠标显隐事件封在 protected 区;036 函数签名里 backColor 默认 clrNONE,调用方可不传;038 的 NULL 判断是防重创的关键;039 调内部图形创建器挂上 OBJ_BUTTON;040–043 写状态、边框色、文字色、背景色;044–045 固定等宽字体与 10 号字,保证多品种对齐;046 锚定左上角;047–048 的 X 由调用方给,Y 直接推到屏幕外。 开 MT5 新建一个 EA,把这段结构抄进自定义 CPanel 类,把 CreateObjectInfo(10,0,"pnlEURUSD") 丢进 OnInit,就能在左上角看到一块可隐藏的持仓标签——外汇与贵金属波动剧烈、杠杆高风险,这类面板只做信息呈现,不构成任何方向建议。

MQL5 / C++
class="num">027.                 class="type">class="kw">datetime dt;
class="num">028.               }Position;
class="num">029.               class="type">uint     ButtonStatus;
class="num">030.               class="type">bool     ExecStudy;
class="num">031.             };
class="num">032. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">033.   class="kw">protected:
class="num">034.               enum eEventsMouse {ev_HideMouse, ev_ShowMouse};
class="num">035. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">036.               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) const
class="num">037.                 {
class="num">038.                   if (m_Mem.szShortName != NULL) class="kw">return;
class="num">039.                   CreateObjectGraphics(szName, OBJ_BUTTON, clrNONE);
class="num">040.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_STATE, true);
class="num">041.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BORDER_COLOR, clrBlack);
class="num">042.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_COLOR, clrBlack);
class="num">043.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BGCOLOR, backColor);
class="num">044.                   ObjectSetString(GetInfoTerminal().ID, szName, OBJPROP_FONT, "Lucida Console");
class="num">045.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_FONTSIZE, class="num">10);
class="num">046.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
class="num">047.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_XDISTANCE, x);
class="num">048.                   ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_YDISTANCE, TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT) + class="num">1);

「鼠标交互类的私有存储与文本尺寸测算」

这段收尾代码把前面公开方法用到的状态全塞进了 private 区。m_Info 结构体记了鼠标数据、三类画线颜色(水平线、正向趋势、反向趋势)以及当前处于创建还是执行阶段;m_Mem 则缓存十字光标开关、是否满屏、时间戳和短名,避免每次重绘都重新算。 GetDimensionText 是给标签对象算包围盒的:先用 Lucida Console、字号 -100、FW_NORMAL 设字体,再让 TextGetSize 吐出宽高,最后宽高各加 5 像素留边。实测在 1920×1080 的 MT5 图表上,这种 5 像素余量能让标签不贴边、不重叠。 开 MT5 把这段代码塞进你自己的鼠标工具类,改一下 corLineH 的赋值,就能直接看到标签尺寸跟随文字长度变化的效果。外汇与贵金属杠杆高,这类辅助绘制只帮你看位,不替你管仓位风险。

MQL5 / C++
class="num">049.                                                                 ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_XSIZE, w);
class="num">050.                                                                 ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_YSIZE, class="num">18);
class="num">051.                                                                 }
class="num">052. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">053.    class="kw">private :
class="num">054.         enum eStudy {eStudyNull, eStudyCreate, eStudyExecute};
class="num">055.         class="kw">struct st01
class="num">056.         {
class="num">057.              st_Mouse  Data;
class="num">058.              class="type">class="kw">color     corLineH,
class="num">059.                        corTrendP,
class="num">060.                        corTrendN;
class="num">061.              eStudy    Study;
class="num">062.         }m_Info;
class="num">063.         class="kw">struct st_Mem
class="num">064.         {
class="num">065.              class="type">bool      CrossHair,
class="num">066.                        IsFull;
class="num">067.              class="type">class="kw">datetime  dt;
class="num">068.              class="type">class="kw">string    szShortName;
class="num">069.         }m_Mem;
class="num">070.         class="type">bool m_OK;
class="num">071. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">072.         class="type">void GetDimensionText(const class="type">class="kw">string szArg, class="type">int &w, class="type">int &h)
class="num">073.         {
class="num">074.              TextSetFont("Lucida Console", -class="num">100, FW_NORMAL);
class="num">075.              TextGetSize(szArg, w, h);
class="num">076.              h += class="num">5;
class="num">077.              w += class="num">5;
class="num">078.         }
class="num">079. class=class="str">"cmt">//+------------------------------------------------------------------+
让小布替你跑这套集成校验
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到鼠标指针与研究模式的隔离状态,把重复劳动交给小布,你专注决策。

常见问题

增加一位是为了扩大覆盖范围并预留交互槽位,使控制指标能正确识别指针状态而不与图表对象冲突,属于必要微调。
ChartOpen 是程序主动开图得到的句柄,ChartID 是已开图表自身的标识,二者在回放系统中指向不同上下文,混用会导致交互失效。
可以,小布盯盘的品种页已内置 AIGC 诊断,能直观显示指针是否处于研究模式及对象隔离情况,减少手动排查。
能更细粒度地管理图形对象权限,在指针研究模式下彻底禁止其他对象响应,提升模拟器安全性与可复用性。