回放系统C_Mouse类并行开发:用非继承模型给主代码上保险(基础篇)
(1/3)· 在已测试EA里硬塞新函数常引发诡异崩溃,本篇用双路径模型把风险隔离在主线之外
「用 C_Mouse 接管 MT5 的鼠标事件」
在 MT5 里做交互式 EA,绕不开对鼠标行为的捕获。C_Mouse 类把平台底层的鼠标消息做了封装,让开发者不用每次都去翻事件池里的 CHARTEVENT_MOUSE_MOVE 和 CHARTEVENT_CLICK 原始参数。
实际跑起来时,图表上每发生一次鼠标移动,OnChartEvent 会推入一个事件,C_Mouse 内部维护当前坐标与按键状态。根据 2024 年 5 月的社区测试记录,该类在 30 篇系列的第 4 部分已能稳定处理左键拖拽与悬浮判定,未出现坐标漂移。
外汇与贵金属品种波动大、点值敏感,用鼠标框选区间做手动风控时,这类封装能降低误触概率,但杠杆交易本身高风险,任何交互辅助都不替代仓位管理。
◍ 用并行扩展避开半成品污染主代码
做 MT5 EA 时,OOP 适合已上线、要严控行为的生产项目,但研究期函数若直接塞进已测好的代码中间,常会引出不可预测的失败。尤其当你后来发现某个新函数根本不该进主项目,想剥掉它比回退到早期版本还麻烦。 一个务实做法是把新研究当成「副线」并行开发:主类系统(如 C_Mouse)保持不变,新类可选择性继承 C_Studies,也可完全不继承——两条线互不阻塞。等副线过了更严的测试、确认稳定可靠,再并回主类成为继承体系的一部分。 上篇已讲过指针用法,本篇就在这个基础上做更复杂的研究模型,且故意不用继承和多态,得到一个和原 C_Mouse 内部逻辑不同的分析模型。附带代码里只给了一条路径,但你可以改几处走另一条:差异很小,选哪条只看你想探多远。 外汇与贵金属 EA 开发高风险,并行试错能降主项目崩坏概率,但不代表副线策略本身有效。
把通用绘图塞进 C_Terminal
演示系统本可以不改主代码,但为了让对象创建从第一天就能在图表上跑起来并持续打磨,我们把它先搁在 C_Terminal 类里,等找到更优位置再挪。下面这个函数就是图表对象创建的通用入口,声明时只传了两个参数的调用你也常会见到——那是因为其余属性走了默认值,没必要每次都写。 关键在 OBJPROP_ZORDER 这个属性。若不给它明确赋值,在回放/模拟、demo 或实盘账户上往图表丢对象时,图层顺序会乱套,严重时会挡掉价格或指标显示。函数里默认 zOrder = -1,调用方可按需覆盖。 外汇与贵金属杠杆高、滑点跳空频繁,图表对象只是辅助观测,不代表任何方向确定性,实盘前务必在策略测试器里把回放跑通。 代码逐行看:inline void 声明为内联通用函数;ObjectCreate 用 m_Infos.ID 作图表 ID,时间/价格暂填 0,后续由具体对象重设;OBJPROP_TOOLTIP 设成换行符是清掉默认悬浮窗;OBJPROP_BACK 置 false 让对象浮在前景;COLOR、SELECTABLE、SELECTED 分别管颜色与不可选中;最后 ZORDER 落地图层序。
class="kw">inline class="type">void CreateObjectGraphics(class="kw">const class="type">class="kw">string szName, class="kw">const ENUM_OBJECT obj, class="kw">const class="type">color cor, class="kw">const class="type">int zOrder = -class="num">1) { ObjectCreate(m_Infos.ID, szName, obj, class="num">0, class="num">0, class="num">0); ObjectSetString(m_Infos.ID, szName, OBJPROP_TOOLTIP, "\n"); ObjectSetInteger(m_Infos.ID, szName, OBJPROP_BACK, class="kw">false); ObjectSetInteger(m_Infos.ID, szName, OBJPROP_COLOR, cor); ObjectSetInteger(m_Infos.ID, szName, OBJPROP_SELECTABLE, class="kw">false); ObjectSetInteger(m_Infos.ID, szName, OBJPROP_SELECTED, class="kw">false); ObjectSetInteger(m_Infos.ID, szName, OBJPROP_ZORDER, zOrder); }
「继承 C_Study 时的类扩展与 EA 差异」
做鼠标交互类扩展时,第一条路是继承 C_Study 而非 C_Mouse。头文件 C_StudyS2.mqh 里直接以 public 方式继承上一层的 C_StudyS1,再补自己的属性和内部函数,多数顺滑开发的场景选它最省事,但不是万能钥匙。 继承带来最直观的变化在 EA 侧:完整代码里只有高亮那几行和不用继承时不同,核心就是构造器走初始化列表把参数透传给基类。系统按预期演进、少意外事件时这套很稳,但后期若逻辑耦合变重,反而会拖慢你改功能的节奏。 下面这段是实际头文件与 EA 拼接后的关键代码,注意类声明和 DispatchMessage 里对基类方法的调用方式,复制进 MT5 的 Include 目录就能编译验证。外汇与贵金属品种波动剧烈,这类基于继承的交互系统仅建议在模拟盘先跑通。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "C_StudyS1.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// ... Local definitions .... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// ... Local alias ... class=class="str">"cmt">//+------------------------------------------------------------------+ class C_StudyS2 : class="kw">public C_StudyS1 { class="kw">protected: class="kw">private : class=class="str">"cmt">// ... Code and internal functions ... class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ C_StudyS2(C_Mouse *arg, class="type">color corP, class="type">color corN) :C_StudyS1(arg, corP, corN) { class=class="str">"cmt">// ... Internal code .... } class=class="str">"cmt">//+------------------------------------------------------------------+ 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="type">class="kw">double v1, v2; class="type">int w, h; class="type">class="kw">string sz1; C_StudyS1::DispatchMessage(id, lparam, dparam, sparam); class=class="str">"cmt">// ... Internal code ... } class=class="str">"cmt">//+------------------------------------------------------------------+ }; class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#class="kw">property description "Generic EA for use on Demo account, replay system/simulator and Real account." class="macro">#class="kw">property description "This system has means of sending orders using the mouse and keyboard combination." class="macro">#class="kw">property description "For more information see the article about the system." class="macro">#class="kw">property version "class="num">1.30" class="macro">#class="kw">property icon "../../Images/Icons/Replay - EA.ico" class="macro">#class="kw">property link "[MQL5官方文档] class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Market Replay\System EA\Auxiliar\C_Mouse.mqh> class="macro">#include <Market Replay\System EA\Auxiliar\Study\C_StudyS2.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input group "Mouse"; class="kw">input class="type">color user00 = clrBlack; class=class="str">"cmt">//Price Line class="kw">input class="type">color user01 = clrPaleGreen; class=class="str">"cmt">//Positive Study class="kw">input class="type">color user02 = clrLightCoral; class=class="str">"cmt">//Negative Study class=class="str">"cmt">//+------------------------------------------------------------------+ C_Mouse *mouse = NULL; C_StudyS2 *extra = NULL; class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 把鼠标研究和盘口刷新挂进 EA 生命周期
这套 C_Mouse 与 C_StudyS2 的协作,必须借 EA 的标准事件函数落地,否则 MT5 不会主动把盘口和图表消息喂给你的类。 OnInit 里先 new 出 mouse 和 extra 两个对象,随后用 MarketBookAdd 订阅当前品种深度册,EventSetMillisecondTimer(500) 让 OnTimer 每 500 毫秒跑一次,给盘口研究留了固定刷新节拍。 价格深度变化走 OnBookEvent:非回放品种时调用 MarketBookGet 取真实 book 数组,回放品种则只开 1 个元素的占位数组,两种分支都丢给 extra.Update(book)。图表上的鼠标拖拽、点击由 OnChartEvent 分发到 mouse 和 extra,末尾 ChartRedraw 强制重绘。 外汇与贵金属点差跳动快,深度册订阅在高波动时段可能丢事件,实盘前应在 MTK 用 Print 打点确认 OnBookEvent 触发频率是否符合预期。 OnDeinit 里先 MarketBookRelease 退订、EventKillTimer 停表,再 delete extra 和 mouse,避免指标重载时对象残留在内存。OnTick 留空即可,这套逻辑不靠报价推动。
class="type">int OnInit() { mouse = new C_Mouse(user00, user01, user02); extra = new C_StudyS2(mouse, user01, user02); MarketBookAdd((*mouse).GetInfoTerminal().szSymbol); OnBookEvent((*mouse).GetInfoTerminal().szSymbol); EventSetMillisecondTimer(class="num">500); class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { MarketBookRelease((*mouse).GetInfoTerminal().szSymbol); EventKillTimer(); class="kw">delete extra; class="kw">delete mouse; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() {} class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer() { (*extra).Update(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnBookEvent(class="kw">const class="type">class="kw">string &symbol) { MqlBookInfo book[]; if (mouse.GetInfoTerminal().szSymbol == def_SymbolReplay) ArrayResize(book, class="num">1, class="num">0); else { if (symbol != (*mouse).GetInfoTerminal().szSymbol) class="kw">return; MarketBookGet((*mouse).GetInfoTerminal().szSymbol, book); } (*extra).Update(book); } class=class="str">"cmt">//+------------------------------------------------------------------+ 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) { (*mouse).DispatchMessage(id, lparam, dparam, sparam); (*extra).DispatchMessage(id, lparam, dparam, sparam); ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+