开发多币种 EA 交易(第 1 部分):多种交易策略的协作(基础篇)
让多个策略在同一终端共存
做 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 时回撤是否吃得住——参数自由度给了,但盲调就是送钱。
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%)。第一组绝对收益低但回撤小,反而有加仓空间。
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 策略测试器用历史数据验证计数边界。
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 根量。
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;