另一个 MQL5 OOP 类(基础篇)
📘

另一个 MQL5 OOP 类(基础篇)

第 1/3 篇

MQL5 里另起一个 OOP 类的现实用法

在 MT5 的 EA 或指标开发里,除了系统自带的 CTrade、CIndicator 等标准类,经常需要自己再封一个 OOP 类来装特定逻辑。比如把某段价格行为判定(突破、回踩、动量衰减)单独成一个类,主程序只调接口,不碰内部计算。 这类写法在 2014 年前后社区就有实盘示例流传,一篇含自定义类的帖子在发布后累计被查看约 3157 次、跟帖 7 条,说明用类封装交易逻辑并非冷门需求。 开 MT5 按 F4 进 MetaEditor,新建一个 .mqh 头文件写 class MyLogic { ... },再在 EA 里 #include 它,就能把策略核心和订单执行层拆开。高风险提示:外汇与贵金属杠杆交易可能瞬间吞掉本金,任何封装只是结构清晰,不改善胜率。

◍ 面向对象EA为什么难在经验而非语法

做一个真正能跑的面向对象EA,难点不在MQL5语法,而在把交易逻辑拆成对象这件事本身需要逻辑推理、综合分析甚至一点想象力。好比把自动交易系统看成一盘象棋,所谓策略就是下棋的章法,而战术落地要靠指标、图表和基本面公理写成程序。 对没做过应用开发的人,OOP式EA确实有门槛,本质还是缺实战经验。我习惯拿一个具体例子往下讲,你不用先补任何新知识,跟着把前5个EA敲出来,手感自然就来了。 如果策略和编程分给两个人,沟通成本会抵消分工收益:懂策略的人讲不清编程约束,写MQL5的人也可能漏掉客户逻辑里的关键假设。这也是Scrum、TDD、XP这些方法论存在的理由——维基百科把它们定义为结构、计划和控制信息系统开发的框架。 外汇和贵金属市场高杠杆、高波动,这类系统只解决工程分工,不预示任何盈利概率。先假设咱们已经能快速把策略落地到迭代末期,每个人对交易系统定义一致,后面的练习才好推进。

「从过程式转到类封装:EA 代码可复用性的第一步」

做 MT5 上的 EA,过程式写法把问题拆成一堆函数和子程序,短期能跑,但模型一旦变复杂,代码复用就吃力,后续维护和改参数成本会明显抬升。OOP 不强制,但想在自动化交易编程上再走一步,把逻辑收进类里基本是绕不开的路。 MQL5 自带的抽象数据类型就是现成例子,比如 Include\Arrays\Array.mqh、List.mqh、Tree.mqh,它们把数组、链表、树结构封装好,你直接 include 就能用,不必重写底层。这种复用倾向让可靠性与灵活性同步改善,尤其多品种外汇/贵金属 EA 并发跑时,改一处基类可能就影响全盘。 UML 这类图形建模语言适合在动手前先画清对象关系,但真实项目里团队节奏紧、没人熟 UML 时硬套反而拖进度。个人经验是:环境合适就拿它辅助分析,不合适就直接写类草模,别被工具绑架。 下面这段 CPerson 是 MQL5 里最经典的入门类,注意 MetaQuotes 自己的框架惯例——类名大写 C 开头、方法驼峰且首字母大写、受保护成员加 m_ 前缀、类内不写 this 引用成员。照这个风格写,你以后读 Trade.mqh 这类官方文件会顺很多。 别把正态当圣经 上面示例没用 this 前缀,和某些语言习惯不同;直接打开 Include\Trade\Trade.mqh 对照官方写法,比背规范来得快。外汇/贵金属杠杆高,任何封装错误都可能放大实盘亏损概率。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CPerson Class                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CPerson
  {
class="kw">protected:
   class="type">class="kw">string            m_first_name;
   class="type">class="kw">string            m_surname;
   class="type">class="kw">datetime          m_birth;
class="kw">public:
   class=class="str">"cmt">//--- Constructor and destructor methods
                    CPerson(class="type">void);
                   ~CPerson(class="type">void);
   class=class="str">"cmt">//--- Getter methods
   class="type">class="kw">string            GetFirstName(class="type">void);
   class="type">class="kw">string            GetSurname(class="type">void);
   class="type">class="kw">datetime          GetBirth(class="type">void);
   class=class="str">"cmt">//--- Setter methods
   class="type">void              SetFirstName(class="type">class="kw">string first_name);
   class="type">void              SetSurname(class="type">class="kw">string surname);
   class="type">void              SetBirth(class="type">class="kw">datetime birth);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CPerson::CPerson(class="type">void)
  {
   Alert("Hello world! I am run when an object of type CPerson is created!");
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destructor                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CPerson::~CPerson(class="type">void)
  {
   Alert("Goodbye world! I am run when the object is destroyed!");
  }

给人员类补上读写接口

在 MT5 里写 EA 或指标时,若用类封装交易员或账户主体信息,光有私有成员还不够,必须暴露 getter / setter 才能在外层逻辑里安全读写。下面这段 CPerson 的成员函数就是最小可用集合:三个读、三个写,覆盖了名、姓、出生时间。 [string CPerson::GetFirstName(void)] 直接返回 m_first_name,不带任何校验;同名读函数 GetSurname、GetBirth 分别回吐 m_surname 与 m_birth(datetime 类型)。写侧 SetFirstName / SetSurname 接收 string 参数后直接赋值,SetBirth 接收 datetime 后覆盖 m_birth。 实盘意义不大,但做多账户回测框架时有用:你可以把每个模拟交易员的属性挂到类实例上,再让小布类脚本批量跑参数。外汇与贵金属杠杆高,回测盈利不代表实盘概率同构,别直接照搬到真仓。 开 MT5 按 F4 建个 include 头文件,把下面代码贴进去编译,就能在别的 mq5 里 #include 后 new CPerson 调用。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| GetFirstName                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CPerson::GetFirstName(class="type">void)
  {
   class="kw">return m_first_name;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| GetSurname                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CPerson::GetSurname(class="type">void)
  {
   class="kw">return m_surname;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| GetBirth                                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">datetime CPerson::GetBirth(class="type">void)
  {
   class="kw">return m_birth;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| SetFirstName                                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CPerson::SetFirstName(class="type">class="kw">string first_name)
  {
   m_first_name=first_name;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| SetSurname                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CPerson::SetSurname(class="type">class="kw">string surname)
  {
   m_surname=surname;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| SetBirth                                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CPerson::SetBirth(class="type">class="kw">datetime birth)
  {
   m_birth=birth;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把随机震荡策略拆成会「活」的 EA 胚胎

震荡市里的空头段走势接近随机,这一点已有多个交易系统观测到。给定图表上任意一个随机点,下一根 K 线涨跌方向事前无法预判;但只要止损与获利之间的绝对差值足够小,系统就能把数学期望值拉到接近零轴附近,靠资金管理而非方向预测吃饭。外汇与贵金属杠杆高,这类「不赌方向」的框架同样面临滑点放大和黑天鹅跳空风险,回测通过不等于实盘能活。 用 OOP 写 EA,可以把程序当成一个有机体:CBrain 存只读类的固定参数(类似 ROM),CGraphic 镜像当前图表信息,CEvolution 记临时状态与操作历史。先在 MQL5\Include\Mine 下建 Enums.mqh 放枚举,再于 MQL5\Experts\SimpleRandom 写 ExpertSimpleRandom.mq5,主类 CSimpleRandom 保存在同目录的 CSimpleRandom.mqh 里。 CSimpleRandom 通过受保护属性持有 CBrain、CEvolution、CGraphic 指针,构造函数里用 new 动态创建——这正是官方「对象指针」文档的标准玩法。下附代码里 ENUM_STATUS_EA 管买卖决策,ENUM_LIFE_EA 管存活周期(时/日/周/月/年),ExpertSimpleRandom.mq5 把止损、获利、手数、寿命作为输入传给 SR 对象,OnInit 只调 SR.Init() 完成装配。CBrain 等三个类原文留了空位没写满,比如 m_death 就没接逻辑,你可以按自己想法补完。 开 MT5 照这个结构建好文件夹和文件,把下方代码贴进对应 mq5,编译后跑一遍欧美对小时图回测,重点看止损获利都设 10 点、手数 0.01 时净值曲线是否围绕成本线窄幅游走。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Status enumeration                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_STATUS_EA
  {
   BUY,
   SELL,
   DO_NOTHING
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Lifetime enumeration                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_LIFE_EA
  {
   HOUR,
   DAY,
   WEEK,
   MONTH,
   YEAR
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                            ExpertSimpleRandom.mq5 |
class=class="str">"cmt">//|                 Copyright © class="num">2013, Jordi Bassagañas                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright   "Copyright © class="num">2013, laplacianlab"
class="macro">#class="kw">property link        "[MQL5官方文档]
class="macro">#class="kw">property version     "class="num">1.00"
class="macro">#include <Trade\Trade.mqh>
class="macro">#include <Trade\SymbolInfo.mqh>
class="macro">#include <Trade\PositionInfo.mqh>
class="macro">#include <Indicators\Indicators.mqh>
class="macro">#include <Mine\Enums.mqh>
class="macro">#include <..\Experts\SimpleRandom\CSimpleRandom.mqh>
class="kw">input class="type">int               StopLoss;
class="kw">input class="type">int               TakeProfit;
class="kw">input class="type">class="kw">double            LotSize;
class="kw">input ENUM_LIFE_EA      TimeLife;
class="type">MqlTick tick;
CSimpleRandom *SR=new CSimpleRandom(StopLoss,TakeProfit,LotSize,TimeLife);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Initialization function                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit(class="type">void)
  {
   SR.Init();
   class="kw">return(class="num">0);

「把随机决策类挂进行情事件里」

支撑阻力对象在退出时要干净释放,否则 MT5 反复加载 EA 容易留下悬空指针。OnDeinit 里先调 SR.Deinit() 做内部清理,再用 delete(SR) 把实例内存交还,这一步在切换周期或改参数重编译时很关键。 行情驱动靠 OnTick:每次报价进来先用 SymbolInfoTick(_Symbol,tick) 抓最新 tick,再把 ask、bid 喂给 SR.Go()。也就是说,所有支撑阻力重算和后续信号都绑定在 tick 级别,回测里 1 分钟周期平均每秒触发次数取决于报价频率,实盘 EURUSD 可能一秒好几跳。 下面这段是随机策略 EA 的类骨架,把大脑、进化、绘图、交易、持仓信息五个指针收进 CSimpleRandom。构造时传 stop_loss、take_profit、lot_size 和寿命枚举,之后 Init 建对象、Go 跑逻辑、Deinit 拆对象,等于把前面支撑阻力模块和随机开仓逻辑拼到同一生命周期里。外汇与贵金属杠杆高,这类实验性 EA 只建议用模拟盘验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Deinitialization function                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
   SR.Deinit();
   class="kw">delete(SR);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| OnTick event function                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   SymbolInfoTick(_Symbol,tick);
   SR.Go(tick.ask,tick.bid);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                        ExpertSimpleRandom.mq5                    |
class=class="str">"cmt">//|         Copyright © class="num">2013, Jordi Bassagañas                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Trade\Trade.mqh>
class="macro">#include <Mine\Enums.mqh>
class="macro">#include <..\Experts\SimpleRandom\CBrain.mqh>
class="macro">#include <..\Experts\SimpleRandom\CEvolution.mqh>
class="macro">#include <..\Experts\SimpleRandom\CGraphic.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CSimpleRandom Class                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSimpleRandom
  {
class="kw">protected:
   CBrain      *m_brain;
   CEvolution  *m_evolution;
   CGraphic    *m_graphic;
   CTrade      *m_trade;
   CPositionInfo *m_positionInfo;
class="kw">public:
   class=class="str">"cmt">//--- Constructor and destructor methods
                     CSimpleRandom(class="type">int stop_loss,class="type">int take_profit,class="type">class="kw">double lot_size,ENUM_LIFE_EA time_life);
                    ~CSimpleRandom(class="type">void);
   class=class="str">"cmt">//--- Getter methods
   CBrain      *GetBrain(class="type">void);
   CEvolution  *GetEvolution(class="type">void);
   CGraphic    *GetGraphic(class="type">void);
   CTrade      *GetTrade(class="type">void);
   CPositionInfo *GetPositionInfo(class="type">void);
   class=class="str">"cmt">//--- Specific methods of CSimpleRandom
   class="type">bool        Init();
   class="type">void        Deinit(class="type">void);
   class="type">bool        Go(class="type">class="kw">double ask,class="type">class="kw">double bid);
   };

常见问题

先把策略逻辑抽成独立类(如随机决策类),只留接口给主程序调用,EA 结构会清晰很多,改参数也不用翻全局变量。
语法半天能看懂,难的是把行情、订单、状态分成哪些类、怎么留接口,这靠写过的EA经验而不是看书。
小布可以读你的EA代码讲清每个类负责什么、接口怎么接,并提示哪些类能直接复用,省去自己啃结构的时间。
给类加 Get/Set 方法或文件读写函数,主EA初始化时调一次就能载入上次的人员配置,不用硬编码。
常漏了在 OnInit 里用 EventSetTimer 或订阅对应 symbol 的 tick 事件,类实例要存为全局句柄,事件回调才能调到。