基于交易模块创建多个 EA 交易:用统一抽象模型打通单品种多实例与多币种测试(基础篇)
🧩

基于交易模块创建多个 EA 交易:用统一抽象模型打通单品种多实例与多币种测试(基础篇)

(1/3)·MT5 净持仓量机制让多 EA 同品种协作和多币种并行测试陷入各自为政,本文先从根基拆解问题

案例拆解 第 1/3 篇
很多交易者直接在 MT5 里挂多个 EA 却发现仓位混在一起无法区分,便以为只能写虚拟订单或硬塞幻数来补救。其实根因是净持仓量模型与多实例需求天然冲突,本系列先从概念层把这套矛盾拆开。

用交易模块拼装多个 EA 的思路

在 MT5 里把核心逻辑写成独立交易模块,再用不同参数或信号源去实例化,就能快速得到多个 EA,而不必每次从零写一遍下单代码。 这种模块化的做法在 2014 年初就有交易者验证过:一篇公开讨论帖在发布后累计获得 6259 次查看、64 条回复,说明社区对可复用交易模块的需求一直存在。 具体落地时,先把开仓、平仓、风控封装成函数或类,主 EA 只负责调用;之后复制工程改几个输入变量,就是另一个品种或周期上的 EA。外汇与贵金属杠杆高,多 EA 同跑要先在策略测试器里分别回测,再上模拟盘观察相互影响。

◍ 从 MT4 到 MT5 的多币种困局

MT4 时代做多币种 EA 几乎走不通,根因有两点:策略测试器不能按订单号跨品种同步回测,MQL4 语言也撑不起复杂数据结构和高效管理。 MQL5 带来面向对象、辅助函数和一套标准库基类,技术底子够了,但客户端和测试器依旧没有内建机制,让单个 EA 在多个品种或周期上并行跑。老习惯是把 EA 挂在某个品种窗口里,品种名和周期被写死,MT5 的并行能力根本没被用起来。 MT5 每个品种只允许一个净持仓(等于该品种总交易量),这种以仓位为中心的设计及时反映了交易者在单一市场上的利益,却让订单可视化变麻烦:过去靠幻数认出自己的挂单就能操作,现在即便某品种净持仓为零,也不代表没有该 EA 的实例在跑。 第三方各显神通——有人写虚拟订单管理程序,有人用幻数把输入并进总持仓,还有专文讲在多个品种上跑同一 EA。这些方法有效,但都从单一视角切入:适合单品种多 EA 的方案,往往解不了多币种。 本文要的是一套通解:让单品种多 EA、多币种、多系统测试全吃掉。设想你的一个 EA 同时跑几十套策略、所有可用品种和周期,还自带资金管理系统,且能被测试器正常回测。 拆开任务就是三维:系统数 × 周期数 × 品种数。以 MACD 样例 EA 挂 8 个主要货币对算,不含周/月周期也有 1×8×19 = 152 种独立组合;组合再大、投资组合再广,轻松破 500,极端超 1000。手动配每种组合再加载不现实,必须让 EA 启动时自动装配组合进内存,按该组合实例的规则交易。外汇与贵金属杠杆高、回撤快,此类自动化框架仅降低配置成本,不预示任何收益。

「模型与实例:策略抽象的父子关系」

在后续内容里,我们不用“交易策略”这个泛称,而用“交易模型”指代一段按特定规则写成的类。它把指标、进出场条件、资金管理都封装进去,但本身不写死参数,属于抽象层。 最直观的例子是双均线穿越:快线向上穿慢线做多,向下穿做空。这句话就能落成一个可交易的模型骨架,但还没法直接跑。 真要实例化,必须补上均线方法、平均周期、看图周期和具体品种。同一个抽象模型可以派生出多个实例,比如周期 20/50 的 EURUSD 实例,和周期 10/30 的 XAUUSD 实例,它们共用逻辑、参数各异。外汇与贵金属杠杆高,实盘前请在 MT5 策略测试器里先验证实例表现。

净持仓量为何不能当模型信号

不少人习惯盯总持仓量,但在多策略同品种运行下,总持仓量和某个模型实例的真实敞口几乎不相关。模型可能是卖持,总持仓却中性;总持仓显示卖持,模型反而可能是买持。 举个具体例子:某品种跑三个独立策略,各自带资金管理系统。系统一先卖 3 张合约,净持仓为 -3;随后系统二买 4 张,净持仓变成 +1;系统三再卖 1 张,净持仓 = -3 + 4 - 1 = 0,回到中性。 中性是否代表三个系统全空仓?不是。此时系统一和系统三共持 4 张未平卖单待回补,系统二持 4 张买单待售出。只有这 8 张合约全部平仓,中性才真的意味着无敞口。外汇与贵金属杠杆高,这类错判会直接放大穿仓风险。 与其每次重算全品种操作序列,更干净的做法是直接抛弃总持仓量统计。总持仓受手动单、其他 EA 模型干扰,根本没法作为单一模型的行为依据。 落地办法:给每个模型实例配一张独立订单表,只装它自己下的市价单、挂单及已删订单。服务器按订单号存了建仓时间、交易量等全量信息,我们要做的只是把订单号绑到对应模型实例上,由实例内的订单表维护当前活动订单列表。开 MT5 用 CTrade::OrderGetTicket 配合自定义 CArray 就能验证这套隔离逻辑。

◍ 用基类加链表把多模型塞进一个EA

想在一个EA里挂无数个交易模型,第一步是抽象出一个统一接口。基类 CModel 只留一个 virtual Processing() 函数,所有具体模型(MACD、均线等)各自重写它,外部EA只认这个接口发信号,不用管内部怎么算。 原文示例里先写了两个独立模型 cmodel_macd 和 cmodel_moving,各自在 Processing() 里 Print 自己的名字。OnInit 里 new 出两个指针,OnTimer 每5秒轮着调用——这种硬编指针的方式模型一多就崩,完全没有可量测性。 数组要求元素同类型,而派生类彼此并不完全一致,塞不进数组。MT5标准库自带的 CList 链表类能绕开这个限制,但它的元素必须是 CObject 后代。所以让 CModel 继承 CObject,具体模型再继承 CModel,就能统一挂进 CList 列表。 list_models 作为全局链表,OnInit 里把模型指针 Add 进去后局部指针即可自毁;OnTick 里用 for(i=0;i<list_models.Total();i++) 循环取出每个元素,转回 CModel* 调 Processing()。某个模型构造失败就不加进列表,不影响其余模型跑。 Processing() 的契约很重:它要自己判市场状态、算保证金和交易量、管自己的持仓逻辑、读账户通用参数做资金管理,且不带输入参数、返回 bool。下面这段基类代码就是整套抽象的骨架。 别把指针当托管对象 局部 new 出的模型指针一旦 Add 进全局 CList,就可以在 OnInit 结束时让其脱离作用域自毁;真正存活的是链表里的节点,不是你手里的指针变量。

MQL5 / C++
class CModel
{
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("Model name is ", 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("Model name is ", m_name);class="kw">return(true);}
};
cmodel_macd      *macd;
cmodel_moving    *moving;

「链表基类把前后指针藏进受保护区」

MT5 标准库里 CObject 是所有可存储元素的老祖宗,它用两个受保护指针 m_prev、m_next 把对象串成双向链表,外部只能透过 Prev()/Next() 的公开封装去读写,不能直接碰内存地址。 看下面这段类声明,构造函数留空,但 Compare() 被设为 protected 且虚函数,默认返回 0——这意味着子类想进列表排序,必须自己重写比较逻辑,否则全部算相等。 Save() 和 Load() 也是虚函数且默认返回 true,说明基类不负责具体序列化,文件句柄由调用方传入;Type() 返回 0 只是占位标识。你在 EA 里若自建指标对象继承它,不重载 Type() 就可能和别的元素撞类型号。 开 MT5 按 F4 进 Include 目录翻 Object.mqh,把这段原样贴进自定义类比对,能直观看到 m_prev/m_next 为何不暴露。外汇和贵金属杠杆高,任何自定义容器出错都可能让回测假象放大实盘亏损,改完务必在策略测试器跑最小样例。

MQL5 / C++
class=class="str">"cmt">//|                                                                  https://www.metaquotes.net/ |
class=class="str">"cmt">//|                                                                 Revision class="num">2010.02.class="num">22 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "StdLibErr.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class CObject.                                                  |
class=class="str">"cmt">//| Purpose: Base class element storage.                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CObject
  {
class="kw">protected:
   CObject             *m_prev;            class=class="str">"cmt">// previous list item
   CObject             *m_next;            class=class="str">"cmt">// next list item
class="kw">public:
                     CObject();
   class=class="str">"cmt">//--- methods of access to class="kw">protected data
   CObject             *Prev()            { class="kw">return(m_prev); }
   class="type">void                Prev(CObject *node) { m_prev=node;    }
   CObject             *Next()            { class="kw">return(m_next); }
   class="type">void                Next(CObject *node) { m_next=node;    }
   class=class="str">"cmt">//--- methods for working with files
   class="kw">virtual class="type">bool        Save(class="type">int file_handle) { class="kw">return(true);  }
   class="kw">virtual class="type">bool        Load(class="type">int file_handle) { class="kw">return(true);  }
   class=class="str">"cmt">//--- method of identifying the object
   class="kw">virtual class="type">int         Type() const          { class="kw">return(class="num">0);     }
class="kw">protected:
   class="kw">virtual class="type">int         Compare(const CObject *node,class="type">int mode=class="num">0) const { class="kw">return(class="num">0); }
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor CObject.                                            |
class=class="str">"cmt">//| INPUT:  no.                                                    |
class=class="str">"cmt">//| OUTPUT: no.                                                    |
把多品种回测编排交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到不同 EA 实例的隔离状态,你只需专注策略逻辑而非底层仓位纠缠。

常见问题

MT5 采用净持仓量模式,同一品种只显示一个总仓位,不像 MT4 可按订单号逐条追踪,需要额外机制区分实例。
总仓位抹平了单个 EA 的未结订单边界,导致无法在策略测试器中并行验证不同系统的进出场与风控。
可以,小布盯盘的 AIGC 模块会按品种页聚合实例标记,省去你手动比对幻数或日志的重复劳动。
不是,虚拟订单是绕过净持仓的权宜方案,本文的抽象模型试图从根上统一单品种与多币种问题,不依赖伪订单。
第 2、3 篇分别进入设计与实战,完整类结构和回测样例会在后续展开,基础概念先在这篇铺垫清楚。