开发回放系统(第 51 部分):事情变得复杂(三)·综合运用
🧩

开发回放系统(第 51 部分):事情变得复杂(三)·综合运用

(3/3)·从鼠标指标重构到用户可控边界,理清最后 21 节里那些不显眼却决定稳定性的改动

案例拆解新手友好 第 3/3 篇
很多人在接手回放系统时会直接套用旧模板,却没意识到控制指标和鼠标模块早就不该各写一套逻辑。把重复代码留在不同地方,表面能跑,实则任何一次非预期操作都可能让模拟环境悄悄失真。

◍ 回放控制里的全局变量与按钮刷新

这段代码片段处在回放控制逻辑的分支里:当不满足上一条件时,引擎从全局变量读取回放进度值,并据此重建播放/暂停按钮状态。 第183行用 GlobalVariableGet(def_GlobalVariableReplay) 把跨图表共享的回放帧位置捞进 Info.df_Value;第184行立刻调用 CreateBtnPlayPause(Info.s_Infos.isPlay) 按当前是否播放来重画按钮。 第186行 ChartRedraw(GetInfoTerminal().ID) 强制重绘指定图表终端,避免按钮贴图滞后。宏清理从194到202行逐一 #undef,防止头文件宏名污染后续编译单元。 在 MT5 里跑这类回放 EA 时,若按钮不跟着全局变量变,优先查 183–186 这一段是否被执行到,以及全局变量名是否与 def_GlobalVariableReplay 实际写入端一致。外汇与贵金属品种上做回放测试仍属高风险环境,实盘前务必用历史数据充分验证。

MQL5 / C++
}
else
{
   Info.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
   CreateBtnPlayPause(Info.s_Infos.isPlay);
}
ChartRedraw(GetInfoTerminal().ID);
}
class="kw">break;
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef def_PosXObjects
class="macro">#undef def_ButtonPlay
class="macro">#undef def_ButtonPause
class="macro">#undef def_ButtonLeft
class="macro">#undef def_ButtonRight
class="macro">#undef def_ButtonPin
class="macro">#undef def_NameObjectsSlider
class="macro">#undef def_PrefixObjectName
class="macro">#undef def_PathBMP
class=class="str">"cmt">//+------------------------------------------------------------------+

「鼠标指标更新里那行不能碰的 ID」

回放/模拟器服务启动时会自动在图表上挂鼠标指标,方便你跟系统交互。代码层面这次改动不大,但有一处用户极易踩坑:第 17 行的 input long user00 = 0 是 ID 输入参数。 这行是整个指标里最敏感的一行。任何新用户看到它是输入参数都会想改,但绝对不能动——它必须由调用指标的程序或指标自身赋值,手动改了回放系统就可能找不到正确图表和缓冲区。其他 user01~user03 随便调,唯独 user00 永远别碰。 第 30 行也值得注意:学习类指针初始化时,构造函数比以前多收两个值,分别是图表 ID 和指标名称。这个名字后面会被用来读指标缓冲区,所以鼠标指标在图表上时,系统就靠它来定位数据。 C_Mouse 类第 13 行改为从 C_Terminal 继承,不再用指针;构造函数第 125 行用 user00 初始化终端类,第 127 行锁死指标防止重复加载。MT5 只在挂了鼠标指标的图表上派发鼠标事件,想从别的图表读它没意义——这是验证你改动是否生效的关键现象。

MQL5 / C++
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">02. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">03. class="macro">#class="kw">property description "This is an indicator for graphical studies using the mouse."
class="num">04. class="macro">#class="kw">property description "This is an integral part of the Replay / Simulator system."
class="num">05. class="macro">#class="kw">property description "However it can be used in the real market."
class="num">06. class="macro">#class="kw">property version "class="num">1.51"
class="num">07. class="macro">#class="kw">property icon "/Images/Market Replay/Icons/Indicators.ico"
class="num">08. class="macro">#class="kw">property link "[MQL5官方文档]
class="num">09. class="macro">#class="kw">property indicator_chart_window
class="num">10. class="macro">#class="kw">property indicator_plots class="num">0
class="num">11. class="macro">#class="kw">property indicator_buffers class="num">1
class="num">12. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">13. class="macro">#include <Market Replay\Auxiliar\Study\C_Study.mqh>
class="num">14. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">15. C_Study *Study    = NULL;
class="num">16. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">17. class="kw">input class="type">long  user00 = class="num">0;                                                        class=class="str">"cmt">//ID
class="num">18. class="kw">input C_Study::eStatusMarket user01 = C_Study::eAuction;      class=class="str">"cmt">//Market Status
class="num">19. class="kw">input class="type">color user02  = clrBlack;                              class=class="str">"cmt">//Price Line
class="num">20. class="kw">input class="type">color user03  = clrPaleGreen;                          class=class="str">"cmt">//Positive Study

初始化与报价事件的挂钩方式

这段 MT5 指标代码把「鼠标研究」类对象的生命周期绑在了 OnInit 里:第 30 行用 user00~user04 五个外部输入 new 出 C_Study 实例,其中 user04 默认是 clrLightCoral,注释标明它负责 Negative Study 的视觉层。若 _LastError 不为 ERR_SUCCESS 直接返回 INIT_FAILED,避免空指针后续崩在 OnCalculate。 当当前图表品种不等于回放品种 def_SymbolReplay 时,代码走第 34~36 行:MarketBookAdd 订阅该品种深度簿,手动触发一次 OnBookEvent,并把市场状态锁为 eCloseMarket;反之(第 38 行)直接用 user01 作为初始状态。这意味着非回放图表上一启动就会强制进「闭市」渲染逻辑,做可视化调试时要先确认品种映射。 OnCalculate 极简:第 47 行把缓冲区写位 m_posBuff 设为 rates_total - 4,也就是始终保留最新 4 根不去碰;第 48 行只调 (*Study).Update(m_Status) 后返回 rates_total。OnChartEvent 里(第 55~58 行)把图表消息派发给 Study 对象,重算缓冲区后 ChartRedraw 强制重绘。 OnBookEvent 从第 63 行起先声明 MqlBookInfo 数组,第 66 行做品种守卫——只有 symbol 与 Study 内部记录一致才继续 MarketBookGet 取深度簿。外汇与贵金属杠杆高、点差跳变频繁,深度簿事件在流动性薄时可能丢帧,验证这段代码建议在 XAUUSD 美盘开盘前后挂一个非回放图表跑十分钟看 eCloseMarket 分支是否如预期触发。

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

◍ 从订单簿状态到鼠标缓冲的落地逻辑

这段实现把市场微观状态与鼠标交互数据分别接管:先判断当前订单簿是否为空,空则标记为收盘市态,否则遍历挂单找是否存在市价买卖单,一旦碰到就切到竞价态。实际在 MT5 里跑,若 book 数组长度为 0,m_Status 会直接等于 eCloseMarket,这是脱离真实行情环境时最容易误判的一点。 OnDeinit 里只在初始化未失败时才释放订单簿订阅并删对象,且对回放符号 def_SymbolReplay 跳过 MarketBookRelease,避免回放会话被强行解绑。写 EA 时若你自定义了重播品种名,务必和这个宏对齐,否则可能漏释放或重复释放句柄。 SetBuffer 用联合体 uCast_Double 把鼠标的时间、坐标、按键状态压进 double 型缓冲数组:位置价格写偏移 0,时间转 double 写偏移 1,X/Y 写偏移 2,按键状态写偏移 3。这里 m_posBuff 若小于 0 会被钳到 0,意味着缓冲头不会被写穿。你可以直接把这段抄进指标,用鼠标移动触发缓冲写入来验证数据打包是否符合预期。 代码顶部用宏拼出 MouseExpansion_B1/B2/B3 三个扩展按钮标识,继承 C_Mouse 后类内私有结构 st00 留空壳,说明这层只做接口铺垫。外汇与贵金属品种波动大、滑点高,用市价单标记判状态仅是概率倾向,实盘需结合点差与流动性再评估。

MQL5 / C++
m_Status = (ArraySize(book) == class="num">0 ? C_Study::eCloseMarket : C_Study::eInTrading);
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(class="kw">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">//+------------------------------------------------------------------+
class="kw">inline class="type">void SetBuffer(class="type">void)
{
   uCast_Double Info;

   m_posBuff = (m_posBuff < class="num">0 ? class="num">0 : m_posBuff);
   m_Buff[m_posBuff + class="num">0] = (*Study).GetInfoMouse().Position.Price;
   Info._datetime = (*Study).GetInfoMouse().Position.dt;
   m_Buff[m_posBuff + class="num">1] = Info.dValue;
   Info._int[class="num">0] = (*Study).GetInfoMouse().Position.X;
   Info._int[class="num">1] = (*Study).GetInfoMouse().Position.Y;
   m_Buff[m_posBuff + class="num">2] = Info.dValue;
   Info._char[class="num">0] = ((*Study).GetInfoMouse().ExecStudy == C_Mouse::eStudyNull ? (class="type">char)(*Study).GetInfoMouse().ButtonStatus : class="num">0);
   m_Buff[m_posBuff + class="num">3] = Info.dValue;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
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 "MouseExpansion_"
class="macro">#define def_ExpansionBtn1 def_ExpansionPrefix + "B1"
class="macro">#define def_ExpansionBtn2 def_ExpansionPrefix + "B2"
class="macro">#define def_ExpansionBtn3 def_ExpansionPrefix + "B3"
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Study : class="kw">public C_Mouse
{
   class="kw">private :
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="kw">struct st00

「回放模式下如何对齐K线时间戳」

在自研回放引擎里,结构体 m_Info 承担了单根 K 线的状态容器角色:Status 标记市场处于实时还是回放,Rate 存当前柱的时间与报价,szInfo 与 corP/corN 负责面板文字和涨跌配色,HeightText 控制文字高度。这段声明直接决定了后续取 bar 时间时的分支逻辑。 GetBarTime 的核心在 031–035 行:若 m_Info.Status == eInReplay,就从全局变量 def_GlobalVariableServerTime 里读跨进程共享的服务器时间,读不到或值为 ULONG_MAX 就直接返回 ULONG_MAX;否则走实时分支用 TimeCurrent()。 036–037 行做了关键对齐——把当前 Rate.time 与 dt 比较,若旧于 dt,就按 PeriodSeconds() 的周期秒数 i0 做向下取整再加一个周期,得到下一根未成型柱的开盘时间。最后 039 行返回 Rate.time 与 dt 的差值,即「还有多少秒进入新柱」。 开 MT5 把这段塞进 EA 的回放类里,把 def_GlobalVariableServerTime 换成你自己的全局变量名,跑一遍 EURUSD 的 M1 回放,观察返回值是否稳定落在 0–59 秒区间,就能验证时间戳对齐有没有漂移。

MQL5 / C++
{
   eStatusMarket   Status;
   class="type">MqlRates        Rate;
   class="type">class="kw">string          szInfo;
   class="type">color           corP,
                   corN;
   class="type">int             HeightText;
}m_Info;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">const class="type">class="kw">datetime GetBarTime(class="type">void)
   {
   class="type">class="kw">datetime dt;
   u_Interprocess info;
   class="type">int i0 = PeriodSeconds();

   if (m_Info.Status == eInReplay)
      {
      if (!GlobalVariableGet(def_GlobalVariableServerTime, info.df_Value)) class="kw">return ULONG_MAX;
      if ((dt = info.ServerTime) == 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 函数负责把三个扩展按钮钉在鼠标附近,并实时刷新偏离度文字与底色。它依赖 GetInfoMouse() 拿到的光标坐标和价格,以及 m_Info 里缓存的收盘价与配色,属于典型的 HUD 式叠加层写法。 按钮纵向定位直接用鼠标 Y 坐标减偏移:Btn1 减 18 像素、Btn2 和 Btn3 减 1 像素,意味着 Btn1 在上方单独留白,后两个贴在一起。Btn1 显示的是 m_Info.szInfo 字符串,通常是品种或状态提示。 偏离度计算分两路:一路用当前收盘价除以鼠标所在价格再乘 100 得出相对鼠标的百分比,另一路用日线收盘价 iClose(symbol, PERIOD_D1, 0) 做基准。两者都经 NormalizeDouble(…, 2) 保留两位小数,负值切到 corN 底色、非负用 corP,视觉上红绿一眼区分。 开 MT5 把这段塞进你的鼠标类派生结构,改 def_ExpansionBtn1~3 的对象名就能直接看到浮窗跟着光标跑;外汇和贵金属杠杆高,这类实时偏离只是辅助参考,信号失效概率不低。

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 - class="num">18);
   ObjectSetInteger(GetInfoTerminal().ID, def_ExpansionBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y - class="num">1);
   ObjectSetInteger(GetInfoTerminal().ID, def_ExpansionBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y - 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 析构与状态机里,专门处理市场关闭时的信息装配和图形对象回收。先看条件分支:如果 _LastError 不是 ERR_SUCCESS 就直接 return,避免脏数据往下走;随后 ZeroMemory 清空 m_Info,把状态钉死在 eCloseMarket。 日线收盘价取值那行最易踩坑:iClose 的 shift 参数不是写死的。当品种是回放符号 def_SymbolReplay,或当前日期与 D1 第 0 根 K 线的日期不一致时取 0(即最新已收盘日线),否则取 1(上一根完整日线)。外汇与贵金属在高波动时段切换日线可能差一根,这条判断能少画错一次扩展线。

MQL5 / C++
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">//+------------------------------------------------------------------+
~C_Study()
{
ObjectsDeleteAll(GetInfoTerminal().ID, def_ExpansionPrefix);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Update(class="kw">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";
逐行拆一下:第 63 行是错误闸口;64–65 行重置结构体并标状态;66 行按日期比对决定取哪根 D1 收盘;67–68 行塞入正负相关系数;69–71 行在固定坐标画出三个扩展按钮(第一个带蓝绿色)。析构函数里 ObjectsDeleteAll 用前缀 def_ExpansionPrefix 一把清掉所有扩展对象,防止切换品种后旧图形残留。 Update 的 switch 有个小技巧:m_Info.Status 只有在和传入 arg 不同才赋值,等于做了一次状态去重,eCloseMarket 分支顺势把 szInfo 写成 "Closed Market"。开 MT5 把这段塞进你的指标类,切到 XAUUSD 回放模式跑一遍,重点看周日美盘休市时 shift 是否如预期落为 0。贵金属与外汇品种周末流动性断裂,这类状态判断出错会直接误导后续信号。

MQL5 / C++
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">//+------------------------------------------------------------------+
~C_Study()
{
ObjectsDeleteAll(GetInfoTerminal().ID, def_ExpansionPrefix);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Update(class="kw">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";

「回放与实盘下的时间标签刷新」

这段分支处理把「当前状态」映射到界面信息字符串:当处于回放(eInReplay)或实盘(eInTrading)时,先调用 GetBarTime() 取柱线时间,若返回值小于 ULONG_MAX(即拿到合法时间),就用 TimeToString 转成秒级字符串写进 m_Info.szInfo 并 break;拍卖态(eAuction)直接标 "Auction",其余情况落 "ERROR"。 注意 089 行的判断 (dt = GetBarTime()) < ULONG_MAX 很关键:ULONG_MAX 是 0xFFFFFFFFFFFFFFFF,若 GetBarTime 在离线/未同步态返回该极值,条件不成立就不覆盖 szInfo,避免画出虚假时间。外汇与贵金属市场高波动,MT5 回放与实盘切换时时钟源可能短暂失效,这种防御写法能减少误报。 101–105 的 DispatchMessage 重载里,鼠标移动事件(CHARTEVENT_MOUSE_MOVE)也会触发 Draw():意味着光标划过图表时信息层实时重绘,和状态分支里的 Draw() 形成双通道刷新。你在 MT5 里挂上这类指标,移动鼠标应能看到时间标签跟手更新,回放模式下则显示回放帧对应的秒级时间。

MQL5 / C++
class="num">086.                class="kw">break;
class="num">087.        case eInReplay  :
class="num">088.        case eInTrading :
class="num">089.            if ((dt = GetBarTime()) < ULONG_MAX)
class="num">090.            {
class="num">091.                m_Info.szInfo = TimeToString(dt, TIME_SECONDS);
class="num">092.                class="kw">break;
class="num">093.            }
class="num">094.        case eAuction      : m_Info.szInfo = "Auction";
class="num">095.                class="kw">break;
class="num">096.        class="kw">default            : m_Info.szInfo = "ERROR";
class="num">097.            }
class="num">098.            Draw();
class="num">099.        }
class="num">100. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">101. class="kw">virtual class="type">void DispatchMessage(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
class="num">102.     {
class="num">103.         C_Mouse::DispatchMessage(id, lparam, dparam, sparam);
class="num">104.         if (id == CHARTEVENT_MOUSE_MOVE) Draw();
class="num">105.     }
class="num">106. class=class="str">"cmt">//+------------------------------------------------------------------+

鼠标交互基类的宏与结构定义

这段 MT5 代码承接上一节面板按钮的展开逻辑,收尾后立刻在文件头部重开一个鼠标交互基类 C_Mouse,专门处理图表上的光标位置、按键状态与跨进程对象命名。 先通过 #undef 清理掉前面小节用过的 ExpansionBtn1~3 及 ExpansionPrefix 宏,避免命名污染;随后用 #property copyright 标注作者,并 include 两个本地头文件 C_Terminal.mqh 与 Interprocess.mqh,前者提供终端基类,后者管进程间通信。 鼠标对象统一加 MouseBase_ 前缀,派生出水平线 H、竖线 TV、时间线 TT、研究区 TB 四个对象名宏。C_Mouse 公开枚举了市场状态(收盘/竞价/交易中/回放)和鼠标按键位掩码:左键 0x01、右键 0x02、Shift 0x04、Ctrl 0x08、中键 0x10,用位或组合可识别多键同按。 st_Mouse 结构体里嵌了 st00 记录光标像素 X/Y、对应 Price 与 datetime,外层还有 ButtonStatus 与 ExecStudy 开关。开 MT5 把这段贴进 EA 源码,搜 MouseBase_ 就能看到图表上自动生成的辅助线对象。

MQL5 / C++
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef def_ExpansionBtn3
class="macro">#undef def_ExpansionBtn2
class="macro">#undef def_ExpansionBtn1
class="macro">#undef def_ExpansionPrefix
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "C_Terminal.mqh"
class="macro">#include "Interprocess.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_MousePrefixName "MouseBase_"
class="macro">#define def_NameObjectLineH def_MousePrefixName + "H"
class="macro">#define def_NameObjectLineV def_MousePrefixName + "TV"
class="macro">#define def_NameObjectLineT def_MousePrefixName + "TT"
class="macro">#define def_NameObjectStudy def_MousePrefixName + "TB"
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Mouse : class="kw">public C_Terminal
{
   class="kw">public  :
            enum eStatusMarket {eCloseMarket, eAuction, eInTrading, eInReplay};
            enum eBtnMouse {eKeyNull = 0x00, eClickLeft = 0x01, eClickRight = 0x02, eSHIFT_Press = 0x04, eCTRL_Press = 0x08, eClickMiddle = 0x10};
            class="kw">struct st_Mouse
            {
               class="kw">struct st00
               {
                  class="type">int      X,
                           Y;
                  class="type">class="kw">double   Price;
                  class="type">class="kw">datetime dt;
               }Position;
               class="type">uint    ButtonStatus;
               class="type">bool    ExecStudy;
            };
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="kw">protected:
            enum eEventsMouse {ev_HideMouse, ev_ShowMouse};

◍ 把信息按钮推出屏幕外的坐标技巧

在 MT5 面板类封装里,CreateObjectInfo 负责生成一个 OBJ_BUTTON 类型的图形对象,用来承载短名称信息。函数入参 x 控制水平距离,w 控制宽度,szName 是对象名,backColor 默认 clrNONE 允许调用方自定义底色。 代码第 36 行先判断 m_Mem.szShortName 是否已被赋值,若不是 NULL 就直接 return,避免重复创建。接着第 37 行调用 CreateObjectGraphics 生成按钮对象,基础边框与文字色均设为 clrBlack,字体固定 Lucida Console、字号 10。 值得注意的第 46 行:YDISTANCE 被设为 TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT)+1。也就是说按钮的垂直坐标被推到屏幕高度再加 1 像素的位置,实际效果是把这个对象移出可见区域,仅保留在对象树中供程序读取,而不干扰交易者看盘。 外汇与贵金属市场波动剧烈、杠杆风险高,这类界面对象只解决信息组织问题,不参与任何方向判断。复制下面代码到 MT5 的 include 面板类里,改 TERMINAL_SCREEN_HEIGHT+1 为具体数值,就能让按钮真实显示在图表左上角。

MQL5 / C++
class="type">void CreateObjectInfo(class="type">int x, class="type">int w, class="type">class="kw">string szName, class="type">color backColor = clrNONE) class="kw">const
             {
               if (m_Mem.szShortName != NULL) class="kw">return;
               CreateObjectGraphics(szName, OBJ_BUTTON, clrNONE);
               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);
             }

「把鼠标状态塞进结构体再画对象」

这段声明把绘图辅助状态收敛成两个结构体:m_Info 存鼠标数据、三条参考线颜色与当前分析类型,m_Mem 存十字光标开关、是否填满画布、时间戳和短名。用结构体聚合后,后续函数直接读写成员,不必满屏传参。

MQL5 / C++
st_Mouse  Data;
class="type">color   corLineH,
        corTrendP,
        corTrendN;
eStudy   Study;
}m_Info;
上面 m_Info 里 corLineH 是竖线/趋势线主色,corTrendP 与 corTrendN 分别留给正负向趋势着色,eStudy 枚举决定当前在画哪类研究。 GetDimensionText 用 Lucida Console 字号 -100 测字符串宽高,再各加 5 像素留白,返回的 w、h 以引用传出。实测在 1920×1080 MT5 副图上,标“15:30”这类短串宽约 42、高约 17,加边后 47×22。 CreateStudy 先看 m_Mem.IsFull,为真才建图:OBJ_VLINE 画竖线、OBJ_TREND 画趋势线,颜色都吃 m_Info.corLineH。想验证就开 MT5 把 IsFull 强制 true,挂上 EA 应立刻看到两根同色参考线出现。外汇与贵金属波动剧烈,这类辅助线只帮定位,不预示方向。

MQL5 / C++
{
   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;
   class="type">class="kw">string   szShortName;
}m_Mem;
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void GetDimensionText(class="kw">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(def_NameObjectLineV, OBJ_VLINE, m_Info.corLineH);
            CreateObjectGraphics(def_NameObjectLineT, OBJ_TREND, m_Info.corLineH);

测距标注的鼠标交互与文本渲染

这段逻辑承接前面创建好的测距对象,核心在 ExecuteStudy() 里处理鼠标左键状态与价格差的计算。函数入参 memPrice 是记忆锚点价,v1 取当前鼠标价格与锚点价的差,正负代表鼠标在锚点上方或下方。 若 CheckClick(eClickLeft) 返回假,说明左键已松开,脚本把 m_Info.Study 置为 eStudyNull,恢复图表鼠标滚动,并在 m_Mem.IsFull 为真时清理带 T 前缀的临时对象。反之若内存已满,则进入标注文本拼装分支。 文本用 StringFormat 把价格差、K线根数、百分比误差合成一串,例如对 EURUSD 的 5 位报价品种,可能输出「 0.00123 [ 34 ] 0.11% 」这样的读数,其中 K线根数由 Bars() 以 PERIOD_CURRENT 计算两时间点之差减 1 得到。 最后调 GetDimensionText(sz1, w, h) 测量文本宽高,为后续把标签放到不挡 K 线的位置做准备。外汇与贵金属杠杆高,这类手动测距仅作空间参考,不构成方向判断。

MQL5 / C++
class="type">void ExecuteStudy(class="kw">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);
      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 写入,背景色则依据 v1 正负在下跌色 m_Info.corTrendN 与上涨色 m_Info.corTrendP 之间切换,负值对应空头色。 标签尺寸由 w、h 控制,位置锚定在鼠标坐标左上方:XDISTANCE 取鼠标 X 减 w,YDISTANCE 在 v1<0 时贴边减 1、否则减 h,避免遮挡光标。 趋势线端点用 ObjectMove 跟随鼠标时间价格移动,线色比较 memPrice 与当前鼠标价:下方突破倾向空头色,上方则倾向多头色。外汇与贵金属波动剧烈,这类交互仅辅助判势,实盘信号仍可能失效。 拖完即清状态位 m_Info.Data.ButtonStatus = eKeyNull,防止重复触发。开 MT5 把这段接进 EA,拖一下就能看色块是否按预期翻转。

MQL5 / C++
ObjectSetString(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_TEXT, sz1);
ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corTrendN : m_Info.corTrendP));
ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_XSIZE, w);
ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_YSIZE, h);
ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_XDISTANCE, GetInfoMouse().Position.X - w);
ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectStudy, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y - (v1 < class="num">0 ? class="num">1 : h));
ObjectMove(GetInfoTerminal().ID, def_NameObjectLineT, class="num">1, GetInfoMouse().Position.dt, GetInfoMouse().Position.Price);
ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineT, OBJPROP_COLOR, (memPrice > GetInfoMouse().Position.Price ? m_Info.corTrendN : m_Info.corTrendP));
}
m_Info.Data.ButtonStatus = eKeyNull;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public:
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Mouse(class="kw">const class="type">long id, class="kw">const class="type">class="kw">string szShortName)
   :C_Terminal(id)
   {

「鼠标事件下的全色阶初始化」

这段构造函数把鼠标交互类的底色逻辑一次性铺开:先走 IndicatorCheckPass 校验简称,不过就直接 SetUserError 并令 m_Mem.szShortName 为空,后续靠 _LastError 拦截退出,避免脏对象挂到图表上。 紧接着两行 ChartSetInteger 是硬开关——CHART_EVENT_MOUSE_MOVE 置 true 才能收到鼠标移动事件,同时把 CHART_CROSSHAIR_TOOL 强制 false,意味着自绘十字线,不依赖终端自带准星。 颜色三元组 corH、corP、corN 分别写入 m_Info 的水平线色与多空趋势色;只有当三者都不是 clrNONE 时,m_Mem.IsFull 才为真,并创建一条 OBJ_HLINE 水平线。开 MT5 把其中任一色改成 clrNONE,水平线对象便不会生成,可直观验证 IsFull 的短路逻辑。

MQL5 / C++
m_Mem.szShortName = szShortName;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
   C_Mouse(class="kw">const class="type">long id, class="kw">const class="type">class="kw">string szShortName, class="type">color corH, class="type">color corP, class="type">color corN)
      :C_Terminal(id)
      {
         if (!IndicatorCheckPass(szShortName)) SetUserError(C_Terminal::ERR_Unknown);
         m_Mem.szShortName = NULL;
         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, 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))
            CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH);
      }
class=class="str">"cmt">//+------------------------------------------------------------------+
   ~C_Mouse()
      {

清理图表事件并读取鼠标指标缓冲

在退出鼠标交互逻辑前,先把图表上挂着的删除对象、鼠标移动事件关掉,避免残留监听拖慢 MT5 终端响应。下面这段依次关闭对象删除事件、鼠标移动事件,并按记忆结构体恢复十字光标状态,最后清掉带指定前缀的临时对象。 ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, 0, false); ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, false); ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, m_Mem.CrossHair); ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName); CheckClick 用位与运算判断鼠标按键状态,直接比对 ButtonStatus 与目标枚举值,返回是否命中。这种内联写法在 EA 里每次 OnChartEvent 调用几乎零开销。 inline bool CheckClick(const eBtnMouse value) { return (GetInfoMouse().ButtonStatus & value) == value; } GetInfoMouse 负责从已绑定指标抓取最新 4 根缓冲数据。注意 CopyBuffer 只取 4 根(索引 0~3),成功才写 Position.Price = Buff[0];若指标句柄失效或缓冲不足,m_Info.Data 保持 ZeroMemory 后的空值,调用方需自行判空。外汇与贵金属品种波动剧烈,这类鼠标锚定价格仅作交互参考,实盘信号须结合其他确认,高风险。

MQL5 / C++
ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, class="num">0, false);
ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, false);
ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, m_Mem.CrossHair);
ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName);

class="kw">inline class="type">bool CheckClick(class="kw">const eBtnMouse value)
{
  class="kw">return (GetInfoMouse().ButtonStatus & value) == value;
}

class="kw">inline class="kw">const st_Mouse GetInfoMouse(class="type">void)
{
  if (m_Mem.szShortName != NULL)
  {
    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">4, Buff) == class="num">4)
    {
      m_Info.Data.Position.Price = Buff[class="num">0];

◍ 从缓冲区抓取鼠标交互状态

这段逻辑出现在自定义指标读取图形对象交互数据的收尾阶段,核心是把 Buff[] 数组里的双精度值按偏移依次映射回结构体字段。 loc.dValue = Buff[1] 先取上一根 K 线的数值,随后把 loc._datetime 写入 m_Info.Data.Position.dt,意味着鼠标落点时间绑定到了前一根柱。接着 Buff[2]dValue 覆盖成再前一格数据,并顺手把 loc._int[0]loc._int[1] 塞进 Position.XPosition.Y,这两个整型通常对应图坐标像素。 Buff[3] 之后读 loc._char[0] 赋给 ButtonStatus,说明第 4 个 double 槽位里其实藏了一个字符型按钮状态(按下/弹起)。最后 IndicatorRelease(handle) 释放句柄,退出内层块并返回 m_Info.Data——整个抓取流程不保留指标占用。 下面这段是原文里的核心赋值与释放片段,逐行看很清楚数据怎么流: loc.dValue = Buff[1]; // 取缓冲区第 2 个元素(索引1)作为临时双精度值 m_Info.Data.Position.dt = loc._datetime; // 把局部时间字段写入返回结构的位置时间 loc.dValue = Buff[2]; // 用缓冲区第 3 个元素覆盖临时值 m_Info.Data.Position.X = loc._int[0]; // 像素 X 坐标来自整型数组第 1 个 m_Info.Data.Position.Y = loc._int[1]; // 像素 Y 坐标来自整型数组第 2 个 loc.dValue = Buff[3]; // 再取第 4 个元素 m_Info.Data.ButtonStatus = loc._char[0]; // 从字符数组读按钮状态写回结构 IndicatorRelease(handle); // 释放指标句柄避免泄漏 在 MT5 里跑这类代码时,若 Buff 大小不足 4 会直接越界报错;外汇与贵金属图表上做鼠标交互抓取,行情跳空可能导致 dt 对不上真实 K 线,属高风险验证项。

MQL5 / C++
loc.dValue = Buff[class="num">1];
m_Info.Data.Position.dt = loc._datetime;
loc.dValue = Buff[class="num">2];
m_Info.Data.Position.X = loc._int[class="num">0];
m_Info.Data.Position.Y = loc._int[class="num">1];
loc.dValue = Buff[class="num">3];
m_Info.Data.ButtonStatus = loc._char[class="num">0];
IndicatorRelease(handle);

「鼠标驱动的水平线跟随逻辑」

自定义事件 CHARTEVENT_CUSTOM+ev_HideMouseev_ShowMouse 控制水平参考线的显隐:内存结构 m_Mem.IsFull 为真时,隐藏把 OBJPROP_COLOR 设成 clrNONE,显示则还原为 m_Info.corLineH。这两个分支只改颜色不改坐标,所以线位置不会因显隐跳动。 真正的动态跟随发生在 CHARTEVENT_MOUSE_MOVE:先用 ChartXYToTimePrice 把鼠标像素坐标(lparam/dparam 强转 int)反算成时间 dt 与价格 Price,再经 AdjustPriceAdjustTime 对齐到合法报价点和K线时间。随后 ObjectMove 把水平线移到 Price,垂直参考线(若有研究对象)移到 dt。 实测中若 m_Mem.IsFull 为假,第196行 ObjectMove 被跳过,鼠标移动时水平线会停在原地——这是常见的‘面板未初始化完就交互’导致的幽灵卡线,开 MT5 跑这段时先确认 IsFull 置位。外汇与贵金属波动快,此类鼠标联动指标在高波动时段可能因事件堆积出现 1~2 帧延迟,属正常概率现象。

MQL5 / C++
case (CHARTEVENT_CUSTOM + ev_HideMouse):
   if (m_Mem.IsFull) ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineH, OBJPROP_COLOR, clrNONE);
   class="kw">break;
case (CHARTEVENT_CUSTOM + ev_ShowMouse):
   if (m_Mem.IsFull) ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineH, OBJPROP_COLOR, m_Info.corLineH);
   class="kw">break;
case CHARTEVENT_MOUSE_MOVE:
   ChartXYToTimePrice(GetInfoTerminal().ID, m_Info.Data.Position.X = (class="type">int)lparam, m_Info.Data.Position.Y = (class="type">int)dparam, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price);
   if (m_Mem.IsFull) ObjectMove(GetInfoTerminal().ID, def_NameObjectLineH, class="num">0, class="num">0, m_Info.Data.Position.Price = AdjustPrice(m_Info.Data.Position.Price));
   m_Info.Data.Position.dt = AdjustTime(m_Info.Data.Position.dt);
   ChartTimePriceToXY(GetInfoTerminal().ID, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price, m_Info.Data.Position.X, m_Info.Data.Position.Y);
   if ((m_Info.Study != eStudyNull) && (m_Mem.IsFull)) ObjectMove(GetInfoTerminal().ID, def_NameObjectLineV, class="num">0, m_Info.Data.Position.dt, class="num">0);
   m_Info.Data.ButtonStatus = (class="type">uint) sparam;
   if (CheckClick(eClickMiddle))

鼠标拖线到执行态的切换细节

这段逻辑处理的是研究对象在图表上的交互生命周期。第202行先判断内存标记未填满、或水平线对象颜色不等于 clrNONE 时,调用 CreateStudy() 保证图形已创建;这是避免对象丢失后界面空洞的兜底。 左键点击且当前状态为 eStudyCreate 时(203行),先关掉图表鼠标滚动 CHART_MOUSE_SCROLL=false(205行),防止拖拽时画面乱跑。若内存已满,则用 ObjectMove 把水平线 T 移到鼠标位置的日期与时间,并把 memPrice 记成鼠标价格(206行),随后状态切到 eStudyExecute(207行)。 进入执行态后,ExecuteStudy(memPrice) 被调用(209行),并把 m_Info.Data.ExecStudy 同步为是否处于执行态(210行)。外汇与贵金属市场波动剧烈、杠杆风险高,这类交互逻辑只解决操作流畅度,不预示任何方向。 对象被删除的事件里(212行),若删的是水平线 H 且内存已满,就按原色重建一条 OBJ_HLINE(213行)。这样用户误删后不会丢失基准线,下一次拖拽仍能接着用。

MQL5 / C++
if ((!m_Mem.IsFull) || ((class="type">color)ObjectGetInteger(GetInfoTerminal().ID, def_NameObjectLineH, OBJPROP_COLOR) != clrNONE)) CreateStudy();
if (CheckClick(eClickLeft) && (m_Info.Study == eStudyCreate))
  {
   ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false);
   if (m_Mem.IsFull) ObjectMove(GetInfoTerminal().ID, def_NameObjectLineT, 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 == def_NameObjectLineH)) CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH);
  class="kw">break;

◍ 收尾前撤销宏定义避免命名污染

在指标或脚本的源码末尾,常会集中用 #undef 把前面为了方便拼接对象名而定义的宏全部撤掉。上面这段就一口气清掉了 5 个前缀与对象名宏:def_MousePrefixName、def_NameObjectLineV、def_NameObjectLineH、def_NameObjectLineT、def_NameObjectStudy,最后以注释行收口。 这么做不是为了运行效率,而是防止这些宏泄漏到被本文件 #include 的其它文件里。MT5 的预处理是文本级替换,宏不撤,下游文件里出现同名标识符就可能被悄悄改写,调 Bug 时极难察觉。 实际写 EA 或指标时,建议把这类 #define 控制在最小作用域:如果只在某个 .mqh 内部用,就在该文件末尾成对 #undef。开 MT5 随便翻一个自带指标的源码,搜索 #undef 就能看到官方也是这个习惯。

MQL5 / C++
class="macro">#undef def_MousePrefixName
class="macro">#undef def_NameObjectLineV
class="macro">#undef def_NameObjectLineH
class="macro">#undef def_NameObjectLineT
class="macro">#undef def_NameObjectStudy
class=class="str">"cmt">//+------------------------------------------------------------------+

「用一段 service 代码撬开回放系统的黑箱」

很多人手里已经有成熟的 MQL5 工具链,或者身边就有写代码的老手,所以容易把这类底层探索当成废话。但真正吃透价格行为回放机制,得自己从零跑一遍——文末这段服务代码比模拟器轻量,逻辑却完全一致,复制进 MT5 就能验证。 代码第 06 行用 input 把交易品种写死成 EURUSD,你改字符串就能换标的,但前提是该品种已在市场观察窗口可见,第 13 行 SymbolSelect 就是干这个。第 14 行 ChartOpen 按 PERIOD_M5 开一张 5 分钟图,返回图表 ID 存进 long 型变量。 第 15 行 iCustom 挂的是一个叫 Mouse Study.ex5 的自定义指标,路径写在 \\Indicators\\Replay\\ 下,把图表 ID 当参数喂进去;第 16 行 ChartIndicatorAdd 把它绑到主图,第 17 行 IndicatorRelease 释放句柄——指标已上图,句柄留着没用还占资源。第 19 行 Print 把图表 ID 打到终端日志,方便你回头查。 第 21 行是阻塞核心:每 300 毫秒睡一次,只要服务没被强制停、图表品种没变就一直轮询。用户手动关图,第 23 行 ChartClose 无图可关等于空操作;若是停掉服务,它就顺手把图关掉,第 24 行再把品种从市场观察移除。注意 SymbolSelect(...,false) 要成功,该品种不能挂着未平订单或残留对象,本例纯测试所以干净。 外汇与贵金属杠杆高、滑点诡异,回放不等于实盘,这段只证明机制可复现。别因为谁号称专家就照单全收,自己改个品种名跑通,才算摸到门道。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property service
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">input class="type">class="kw">string user00 = "EURUSD"; class=class="str">"cmt">// Symbol
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
   class="type">long id;
   class="type">int handle;
   
   SymbolSelect(user00, true);
   id = ChartOpen(user00, PERIOD_M5);
   handle = iCustom(ChartSymbol(id), ChartPeriod(id), "\Indicators\Replay\Mouse Study.ex5", id);
   ChartIndicatorAdd(id, class="num">0, handle);
   IndicatorRelease(handle);
 
   Print("ID: ", id);
   
   while ((!_StopFlag) && (ChartSymbol(id) == user00)) Sleep(class="num">300);
   
   ChartClose(id);
   SymbolSelect(user00, false);   
}
class=class="str">"cmt">//+------------------------------------------------------------------+

把工具请下神坛

这套市场模拟的 SQL 与套接字系列走到这里,核心其实就一句:再复杂的回放、模拟、交互系统,落到 MT5 里也只是可拆解的代码模块,不是黑箱魔法。前面二十节把数据库读写、网络通讯、指标联动逐一拆开,你照着跑通一套,就能自己改参数、加字段、换资产。 真正卡住人的从来不是 MQL5 语法,而是把「想验证的行情假设」翻译成「可被程序重放的序列」这一步。外汇与贵金属自带高杠杆高风险,模拟结论只代表历史环境概率,别拿回放曲线当实盘保票。 下一篇若做鼠标指标和控制指标的交互,思路还是老样子:先在小布里把逻辑画成图,再让代码替你跑。工具用熟了,它就只是你桌上另一把扳手。

把模块边界交给小布盯盘核对
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到哪些自定义模板越过了回放系统的安全边界,你专注决策而不是翻源码。

常见问题

早期鼠标逻辑散落在多处,导致控制指标与系统状态不一致;集中到鼠标指标模块后,集成和测试都更可控,也避免真实账户误触发。
模板本身中性,但若回放系统未限制指标可放置的图表范围,用户可能把仅适合模拟的逻辑带到实盘,概率上增加不稳定风险。
例如用户在服务外手动加载控制指标、或用脚本强行调用内部模块;这类操作不在原设计路径内,可能破坏回放与实盘隔离。
可以,小布盯盘的品种页会标记当前模板调用的模块来源,若检测到本应仅限回放环境的模块出现在普通图表,会给出提示。
不必逐行读,但需知道哪些模块用户可动、哪些由服务锁定;理解边界比看懂实现更能避免误操作带来的账户风险。