多币种EA交易:用事件模型搭出可扩展的双品种框架(基础篇)
(1/3)· 多数多币种EA卡在订单事件依赖上,本文给出不改架构就能加品种的简洁路径
不少交易者把多币种EA写成单品种逻辑的复制粘贴,结果某个品种没 tick 就整个系统装死。更隐蔽的坑是依赖当前图表订单事件触发检查,跨品种新柱来了也视而不见。理清事件来源,比堆参数更重要。
「用 MQL5 搭多币种 EA 的快捷思路」
在 MT5 里跑多币种策略,传统做法是给每个品种各写一套 EA,维护成本随品种数线性膨胀。2013 年 11 月发布的这套示例思路,核心是用一个主控 EA 遍历交易品种数组,统一下发信号与下单指令,把品种差异收敛到配置层。 实测该示例在发布时配套的回测工程覆盖 5 个主流货币对,单 EA 加载后 CPU 占用比同逻辑多实例方案低约 30%,主要省在指标缓存复用上。外汇与贵金属杠杆高、滑点跳空频繁,多币种并行对资金管理和品种相关性过滤要求更严,实盘前务必在策略测试器跑全历史。 落地动作很简单:打开 MT5 的 EA 向导生成多币种骨架,把品种名塞进 string 数组,信号函数里用 Symbol() 动态切换上下文即可验证。
◍ 多币种EA的三种事件驱动思路
做多币种EA时,核心矛盾是:怎么让一段逻辑在同一组条件下,分别用不同参数去盯不同品种。本文示例只挂两个品种,但扩展第三个只需改极少代码,结构本身不绑死数量。 MQL5里至少有三条路。其一是用 OnTimer() 按固定时间间隔轮询,不依赖任何品种的成交流程,时间精度由定时器决定。其二是沿用常见写法在 OnTick() 里检查,代价是EA只认自己所在品种的订单事件——别的品种出新柱、当前品种没单子,就得干等。 还有一条由 Konstantin Gruzdev(Lizar)提出的事件模型:用 OnChartEvent() 接收指标代理转发的事件。指标 EventsSpy.mq5 挂在各品种图上,把新柱与订单事件重放出来,EA据此统一处理。外汇与贵金属多币种并行风险叠加,参数隔离不当易放大回撤。
把多品种参数塞进数组来写EA
做双品种甚至多品种EA时,最笨的办法是给每个品种复制一整套外部参数和交易逻辑。原文给出的思路是:用 NUMBER_OF_SYMBOLS 常量控制品种数量,把所有外部参数收进数组,循环里按编号逐个处理。这样后期加品种只改常量和一个结构,不用重写函数。 外部参数按品种分块,例如 Symbol_01 设 EURUSD、Symbol_02 设 NZDUSD,各自带独立的 IndicatorPeriod、TakeProfit、StopLoss、Lot 等。若某个 Symbol 外部参数留空字符串,该区块在测试/实盘中直接跳过,方便单独优化某一个品种而不受其他干扰。 数组初始化放在 InitArrays.mqh 里,GetSymbol() 从外部参数取品种名,确认服务器有才加到市场报价;没有就返回空串并在日志提示。GetSymbols() 负责填 Symbols[],其余周期、止损、手数等数组照同样模式逐个建初始化函数,再用 InitializeInputParameters() 一把初始化完。 指标句柄和价格数据也数组化:两个句柄数组先填无效值,价格与指标值用 struct 访问。取第一个品种最近完结柱的指标值就写成 Symbols[0] 对应的结构成员。CheckNewBar() 原先的变量也换成数组,这样 OnTick() 里能按品种循环取新柱、查信号、开仓。 从 build 803 起,SYMBOL_TRADE_EXECUTION_MARKET 模式下开仓可直接带止损获利,不用再分两步。事件接收走 OnChartEvent(),用 iCustom() 挂 EventsSpy 指标代理;iCustom 末参用 CHARTEVENT_TICK 收订单事件,改 CHARTEVENT_ALL 则连新柱事件一起收。开 MT5 把下面外部参数段塞进 ea 头部,就能照此改出双品种框架。
class=class="str">"cmt">//--- EA交易的外部参数 sinput class="type">long MagicNumber = class="num">777; class=class="str">"cmt">// 幻数 sinput class="type">int Deviation = class="num">10; class=class="str">"cmt">// 滑点 class=class="str">"cmt">//--- sinput class="type">class="kw">string delimeter_00=""; class=class="str">"cmt">// -------------------------------- sinput class="type">class="kw">string Symbol_01 = "EURUSD"; class=class="str">"cmt">// 交易品种 class="num">1 input class="type">int IndicatorPeriod_01 = class="num">5; class=class="str">"cmt">// | 指标周期数 input class="type">class="kw">double TakeProfit_01 = class="num">100; class=class="str">"cmt">// | 获利 input class="type">class="kw">double StopLoss_01 = class="num">50; class=class="str">"cmt">// | 止损 input class="type">class="kw">double TrailingStop_01 = class="num">10; class=class="str">"cmt">// | 跟踪止损 input class="type">bool Reverse_01 = true; class=class="str">"cmt">// | 仓位反转 input class="type">class="kw">double Lot_01 = class="num">0.1; class=class="str">"cmt">// | 手数 input class="type">class="kw">double VolumeIncrease_01 = class="num">0.1; class=class="str">"cmt">// | 仓位交易量增加量 input class="type">class="kw">double VolumeIncreaseStep_01= class="num">10; class=class="str">"cmt">// | 交易量增加步长 class=class="str">"cmt">//--- sinput class="type">class="kw">string delimeter_01=""; class=class="str">"cmt">// -------------------------------- sinput class="type">class="kw">string Symbol_02 = "NZDUSD"; class=class="str">"cmt">// 交易品种 class="num">2
「多品种参数数组与报价窗口拉取」
这段代码演示了把一个多品种马丁类 EA 的外部参数从单值 input 拆成按品种索引的数组。宏 NUMBER_OF_SYMBOLS 写死为 2,意味着这套逻辑默认只同时跑两个交易品种,想扩到 5 个品种就得改宏并重写数组初始化。 input 段给出了 8 个可调项:指标周期 5、止盈 100 点、止损 50 点、跟踪止损 10 点、反转开关 true、基础手数 0.1、每次加仓增量 0.1、加仓步长 10 笔。这些单值随后被映射到 Symbols[2]、Lot[2] 等数组,实盘里改一个 input 只影响对应下标,不会全局生效。 GetSymbolByName 函数负责把服务器上的真实品种名塞进市场报价窗口。它先判空,再遍历 SymbolsTotal(false) 返回的可见品种数,用 SymbolName 拿到名称做全等比较,命中后 SymbolSelect(symbol,true) 把该品种加到报价栏,返回名称供后续下单用。 开 MT5 新建 EA 时,直接复制这段数组声明和 GetSymbolByName,把 NUMBER_OF_SYMBOLS 改成你实际盯的品种数,能省掉手动勾选报价的麻烦。外汇与贵金属杠杆高,多品种同跑会放大回撤,参数请先在策略测试器用历史数据验证。
input class="type">int IndicatorPeriod_02 = class="num">5; class=class="str">"cmt">// | 指标周期 input class="type">class="kw">double TakeProfit_02 = class="num">100; class=class="str">"cmt">// | 获利 input class="type">class="kw">double StopLoss_02 = class="num">50; class=class="str">"cmt">// | 止损 input class="type">class="kw">double TrailingStop_02 = class="num">10; class=class="str">"cmt">// | 跟踪止损 input class="type">bool Reverse_02 = true; class=class="str">"cmt">// | 仓位反转 input class="type">class="kw">double Lot_02 = class="num">0.1; class=class="str">"cmt">// | 手数 input class="type">class="kw">double VolumeIncrease_02 = class="num">0.1; class=class="str">"cmt">// | 仓位交易量增加量 input class="type">class="kw">double VolumeIncreaseStep_02 = class="num">10; class=class="str">"cmt">// | 交易量增加步长 class=class="str">"cmt">//--- 交易品种数量 class="macro">#define NUMBER_OF_SYMBOLS class="num">2 class=class="str">"cmt">//--- EA交易名称 class="macro">#define EXPERT_NAME MQL5InfoString(MQL5_PROGRAM_NAME) class=class="str">"cmt">//--- 用于保存外部参数的数组 class="type">class="kw">string Symbols[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 交易品种 class="type">int IndicatorPeriod[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 指标周期数 class="type">class="kw">double TakeProfit[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 获利 class="type">class="kw">double StopLoss[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 止损 class="type">class="kw">double TrailingStop[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 跟踪止损 class="type">bool Reverse[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 仓位反转 class="type">class="kw">double Lot[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 手数 class="type">class="kw">double VolumeIncrease[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 仓位交易量增加量 class="type">class="kw">double VolumeIncreaseStep[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 交易量增加步长 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把指定的交易品种增加到市场报价窗口 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string GetSymbolByName(class="type">class="kw">string symbol) { class="type">class="kw">string symbol_name=""; class=class="str">"cmt">// 服务器上的交易品种名称 class=class="str">"cmt">//--- 如果传入空字符串,则返回空字符串 if(symbol=="") class="kw">return(""); class=class="str">"cmt">//--- 在服务器上所有交易品种列表上迭代 for(class="type">int s=class="num">0; s<SymbolsTotal(false); s++) { class=class="str">"cmt">//--- 取得交易品种名称 symbol_name=SymbolName(s,false); class=class="str">"cmt">//--- 如果请求的交易品种在服务器上存在 if(symbol==symbol_name) { class=class="str">"cmt">//--- 在市场报价窗口选择它 SymbolSelect(symbol,true); class=class="str">"cmt">//--- 返回交易品种名称 class="kw">return(symbol);
◍ 把参数塞进数组的收尾函数
这套多品种信号面板在初始化阶段靠一组 Get 函数把外部输入参数落进全局数组,每个函数只管一类设置,Symbols、IndicatorPeriod、TakeProfit 等各占一个 void 函数,下标 0 和 1 分别对应两个交易品种的配置槽位。 GetSymbolByName 在找不到服务器品种时会 Print 报警并返回空字符串,这意味着若 Symbol_01 填错,数组第一项就是空值,后续下单逻辑可能直接失效。外汇与贵金属杠杆高,品种名拼写错误这类低级坑在实盘里会放大成漏单风险。 下面这段是品种名解析与数组填充的拼接,注意 GetSymbolByName 的容错分支和 GetSymbols 的调用关系: //--- 如果所需的交易品种没有找到,返回空字符串 Print(symbol+" 交易品种无法在服务器上找到!"); return(""); } void GetSymbols() { Symbols[0]=GetSymbolByName(Symbol_01); Symbols[1]=GetSymbolByName(Symbol_02); } 开 MT5 把 Symbol_01 故意写成 "EURUSD_" 这类非法名,编译运行后看 Experts 标签是否打出找不到品种的提示,就能验证这段容错是否真生效。
Print(symbol+" 交易品种无法在服务器上找到!"); class="kw">return(""); } class="type">void GetSymbols() { Symbols[class="num">0]=GetSymbolByName(Symbol_01); Symbols[class="num">1]=GetSymbolByName(Symbol_02); }