理解编程范式(第 2 部分):面向对象方式开发价格行为智能系统·进阶篇
📱

理解编程范式(第 2 部分):面向对象方式开发价格行为智能系统·进阶篇

(2/3)· 从手机蓝图讲起,用类与对象把上一篇的EMA烛条系统重写成OOP范式

新手友好 第 2/3 篇
很多交易者抄来的EA全是平铺的函数和全局变量,改个入场逻辑就得通篇找引用。把过程代码硬套进复杂策略,后期维护和加功能基本等于重写。先理清类与对象的边界,能少踩一大半坑。

「用继承把EA里的对象层级搭起来」

MQL5 里写复杂 EA,最忌把所有属性平铺在一个结构体里。用 class 做基类,再派生子类的写法,能让行情外的辅助逻辑(比如参数分组、状态机)更清晰,也方便后续在 OnTick 里只调接口不碰细节。 下面这段先在 OnInit 里用 PhoneClass 造两个实例:型号 101 黑键盘非触屏 4 寸,型号 102 天蓝触屏 AMOLED 6 寸,各自调 PrintPhoneSpecs() 把规格打到日志。

MQL5 / C++
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);
  }
继承的关键在子类构造函数冒号后的初始化列表。SmartPhoneClass 继承 PhoneClass,构造时先把父类的五个参数传上去,再给自己的 operatingSystem、numberOfCameras 赋值。
MQL5 / C++
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;
   }
};
子类重写 PrintPhoneSpecs 时用了 override 关键字,先打分隔线和自己的系统、摄像头数,再显式调 PhoneClass::PrintPhoneSpecs() 把基类规格补上。日志里会看到型号 104 白触屏 AMOLED 6 寸 Android 三摄,紧跟着基类那五行参数——层级关系一眼可查。 开 MT5 建个空 EA,把上面两段 include 和类定义按顺序塞进去,编译后看 Experts 标签。若日志出现两道分隔线包裹的智能手机规格块,说明继承链通了,可以往 OnTick 里挂真实报价逻辑。外汇与贵金属杠杆高,这类封装只解决代码组织,不降低账户风险。

MQL5 / C++
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 存在滑点、断线等高风险,验证代码逻辑时务必先在策略测试器跑历史数据。

MQL5 / C++
  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%;外汇及贵金属杠杆交易风险高,这个数值只是特定区间回测结果,换品种或周期可能明显偏离。

MQL5 / C++
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 设太大可能让回撤在极端波动下超预期。

MQL5 / C++
  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 跑一遍模拟盘,就能看到触发清仓的节奏。外汇与贵金属杠杆高,这类百分比清仓只能控敞口,不保证不回撤。

MQL5 / C++
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
把范式对照交给小布
小布盯盘内置的AIGC会把你挂载的EA按过程式与对象式结构做差异标注,打开对应品种页就能直接看哪段代码耦合过重。把重复劳动交给小布,你专注决策。

常见问题

类是描述对象属性和方法的蓝图,方法是在类中定义、用于执行具体任务的函数,相当于蓝图里写明的功能清单。
可以,小布盯盘的AIGC会对EA做结构扫描并标记范式类型,方便你快速定位需要重构的平铺代码段。
转换过程能直观暴露两种范式差异,对象模型让EMA与烛条逻辑封装更清晰,后续扩展信号组合更省力。
不一定,对象建模的是概念或真实事物,价格行为系统里一个订单管理器对象就不对应物理实体,只抽象了状态与行为。
封装最常用,把指标句柄和交易动作收进类里;继承多用于多品种管理器;多态在简单价格行为EA中较少直接用到。