使用面向对象的方法来编写EA的模式·综合运用
(3/3)·从开发阶段矩阵到七种MQL5运行模式,一篇收束前两部分未尽的工程化拼图
◍ 策略基类的虚函数与信号检查骨架
在 MT5 里写一套可复用的 EA 框架,第一步往往是把「主流程」和「信号判定」拆进基类。下面这段 CModeBase 的声明,把开仓、平仓、移动止损都留成虚函数,子类只重写逻辑,不碰调度。 公开区里 Main/Open/Close/Trail 全是 virtual void,等于给后代类留了四个空插槽;CheckMqlMode 是静态方法,用来在初始化时判断当前是实时还是回测环境。SetTrailing 接收两个 uint 参数,分别控制止损绝对值与步进,调参时直接改这两个数就能切换激进或保守风控。 保护区的 CheckOpenSignal / CheckCloseSignal / CheckTrailSignal 统一返回 ENUM_ORDER_TYPE,把「该不该动手」收敛成枚举,避免满屏 bool 旗标;GetMacdVal 带一个 _is_main 默认 true,说明主线和信号线取值走同一入口。私有区的 NormalPrice / NormalSL / NormalTP / NormalLot 负责把外部传来的浮点按品种精度与仓位规则做裁剪,外汇与贵金属点差跳动大,这类标准化函数能少踩很多坑。 CModeTester 直接继承 CModeBase,注释写明专用于测试模式,意味着你新写的策略先丢进这个类跑历史数据,验证信号链没断再换实盘派生类。外汇贵金属高风险,回测通过不等于实盘概率占优,开 MT5 用策略测试器跑一遍 EURUSD 的 2023 年 tick 数据最直观。
class="kw">virtual class="type">void Main(class="type">void){}; class=class="str">"cmt">//--- 程序 class="kw">virtual class="type">void Open(class="type">void){}; class="kw">virtual class="type">void Close(class="type">void){}; class="kw">virtual class="type">void Trail(class="type">void){}; class=class="str">"cmt">//--- 服务 class="kw">static ENUM_MQL_MODE CheckMqlMode(class="type">void); ENUM_MQL_MODE GetMqlMode(class="type">void); class="type">void SetMqlMode(class="kw">const ENUM_MQL_MODE _mode); class="type">void SetTrailing(class="kw">const class="type">uint _trailing,class="kw">const class="type">uint _trail_step); class="kw">protected: class=class="str">"cmt">//--- 函数 ENUM_ORDER_TYPE CheckOpenSignal(class="kw">const ENUM_ORDER_TYPE _open_sig); ENUM_ORDER_TYPE CheckCloseSignal(class="kw">const ENUM_ORDER_TYPE _close_sig); ENUM_ORDER_TYPE CheckTrailSignal(class="kw">const ENUM_ORDER_TYPE _trail_sig,class="type">class="kw">double &_sl_pr); class=class="str">"cmt">//--- class="type">class="kw">double GetMacdVal(class="kw">const class="type">int _idx,class="kw">const class="type">bool _is_main=true); class="kw">private: class=class="str">"cmt">//--- 宏 class="type">bool RefreshIndicatorData(class="type">void); class=class="str">"cmt">//--- 标准化 class="type">class="kw">double NormalPrice(class="type">class="kw">double d); class="type">class="kw">double NormalDbl(class="type">class="kw">double d,class="type">int n=-class="num">1); class="type">class="kw">double NormalSL(class="kw">const ENUM_ORDER_TYPE _ord_type,class="type">class="kw">double op,class="type">class="kw">double pr, class="type">uint SL,class="type">class="kw">double stop); class="type">class="kw">double NormalTP(class="kw">const ENUM_ORDER_TYPE _ord_type,class="type">class="kw">double op,class="type">class="kw">double pr, class="type">uint _TP,class="type">class="kw">double stop); class="type">class="kw">double NormalLot(class="kw">const class="type">class="kw">double _lot); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CModeTester 类 | class=class="str">"cmt">//| 目的:用于测试模式的类 | class=class="str">"cmt">//| 继承于CModeBase类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CModeTester : class="kw">public CModeBase { class=class="str">"cmt">//--- === 方法 === --- class="kw">public: class=class="str">"cmt">//--- 构造函数/析构函数
「把交易循环塞进一个 Main 里跑」
回测框架里最省事的做法,是把平仓、开仓、追踪止损三个动作收进一个虚函数 Main,每根 K 线只调一次 this.Main()。下面这段 CModeTester 的声明和 Main 实现,就是典型骨架。 [CODE] void CModeTester(void){}; void ~CModeTester(void){}; //--- 模块 virtual void Main(void); //--- 程序 virtual void Open(void); virtual void Close(void); virtual void Trail(void); }; //+------------------------------------------------------------------+
| // | 主模块 |
|---|
//+------------------------------------------------------------------+ void CModeTester::Main(void) { //--- 1) 平仓 this.Close(); //--- 2) 开仓 this.Open(); //--- 3) 滑动止损 this.Trail(); } [/CODE] 逐行看:前两句是构造和析构的空实现;Main、Open、Close、Trail 都标了 virtual,意味着子类能各自重写逻辑。Main 函数体内严格按 平仓→开仓→追踪 的顺序调用,避免先开仓再平旧仓造成的手数叠加。 实盘日志里能直接验证这套顺序。2014.11.14 22:15:00 这根 bar,先打印 Signal to open: SELL,紧接着 FI 行显示倒数第二根 bar 的 main 值 0.002117、signal 值 0.002109,DL 行显示最后一根 bar main 0.002001、signal 0.002118——signal 上穿触发卖单。随后 LO 行成交 market sell 0.03 EURUSD.e,价格 1.25242,点差 0.9 点(1.25251-1.25242)。 外汇和贵金属杠杆高,这类按 bar 触发的回测信号在实盘可能因滑点、停牌而偏离;上述 0.03 手 EURUSD.e 成交仅作框架验证,不暗示任何收益。开 MT5 用策略测试器跑同时间段,把 Main 里的三个调用顺序调换,能直观看到平仓滞后带来的浮亏扩大。
class="type">void CModeTester(class="type">void){}; class="type">void ~CModeTester(class="type">void){}; class=class="str">"cmt">//--- 模块 class="kw">virtual class="type">void Main(class="type">void); class=class="str">"cmt">//--- 程序 class="kw">virtual class="type">void Open(class="type">void); class="kw">virtual class="type">void Close(class="type">void); class="kw">virtual class="type">void Trail(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 主模块 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CModeTester::Main(class="type">void) { class=class="str">"cmt">//--- class="num">1) 平仓 this.Close(); class=class="str">"cmt">//--- class="num">2) 开仓 this.Open(); class=class="str">"cmt">//--- class="num">3) 滑动止损 this.Trail(); }
从成交日志反推平仓信号触发
一段真实的 EURUSD.e 日志能拆出策略的平仓逻辑。2014.11.14 22:15 以 1.25242 市价卖空 0.03 手,持仓到 2014.11.17 13:30:20 才收到 SELL 平仓信号,随即以 1.25047 买入平仓,单边滑点显示成交价落在卖一 1.25039 与买一 1.25047 之间。 触发依据是两根bar的主线与信号线差值:倒数第二根 main -0.001218 / signal -0.001148,最后一根 main -0.001123 / signal -0.001189。信号线在末根 bar 下穿幅度扩大( signal 从 -0.001148 走到 -0.001189),主线收敛,组合被判定为趋势衰竭。 这类日志直接暴露了 EA 的离场偏好:不在实时 tick 平仓,而是等当前 bar 走完、用已闭合的两根 bar 做交叉确认。外汇与贵金属杠杆高,这种延迟确认在跳空行情中可能扩大回撤,建议在 MT5 策略测试器里把同段日志导出来比对成交时间,验证你自己的平仓延迟参数。
◍ 把测试下单记录画到图上
从一段真实回测日志能看到,2014.11.17 13:30:20 在 EURUSD.e 以 1.25047 市价买入 0.03 手,这条记录由 CTrade::OrderSend 打出,说明策略在可视化测试模式下确实触发了开仓。 要把这类信号留在图表上供肉眼复核,可继承 CModeTester 写一个 CModeVisual 类。它内部用 CArrayObj 存图形对象,并用 m_subwindow_max / m_subwindow_min 记录子窗口的上下边界,方便把信号线缩放到正确位置。 类里公开了 Open() 与 Close() 两个虚函数,分别对应测试开始与结束时的绘图动作;私有方法 CreateSignalLine() 负责按是否开仓信号、是否新 K 线来画线,CreateRectangle() 按订单类型画矩形区,RefreshRectangles() 做批量刷新。外汇与贵金属回测含高杠杆风险,图形验证仅辅助判断信号逻辑,不预示实盘盈亏。
class CModeVisual : class="kw">public CModeTester { class=class="str">"cmt">//--- === 数据成员 === --- class="kw">private: CArrayObj m_objects_arr; class="type">class="kw">double m_subwindow_max; class="type">class="kw">double m_subwindow_min; class=class="str">"cmt">//--- === 方法 === --- class="kw">public: class=class="str">"cmt">//--- 构造函数/析构函数 class="type">void CModeVisual(class="type">void); class="type">void ~CModeVisual(class="type">void); class=class="str">"cmt">//--- 程序 class="kw">virtual class="type">void Open(class="type">void); class="kw">virtual class="type">void Close(class="type">void); class="kw">private: class="type">bool CreateSignalLine(class="kw">const class="type">bool _is_open_sig,class="kw">const class="type">bool _is_new_bar=true); class="type">bool CreateRectangle(class="kw">const ENUM_ORDER_TYPE _signal); class="type">void RefreshRectangles(class="type">void); };
「多模式开发的下一步落点」
把交易算法拆成多模式来写,确实会多出不少开发开销,但每一步都能逼你摸清楚 MT5 和 MQL5 各个阶段的细节,面向对象写法在这儿能省掉大量重复劳动。 本次附带的 CisNewBar.mqh(6.99 KB)与 Modes.mqh(31.24 KB)已经把新 K 线判定和模式切换的基础框架搭好,Test1_Modes_EA.mq5(4.75 KB)和 Test2_Modes_EA.mq5(5.04 KB)可直接丢进策略测试器跑通两种模式。 优化逻辑和时间框架聚合模式留到后续统计特性文章再展开,有兴趣的现在就能把 ZIP 里的五个 mq5 拖进 MT5 验证一遍基础架子的行为。外汇与贵金属波动剧烈、杠杆风险高,任何多模式 EA 都先在模拟盘确认稳定性再谈实盘。