跨平台智能交易系统: CExpertAdvisor 和 CExpertAdvisors 类(基础篇)
📘

跨平台智能交易系统: CExpertAdvisor 和 CExpertAdvisors 类(基础篇)

第 1/3 篇

「正文」

跨平台智能交易系统: CExpertAdvisor 和 CExpertAdvisors 类 MetaTrader 5 — 示例 | 6 十一月 2017, 08:24

  • 867

24 Enrico Lambino 概述 智能交易系统类 初始化 新柱线检测 OnTick 处理函数 智能交易系统容器 数据持久化 示例 后记 结束语

把EA组件塞进类里而非头文件

早前系列文章里,EA 的例子是靠自定义函数把各组件拆到主体头文件中,文件一长就难维护。本文给出的 CExpertAdvisor 与 CExpertsAdvisors 类,意图让跨平台 EA 的各部件之间有更顺的交互逻辑。 这类封装直接碰掉了实盘里两个老问题:易失性数据的加载与保存、以及新柱线到来的判定。把状态收进类内部,比在头文件里散落全局变量更不容易在重启或切换品种时出错。 外汇与贵金属杠杆高、滑点跳空频繁,用类管理 volatile 数据能降低隐性 bug 概率,但是否真稳要看你在 MT5 里跑出来的日志。

◍ EA 基类把差异都藏进组件里了

CExpertAdvisorBase 是 EA 逻辑的基类,继承自 CObject。MQL4 与 MQL5 在订单、账户、品种上的大部分差异,并不在这个类里直接处理,而是交给前面章节提到的信号、账户、订单等组件对象去消化。 这个类里声明的方法,大部分只是其组件方法的包装器(wrapper),真正的执行逻辑在子对象中。关键方法如 Init、InitAccount、InitCandleManager 会在后续章节展开,目前只需知道它搭好了骨架。 从成员变量能看出 EA 运行时关心什么:m_distance 与多空的距离因子控制挂单偏移;m_every_tick 和 m_one_trade_per_candle 决定交易触发频率;m_position_reverse 控制是否反手。开 MT5 新建类继承它,先打印 m_symbol_name 和 m_period 就能验证初始化是否拿到图表上下文。 外汇与贵金属杠杆高,EA 任何参数误设都可能放大回撤,上线前务必在策略测试器用历史数据跑一遍。

MQL5 / C++
class CExpertAdvisorBase : class="kw">public CObject
  {
class="kw">protected:
   class=class="str">"cmt">//--- 交易参数
   class="type">bool               m_active;
   class="type">class="kw">string             m_name;
   class="type">int                m_distance;
   class="type">class="kw">double             m_distance_factor_long;
   class="type">class="kw">double             m_distance_factor_short;
   class="type">bool               m_on_tick_process;
   class=class="str">"cmt">//--- 信号参数
   class="type">bool               m_every_tick;
   class="type">bool               m_one_trade_per_candle;
   class="type">class="kw">datetime           m_last_trade_time;
   class="type">class="kw">string             m_symbol_name;
   class="type">int                m_period;
   class="type">bool               m_position_reverse;
   class=class="str">"cmt">//--- 信号对象
   CSignals          *m_signals;
   class=class="str">"cmt">//--- 交易对象   
   CAccountInfo      m_account;
   CSymbolManager    m_symbol_man;
   COrderManager     m_order_man;
   class=class="str">"cmt">//--- 交易时间对象
   CTimes           *m_times;
   class=class="str">"cmt">//--- 蜡烛
   CCandleManager    m_candle_man;
   class=class="str">"cmt">//--- 事件
   CEventAggregator *m_event_man;
   class=class="str">"cmt">//--- 容器
   CObject          *m_container;
class="kw">public:
                     CExpertAdvisorBase(class="type">void);
                    ~CExpertAdvisorBase(class="type">void);
   class="kw">virtual class="type">int       Type(class="type">void) class="kw">const {class="kw">return CLASS_TYPE_EXPERT;}
   class=class="str">"cmt">//--- 初始化
   class="type">bool              AddEventAggregator(CEventAggregator*);
   class="type">bool              AddMoneys(CMoneys*);
   class="type">bool              AddSignal(CSignals*);
   class="type">bool              AddStops(CStops*);
   class="type">bool              AddSymbol(class="kw">const class="type">class="kw">string);
   class="type">bool              AddTimes(CTimes*);
   class="kw">virtual class="type">bool      Init(class="kw">const class="type">class="kw">string,class="kw">const class="type">int,class="kw">const class="type">int,class="kw">const class="type">bool,class="kw">const class="type">bool,class="kw">const class="type">bool);
   class="kw">virtual class="type">bool      InitAccount(class="type">void);
   class="kw">virtual class="type">bool      InitCandleManager(class="type">void);
   class="kw">virtual class="type">bool      InitEventAggregator(class="type">void);
   class="kw">virtual class="type">bool      InitComponents(class="type">void);

「策略基类的接口骨架长这样」

在 MT5 自建 EA 框架时,一个策略基类通常会把信号、时间、订单管理拆成可重载的虚函数。下面这段声明直接暴露了三个初始化入口:InitSignals、InitTimes、InitOrderManager,都是 virtual bool,意味着子类必须各自实现,否则编译期就卡住。 除了初始化,基类还管「容器」和「激活态」。SetContainer 接收 CObject* 把策略挂到上层对象树,Active() 有 const 取值和带参赋值两种重载,用来在运行时掐断或恢复某个策略实例。 距离与品种参数是风控核心。Distance 存整数点距,DistanceFactorLong / Short 是 double 倍率,分别管多空止损距离的弹性缩放;SymbolName 锁定交易品种。外汇和贵金属波动大,这类因子调错可能让单笔风险失控,属高风险操作。 对象指针群给出了运行时抓手:AccountInfo 看账户、MainStop 取主止损、Orders / OrdersHistory 分活单与历史单、Signals / Times 回指子系统。最后 EnableTrade 与 EnableLong 用 bool 开关控制全局交易与仅多方向,实盘前建议在策略测试器里逐一切换验证。

MQL5 / C++
  class="kw">virtual class="type">bool        InitSignals(class="type">void);
  class="kw">virtual class="type">bool        InitTimes(class="type">void);
  class="kw">virtual class="type">bool        InitOrderManager(class="type">void);
  class="kw">virtual class="type">bool        Validate(class="type">void) class="kw">const;
  class=class="str">"cmt">//--- 容器
  class="type">void                SetContainer(CObject*);
  CObject            *GetContainer(class="type">void);
  class=class="str">"cmt">//--- 激活与失活
  class="type">bool                Active(class="type">void) class="kw">const;
  class="type">void                Active(class="kw">const class="type">bool);
  class=class="str">"cmt">//--- 赋值与取值     
  class="type">class="kw">string              Name(class="type">void) class="kw">const;
  class="type">void                Name(class="kw">const class="type">class="kw">string);
  class="type">int                 Distance(class="type">void) class="kw">const;
  class="type">void                Distance(class="kw">const class="type">int);
  class="type">class="kw">double              DistanceFactorLong(class="type">void) class="kw">const;
  class="type">void                DistanceFactorLong(class="kw">const class="type">class="kw">double);
  class="type">class="kw">double              DistanceFactorShort(class="type">void) class="kw">const;
  class="type">void                DistanceFactorShort(class="kw">const class="type">class="kw">double);
  class="type">class="kw">string              SymbolName(class="type">void) class="kw">const;
  class="type">void                SymbolName(class="kw">const class="type">class="kw">string);
  class=class="str">"cmt">//--- 对象指针
  CAccountInfo       *AccountInfo(class="type">void);
  CStop              *MainStop(class="type">void);
  CMoneys            *Moneys(class="type">void);
  COrders            *Orders(class="type">void);
  COrders            *OrdersHistory(class="type">void);
  CStops             *Stops(class="type">void);
  CSignals           *Signals(class="type">void);
  CTimes             *Times(class="type">void);
  class=class="str">"cmt">//--- 订单管理
  class="type">class="kw">string              Comment(class="type">void) class="kw">const;
  class="type">void                Comment(class="kw">const class="type">class="kw">string);
  class="type">bool                EnableTrade(class="type">void) class="kw">const;
  class="type">void                EnableTrade(class="type">bool);
  class="type">bool                EnableLong(class="type">void) class="kw">const;

EA 参数接口的读写双生方法

MT5 里写 EA 的 C++ 风格类时,常见手法是把每个可调参数做成一对方法:无参 const 版本负责读,带参版本负责写。上面这段声明里,EnableLong、LotSize、Magic、MaxTrades 等都遵循这个模式,开 MT5 新建一个 Expert 类就能照抄骨架。 以 LotSize 为例:double LotSize(void) const 返回当前手数,void LotSize(const double) 接收新手数并写入成员。这种 getter/setter 分离让策略逻辑读取配置时不会被误改,回测时也能在 OnInit 里统一设值。 信号管理那组更有实战味:EveryTick 控制是否每 tick 计算信号,OneTradePerCandle 限制一根 K 线只开一单,PositionReverse 决定是否反手。把 OneTradePerCandle 设 true 后,欧美 5 分钟图上重复开仓次数可能从平均每根 3 次降到 1 次。 AddCandle 接收字符串加整数,用来挂额外周期蜡烛做多周期确认;外汇和贵金属波动受杠杆放大,这类参数乱设可能让回撤超出承受,上真仓前先跑历史数据验证。

MQL5 / C++
class="type">void                EnableLong(class="type">bool);
class="type">bool                EnableShort(class="type">void) class="kw">const;
class="type">void                EnableShort(class="type">bool);
class="type">int                 Expiration(class="type">void) class="kw">const;
class="type">void                Expiration(class="kw">const class="type">int);
class="type">class="kw">double              LotSize(class="type">void) class="kw">const;
class="type">void                LotSize(class="kw">const class="type">class="kw">double);
class="type">int                 MaxOrdersHistory(class="type">void) class="kw">const;
class="type">void                MaxOrdersHistory(class="kw">const class="type">int);
class="type">int                 Magic(class="type">void) class="kw">const;
class="type">void                Magic(class="kw">const class="type">int);
class="type">uint                MaxTrades(class="type">void) class="kw">const;
class="type">void                MaxTrades(class="kw">const class="type">int);
class="type">int                 MaxOrders(class="type">void) class="kw">const;
class="type">void                MaxOrders(class="kw">const class="type">int);
class="type">int                 OrdersTotal(class="type">void) class="kw">const;
class="type">int                 OrdersHistoryTotal(class="type">void) class="kw">const;
class="type">int                 TradesTotal(class="type">void) class="kw">const;
class=class="str">"cmt">//--- 信号管理
class="type">int                 Period(class="type">void) class="kw">const;
class="type">void                Period(class="kw">const class="type">int);
class="type">bool                EveryTick(class="type">void) class="kw">const;
class="type">void                EveryTick(class="kw">const class="type">bool);
class="type">bool                OneTradePerCandle(class="type">void) class="kw">const;
class="type">void                OneTradePerCandle(class="kw">const class="type">bool);
class="type">bool                PositionReverse(class="type">void) class="kw">const;
class="type">void                PositionReverse(class="kw">const class="type">bool);
class=class="str">"cmt">//--- 额外蜡烛
class="type">void                AddCandle(class="kw">const class="type">class="kw">string,class="kw">const class="type">int);
class=class="str">"cmt">//--- 新柱线检测

◍ 把生命周期接口收进类里管

EA 框架要做到可恢复、可卸载,得把 MT5 的事件入口和内部状态拆成一组虚函数。上面这段声明把 OnTick、OnChartEvent、OnTimer、OnTrade、OnDeinit 全部虚化,意味着子类可以只重写关心的那一个,其余沿用默认空实现。 protected 区才是实战重点:IsNewBar 用 symbol 名加周期整数判断新蜡烛,ManageOrders 与 ManageOrdersHistory 分开跑,避免每 tick 都扫历史订单拖慢回测。Time 返回 datetime、TradeOpen 带品种名和 ENUM_ORDER_TYPE 下单,都是给信号层直接调用的。 Deinit 不是只调一个析构,而是拆成 Account、Candle、Signals、Symbol、Times 五个独立清理函数。这样切换品种或重加载时,只清对应模块,EA 在外汇和贵金属这种高波动品种上重启不易闪崩。 Save 和 Load 接收 int 参数做版本号,老模板存档读不进新版时直接返回 false,比强行反序列化安全。开 MT5 建个空类把这套声明抄进去,编译过说明你的生命周期骨架没漏虚函数。

MQL5 / C++
  class="type">void                DetectNewBars(class="type">void);
  class=class="str">"cmt">//-- 事件
  class="kw">virtual class="type">bool        OnTick(class="type">void);
  class="kw">virtual class="type">void        OnChartEvent(class="kw">const class="type">int,class="kw">const class="type">long&,class="kw">const class="type">class="kw">double&,class="kw">const class="type">class="kw">string&);
  class="kw">virtual class="type">void        OnTimer(class="type">void);
  class="kw">virtual class="type">void        OnTrade(class="type">void);
  class="kw">virtual class="type">void        OnDeinit(class="kw">const class="type">int,class="kw">const class="type">int);
  class=class="str">"cmt">//--- 恢复
  class="kw">virtual class="type">bool        Save(class="kw">const class="type">int);
  class="kw">virtual class="type">bool        Load(class="kw">const class="type">int);
class="kw">protected:
  class=class="str">"cmt">//--- 蜡烛管理  
  class="kw">virtual class="type">bool        IsNewBar(class="kw">const class="type">class="kw">string,class="kw">const class="type">int);
  class=class="str">"cmt">//--- 订单管理
  class="kw">virtual class="type">void        ManageOrders(class="type">void);
  class="kw">virtual class="type">void        ManageOrdersHistory(class="type">void);
  class="kw">virtual class="type">void        OnTradeTransaction(COrder*) {}
  class="kw">virtual class="type">class="kw">datetime    Time(class="kw">const class="type">int);
  class="kw">virtual class="type">bool        TradeOpen(class="kw">const class="type">class="kw">string,class="kw">const ENUM_ORDER_TYPE,class="type">class="kw">double,class="type">bool);
  class=class="str">"cmt">//--- 品种管理
  class="kw">virtual class="type">bool        RefreshRates(class="type">void);
  class=class="str">"cmt">//--- 逆初始化
  class="type">void                DeinitAccount(class="type">void);
  class="type">void                DeinitCandle(class="type">void);
  class="type">void                DeinitSignals(class="type">void);
  class="type">void                DeinitSymbol(class="type">void);
  class="type">void                DeinitTimes(class="type">void);
};

常见问题

头文件膨胀后耦合严重,应把组件塞进类里,只在基类管生命周期,改策略只动组件。
继承基类并重写组件接口即可,基础买卖差异由基类处理,你只填信号与手数。
可以,小布能扫描你的EA结构,标出参数接口缺失的读写双生方法并给修改建议。
至少留初始化、每tick、退出三处接口,收进类里统一管理避免散落各处。
读写必须成对且类型一致,否则回测时参数载入会静默失败,建议加默认值校验。