开发多币种 EA 交易(第 3 部分):架构修改·进阶篇
🧩

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

(2/3)· 当多个策略实例挤在同一账户,最小手数与反向仓位的坑怎么填

新手友好 第 2/3 篇
把现成策略直接塞进多币种EA,最常翻车的是手数被砍到不够最小开仓,整组策略直接哑火。还有反向同量仓位对开,表面净敞口为零,掉期费却照扣不误。本篇要动的架构,正是冲这两个暗雷去的。

◍ 虚拟顾问的收发骨架怎么搭

在 MT5 里做虚拟持仓回测,核心是把「策略逻辑」和「下单通道」拆开。CVirtualAdvisor 这个类就是这么干的:protected 区挂了一个 CVirtualReceiver 指针 m_receiver,专门负责把虚拟仓位送到市场模拟层;public 区只暴露构造、析构和 Tick 重写,对外接口极薄。 构造时默认传 ulong p_magic = 1,意思是魔数写死为 1,直接调 CVirtualReceiver::Instance(p_magic) 拿静态单例。这样多顾问并行时,靠不同 magic 隔离接收器,不会串仓。 析构里只有一句 delete m_receiver,把接收器实例释放掉;Tick() 才是每根报价进来后的主循环——先 m_receiver.Tick() 处理虚拟仓位状态,再 CAdvisor::Tick() 跑策略本身,最后 m_receiver.Correct() 做市场成交量修正。三者顺序不能乱,否则仓位和信号会对不上。 外汇与贵金属杠杆高、滑点随机,这套虚拟层只解决回测隔离,不预示实盘收益,复制去 MT5 编译时先把 magic 改成你账号不冲突的值。

MQL5 / C++
class="kw">protected:
  CVirtualReceiver  *m_receiver; class=class="str">"cmt">// Receiver object that brings positions to the market
class="kw">public:
                CVirtualAdvisor(class="type">ulong p_magic = class="num">1); class=class="str">"cmt">// Constructor
                ~CVirtualAdvisor();                class=class="str">"cmt">// Destructor
  class="kw">virtual class="type">void      Tick() class="kw">override;               class=class="str">"cmt">// OnTick event handler
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(class="type">ulong p_magic = class="num">1) :
class=class="str">"cmt">// Initialize the receiver with a class="kw">static receiver
  m_receiver(CVirtualReceiver::Instance(p_magic)) {};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destructor                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualAdvisor::~CVirtualAdvisor() {
  class="kw">delete m_receiver;     class=class="str">"cmt">// Remove the recipient
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| OnTick event handler                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualAdvisor::Tick(class="type">void) {
class=class="str">"cmt">// Receiver handles class="kw">virtual positions
  m_receiver.Tick();

class=class="str">"cmt">// Start handling in strategies
  CAdvisor::Tick();

class=class="str">"cmt">// Adjusting market volumes
  m_receiver.Correct();
}
class=class="str">"cmt">//+------------------------------------------------------------------+

「给虚拟仓位接上接收器和策略的指针」

虚拟仓位类现在需要持有指向 m_receiver 和 m_strategy 两个对象的指针,这两个字段不能在类内部凭空生成,只能在构造时由外部传入。构造函数因此改成了带 CVirtualReceiver* 和 CVirtualStrategy* 参数的形式,初始化列表里补了 m_receiver(p_receiver) 和 m_strategy(p_strategy) 两行,把外部对象的地址接进来。 为了方便后续排查和日志输出,类里加了 Id() 和 Symbol() 两个读取器,分别返回 m_id 和 m_symbol。其中 m_id 来自静态计数器 s_count 自增后的值,也就是每新建一个虚拟仓位对象,ID 就比之前多 1,这个细节在回测多仓位并发时很有用。 通知逻辑只挂在 Open() 和 Close() 上。开仓成功后会调用 m_receiver.OnOpen(GetPointer(this)) 把当前对象指针抛给接收器,同时 m_strategy.OnOpen() 触发策略侧动作;策略处理函数暂时不吃参数,所以实现里直接空参调用。外汇与贵金属市场高杠杆、滑点随机,这类虚拟撮合仅用于历史行情回放,实盘信号仍需独立验证。 代码继续留在 VirtualOrder.mqh 同名文件里,MT5 里 include 进来就能直接编译跑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class of class="kw">virtual orders and positions                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CVirtualOrder {
class="kw">private:
class=class="str">"cmt">//--- Static fields...

class=class="str">"cmt">//--- Related recipient objects and strategies
  CVirtualReceiver   *m_receiver;
  CVirtualStrategy   *m_strategy;
class=class="str">"cmt">//--- Order(position) properties ...

class=class="str">"cmt">//--- Closed order(position) properties ...

class=class="str">"cmt">//--- Private methods

class="kw">public:
                     CVirtualOrder(
  CVirtualReceiver *p_receiver,
  CVirtualStrategy *p_strategy
  );                                      class=class="str">"cmt">// Constructor
class=class="str">"cmt">//--- Methods for checking the position(order) status ...

class=class="str">"cmt">//--- Methods for receiving position(order) properties ...
  class="type">ulong               Id() {              class=class="str">"cmt">// ID
    class="kw">return m_id;
  }
  class="type">class="kw">string              Symbol() {          class=class="str">"cmt">// Symbol
    class="kw">return m_symbol;
  }
class=class="str">"cmt">//--- Methods for handling positions(orders) ...

};
CVirtualOrder::CVirtualOrder(CVirtualReceiver *p_receiver, CVirtualStrategy *p_strategy) :
class=class="str">"cmt">// Initialization list
  m_id(++s_count),  class=class="str">"cmt">// New ID = object counter + class="num">1
  m_receiver(p_receiver),
  m_strategy(p_strategy),
  ...,
  m_point(class="num">0) {
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Open a class="kw">virtual position                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualOrder::Open(...) {
  class=class="str">"cmt">// If the position is already open, then do nothing ...
  if(s_symbolInfo.Name(symbol)) {  class=class="str">"cmt">// Select the desired symbol
    class=class="str">"cmt">// Update information about current prices ...
    class=class="str">"cmt">// Initialize position properties ...

    class=class="str">"cmt">// Depending on the direction, set the opening price, as well as the SL and TP levels ...

    class=class="str">"cmt">// Notify the recipient and the strategy that the position(order) is open
    m_receiver.OnOpen(GetPointer(this));
    m_strategy.OnOpen();
     ...
    class="kw">return true;
  }
  class="kw">return false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Close a position                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualOrder::Close() {
  if(IsOpen()) { class=class="str">"cmt">// If the position is open
    ...
    class=class="str">"cmt">// Define the closure reason to be displayed in the log ...

平仓后把控制权交还策略与接收端

在持仓或挂单被平掉之后,EA 不能闷头自己处理完就结束,得把事件显式通知出去。上面这段把收盘价按类型存好,随后立即调用接收端与策略的 OnClose 接口,让外部逻辑知道这一笔已经收尾。 m_receiver.OnClose(GetPointer(this)) 把当前 CTrade 派生对象的指针传给接收器,接收器据此做日志、统计或推送;m_strategy.OnClose() 则清空策略层对该笔持仓的状态标记。漏掉这两句,策略可能还以为仓在,下一次信号判断就会出错。 在 MT5 里实跑时,可在 OnClose 里打印 PositionGetInteger(POSITION_TICKET) 是否为 0,确认平仓事件确实已传导,而不是只停在本地变量层。外汇与贵金属杠杆高,事件漏通知会造成重复开仓,回测看不出、实盘才爆。

MQL5 / C++
class=class="str">"cmt">// Save the close price depending on the type ...

class=class="str">"cmt">// Notify the recipient and the strategy that the position(order) is open
m_receiver.OnClose(GetPointer(this));
m_strategy.OnClose();
 }

◍ 用单件模式锁住虚拟接收器实例

把 CVirtualReceiver 做成 Singleton,核心就三件事:构造函数私有化、加一个 static 指针 s_instance 存唯一对象、用 static 方法 Instance() 在空的时候 new 一个、非空直接返指针。这样整个 EA 里虚拟仓位转实盘的中枢只有一份,不会出现多个接收器互相覆盖仓位状态。 每个策略实例通过静态 Get() 向接收器要 N 个虚拟仓位,接收器在 m_orders 数组里挂好指针,同时也填进策略自己的虚拟仓位数组。注意 Get() 的入参 n 就是策略需要的虚拟仓位数量,调的时候数错了,后面 Correct() 会漏调。 接收器还持有一组交易品种接收器指针(CVirtualSymbolReceiver,尚未实现但已知职责是按品种直开直平)。Correct() 在接收器这层只是个壳:循环调每个品种接收器的 Correct(),真正干活在下层。 为了减少无谓交易触发,加 IsTradeAllowed() 查终端和账户状态,并用 m_isChanged 标记虚拟仓位有无变动。只有标记为真才进 Correct(),调成功后清标记。标记设置在 CVirtualOrder 的 OnOpen()/OnClose() 里——开平虚拟仓位即置位,同时首次开某品种虚拟仓时要现建品种接收器并通知,后续只通知已有接收器。 虚拟仓位除了信号开平,还能在 Tick() 里撞止损止盈自平。CVirtualReceiver::Tick() 负责每个分时遍历 m_orders 全调一遍。最后析构函数必须释放 m_orders 里所有 new 出来的对象,否则 MT5 跑久了内存泄漏会让 EA 变卡甚至崩。把上述代码存成 VirtualReceiver.mqh 即可在 MT5 里 include 验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class for converting open volumes to market positions(receiver) |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CVirtualReceiver : class="kw">public CReceiver {
class="kw">protected:
class=class="str">"cmt">// Static pointer to a single class instance
  class="kw">static  CVirtualReceiver *s_instance;
  ...
  CVirtualReceiver(class="type">ulong p_magic = class="num">0);    class=class="str">"cmt">// Private constructor
class="kw">public:
class=class="str">"cmt">//--- Static methods
  class="kw">static
  CVirtualReceiver  *Instance(class="type">ulong p_magic = class="num">0);      class=class="str">"cmt">// Singleton - creating and getting a single instance
...
};
class=class="str">"cmt">// Initializing a class="kw">static pointer to a single class instance
CVirtualReceiver *CVirtualReceiver::s_instance = NULL;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Singleton - creating and getting a single instance              |
class=class="str">"cmt">//+------------------------------------------------------------------+
CVirtualReceiver* CVirtualReceiver::Instance(class="type">ulong p_magic = class="num">0) {
  if(!s_instance) {
    s_instance = new CVirtualReceiver(p_magic);
  }
  class="kw">return s_instance;
}
class CVirtualReceiver : class="kw">public CReceiver {
class="kw">protected:
  ...
  CVirtualOrder      *m_orders[];          class=class="str">"cmt">// Array of class="kw">virtual positions
  ...
class="kw">public:
class=class="str">"cmt">//--- Static methods
  ...
  class="kw">static class="type">void        Get(CVirtualStrategy *strategy,
                         CVirtualOrder *&orders[],
                         class="type">int n); class=class="str">"cmt">// Allocate the necessary amount of class="kw">virtual positions to the strategy
  ...
};
...
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Allocate the necessary amount of class="kw">virtual positions to strategy  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">static class="type">void CVirtualReceiver::Get(CVirtualStrategy *strategy,   class=class="str">"cmt">// Strategy
                                 CVirtualOrder *&orders[],     class=class="str">"cmt">// Array of strategy positions
                                 class="type">int n                         class=class="str">"cmt">// Required number
                                 ) {

「虚拟持仓的接收与调仓闸门」

在 MT5 的 EA 框架里,虚拟持仓系统靠一个 Receiver 单例来集中接管开平信号。下面这段初始化把订单数组扩容后,逐个 new 出 CVirtualOrder 对象并挂到 self.m_orders,相当于在内存里先铺好一批待撮合的仓位壳子。 CVirtualReceiver *self = Instance(); // Receiver singleton ArrayResize(orders, n); // Expand the array of virtual positions FOREACH(orders, orders[i] = new CVirtualOrder(self, strategy); // Fill the array with new objects APPEND(self.m_orders, orders[i])) // Register the created virtual position ... Correct() 是调仓总闸:仅当 m_isChanged 为真且 IsTradeAllowed() 通过时才遍历各品种接收器做音量修正,跑完把 m_isChanged 翻成 !res。也就是说,若任一手符号级 Correct 返回 false,本次调整视为失败且不清除变更标记,下一 tick 还会再触发。 bool CVirtualReceiver::Correct() { bool res = true; if(m_isChanged && IsTradeAllowed()) { FOREACH(m_symbolReceivers, res &= m_symbolReceivers[i].Correct()); m_isChanged = !res; } return res; } IsTradeAllowed() 同时查了 5 个环境位:MQL_TRADE_ALLOWED、TERMINAL_TRADE_ALLOWED、ACCOUNT_TRADE_EXPERT、ACCOUNT_TRADE_ALLOWED、TERMINAL_CONNECTED。少一个为真,虚拟单就不会下发——实盘外汇/贵金属波动大、断线常见,这个多重校验能避免重连瞬间乱补单。 bool CVirtualReceiver::IsTradeAllowed() { return (true && MQLInfoInteger(MQL_TRADE_ALLOWED) && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && AccountInfoInteger(ACCOUNT_TRADE_EXPERT) && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED) && TerminalInfoInteger(TERMINAL_CONNECTED) ); } 开平事件由 OnOpen / OnClose 两个 public 方法承接,外部策略只需把 CVirtualOrder* 抛进来,Receiver 自己决定怎么落进符号级队列。想验证的话,在 MT5 里给 Correct() 加 Print(res) 跑一晚,大概率能看到断线期间 res 连续为 false 的日志。

MQL5 / C++
CVirtualReceiver *self = Instance();              class=class="str">"cmt">// Receiver singleton
ArrayResize(orders, n);                          class=class="str">"cmt">// Expand the array of class="kw">virtual positions
FOREACH(orders,
  orders[i] = new CVirtualOrder(self, strategy); class=class="str">"cmt">// Fill the array with new objects
  APPEND(self.m_orders, orders[i]))              class=class="str">"cmt">// Register the created class="kw">virtual position
...
class CVirtualReceiver : class="kw">public CReceiver {
class="kw">protected:
  ...
  CVirtualSymbolReceiver  *m_symbolReceivers[];     class=class="str">"cmt">// Array of recipients for individual symbols
class="kw">public:
  ...
class=class="str">"cmt">//--- Public methods
  class="kw">virtual class="type">bool      Correct() class="kw">override;            class=class="str">"cmt">// Adjustment of open volumes
};
class CVirtualReceiver : class="kw">public CReceiver {
...
  class="type">bool              m_isChanged;                   class=class="str">"cmt">// Are there any changes in open positions?
...
  class="type">bool              IsTradeAllowed();              class=class="str">"cmt">// Is trading available?
class="kw">public:
  ...
  class="kw">virtual class="type">bool      Correct() class="kw">override;            class=class="str">"cmt">// Adjustment of open volumes
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Adjust open volumes                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualReceiver::Correct() {
  class="type">bool res = true;
  if(m_isChanged && IsTradeAllowed()) {
    class=class="str">"cmt">// If there are changes, then we call the adjustment of the recipients of individual symbols
    FOREACH(m_symbolReceivers, res &= m_symbolReceivers[i].Correct());
    m_isChanged = !res;
  }
  class="kw">return res;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Is trading available?                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualReceiver::IsTradeAllowed() {
  class="kw">return (true
    && MQLInfoInteger(MQL_TRADE_ALLOWED)
    && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)
    && AccountInfoInteger(ACCOUNT_TRADE_EXPERT)
    && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)
    && TerminalInfoInteger(TERMINAL_CONNECTED)
    );
}
class CVirtualReceiver : class="kw">public CReceiver {
  ...
class="kw">public:
  ...
class=class="str">"cmt">//--- Public methods
  class="type">void      OnOpen(CVirtualOrder *p_order);        class=class="str">"cmt">// Handle class="kw">virtual position opening
  class="type">void      OnClose(CVirtualOrder *p_order);       class=class="str">"cmt">// Handle class="kw">virtual position closing
  ...
};

虚拟持仓的开关与心跳怎么接

虚拟订单接收器把开仓、平仓、逐笔心跳三类事件拆开处理,核心是把订单按品种路由到对应的子接收器。开仓时若品种接收器不存在就现场 new 一个再追加进数组,存在则直接复用,避免重复实例化。 平仓逻辑更轻:只在品种接收器已存在时转发关闭通知,找不到就静默跳过,不补建接收器。两个分支都把 m_isChanged 置真,供上层判断是否需要重绘或推送到「小布盯盘」。 Tick() 用 FOREACH 把行情脉冲逐条灌给 m_orders 里的每个虚拟仓位;析构函数则反向清空,delete 所有残留虚拟单,防止 EA 重载时内存泄漏。外汇与贵金属杠杆高,虚拟回测和实盘滑点可能偏差明显,参数请在 MT5 策略测试器先跑一轮再上真仓。 下面这段可直接粘进你的 virtual_receiver.mqh 对照改:

MQL5 / C++
class=class="str">"cmt">//| Handle opening a class="kw">virtual position                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) {
   class="type">class="kw">string symbol = p_order.Symbol();           class=class="str">"cmt">// 取当前订单的交易品种
   CVirtualSymbolReceiver *symbolReceiver;
   class="type">int i;
   FIND(m_symbolReceivers, symbol, i);         class=class="str">"cmt">// 在品种接收器数组里找该品种
   if(i == -class="num">1) {
      class=class="str">"cmt">// 没找到就新建一个该品种的接收器
      symbolReceiver = new CVirtualSymbolReceiver(m_magic, symbol);
      class=class="str">"cmt">// 把它塞进品种接收器数组
      APPEND(m_symbolReceivers, symbolReceiver);
   } else {
      class=class="str">"cmt">// 找到了就直接用现成的
      symbolReceiver = m_symbolReceivers[i];
   }

   symbolReceiver.Open(p_order); class=class="str">"cmt">// 通知品种接收器:新仓位来了
   m_isChanged = true;           class=class="str">"cmt">// 标记状态有变动
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Handle closing a class="kw">virtual position                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualReceiver::OnClose(CVirtualOrder *p_order) {
   class="type">class="kw">string symbol = p_order.Symbol();   class=class="str">"cmt">// 取平仓单的品种
   class="type">int i;
   FIND(m_symbolReceivers, symbol, i); class=class="str">"cmt">// 找品种接收器
   if(i != -class="num">1) {
      m_symbolReceivers[i].Close(p_order);  class=class="str">"cmt">// 通知对应接收器平仓
      m_isChanged = true;                   class=class="str">"cmt">// 标记有变动
   }
}
class CVirtualReceiver : class="kw">public CReceiver {
   ...
class="kw">public:
   ...
class=class="str">"cmt">//--- Public methods
   class="type">void              Tick();   class=class="str">"cmt">// 处理虚拟订单数组的每次报价
   ...
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Handle a tick for the array of class="kw">virtual orders(positions)               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualReceiver::Tick() {
   FOREACH(m_orders, m_orders[i].Tick()); class=class="str">"cmt">// 每个虚拟单都吃一次Tick
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destructor                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CVirtualReceiver::~CVirtualReceiver() {
   FOREACH(m_orders, class="kw">delete m_orders[i]); class=class="str">"cmt">// 清掉所有虚拟仓位
}
把多策略诊断交给小布
这些架构层面的冲突点,小布盯盘的AIGC已内置扫描,打开对应品种页就能看到并行策略的净敞口与掉期损耗提示,你只管判断要不要重构。

常见问题

因为公开仓位最小规模固定,总资金被多个实例分摊后,单个策略可分配手数可能低于 broker 最小限制,从而被排除。概率上并行实例越多或存款越小越易触发。
虚拟仓位仅记录策略意图与体量,不直接下单;真实开仓由接收器统一执行,以此规避策略各自开仓带来的冲突与最小手数问题。
可以,小布盯盘的品种页会呈现并行策略净敞口与反向仓位的库存费累积,帮助你在重构前先量化冲突规模。
把市场仓位操作从策略剥离,新增虚拟仓位与策略交易量接收器,由接收器集中处理真实订单,缓解手数稀释与对立仓问题。
负责汇总各策略虚拟仓位请求、按规则映射到真实订单、管理手数缩放与冲突合并,具体实现见本篇第4节。