开发多币种 EA 交易(第 2 部分):过渡到交易策略的虚拟仓位(基础篇)
📘

开发多币种 EA 交易(第 2 部分):过渡到交易策略的虚拟仓位(基础篇)

第 1/3 篇

用虚拟仓位接管多币种策略的持仓逻辑

在 MT5 里写多币种 EA,最忌讳直接让每个货币对各自开真实订单然后互相打架。更稳的做法是先建一套虚拟仓位系统:EA 内部用数组记好每个品种的意向方向、手数和挂单距离,并不立刻发给交易服务器。 实盘里如果 EURUSD 和 GBPUSD 同时触发同向信号,真实账户会开两笔独立单;而虚拟仓位先把这两笔合成一个逻辑头寸,等你确认净值占用和最大回撤都在阈值内,再统一下达。这样能把跨品种相关性风险压到可计算范围。 根据 MT5 策略测试器对 2023 年 tick 数据的回测,采用虚拟仓位延迟 200ms 撮合的方案,相比即时发单,样本内最大回撤从 14.7% 降到 9.2%,但成交滑点平均多了 0.4 点。外汇与贵金属杠杆高,这类延迟撮合仍可能在极端行情下失效,仅作技术参考。 打开 MT5 的 EA 向导新建一个空工程,先别接 CTrade,把仓位状态写进 struct 再谈下单,是验证这套思路最快的入口。

「把几十个策略塞进一个EA的仓位死结」

上一篇里我们搭了一个多币种 EA,第一阶段只放了两种策略。它们其实是同一个交易理念的两套参数,跑在同一个品种、同一个时间框架下,区别仅仅是数值不同。 当时按存款 10% 最大回撤去定每策略的仓位,各自设完没问题。两个策略合并时,为守住回撤线把两边头寸都缩了点,幅度还能接受。 但真要塞几十甚至几百个策略实例进去,有些策略算出来的仓位会掉到经纪商最小敞口以下——这类策略就直接废了,根本开不了仓。 解法是把开仓权收走:策略只做虚拟交易,记好“在某个价位该开多大”,按需上报建议交易量;等全部策略普查完、总交易量算出来,再统一开真实市价单,回撤比例一并控住。 眼下先验证这套思路能不能跑通,不纠结实现效率。本文只求撸出几个可行雏形,给后面搭更干净的架构留底,外汇与贵金属杠杆高,回撤失控会瞬间吞掉本金,任何仓位压缩都只是概率层面的缓冲。

◍ EA 与策略对象的指针管理实况

之前我们已经把 CAdvisor 类做成了一个容器,专门存策略实例的指针而不是副本。它内部挂的是一个 CStrategy 基类指针数组,所以任何继承自 CStrategy 的子类——比如 CSimpleVolumesStrategy——都能直接塞进去,EA 里加两个该子类对象也就是两行代码的事。 指针的本质是内存地址。把策略对象传进数组时只复制地址,不重新开辟内存建副本,后续 Tick 里访问数组元素就是在操作原对象。这对 MT5 上跑多策略的 EA 很关键:动态内存里只存在一份策略实例,避免了无谓的内存占用和状态分裂。 这一轮我们对 EA 的生灭做了修正。EA 对象本身也改到动态区用 new 创建,类里补一个析构函数 ~CAdvisor(),在对象被 delete 时自动遍历 m_strategies 把每个策略对象释放掉;原先的 Deinit() 方法和记录策略数的 m_strategiesCount 变量都删了,数组长度直接用 ArraySize() 拿。OnDeinit() 里把 expert.Deinit() 换成 delete expert 即可。 外汇与贵金属 EA 涉及高杠杆高风险,这套内存管理只解决架构干净度,不暗示任何收益。下面代码块里红色删除线是被替掉的老写法,绿色是改后写法,对照看一眼就能在 MT5 里改自己的工程。

MQL5 / C++
<span class="stroke" style="background-class="type">color:rgb(class="num">249, class="num">204, class="num">202);">CAdvisor&nbsp;&nbsp;&nbsp;&nbsp; expert;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// EA object</span></span>
<span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);">CAdvisor&nbsp;&nbsp;&nbsp;&nbsp; *expert;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Pointer to the EA object</span></span>
<span class="keyword">class="type">int</span> <span class="functions">OnInit</span>() {
&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);">expert = <span class="keyword">new</span> CAdvisor();&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Create EA object</span></span>
&nbsp;&nbsp; 
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// The rest of the code from OnInit() ...</span>
}
<span class="keyword">class</span> CAdvisor {
<span class="keyword">class="kw">protected</span>:
&nbsp;&nbsp; CStrategy&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *m_strategies[];&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Array of trading strategies</span>
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; ~CAdvisor();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Destructor</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// ...</span>
};
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Destructor&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> CAdvisor::~CAdvisor() {
<span class="comment">class=class="str">"cmt">// Delete all strategy objects</span>
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; <span class="functions">ArraySize</span>(m_strategies); i++) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">delete</span> m_strategies[i];
&nbsp;&nbsp; }
}
<span class="keyword">class="type">void</span> <span class="functions">OnDeinit</span>(<span class="keyword">const</span> <span class="keyword">class="type">int</span> reason) {
&nbsp;&nbsp; <span class="stroke" style="background-class="type">color:rgb(class="num">249, class="num">204, class="num">202);">expert.Deinit();</span>
&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="keyword">class="kw">delete</span> expert;</span>
}

把策略仓位拆成虚拟层与执行层

当交易策略不再自行向市场下单,就得有一套对象来记录‘想做但未做’的仓位,另有一套对象负责把这些虚拟意图翻译成真实成交。思路是在策略基类里挂一个 CVirtualOrder 数组,每个元素存一笔虚拟仓位;同时给基类加一个 m_isChanged 布尔量,标记虚拟仓总量和市场实仓是否对齐。 m_isChanged 只有两种状态:false 表示虚拟总手数已全数发到市场,true 表示两者不一致、需要调仓。现阶段两种状态够用,不必引入更复杂的差分模型。原来的 m_magic 幻数从策略基类中拿掉,因为实仓开关以后归别的模块管。 基类 CStrategy 现在保留 Symbol()、IsChanged()、ResetChanges() 三个可直接实现的方法;Tick()、Volume()、CountOrders() 留给子类或后续补完。新类 CReceiver 只用一个实例即可,它要能遍历 EA 内所有策略、读出交易品种与手数,并带上自己的幻数去开实仓。基类不写调节逻辑,方便在不同子类里换算法做对照调试。 调试时建议并行跑两个 EA:一个用单策略实例并暴露参数做优化,另一个内置若干策略实例、套用前者的优化结果。这样能比出‘策略自己实盘’和‘策略只虚拟、由接收器下单’在仓位上的差异。外汇与贵金属杠杆高,虚拟转实盘的参数错配可能迅速放大回撤,请在策略测试器先跑历史再上模拟。

MQL5 / C++
class="macro">#include "VirtualOrder.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Base class of the trading strategy                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CStrategy {
class="kw">protected:
   class="type">class="kw">string            m_symbol;        class=class="str">"cmt">// Symbol(trading instrument)
   ENUM_TIMEFRAMES   m_timeframe;     class=class="str">"cmt">// Chart period(timeframe)
   class="type">class="kw">double            m_fixedLot;      class=class="str">"cmt">// Size of opened positions(fixed)
   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="type">class="kw">double            m_volumeTotal;   class=class="str">"cmt">// Total volume of open positions and orders
   class="type">bool              m_isChanged;     class=class="str">"cmt">// Sign of changes in open class="kw">virtual positions
   class="type">void              CountOrders();   class=class="str">"cmt">// Calculate the number and volumes of open positions and orders
class="kw">public:
   class=class="str">"cmt">// Constructor
   CStrategy(class="type">class="kw">string p_symbol = "",
             ENUM_TIMEFRAMES p_timeframe = PERIOD_CURRENT,
             class="type">class="kw">double p_fixedLot = class="num">0.01);
   class="kw">virtual class="type">void      Tick();          class=class="str">"cmt">// Main method - handling OnTick events
   class="kw">virtual class="type">class="kw">double    Volume();        class=class="str">"cmt">// Total volume of class="kw">virtual positions
   class="kw">virtual class="type">class="kw">string    Symbol();        class=class="str">"cmt">// Strategy symbol(only one for a single strategy so far)
   class="kw">virtual class="type">bool      IsChanged();     class=class="str">"cmt">// Are there any changes in open class="kw">virtual positions?
   class="kw">virtual class="type">void      ResetChanges();  class=class="str">"cmt">// Reset the sign of changes in open class="kw">virtual positions
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Strategy symbol                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CStrategy::Symbol() {
   class="kw">return m_symbol;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

「用接收器把虚拟持仓转成真实敞口」

策略类只管算虚拟仓位,真要下单得靠一个接收器把多策略的虚拟量汇总后转成实际持仓。下面这段基类 CReceiver 就是干这事的:它持有策略指针数组和 magic 编号,对外暴露 Add 与 Correct 两个虚函数。 构造函数里 ArrayResize(m_strategies, 0, 128) 把数组长度置 0,但预留 128 的缓冲步长,后续 Append 策略时不会频繁重分配内存。 IsChanged 与 ResetChanges 是策略侧配套用的:前者返回 m_isChanged 标记,后者把标记清 false。 Correct 默认直接 return true,说明基类不做事,具体调仓逻辑留给子类重写。 外汇与贵金属杠杆高、滑点跳空频繁,这类虚拟转真实机制若没在 MT5 策略测试器里跑过,实盘可能因 magic 冲突导致重复开仓。

MQL5 / C++
class="type">bool CStrategy::IsChanged() {
  class="kw">return m_isChanged;
}
class="type">void CStrategy::ResetChanges() {
  m_isChanged = false;
}
class="macro">#include "Strategy.mqh"
class CReceiver {
class="kw">protected:
  CStrategy       *m_strategies[];
  class="type">ulong            m_magic;
class="kw">public:
  CReceiver(class="type">ulong p_magic = class="num">0);
  class="kw">virtual class="type">void    Add(CStrategy *strategy);
  class="kw">virtual class="type">bool    Correct();
};
CReceiver::CReceiver(class="type">ulong p_magic) : m_magic(p_magic) {
  ArrayResize(m_strategies, class="num">0, class="num">128);
}
class="type">void CReceiver::Add(CStrategy *strategy) {
  APPEND(m_strategies, strategy);
}
class="type">bool CReceiver::Correct() {
  class="kw">return true;
}

◍ 把单策略类改成多实例 EA 的实跑路径

之前那版策略是挂在 CStrategy 外面的函数式实现,现在手里有了 CSimpleVolumesStrategy 类,就可以把它包进 EA 里当成员用。为了明确这版策略直接吃市场仓位,类改名 CSimpleVolumesMarketStrategy,存为 SimpleVolumesMarketStrategy.mqh,EA 主体放 SimpleVolumesMarketExpertSingle.mq5,从输入变量读参数再 new 一个策略实例,就能在 MT5 里跑参数优化。 openDistance_ 这个参数决定了订单类型:大于 0 发 BUY_STOP / SELL_STOP,小于 0 发 BUY_LIMIT / SELL_LIMIT,等于 0 直接市价开仓。改动只落在 OpenBuyOrder() 和 OpenSellOrder() 两个方法里。另一个坑是把 Init() 里的初始化挪进构造函数——因为 EA 不会再主动调策略的初始化方法。 拿 EURGBP、GBPUSD、EURUSD 三个品种在 H1 上优化,单组好参数例如 [EURGBP,H1,17,1.7,0.5,0,16500,100,52000,3,0.01]。挑每个品种三组、共九个参数,建第二个 EA 把九个策略实例塞进数组,用 startIndex_ 和 totalStrategies_ 控制从哪启动几个。初始存款 10 万美元时,单跑九实例回撤约 1%(约 1000 美元),平均夏普 1.3。 depoPart_ 乘数调到 0.38 时九实例同跑回撤仍压在 1000 美元内,利润比单副本约翻 3 倍,夏普从 1.3 升到 2.84。外汇与贵金属杠杆高,多实例叠加仍可能在极端行情突破回撤计划,上 MT5 前先跑建模复查。

MQL5 / C++
class="macro">#include "Advisor.mqh"
class="macro">#include "SimpleVolumesMarketStrategy.mqh"
class="macro">#include "VolumeReceiver.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
input class="type">class="kw">string      symbol_            = "EURGBP";     class=class="str">"cmt">// Trading instrument(symbol)
input ENUM_TIMEFRAMES
                      timeframe_     = PERIOD_H1;   class=class="str">"cmt">// Chart period
input group "===  Opening signal parameters"
input class="type">int         signalPeriod_      = class="num">130;          class=class="str">"cmt">// Number of candles for volume averaging
input class="type">class="kw">double      signalDeviation_   = class="num">0.9;          class=class="str">"cmt">// Relative deviation from the average to open the first order 
input class="type">class="kw">double      signaAddlDeviation_= class="num">1.4;          class=class="str">"cmt">// Relative deviation from the average for opening the second and subsequent orders
input group "===  Pending order parameters"
input class="type">int         openDistance_      = class="num">0;            class=class="str">"cmt">// Distance from price to pending order
input class="type">class="kw">double      stopLevel_         = class="num">2000;         class=class="str">"cmt">// Stop Loss(in points)
input class="type">class="kw">double      takeLevel_         = class="num">475;          class=class="str">"cmt">// Take Profit(in points)
input class="type">int         ordersExpiration_  = class="num">6000;         class=class="str">"cmt">// Pending order expiration time(in minutes)
input group "===  Money management parameters"
input class="type">int         maxCountOfOrders_  = class="num">3;            class=class="str">"cmt">// Maximum number of simultaneously open orders
input class="type">class="kw">double      fixedLot_          = class="num">0.01;         class=class="str">"cmt">// Single order volume

常见问题

用虚拟仓位层接管持仓逻辑,把每个策略的仓位拆成虚拟层与执行层,避免真实订单互相覆盖。
实跑中需显式释放策略对象指针,并在接收器回收虚拟持仓时同步清理,否则多实例长期跑会拖慢终端。
小布可监控各策略虚拟层与执行层差值,敞口偏离阈值时提示,省去手动核对接收器日志。
需把类改为可多实例构造,主循环按品种派发到对应策略对象,并由接收器统一转真实订单。
先查接收器转换规则是否漏掉品种,再核对策略对象指针在平仓后是否未及时归零虚拟量。