开发多币种 EA 交易(第 1 部分):多种交易策略的协作(基础篇)
📘

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

第 1/3 篇

让多个策略在同一终端共存

做 EA 时大多只跑一条交易思路,但实战里往往手头攒着好几套逻辑各自能赚钱。难点在于 MT5 一个终端里挂多个 EA,彼此抢资源、发单冲突,很难长期稳定协作,所以多数人只能留两三个最顺手的。 作者 Yuriy Bykov 在 2024.08.28 发布的这组文章里提到,他经手过各类策略,单 EA 单思路是常态;原文页面显示阅读 2 447、评论 31,说明多策略协同是实盘圈真实痛点。 如果为了终端稳定直接砍掉本来可行的策略,机会成本并不低。这一节先抛出核心问题:怎么让不同策略在同一 MT5 终端里协同跑,而不是互相拖后腿。

「先框定要解决的问题」

做多策略同账户协作前,得先把「手里有什么」和「想要什么」摊开。手头通常是几套现成 EA 或规则化交易逻辑,跑在不同品种和周期上,加上一笔起始存款和一个最大允许回撤上限。 想要落地的目标很具体:多策略在一个账户里协同;存款按均分或指定比例切给各策略;根据最大回撤自动算该开多少仓;终端重启不能把状态搞丢;最好 MT5 和 MT4 都能跑。 技术路线上用面向对象写法、MQL5 加 MT5 标准测试器来推进。任务不轻,得拆阶段做。第一阶段先拿一个简单交易思路写出基础 EA,优化后挑出两组最佳参数,再包一个内含两份该 EA 副本的父 EA,看协作后的结果差异。

◍ 把放量惯性思路落成可跑的EA规则

思路原型很简单:某品种密集交易时,单位时间价格波动往往比清淡时段更大;一旦发现放量且价格已朝某方向动,后续短周期内沿原方向延续的概率偏高。外汇与贵金属杠杆高、跳空频繁,这种惯性只是统计倾向,不是必然。 要把它变成策略,先得定义“放量”。直接用固定分时成交量不行,不同品种强度差太大。原文取最近 K 根已收盘烛形的成交量均值 V_avr 作基准,当前烛形量 V 超过 V_avr*(1+D+N*D_add) 才触发,D 控首单阈值、D_add 控加仓阈值、N 是已挂未平单数。 订单形态选 BUY_STOP / SELL_STOP:价格上涨就挂突破买停,下跌挂卖停,避开回调单的不确定性。每单独立挂、独立到期删单;成头寸后只靠止损止盈出。因 N 计的是订单与头寸总数、不加仓原单,这套逻辑只能跑在逐仓对冲账户上,否则净值核算会乱。 下面这段是原文给的判定骨架,可直接塞进 MT5 EA 的 OnTick 前段做条件门: [CODE] // 计算最近 K 根收盘烛形平均成交量 double V_avr = 0; for(int i=1; i<=K; i++) V_avr += Volume[i]; V_avr /= K; // 当前烛形成交量 double V = Volume[0]; // 已开挂单与头寸数 int N = OrdersTotal() + PositionsTotal(); // 放量突破条件 if(N < N_max && V > V_avr * (1 + D + N * D_add)){ double dir = Close[0] > Open[0] ? 1 : -1; // 涨为正 // 涨: BUY_STOP 跌: SELL_STOP,距离/止损/止盈/到期由外部参数给 } [/CODE] K、D、D_add、距离、止损止盈、到期分钟、N_max、单量都是输入参数。实盘前建议先开 MT5 策略测试器,用 XAUUSD 的 M15 跑三年数据,看 D=1、D_add=0.5 时回撤是否吃得住——参数自由度给了,但盲调就是送钱。

MQL5 / C++
class=class="str">"cmt">// 计算最近 K 根收盘烛形平均成交量
class="type">class="kw">double V_avr = class="num">0;
for(class="type">int i=class="num">1; i<=K; i++) V_avr += Volume[i];
V_avr /= K;
class=class="str">"cmt">// 当前烛形成交量
class="type">class="kw">double V = Volume[class="num">0];
class=class="str">"cmt">// 已开挂单与头寸数
class="type">int N = OrdersTotal() + PositionsTotal();
class=class="str">"cmt">// 放量突破条件
if(N < N_max && V > V_avr * (class="num">1 + D + N * D_add)){
   class="type">class="kw">double dir = Close[class="num">0] > Open[class="num">0] ? class="num">1 : -class="num">1; class=class="str">"cmt">// 涨为正
   class=class="str">"cmt">// 涨: BUY_STOP 跌: SELL_STOP,距离/止损/止盈/到期由外部参数给
}

从参数分组到挂单逻辑的EA骨架

写 EA 第一步是把输入参数按用途分组,并用 input group 注释隔离。这样在 MT5 参数对话框和策略测试器里,每组下面挂自己的变量,比一堆同名缩写好读得多。默认先给一组能跑的值,真正的最优解留到优化阶段再找。 涉及下单就要有 CTrade 全局对象,它在函数外声明,整个 EA 都能调。CSymbolInfo 用来抓当前品种报价与属性,COrderInfo / CPositionInfo 则负责遍历挂单与持仓,数量存进 countOrders / countPositions 两个全局变量。 平均成交量用 iVolumes 指标算,句柄存全局,再把缓冲区拷进预分配数组。OnInit 里加载指标、按 K 线数定数组大小并设为时间序列寻址、给交易对象设幻数。OnTick 先调 UpdateCounts 只数自己幻数的单,再用 SignalForOpen 返回 +1 / 0 / -1 决定挂 BUY_STOP 还是 SELL_STOP,OpenBuyOrder / OpenSellOrder 分开写。 优化 EURGBP H1、2018-01-01 至 2023-01-01、起始 100,000 美元、最小 0.01 手,两组参数净收益分别约 260 美元与 666 美元。换经纪商报价结果可能偏差明显,外汇与贵金属属高风险,回测不代表实盘。 用大本金测不是炫富:若固定手数遇上大于余额的回撤,测试会提前死掉,你根本不知道调小手数就能活。例子里 1,000 美元起步赚到 11,000 但峰值回撤 2,000,只是运气好回撤晚来;若早来就爆仓。按 10% 允许回撤反推,起始至少 20,000 美元才扛得住,此时收益率降到 50%。 两张参数表按 10,000 美元、10% 回撤重算:第一组回撤 28.70(0.04%)可加仓到 0.34 手,收益约 9,073(91%);第二组回撤 92.72(0.09%)只能上 0.10 手,收益约 7,185(72%)。第一组绝对收益低但回撤小,反而有加仓空间。

MQL5 / C++
input group "===  Opening signal parameters"
input class="type">int      signalPeriod_        = class="num">48;     class=class="str">"cmt">// Number of candles for volume averaging 
input class="type">class="kw">double   signalDeviation_     = class="num">1.0;    class=class="str">"cmt">// Relative deviation from the average to open the first order 
input class="type">class="kw">double   signaAddlDeviation_  = class="num">1.0;    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">200;    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">75;     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
input group "===  EA parameters"
input class="type">class="kw">ulong    magicN_              = class="num">27181;  class=class="str">"cmt">// Magic

「用标准交易类接管订单与仓位计数」

EA 里直接调 CTrade、CPositionInfo 这些标准类,比自己写裸函数省事且不易出错。下面这段声明把交易执行、品种属性、挂单与持仓信息全部封装成对象,再配两个 int 计数器记录当前 Magic 下的挂单数与持仓数。 OnInit 中先用 iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK) 取 tick 成交量句柄;ArrayResize 按 signalPeriod_ 定长,ArraySetAsSeries 设成时间序列方便取最新值;trade.SetExpertMagicNumber(magicN_) 把后续所有下单绑定到同一魔术码,便于过滤自有订单。 UpdateCounts 每次重置双计数器,分别用 PositionsTotal 和 OrdersTotal 遍历,靠 SelectByIndex + Magic 比对只统计本 EA 的仓与单。OnTick 里先刷新计数,当 countOrders+countPositions 小于 maxCountOfOrders_ 才允许 SignalForOpen 出新信号,signal==1 走 OpenBuyOrder,signal==-1 走 OpenSellOrder,避免超量挂单。 外汇与贵金属杠杆高,这类自动计数逻辑若魔术码设错,可能把手动单也算进去,实盘前务必在 MT5 策略测试器用历史数据验证计数边界。

MQL5 / C++
CTrade             trade;             class=class="str">"cmt">// Object for performing trading operations 
CSymbolInfo       symbolInfo;         class=class="str">"cmt">// Object for obtaining data on the symbol properties
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

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="type">int OnInit() {
   class=class="str">"cmt">// Load the indicator to get tick volumes
   iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK);
   
   class=class="str">"cmt">// Set the size of the tick volume receiving array and the required addressing
   ArrayResize(volumes, signalPeriod_);
   ArraySetAsSeries(volumes, true);
   class=class="str">"cmt">// Set Magic Number for placing orders via &class="macro">#x27;trade&class="macro">#x27;
   trade.SetExpertMagicNumber(magicN_);
   
   class="kw">return(INIT_SUCCEEDED);
}
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="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="macro">#include <Trade\OrderInfo.mqh>
class="macro">#include <Trade\PositionInfo.mqh>
class="macro">#include <Trade\SymbolInfo.mqh>
class="macro">#include <Trade\Trade.mqh>
input group "===  Opening signal parameters"

◍ 把挂单参数和风控变量落到代码里

这段输入声明把前面说的成交量偏离逻辑转成了可直接编译的 EA 骨架。signalPeriod_ 设 48,代表用最近 48 根 K 线的 tick 成交量做均值基准;signalDeviation_ 与 signaAddlDeviation_ 都是 1.0,即价格相对均量偏离 100% 时触发首单,后续加仓同样按 100% 偏离阈值处理。 挂单组里 openDistance_=200 点、stopLevel_=2000 点、takeLevel_=75 点,这组数字意味着止损空间是止盈的 26 倍以上,属于典型网格逆势思路,外汇与贵金属品种在高波动时段可能快速扫损。ordersExpiration_=6000 分钟(约 4.16 天)控制挂单存活期。 资金管理上 maxCountOfOrders_=3、fixedLot_=0.01,单品种同时最多 3 张 0.01 手单,magicN_=27181 用来隔离本 EA 订单。下面代码逐行拆出初始化段:先取当前品种 tick 成交量句柄,再把 volumes 数组按 48 长度重置并设为时间序列寻址,方便后面按索引取最近 N 根量。

MQL5 / C++
input class="type">int      signalPeriod_       = class="num">48;    class=class="str">"cmt">// Number of candles for volume averaging
input class="type">class="kw">double    signalDeviation_    = class="num">1.0;    class=class="str">"cmt">// Relative deviation from the average to open the first order
input class="type">class="kw">double    signaAddlDeviation_ = class="num">1.0;    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">200;    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">75;     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
input group "===  EA parameters"
input class="type">class="kw">ulong     magicN_             = class="num">27181;  class=class="str">"cmt">// Magic
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=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Initialization function of the expert                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
class=class="str">"cmt">// Load the indicator to get tick volumes
   iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK);
class=class="str">"cmt">// Set the size of the tick volume receiving array and the required addressing
   ArrayResize(volumes, signalPeriod_);
   ArraySetAsSeries(volumes, true);
class=class="str">"cmt">// Set Magic Number for placing orders via &class="macro">#x27;trade&class="macro">#x27;

常见问题

会。先框定每套策略负责的品种和仓位上限,用参数分组隔离,避免共用同一笔风控变量。
把‘放量+价格惯性’写成入场条件,再配挂单参数与止损位,用标准交易类接管下单和仓位计数。
小布可对接品种页做AIGC诊断,提示策略间仓位重叠与风控变量越界,你只需打开对应页查看。
挂单参数单独成组,风控变量(如总敞口、单笔上限)独立声明,初始化时分别赋默认值再传入交易类。
是仓位计数没接管好,导致重复开仓。用标准交易类统一管订单与计数,比自己写循环稳得多。