跨平台智能交易系统: CExpertAdvisor 和 CExpertAdvisors 类·进阶篇
🧩

跨平台智能交易系统: CExpertAdvisor 和 CExpertAdvisors 类·进阶篇

(2/3)· 从 OnInit 到新柱线判定,拆解跨平台 EA 类的交互骨架与易错点

进阶 第 2/3 篇
把策略组件散写在 EA 头文件里,初始化一多就乱。CExpertAdvisor 用一行调用把事件分发收口,新柱线判定也别再手写时间比对踩坑。

◍ EA 启动时的对象装配逻辑

在 MT5 智能交易系统里,OnInit 阶段的核心不是写策略,而是把资金管理、信号、品种对象等先实例化,再挂到 CExpertAdvisor 的实例上。挂好之后,后续任何事件函数里只需要一行调用对应处理方法,写法上跟标准库 CExpert 的思路接近。 创建 CExpertAdvisor 实例后必须调它的 Init 方法。这里会确定交易品种(转成 CSymbolInfo 对象)、默认周期,以及三个关键开关:every_tick 控制逐笔分价还是每根蜡烛首笔才跑核心逻辑;one_trade_per_candle 限制一根蜡烛最多开一单,避免同根蜡烛反复入场;position_reverse 决定反向信号时是否先平再反手。 OnInit 收尾前还要调 InitComponents,它逐个调用各组件的 Init 和 Validate。任一组件初始化失败就 Print 报错并返回 false,只有五个组件(信号、时间、订单管理、蜡烛管理、事件聚合)全过才返回 true。 下面这段是 Init 与 InitComponents 的原文实现,逐行拆一下能直接照抄到自己的 EA 框架里。

MQL5 / C++
class="type">bool CExpertAdvisorBase::Init(class="type">class="kw">string symbol,class="type">int period,class="type">int magic,class="type">bool every_tick=true,class="type">bool one_trade_per_candle=true,class="type">bool position_reverse=true)
  {
  m_symbol_name=symbol;                      class=class="str">"cmt">// 记录品种名
  CSymbolInfo *instrument;
  if((instrument=new CSymbolInfo)==NULL)     class=class="str">"cmt">// 新建品种信息对象,失败则返回
    class="kw">return class="kw">false;
  if(symbol==NULL) symbol=Symbol();          class=class="str">"cmt">// 未指定品种则用当前图表的
  if(!instrument.Name(symbol))               class=class="str">"cmt">// 绑定品种名,失败返回
    class="kw">return class="kw">false;
  instrument.Refresh();                      class=class="str">"cmt">// 刷新品种数据
  m_symbol_man.Add(instrument);              class=class="str">"cmt">// 加入品种管理器
  m_symbol_man.SetPrimary(m_symbol_name);    class=class="str">"cmt">// 设主品种
  m_period=(ENUM_TIMEFRAMES)period;          class=class="str">"cmt">// 保存周期
  m_every_tick=every_tick;                   class=class="str">"cmt">// 逐笔 or 首笔开关
  m_order_man.Magic(magic);                  class=class="str">"cmt">// 订单魔术码
  m_position_reverse=position_reverse;       class=class="str">"cmt">// 反手开关
  m_one_trade_per_candle=one_trade_per_candle;class=class="str">"cmt">// 每根限一单开关
  CCandle *candle=new CCandle();             class=class="str">"cmt">// 新建蜡烛对象
  candle.Init(instrument,m_period);          class=class="str">"cmt">// 初始化蜡烛
  m_candle_man.Add(candle);                  class=class="str">"cmt">// 加入蜡烛管理器
  Magic(magic);
  class="kw">return class="kw">false;                              class=class="str">"cmt">// 注意:原文此处返回 class="kw">false,疑似笔误,实际应返回 true 才合理
  }
class="type">bool CExpertAdvisorBase::InitComponents(class="type">void)
  {
  if(!InitSignals())                         class=class="str">"cmt">// 初始化信号组件
    {
    Print(__FUNCTION__+": 信号初始化错误");
    class="kw">return class="kw">false;
    }
  if(!InitTimes())                           class=class="str">"cmt">// 初始化时间组件
    {
    Print(__FUNCTION__+": 时间初始化错误");
    class="kw">return class="kw">false;
    }
  if(!InitOrderManager())                    class=class="str">"cmt">// 初始化订单管理
    {
    Print(__FUNCTION__+": 订单管理器初始化错误");
    class="kw">return class="kw">false;
    }
  if(!InitCandleManager())                   class=class="str">"cmt">// 初始化蜡烛管理
    {
    Print(__FUNCTION__+": 蜡烛管理器初始化错误");
    class="kw">return class="kw">false;
    }
  if(!InitEventAggregator())                 class=class="str">"cmt">// 初始化事件聚合
    {
    Print(__FUNCTION__+": 事件聚合器初始化错误");
    class="kw">return class="kw">false;
    }
  class="kw">return true;                               class=class="str">"cmt">// 全过才返回 true
  }

新柱线怎么在 EA 里被认出来

很多只在蜡烛开盘第一笔分价动手的策略,核心就一件事:准确判断「这根 K 线是新的吗」。CCandleBase 把这套判断收进了类里,EA 只要在每笔 tick 调一次 Check,就能拿到布尔结果,后续你想挂单、改参数、发通知都接在后面。 判定逻辑不复杂,但有三个分支,至少命中一个才算新柱线:最后记录的开盘时间 ≠ 当前柱线开盘时间;或最后记录的开盘价 ≠ 当前柱线开盘价;或最后记录开盘时间为 0(即 EA 刚加载还没历史状态)。前两条是常态比对,第三条只发生在 EA 初始化那一瞬间——有人把加载后的第一根直接当新柱线用,有人偏要等图表真画出下一根,两种取向都合理,看你是哪种流派。 CCandle 实例按「品种 + 时间帧」建,比如 EURUSD M15 和 EURUSD H1 各一个;同品种同周期重复 new 是冗余,应该用 CCandleManager 按名查复用。这样同一个 EA 能同时盯多周期新蜡烛,不用自己维护数组。 除了探新柱,这两个类还管「这笔品种周期是否已经交易过」的标志位,靠 TradeProcessed 的 getter/setter 切。CCandleManager 接到请求会转发给对应 CCandle,后者直接写成员 m_trade_processed。外汇和贵金属波动猛,tick 漏判可能错过信号,建议你在 MT5 用 Print 把 LastTime 和当前 time 打出来,跑一晚验证三个条件在你 broker 上的触发节奏。

MQL5 / C++
class CCandleBase : class="kw">public CObject
{
class="kw">protected:
  class="type">bool              m_new;
  class="type">bool              m_wait_for_new;
  class="type">bool              m_trade_processed;
  class="type">int               m_period;
  class="type">bool              m_active;
  class="type">MqlRates          m_last;
  CSymbolInfo      *m_symbol;
  CEventAggregator *m_event_man;
  CObject          *m_container;
class="kw">public:
                     CCandleBase(class="type">void);
                    ~CCandleBase(class="type">void);
  class="kw">virtual class="type">int       Type(class="type">void) class="kw">const {class="kw">return(CLASS_TYPE_CANDLE);}
  class="kw">virtual class="type">bool      Init(CSymbolInfo*,class="kw">const class="type">int);
  class="kw">virtual class="type">bool      Init(CEventAggregator*);
  CObject          *GetContainer(class="type">void);
  class="type">void              SetContainer(CObject*);
  class=class="str">"cmt">//--- 赋值与取值
  class="type">void              Active(class="type">bool);
  class="type">bool              Active(class="type">void) class="kw">const;
  class="type">class="kw">datetime          LastTime(class="type">void) class="kw">const;
  class="type">class="kw">double            LastOpen(class="type">void) class="kw">const;
  class="type">class="kw">double            LastHigh(class="type">void) class="kw">const;
  class="type">class="kw">double            LastLow(class="type">void) class="kw">const;
  class="type">class="kw">double            LastClose(class="type">void) class="kw">const;
  class="type">class="kw">string            SymbolName(class="type">void) class="kw">const;
  class="type">int               Timeframe(class="type">void) class="kw">const;
  class="type">void              WaitForNew(class="type">bool);
  class="type">bool              WaitForNew(class="type">void) class="kw">const;
  class=class="str">"cmt">//--- 处理

「K线基类怎么判定新蜡烛」

CCandleBase 把「新K线」的判定收敛到 Compare() 与 Check() 两个虚函数里,外部只需周期性调用 Check() 就能拿到状态,不用自己写时间比对。 Check() 先判断 Active(),未激活直接 return;随后用 CopyRates 取最近 1 根已闭合的 MqlRates(起始位置 1、取 1 根),若返回 -1 也退出。拿到 rates[0] 后丢给 Compare(),不等就置 IsNewCandle(true)、TradeProcessed(false) 并刷新 m_last。 Compare() 的返回逻辑是:m_last.time 与当前 time 不同,或 open 按 TickSize 归一化后不等,或首次运行(m_last.time==0 且不在等待新K线)任一成立即视为新蜡烛。用 open/TickSize 取整比对,能避开浮点误差导致的误判。 CCandleManagerBase 则继承 CArrayObj,用 Add(string symbol, int period) 管理多品种多周期蜡烛对象,并暴露 IsNewCandle(symbol,period) 供上层直接查。外汇与贵金属波动快,这类封装在 EA 里跑须留意 TickSize 在极端点差下的跳变风险。

MQL5 / C++
class="kw">virtual class="type">bool        TradeProcessed(class="type">void) class="kw">const;
class="kw">virtual class="type">void        TradeProcessed(class="type">bool);
class="kw">virtual class="type">void        Check(class="type">void);
class="kw">virtual class="type">void        IsNewCandle(class="type">bool);
class="kw">virtual class="type">bool        IsNewCandle(class="type">void) class="kw">const;
class="kw">virtual class="type">bool        Compare(class="type">MqlRates &) class="kw">const;
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);
};
CCandleBase::Check(class="type">void)
  {
  if(!Active())
    class="kw">return;
  IsNewCandle(class="kw">false);
  class="type">MqlRates rates[];
  if(CopyRates(m_symbol.Name(),(ENUM_TIMEFRAMES)m_period,class="num">1,class="num">1,rates)==-class="num">1)
    class="kw">return;
  if(Compare(rates[class="num">0]))
   {
    IsNewCandle(true);
    TradeProcessed(class="kw">false);
    m_last=rates[class="num">0];
   }
  }
class="type">bool CCandleBase::Compare(class="type">MqlRates &rates) class="kw">const
  {
  class="kw">return (m_last.time!=rates.time ||
          (m_last.open/m_symbol.TickSize())!=(rates.open/m_symbol.TickSize()) ||
          (!m_wait_for_new && m_last.time==class="num">0));
  }
class CCandleManagerBase : class="kw">public CArrayObj
  {
class="kw">protected:
  class="type">bool                m_active;
  CSymbolManager     *m_symbol_man;
  CEventAggregator *m_event_man;
  CObject            *m_container;
class="kw">public:
                     CCandleManagerBase(class="type">void);
                    ~CCandleManagerBase(class="type">void);
  class="kw">virtual class="type">int        Type(class="type">void) class="kw">const {class="kw">return(CLASS_TYPE_CANDLE_MANAGER);}
  class="kw">virtual class="type">bool       Init(CSymbolManager*,CEventAggregator*);
  class="kw">virtual class="type">bool       Add(class="kw">const class="type">class="kw">string,class="kw">const class="type">int);
  CObject           *GetContainer(class="type">void);
  class="type">void               SetContainer(CObject *container);
  class="type">bool               Active(class="type">void) class="kw">const;
  class="type">void               Active(class="type">bool active);
  class="kw">virtual class="type">void       Check(class="type">void) class="kw">const;
  class="kw">virtual class="type">bool       IsNewCandle(class="kw">const class="type">class="kw">string,class="kw">const class="type">int) class="kw">const;

◍ 用指针判空规避取烛崩溃

在 MT5 自定义烛管理类里,Get() 返回的是 CCandle 指针,直接点方法可能踩到空指针。上面这段基类实现先用 CheckPointer 判空,再转发 TradeProcessed(),比在外面写一堆 if 更干净。 TradeProcessed 被重载了两次:一个带 symbol+timeframe 由管理器调用,一个无参由单根烛对象调用返回 m_trade_processed 布尔状态,还有一个带 bool 值的版本用于写回状态。三者分工是「查某品种某周期是否已处理」与「单烛自身状态读写」。 开 MT5 把这段塞进你的 EA 框架,编译后随便传个不存在的 symbol 调 Get,若没判空会直接抛无效指针异常;加上 CheckPointer 后稳定返回 false,回测里能少掉不少莫名终止。外汇与贵金属波动剧烈,这类底层容错只解决稳定性,不预示任何方向。

MQL5 / C++
class="kw">virtual CCandle *Get(class="kw">const class="type">class="kw">string,class="kw">const class="type">int) class="kw">const;
 class="kw">virtual class="type">bool       TradeProcessed(class="kw">const class="type">class="kw">string,class="kw">const class="type">int) class="kw">const;
 class="kw">virtual class="type">void       TradeProcessed(class="kw">const class="type">class="kw">string,class="kw">const class="type">int,class="kw">const class="type">bool) class="kw">const;
 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="type">bool CCandleManagerBase::TradeProcessed(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">int timeframe) class="kw">const
  {
  CCandle *candle=Get(symbol,timeframe);
  if(CheckPointer(candle))
    class="kw">return candle.TradeProcessed();
  class="kw">return class="kw">false;
  }
class="type">bool CCandleBase::TradeProcessed(class="type">void) class="kw">const
  {
  class="kw">return m_trade_processed;
  }
CCandleBase::TradeProcessed(class="type">bool value)
  {
  m_trade_processed=value;
  }

OnTick 里的分价防重与信号闸门

CExpertAdvisor 的 OnTick 是 EA 运行的主战场,绝大多数动作都在这一函数内发生。它理想情况下只在 OnTick 事件里被调用,但也可能从 OnChartEvent 等入口触发,因此类内用逐笔分价标记做互斥:若标记未复位时方法被重入,且当前分价满足交易条件,就可能在同一分价上重复发单。 方法开头先翻转分价标记,再刷新行情数据。刷新失败时会把标记重置并直接退出,等下一笔分价到来,这能避免用陈旧报价重复处理。默认每笔分价都检测信号,但你可以改逻辑成「仅新柱线检测」来提速,回测与优化时尤为明显。 m_position_reverse 负责中和反向仓:MT4/MT5 对冲模式下,与信号反方向的仓位被平仓,同方向不动;MT5 净持模式因单方向限制,会用等量反向手数开仓抵消。真正放行交易要看 m_signals 的信号,再叠时间过滤器或「仅新柱交易」等开关,全满足才进场。外汇与贵金属杠杆高,这类重入或重复发单会带来远超预期的滑点风险。 分价处理收尾会把标记设回 false,相当于交还处理权。想验证重入危害,可在 OnChartEvent 里手动调一次 OnTick,观察同分价是否双开。

「多EA同图运行的容器解法」

CExpertAdvisor 这类对象也有自己的批量管理容器,基类叫 CExpertAdvisorsBase,它从 CArrayObj 继承,本质是用对象数组把多个 EA 实例收在一起。 从基类声明能看到,它复刻了单 EA 的主要公开接口,比如 OnTick、OnChartEvent、OnTimer、OnTrade 等事件方法,还带了 Active 开关和 OrdersTotal / TradesTotal 这类统计接口。 OnTick 的实现逻辑很直接:对数组里每一个 CExpertAdvisor 实例做迭代,逐个调用其 OnTick。这意味着你只要 new 出多个 EA 实例、把指针塞进 CExpertAdvisors 容器,再在图表主 EA 的 OnTick 里调容器的 OnTick,就能在一个图表上跑多套策略。 用标准库的 CArrayObj 或其子类也能达到类似效果,但 CExpertAdvisors 是把多实例协同固化好的现成路径,可能是单图表挂多个智能交易系统最省事的做法。外汇与贵金属杠杆高,多 EA 同图会放大仓位与滑点风险,参数和手数需自行压低验证。 下面这段基类声明和 OnTick 骨架,建议直接贴进 MT5 的 MQH 里编译看继承关系: class CExpertAdvisorsBase : public CArrayObj —— 定义容器类,继承自对象数组基类 protected: bool m_active —— 容器级激活开关 protected: int m_uninit_reason —— 卸载原因代码 protected: CObject *m_container —— 反向指向所属容器 public: CExpertAdvisorsBase(void) / ~CExpertAdvisorsBase(void) —— 构造与析构 virtual int Type(void) const {return CLASS_TYPE_EXPERTS;} —— 返回类类型标识 virtual int UninitializeReason(void) const {return m_uninit_reason;} —— 取卸载原因 void SetContainer(CObject *container) / CObject *GetContainer(void) —— 设/取所属容器 bool Active(void) const / void Active(const bool) —— 读/写激活状态 int OrdersTotal(void) const / OrdersHistoryTotal / TradesTotal —— 订单与持仓统计 virtual bool Validate(void) const / InitComponents(void) const —— 校验与组件初始化 virtual void OnTick(void) —— 核心:遍历调用各实例 OnTick virtual void OnChartEvent / OnTimer / OnTrade / OnDeinit —— 其他事件透传 virtual bool CreateElement / Save / Load —— 元素创建与状态存取 void CExpertAdvisorsBase::OnTick(void) { —— 方法起始,函数体内部即迭代逻辑

MQL5 / C++
class CExpertAdvisorsBase : class="kw">public CArrayObj
  {
class="kw">protected:
   class="type">bool                m_active;
   class="type">int                 m_uninit_reason;
   CObject             *m_container;
class="kw">public:
                      CExpertAdvisorsBase(class="type">void);
                      ~CExpertAdvisorsBase(class="type">void);
   class="kw">virtual class="type">int         Type(class="type">void) class="kw">const {class="kw">return CLASS_TYPE_EXPERTS;}
   class="kw">virtual class="type">int         UninitializeReason(class="type">void) class="kw">const {class="kw">return m_uninit_reason;}
   class=class="str">"cmt">//--- 取值和赋值
   class="type">void                SetContainer(CObject *container);
   CObject             *GetContainer(class="type">void);
   class="type">bool                Active(class="type">void) class="kw">const;
   class="type">void                Active(class="kw">const class="type">bool);
   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="kw">virtual class="type">bool        Validate(class="type">void) class="kw">const;
   class="kw">virtual class="type">bool        InitComponents(class="type">void) class="kw">const;
   class=class="str">"cmt">//--- 事件
   class="kw">virtual class="type">void        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        CreateElement(class="kw">const class="type">int);
   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="type">void CExpertAdvisorsBase::OnTick(class="type">void)
  {
交给小布盯盘看组件状态
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 加载与每根蜡烛执行限制的实际表现,你只管调策略。

常见问题

会创建品种对象、默认周期、分价或新蜡烛执行规则、每根蜡烛交易限制及反向信号逆转等组件实例,并在末尾调用 InitComponents 做校验。
可借助 CCandle 类封装的开盘价与开盘时间状态比较,避免手动维护全局变量带来的跨平台重置问题,概率上更稳。
能,小布盯盘对应品种页会呈现 EA 执行约束的实际运行状态,便于你快速核对是否按预期只在新柱首笔分价触发。
因为 CExpertAdvisor 实例把各组件方法包成包装器,事件函数只需转调对应处理程序,类似 MQL5 标准库 CExpert 的思路。