理解编程范式(第 2 部分):面向对象方式开发价格行为智能系统·进阶篇
(2/3)· 从手机蓝图讲起,用类与对象把上一篇的EMA烛条系统重写成OOP范式
「用继承把EA里的对象层级搭起来」
MQL5 里写复杂 EA,最忌把所有属性平铺在一个结构体里。用 class 做基类,再派生子类的写法,能让行情外的辅助逻辑(比如参数分组、状态机)更清晰,也方便后续在 OnTick 里只调接口不碰细节。 下面这段先在 OnInit 里用 PhoneClass 造两个实例:型号 101 黑键盘非触屏 4 寸,型号 102 天蓝触屏 AMOLED 6 寸,各自调 PrintPhoneSpecs() 把规格打到日志。
class="macro">#include "PhoneClass.mqh" class="type">int OnInit() { PhoneClass myPhoneObject1(class="num">101, "Black", "Keyboard", "Non-touch LCD", class="num">4); PhoneClass myPhoneObject2(class="num">102, "SkyBlue", "Touchscreen", "Touch AMOLED", class="num">6); myPhoneObject1.PrintPhoneSpecs(); myPhoneObject2.PrintPhoneSpecs(); class="kw">return(INIT_SUCCEEDED); }
class SmartPhoneClass : class="kw">public PhoneClass { class="kw">private: class="type">class="kw">string operatingSystem; class="type">int numberOfCameras; class="kw">public: SmartPhoneClass(class="type">int modelNo, class="type">class="kw">string colorOfPhone, class="type">class="kw">string typeOfInput, class="type">class="kw">string typeOfScreen, class="type">int sizeOfScreen, class="type">class="kw">string os, class="type">int totalCameras) : PhoneClass(modelNo, colorOfPhone, typeOfInput, typeOfScreen, sizeOfScreen) { operatingSystem = os; numberOfCameras = totalCameras; } };
class="macro">#include "PhoneClass.mqh" class="type">int OnInit() { PhoneClass myPhoneObject1(class="num">101, "Black", "Keyboard", "Non-touch LCD", class="num">4); PhoneClass myPhoneObject2(class="num">102, "SkyBlue", "Touchscreen", "Touch AMOLED", class="num">6); myPhoneObject1.PrintPhoneSpecs(); myPhoneObject2.PrintPhoneSpecs(); class="kw">return(INIT_SUCCEEDED); } class SmartPhoneClass : class="kw">public PhoneClass { class="kw">private: class="type">class="kw">string operatingSystem; class="type">int numberOfCameras; class="kw">public: SmartPhoneClass(class="type">int modelNo, class="type">class="kw">string colorOfPhone, class="type">class="kw">string typeOfInput, class="type">class="kw">string typeOfScreen, class="type">int sizeOfScreen, class="type">class="kw">string os, class="type">int totalCameras) : PhoneClass(modelNo, colorOfPhone, typeOfInput, typeOfScreen, sizeOfScreen) { operatingSystem = os; numberOfCameras = totalCameras; } class="type">void PrintPhoneSpecs() class="kw">override { Print("-----------------------------------------------------------"); Print("Smartphone Specifications(including base phone specs):"); Print("-----------------------------------------------------------"); PrintFormat("Operating System: %s \nNumber of Cameras: %i", operatingSystem, numberOfCameras); PhoneClass::PrintPhoneSpecs(); Print("-----------------------------------------------------------"); } };
派生类如何改写基类打印逻辑
下面这段 MQL5 片段演示了基类与派生类方法调用的差异:对基类对象调用 PrintPhoneSpecs() 走的是原始实现,而对 SmartPhoneClass 实例调用同名方法,实际执行的是派生类重写的版本。 在 MT5 里粘贴这段代码到 EA 的 OnInit 中,编译运行后从日志能直接看到两行规格输出不同,证明虚方法重写已生效。 外汇与贵金属交易使用 EA 存在滑点、断线等高风险,验证代码逻辑时务必先在策略测试器跑历史数据。
myPhoneObject1.PrintPhoneSpecs(); class=class="str">"cmt">// base class method mySmartPhoneObject1.PrintPhoneSpecs(); class=class="str">"cmt">// overriden method by the derived class (SmartPhoneClass) class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(class="kw">const class="type">int reason){} class="type">void OnTick(){}
◍ MQL5 里类的六个属性与 C 前缀惯例
OOP 在 MQL5 里落地的六个属性先列清楚:封装把数据和方法捆进类里,内部细节对外隐藏;抽象只抓本质行为,把复杂系统削平;继承让子类复用父类逻辑;多态允许不同对象当公共基类用,换实现不换接口;类当蓝图、对象是实例,模块化切分代码;消息传递靠对象互发信号驱动交互。EIP(封装、继承、多态)是核心,理解后写的代码更工整、易维护。
MQL5 类名惯例是加 'C' 前缀,比如标准库里的 CExpert、CIndicator、CStrategy,一眼能和函数或变量区分开。这不是语法强制,编译器不报错,但读你代码的人会更快定位类型。
实际开 MT5 建类时,建议直接 class CMySignal,别省这个前缀。团队接手或半年后你自己回看,命名成本会低很多;外汇与贵金属杠杆高,策略代码若因命名乱而误改逻辑,回测和实盘都可能出偏差。
「把过程化价格行为EA重构成类」
把之前过程化的 PriceActionEMA 策略改写成 OOP 版本,核心动作是新建一个 CEmaExpertAdvisor 类,把原来散落的全局变量收编成成员变量,把函数收编成方法。类文件放在 Experts\OOP_Article\PriceActionEMA\EmaExpertAdvisor.mqh,EA 主体放在同目录的 OOP_PriceActionEMA.mq5。 原策略只靠一条 EMA 和烛条实体方向决策:阳线且整根烛高于 EMA 才做多,阴线且整根烛低于 EMA 才做空,新蜡烛出现时若条件仍在就续开仓;离场靠账户盈亏百分比阈值或传统止盈止损。H1 及以上周期优化出来的参数更稳,这是实盘前该自己用策略测试器跑一遍的前提。 重构时从标准库引入 CTrade,实例化出 myTrade 对象,开平仓代码量能砍掉一大截。原来过程化的用户输入全局变量全部转为 private 成员,通过构造函数传参初始化;其余状态变量放 public。ManageProfitAndLoss() 和 BuySellPosition(...) 两个方法必须改写去调 myTrade,其余方法逻辑直接从过程化代码搬。 EA 主体里先 #include 类头文件,声明用户输入全局变量,再用构造函数生成 ea 实例。OnInit 调 ea.GetInit,OnDeinit 调 ea.GetDeinit,OnTick 里依次调 GetEma / GetPositionsData / TradingIsAllowed / TradeNow / ManageProfitAndLoss / PrintOnChart,模块边界比过程化清晰得多。 回测验证用 MT5 自带 Metaquotes 模拟账户,品种 EURJPY,2023.01–2024.03 共 1 年 2 个月,每次即时报价建模,本金 1 万美元、杠杆 1:100。优化参数下该 EA 年化倾向约 41%,净值回撤约 5%;外汇及贵金属杠杆交易风险高,这个数值只是特定区间回测结果,换品种或周期可能明显偏离。
class="macro">#include <Trade\Trade.mqh> class CEmaExpertAdvisor { class="kw">public: CTrade myTrade; class="kw">public: CEmaExpertAdvisor( class="type">long _magicNumber, ENUM_TIMEFRAMES _tradingTimeframe, class="type">int _emaPeriod, class="type">int _emaShift, class="type">bool _enableTrading, class="type">bool _enableAlerts, class="type">class="kw">double _accountPercentageProfitTarget, class="type">class="kw">double _accountPercentageLossTarget, class="type">int _maxPositions, class="type">int _tp, class="type">int _sl ); ~CEmaExpertAdvisor(); }; class="macro">#include <Trade\Trade.mqh> CTrade myTrade; class="kw">private: class="type">long magicNumber; ENUM_TIMEFRAMES tradingTimeframe; class="type">int emaPeriod; class="type">int emaShift; class="type">bool enableTrading; class="type">bool enableAlerts; class="type">class="kw">double accountPercentageProfitTarget;
把EA状态塞进一个类里
把原本散落在全局的变量和函数收拢成 CEmaExpertAdvisor 类,是这套均线策略工程化的关键一步。私有段先锁死风控与品种参数:accountPercentageLossTarget 管回撤上限,maxPositions 限同向持仓数,TP 与 SL 以点数计,这些字段直接决定后面开仓函数能不能放行。 公开段则暴露运行态:movingAverage[] 接 EMA 序列,emaHandle 是指标句柄,buyOk/sellOk 标记趋势过滤后的方向许可;startingCapital 与 accountPercentageProfit 用来算权益曲线收益比,totalOpenBuyPositions 等字段实时统计买卖持仓量与浮盈。注意 closedCandleTime 这个 datetime 变量,它专门抓新 K 线成型,避免一根柱内重复触发。 方法声明把原流程函数重写成成员:GetEma() 取均线、TradingIsAllowed() 做总闸、TradeNow() 执行、ManageProfitAndLoss() 管移动止损与清仓。你在 MT5 里新建 EA 时,照这个骨架把全局变量改成类成员,编译后能在「导航器—专家属性」里直接看到参数绑定,比裸写 procedure 少踩一半作用域坑。外汇与贵金属杠杆高,maxPositions 设太大可能让回撤在极端波动下超预期。
class="type">class="kw">double accountPercentageLossTarget; class="type">int maxPositions; class="type">int TP; class="type">int SL; class="kw">public: class=class="str">"cmt">//--- EA global variables class=class="str">"cmt">// Moving average variables class="type">class="kw">double movingAverage[]; class="type">int emaHandle; class="type">bool buyOk, sellOk; class="type">class="kw">string movingAverageTrend; class=class="str">"cmt">// Strings for the chart comments class="type">class="kw">string commentString, accountCurrency, tradingStatus, accountStatus; class=class="str">"cmt">// Capital management variables class="type">class="kw">double startingCapital, accountPercentageProfit; class=class="str">"cmt">// Orders and positions variables class="type">int totalOpenBuyPositions, totalOpenSellPositions; class="type">class="kw">double buyPositionsProfit, sellPositionsProfit, buyPositionsVol, sellPositionsVol; class="type">class="kw">datetime closedCandleTime;class=class="str">"cmt">//used to detect new candle formations class=class="str">"cmt">// Class method declarations(formerly procedural standalone functions) class="type">int GetInit(); class="type">void GetDeinit(); class="type">void GetEma(); class="type">void GetPositionsData(); class="type">bool TradingIsAllowed(); class="type">void TradeNow(); class="type">void ManageProfitAndLoss(); class="type">void PrintOnChart(); class="type">bool BuySellPosition(class="type">int positionType, class="type">class="kw">string positionComment); class="type">bool PositionFound(class="type">class="kw">string symbol, class="type">int positionType, class="type">class="kw">string positionComment); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| METHODS DEFINITIONS | class=class="str">"cmt">//+------------------------------------------------------------------+ CEmaExpertAdvisor::CEmaExpertAdvisor(class="type">long _magicNumber, ENUM_TIMEFRAMES _tradingTimeframe, class="type">int _emaPeriod, class="type">int _emaShift, class="type">bool _enableTrading, class="type">bool _enableAlerts, class="type">class="kw">double _accountPercentageProfitTarget, class="type">class="kw">double _accountPercentageLossTarget,
◍ 把类骨架接上平仓逻辑
上面那段构造函数把 EA 的运行参数一次性塞进类成员:magicNumber 用于区分本方订单,tradingTimeframe 指定盯盘周期,emaPeriod 与 emaShift 控制均线偏移,enableTrading 和 enableAlerts 管开关,accountPercentageProfitTarget / accountPercentageLossTarget 是账户百分比盈亏阈值,maxPositions 限制同向持仓数,TP、SL 以点数传入。 类里其余方法多是空壳:GetInit、GetDeinit、GetEma、GetPositionsData、TradingIsAllowed、TradeNow、PrintOnChart、BuySellPosition、PositionFound 都只留了注释占位,真正写了实质判断的是 ManageProfitAndLoss。 ManageProfitAndLoss 的退出条件很直接:当 accountPercentageProfit 大于等于盈利目标,或小于等于负的亏损目标(lossLevel = -accountPercentageLossTarget),又或者某方向持仓已达 maxPositions 且账户整体盈利为正,就触发清仓。清仓时先用 PositionsTotal() 拿总数,再从尾到头倒序用 PositionGetTicket(x) 逐仓取票号,避免删除过程中索引错位。 倒序遍历是 MT5 里删仓的老规矩——正向删会让后面仓位下标整体前移,漏单概率很高。你在 MT5 里把这段接进自己的 EA,把 accountPercentageLossTarget 设成 2、maxPositions 设成 3 跑一遍模拟盘,就能看到触发清仓的节奏。外汇与贵金属杠杆高,这类百分比清仓只能控敞口,不保证不回撤。
class="type">int _maxPositions, class="type">int _tp, class="type">int _sl) { magicNumber = _magicNumber; tradingTimeframe = _tradingTimeframe; emaPeriod = _emaPeriod; emaShift = _emaShift; enableTrading = _enableTrading; enableAlerts = _enableAlerts; accountPercentageProfitTarget = _accountPercentageProfitTarget; accountPercentageLossTarget = _accountPercentageLossTarget; maxPositions = _maxPositions; TP = _tp; SL = _sl; } class=class="str">"cmt">//+------------------------------------------------------------------+ CEmaExpertAdvisor::~CEmaExpertAdvisor() {} class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CEmaExpertAdvisor::GetInit() { class=class="str">"cmt">//method body.... } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEmaExpertAdvisor::GetDeinit() { class=class="str">"cmt">//method body.... } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEmaExpertAdvisor::GetEma() { class=class="str">"cmt">//method body.... } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEmaExpertAdvisor::GetPositionsData() { class=class="str">"cmt">//method body.... } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEmaExpertAdvisor::TradingIsAllowed() { class=class="str">"cmt">//method body.... } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEmaExpertAdvisor::TradeNow() { class=class="str">"cmt">//method body.... } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEmaExpertAdvisor::ManageProfitAndLoss() { class=class="str">"cmt">//method body.... } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEmaExpertAdvisor::PrintOnChart() { class=class="str">"cmt">//method body.... } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEmaExpertAdvisor::BuySellPosition(class="type">int positionType, class="type">class="kw">string positionComment) { class=class="str">"cmt">//method body.... } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEmaExpertAdvisor::PositionFound(class="type">class="kw">string symbol, class="type">int positionType, class="type">class="kw">string positionComment) { class=class="str">"cmt">//method body.... } class="type">void CEmaExpertAdvisor::ManageProfitAndLoss() { class=class="str">"cmt">//if the account percentage profit or loss target is hit, class="kw">delete all positions class="type">class="kw">double lossLevel = -accountPercentageLossTarget; if( (accountPercentageProfit >= accountPercentageProfitTarget || accountPercentageProfit <= lossLevel) || ((totalOpenBuyPositions >= maxPositions || totalOpenSellPositions >= maxPositions) && accountPercentageProfit > class="num">0) ) { class=class="str">"cmt">//class="kw">delete all open positions if(PositionsTotal() > class="num">0) { class=class="str">"cmt">//variables for storing position properties values class="type">class="kw">ulong positionTicket; class="type">long positionMagic, positionType; class="type">class="kw">string positionSymbol; class="type">int totalPositions = PositionsTotal(); class=class="str">"cmt">//scan all the open positions for(class="type">int x = totalPositions - class="num">1; x >= class="num">0; x--) { positionTicket = PositionGetTicket(x);class=class="str">"cmt">//gain access to other position properties by selecting the ticket