基于交易模块创建多个 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 结束时让其脱离作用域自毁;真正存活的是链表里的节点,不是你手里的指针变量。
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 为何不暴露。外汇和贵金属杠杆高,任何自定义容器出错都可能让回测假象放大实盘亏损,改完务必在策略测试器跑最小样例。
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. |