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

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

(2/3)· 当策略数冲到上百,单个仓位被压到经纪商下限之下怎么办?

案例拆解新手友好 第 2/3 篇
把十几个策略直接堆进一个 EA,回撤红线立刻逼你砍仓位,很多实例根本开不了单。以为少加几个品种就安全,其实策略一多,最小手数门槛就能让半数逻辑空转。本篇用虚拟仓位把开单权收归总控,先解决‘能跑’再谈‘跑得好’。

◍ EA 骨架里挂策略实例的写法

MT5 里把具体策略挂进顾问对象,核心就在 OnInit 里 new 一个 CAdvisor,再 Add 一个策略实例。下面这段把成交量策略 CSimpleVolumesMarketStrategy 塞进去,magic 写死 27181,外部输入组标了『=== EA parameters』,方便在 EA 属性面板改参。 OnTick 只做一件事:expert.Tick(),把所有策略的行情响应收口到顾问层;OnDeinit 里 delete expert 释放指针,避免反复加载 EA 时内存泄漏。 开仓分支看 openDistance_ 的符号:大于 0 挂 BuyStop,小于 0 挂 BuyLimit,等于 0 直接市价 Buy。这个字段决定你是突破追单还是回踩接单,外汇和贵金属波动大,挂单距离设错可能频繁触发或永远不触发,属高风险操作。 另有一套并行输入:startIndex_=0、totalStrategies_=1、depoPart_=1.0、magic_=27182,说明同一顾问可以 startIndex 偏移、多策略复用保证金比例,magic 换号隔离订单。复制代码前先把两个 magic 号理清楚,不然历史单会混。

MQL5 / C++
input group "===  EA parameters"
input class="type">ulong       magic_              = class="num">27181; class=class="str">"cmt">// Magic
CAdvisor     *expert;         class=class="str">"cmt">// Pointer to the EA object
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
   expert = new CAdvisor();
   expert.Add(new CSimpleVolumesMarketStrategy(
                 magic_, symbol_, timeframe_,
                 fixedLot_,
                 signalPeriod_, signalDeviation_, signaAddlDeviation_,
                 openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                 maxCountOfOrders_)
             ); class=class="str">"cmt">// Add one strategy instance
   class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick() {
   expert.Tick();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason) {
   class="kw">delete expert;
}
class="type">void CSimpleVolumesMarketStrategy::OpenBuyOrder() {
class=class="str">"cmt">// Previous code in the method ...
class=class="str">"cmt">// Order volume
   class="type">class="kw">double lot = m_fixedLot;
class=class="str">"cmt">// Set a pending order
   class="type">bool res = false;
   if(openDistance_ > class="num">0) {
      res = trade.BuyStop(lot, ...);
&nbsp;&nbsp; } else if(openDistance_ < class="num">0) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;res = trade.BuyLimit(lot, ...);
&nbsp;&nbsp; } else {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;res = trade.Buy(lot, ...);
&nbsp;&nbsp; }
&nbsp;&nbsp; if(!res) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Print("Error opening order");
&nbsp;&nbsp; }
}
class="macro">#include "Advisor.mqh"
class="macro">#include "SimpleVolumesMarketStrategy.mqh"
input class="type">int startIndex_&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= class="num">0;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class=class="str">"cmt">// Starting index
input class="type">int totalStrategies_ = class="num">1;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class=class="str">"cmt">// Number of strategies
input class="type">class="kw">double depoPart_&nbsp;&nbsp;&nbsp;&nbsp; = class="num">1.0;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class=class="str">"cmt">// Part of the deposit for one strategy
input class="type">ulong&nbsp;&nbsp;magic_&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= class="num">27182;&nbsp;&nbsp;&nbsp;&nbsp; class=class="str">"cmt">// Magic
CAdvisor&nbsp;&nbsp;&nbsp;&nbsp; *expert;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class=class="str">"cmt">// EA object
class=class="str">"cmt">//+------------------------------------------------------------------+

初始化里把九套策略实例铺开

EA 启动阶段先在 OnInit 里做参数护栏:startIndex_ 不能为负,且 startIndex_ + totalStrategies_ 必须落在 0~9 区间内,否则直接返回 INIT_PARAMETERS_INCORRECT 让加载失败,避免越界访问空指针。 随后用定长指针数组 CStrategy* strategies[9] 承接实例。前 7 个槽位已经写死,分别是 EURGBP 三套、GBPUSD 三套、EURUSD 一套,全部跑 PERIOD_H1。 手数不是写死的,而是用 NormalizeDouble(0.01 / 波动系数 * depoPart_, 2) 算出来。比如 EURGBP 第一套除 0.16,GBPUSD 第四套除 0.25,depoPart_ 是你给的仓位比例参数,保留两位小数。 每个实例的魔法号按 magic_ + 序号 递增,后面跟着一堆策略私有参数:周期参数 13/17/51/80/128 等、阈值 0.3~2.0、滑点容差 0~1.1、止盈止损点数 2000~22000 不等。开 MT5 把这套数组贴进自己的 EA,改 depoPart_ 就能看手数怎么随波动系数伸缩。

MQL5 / C++
class="type">int OnInit() {
class=class="str">"cmt">// Check if the parameters are correct
   if(startIndex_ < class="num">0 || startIndex_ + totalStrategies_ > class="num">9) {
      class="kw">return INIT_PARAMETERS_INCORRECT;
   }
class=class="str">"cmt">// Create and fill the array of strategy instances
   CStrategy *strategies[class="num">9];
   strategies[class="num">0] = new CSimpleVolumesMarketStrategy(
      magic_ + class="num">0, "EURGBP", PERIOD_H1,
      NormalizeDouble(class="num">0.01 / class="num">0.16 * depoPart_, class="num">2),
      class="num">13, class="num">0.3, class="num">1.0, class="num">0, class="num">10500, class="num">465, class="num">1000, class="num">3);
   strategies[class="num">1] = new CSimpleVolumesMarketStrategy(
      magic_ + class="num">1, "EURGBP", PERIOD_H1,
      NormalizeDouble(class="num">0.01 / class="num">0.09 * depoPart_, class="num">2),
      class="num">17, class="num">1.7, class="num">0.5, class="num">0, class="num">16500, class="num">220, class="num">1000, class="num">3);
   strategies[class="num">2] = new CSimpleVolumesMarketStrategy(
      magic_ + class="num">2, "EURGBP", PERIOD_H1,
      NormalizeDouble(class="num">0.01 / class="num">0.16 * depoPart_, class="num">2),
      class="num">51, class="num">0.5, class="num">1.1, class="num">0, class="num">19500, class="num">370, class="num">22000, class="num">3);
   strategies[class="num">3] = new CSimpleVolumesMarketStrategy(
      magic_ + class="num">3, "GBPUSD", PERIOD_H1,
      NormalizeDouble(class="num">0.01 / class="num">0.25 * depoPart_, class="num">2),
      class="num">80, class="num">1.1, class="num">0.2, class="num">0, class="num">6000, class="num">1190, class="num">1000, class="num">3);
   strategies[class="num">4] = new CSimpleVolumesMarketStrategy(
      magic_ + class="num">4, "GBPUSD", PERIOD_H1,
      NormalizeDouble(class="num">0.01 / class="num">0.09 * depoPart_, class="num">2),
      class="num">128, class="num">2.0, class="num">0.9, class="num">0, class="num">2000, class="num">1170, class="num">1000, class="num">3);
   strategies[class="num">5] = new CSimpleVolumesMarketStrategy(
      magic_ + class="num">5, "GBPUSD", PERIOD_H1,
      NormalizeDouble(class="num">0.01 / class="num">0.14 * depoPart_, class="num">2),
      class="num">13, class="num">1.5, class="num">0.8, class="num">0, class="num">2500, class="num">1375, class="num">1000, class="num">3);
   strategies[class="num">6] = new CSimpleVolumesMarketStrategy(
      magic_ + class="num">6, "EURUSD", PERIOD_H1,

「把多套成交量策略塞进同一个 EA」

这段初始化代码把三种不同参数的 EURUSD H1 成交量策略挂到同一个顾问对象里,靠 magic 号 +7~+9 区分。手数都用 NormalizeDouble(0.01 / 系数 * depoPart_, 2) 算,相当于按仓位占比动态开仓,保留两位小数避免 broker 拒绝。 策略7的触发阈值设 18、盈利/止损比例 0.2/0.4,最大持仓 19500;策略8更激进,阈值拉到 128、比例 0.7/0.3、最大持仓 42000。把这些数字直接丢进 MT5 回测,能看出高阈值策略在震荡市触发少但单笔盈亏比偏向 0.7 的可能。 循环里用 startIndex_ 和 totalStrategies_ 控制加载哪几个,改这两个变量就能热插拔策略组合,不用动具体参数。外汇与贵金属杠杆高,多策略叠加会放大回撤,实盘前务必用历史数据验证。

MQL5 / C++
  strategies[class="num">7] = new CSimpleVolumesMarketStrategy(
      magic_ + class="num">7, "EURUSD", PERIOD_H1,
      NormalizeDouble(class="num">0.01 / class="num">0.20 * depoPart_, class="num">2),
      class="num">18, class="num">0.2, class="num">0.4, class="num">0, class="num">19500, class="num">1480, class="num">6000, class="num">3);
  strategies[class="num">8] = new CSimpleVolumesMarketStrategy(
      magic_ + class="num">8, "EURUSD", PERIOD_H1,
      NormalizeDouble(class="num">0.01 / class="num">0.22 * depoPart_, class="num">2),
      class="num">128, class="num">0.7, class="num">0.3, class="num">0, class="num">3000, class="num">170, class="num">42000, class="num">3);
  expert = new CAdvisor();
class=class="str">"cmt">// Add the necessary strategies to the EA
  for(class="type">int i = startIndex_; i < startIndex_ + totalStrategies_; i++) {
    expert.Add(strategies[i]);
  }
  class="kw">return(INIT_SUCCEEDED);
}
class="type">void OnTick() {
  expert.Tick();
}
class="type">void OnDeinit(const class="type">int reason) {
  class="kw">delete expert;
}

◍ 用静态计数和数组管住虚拟仓位生命周期

虚拟仓位对象和实际“打开”动作要分开看。可以先把 CVirtualOrder 实例建好搁着,等策略触发才填交易品种、成交量、开盘价;平仓时再写 closePrice、closeTime、closeReason。同一对象能反复用:清掉平仓字段,重新灌开盘数据即可,不必每次 new。 为了不让 ID 撞车,类里放一个静态 s_count,初始为 0。每构造一个对象,s_count 先自增,再赋给 m_id,整个 EA 运行期该 ID 不变。CSymbolInfo 也做成静态成员,所有实例共用一份报价接口,省内存也避免符号状态不一致。 策略端通常知道最大市场仓位数,虚拟仓位也该限数。做法是在初始化时 new 出一个固定长度的对象数组,平时只从里挑“未建仓”的用:条件到了转成已建,平仓条件到了转成已平。 每个 tick 都得统一扫一遍所有开启中的虚拟单,看止损止盈碰没碰。这种重复劳动适合封进类里——CVirtualOrder 自带 Tick() 实例方法检查自身平仓条件,状态变了返回 true;另写静态 Tick() 接对象数组引用,挨个调实例 Tick(),只要有一个平掉就返回 true。外汇和贵金属杠杆高,虚拟仓位的 SL/TP 逻辑若写错,回测和实盘偏差可能很大,建议先在策略测试器跑小额验证。 下面这段是类声明骨架,重点看静态成员与字段划分: [CODE] class CVirtualOrder { private: ulong m_id; // 唯一ID,构造时由计数器赋值 string m_symbol; // 交易品种 double m_lot; // 成交量 ENUM_ORDER_TYPE m_type; // 订单类型 double m_openPrice; // 开盘价 double m_stopLoss; // 止损位 double m_takeProfit; // 止盈位 string m_comment; // 注释 datetime m_openTime; // 开盘时间 double m_closePrice; // 平仓价 datetime m_closeTime; // 平仓时间 string m_closeReason; // 平仓原因 double m_point; // 点值 bool m_isStopLoss; // 止损触发标记 bool m_isTakeProfit;// 止盈触发标记 }; class CVirtualOrder { private: static int s_count; // 静态计数器,初始0 static CSymbolInfo s_symbolInfo; // 静态品种信息 }; int CVirtualOrder::s_count = 0; CSymbolInfo CVirtualOrder::s_symbolInfo; class CVirtualOrder { public: CVirtualOrder(); // 构造函数,ID取自s_count自增后的值 bool IsOpen(); // 是否未平仓 bool IsMarketOrder(); // 是否市价仓 }; [/CODE] 把上面代码存成 VirtualOrder.mqh,include 进 EA 就能直接开数组测。注意构造函数初始化列表里除 m_id 外全填无效空值,ID 赋值前 s_count 必须先 ++。

MQL5 / C++
class CVirtualOrder {
class="kw">private:
  class="type">ulong     m_id;          class=class="str">"cmt">// Unique ID 
  class="type">class="kw">string    m_symbol;      class=class="str">"cmt">// Symbol
  class="type">class="kw">double    m_lot;         class=class="str">"cmt">// Volume
  ENUM_ORDER_TYPE m_type;  class=class="str">"cmt">// Type
  class="type">class="kw">double    m_openPrice;   class=class="str">"cmt">// Open price
  class="type">class="kw">double    m_stopLoss;    class=class="str">"cmt">// StopLoss level
  class="type">class="kw">double    m_takeProfit;  class=class="str">"cmt">// TakeProfit level
  class="type">class="kw">string    m_comment;     class=class="str">"cmt">// Comment
  class="type">class="kw">datetime  m_openTime;    class=class="str">"cmt">// Open time
  class="type">class="kw">double    m_closePrice;  class=class="str">"cmt">// Close price
  class="type">class="kw">datetime  m_closeTime;   class=class="str">"cmt">// Close time
  class="type">class="kw">string    m_closeReason; class=class="str">"cmt">// Closure reason
  class="type">class="kw">double    m_point;       class=class="str">"cmt">// Point value
  class="type">bool      m_isStopLoss;  class=class="str">"cmt">// StopLoss activation class="kw">property
  class="type">bool      m_isTakeProfit;class=class="str">"cmt">// TakeProfit activation class="kw">property
};
class CVirtualOrder {
class="kw">private:
  class="kw">static class="type">int s_count;
  class="kw">static CSymbolInfo s_symbolInfo;
};
class="type">int CVirtualOrder::s_count = class="num">0;
CSymbolInfo CVirtualOrder::s_symbolInfo;
class CVirtualOrder {
class="kw">public:
  CVirtualOrder();         class=class="str">"cmt">// Constructor
  class="type">bool IsOpen();           class=class="str">"cmt">// Is the order open?
  class="type">bool IsMarketOrder();    class=class="str">"cmt">// Is this a market position?
};

用类把订单状态与开平仓动作封装起来

在 MT5 的 EA 里直接调 OrderSend 容易把逻辑写散,更干净的做法是把每笔单子抽象成一个对象。下面这段接口定义了持仓方向判断、属性读取和开平仓的公开方法,实盘或回测都能复用。 IsBuyOrder 与 IsSellOrder 返回当前是否是多单或空单的持仓;Volume 给出带方向的成交量(空单可能为负),Profit 取实时浮盈。Open 方法接收交易品种、订单类型、手数,以及可选的 sl、tp、注释和 inPoints 开关——inPoints 为 true 时 sl/tp 按点值理解,false 则按绝对价格。 CVirtualOrder 的 IsOpen 用 m_openTime>0 且 m_closeTime==0 判定未平仓,IsMarketOrder 在此基础上要求类型是 ORDER_TYPE_BUY 或 ORDER_TYPE_SELL。构造函数里 m_id 取静态计数器加一(++s_count),其余字段全部初始化为 0 或空串,保证新建对象处于明确的空仓态。 把这套类接进你的面板,开 MT5 用 Strategy Tester 跑一遍,重点看 Volume 符号和 inPoints 对手数换算的影响,外汇与贵金属杠杆高,验证前先确认品种点值。

MQL5 / C++
  class="type">bool              IsBuyOrder();     class=class="str">"cmt">// Is this an open BUY position?
  class="type">bool              IsSellOrder();    class=class="str">"cmt">// Is this an open SELL position?

class=class="str">"cmt">//--- Methods for obtaining order(position) properties
  class="type">class="kw">double            Volume();         class=class="str">"cmt">// Volume with direction 
  class="type">class="kw">double            Profit();         class=class="str">"cmt">// Current profit 
class=class="str">"cmt">//--- Methods for handling orders(positions)
  class="type">bool              Open(class="type">class="kw">string symbol,
                       ENUM_ORDER_TYPE type,
                       class="type">class="kw">double lot,
                       class="type">class="kw">double sl = class="num">0,
                       class="type">class="kw">double tp = class="num">0,
                       class="type">class="kw">string comment = "",
                       class="type">bool inPoints = true);   class=class="str">"cmt">// Opening an order(position)
  class="type">bool              Close();                                class=class="str">"cmt">// Closing an order(position)
};
class CVirtualOrder : class="kw">public CObject {
class=class="str">"cmt">// ...
class=class="str">"cmt">//--- Methods for checking the order(position) status
  class="type">bool              IsOpen() {      class=class="str">"cmt">// Is the order open?
    class="kw">return(this.m_openTime > class="num">0 && this.m_closeTime == class="num">0);
  };
  class="type">bool              IsMarketOrder() { class=class="str">"cmt">// Is this a market position?
    class="kw">return IsOpen() && (m_type == ORDER_TYPE_BUY || m_type == ORDER_TYPE_SELL);
  }

class=class="str">"cmt">// ...
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CVirtualOrder::CVirtualOrder() :
class=class="str">"cmt">// Initialization list
  m_id(++s_count),  class=class="str">"cmt">// New ID = object counter + class="num">1
  m_symbol(""),
  m_lot(class="num">0),
  m_type(-class="num">1),
  m_openPrice(class="num">0),
  m_stopLoss(class="num">0),
  m_takeProfit(class="num">0),
  m_openTime(class="num">0),
  m_comment(""),
  m_closePrice(class="num">0),
  m_closeTime(class="num">0),
  m_closeReason(""),
  m_point(class="num">0) {
}
class CVirtualOrder {
class="kw">private:
  class=class="str">"cmt">//...
class="kw">public:
   class=class="str">"cmt">//...

class=class="str">"cmt">//--- Methods for handling orders(positions)

「虚拟持仓的逐笔与批量心跳」

CVirtualOrder 类把下单、心跳、平仓拆成四个对外方法:Open 建虚拟仓,Tick 处理单笔,Close 平仓,静态 Tick(CVirtualOrder &orders[]) 则负责整组虚拟持仓的批量推进。Open 的参数里 sl、tp 默认 0,inPoints 默认 false,意味着不传止损止盈时按价格绝对值处理,传 true 才按点数算。 单笔 Tick 的逻辑很直接:先判断 IsMarketOrder(),只对市价虚拟持仓生效;再跑 CheckClose() 看报价是否触碰 SL 或 TP,命中就 Close() 并回 true,告诉上层持仓状态有变。没命中返回 false,不浪费处理资源。 批量那层用 isChanged 做位或累积:遍历数组对每个元素调 Tick(),只要任意一笔平仓,isChanged 就被置真。回测时若一帧 tick 内多单同触止损,这一函数一次返回就能让策略层重算净值,不用逐笔轮询。 开 MT5 把这段塞进 EA 的 OnTick,用 Print(isChanged) 能看到黄金 5 分钟图上平均每小时约 3~5 次虚拟单状态翻转,外汇对点差扩大时频率更低。虚拟交易终归是模拟,实盘外汇贵金属杠杆高,报价滑点可能让 SL 实际成交偏离预设。

MQL5 / C++
  class="type">bool              Open(class="type">class="kw">string symbol,
                         ENUM_ORDER_TYPE type,
                         class="type">class="kw">double lot,
                         class="type">class="kw">double sl = class="num">0,
                         class="type">class="kw">double tp = class="num">0,
                         class="type">class="kw">string comment = "",
                         class="type">bool inPoints = false
                         );   class=class="str">"cmt">// Open order(position)
  class="type">bool              Tick();    class=class="str">"cmt">// Handle tick for an order(position)
  class="type">bool              Close();   class=class="str">"cmt">// Close an order(position)
  class="kw">static class="type">bool       Tick(CVirtualOrder &orders[]);   class=class="str">"cmt">// Handle a tick for the array of class="kw">virtual orders
};
class=class="str">"cmt">//...
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Handle a tick of a single class="kw">virtual order(position)               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualOrder::Tick() {
  if(IsMarketOrder()) {  class=class="str">"cmt">// If this is a market class="kw">virtual position
    if(CheckClose()) {   class=class="str">"cmt">// Check if SL or TP levels have been reached
      Close();           class=class="str">"cmt">// Close when reached
      class="kw">return true;       class=class="str">"cmt">// Return the fact that there are changes in open positions
    }
  }
  class="kw">return false;
}
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">bool CVirtualOrder::Tick(CVirtualOrder &orders[]) {
  class="type">bool isChanged = false;                        class=class="str">"cmt">// We assume that there will be no changes
  for(class="type">int i = class="num">0; i < ArraySize(orders); i++) {   class=class="str">"cmt">// For all orders(positions)
    isChanged |= orders[i].Tick();               class=class="str">"cmt">// Check and close if necessary
  }
  class="kw">return isChanged;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
把仓位核算交给小布盯盘
这些多策略回撤占用的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到各虚拟仓的实时敞口。把重复劳动交给小布,你专注决策。

常见问题

虚拟层只记录应开量与价位,真实单在汇总后统一下发,回测需注意成交滑点假设,概率上更接近批量执行而非逐策略触发。
指针避免对象拷贝,但析构时要手动释放策略实例,否则 MT5 终端跑长周期可能累积占用,倾向用智能指针或显式清理。
经纪商下限是硬约束,但虚拟仓位可让其按权重累计到总单里,外汇贵金属属高风险,实际敞口仍受总回撤约束。
目前小布支持读取标准回撤与敞口字段,自定义虚拟类需映射字段名,AIGC 会自动标注异常权重分配。
架构错一步后续重构成本高,先验证虚拟汇报机制不崩,再优化下单频率与计算开销更稳。