利用 MQL5 面向对象编程法编写
📘

利用 MQL5 面向对象编程法编写"EA 交易"(基础篇)

第 1/3 篇

◍ 用 MQL5 类封装写一个 EA 骨架

在 MT5 里写 EA,最省事的做法是把交易逻辑塞进一个类里,而不是在 OnTick 里堆过程式代码。这样后续加信号过滤、改手数规则,只动类内部方法,主流程不会被改崩。 下面这段示例来自社区公开代码(2013-11-08 发布,浏览 8029、评论 31),用 CTrade 派生思路把下单动作收进对象。注意它没处理点差与库存费,实盘前必须自己补。 外汇与贵金属杠杆高、滑点随机,任何封装好的下单类都不保证胜率,只解决结构清晰问题。

「从流程到对象:本文要落地的东西」

上一份材料里已经把 EA 的建、调、测走了一遍,步骤不复杂,但 MQL5 真正能打的并不只是那点脚本式写法。 本文把重心放到面向对象法上,把之前提到的概念转成可复用的代码结构。很多人一听类、继承就头大,但跑完这套你就能自己写一版面向对象 EA。 如果你没碰过前一篇的创建与回测流程,先去把那个过一遍,不然下面直接上类会缺上下文。

把EA逻辑塞进一个类里

MQL5 相比 MQL4 最实在的升级之一,就是原生支持面向对象。核心好处不是花哨,而是把实现细节封在类内部:你改了类的私有逻辑,外面调用它的 EA 代码一行都不用动。 类本质就是带函数的数据结构。它把变量(属性)和函数(方法)打包成成员。比如你想描述一辆车,门、座、轮胎、重量是属性,启动、换挡、熄火、按喇叭是方法,全塞进一个 CAR 类。声明时用 class 加类名加大括号,成员前面标 private / protected / public 决定谁能碰。 没写访问关键词的成员默认全是 private。下面这个类 CAR 有 8 个成员,4 个私有整数或双精度数据,4 个公共函数(两个返回 bool、两个返回 void): class CAR { private: int doors, seats, tires; double weight; public: bool start(); bool stop(); void changegear(); void horn(); }; 创建对象就是 CAR Honda; 或 CAR Toyota;,想建多少建多少。Honda 和 Toyota 都能调公共部分的 start()、horn() 这些,私有数据外部够不着。 类里有两个特殊成员:构造函数 class_name() 在新建对象时自动跑,做内存分配和初始化,MQL5 里无参无返回、只能有一个;析构函数 ~class_name() 在对象销毁时自动跑,做反初始化。 继承能省重复代码。从 CAR 衍生 SALOON 类: class SALOON : public CAR { public: void runathighspeed(); bool loadluggage(); }; SALOON 自己加 2 个成员,还继承 CAR 的 7 个受保护/公共成员,能调 start() 也能跑自己的 runathighspeed()。衍生类对象创建时基类构造先跑,销毁时衍生类析构先跑。MQL5 不支持多重继承,别琢磨了。 虚函数让衍生类改写行为。把 CAR 的 changegear() 标成 virtual,SALOON 和 WAGON 就能各自实现自己的换挡逻辑——这就是多态,基类定义接口、衍生类给不同实现。 定义成员函数用双冒号 :: 作用域运算符。horn() 可以在类里直接写,其它放外面: bool CAR::start() { return true; } 函数本质就是拆任务:返回类型、名称、参数、语句块。比如 int doaddition(int x,int y) { return x+y; } 丢 2 和 3 进去吐 5。 开 MT5 新建一个 mq5,照上面 CAR 类敲一遍,再写 CAR Honda; Honda.start(); 编译跑通,你就摸到了用类包装 EA 逻辑的门槛。外汇和贵金属杠杆高,回测通过不代表实盘稳,封装只是工程手段。

MQL5 / C++
class CAR {
class="kw">private:
   class="type">int doors, seats, tires;
   class="type">class="kw">double weight;
class="kw">public:
   class="type">bool start();
   class="type">bool stop();
   class="type">void changegear();
   class="type">void horn();
};

class SALOON : class="kw">public CAR {
class="kw">public:
   class="type">void runathighspeed();
   class="type">bool loadluggage();
};

◍ 用类把交易对象封装起来

在 MQL5 里写 EA 或指标,遇到需要反复调用的复杂状态(比如一辆车有门、座、胎、重量,还有启动换挡等动作),用 class 比散着写全局变量更不容易乱。类把数据和方法绑在一起,外部只碰你公开的部分。 访问权限是类的骨架。private 下的成员(如 doors、tyres)只能类内部改;public 下的 start()、stop() 能被外部直接调;protected 则留给派生类接着用。下面这段 CAR 的定义里,tyres 被收进 private,外部想直接读它就会编译报错。 [CODE] class CAR { protected: int doors; int sits; double weight; public: bool start(); void changegear(); void stop(); bool horn(); private: int tyres; }; [/CODE] 继承能省重复代码。SALOON 用 public 继承 CAR 后,自动带着 doors、start() 这些成员,自己只补 maxspeed 和 runathighspeed()。实盘里你可以让 BaseSignal 被 TrendSignal、BreakoutSignal 分别继承,共用同一套缓冲管理。 构造函数和析构函数控制生命周期。class_name() 在对象创建时跑,~class_name() 在销毁时跑,适合在里面开/关指标句柄、释放数组。漏写析构去释放 iMA 句柄,回测跑 10 万根 K 线可能悄悄漏掉上百个句柄。 virtual 方法给多态留口子。把 changegear() 标成 virtual,派生类就能重写自己的换挡逻辑,父类指针调用时实际跑子类版本——写多品种信号路由时会用上。

MQL5 / C++
class CAR
{
  class="kw">protected:
    class="type">int     doors;
    class="type">int     sits;
    class="type">class="kw">double  weight;
  class="kw">public:
    class="type">bool    start();
    class="type">void    changegear();
    class="type">void    stop();
    class="type">bool    horn();
  class="kw">private:
    class="type">int     tyres;
};

「从汽车类看 MQL5 的封装与虚函数重载」

这段示例用 CAR 基类派生 SALOON 和 WAGON,演示了 MQL5 里 class 的访问控制与 virtual 函数机制。基类 CAR 把 tyres 放在 private,doors/sits/weight 放在 protected,派生类能碰后者却碰不到前者,编译层面就卡死了越界访问。 SALOON 和 WAGON 都重写了 changegear(),但挡位映射完全不同:SALOON 是 reverse/low/high,WAGON 是 low/high/reverse。这种多态在 EA 里意味着同一接口可以根据品种类型切换下单或平仓逻辑,而不用写一堆 if-else。 注意原文类声明顺序有坑:SALOON 继承 CAR 时 CAR 还没定义完,实际在 MT5 编译器会报 'CAR' is not defined。真要跑,得把 CAR 的完整定义放到派生类前面。 函数部分也顺带给了语法骨架:Return_type function_name(parameters) 包住表达式,doaddition(2,3) 直接返回 5,说明基础传参和返回值的写法。开 MT5 新建脚本把下面代码贴进去,能直观看到类结构和调用方式。

MQL5 / C++
class CAR
{
  class="kw">protected:
    class="type">int      doors;
    class="type">int      sits;
    class="type">class="kw">double   weight;
  class="kw">public:
    class="type">void     CAR() class=class="str">"cmt">// 构造函数
    class="type">bool     start();
    class="type">void     changegear();
    class="type">void     stop();
    class="type">bool     horn(){press horn;}
  class="kw">private:
    class="type">int      tyres;
};
class SALOON : class="kw">public CAR
{
  class="kw">private:
    class="type">int      maxspeed;
  class="kw">public:
    class="type">void     runathighspeed();
    class="kw">virtual  class="type">void    changegear(){gear1=reverse; gear2=low; gear3=high;}
};
class WAGON : class="kw">public CAR
{
  class="kw">private:
    class="type">bool     hasliftback;
  class="kw">public:
    class="kw">virtual  class="type">void    changegear(){gear1=low; gear2=high; gear3=reverse;}
};
class="type">void CAR::CAR()
{
class=class="str">"cmt">// 在此初始化成员变量
}
class="type">bool CAR::start()
{
class=class="str">"cmt">// 此处是汽车启动程序
}
class="type">void CAR::changegear()
{
class=class="str">"cmt">// 此处为汽车换挡程序
}
class="type">void CAR::stop()
{
class=class="str">"cmt">// 此处为汽车停止程序
}
class="type">int doaddition(class="type">int x, class="type">int y)
{
class="kw">return (x+y);
}
class="type">int doaddition(class="num">2,class="num">3) class=class="str">"cmt">// 返回 class="num">5

把均线+ADX策略封进一个可复用类

先把策略说死再写代码,否则类成员会变成拍脑袋的垃圾。这里用的规则很具体:MA-8 向上且收盘价站上 MA-8 做多,MA-8 向下且收盘价跌破 MA-8 做空;同时 ADX(8) 必须 >22 才允许交易,ADX 递减或 <22 一律不动。止损 30 点,利润目标 100 点,只在新柱形成时扫描信号。外汇与贵金属杠杆高,实盘前务必在策略测试器用历史数据验证这套触发逻辑。 类里要管的不仅仅是买卖。指标句柄在 OnInit 里建、OnDeinit 里释放,缓冲区在 OnTick 里抓;还要在开仓前用 AccountInfoDouble 读可用预付款、用 SymbolInfoDouble 读合约大小,算出手数对应的保证金是否超过设定百分比。下面这段是类构造函数,用 ZeroMemory 把请求和结果结构清零,避免上一次残值污染新单。 [CODE] MyExpert::MyExpert() { ZeroMemory(m_trade_request); ZeroMemory(m_trade_result); ArrayInitialize(m_ma_buffer,0); ArrayInitialize(m_adx_buffer,0); } [/CODE] 逐行看:第一行是构造函数定义,双冒号说明它在 MyExpert 类作用域内;ZeroMemory(m_trade_request) 把 MqlTradeRequest 结构复位;下一行同理清 MqlTradeResult;两个 ArrayInitialize 把 MA 和 ADX 缓冲区数组填 0。不这么做,CopyBuffer 没覆盖到的元素会带着旧值进判断。 MarginOK 的算法分两种市况:基础货币是 USD 时,要求预付款 = 合约大小/杠杆;非 USD 如 EUR/USD,则再乘当前价。函数比对这个值和你允许用的预付款百分比,超了就返回 FALSE 不下单。把这个检查封装进类,EA 主体就不会到处写账户逻辑。 类写完后别急着单编译 .mqh,MetaEditor 会报 no executable file produced,这是正常的。直接去写调用它的 EA,把 my_oop_ea 挂上图表调试,包含文件会跟着一起被查错,这比单独点调试按钮靠谱。

MQL5 / C++
MyExpert::MyExpert()
  {
   ZeroMemory(m_trade_request);
   ZeroMemory(m_trade_result);
   ArrayInitialize(m_ma_buffer,class="num">0);
   ArrayInitialize(m_adx_buffer,class="num">0);
  }

常见问题

先把开仓、平仓、过滤条件列成普通函数,再把这些状态和动作收进一个类里,先跑通空类框架再填逻辑。
不用,把品种、周期、参数都做成类成员变量,换品种只改实例化时的传参,核心方法不用动。
可以,把策略描述或代码片段发给小布,它能指出类结构遗漏、虚函数误用和参数硬编码风险。
解决不同策略子类复用同一套调度框架的问题,比如趋势类和震荡类重写同一个OnTick入口但内部逻辑不同。
好处是报错处理、手数计算和品种校验集中在一处,改规则只动一个文件,回测也更不容易漏边界。