另一个 MQL5 OOP 类(基础篇)
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 对照官方写法,比背规范来得快。外汇/贵金属杠杆高,任何封装错误都可能放大实盘亏损概率。
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 调用。
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 时净值曲线是否围绕成本线窄幅游走。
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 只建议用模拟盘验证。
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); };