跨平台智能交易系统: CExpertAdvisor 和 CExpertAdvisors 类·综合运用
📘

跨平台智能交易系统: CExpertAdvisor 和 CExpertAdvisors 类·综合运用

第 3/3 篇

「EA 容器的逐票驱动写法」

在 MT5 里做多策略并行,常见做法是把每个 CExpertAdvisor 实例塞进一个容器,由总控在每笔报价进来时统一派发。上面这段就是典型的派发入口:先确认容器处于激活态,否则直接退出,避免空跑浪费资源。 循环从 0 跑到 Total(),逐个取出下标对应的专家顾问指针,调用各自的 OnTick()。这意味着你挂 N 个 EA,只需这一个计时入口就能让它们同时响应行情,不用每个 EA 各开一个事件钩子。 实盘里若 Total() 返回 8,则一次报价会触发 8 次 OnTick 执行;若其中某个 e 未正确初始化,指针为空就会在 e.OnTick() 处崩线程。开 MT5 把这段嵌进你的 CEA 集合类,先打印 Total() 值确认实例数符合预期,再决定要不要加空指针保护。

MQL5 / C++
  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 持久化逻辑写错可能导致重复开仓,实盘前务必在策略测试器里断电离线验证。

MQL5 / C++
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 引用是否非空最直观。

MQL5 / C++
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 结果逐步验证。

MQL5 / C++
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 里比对编译。

MQL5 / C++
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 接管;这种结构下回测若遇跳空,浮亏可能超出预期。

MQL5 / C++
  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 取值)。外汇与贵金属杠杆高,信号叠加只降噪不降风险,实盘前务必用历史数据验证参数敏感性。

MQL5 / C++
  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 用策略测试器逐文件验证比盲目跟更稳妥。

MQL5 / C++
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;
  }

常见问题

用逐票驱动写法,在容器循环里按品种实例化并调用各自EA的OnTick,保证每个符号独立处理订单与信号。
把订单状态和上下文落盘到文件,重启时回读并重建指针,避免重新初始化导致持仓判断错误。
可以,小布能读取你的EA结构和落盘逻辑,指出指针重建与订单容器新建里的常见漏洞,省去手动排查。
在容器里用唯一键判断元素是否已存在,存在则复用、不存在才新建,防止每次刷新都生成新实例。
在EA实例层封装资金与时间过滤器,容器调度前先跑过滤函数,不满足条件的实例直接跳过OnTick。