基于交易模块创建多个 EA 交易·进阶篇
🧩

基于交易模块创建多个 EA 交易·进阶篇

(2/3)·净持仓时代幻数失效,单品种多EA与多币种并行测试如何不各自为战

含代码示例 第 2/3 篇

MT5的净持仓机制让传统按幻数识别未结订单的做法不再可靠,同一品种上跑多个EA实例时,你很难判断哪笔仓位归谁。多数人要么写虚拟订单层,要么硬塞幻数进总持仓,方案能跑却无法同时覆盖多币种场景。本文给出一套可复用的交易模型基类,把这两类麻烦一次性收口。

「用链表把交易模型串起来」

MQL5 里做多策略并行,最省事的办法是把每个模型写成继承 CObject 的子类,再丢进 CList 链表统一管理。基类 CModel 只留一个虚函数 Processing(),子类 cmodel_macd、cmodel_moving 各自重写,运行时就能挨个调。 下面这段是最小可跑骨架:CObject 构造函数把前后指针置 NULL,CModel 派生后设了 m_name,两个具体模型在 Processing() 里只 Print 自身名字。OnInit 里 new 出链表和两个模型指针,用 CheckPointer 确认是 POINTER_DYNAMIC 后才 Add 进链表,返回不是 -1 就代表挂链成功。 实际验证时把这段贴进 MT5 新建的 mq5,编译跑起来,终端会打印「Model MACD successfully created」以及两次 Processing 的模型名。外汇与贵金属杠杆高,模型只负责结构搭建,信号真假还需结合盘面概率判断。 别把 Print 当信号源 链表里的 Processing 此刻只是占位输出,直接拿来开仓属于裸奔。先塞真实指标逻辑,再谈胜率。

MQL5 / C++
class="type">void CObject::CObject()
  {
class=class="str">"cmt">//--- initialize class="kw">protected data
   m_prev=NULL;
   m_next=NULL;
  }
class CModel : class="kw">public CObject
class CModel:CObject
{
class="kw">protected:
   class="type">class="kw">string            m_name;
class="kw">public:
            class="type">void            CModel(){m_name="Model base";}
   class="type">bool class="kw">virtual      Processing(class="type">void){class="kw">return(true);}
};
class cmodel_macd : class="kw">public CModel
{
class="kw">public:
   class="type">void            cmodel_macd(){m_name="MACD Model";}
   class="type">bool            Processing(){Print("Processing ", m_name, "...");class="kw">return(true);}
};
class cmodel_moving : class="kw">public CModel
{
class="kw">public:
   class="type">void            cmodel_moving(){m_name="Moving Average";}
   class="type">bool            Processing(){Print("Processing ", m_name, "...");class="kw">return(true);}
};
CList *list_models;
class="type">void OnInit()
{
   class="type">int rezult;
   cmodel_macd        *m_macd;
   cmodel_moving      *m_moving;
   list_models =       new CList();
   m_macd  =           new cmodel_macd();
   m_moving =          new cmodel_moving();
   if(CheckPointer(m_macd)==POINTER_DYNAMIC){
      rezult=list_models.Add(m_macd);
      if(rezult!=-class="num">1)Print("Model MACD successfully created");

用链表把指标模型跑起来

在 OnInit 里用 CheckPointer 确认 m_macd 与 m_moving 都是 POINTER_DYNAMIC 后,才通过 list_models.Add() 把两个模型挂进链表;返回 -1 就在日志打失败,否则打成功。这样 EA 启动时两类模型(MACD、MA)就进了同一个调度容器。 OnTick 中的循环是真正驱动逻辑:for(int i=0;i<list_models.Total();i++) 遍历链表,GetNodeAtIndex(i) 取出节点后调 Processing()。也就是说每根 tick 两个模型都会被顺序处理一次,无需手写重复调用。 日志给出了实跑证据:2010.10.10 14:18:21 在 EURUSD D1 上 Model MOVING AVERAGE / MACD was created successfully,随后 14:18:31 出现 Prosessing Moving Average... 与 Processing MACD Model...,证明链表添加与逐节点执行均生效。外汇与贵金属品种波动剧烈,此类多模型并行处理只降低代码耦合,不预示任何方向,实盘请自测滑点与重算开销。 OnDeinit 里一句 delete list_models 释放整条链表,避免指标句柄在反初始化时泄漏。开 MT5 把这段贴进 EA,改 Total() 前的模型数量就能验证调度是否如预期。

MQL5 / C++
else           Print("Creation of Model MACD has failed");
  }
  class=class="str">"cmt">//Check valid pointer
  if(CheckPointer(m_moving)==POINTER_DYNAMIC){
    rezult=list_models.Add(m_moving);
    if(rezult!=-class="num">1)Print("Model MOVING AVERAGE successfully created");
    else           Print("Creation of Model MOVING AVERAGE has failed");
  }
}
class="type">void OnTick()
{
  CModel                *current_model;
  for(class="type">int i=class="num">0;i<list_models.Total();i++){
    current_model=list_models.GetNodeAtIndex(i);
    current_model.Processing();
  }
}
class="type">void OnDeinit(const class="type">int reason)
{
  class="kw">delete list_models;
}
class="num">2010.10.class="num">10 class="num">14:class="num">18:class="num">31    ch01_simple_model(EURUSD,D1)   Prosessing Moving Average...
class="num">2010.10.class="num">10 class="num">14:class="num">18:class="num">31    ch01_simple_model(EURUSD,D1)   Processing MACD Model...
class="num">2010.10.class="num">10 class="num">14:class="num">18:class="num">21    ch01_simple_model(EURUSD,D1)   Model MOVING AVERAGE was created successfully
class="num">2010.10.class="num">10 class="num">14:class="num">18:class="num">21    ch01_simple_model(EURUSD,D1)   Model MACD was created successfully  
rezult=list_models.Add(m_macd);
rezult=list_models.Add(m_moving);
CModel                *current_model;
for(class="type">int i=class="num">0;i<list_models.Total();i++){
  current_model=list_models.GetNodeAtIndex(i);
  current_model.Processing();
}

◍ 用虚拟订单表兜住 MT5 的仓位模糊性

MT4 里每张订单独立挂在 Trade 选项卡,平仓针对具体订单号;MT5 改了规则,挂单成交后进入服务器历史,终端只暴露总持仓量。EA 想平掉自己那部分仓位时,没有“按原订单号反向”的概念,只能全平或部平,但部平哪一段说不清。 若靠翻历史订单去匹配 EA 自己发的单,再关联市况拦截回报,实现成本极高,且难判断某单之前是否已被拦过。假设当前仓位只归一个策略的这一次 EA,也仅适用于单策略单实例,多系统同跑就崩。 最直接的办法是在模型内存里自建一张虚拟订单表:模型发单时把 ticket 记进 CTableOrders 实例,用熟悉的链表挂到 ListTableOrders。知道 ticket 就能反查几乎所有信息,模型与订单号因此绑定。 MT5 的 SL/TP 是绑总仓位的——例如先开 1.0 手 EURUSD 买无止损,后开 0.1 手买设距现价 100 点止损止盈,价格触线时 1.1 手整仓被平。所以多系统 EA 不能依赖系统级 SL/TP,只能在虚拟订单内写自己的模拟止损止盈;价位一到,EA 自行拦截该单并从活动表移除。 CTableOrders 的 Add() 是先拿订单号去历史订单分配(用 CHistoryOrderInfo),失败再试当前挂单 OrderSelect()。文中所有交易模型只做市价执行(ORDER_TYPE_BUY / ORDER_TYPE_SELL),挂单策略靠这张表也能落地。外汇与贵金属杠杆高,自建虚拟表不消除爆仓风险,参数错了照样快速回撤。

MQL5 / C++
class="macro">#class="kw">property copyright "Copyright class="num">2010, MetaQuotes Software Corp."
class="macro">#include <Trade\OrderInfo.mqh>
class="macro">#include <Trade\HistoryOrderInfo.mqh>
class="macro">#include <Arrays\List.mqh>
class CTableOrders : CObject
{
class="kw">private:
  class="type">ulong m_magic;      class=class="str">"cmt">// EA幻数,标识发出订单的EA
  class="type">ulong m_ticket;     class=class="str">"cmt">// 基础订单号
  class="type">ulong m_ticket_sl;  class=class="str">"cmt">// 绑定基础订单的模拟止损单号
  class="type">ulong m_ticket_tp;  class=class="str">"cmt">// 绑定基础订单的模拟止盈单号
  ENUM_ORDER_TYPE m_type; class=class="str">"cmt">// 订单类型
  class="type">class="kw">datetime m_time_setup;  class=class="str">"cmt">// 订单建立时间
  class="type">class="kw">double m_price;     class=class="str">"cmt">// 订单价格
  class="type">class="kw">double m_sl;        class=class="str">"cmt">// 止损价(虚拟)
  class="type">class="kw">double m_tp;        class=class="str">"cmt">// 止盈价(虚拟)
  class="type">class="kw">double m_volume_initial; class=class="str">"cmt">// 订单交易量
class="kw">public:
  CTableOrders();
  class="type">bool Add(COrderInfo &order_info, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit);

「订单容器类的存取与初始化写法」

把历史订单和当下挂单统一收进一个 CTableOrders 结构,核心就是两个重载的 Add 方法:一个吃 CHistoryOrderInfo,一个吃 COrderInfo,都靠外部传入 stop_loss / take_profit 来补全止损止盈字段。 类里暴露的只读接口很直白——StopLoss()、TakeProfit()、Magic()、Ticket()、Price() 等直接 return 私有成员,Type() 则把 m_type 强转回 ENUM_ORDER_TYPE 方便后续按类型分支。 默认构造函数把 m_magic、m_ticket、m_type、m_time_setup 清零,m_price 与 m_volume_initial 置 0.0,意味着新建对象不绑定任何订单,必须等 Add 成功后才算落地。 Add 内部先 HistoryOrderSelect / OrderSelect 按票号定位,失败就返回 false,调用方得自己处理这种未选中情况;外汇与贵金属品种波动剧烈,回测里若票号失效会导致容器漏单,实盘前建议在策略测试器用 EURUSD 的 2023 年数据跑一遍确认命中率。

MQL5 / C++
  class="type">bool               Add(CHistoryOrderInfo &history_order_info, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit);
  class="type">class="kw">double             StopLoss(class="type">void){class="kw">return(m_sl);}
  class="type">class="kw">double             TakeProfit(class="type">void){class="kw">return(m_tp);}
  class="type">ulong              Magic(){class="kw">return(m_magic);}
  class="type">ulong              Ticket(){class="kw">return(m_ticket);}
  class="type">int                Type() const;
  class="type">class="kw">datetime           TimeSetup(){class="kw">return(m_time_setup);}
  class="type">class="kw">double             Price(){class="kw">return(m_price);}
  class="type">class="kw">double             VolumeInitial(){class="kw">return(m_volume_initial);}
};
CTableOrders::CTableOrders(class="type">void)
{
   m_magic=class="num">0;
   m_ticket=class="num">0;
   m_type=class="num">0;
   m_time_setup=class="num">0;
   m_price=class="num">0.0;
   m_volume_initial=class="num">0.0;
}
class="type">bool CTableOrders::Add(CHistoryOrderInfo &history_order_info, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit)
{
   if(HistoryOrderSelect(history_order_info.Ticket())){
      m_magic=history_order_info.Magic();
      m_ticket=history_order_info.Ticket();
      m_type=history_order_info.Type();
      m_time_setup=history_order_info.TimeSetup();
      m_volume_initial=history_order_info.VolumeInitial();
      m_price=history_order_info.PriceOpen();
      m_sl=stop_loss;
      m_tp=take_profit;
      class="kw">return(true);
   }
   else class="kw">return(false);
}
class="type">bool CTableOrders::Add(COrderInfo &order_info, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit)
{
   if(OrderSelect(order_info.Ticket())){
      m_magic=order_info.Magic();
      m_ticket=order_info.Ticket();
      m_type=order_info.Type();
      m_time_setup=order_info.TimeSetup();
      m_volume_initial=order_info.VolumeInitial();
      m_price=order_info.PriceOpen();
      m_sl=stop_loss;
      m_tp=take_profit;
      class="kw">return(true);
   }
   else class="kw">return(false);
}
class="type">int   CTableOrders::Type() const
{
   class="kw">return((ENUM_ORDER_TYPE)m_type);
}

CModel 基类把交易模型骨架定死了

写 EA 时如果多个模型要共用一套订单表和风控逻辑,先理解 CModel 这个基类能省掉大量重复代码。它把任何交易模型都需要的常量收拢到一起:幻数 m_magic、交易品种 m_symbol、周期 m_timeframe、模型名 m_model_name,外加一个 m_delta 用作资金管理的系数——比如账户风险 2% 就填 0.02,不用资本化公式时默认 0.0 即可。 订单数据走指针动态创建:CTableOrders *table 存订单表,CList *ListTableOrders 存每个订单的副本。辅助类从 CAccountInfo 到 CPositionInfo 一应俱全,派生模型不用再自己调 OrderSelect / OrderSend 那套底层函数。 m_timing 里塞的是 t_period 结构,解决多模型不同周期触发的问题。OnTimer 不够用,因为小时图模型要每根小时柱触发、日图模型要每根日柱触发。t_period 记上次柱时间,比较当前柱时间不等就返回 true 并更新,相等返回 false,新柱判定就这么简单。 SendOrder 是唯一的发单出口。它先用 CheckLot 校验可行交易量:假设双向上限 15 标准手,已有 3 手买仓,模型想加 18.6 手买,CheckLot 会修正成 12 手;若已有的是卖仓则直接给 15 手。超量返回 EMPTY_VALUE 必须往上传递。 预付款不够时 CheckMargin 会尝试缩量,连最小手都保不住就进追加状态返回 EMPTY_VALUE。遇到超大单量还会拆单:想买 11.3 手 EURUSD、上限 5.0 手,就拆成 5.0 + 5.0 + 1.3 三个独立订单进表,各自带止损获利,平仓时按顺序扫。ADD_ORDER 进表、DELETE_ORDER 发完删链接,靠订单号或填零区分。 基类还提供 Add / Delete / GetNumberOrders / CloseAllPosition / Processing 等函数,Init 在构造里自动跑。下面这段是类声明的核心部分,指针成员和辅助类一目了然。

MQL5 / C++
class CModel : class="kw">public CObject
{
class="kw">protected:
   class="type">long             m_magic;
   class="type">class="kw">string           m_symbol;
   ENUM_TIMEFRAMES m_timeframe;
   class="type">class="kw">string           m_model_name;
   class="type">class="kw">double           m_delta;
   CTableOrders    *table;
   CList           *ListTableOrders;
   CAccountInfo     m_account_info;
   CTrade           m_trade;
   CSymbolInfo      m_symbol_info;
   COrderInfo       m_order_info;
   CHistoryOrderInfo m_history_order_info;
   CPositionInfo    m_position_info;
   CDealInfo        m_deal_info;
   t_period         m_timing;
class="kw">public:
                     CModel()   { Init();   }
                    ~CModel() { Deinit(); }

◍ 订单模型与周期切换的底层接口

这套交易模型把订单对象的核心读写都收口在几个访问器里:Name() 双向重载可读写模型名,Timeframe() 只暴露周期枚举,Symbol() 同样双向重载。虚拟函数 Init、Deinit、Processing 留了扩展点,Deinit 里直接 delete ListTableOrders 释放挂单表,避免 EA 卸载时泄漏。 protected 区才是真正落单的逻辑:Add() 接收 COrderInfo 或 CHistoryOrderInfo 并带入 sl/tp,SendOrder() 汇总 symbol、订单类型、模式、票号、手数、价格、止损止盈和注释一次性发单。CloseAllPosition 与两种 Delete 重载(按枚举或 ticket)构成持仓清理面。 周期触发靠 t_period 结构体和 timing() 配合。t_period 用 m1 到 mn1 共 21 个 datetime 字段记住各周期上一根 K 线的开盘时间,current 存当前。timing() 每次只 CopyRates 取最新 1 根,若 rez==0 打印 Error timing 并返回 false;否则按 tf 比对 raters[0].time 与对应字段,相同说明本根已处理过返回 false,不同则更新字段并返回 true——这是典型的「每根 K 线只跑一次」闸门。 把这段直接塞进你的 EA 头文件,在 OnTick 里对 PERIOD_M1 调 timing,就能验证黄金 1 分钟图平均每日触发约 1440 次、其中因重复被拦掉的占绝大多数。外汇与贵金属波动剧烈,这类高频触发逻辑需自行压测滑点风险。

MQL5 / C++
class="type">class="kw">string Name(){class="kw">return(m_model_name);}
class="type">void Name(class="type">class="kw">string name){m_model_name=name;}
ENUM_TIMEFRAMES Timeframe(class="type">void){class="kw">return(m_timeframe);}
class="type">class="kw">string Symbol(class="type">void){class="kw">return(m_symbol);}
class="type">void Symbol(class="type">class="kw">string set_symbol){m_symbol=set_symbol;}
class="type">bool class="kw">virtual Init();
class="type">void class="kw">virtual Deinit(){class="kw">delete ListTableOrders;}
class="type">bool class="kw">virtual Processing(){class="kw">return (true);}
class="type">class="kw">double GetMyPosition();
class="type">bool Delete(ENUM_TYPE_DELETED_ORDER);
class="type">bool Delete(class="type">ulong Ticket);
class="type">void CloseAllPosition();
class=class="str">"cmt">//class="type">bool class="kw">virtual Trade();
class="kw">protected:
class="type">bool Add(COrderInfo &order_info, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit);
class="type">bool Add(CHistoryOrderInfo &history_order_info, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit);
class="type">void GetNumberOrders(n_orders &orders);
class="type">bool SendOrder(class="type">class="kw">string symbol, ENUM_ORDER_TYPE op_type, ENUM_ORDER_MODE op_mode, class="type">ulong ticket, class="type">class="kw">double lot,
class="type">class="kw">double price, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit, class="type">class="kw">string comment);
};
class="kw">struct t_period
{
 class="type">class="kw">datetime m1;
 class="type">class="kw">datetime m2;
 class="type">class="kw">datetime m3;
 class="type">class="kw">datetime m4;
 class="type">class="kw">datetime m5;
 class="type">class="kw">datetime m6;
 class="type">class="kw">datetime m10;
 class="type">class="kw">datetime m12;
 class="type">class="kw">datetime m15;
 class="type">class="kw">datetime m20;
 class="type">class="kw">datetime m30;
 class="type">class="kw">datetime h1;
 class="type">class="kw">datetime h2;
 class="type">class="kw">datetime h3;
 class="type">class="kw">datetime h4;
 class="type">class="kw">datetime h6;
 class="type">class="kw">datetime h8;
 class="type">class="kw">datetime h12;
 class="type">class="kw">datetime d1;
 class="type">class="kw">datetime w1;
 class="type">class="kw">datetime mn1;  
 class="type">class="kw">datetime current;
};
class="type">bool timing(class="type">class="kw">string symbol, ENUM_TIMEFRAMES tf, t_period &timeframes)
{
 class="type">int rez;
 class="type">MqlRates raters[class="num">1];
 rez=CopyRates(symbol, tf, class="num">0, class="num">1, raters);
 if(rez==class="num">0)
{
   Print("Error timing");
   class="kw">return(false);
 }
 class="kw">switch(tf){
  case PERIOD_M1:
    if(raters[class="num">0].time==timeframes.m1)class="kw">return(false);
    else{timeframes.m1=raters[class="num">0].time; class="kw">return(true);}
}
把多实例诊断交给小布
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到当前净持仓下各EA实例的虚拟归属与冲突提示,你专注决策而非对账。

常见问题

因为品种层面只保留一个汇总净持仓,不再逐订单挂魔术号,需借助虚拟订单表在模型层重建归属关系。
前者争用同一净持仓需实例隔离,后者跨品种需统一调度接口;本文基类把两者抽象成同一交易模型以降低重复开发。
可以,小布在品种页内置了基于虚拟订单表的归属视图,能标出可能的魔术号重叠与平仓竞争,省去手动翻日志。
一般需重写开仓条件、平仓规则和虚拟表同步函数,具体代码见本篇第5节示例。
维护成本高且易在跨品种回测时行为不一致,统一模型基类能复用数据组织与测试接口。