开发多币种 EA 交易(第 3 部分):架构修改(基础篇)
「把单币种 EA 改成多币种要动哪根骨架」
在 MT5 里写 EA,默认模板只盯一个品种。想让它同时扫 EURUSD、XAUUSD、GBPJPY 这类多标的,第一刀得砍在架构层,而不是在 OnTick 里硬塞几个 Symbol 判断。 核心改动是把「当前图表品种」的依赖从全局剥离,改成由外部参数数组驱动。原文作者 Yuriy Bykov 在 2024-09-23 发布的系列第 3 篇里,基础工程把品种列表做成了可配置输入,EA 初始化时遍历该列表分别绑定交易对象。 一个容易踩的坑:MT5 的 HistorySelect 和 PositionSelect 默认只认当前品种上下文,多币种下必须带 symbol 参数显式调用,否则回测时只跑出当前图表那一根线的结果。外汇与贵金属杠杆高、跳空频繁,多币种并行会放大隔夜风险与保证金占用,参数上线前务必在策略测试器用真实点差跑一遍。
◍ 多币种 EA 的演进脉络
在前面两篇里,我们一直在搭一个能同时跑多种策略的多币种 EA。第二版给出的实现方式,跟第一篇里提出的方案已经明显不是一回事,说明这条路线还在找最优解,并没有定型。 把实现里的琐碎细节暂时放下,从整体看这套系统,才能知道下一步该往哪改。顺着它这段不长但变化很快的演变过程往回看,能更清楚当前架构的来路和短板。 外汇与贵金属多币种策略叠加杠杆,回撤放大是高风险常态,任何架构调整都先在 MT5 策略测试器里用历史数据验证再上实盘。
EA 聚合器怎么跑起来
在 OnInit() 里先 new 一个 CAdvisor(或其子类)对象,再把各个 CStrategy 子类实例塞进它的策略数组。这一步只是搭骨架,真正干活在 OnTick()。 每次报价进来,OnTick() 调 CAdvisor::Tick(),它内部遍历策略数组,逐个触发 CStrategy::Tick()。策略自己在 Tick() 里完成开仓、平仓判断与执行。 这种聚合的好处很直接:只要你手上有按某种策略写的 EA 源码,改几行就能让它和多策略实例并行跑,不用重写整套逻辑。 但坑也明显。组合多个策略时,为控风险往往要把每个实例的仓位调小。MT5 公开品种最小开仓手数固定(如 0.01 标准手),当策略实例数变多或初始入金偏小,某些实例会因手数被压到下限以下而根本开不了仓,甚至全部闲置。实例越多、存款越少,这种概率越大。 另一个现实问题:不同策略实例可能在同一时刻开出方向相反、大小相等的仓位。合并看净敞口为零,但两边隔夜库存费(swap)照扣不误,纯亏手续费。外汇与贵金属杠杆高,这类隐性成本会放大本金侵蚀风险。
「把仓位操作收口到虚拟层」
第一种运行模式里,策略能直接开平市场仓位,导致多实例间互相干扰、小仓位被忽略。第二篇文章改了路子:所有跟市场仓位相关的动作集中到一个独立模块,策略本身不再有直连市场的权限。代价是现成策略要重写一部分接口,但换来了对第一种模式短板的覆盖。 新结构里冒出两个实体:虚拟仓位(CVirtualOrder 类)和策略交易量接收器(CReceiver 类及其子类)。EA 启动阶段,OnInit() 里依次做四件事:建接收器对象、建 EA 对象并把接收器塞进去、建策略对象并挂到 EA 的策略数组、每个策略再建自己的虚拟仓位对象数组。 行情到来时,OnTick() 调用 CAdvisor::Tick(),它遍历策略跑 CStrategy::Tick()。策略在内部开平虚拟仓位,一旦虚拟持仓组合变了就置一个变更标志。只要有一个策略标志位被置起,接收器就启动调仓方法去对齐市场真实未平仓量,成功后再清掉所有策略的标志。 这么改之后,哪怕某个策略实例只开了极小的虚拟量,也可能成为压过市场最小允许交易量的那滴水,真实仓位随即被打开——不会再出现某些实例完全不影响盘面的情况。附带的好处偏向实盘:库存费可能下降、存款占用倾向减少、观测回撤降低,夏普和利润因子这类评估指标也更干净。外汇与贵金属杠杆高,这类架构省成本不代表风险降低,回撤缩小也可能只是账面平滑。 测试第二种模式时暴露出几处别扭:虚拟仓位的止损止盈判断被挪进 CVirtualOrder,但基类因此被塞进太具体的实现;CStrategy 为了给接收器报总虚拟量加了 Volume() 方法,却顺手切断了单策略多品种虚拟开仓的可能,只能测单品种;EA 和接收器各自存一份策略指针数组,代码重复;接收器靠品种数组过滤,又带来使用限制。 下一步的清理方向已经定:把 CStrategy / CAdvisor 基类瘦身后,派生出 CVirtualStrategy 和 CVirtualAdvisor 专门跑虚拟交易分支;虚拟仓位改为持有接收器与策略的双指针,开关仓直接通知两边;所有虚拟仓位收进接收器持有的一个共用数组,策略只从中领元素用;接收器做成单例(CVirtualReceiver),内部再按品种挂 CVirtualSymbolReceiver,仓位开关自动绑解。具体实现留到代码层落地。
◍ 把基类砍到只剩骨架
做框架瘦身时,CStrategy 只留一个纯虚 Tick() 就够了,其他逻辑全压到子类。这样基类头文件能压到 10 行以内,编译依赖也干净。 CAdvisor 里顺手塞了个 Macros.mqh,用宏替掉反复写的数组操作。APPEND 靠 ArrayResize 返回的新长度减 1 定位末位写入;FIND 倒序扫数组,命中写索引否则留 -1;ADD 先 FIND 再决定是否 APPEND,避免重复元素;FOREACH 把索引锁在本地 i 里跑主体;REMOVE_AT 做前移压缩,REMOVE 组合 FIND+REMOVE_AT。宏不调函数,栈上展开,读起来直白。 接收器从 CAdvisor 抽掉后,OnTick 只负责转发给策略。派生层 CVirtualStrategy 挂虚拟仓位数组与未平总数,开平方法暂时只触发重算;CVirtualAdvisor 才在构造里用 CVirtualReceiver::Instance() 绑定唯一接收器指针,析构释放。策略侧完全不跟踪变化,所有权交给接收器自己判。 外汇与贵金属 EA 改这种结构时注意:基类的纯虚约束一旦动错,MT5 会直接拒编,建议先在策略测试器跑空 Tick 验证继承链。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Base class of the trading strategy | class=class="str">"cmt">//+------------------------------------------------------------------+ class CStrategy { class="kw">public: class="kw">virtual class="type">void Tick() = class="num">0; class=class="str">"cmt">// Handle OnTick events }; class=class="str">"cmt">// Useful macros for array operations class="macro">#ifndef __MACROS_INCLUDE__ class="macro">#define APPEND(A, V) A[ArrayResize(A, ArraySize(A) + class="num">1) - class="num">1] = V; class="macro">#define FIND(A, V, I) { for(I=ArraySize(A)-class="num">1;I>=class="num">0;I--) { if(A[I]==V) break; } } class="macro">#define ADD(A, V) { class="type">int i; FIND(A, V, i) if(i==-class="num">1) { APPEND(A, V) } } class="macro">#define FOREACH(A, D) { for(class="type">int i=class="num">0, im=ArraySize(A);i<im;i++) {D;} } class="macro">#define REMOVE_AT(A, I) { class="type">int s=ArraySize(A);for(class="type">int i=I;i<s-class="num">1;i++) { A[i]=A[i+class="num">1]; } ArrayResize(A, s-class="num">1);} class="macro">#define REMOVE(A, V) { class="type">int i; FIND(A, V, i) if(i>=class="num">0) REMOVE_AT(A, i) } class="macro">#define __MACROS_INCLUDE__ class="macro">#endif class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Macros.mqh" class="macro">#include "Strategy.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA base class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CAdvisor { class="kw">protected: CStrategy *m_strategies[]; class=class="str">"cmt">// Array of trading strategies class="kw">public: ~CAdvisor(); class=class="str">"cmt">// Destructor class="kw">virtual class="type">void Tick(); class=class="str">"cmt">// OnTick event handler class="kw">virtual class="type">void Add(CStrategy *strategy); class=class="str">"cmt">// Method for adding a strategy }; class=class="str">"cmt">//+------------------------------------------------------------------+
把策略塞进顾问类再跑虚拟单
上面这段把「顾问-策略」的骨架收了口:析构时清空策略数组,每 tick 轮询所有策略的 Tick(),Add() 往数组尾塞新策略指针。 FOREACH 是框架宏,等价遍历下标 i;m_strategies 是指针数组,delete 时只释放堆上的 CStrategy 派生对象,不碰数组本身。 CVirtualStrategy 继承 CStrategy,内部挂了 m_orders[] 虚拟单数组和 m_ordersTotal 计数器。CountOrders() 把总数归零后,逐单判 IsOpen(),命中就 +1——这是后续开平仓事件里实时统计仓位的底层依赖。 OnOpen/OnClose 两个虚函数都只调 CountOrders(),意味着真实账户不成交,所有仓位状态纯靠本地数组维护。外汇与贵金属杠杆高,虚拟单和实盘滑点、点差脱节,回测漂亮不等于实盘能复现,上 MT5 前先想清这块偏差。
class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CAdvisor::~CAdvisor() { class=class="str">"cmt">// Delete all strategy objects FOREACH(m_strategies, class="kw">delete m_strategies[i]); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnTick event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CAdvisor::Tick(class="type">void) { class=class="str">"cmt">// Call OnTick handling for all strategies FOREACH(m_strategies, m_strategies[i].Tick()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Strategy adding method | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CAdvisor::Add(CStrategy *strategy) { APPEND(m_strategies, strategy); class=class="str">"cmt">// Add the strategy to the end of the array } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Strategy.mqh" class="macro">#include "VirtualOrder.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class of a trading strategy with class="kw">virtual positions | class=class="str">"cmt">//+------------------------------------------------------------------+ class CVirtualStrategy : class="kw">public CStrategy { class="kw">protected: CVirtualOrder *m_orders[]; class=class="str">"cmt">// Array of class="kw">virtual positions(orders) class="type">int m_ordersTotal; class=class="str">"cmt">// Total number of open positions and orders class="kw">virtual class="type">void CountOrders(); class=class="str">"cmt">// Calculate the number of open positions and orders class="kw">public: class="kw">virtual class="type">void OnOpen(); class=class="str">"cmt">// Event handler for opening a class="kw">virtual position(order) class="kw">virtual class="type">void OnClose(); class=class="str">"cmt">// Event handler for closing a class="kw">virtual position(order) }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Counting open class="kw">virtual positions and orders | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualStrategy::CountOrders() { m_ordersTotal = class="num">0; FOREACH(m_orders, if(m_orders[i].IsOpen()) { m_ordersTotal += class="num">1; }) } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Event handler for opening a class="kw">virtual position(order) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualStrategy::OnOpen() { CountOrders(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Event handler for closing a class="kw">virtual position(order) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualStrategy::OnClose() { CountOrders(); } class="macro">#include "Advisor.mqh" class="macro">#include "VirtualReceiver.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class of the EA handling class="kw">virtual positions(orders) | class=class="str">"cmt">//+------------------------------------------------------------------+ class CVirtualAdvisor : class="kw">public CAdvisor {