开发多币种 EA 交易(第 1 部分):多种交易策略的协作·进阶篇
🤖

开发多币种 EA 交易(第 1 部分):多种交易策略的协作·进阶篇

(2/3)· 当多个现成 EA 难以同终端共存,回撤与仓位分配就成了绕不开的坑

实战向进阶 第 2/3 篇
很多交易者把几个盈利 EA 分别挂在不同终端,只为避开互相抢单和风控冲突。其实用一套协作框架,就能让它们在同一个账户里按比例分仓、共用回撤上限,省掉多终端盯盘的麻烦。

「挂单节奏由持仓计数卡住」

EA 在 OnTick 里先跑 UpdateCounts(),把属于自己 Magic 号的持仓与挂单分别数一遍,再判断 countOrders + countPositions 是否小于 maxCountOfOrders_。这一步直接决定了同屏最多只会有多少笔未平仓位,外汇与贵金属品种在高波动时若把上限调太大,可能触发券商的并发订单限制。 信号判定走 SignalForOpen(),返回 1 就 OpenBuyOrder() 挂 BUY_STOP,返回 -1 则 OpenSellOrder() 挂 SELL_STOP。注意它只认这两种返回值,其他情况当周期不动作,避免重复追单。 UpdateCounts() 的计数逻辑很直白:PositionsTotal() 和 OrdersTotal() 两个循环里,都用 SelectByIndex(i) 且比对 Magic() == magicN_。这意味着你如果在 MT5 手动开同品种单子但 Magic 不同,EA 不会把它算进额度,可能悄悄超出你预期的风险暴露。 开 BUY_STOP 时先 RefreshRates() 拿实时 bid/ask,distance 取 openDistance_ 与 spread 的较大值,保证挂单距离不小于点差。价格按 ask + distance*point 计算,SL 放在 price - stopLevel_*point,TP 则是 price + (takeLevel_ + spread)*point,把点差补进盈利目标里。

MQL5 / C++
trade.SetExpertMagicNumber(magicN_);
class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "Tick" event handler function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick() {
class=class="str">"cmt">// Count open positions and orders
  UpdateCounts();
class=class="str">"cmt">// If their number is less than allowed
  if(countOrders + countPositions < maxCountOfOrders_) {
    class=class="str">"cmt">// Get an open signal
    class="type">int signal = SignalForOpen();
    if(signal == class="num">1) {              class=class="str">"cmt">// If there is a buy signal, then
      OpenBuyOrder();              class=class="str">"cmt">// open the BUY_STOP order
    } else if(signal == -class="num">1) {      class=class="str">"cmt">// If there is a sell signal, then
      OpenSellOrder();             class=class="str">"cmt">// open the SELL_STOP order
    }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculate the number of open orders and positions                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void UpdateCounts() {
class=class="str">"cmt">// Reset position and order counters
  countPositions = class="num">0;
  countOrders = class="num">0;
class=class="str">"cmt">// Loop through all positions
  for(class="type">int i = class="num">0; i < PositionsTotal(); i++) {
    class=class="str">"cmt">// If the position with index i is selected successfully and its Magic is ours, then we count it
    if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) {
      countPositions++;
    }
  }
class=class="str">"cmt">// Loop through all orders
  for(class="type">int i = class="num">0; i < OrdersTotal(); i++) {
    class=class="str">"cmt">// If the order with index i is selected successfully and its Magic is the one we need, then we consider it
    if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) {
      countOrders++;
    }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Open the BUY_STOP order                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OpenBuyOrder() {
class=class="str">"cmt">// Update symbol current price data
  symbolInfo.Name(Symbol());
  symbolInfo.RefreshRates();
class=class="str">"cmt">// Retrieve the necessary symbol and price data
  class="type">class="kw">double point = symbolInfo.Point();
  class="type">int digits = symbolInfo.Digits();
  class="type">class="kw">double bid = symbolInfo.Bid();
  class="type">class="kw">double ask = symbolInfo.Ask();
  class="type">int spread = symbolInfo.Spread();
class=class="str">"cmt">// Let&class="macro">#x27;s make sure that the opening distance is not less than the spread
  class="type">int distance = MathMax(openDistance_, spread);
class=class="str">"cmt">// Opening price
  class="type">class="kw">double price = ask + distance * point;

class=class="str">"cmt">// StopLoss and TakeProfit levels
  class="type">class="kw">double sl = NormalizeDouble(price - stopLevel_ * point, digits);
  class="type">class="kw">double tp = NormalizeDouble(price + (takeLevel_ + spread) * point, digits);

class=class="str">"cmt">// Expiration time

◍ 挂单函数里被忽略的价差保护

上面这段是 BuyStop 与 SellStop 两个挂单函数的核心实现,重点不在『挂哪』,而在『挂多远』。SellStop 里先用 MathMax(openDistance_, spread) 取开盘距离与实时点差的大者,避免挂单价离 bid 太近被经纪商拒单——这是 MT5 实盘里最容易踩的坑。 价格计算上,SellStop 的触发价 = bid - distance * point,SL 放在 price + stopLevel_ * point,TP 则扣掉了 spread:price - (takeLevel_ + spread) * point。注意 TP 把点差算进去了,意味着策略在回测里若忽略 spread,实盘盈利会系统性偏薄。 过期时间统一用 TimeCurrent() + ordersExpiration_ * 60,单位是秒;若 ordersExpiration_ 设 1440,挂单存活约 1 天。外汇与贵金属杠杆高,挂单未成交过期只是成本损耗,但滑点可能让实际成交偏离预期,需自行在 MT5 策略测试器验证。 别把正态当圣经 SellStop 的 TP 减 spread 只是代码层的修正,不保证实盘胜率;点差在重大数据发布时可能瞬间扩到日常 5~10 倍,挂单距离要留余量。

MQL5 / C++
class="type">class="kw">datetime expiration = TimeCurrent() + ordersExpiration_ * class="num">60;

class=class="str">"cmt">// Order volume
class="type">class="kw">double lot = fixedLot_;

class=class="str">"cmt">// Set a pending order
class="type">bool res = trade.BuyStop(lot,
                          NormalizeDouble(price, digits),
                          Symbol(),
                          NormalizeDouble(sl, digits),
                          NormalizeDouble(tp, digits),
                          ORDER_TIME_SPECIFIED,
                          expiration);
  if(!res) {
    Print("Error opening order");
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Open the SELL_STOP order                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OpenSellOrder() {
class=class="str">"cmt">// Update symbol current price data
  symbolInfo.Name(Symbol());
  symbolInfo.RefreshRates();
class=class="str">"cmt">// Retrieve the necessary symbol and price data
  class="type">class="kw">double point = symbolInfo.Point();
  class="type">int digits = symbolInfo.Digits();
  class="type">class="kw">double bid = symbolInfo.Bid();
  class="type">class="kw">double ask = symbolInfo.Ask();
  class="type">int spread = symbolInfo.Spread();
class=class="str">"cmt">// Let&class="macro">#x27;s make sure that the opening distance is not less than the spread
  class="type">int distance = MathMax(openDistance_, spread);
class=class="str">"cmt">// Opening price
  class="type">class="kw">double price = bid - distance * point;

class=class="str">"cmt">// StopLoss and TakeProfit levels
  class="type">class="kw">double sl = NormalizeDouble(price + stopLevel_ * point, digits);
  class="type">class="kw">double tp = NormalizeDouble(price - (takeLevel_ + spread) * point, digits);
class=class="str">"cmt">// Expiration time
  class="type">class="kw">datetime expiration = TimeCurrent() + ordersExpiration_ * class="num">60;
class=class="str">"cmt">// Order volume
  class="type">class="kw">double lot = fixedLot_;
class=class="str">"cmt">// Set a pending order
  class="type">bool res = trade.SellStop(lot,
                            NormalizeDouble(price, digits),
                            Symbol(),
                            NormalizeDouble(sl, digits),
                            NormalizeDouble(tp, digits),
                            ORDER_TIME_SPECIFIED,
                            expiration);
  if(!res) {
    Print("Error opening order");
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+

用成交量突变抓挂单信号

这套挂单信号逻辑不看指标交叉,只看成交量相对均值是否异常放大。核心在 SignalForOpen():先把最近 signalPeriod_ 根 K 线的成交量搬进数组,算均值,再拿当前成交量去比阈值。 阈值不是固定倍数,而是 avrVolume * (1 + signalDeviation_ + (countOrders + countPositions) * signaAddlDeviation_)。也就是说,你账户里已挂单和持仓越多,触发信号需要的放量幅度越高,避免在拥挤价位反复追。 当前量超阈值后,还要看开盘价和收盘价关系:开小于收给 buy 信号(返回 1),否则给 sell 信号(返回 -1)。外汇与贵金属波动受杠杆与消息面影响,放量信号只代表概率倾斜,实盘前务必在 MT5 策略测试器用历史数据验证。 ArrayAverage() 是个手写的均值函数,遍历数组求和再除以 MathMax(1, total),避免空数组除零。你可以直接把下面代码贴进 EA 的 include 段,改 signalPeriod_ 和 signalDeviation_ 两个参数就能调灵敏度。

MQL5 / C++
class=class="str">"cmt">//| Signal for opening pending orders                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int SignalForOpen() {
class=class="str">"cmt">// By class="kw">default, there is no signal
   class="type">int signal = class="num">0;
class=class="str">"cmt">// Copy volume values from the indicator buffer to the receiving array
   class="type">int res = CopyBuffer(iVolumesHandle, class="num">0, class="num">0, signalPeriod_, volumes);
class=class="str">"cmt">// If the required amount of numbers have been copied
   if(res == signalPeriod_) {
      class=class="str">"cmt">// Calculate their average value
      class="type">class="kw">double avrVolume = ArrayAverage(volumes);
      class=class="str">"cmt">// If the current volume exceeds the specified level, then
      if(volumes[class="num">0] > avrVolume * (class="num">1 + signalDeviation_ + (countOrders + countPositions) * signaAddlDeviation_)) {
         class=class="str">"cmt">// if the opening price of the candle is less than the current(closing) price, then 
         if(iOpen(Symbol(), PERIOD_CURRENT, class="num">0) < iClose(Symbol(), PERIOD_CURRENT, class="num">0)) {
            signal = class="num">1; class=class="str">"cmt">// buy signal
         } else {
            signal = -class="num">1; class=class="str">"cmt">// otherwise, sell signal
         }
      }
   }
   class="kw">return signal;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Number array average value                                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double ArrayAverage(const class="type">class="kw">double &array[]) {
   class="type">class="kw">double s = class="num">0;
   class="type">int total = ArraySize(array);
   for(class="type">int i = class="num">0; i < total; i++) {
      s += array[i];
   }
   class="kw">return s / MathMax(class="num">1, total);
}
class=class="str">"cmt">//+------------------------------------------------------------------+

「把通用策略骨架先钉死」

做多策略 EA 时,第一件该做的事是把「所有策略都共有的东西」抽出来。交易品种、时间框架、开仓幻数、固定手数,这四个字段跟具体信号无关,却每个子类都逃不掉。 在 MT5 里用 CStrategy 基类承接这些属性,Init() 和 Tick() 直接写成纯虚函数(=0),逼着后面的具体策略去实现自己的初始化与报价处理逻辑。基类本身不写交易规则,只保证对象一被 new 出来,四个参数就通过构造函数初始化列表落位。 下面这段存成 Strategy.mqh 就能用。外汇与贵金属杠杆高、滑点跳空频繁,幻数隔离不同 EA 的订单尤其重要,否则复盘时你分不清哪笔单子是谁开的。 代码逐行看:class 继承 CObject 方便以后塞进指针容器;protected 里 m_magic 是订单识别码,m_symbol 限定品种,m_timeframe 锁周期,m_fixedLot 暂定固定手数(后续再扩展可变仓位);public 的构造函数收四个入参,纯虚的 Init 对应 OnInit、Tick 对应 OnTick,派生类必须重写。构造函数体用初始化列表一次性赋值,比在函数体内写 m_xxx = p_xxx 少一次默认构造开销。

MQL5 / C++
class CStrategy : class="kw">public CObject {
class="kw">protected:
   class="type">class="kw">ulong            m_magic;            class=class="str">"cmt">// Magic
   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)
class="kw">public:
   class=class="str">"cmt">// Constructor
   CStrategy(class="type">class="kw">ulong p_magic,
             class="type">class="kw">string p_symbol,
             ENUM_TIMEFRAMES p_timeframe,
             class="type">class="kw">double p_fixedLot);
   class="kw">virtual class="type">int      Init() = class="num">0; class=class="str">"cmt">// Strategy initialization - handling OnInit events
   class="kw">virtual class="type">void     Tick() = class="num">0; class=class="str">"cmt">// Main method - handling OnTick events
};
CStrategy::CStrategy(
   class="type">class="kw">ulong p_magic,
   class="type">class="kw">string p_symbol,
   ENUM_TIMEFRAMES p_timeframe,
   class="type">class="kw">double p_fixedLot) :
class=class="str">"cmt">// Initialization list
   m_magic(p_magic),
   m_symbol(p_symbol),
   m_timeframe(p_timeframe),
   m_fixedLot(p_fixedLot)
{}

◍ 把简单EA逻辑塞进策略子类

把原始简单 EA 的开仓逻辑搬进 CSimpleVolumesStrategy 子类时,第一件事是把所有输入变量和全局变量变成类的成员。原来 EA 里的 fixedLot_ 和 magicN_ 直接删掉,改用从 CStrategy 基类继承的 m_fixedLot 和 m_magic,编译器在链接时才认得到归属。 OnInit() 和 OnTick() 不再是最外层函数,而是变成类的公有方法 Init() 和 Tick()。其他辅助函数降级为私有方法,只能被本类内部调用——这层封装能避免 EA 主体误改信号参数。注意函数实现处要加 'CSimpleVolumesStrategy::' 前缀,否则编译器当普通函数处理。 原 EA 在声明时给输入参数赋默认值,但 MT5 编译后弹出的参数框会覆盖代码值。类描述里做不到声明即赋值,所以必须写构造函数,且得是公有的,不然外部根本 new 不出策略对象。构造函数里前四个参数通过调用基类构造函数传递,其余参数赋给对应成员。 最后做文本替换:所有 fixedLot_ 改成 m_fixedLot,magicN_ 改成 m_magic;Symbol() 换成基类 m_symbol,PERIOD_CURRENT 换成 m_timeframe。整段存成 SimpleVolumesStrategy.mqh 放当前目录,EA 就能 include 进来跑。外汇与贵金属品种波动剧烈,封装后回测仍可能因滑点偏离历史表现。

MQL5 / C++
class="macro">#include "Strategy.mqh"
class CSimpleVolumeStrategy : class="kw">public CStrategy {
  class=class="str">"cmt">//---  Open signal parameters
  class="type">int               signalPeriod_;      class=class="str">"cmt">// Number of candles for volume averaging
  class="type">class="kw">double            signalDeviation_;   class=class="str">"cmt">// Relative deviation from the average to open the first order
  class="type">class="kw">double            signaAddlDeviation_; class=class="str">"cmt">// Relative deviation from the average for opening the second and subsequent orders
  class=class="str">"cmt">//---  Pending order parameters
  class="type">int               openDistance_;      class=class="str">"cmt">// Distance from price to pending order
  class="type">class="kw">double            stopLevel_;        class=class="str">"cmt">// Stop Loss(in points)
  class="type">class="kw">double            takeLevel_;        class=class="str">"cmt">// Take Profit(in points)
  class="type">int               ordersExpiration_;  class=class="str">"cmt">// Pending order expiration time(in minutes)
  class=class="str">"cmt">//---  Money management parameters
  class="type">int               maxCountOfOrders_;  class=class="str">"cmt">// Maximum number of simultaneously open orders
  CTrade            trade;             class=class="str">"cmt">// Object for performing trading operations
  COrderInfo        orderInfo;         class=class="str">"cmt">// Object for receiving information about placed orders
  CPositionInfo     positionInfo;      class=class="str">"cmt">// Object for receiving information about open positions
  class="type">int               countOrders;       class=class="str">"cmt">// Number of placed pending orders
  class="type">int               countPositions;    class=class="str">"cmt">// Number of open positions
  CSymbolInfo       symbolInfo;        class=class="str">"cmt">// Object for obtaining data on the symbol properties
  class="type">int               iVolumesHandle;    class=class="str">"cmt">// Tick volume indicator handle
  class="type">class="kw">double            volumes[];         class=class="str">"cmt">// Receiver array of indicator values(volumes themselves) 
};
class CSimpleVolumeStrategy : class="kw">public CStrategy {
class="kw">private:
  class=class="str">"cmt">//---  ... previous code
  class="type">class="kw">double            volumes[];         class=class="str">"cmt">// Receiver array of indicator values(volumes themselves)
  class=class="str">"cmt">//--- Methods
  class="type">void              UpdateCounts();    class=class="str">"cmt">// Calculate the number of open orders and positions

把策略类接进 MT5 的构造函数参数

CSimpleVolumeStrategy 继承自 CStrategy,公开方法只有 Init 和 Tick 两个虚函数,其余信号计算、挂单动作都封在私有段里。真正决定策略行为的,是构造函数里那一长串入参,少一个 MT5 都编不过。 构造函数要收 12 个参数:magic 号、交易品种、周期枚举、固定手数、信号周期、两级信号偏差、挂单距离、止损点数、止盈点数、订单过期秒数、最大挂单数。把 p_signalPeriod 设成 20、p_signalDeviation 设 1.5 这类组合,回测里可能比默认值更敏感,但也更容易在贵金属跳空时被扫。 外汇和贵金属杠杆高,挂单距离 p_openDistance 若小于经纪商止损水平,OrderSend 会直接返错。建议先在策略测试器里把 p_ordersExpiration 设 0 观察成交密度,再逐步放开。 下面这段是类声明与构造签名的实际写法,逐行看参数含义比读文档快: class CSimpleVolumeStrategy : public CStrategy { private: //--- ... previous code public: //--- Public methods CSimpleVolumeStrategy( ulong p_magic, // 策略魔数,区分 EA 订单 string p_symbol, // 交易符号,如 "XAUUSD" ENUM_TIMEFRAMES p_timeframe, // 计算周期 double p_fixedLot, // 每单固定手数 int p_signalPeriod, // 信号均线周期 double p_signalDeviation, // 信号偏差阈值1 double p_signaAddlDeviation, // 信号附加偏差 int p_openDistance, // 挂单距市价点数 double p_stopLevel, // 止损点数 double p_takeLevel, // 止盈点数 int p_ordersExpiration, // 订单有效秒数 int p_maxCountOfOrders // 最大挂单计数 ); // 构造函数结束 virtual int Init(); // 初始化 virtual void Tick(); // 每 tick 调用 };

MQL5 / C++
class CSimpleVolumeStrategy : class="kw">public CStrategy {
class="kw">private:
  class=class="str">"cmt">//---  ... previous code
class="kw">public:
  class=class="str">"cmt">//--- Public methods
  CSimpleVolumeStrategy(
    class="type">class="kw">ulong p_magic,
    class="type">class="kw">string p_symbol,
    ENUM_TIMEFRAMES p_timeframe,
    class="type">class="kw">double p_fixedLot,
    class="type">int p_signalPeriod,
    class="type">class="kw">double p_signalDeviation,
    class="type">class="kw">double p_signaAddlDeviation,
    class="type">int p_openDistance,
    class="type">class="kw">double p_stopLevel,
    class="type">class="kw">double p_takeLevel,
    class="type">int p_ordersExpiration,
    class="type">int p_maxCountOfOrders
  );
  class="kw">virtual class="type">int Init();
  class="kw">virtual class="type">void Tick();
};
交给小布盯盘看协作仓位
这些多策略同账户下的回撤占用与分仓比例,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时诊断,你只管调策略权重。

常见问题

各自独立计算仓位和止损,可能同时触发导致超出预设回撤,且终端重启时订单状态恢复容易错乱,需要统一协调层。
可以,小布盯盘内置了多策略同账户的占用诊断,能按品种页展示各策略分仓与回撤比例,减少手动核算。
不能设固定阈值,应以若干根烛形的分时成交量均值为基准,再按品种波动特性动态比较,避免冷门品种误触发。
应设定明确有效期并在到期删除,同时通知协作层释放该仓位额度,防止死单占用回撤预算。