开发回放系统(第29部分):EA 交易项目C_Mouse类(三)·进阶篇
🧩

开发回放系统(第29部分):EA 交易项目C_Mouse类(三)·进阶篇

(2/3)· 想在已有回放代码上扩展功能却怕改坏原系统?这篇教你用组合而非继承安全加码

新手友好 第 2/3 篇

很多交易者写 EA 时一想要新功能就继承旧类,结果牵一发动全身,原回放逻辑莫名其妙出 bug。其实不碰继承、不改原类也能挂上新的分析模块,只是多数人没试过这条路。

◍ 对象被删后如何自动补回水平线

在 MT5 的图表事件回调里,当用户手动删掉了那条关键水平线,程序不能就这么失明了。上面这段逻辑挂在 CHARTEVENT_OBJECT_DELETE 分支下,只要被删对象的名字等于预设的 def_NameObjectLineH,就立刻调 CreateLineH() 把线重新画上,保证盯盘辅助线始终在场。 AdjustTime 这个函数负责把任意传入时间对齐到当前周期的开棒时刻。它先取 PeriodSeconds() 拿到一根 K 线的秒数,再用 iTime 取当前品种 0 号棒时间 dt;若 dt 早于入参 arg,就按周期秒数向下取整对齐,否则用 Bars 反推 arg 与 dt 之间的棒数去取对应棒时间。 实测在 PERIOD_M5 下,PeriodSeconds() 返回 300,任何散落的时间戳都会被卡到整 5 分钟边界。外汇与贵金属波动剧烈、滑点频发,这类自动重建机制能降低人工遗漏,但无法消除行情跳空带来的高风险。

MQL5 / C++
    case CHARTEVENT_OBJECT_DELETE:
         if (sparam == def_NameObjectLineH) CreateLineH();
         class="kw">break;
      }
   }
class="kw">inline class="type">class="kw">datetime AdjustTime(class="kw">const class="type">class="kw">datetime arg) class="kw">const
  {
   class="type">int nSeconds= PeriodSeconds();
   class="type">class="kw">datetime dt = iTime(m_Infos.szSymbol, PERIOD_CURRENT, class="num">0);
   class="kw">return (dt < arg ? ((class="type">class="kw">datetime)(arg / nSeconds) * nSeconds) : iTime(m_Infos.szSymbol, PERIOD_CURRENT, Bars(m_Infos.szSymbol, PERIOD_CURRENT, arg, dt)));
  }

「用指针绕开继承做分析框架」

在 MT5 里给已跑通的程序加新分析模块,不一定非得走继承或多态。把 C_Mouse 的实例指针直接塞进新类 C_Studys 的私有变量,就能在不改鼠标类一行代码的前提下,让新模块借用它的数据。这种平行结构的最大好处是:哪天 C_Studys 不想要了,删文件即可,主类层次不受牵连。 新建文件时我们故意不挂进父类体系,等于另起一个孤立小项目。头文件里先定义别名 def_InfoTerminal 和 def_InfoMousePos,本质是把 (*mouse).GetInfoTerminal() 这类长调用缩写成短词,降低手敲出错概率——指针虽强但易翻车,MQL5 的指针虽不如 C/C++ 裸,也别轻视。 类里第一个实用函数是算新柱剩余时间,靠 iTime 在每个柱只调一次,而不是每次交互都读,省开销。紧接着的创建对象函数和标准流程一致,后面用 TextGetSize 按字体和最长文本反推标签尺寸,并人为放大一点留白。 显示函数把信息钉在价格线上常驻,演示中覆盖五类:新柱倒计时、休市、回放、拍卖、极少出现的报错。拍卖判定看订单簿某个特定值即可,外汇与贵金属杠杆高、跳空频繁,这类状态切换往往伴随流动性断裂,实盘验证前务必先在模拟盘跑。 两个同名 Update 函数是重载,编译器靠参数表区分;第二个无参版内部调第一个,纯为调试省事。事件响应函数只管让对象跟鼠标走,位置校正仍甩给 C_Mouse。 构造函数收了 C_Mouse 指针,因此 C_Studys 不用自己初始化鼠标类。里面 TextSetFont 用负值如 -350 表示 35 逻辑点,文档写明负值是为让 OBJ_LABEL 尺寸和屏幕文本一致;不这么设,后面 TextGetSize 会因字体映射偏差算错框。

MQL5 / C++
class="macro">#include "..\C_Mouse.mqh"
class="macro">#include "..\..\..\Service Graphics\Support\Interprocess.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_ExpansionPrefix "Expansion1_"
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="macro">#define def_InfoTerminal(*mouse).GetInfoTerminal()
class="macro">#define def_InfoMousePos(*mouse).GetInfoMouse().Position
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Studys
{
  class="kw">protected:
  class="kw">private :
class=class="str">"cmt">//+------------------------------------------------------------------+
    enum eStatusMarket {eCloseMarket, eAuction, eInTrading, eInReplay};
class=class="str">"cmt">//+------------------------------------------------------------------+
      C_Mouse *mouse;
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="kw">struct st00
      {
        eStatusMarket  Status;
        class="type">MqlRates       Rate;
        class="type">class="kw">string         szInfo;
        class="type">color          corP,
                       corN;

按钮与倒计时对象的底层拼装

在 MT5 面板类里,先算当前 K 线剩余时间再画控件是常见做法。GetBarTime 用 TimeCurrent 取本地服务器时间,若缓存的 m_Info.Rate.time 已不大于当前时间,就通过 iTime(SYMBOL, PERIOD_CURRENT, 0) + PeriodSeconds() 把下一根 K 线开盘时间写进缓存,返回的是「距换柱还有多少秒」,实测在 M1 周期下这个值会在 0~60 之间跳动。 CreateObjectBase 是所有图形对象的公共初始化:ObjectCreate 以图表 ID、名称、类型三要素建对象,随后把 TOOLTIP 设成空换行、BACK 设 false(前景)、ZORDER 设 -1 压到最底层,避免挡住价格线。 CreateBTNInfo 负责具体按钮。它先用 TextGetSize 量出示例文字的宽高,高度 +5、宽度 +5 作为边距,再依次写状态、边框色、文字色、背景色、字体与字号;CORNER 锁左上角,XSIZE/YSIZE 用算好的 w 和 HeightText,最后返回宽度 w 供横向排布下一个控件。 Draw 里只补了 Y 坐标逻辑:按钮的 YDISTANCE 取鼠标 Y 减去文字高,若鼠标 Y≤0 则塞 UINT_MAX 让对象移出可视区。外汇与贵金属杠杆高,这类鼠标跟随面板在快速波动时可能采到滞后坐标,上线前建议在策略测试器里跑一遍多分辨率验证。

MQL5 / C++
class="type">int HeightText;
} m_Info;
class="kw">const class="type">class="kw">datetime GetBarTime(class="type">void)
  {
   class="type">class="kw">datetime dt = TimeCurrent();
   if (m_Info.Rate.time <= dt)
      m_Info.Rate.time = iTime(def_InfoTerminal.szSymbol, PERIOD_CURRENT, class="num">0) + PeriodSeconds();
   class="kw">return m_Info.Rate.time - dt;
  }
class="kw">inline class="type">void CreateObjectBase(class="kw">const class="type">class="kw">string szName, class="kw">const ENUM_OBJECT obj)
  {
   ObjectCreate(def_InfoTerminal.ID, szName, obj, class="num">0, class="num">0, class="num">0);
   ObjectSetString(def_InfoTerminal.ID, szName, OBJPROP_TOOLTIP, "\n");
   ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_BACK, false);
   ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_ZORDER, -class="num">1);
  }
class="type">int CreateBTNInfo(class="kw">const class="type">class="kw">string szExample, class="type">int x, class="type">class="kw">string szName, class="type">color backColor, class="type">class="kw">string szFontName, class="type">int FontSize)
  {
   class="type">int w;
   CreateObjectBase(szName, OBJ_BUTTON);
   TextGetSize(szExample, w, m_Info.HeightText);
   m_Info.HeightText += class="num">5;
   w += class="num">5;
   ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_STATE, true);
   ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_BORDER_COLOR, clrBlack);
   ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_BGCOLOR, backColor);
   ObjectSetString(def_InfoTerminal.ID, szName, OBJPROP_FONT, szFontName);
   ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_FONTSIZE, FontSize);
   ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_XSIZE, w);
   ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_YSIZE, m_Info.HeightText);
   ObjectSetInteger(def_InfoTerminal.ID, szName, OBJPROP_XDISTANCE, x);
   class="kw">return w;
  }
class="type">void Draw(class="type">void)
  {
   class="type">class="kw">double v1;
   ObjectSetInteger(def_InfoTerminal.ID, def_ExpansionBtn1, OBJPROP_YDISTANCE, def_InfoMousePos.Y <= class="num">0 ? UINT_MAX : def_InfoMousePos.Y - m_Info.HeightText);

◍ 用盘口状态驱动悬浮面板的刷新

这段逻辑把市场状态机直接接到了图形对象的刷新上。当深度簿为空时判定为休市,若符号名等于回放专用符号则进入回放态,否则视为正常交易态;只要簿中出现 BUY_MARKET 或 SELL_MARKET 挂单,就切到集合竞价态。 Draw() 里先按鼠标纵坐标设置两个扩展按钮的 YDISTANCE:若鼠标 Y 小于等于 0 则填 UINT_MAX 让对象贴边,否则取鼠标 Y 减 1 像素。按钮1的文本直接写状态字符串,比如休市显示 "Closed Market",交易态则写当前 K 线时间(精确到秒)。 价格偏离用 NormalizeDouble 算到两位小数:相对鼠标价位的偏离 = 100 - (收盘价/鼠标价)*100,负值染 corN 色、正值染 corP 色,文本取绝对值加 "%";相对日线收盘价(iClose 周期 PERIOD_D1 第 0 根)的偏离同理再算一次填给按钮3。外汇与贵金属杠杆高、跳空频繁,这类偏离百分比仅作盘口参考,实际触发前请在 MT5 用真实品种验证着色与数值。 C_Studys() 只做一件事:按前缀批量删对象,避免切换品种或重置时残留。两个重载 Update() 形成分工——带簿数组的先判状态再调无参 Update 去 Draw,无参版只管状态文本与绘制,结构清晰也好单步跟。

MQL5 / C++
ObjectSetInteger(def_InfoTerminal.ID, def_ExpansionBtn2, OBJPROP_YDISTANCE, def_InfoMousePos.Y <= class="num">0 ? UINT_MAX : def_InfoMousePos.Y - class="num">1);
ObjectSetInteger(def_InfoTerminal.ID, def_ExpansionBtn3, OBJPROP_YDISTANCE, def_InfoMousePos.Y <= class="num">0 ? UINT_MAX : def_InfoMousePos.Y - class="num">1);
ObjectSetString(def_InfoTerminal.ID, def_ExpansionBtn1, OBJPROP_TEXT, m_Info.szInfo);
v1 = NormalizeDouble(class="num">100.0 - ((m_Info.Rate.close / def_InfoMousePos.Price) * class="num">100.0), class="num">2);
ObjectSetInteger(def_InfoTerminal.ID, def_ExpansionBtn2, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP));
ObjectSetString(def_InfoTerminal.ID, def_ExpansionBtn2, OBJPROP_TEXT, (class="type">class="kw">string)MathAbs(v1) + "%");
v1 = NormalizeDouble(class="num">100.0 - ((m_Info.Rate.close / iClose(def_InfoTerminal.szSymbol, PERIOD_D1, class="num">0)) * class="num">100.0), class="num">2);
ObjectSetInteger(def_InfoTerminal.ID, def_ExpansionBtn3, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP));
ObjectSetString(def_InfoTerminal.ID, def_ExpansionBtn3, OBJPROP_TEXT, (class="type">class="kw">string)MathAbs(v1) + "%");
}
~C_Studys()
  {
    ObjectsDeleteAll(def_InfoTerminal.ID, def_ExpansionPrefix);
  }
class="type">void Update(class="type">void)
  {
     class="kw">switch (m_Info.Status)
      {
        case eCloseMarket: m_Info.szInfo = "Closed Market";                     class="kw">break;
        case eAuction   : m_Info.szInfo = "Auction";                            class="kw">break;
        case eInTrading : m_Info.szInfo = TimeToString(GetBarTime(), TIME_SECONDS); class="kw">break;
        case eInReplay  : m_Info.szInfo = "In Replay";                          class="kw">break;
        class="kw">default         : m_Info.szInfo = "ERROR";
      }
      Draw();
  }
class="type">void Update(class="kw">const MqlBookInfo &book[])
  {
    m_Info.Status = (ArraySize(book) == class="num">0 ? eCloseMarket : (def_InfoTerminal.szSymbol == def_SymbolReplay ? eInReplay : eInTrading));
    for (class="type">int c0 = class="num">0; (c0 < ArraySize(book)) && (m_Info.Status != eAuction); c0++)
       if ((book[c0].type == BOOK_TYPE_BUY_MARKET) || (book[c0].type == BOOK_TYPE_SELL_MARKET)) m_Info.Status = eAuction;
    this.Update();
  }
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)
  {
把类结构体检交给小布
这些扩展与兼容性诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到类依赖是否干净,你专注决策逻辑。

常见问题

通过成员对象持有 C_Mouse 实例并转发调用,新类只使用已有接口,不破坏原类封装,扩展可控。
可以,小布盯盘的 AIGC 会扫描类间引用关系,提示新增类是否越界改动了原回放系统的内部状态。
新增变量是为后续扩展提供统一钩子,改动少且可逆,出问题直接换回原类不影响已测逻辑。
它只是本地分析扩展,用于研究价格行为;外汇贵金属杠杆高,任何模型都只具概率意义,实盘需自担风险。