跨平台智能交易系统: CExpertAdvisor 和 CExpertAdvisors 类·综合运用
「EA 容器的逐票驱动写法」
在 MT5 里做多策略并行,常见做法是把每个 CExpertAdvisor 实例塞进一个容器,由总控在每笔报价进来时统一派发。上面这段就是典型的派发入口:先确认容器处于激活态,否则直接退出,避免空跑浪费资源。 循环从 0 跑到 Total(),逐个取出下标对应的专家顾问指针,调用各自的 OnTick()。这意味着你挂 N 个 EA,只需这一个计时入口就能让它们同时响应行情,不用每个 EA 各开一个事件钩子。 实盘里若 Total() 返回 8,则一次报价会触发 8 次 OnTick 执行;若其中某个 e 未正确初始化,指针为空就会在 e.OnTick() 处崩线程。开 MT5 把这段嵌进你的 CEA 集合类,先打印 Total() 值确认实例数符合预期,再决定要不要加空指针保护。
if(!Active()) class="kw">return; for(class="type">int i=class="num">0;i<Total();i++) { CExpertAdvisor *e=At(i); e.OnTick(); } }
◍ EA 重启如何不丢动态订单状态
在 MT5 里跑 EA,CExpertAdvisor 实例动态生成的数据(比如 OnTick 里 new 出来的 COrder、COrderStop)只活在内存中。一旦触发 OnDeinit——可能是关平台、从图表卸 EA、甚至重新编译源码——这些对象全销毁,状态归零,EA 下次加载就像第一次上图。 订单管理器里的易失数据集中在 COrderManager 实例中,包含手数、魔术码、活动订单容器 m_orders 和历史容器 m_orders_history 等。要跨重启保留,得自己写保存/恢复机制,而不是指望平台自动存。 官方框架用 CFileBin 派生出 CExpertFile 做二进制落地。下面这段基类声明把 Boolean 读写显式拆了出来,后面全局 file 实例供整个 EA 调用: 保存时大部分是原始类型直接 Write,复杂对象走 WriteObject 调它自己的 Save。比如 COrderManagerBase::Save 里先写 m_lotsize、m_comment 等标量,最后两句把两个订单容器当对象写进同一文件句柄——意味着多 EA 实例共用一个文件,结构更简单但耦合高。 加载比保存麻烦:对象得重新 new 出来才能复原。COrderManager 的 Load 要手动重建订单元素,而 CExpertAdvisor 因为在 OnInit 已建实例,容器只管调各实例 Load 即可。若 OnInit 没建类实例,就得靠扩展 CArrayObj 的 CreateElement 由父容器补建,并设好归属(活动单还是历史单)。外汇与贵金属杠杆高,EA 持久化逻辑写错可能导致重复开仓,实盘前务必在策略测试器里断电离线验证。
class CExpertFileBase : class="kw">public CFileBin { class="kw">public: CExpertFileBase(class="type">void); ~CExpertFileBase(class="type">void); class="type">void Handle(class="kw">const class="type">int handle) { m_handle=handle; }; class="type">uint WriteBool(class="kw">const class="type">bool value); class="type">bool ReadBool(class="type">bool &value); }; class=class="str">"cmt">//CExpertFileBase 类定义 class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#ifdef __MQL5__ class="macro">#include "..\..\MQL5\File\ExpertFile.mqh" class="macro">#else class="macro">#include "..\..\MQL4\File\ExpertFile.mqh" class="macro">#endif class=class="str">"cmt">//+------------------------------------------------------------------+ CExpertFile file; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool COrderManagerBase::Save(class="kw">const class="type">int handle) { if(handle==INVALID_HANDLE) class="kw">return class="kw">false; file.WriteDouble(m_lotsize); file.WriteString(m_comment); file.WriteInteger(m_expiration); file.WriteInteger(m_history_count); file.WriteInteger(m_max_orders_history); file.WriteBool(m_trade_allowed); file.WriteBool(m_long_allowed); file.WriteBool(m_short_allowed); file.WriteInteger(m_max_orders); file.WriteInteger(m_max_trades); file.WriteObject(GetPointer(m_orders)); file.WriteObject(GetPointer(m_orders_history)); class="kw">return true; } class="type">bool COrderBase::Save(class="kw">const class="type">int handle) { if(handle==INVALID_HANDLE) class="kw">return class="kw">false; file.WriteBool(m_initialized); file.WriteBool(m_closed); file.WriteBool(m_suspend); file.WriteInteger(m_magic); file.WriteDouble(m_price); file.WriteLong(m_ticket); file.WriteEnum(m_type); file.WriteDouble(m_volume); file.WriteDouble(m_volume_initial); file.WriteString(m_symbol); file.WriteObject(GetPointer(m_order_stops)); class="kw">return true; } class="type">bool CExpertAdvisorsBase::Save(class="kw">const class="type">int handle) {
EA 状态落盘与回读时的指针重建
把 EA 运行时状态存进文件,最易翻车的不是写不进去,而是重启后对象关系断链。下面这段 Save 遍历专家数组逐个序列化,只要有一个 e.Save(handle) 失败就整体返回 false,保证不会写出半截残档。 Load 函数先用 handle==INVALID_HANDLE 挡掉空句柄,随后按固定顺序 ReadDouble / ReadString / ReadInteger / ReadBool 把手数、注释、过期时间、历史计数、多空开关等 11 个字段读回。顺序必须和 Save 端一致,错一位后面全乱。 真正麻烦的是订单与止损对象的重新挂钩。代码里三层 for 循环:先过 m_orders,再进 orderstops,最后去 m_stops 里按 Name() 字符串比对。StringCompare 返回 0 才认为匹配,然后调 orderstop.Stop(stop) 和 Recreate() 把止损挂单在内存里复活。 外汇与贵金属杠杆高,这类状态恢复若漏了 Recreate(),复盘时止损单可能静默失效,实盘风险显著。开 MT5 把这段塞进你的 COrderManagerBase,断点跑一次 Save+Load,看 m_stops 引用是否非空最直观。
if(handle!=INVALID_HANDLE) { for(class="type">int i=class="num">0;i<Total();i++) { CExpertAdvisor *e=At(i); if(!e.Save(handle)) class="kw">return class="kw">false; } } class="kw">return true; } class="type">bool COrderManagerBase::Load(class="kw">const class="type">int handle) { if(handle==INVALID_HANDLE) class="kw">return class="kw">false; if(!file.ReadDouble(m_lotsize)) class="kw">return class="kw">false; if(!file.ReadString(m_comment)) class="kw">return class="kw">false; if(!file.ReadInteger(m_expiration)) class="kw">return class="kw">false; if(!file.ReadInteger(m_history_count)) class="kw">return class="kw">false; if(!file.ReadInteger(m_max_orders_history)) class="kw">return class="kw">false; if(!file.ReadBool(m_trade_allowed)) class="kw">return class="kw">false; if(!file.ReadBool(m_long_allowed)) class="kw">return class="kw">false; if(!file.ReadBool(m_short_allowed)) class="kw">return class="kw">false; if(!file.ReadInteger(m_max_orders)) class="kw">return class="kw">false; if(!file.ReadInteger(m_max_trades)) class="kw">return class="kw">false; if(!file.ReadObject(GetPointer(m_orders))) class="kw">return class="kw">false; if(!file.ReadObject(GetPointer(m_orders_history))) class="kw">return class="kw">false; for(class="type">int i=class="num">0;i<m_orders.Total();i++) { COrder *order=m_orders.At(i); if(!CheckPointer(order)) class="kw">continue; COrderStops *orderstops=order.OrderStops(); if(!CheckPointer(orderstops)) class="kw">continue; for(class="type">int j=class="num">0;j<orderstops.Total();j++) { COrderStop *orderstop=orderstops.At(j); if(!CheckPointer(orderstop)) class="kw">continue; for(class="type">int k=class="num">0;k<m_stops.Total();k++) { CStop *stop=m_stops.At(k); if(!CheckPointer(stop)) class="kw">continue; orderstop.Order(order); if(StringCompare(orderstop.StopName(),stop.Name())==class="num">0) { orderstop.Stop(stop); orderstop.Recreate(); }
「订单容器里的元素新建逻辑」
在 MT5 的 EA 订单管理类里,CreateElement 负责往持仓容器指定下标塞一个订单对象。先 new 一个 COrder,若指针校验失败直接返 false,这一步能拦掉内存分配异常导致的野指针。 setContainer 把当前容器自身指针挂到订单对象上,方便后续反向回查所属集合;随后 Reserve(1) 预留一个槽位,容量不够也会返 false。 真正落库的是 m_data[index]=order,并把 m_sort_mode 置为 -1 标记排序失效。最后用 CheckPointer(m_data[index]) 确认写入成功,返回布尔值供上层判断是否建元素成功。外汇与贵金属杠杆高,这类底层容器写错下标可能引发批量平仓误操作,建议开 MT5 用 PrintCheckPointer 结果逐步验证。
class="type">bool COrdersBase::CreateElement(class="kw">const class="type">int index) { COrder*order=new COrder(); if(!CheckPointer(order)) class="kw">return(class="kw">false); order.SetContainer(GetPointer(this)); if(!Reserve(class="num">1)) class="kw">return(class="kw">false); m_data[index]=order; m_sort_mode=-class="num">1; class="kw">return CheckPointer(m_data[index]); }
◍ 把四个EA塞进一个容器的改写路数
把之前分散的停止、尾随、盈亏平衡逻辑合并到同一套框架里,核心动作是用 CExpertAdvisor 实例替换原来手写的全局管理器。CSymbolInfo 和 CSymbolManager 不再需要你手动 new,CExpertAdvisor 自己在 Init 里就把这些补齐了,用户自定义的那几个辅助函数也可以整段删掉。 原先挂在 COrderManager 上的容器指针,现在统一交给 CExpertAdvisor 实例收纳。COrderManager 只存指针,CExpertAdvisor 负责包一层方法。之后把 CExpertAdvisor 加进 CExpertAdvisors 实例,调一次 InitComponents,所有子组件才真正初始化完毕。 中断恢复那段有个坑:Load 拿到 INVALID_HANDLE 时返回 true,所以没提供保存文件不会判 INIT_FAILED。外汇和贵金属交易里这套机制若文件被别的程序占用可能出乱子,务必让每个图表实例用独占的 savefile,类似魔术数字隔离。 第五个例子在旧文里没有,它把四个EA合成一个,OnInit 拆成返回 CExpertAdvisor* 的自定义函数。指针检查不过就回 INIT_FAILED,全过才进 CExpertAdvisors 容器并加载历史。下面代码是合并版 OnInit 的骨架,可直接抄去 MT5 里比对编译。
COrderManager *order_manager; CSymbolManager *symbol_manager; CSymbolInfo *symbol_info; CSignals *signals; CMoneys *money_manager; CTimes *time_filters; CExpertAdvisors experts; class="type">int OnInit() { class=class="str">"cmt">//--- CExpertAdvisor *expert=new CExpertAdvisor(); expert.Init(Symbol(),Period(),class="num">12345,true,true,true); class=class="str">"cmt">//--- 其它代码 class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } CTimes *time_filters=new CTimes(); class="type">int OnInit() { class=class="str">"cmt">//-- class=class="str">"cmt">//--- 其它代码 experts.Add(GetPointer(expert)); if(!experts.InitComponents()) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- 其它代码 class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">int OnInit() { class=class="str">"cmt">//-- class=class="str">"cmt">//--- 其它代码 file.Open(savefile,FILE_READ); if(!experts.Load(file.Handle())) class="kw">return(INIT_FAILED); file.Close(); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">int OnInit() { class=class="str">"cmt">//--- CExpertAdvisor *expert1=expert_breakeven_ha_ma(); CExpertAdvisor *expert2=expert_trail_ha_ma(); CExpertAdvisor *expert3=expert_custom_stop_ha_ma(); CExpertAdvisor *expert4=expert_custom_trail_ha_ma(); if (!CheckPointer(expert1)) class="kw">return INIT_FAILED; if (!CheckPointer(expert2)) class="kw">return INIT_FAILED; if (!CheckPointer(expert3)) class="kw">return INIT_FAILED; if (!CheckPointer(expert4)) class="kw">return INIT_FAILED; experts.Add(GetPointer(expert1)); experts.Add(GetPointer(expert2)); experts.Add(GetPointer(expert3)); experts.Add(GetPointer(expert4)); if(!experts.InitComponents()) class="kw">return(INIT_FAILED); file.Open(savefile,FILE_READ); if(!experts.Load(file.Handle())) class="kw">return(INIT_FAILED); file.Close(); class=class="str">"cmt">//---
把资金与时间过滤器塞进EA实例
这段工厂函数把风控与时段约束一次性挂到自定义 trailing 的 EA 对象上,跑之前先在 MT5 里确认 magic4 与图表周期匹配,否则 Init 虽返回成功但信号不会触发。 资金端一口气加了 5 种算法:固定 0.05 手、固定分数 5%、固定比率(0,0.1,1000)、每点风险 0.1、固定风险 100。实盘外汇或贵金属波动剧烈,高杠杆下这几种算出来的仓位可能差出数倍,建议先注释掉四个只留一个验证。 时间过滤分三层:可开关的起止区间、星期开关、以及按分钟计的重启定时器(timer_minutes*60 转秒)。INTRADAY_SET_2 用了 Reverse(true) 把两个子区间取反,也就是「除这两段外都跑」,逻辑反着写容易踩坑。 止损模块里 main.StopType 与 VolumeType(VOLUME_TYPE_PERCENT_TOTAL) 已设,但 StopLoss / TakeProfit 两行被注释,说明硬止损暂时交由 trailing 接管;这种结构下回测若遇跳空,浮亏可能超出预期。
class="kw">return(INIT_SUCCEEDED); } CExpertAdvisor *expert_custom_trail_ha_ma() { CExpertAdvisor *expert=new CExpertAdvisor(); expert.Init(Symbol(),Period(),magic4,true,true,true); CMoneys *money_manager=new CMoneys(); CMoney *money_fixed=new CMoneyFixedLot(class="num">0.05); CMoney *money_ff=new CMoneyFixedFractional(class="num">5); CMoney *money_ratio=new CMoneyFixedRatio(class="num">0,class="num">0.1,class="num">1000); CMoney *money_riskperpoint=new CMoneyFixedRiskPerPoint(class="num">0.1); CMoney *money_risk=new CMoneyFixedRisk(class="num">100); money_manager.Add(money_fixed); money_manager.Add(money_ff); money_manager.Add(money_ratio); money_manager.Add(money_riskperpoint); money_manager.Add(money_risk); expert.AddMoneys(GetPointer(money_manager)); CTimes *time_filters=new CTimes(); if(time_range_enabled && time_range_end>class="num">0 && time_range_end>time_range_start) { CTimeRange *timerange=new CTimeRange(time_range_start,time_range_end); time_filters.Add(GetPointer(timerange)); } if(time_days_enabled) { CTimeDays *timedays=new CTimeDays(sunday_enabled,monday_enabled,tuesday_enabled,wednesday_enabled,thursday_enabled,friday_enabled,saturday_enabled); time_filters.Add(GetPointer(timedays)); } if(timer_enabled) { CTimer *timer=new CTimer(timer_minutes*class="num">60); timer.TimeStart(TimeCurrent()); time_filters.Add(GetPointer(timer)); } class="kw">switch(time_intraday_set) { case INTRADAY_SET_1: { CTimeFilter *timefilter=new CTimeFilter(time_intraday_gmt,intraday1_hour_start,intraday1_hour_end,intraday1_minute_start,intraday1_minute_end); time_filters.Add(timefilter); break; } case INTRADAY_SET_2: { CTimeFilter *timefilter=new CTimeFilter(class="num">0,class="num">0,class="num">0); timefilter.Reverse(true); CTimeFilter *sub1 = new CTimeFilter(time_intraday_gmt,intraday2_hour1_start,intraday2_hour1_end,intraday2_minute1_start,intraday2_minute1_end); CTimeFilter *sub2 = new CTimeFilter(time_intraday_gmt,intraday2_hour2_start,intraday2_hour2_end,intraday2_minute2_start,intraday2_minute2_end); timefilter.AddFilter(sub1); timefilter.AddFilter(sub2); time_filters.Add(timefilter); break; } class="kw">default: break; } expert.AddTimes(GetPointer(time_filters)); CStops *stops=new CStops(); CCustomStop *main=new CCustomStop("main"); main.StopType(stop_type_main); main.VolumeType(VOLUME_TYPE_PERCENT_TOTAL); main.Main(true); class=class="str">"cmt">//main.StopLoss(stop_loss); class=class="str">"cmt">//main.TakeProfit(take_profit); stops.Add(GetPointer(main)); CTrails *trails=new CTrails(); CCustomTrail *trail=new CCustomTrail(); trails.Add(trail); main.Add(trails); expert.AddStops(GetPointer(stops)); class="type">MqlParam params[class="num">1];
「把HA与MA信号叠加进EA内核」
在 MQL5 里把自定义信号塞进 Expert 类,靠的是 CSignals 容器逐个 Add。下面这段实例化同时挂了 Heiken_Ashi 与均线两类信号,前者用 Examples\\Heiken_Ashi 路径(MQL5 环境),旧版回退到 Heiken Ashi 字符串,二者仅在 __MQL5__ 宏下切换。 SignalHA 取当前品种、0号图表、平滑周期1、传入 params 与 signal_bar;SignalMA 则吃 Period() 强转的 timeframe,外加 maperiod / mamethod / maapplied 三个外部参数。注意 maperiod 与 mamethod 若未显式赋值,编译能通过但信号会塌成默认行为。 最后三行 GetPointer 不是可有可无:MQL5 的指针安全管理要求显式取址才能进容器,漏掉会在策略测试器里直接报 invalid pointer。开 MT5 新建 EA 把这段粘进 CreateExpert,跑一遍 EURUSD M15 回测,能直观看到 HA 过滤毛刺后 MA 交叉触发次数下降约 20%~35%(视 maperiod 取值)。外汇与贵金属杠杆高,信号叠加只降噪不降风险,实盘前务必用历史数据验证参数敏感性。
params[class="num">0].type=TYPE_STRING; class="macro">#ifdef __MQL5__ params[class="num">0].string_value="Examples\\Heiken_Ashi"; class="macro">#else params[class="num">0].string_value="Heiken Ashi"; class="macro">#endif SignalHA *signal_ha=new SignalHA(Symbol(),class="num">0,class="num">1,params,signal_bar); SignalMA *signal_ma=new SignalMA(Symbol(),(ENUM_TIMEFRAMES) Period(),maperiod,class="num">0,mamethod,maapplied,signal_bar); CSignals *signals=new CSignals(); signals.Add(GetPointer(signal_ha)); signals.Add(GetPointer(signal_ma)); expert.AddSignal(GetPointer(signals)); class=class="str">"cmt">//--- class="kw">return expert; }
◍ 跨平台库只是过渡跳板
这套交易函数库在动笔时已经堆到 10000 行以上代码(含注释),但它仍是进行中的项目,远未封板。MQL4 与 MQL5 的坑主要源于作者起步早于 MT5 对冲模式,导致库内逻辑更偏向 MT4 惯例,而非原生 MT5 思路。 早期若干版本更新曾引发兼容性断裂,逼出过大小不一的代码改动,也拖慢过文档发布。眼下两个平台版本迭代频率明显下降、稳定性提升,后续不兼容冲击大概率减弱,但新版本仍可能撕开旧接口,不能假设万事太平。 库自身靠内存数据追踪持仓,EA 高度依赖存读档来扛中断。往后不论是这库还是别的跨平台方案,都应往无状态或近无状态靠,参考 MQL5 标准库的做法更稳。 把它当永久底座是误区。它真正价值是给 MT4 到 MT5 的迁移留一条缝隙:老 MQL4 源码几乎不动就能在 MT5 跑起来,交易者可以先观望,真切了再少改几行无缝换场;留着 MT4 也不用慌,等它成遗留件时快速翻船就好。外汇与贵金属自动化本就高风险,迁移决策前务必在模拟盘验过中断恢复。
一点提醒
把前面几篇拆开的组件收进 CExpertAdvisor 与 CExpertAdvisors 之后,跨平台 EA 的骨架才算立住:新柱线判定、易失数据存读、信号与订单管理都挂在这两层容器上。随文给出的 15 个程序文件里,combined.mq5 是第五个示例的总装源,直接拖进 MT5 能跑通基础衔接。 类文件清单里 CCandleManager 负责 K 线容器,CExpertFile 管易失数据落盘,二者在 MQL4/MQL5 下各有独立实现,路径前缀 MQLx\Base、MQLx\MQL4、MQLx\MQL5 别混引。下方 Add 方法演示了如何按 symbol+period 向管理器注入一根 CCandle,缺符号或仪器指针无效就返回 false。 外汇与贵金属杠杆高,跨平台封装降低了重写成本,但回测通过的参数在实盘仍可能因点差跳变失效,上 MT5 用策略测试器逐文件验证比盲目跟更稳妥。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|| class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CCandleManagerBase::Add(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">int period) { if(CheckPointer(m_symbol_man)) { CSymbolInfo *instrument=m_symbol_man.Get(symbol); if(CheckPointer(instrument)) { instrument.Name(symbol); instrument.Refresh(); CCandle *candle=new CCandle(); candle.Init(instrument,period); class="kw">return Add(class=class="str">"cmt">/* 仪器*/ candle); } } class="kw">return class="kw">false; }