开发回放系统(第 59 部分):新的未来·综合运用
⚙️

开发回放系统(第 59 部分):新的未来·综合运用

(3/3)·模板应用竟让控制指标被静默移除,图表莫名关闭的锅到底该谁背?

含代码示例实战向 第 3/3 篇
很多人以为 ChartApplyTemplate 一调用模板就稳稳生效,结果图表突然被关还查不出原因。真相是 MetaTrader 5 异步处理模板,控制指标被删时收到的是 REASON_REMOVE 而非 REASON_TEMPLATE。这个认知缺口不补上,回放系统随时可能在你最不需要的时候崩。

「按钮对象的坐标与样式落点」

在 MT5 自定义指标或 EA 里,把交互控件藏出屏幕可视区是个实用技巧。下面这段受保护方法把按钮的 Y 坐标直接设成屏幕高度加 1,等于把它推到视图外,但对象仍存在于终端对象池,后续靠代码切换状态就能调出来。 方法 CreateObjToStudy 接收 x(水平距离)、w(宽度)、szName(对象名)和 backColor(背景色,默认 clrNONE)。先判 m_OK 标志,未就绪直接 return;随后建 OBJ_BUTTON 类型对象,并强制 OBJPROP_STATE 为 true,边框与文字色均设 clrBlack,字体锁死 Lucida Console、字号 10,左上角锚定。 关键在 043 行:YDISTANCE 取 TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT)+1,即屏高多 1 像素;XSIZE 用入参 w,YSIZE 固定 18。这样按钮实体在屏外,却不占图表空间,适合做后台状态开关。外汇与贵金属杠杆高,这类隐藏控件若误触发可能改参数,实盘前务必在策略测试器里核对象坐标。 私有段还定义了 eStudy 枚举(空/创建/执行)与两个结构体 st01、st_Mem,前者存鼠标数据、三类画线色和当前研究阶段,后者记十字光标开关、填满标志和时间——这是后续研究状态机的底层容器。

MQL5 / C++
class="kw">protected:
 class=class="str">"cmt">//+------------------------------------------------------------------+
 class="type">void CreateObjToStudy(class="type">int x, class="type">int w, class="type">class="kw">string szName, class="type">color backColor = clrNONE) const
 {
 if (!m_OK) class="kw">return;
 CreateObjectGraphics(szName, OBJ_BUTTON);
 ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_STATE, true);
 ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BORDER_COLOR, clrBlack);
 ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_COLOR, clrBlack);
 ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BGCOLOR, backColor);
 ObjectSetString(GetInfoTerminal().ID, szName, OBJPROP_FONT, "Lucida Console");
 ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_FONTSIZE, class="num">10);
 ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
 ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_XDISTANCE, x);
 ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_YDISTANCE, TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT) + class="num">1);
 ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_XSIZE, w);
 ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_YSIZE, class="num">18);
 }
 class=class="str">"cmt">//+------------------------------------------------------------------+
 class="kw">private :
 enum eStudy {eStudyNull, eStudyCreate, eStudyExecute};
 class="kw">struct st01
 {
 st_Mouse Data;
 class="type">color corLineH,
 corTrendP,
 corTrendN;
 eStudy Study;
 }m_Info;
 class="kw">struct st_Mem
 {
 class="type">bool CrossHair,
 IsFull;
 class="type">class="kw">datetime dt;

量尺面板的文本与图形初始化

这段结构把测量工具的内存状态和绘制动作拆得很清楚。m_Mem 结构里用 szShortName、szLineH、szLineV、szLineT、szBtnS 五个字符串字段分别存短名和四条参考线/按钮的对象名,m_OK 标记初始化是否成功。 GetDimensionText 负责按字体算文字包围盒。它先用 TextSetFont("Lucida Console", -100, FW_NORMAL) 设等宽字体,再用 TextGetSize 取宽高,最后各加 5 像素留白——这意味着你改字体大小时,按钮热区会跟着变,不会卡字。 CreateStudy 在 m_Mem.IsFull 为真时才建图:竖线用 OBJ_VLINE,趋势线用 OBJ_TREND 且线宽设为 2,按钮用 CreateObjToStudy(0,0,...) 锚在左上。建完即把状态切到 eStudyCreate。 ExecuteStudy 接收 memPrice 做差值 v1 = 鼠标价 - memPrice;若左键没点中(!CheckClick(eClickLeft)),直接清状态并恢复 CHART_MOUSE_SCROLL。外汇和贵金属波动快,这种量尺在跳空时 v1 可能瞬间偏大,仅作参考、不预示方向。

MQL5 / C++
class="type">class="kw">string  szShortName,
                 szLineH,
                 szLineV,
                 szLineT,
                 szBtnS;
               }m_Mem;
      class="type">bool m_OK;
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="type">void GetDimensionText(const class="type">class="kw">string szArg, class="type">int &w, class="type">int &h)
         {
            TextSetFont("Lucida Console", -class="num">100, FW_NORMAL);
            TextGetSize(szArg, w, h);
            h += class="num">5;
            w += class="num">5;
         }
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="type">void CreateStudy(class="type">void)
         {
            if (m_Mem.IsFull)
            {
               CreateObjectGraphics(m_Mem.szLineV = macro_NameObjectStudy, OBJ_VLINE, m_Info.corLineH);
               CreateObjectGraphics(m_Mem.szLineT = macro_NameObjectStudy, OBJ_TREND, m_Info.corLineH);
               ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szLineT, OBJPROP_WIDTH, class="num">2);
               CreateObjToStudy(class="num">0, class="num">0, m_Mem.szBtnS = macro_NameObjectStudy);
            }
            m_Info.Study = eStudyCreate;
         }
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="type">void ExecuteStudy(const class="type">class="kw">double memPrice)
         {
            class="type">class="kw">double v1 = GetInfoMouse().Position.Price - memPrice;
            class="type">int w, h;
            
            if (!CheckClick(eClickLeft))
            {
               m_Info.Study = eStudyNull;
               ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true);

◍ 鼠标拖拽时的标签与趋势线重绘

当内存标记 m_Mem.IsFull 为真且处于拖拽完成分支时,脚本先清掉带前缀的临时对象,避免画面残留。若仍满足 IsFull,则进入标签与趋势线的实时刷新逻辑。 第103–104行用 StringFormat 拼出一段信息:当前鼠标与锚点价差绝对值保留 nDigits 位小数,方括号内是两根时间锚点之间的 K 线根数(Bars 差值减1),末尾附上相对 memPrice 的百分比波动,精度锁在 02.02f。 第106–111行把这段文字写进 szBtnS 对象,并根据 v1 正负切换背景色(跌用 corTrendN、涨用 corTrendP),同时按测量出的 w、h 重设按钮尺寸与坐标偏移——注意 Y 轴在负值情形下只上移1像素,正值则贴底对齐。 第112–113行移动 szLineT 到鼠标最新位置,线色同样由 memPrice 与实时 Price 的大小决定。外汇与贵金属波动剧烈,这类浮层只作辅助参考,实际下单仍以小周期确认信号为准,杠杆品种高风险。 下面这段是相关绘制与坐标约束的核心片段,可直接贴入 MT5 自定义指标的事件分支做验证:

MQL5 / C++
if (m_Mem.IsFull) ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName + "T");
 }else if (m_Mem.IsFull)
 {
 class="type">class="kw">string sz1 = StringFormat(" %." + (class="type">class="kw">string)GetInfoTerminal().nDigits + "f [ %d ] %class="num">02.02f%% ",
 MathAbs(v1), Bars(GetInfoTerminal().szSymbol, PERIOD_CURRENT, m_Mem.dt, GetInfoMouse().Position.dt) - class="num">1, MathAbs((v1 / memPrice) * class="num">100.0));
 GetDimensionText(sz1, w, h);
 ObjectSetString(GetInfoTerminal().ID, m_Mem.szBtnS, OBJPROP_TEXT, sz1);
 ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szBtnS, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corTrendN : m_Info.corTrendP));
 ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szBtnS, OBJPROP_XSIZE, w);
 ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szBtnS, OBJPROP_YSIZE, h);
 ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szBtnS, OBJPROP_XDISTANCE, GetInfoMouse().Position.X_Adjusted - w);
 ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szBtnS, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - (v1 < class="num">0 ? class="num">1 : h));
 ObjectMove(GetInfoTerminal().ID, m_Mem.szLineT, class="num">1, GetInfoMouse().Position.dt, GetInfoMouse().Position.Price);
 ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szLineT, OBJPROP_COLOR, (memPrice > GetInfoMouse().Position.Price ? m_Info.corTrendN : m_Info.corTrendP));
 }
 m_Info.Data.ButtonStatus = eKeyNull;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void DecodeAlls(class="type">int xi, class="type">int yi)
{
 class="type">int w = class="num">0;

 xi = (xi > class="num">0 ? xi : class="num">0);
 yi = (yi > class="num">0 ? yi : class="num">0);

「鼠标坐标与图表时间的双向换算」

在 MT5 自定义鼠标交互里,屏幕像素和行情坐标必须反复互转。上面这段用 ChartXYToTimePrice 把鼠标像素 (xi, yi) 换算成时间 dt 和价格 Price,再用 AdjustTime / AdjustPrice 做吸附修正,避免落在非交易时段或非法报价上。 紧接着用 ChartTimePriceToXY 反算回像素,并减去 CHART_WINDOW_YDISTANCE 得到子窗口内的真实纵向偏移。实际跑起来你会发现:若忽略第 128 行这个减去窗口距离的步骤,Y_Adjusted 会整体偏移一个标题栏高度,画线对象就对不上光标。 构造函数里顺手开了 CHART_EVENT_MOUSE_MOVE,同时关掉 CHART_CROSSHAIR_TOOL(第 148–149 行)。这样你的 EA 能收到鼠标移动事件,又不会被系统十字线干扰。外汇和贵金属波动剧烈,这类坐标捕捉逻辑在高波动跳空时可能失效,验证时建议先开模拟盘。

MQL5 / C++
ChartXYToTimePrice(GetInfoTerminal().ID, m_Info.Data.Position.X_Graphics = (class="type">short)xi, m_Info.Data.Position.Y_Graphics = (class="type">short)yi, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price);
m_Info.Data.Position.dt = AdjustTime(m_Info.Data.Position.dt);
m_Info.Data.Position.Price = AdjustPrice(m_Info.Data.Position.Price);
ChartTimePriceToXY(GetInfoTerminal().ID, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price, xi, yi);
yi -= (class="type">int)ChartGetInteger(GetInfoTerminal().ID, CHART_WINDOW_YDISTANCE, GetInfoTerminal().SubWin);
m_Info.Data.Position.X_Adjusted = (class="type">short) xi;
m_Info.Data.Position.Y_Adjusted = (class="type">short) yi;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public  :
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Mouse(const class="type">long id, const class="type">class="kw">string szShortName)
   :C_Terminal(id),
   m_OK(class="kw">false)
   {
    m_Mem.szShortName = szShortName;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
   C_Mouse(const class="type">long id, const class="type">class="kw">string szShortName, class="type">color corH, class="type">color corP, class="type">color corN)
   :C_Terminal(id)
   {
    if (!(m_OK = IndicatorCheckPass(m_Mem.szShortName = szShortName))) SetUserError(C_Terminal::ERR_Unknown);
    if (_LastError != ERR_SUCCESS) class="kw">return;
    m_Mem.CrossHair = (class="type">bool)ChartGetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL);
    ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, true);
      ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, class="kw">false);
    ZeroMemory(m_Info);
    m_Info.corLineH  = corH;
    m_Info.corTrendP = corP;
    m_Info.corTrendN = corN;
    m_Info.Study = eStudyNull;
    if (m_Mem.IsFull = (corP != clrNONE) && (corH != clrNONE) && (corN != clrNONE))

鼠标类析构与状态读取的实现细节

鼠标交互模块的析构函数 ~C_Mouse() 在对象失效时直接 return,避免重复清理;若已初始化则先关闭图表的对象删除事件监听,并按窗口是否存在恢复鼠标移动事件开关。 第163到166行依次调用 ChartSetInteger 复位 CHART_EVENT_OBJECT_DELETE、CHART_EVENT_MOUSE_MOVE、CHART_CROSSHAIR_TOOL,最后用 ObjectsDeleteAll 以统一前缀 def_MousePrefixName 清空所有辅助对象,防止残留水平线拖累图表。 CheckClick 用位与运算判断按键状态:返回 (ButtonStatus & value)==value,调用方传入 eBtnMouse 枚举即可识别左键、右键或中键,无需轮询。 GetInfoMouse 在 m_OK 为假时走指标缓冲读取:通过 ChartIndicatorGet 拿句柄,CopyBuffer 取1根数据,成功(返回值为1)才把 double 强转拆解到 ButtonStatus 与坐标字段,失败则 ZeroMemory 清空。外汇与贵金属品种波动剧烈,这类自定义鼠标捕捉在高杠杆下仅作辅助,实盘信号误触概率不可忽视。

MQL5 / C++
CreateObjectGraphics(m_Mem.szLineH = (def_MousePrefixName + (class="type">class="kw">string)ObjectsTotal(class="num">0)), OBJ_HLINE, m_Info.corLineH);
ChartRedraw(GetInfoTerminal().ID);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
~C_Mouse()
  {
   if (!m_OK) class="kw">return;
   ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, class="kw">false);
   ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, ChartWindowFind(GetInfoTerminal().ID, m_Mem.szShortName) != -class="num">1);
   ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, m_Mem.CrossHair);
   ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">bool CheckClick(const eBtnMouse value)
  {
   class="kw">return (GetInfoMouse().ButtonStatus & value) == value;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline const st_Mouse GetInfoMouse(class="type">void)
  {
   if (!m_OK)
    {
     class="type">class="kw">double Buff[];
     uCast_Double loc;
     class="type">int handle = ChartIndicatorGet(GetInfoTerminal().ID, class="num">0, m_Mem.szShortName);

     ZeroMemory(m_Info.Data);
     if (CopyBuffer(handle, class="num">0, class="num">0, class="num">1, Buff) == class="num">1)
      {
       loc.dValue = Buff[class="num">0];
       m_Info.Data.ButtonStatus = loc._8b[class="num">0];
       DecodeAlls((class="type">int)loc._16b[class="num">1], (class="type">int)loc._16b[class="num">2]);
      }
     IndicatorRelease(handle);
    }

◍ 把鼠标状态压进指标缓冲区的底层写法

这段逻辑解决一个很实际的问题:鼠标在图表上的位置、按键状态如何在不阻塞主线程的前提下,塞进自定义指标的缓冲数组供后续绘制调用。核心思路是用联合体做二进制打包,而不是传一堆独立参数。 SetBuffer 里先声明 uCast_Double info,这是一个把 8 字节 double 重新解释成字节/字数组的联合体。第 199 行把按键状态写进 _8b[0],仅当当前没有激活其他 study 类型(eStudyNull)时才记录 ButtonStatus,否则填 0;第 200、201 行分别把图形坐标 X、Y 以 ushort 写入 _16b[1]、_16b[2],坐标范围被限制在 0~65535 内。 第 202 行是落点:Buff[rates_total - 1] = info.dValue,也就是把打包后的 double 直接写进最新一根 K 线的缓冲位。这样每来一次鼠标事件,指标末位就刷新一次,回看历史柱只会拿到当时打包的快照。 DispatchMessage 负责事件分流。215~220 行处理自定义事件 evHideMouse / evShowMouse,通过 ObjectSetInteger 改水平参考线的 OBJPROP_COLOR 为 clrNONE 或回设 m_Info.corLineH,实现鼠标轨迹线的隐藏与显示;221 行捕获 CHARTEVENT_MOUSE_MOVE 后调 DecodeAlls 把 lparam、dparam 还原成坐标。外汇与贵金属图表上这类交互指标刷新极快,实盘使用需注意高频事件可能带来的 CPU 占用波动。

MQL5 / C++
class="kw">inline class="type">void SetBuffer(const class="type">int rates_total, class="type">class="kw">double &Buff[])
{
   uCast_Double info;
   
   info._8b[class="num">0] = (class="type">uchar)(m_Info.Study == C_Mouse::eStudyNull ? m_Info.Data.ButtonStatus : class="num">0);
   info._16b[class="num">1] = (class="type">class="kw">ushort) m_Info.Data.Position.X_Graphics;
   info._16b[class="num">2] = (class="type">class="kw">ushort) m_Info.Data.Position.Y_Graphics;
   Buff[rates_total - class="num">1] = info.dValue;
}
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)
{
   class="type">int w = class="num">0;
   class="kw">static class="type">class="kw">double memPrice = class="num">0;
   
   if (m_OK)      
   {
      C_Terminal::DispatchMessage(id, lparam, dparam, sparam);
      class="kw">switch (id)
      {
         case (CHARTEVENT_CUSTOM + evHideMouse):
            if (m_Mem.IsFull)  ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szLineH, OBJPROP_COLOR, clrNONE);
            class="kw">break;
         case (CHARTEVENT_CUSTOM + evShowMouse):
            if (m_Mem.IsFull) ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szLineH, OBJPROP_COLOR, m_Info.corLineH);
            class="kw">break;
         case CHARTEVENT_MOUSE_MOVE:
            DecodeAlls((class="type">int)lparam, (class="type">int)dparam);

「鼠标交互下的图形对象重绘逻辑」

这段事件处理代码展示了在 MT5 图表上通过鼠标动作驱动水平线、垂直线与临时线重定位的实现路径。当内存标记 m_Mem.IsFull 为真时,水平线 szLineH 会被 ObjectMove 直接拉到当前持仓价,垂直线 szLineV 则绑定到持仓时间,二者随数据刷新实时贴合。 中键点击触发 CreateStudy 的条件很苛刻:仅在内存未满,或水平线颜色不等于 clrNONE 时才新建分析对象。左键点击且处于 eStudyCreate 状态时,先关闭 CHART_MOUSE_SCROLL 防止误滚动,再把临时线 szLineT 移到鼠标所在时间 dt 与价格 memPrice,并切换状态到 eStudyExecute。 若对象被手动删除(CHARTEVENT_OBJECT_DELETE),只要删的是水平线 szLineH 且内存已满,就立刻用 CreateObjectGraphics 按原色重画,避免分析基准丢失。外汇与贵金属图表上这类自动重绘逻辑能省去手动拖线,但高频重绘在 tick 密集时段可能带来轻微卡顿,实盘前建议在策略测试器里跑一遍确认响应延迟。

MQL5 / C++
if (m_Mem.IsFull) ObjectMove(GetInfoTerminal().ID, m_Mem.szLineH, class="num">0, class="num">0, m_Info.Data.Position.Price);
if ((m_Info.Study != eStudyNull) && (m_Mem.IsFull)) ObjectMove(GetInfoTerminal().ID, m_Mem.szLineV, class="num">0, m_Info.Data.Position.dt, class="num">0);
m_Info.Data.ButtonStatus = (class="type">uchar) sparam;
if (CheckClick(eClickMiddle))
  if ((!m_Mem.IsFull) || ((class="type">color)ObjectGetInteger(GetInfoTerminal().ID, m_Mem.szLineH, OBJPROP_COLOR) != clrNONE)) CreateStudy();
if (CheckClick(eClickLeft) && (m_Info.Study == eStudyCreate))
  {
   ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, class="kw">false);
   if (m_Mem.IsFull)  ObjectMove(GetInfoTerminal().ID, m_Mem.szLineT, class="num">0, m_Mem.dt = GetInfoMouse().Position.dt, memPrice = GetInfoMouse().Position.Price);
   m_Info.Study = eStudyExecute;
  }
if (m_Info.Study == eStudyExecute) ExecuteStudy(memPrice);
m_Info.Data.ExecStudy = m_Info.Study == eStudyExecute;
class="kw">break;
case CHARTEVENT_OBJECT_DELETE:
  if ((m_Mem.IsFull) && (sparam == m_Mem.szLineH))
    CreateObjectGraphics(m_Mem.szLineH, OBJ_HLINE, m_Info.corLineH);
  class="kw">break;

回放模式下的K线时间对齐逻辑

在自研鼠标交互指标里,C_Study 类通过 GetBarTime 方法处理实时与回放两种市场状态的时间基准。回放状态由 m_Info.Status == eInReplay 判定,若 m_Info.TimeDevice 为 ULONG_MAX 则直接返回 ULONG_MAX,意味着设备时间未初始化时不参与计算。 非回放时取 TimeCurrent() 作为当前时间 dt,再用 PeriodSeconds() 拿到周期秒数 i0。第 037 行把 Rate.time 对齐到下一根 K 线开盘:((ulong)dt / i0) * i0 + i0,例如 M1 周期下 dt=1700000123 会进位到 1700000180。 最后返回 m_Info.Rate.time - dt,即距离下一根bar生成的剩余秒数。外汇与贵金属杠杆高,回测这类时间函数时先在 MT5 策略测试器用历史数据跑一遍,确认 eInReplay 分支不会在实盘误触发。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "..\C_Mouse.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_ExpansionPrefix def_MousePrefixName + "Expansion_"
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Study : class="kw">public C_Mouse
{
   class="kw">private   :
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="kw">struct st00
      {
         eStatusMarket  Status;
         class="type">MqlRates       Rate;
         class="type">class="kw">string         szInfo,
                        szBtn1,
                        szBtn2,
                        szBtn3;
         class="type">color          corP,
                        corN;
         class="type">int            HeightText;
         class="type">bool           bvT, bvD, bvP;
         class="type">class="kw">datetime       TimeDevice;
      }m_Info;
class=class="str">"cmt">//+------------------------------------------------------------------+
      const class="type">class="kw">datetime GetBarTime(class="type">void)
         {
            class="type">class="kw">datetime dt;
            class="type">int i0 = PeriodSeconds();
            
            if (m_Info.Status == eInReplay)
            {
               if ((dt = m_Info.TimeDevice) == ULONG_MAX) class="kw">return ULONG_MAX;
            }else dt = TimeCurrent();
            if (m_Info.Rate.time <= dt)
               m_Info.Rate.time = (class="type">class="kw">datetime)(((class="type">ulong) dt / i0) * i0) + i0;

            class="kw">return m_Info.Rate.time - dt;
         }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 鼠标悬停时的浮层绘制逻辑

这个 Draw 函数负责在图表上根据鼠标位置实时刷新三个信息浮层:bvT 控制基础信息文本,bvD 控制鼠标价与收盘价的偏移百分比,bvP 控制日线收盘与当前收盘的偏移百分比。

MQL5 / C++
第 48 行把按钮 1 的纵向距离设为鼠标 Y 坐标减 18 像素,第 49 行写入 m_Info.szInfo 文本;这里 bvT 为真才执行,说明基础浮层只在特定开关打开时显示。 bvD 分支里,v1 用 NormalizeDouble 保留两位小数,算的是 (鼠标价 - 收盘价)/收盘价 × 100,即鼠标位置相对当前收盘的涨跌幅百分比。第 55 行按 v1 正负切换背景色:负用 corN、正用 corP,第 56 行用 StringFormat 输出绝对值的 %.2f%% 文本。 bvP 分支逻辑类似,但基准换成 iClose 的日线收盘价(PERIOD_D1, 0),算的是日线收盘相对当前收盘的偏离。三个分支都依赖 GetInfoMouse().Position.Y_Adjusted 做垂直定位,差值仅 18 或 1 像素,说明浮层紧贴光标。 外汇与贵金属波动快,这类实时百分比浮层只作参考,实际下单仍可能因点差和滑点偏离显示值,属高风险品种,请自行在 MT5 里挂脚本验证。

MQL5 / C++
class="type">void Draw(class="type">void)
{
   class="type">class="kw">double v1;
   
   if (m_Info.bvT)
   {
      ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">18);
      ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo);
   }
   if (m_Info.bvD)
   {
      v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * class="num">100.0), class="num">2);
      ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">1);
      ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP));
      ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1)));
   }
   if (m_Info.bvP)
   {
      v1 = NormalizeDouble((((iClose(GetInfoTerminal().szSymbol, PERIOD_D1, class="num">0) - m_Info.Rate.close) / m_Info.Rate.close) * class="num">100.0), class="num">2);
      ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">1);
      ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP));
      ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1)));
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void CreateObjInfo(EnumEvents arg)
{
   class="kw">switch (arg)
   {
      case evShowBarTime:

「鼠标事件驱动的信息层增删」

这段逻辑把图表上的辅助信息对象做成可切换层:鼠标触发不同事件时,调用 C_Mouse::CreateObjToStudy 按坐标生成对象,并把对应布尔标记置真。 创建时坐标参数很关键,例如 evShowBarTime 用 (2,110) 配色 clrPaleTurquoise,evShowDailyVar 用 (2,53),evShowPriceVar 用 (58,53);对象名取 def_ExpansionPrefix 拼接 ObjectsTotal(0) 的当前计数,保证不重名。 反向操作由 RemoveObjInfo 完成:根据传入事件取出已存的对象名 sz,并把 bvT/bvD/bvP 清为假,外部可据此隐藏图层。外汇与贵金属图表高频刷新,这类开关若放在 OnChartEvent 里误触,可能瞬间堆出几十个废弃对象,建议开 MT5 在脚本里打印 ObjectsTotal(0) 观察峰值。

MQL5 / C++
C_Mouse::CreateObjToStudy(class="num">2, class="num">110, m_Info.szBtn1 = (def_ExpansionPrefix + (class="type">class="kw">string)ObjectsTotal(class="num">0)), clrPaleTurquoise);
m_Info.bvT = true;
class="kw">break;
case evShowDailyVar:
C_Mouse::CreateObjToStudy(class="num">2, class="num">53, m_Info.szBtn2 = (def_ExpansionPrefix + (class="type">class="kw">string)ObjectsTotal(class="num">0)));
m_Info.bvD = true;
class="kw">break;
case evShowPriceVar:
C_Mouse::CreateObjToStudy(class="num">58, class="num">53, m_Info.szBtn3 = (def_ExpansionPrefix + (class="type">class="kw">string)ObjectsTotal(class="num">0)));
m_Info.bvP = true;
class="kw">break;
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void RemoveObjInfo(EnumEvents arg)
{
class="type">class="kw">string sz;

class="kw">switch (arg)
{
case evHideBarTime:
sz = m_Info.szBtn1;
m_Info.bvT = class="kw">false;
class="kw">break;
case evHideDailyVar:
sz = m_Info.szBtn2;
m_Info.bvD  = class="kw">false;
class="kw">break;
case evHidePriceVar:
sz = m_Info.szBtn3;
m_Info.bvP = class="kw">false;
class="kw">break;
}

对象删除与状态切换的底层写法

在 MT5 自定义指标里动态清理图形对象,先关事件再删对象是最稳的。下面这段把 CHART_EVENT_OBJECT_DELETE 临时置 false,就是避免删除瞬间又触发一次对象删除事件、导致递归或闪烁。 ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); ObjectDelete(GetInfoTerminal().ID, sz); ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); C_Study 构造函数里用 iClose 取日线收盘价,偏移量按是否回放品种或日期是否跨日来选 0 或 1:当前日期和 D1 最新 K 线日期不一致时取 1,否则取 0。这个细节决定你面板显示的是「昨收」还是「今收」。 Update 函数用 switch 处理市场状态:eCloseMarket 直接标 "Closed Market";eInReplay 和 eInTrading 共用一支逻辑,先拿 GetBarTime() 判断是否小于 ULONG_MAX,再决定后续刷新。外汇与贵金属波动剧烈,这类状态机写错可能导致面板卡死或误显,上 MT5 用 Print 打点验证最实在。

MQL5 / C++
ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, class="kw">false);
ObjectDelete(GetInfoTerminal().ID, sz);
ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public  :
class=class="str">"cmt">//+------------------------------------------------------------------+
   C_Study(class="type">long IdParam, class="type">class="kw">string szShortName, class="type">color corH, class="type">color corP, class="type">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;
      CreateObjInfo(evShowBarTime);
      CreateObjInfo(evShowDailyVar);
      CreateObjInfo(evShowPriceVar);
   }
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))
      {
         case eCloseMarket  :
            m_Info.szInfo = "Closed Market";
            class="kw">break;
         case eInReplay     :
         case eInTrading    :
            if ((dt = GetBarTime()) < ULONG_MAX)
            {

◍ 用自定义事件切换图表信息层

在 MT5 的鼠标交互类里,DispatchMessage 是接收图表事件的总入口。上面这段把 CHARTEVENT_CUSTOM 偏移量拼上自定义枚举(evHideBarTime、evShowBarTime 等),用来控制时间轴、日内波幅、价格变量的显隐。 C_Mouse::DispatchMessage(id, lparam, dparam, sparam) 先交给基类处理通用鼠标逻辑,再用 switch(id) 分流到具体动作。Hide 类事件调 RemoveObjInfo 删对象,Show 类事件调 CreateObjInfo 重建,等于用事件驱动图层开关,不碰 OnChartEvent 全局回调也能做局部刷新。 注意第 139 行 TimeToString(dt, TIME_SECONDS) 把 datetime 转成带秒的字符串写进 m_Info.szInfo,而 eAuction 分支直接写死 "Auction",default 落 "ERROR"——说明这个结构体只承载展示文本,真实状态机在别处。外汇与贵金属图表加载此类 EA 属高风险操作,事件 ID 冲突可能导致图层不刷。 开 MT5 把这段塞进你的 C_Mouse 派生类,故意发一个 CHARTEVENT_CUSTOM+evShowDailyVar 的自定义事件,看日均波幅标签是否重现,就能验证事件路由通不通。

MQL5 / C++
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);
   class="kw">switch (id)
     {
     case CHARTEVENT_CUSTOM + evHideBarTime:
       RemoveObjInfo(evHideBarTime);
       class="kw">break;
     case CHARTEVENT_CUSTOM + evShowBarTime:
       CreateObjInfo(evShowBarTime);
       class="kw">break;
     case CHARTEVENT_CUSTOM + evHideDailyVar:
       RemoveObjInfo(evHideDailyVar);
       class="kw">break;
     case CHARTEVENT_CUSTOM + evShowDailyVar:
       CreateObjInfo(evShowDailyVar);
       class="kw">break;
     case CHARTEVENT_CUSTOM + evHidePriceVar:
       RemoveObjInfo(evHidePriceVar);
       class="kw">break;

「图表事件怎么分流到绘图逻辑」

这段代码片段展示了一个基于鼠标和自定义事件的图表交互分流结构。它挂在指标主文件里,负责把 MT5 抛过来的事件代号映射到具体动作,而不是一股脑重绘。 case CHARTEVENT_CUSTOM + evShowPriceVar 触发时,调用 CreateObjInfo(evShowPriceVar) 生成价格变异信息对象,随后 break 退出分支。evSetServerTime 事件则把 dparam 强转成 datetime 写进 m_Info.TimeDevice,用来同步设备时间。 鼠标移动走 CHARTEVENT_MOUSE_MOVE,只调一句 Draw() 就 break,说明悬停时的视觉反馈是轻量的。所有分支走完,第 181 行用 ChartRedraw(GetInfoTerminal().ID) 对指定图表 ID 做一次重绘,避免无谓的全局刷新。 后面紧接的指标属性块里,#property version 标着 1.59,indicator_plots 设 0 但 indicator_buffers 留了 1,意味着这是个不画常规线、只借缓冲做内部计算的辅助指标。include 指向 Market Replay\Auxiliar\Study\C_Study.mqh,实例名 Study 初始化为 NULL,要在 OnInit 里 new 才生效。

MQL5 / C++
case CHARTEVENT_CUSTOM + evShowPriceVar:
   CreateObjInfo(evShowPriceVar);
   class="kw">break;
case (CHARTEVENT_CUSTOM + evSetServerTime):
   m_Info.TimeDevice = (class="type">class="kw">datetime)dparam;
   class="kw">break;
case CHARTEVENT_MOUSE_MOVE:
   Draw();
   class="kw">break;
}
ChartRedraw(GetInfoTerminal().ID);
}
};
class="macro">#undef def_ExpansionPrefix
class="macro">#undef def_MousePrefixName
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property description "This is an indicator for graphical studies using the mouse."
class="macro">#class="kw">property description "This is an integral part of the Replay / Simulator system."
class="macro">#class="kw">property description "However it can be used in the real market."
class="macro">#class="kw">property version "class="num">1.59"
class="macro">#class="kw">property icon "/Images/Market Replay/Icons/Indicators.ico"
class="macro">#class="kw">property link "[MQL5官方文档]
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_plots class="num">0
class="macro">#class="kw">property indicator_buffers class="num">1
class="macro">#include <Market Replay\Auxiliar\Study\C_Study.mqh>
C_Study *Study = NULL;
input class="type">color user02 = clrBlack; class=class="str">"cmt">//Price Line
input class="type">color user03 = clrPaleGreen; class=class="str">"cmt">//Positive Study

把鼠标研究挂进指标生命周期

这段初始化与计算逻辑,是把一个交互式市场研究类 C_Study 塞进 MT5 自定义指标骨架里的关键。OnInit 里第 28 行 new 出实例,传入 0 号子窗口、名称与三套配色(user02/user03/user04),其中 user04 默认 clrLightCoral 专标负面研究。若 _LastError 非 ERR_SUCCESS 直接 INIT_FAILED,实盘里常见原因是图表品种权限或内存分配失败。 当当前品种不等于回放专用品种 def_SymbolReplay 时,走真实市场分支:MarketBookAdd 订阅盘口,强制触发一次 OnBookEvent,状态置 eCloseMarket;否则进入 eInReplay 回放态。第 37–38 行把 m_Buff 绑成 INDICATOR_DATA 缓冲并清空为 EMPTY_VALUE,这是后续画图的数据槽。 OnCalculate 极简:把 rates_total 记进 m_posBuff,调 Study.Update(m_Status) 后原样返回。真正的绘制不在算价,而在 OnChartEvent——它把鼠标/键盘消息派发给 Study,再 SetBuffer 把缓冲推给指标,最后 ChartRedraw 重绘。 盘口事件里第 65 行 MarketBookGet 取簿记,第 66 行用 ArraySize(book)==0 判死市:空簿记即 eCloseMarket,否则 eInTrading。外汇与贵金属盘口深度随经纪商浮动,薄簿记下状态误判概率偏高,属高风险标的常态。

MQL5 / C++
class="num">19. input class="type">color  user04   = clrLightCoral;                 class=class="str">"cmt">//Negative Study
class="num">20. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">21. C_Study::eStatusMarket m_Status;
class="num">22. class="type">int m_posBuff = class="num">0;
class="num">23. class="type">class="kw">double m_Buff[];
class="num">24. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">25. class="type">int OnInit()
class="num">26. {
class="num">27.    ResetLastError();
class="num">28.    Study = new C_Study(class="num">0, "Indicator Mouse Study", user02, user03, user04);
class="num">29.    if (_LastError != ERR_SUCCESS) class="kw">return INIT_FAILED;
class="num">30.    if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay)
class="num">31.    {
class="num">32.     MarketBookAdd((*Study).GetInfoTerminal().szSymbol);
class="num">33.     OnBookEvent((*Study).GetInfoTerminal().szSymbol);
class="num">34.     m_Status = C_Study::eCloseMarket;
class="num">35.    }else
class="num">36.     m_Status = C_Study::eInReplay;
class="num">37.    SetIndexBuffer(class="num">0, m_Buff, INDICATOR_DATA);
class="num">38.    ArrayInitialize(m_Buff, EMPTY_VALUE);
class="num">39.    
class="num">40.    class="kw">return INIT_SUCCEEDED;
class="num">41. }
class="num">42. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">43. class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[])
class="num">44. {
class="num">45.    m_posBuff = rates_total;
class="num">46.    (*Study).Update(m_Status);    
class="num">47.    
class="num">48.    class="kw">return rates_total;
class="num">49. }
class="num">50. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">51. class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam)
class="num">52. {
class="num">53.    (*Study).DispatchMessage(id, lparam, dparam, sparam);
class="num">54.    (*Study).SetBuffer(m_posBuff, m_Buff);
class="num">55.    
class="num">56.    ChartRedraw((*Study).GetInfoTerminal().ID);
class="num">57. }
class="num">58. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">59. class="type">void OnBookEvent(const class="type">class="kw">string &symbol)
class="num">60. {
class="num">61.    MqlBookInfo book[];
class="num">62.    C_Study::eStatusMarket loc = m_Status;
class="num">63.    
class="num">64.    if (symbol != (*Study).GetInfoTerminal().szSymbol) class="kw">return;
class="num">65.    MarketBookGet((*Study).GetInfoTerminal().szSymbol, book);
class="num">66.    m_Status = (ArraySize(book) == class="num">0 ? C_Study::eCloseMarket : C_Study::eInTrading);

◍ 用市场深度捕捉竞价切换的落点

这段逻辑干的事很直接:遍历当前品种的 Market Book 数组,只要撞上 BUY_MARKET 或 SELL_MARKET 类型的挂单,就把状态机 m_Status 切到 eAuction。也就是说,真实市价单进场的那一刻,被当成竞价段开始的信号,而不是等 K 线收盘才后知后觉。 循环里用 c0 < ArraySize(book) 做边界,同时用 m_Status != C_Study::eAuction 当短路条件——一旦状态已切,立刻停扫,避免无谓开销。第 69 行拿 loc 和 m_Status 比,不等才调 Update,等于把「状态真变了才刷新」写死,减少重复绘制。 OnDeinit 里有个坑:reason 不是 INITFAILED 时才走释放逻辑,且只对非回放符号调用 MarketBookRelease。如果你在 EURUSD 上挂了脚本又切到回放品种,漏掉这行就会让深度订阅一直挂着,MT5 终端内存曲线会慢慢爬。删 Study 对象放最后,保证反初始化顺序不崩。 外汇与贵金属自带高杠杆与滑点风险,市价单触发的竞价识别在流动性薄时可能误判,建议开 MT5 用真实账户外的模拟环境先跑这段代码验证触发频率。

MQL5 / C++
for (class="type">int c0 = class="num">0; (c0 < ArraySize(book)) && (m_Status != C_Study::eAuction); c0++)
   if ((book[c0].type == BOOK_TYPE_BUY_MARKET) || (book[c0].type == BOOK_TYPE_SELL_MARKET)) m_Status = C_Study::eAuction;
if (loc != m_Status) (*Study).Update(m_Status);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
{
   if (reason != REASON_INITFAILED)
   {
      if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay)
         MarketBookRelease((*Study).GetInfoTerminal().szSymbol);
   }
   class="kw">delete Study;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

「记住这一条就够了」

这一路我们慢,但不是没动。前面几篇里验证过的那些 MT5 被说「做不到」的事,其实都能靠先提出假设、再写代码实测来确认,不少人嘴上的平台限制只是他们自己没试过。 作者留的那个鼠标指标演示视频里有一处已知缺陷,他自己也点明了——不急着修,等把 MT5 底层机制摸更透再说。这种「留坑下次填」的节奏,反倒比假装一步到位更真实。 接下来要做的,是把回放/模拟系统搬到真实账户和模拟账户上跑。外汇和贵金属波动剧烈、杠杆高风险大,任何回放结论都只是概率倾向,真金白银前先在策略测试器里复一遍。 你要是也在跟这套系列,最该记住的就是:别信「MT5 不行」的断言,自己开终端写个片段验一下,比看十篇争论有用。

把异步排查交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到指标卸载与图表关闭的关联记录,你专注决策而非翻消息日志。

常见问题

MetaTrader 5 在模板应用过程中主动从图表移除指标,该动作触发 Deinit 且原因变量被置为 REASON_REMOVE,与直观预期不符,属异步机制导致。
可以,小布盯盘内置的 AIGC 看盘口能标记指标卸载与图表关闭事件,省去手动比对终端消息窗的低效劳动。
同步会令平台等待模板内多指标及 EA 初始化,可能冻结数秒甚至崩溃,因此平台选择异步以提升性能与稳定性。
可在模板应用与日程表更新间插入小延迟,避免控制模块误判图表状态而强制关闭服务,具体节奏需结合负载测试。
关于 Deinit 异步顺序的完整讨论见《开发回放系统·基础篇》与《开发回放系统·实战篇》,本篇做综合收束。