开发回放系统(第29部分):EA 交易项目C_Mouse类(三)·进阶篇
(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 分钟边界。外汇与贵金属波动剧烈、滑点频发,这类自动重建机制能降低人工遗漏,但无法消除行情跳空带来的高风险。
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 会因字体映射偏差算错框。
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 让对象移出可视区。外汇与贵金属杠杆高,这类鼠标跟随面板在快速波动时可能采到滞后坐标,上线前建议在策略测试器里跑一遍多分辨率验证。
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,无参版只管状态文本与绘制,结构清晰也好单步跟。
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) {