跨平台智能交易系统: CExpertAdvisor 和 CExpertAdvisors 类·进阶篇
(2/3)· 从 OnInit 到新柱线判定,拆解跨平台 EA 类的交互骨架与易错点
◍ 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 框架里。
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 上的触发节奏。
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 在极端点差下的跳变风险。
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,回测里能少掉不少莫名终止。外汇与贵金属波动剧烈,这类底层容错只解决稳定性,不预示任何方向。
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) { —— 方法起始,函数体内部即迭代逻辑
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) {