开发多币种 EA 交易(第 3 部分):架构修改(基础篇)
📘

开发多币种 EA 交易(第 3 部分):架构修改(基础篇)

第 1/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 验证继承链。

MQL5 / C++
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 前先想清这块偏差。

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

常见问题

优先抽离币种相关的硬编码,把行情获取和订单操作收口到统一接口层,再让主循环按币种列表遍历。
先平行复制多份单币逻辑,再做聚合器统一调度,最后把仓位收口到虚拟层、基类瘦身为纯骨架。
小布可以读取你的EA结构描述,标出仍未虚拟化的订单操作和冗余基类方法,给出重构优先级。
聚合器维护币种数组,每根K线按序调用各顾问实例的OnTick,虚拟层汇总仓位后统一下发真实订单。
顾问类只发虚拟开平仓信号给虚拟层,由虚拟层换算成真实账户动作,避免每个币种直连交易接口。