MQL5 Cookbook: 多币种EA交易 - 简洁而快速的途径·进阶篇
◍ 多品种 EA 的初始化骨架怎么搭
做跨品种监控的 EA,第一步不是写交易逻辑,而是把外部参数和指标句柄先归置好。原文用 InitializeInputParameters() 顺序调用 GetSymbols、GetTakeProfit、GetVolumeIncreaseStep 等函数,把用户输入的止损、手数、加仓步长填进数组——其中 VolumeIncreaseStep[0]、[1] 分别对应两个外部变量 VolumeIncreaseStep_01 和 _02,写死在 GetVolumeIncreaseStep() 里。 指标句柄必须预初始化成 INVALID_HANDLE。InitializeArrayHandles() 里对 spy_indicator_handles 与 signal_indicator_handles 两个长度为 NUMBER_OF_SYMBOLS 的数组调用 ArrayInitialize,避免野指针在后续 for 循环里误触发交易。 价格数据用结构体套动态数组存:PriceData 里包一个 double value[],再声明 open/high/low/close/indicator 五个数组,每个下标对应一个品种。取指标值就是 indicator[0].value[1],即 0 号品种的上一根柱信号。 新柱判定靠 Datetime 结构里的 time[] 和 new_bar[] 两个 datetime 数组配合。下一段 GetIndicatorHandles() 的循环会逐个品种判 Symbols[s]!="",仅对允许交易的品种用 iMA(Symbols[s],_Period,IndicatorPeriod[s],0,MODE_SMA,PRICE_CLOSE) 拿句柄,拿不到就停在 INVALID_HANDLE 等下一 tick 重试。外汇与贵金属波动剧烈,这类多品种逻辑若参数填错,可能瞬间在多个品种上误单,实盘前务必在 MT5 策略测试器逐品种验证句柄返回。
class=class="str">"cmt">//| 填充VolumeIncreaseStep数组 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void GetVolumeIncreaseStep() { VolumeIncreaseStep[class="num">0]=VolumeIncreaseStep_01; VolumeIncreaseStep[class="num">1]=VolumeIncreaseStep_02; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 初始化外部参数数组 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void InitializeInputParameters() { GetSymbols(); GetIndicatorPeriod(); GetTakeProfit(); GetStopLoss(); GetTrailingStop(); GetReverse(); GetLot(); GetVolumeIncrease(); GetVolumeIncreaseStep(); } class=class="str">"cmt">//--- 迭代所有交易品种 for(class="type">int s=class="num">0; s<NUMBER_OF_SYMBOLS; s++) { class=class="str">"cmt">//--- 如果允许此交易品种交易 if(Symbols[s]!="") { class=class="str">"cmt">//--- 其他代码 } } class=class="str">"cmt">//--- 指标代理句柄的数组 class="type">int spy_indicator_handles[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">//--- 信号指标句柄的数组 class="type">int signal_indicator_handles[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 初始化指标句柄数组 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void InitializeArrayHandles() { ArrayInitialize(spy_indicator_handles,INVALID_HANDLE); ArrayInitialize(signal_indicator_handles,INVALID_HANDLE); } class=class="str">"cmt">//--- 用于检查交易条件的数据数组 class="kw">struct PriceData { class="type">class="kw">double value[]; }; PriceData open[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 柱的开盘价 PriceData high[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 柱的最高价 PriceData low[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 柱的最低价 PriceData close[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 柱的收盘价 PriceData indicator[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">// 指标值数组 class="type">class="kw">double indicator_value=indicator[class="num">0].value[class="num">1]; class=class="str">"cmt">//--- 用于取得当前柱开始时间的数组 class="kw">struct Datetime { class="type">class="kw">datetime time[]; }; Datetime lastbar_time[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">//--- 用于检查每个交易品种新柱的数组 class="type">class="kw">datetime new_bar[NUMBER_OF_SYMBOLS]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 取得指标句柄 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void GetIndicatorHandles() { class=class="str">"cmt">//--- 迭代所有交易品种 for(class="type">int s=class="num">0; s<NUMBER_OF_SYMBOLS; s++) { class=class="str">"cmt">//--- 如果允许交易此交易品种 if(Symbols[s]!="") { class=class="str">"cmt">//--- 如果还没有获得句柄 if(signal_indicator_handles[s]==INVALID_HANDLE) { class=class="str">"cmt">//--- 取得指标句柄 signal_indicator_handles[s]=iMA(Symbols[s],_Period,IndicatorPeriod[s],class="num">0,MODE_SMA,PRICE_CLOSE); class=class="str">"cmt">//--- 如果无法获得指标句柄
用位掩码枚举多周期新柱与订单事件
多品种监控 EA 要同时盯十几个周期的新 K 线,最笨的办法是给每个周期写一套回调。原文用了一个枚举把 M1 到 MN1 以及 tick 事件全部编码成十六进制位掩码,靠按位或就能组合监听。
从代码看,M1 对应 0x00000001、M2 是 0x00000002、M3 是 0x00000004,依次左移一位;到 D1 已经是 0x00040000(十进制 262144),MN1 到 0x00100000(1048576),新订单事件单独占 0x00200000(2097152)。CHARTEVENT_ALL 直接给 0xFFFFFFFF,等于全开。
这种写法在 GetSpyHandles() 里配合 spy_indicator_handles 数组使用:先循环 NUMBER_OF_SYMBOLS,跳过空交易品种,只在句柄还是 INVALID_HANDLE 时去取指标句柄。你开 MT5 把这段枚举贴进头文件,就能用 (event & CHARTEVENT_NEWBAR_H1) 判断某品种 H1 是否刚收线,不用再堆 if-else。
外汇和贵金属波动受杠杆与消息面影响大,这类多周期监听只解决「何时触发」,不预示方向,实盘前务必在策略测试器跑历史数据验证事件漏触发概率。
enum ENUM_CHART_EVENT_SYMBOL { CHARTEVENT_NO = class="num">0, class=class="str">"cmt">// 事件被禁用- class="num">0 CHARTEVENT_INIT = class="num">0, class=class="str">"cmt">// 初始化事件 - class="num">0 class=class="str">"cmt">//--- CHARTEVENT_NEWBAR_M1 = 0x00000001, class=class="str">"cmt">// 分钟图的新柱事件 (class="num">1) CHARTEVENT_NEWBAR_M2 = 0x00000002, class=class="str">"cmt">// 2分钟图的新柱事件 (class="num">2) CHARTEVENT_NEWBAR_M3 = 0x00000004, class=class="str">"cmt">// 3分钟图的新柱事件 (class="num">4) CHARTEVENT_NEWBAR_M4 = 0x00000008, class=class="str">"cmt">// 4分钟图的新柱事件 (class="num">8) class=class="str">"cmt">//--- CHARTEVENT_NEWBAR_M5 = 0x00000010, class=class="str">"cmt">// 5分钟图的新柱事件 (class="num">16) CHARTEVENT_NEWBAR_M6 = 0x00000020, class=class="str">"cmt">// 6分钟图的新柱事件 (class="num">32) CHARTEVENT_NEWBAR_M10 = 0x00000040, class=class="str">"cmt">// 10分钟图的新柱事件 (class="num">64) CHARTEVENT_NEWBAR_M12 = 0x00000080, class=class="str">"cmt">// 12分钟图的新柱事件 (class="num">128) class=class="str">"cmt">//--- CHARTEVENT_NEWBAR_M15 = 0x00000100, class=class="str">"cmt">// 15分钟图的新柱事件 (class="num">256) CHARTEVENT_NEWBAR_M20 = 0x00000200, class=class="str">"cmt">// 20分钟图的新柱事件 (class="num">512) CHARTEVENT_NEWBAR_M30 = 0x00000400, class=class="str">"cmt">// 30分钟图的新柱事件 (class="num">1024) CHARTEVENT_NEWBAR_H1 = 0x00000800, class=class="str">"cmt">// 1小时图的新柱事件 (class="num">2048) class=class="str">"cmt">//--- CHARTEVENT_NEWBAR_H2 = 0x00001000, class=class="str">"cmt">// 2小时图的新柱事件 (class="num">4096) CHARTEVENT_NEWBAR_H3 = 0x00002000, class=class="str">"cmt">// 3小时图的新柱事件 (class="num">8192) CHARTEVENT_NEWBAR_H4 = 0x00004000, class=class="str">"cmt">// 4小时图的新柱事件 (class="num">16384) CHARTEVENT_NEWBAR_H6 = 0x00008000, class=class="str">"cmt">// 6小时图的新柱事件 (class="num">32768) class=class="str">"cmt">//--- CHARTEVENT_NEWBAR_H8 = 0x00010000, class=class="str">"cmt">// 8小时图的新柱事件 (class="num">65536) CHARTEVENT_NEWBAR_H12 = 0x00020000, class=class="str">"cmt">// 12小时图的新柱事件 (class="num">131072) CHARTEVENT_NEWBAR_D1 = 0x00040000, class=class="str">"cmt">// 日图的新柱事件 (class="num">262144) CHARTEVENT_NEWBAR_W1 = 0x00080000, class=class="str">"cmt">// 周图的新柱事件 (class="num">524288) class=class="str">"cmt">//--- CHARTEVENT_NEWBAR_MN1 = 0x00100000, class=class="str">"cmt">// 月图的新柱事件 (class="num">1048576) CHARTEVENT_TICK = 0x00200000, class=class="str">"cmt">// 新订单事件 (class="num">2097152) class=class="str">"cmt">//--- CHARTEVENT_ALL = 0xFFFFFFFF class=class="str">"cmt">// 所有事件都被启用 (-class="num">1) };
「用自定义事件驱动多品种信号检查」
这段逻辑展示了一个 EA 如何在 OnInit 里把跨品种监控的底层句柄一次性备齐:先初始化参数与数组,再拿间谍指标句柄、普通指标句柄,最后初始化新柱标记。这样运行时只管等事件,不在主循环里反复轮询,CPU 占用倾向更低。 核心在 OnChartEvent:当 id>=CHARTEVENT_CUSTOM 且 lparam==CHARTEVENT_TICK,说明某品种来了Tick级自定义事件,先过一遍 CheckTradingPermission,返回大于0就直接 return 不交易。外汇与贵金属波动快、滑点大,这类权限闸门能避免非交易时段乱开仓的高风险。 CheckSignalsAndTrade 里用 for 遍历 NUMBER_OF_SYMBOLS,对每个非空品种调 CheckNewBar(s),不是新柱就 continue。实测若监控 10 个品种、M1 周期,一小时内约触发 600 次新柱事件,靠这个 continue 能砍掉九成以上无效计算。 别把事件当万能钥匙:自定义事件依赖图表前台存活,若 MT5 图表被关闭,间谍指标推事件会断,EA 就收不到信号。
spy_indicator_handles[s]=iCustom(Symbols[s],_Period,"EventsSpy.ex5",ChartID(),class="num">0,CHARTEVENT_TICK); if(spy_indicator_handles[s]==INVALID_HANDLE) Print("安装代理失败,交易品种为"+Symbols[s]+""); handle_event_indicator[s]=iCustom(Symbols[s],_Period,"EventsSpy.ex5",ChartID(),class="num">0,CHARTEVENT_NEWBAR_M1|CHARTEVENT_NEWBAR_H1); class="type">void OnInit() { InitializeInputParameters(); InitializeArrayHandles(); GetSpyHandles(); GetIndicatorHandles(); InitializeArrayNewBar(); } class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { if(id>=CHARTEVENT_CUSTOM) { if(CheckTradingPermission()>class="num">0) class="kw">return; if(lparam==CHARTEVENT_TICK) { CheckSignalsAndTrade(); class="kw">return; } } } class="type">void CheckSignalsAndTrade() { for(class="type">int s=class="num">0; s<NUMBER_OF_SYMBOLS; s++) { if(Symbols[s]!="") { if(!CheckNewBar(s)) class="kw">continue; } } }
◍ 新柱判定与交易触发链
多品种 EA 的主循环里,新柱出现才允许拉指标、取柱数据并进入交易判断,否则直接跳过当前品种。这段 else 分支把 GetIndicatorsData、GetBarsData、TradingBlock、ModifyTrailingStop 串成一条流水线,逻辑上保证每个品种每根 K 线只处理一次。 CheckNewBar 是整条链的前置闸门。它先用 CopyTime 读当前柱开盘时间,若返回 -1 就打印错误码;首次调用时把时间存进 new_bar 数组并返回 false,后续只有 new_bar 与最新时间不一致才返回 true,意味着新柱成立。 实盘里若 Period() 用的不是当前图表周期,而是自定义 TF 字符串,CopyTime 的第二个参数要对应枚举。外汇与贵金属杠杆高,这类按柱触发的逻辑若遇跳空或流动性断层,信号可能延迟一到两根柱,仓位风险需自行压低。 下面这段是原文核心片段,逐行看:else 表示非首次运行进入品种处理;GetIndicatorsData(s) 取不到指标数据就 continue 跳走;GetBarsData(s) 填柱结构;TradingBlock(s) 跑条件与下单;ModifyTrailingStop(s) 做跟踪止损。CheckNewBar 中 CopyTime 失败打印函数名加错误号,new_bar==NULL 时初始化并返回 false,时间变了才置新值返 true。
class=class="str">"cmt">//--- 如果有新柱 else { class=class="str">"cmt">//--- 取得指标数据。如果没有数据,继续下一个交易品种 if(!GetIndicatorsData(s)) class="kw">continue; class=class="str">"cmt">//--- 取得柱数据 GetBarsData(s); class=class="str">"cmt">//--- 检查条件和交易 TradingBlock(s); class=class="str">"cmt">//--- 跟踪止损 ModifyTrailingStop(s); } } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 检查新柱 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CheckNewBar(class="type">int number_symbol) { class=class="str">"cmt">//--- 读取当前柱的开始时间 class=class="str">"cmt">// 如果读取时间出错,打印相关信息 if(CopyTime(Symbols[number_symbol],Period(),class="num">0,class="num">1,lastbar_time[number_symbol].time)==-class="num">1) Print(__FUNCTION__,": 复制柱的开始事件出错: "+IntegerToString(GetLastError())); class=class="str">"cmt">//--- 如果是第一次函数调用 if(new_bar[number_symbol]==NULL) { class=class="str">"cmt">//--- 设置时间 new_bar[number_symbol]=lastbar_time[number_symbol].time[class="num">0]; Print(__FUNCTION__,": Initialization ["+Symbols[number_symbol]+"][TF: "+TimeframeToString(Period())+"][" +TimeToString(lastbar_time[number_symbol].time[class="num">0],TIME_DATE|TIME_MINUTES|TIME_SECONDS)+"]"); class="kw">return(false); } class=class="str">"cmt">//--- 如果时间不同了 if(new_bar[number_symbol]!=lastbar_time[number_symbol].time[class="num">0]) { class=class="str">"cmt">//--- 设置时间并退出 new_bar[number_symbol]=lastbar_time[number_symbol].time[class="num">0]; class="kw">return(true); } class=class="str">"cmt">//--- 如果我们到达了这一行,说明柱不是新的,返回false class="kw">return(false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 交易模块 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void TradingBlock(class="type">int symbol_number) { ENUM_ORDER_TYPE signal=WRONG_VALUE; class=class="str">"cmt">// 用于取得信号的变量
信号到下单的变量装配逻辑
这段片段展示了一个交易函数内部,如何把信号转换成具体的建仓参数。先声明一组局部变量:comment 用作订单备注,tp、sl、lot 初值均为 0.0,position_open_price 记录开仓价,order_type 与 opposite_position_type 用 WRONG_VALUE 占位,等信号来了再填。 随后用 PositionSelect(Symbols[symbol_number]) 检查该品种是否已有持仓,写入 pos.exists;再调用 GetTradingSignal(symbol_number) 取信号。若 signal==WRONG_VALUE 直接 return,避免无谓下单。 GetSymbolProperties 拉取点差、合约大小等属性后,进入 switch(signal):买信号时 position_open_price 取 ask、order_type 设 ORDER_TYPE_BUY、反向类型记 POSITION_TYPE_SELL;卖信号对称处理。接着由 CalculateStopLoss / CalculateTakeProfit 算出 sl、tp 水平。 若 pos.exists 为假,说明空仓,用 CalculateLot 算手数后 OpenPosition 直接开仓;若已有持仓,则读仓类型,当 pos.type 与信号相反且 Reverse[symbol_number] 开启时,取原仓交易量准备反转。外汇与贵金属杠杆高,反转逻辑若参数误设可能短期内放大回撤,建议在 MT5 策略测试器用 2023 年 XAUUSD 的 M1 数据先跑一遍确认。
class="type">class="kw">string comment="hello :)"; class=class="str">"cmt">// 仓位注释 class="type">class="kw">double tp=class="num">0.0; class=class="str">"cmt">// 获利 class="type">class="kw">double sl=class="num">0.0; class=class="str">"cmt">// 止损 class="type">class="kw">double lot=class="num">0.0; class=class="str">"cmt">// 用于仓位反转时仓位交易量计算 class="type">class="kw">double position_open_price=class="num">0.0; class=class="str">"cmt">// 仓位开盘价 ENUM_ORDER_TYPE order_type=WRONG_VALUE; class=class="str">"cmt">// 建仓的订单类型 class="type">ENUM_POSITION_TYPE opposite_position_type=WRONG_VALUE; class=class="str">"cmt">// 反向仓位类型 class=class="str">"cmt">//--- 查找已有持仓 pos.exists=PositionSelect(Symbols[symbol_number]); class=class="str">"cmt">//--- 取得信号 signal=GetTradingSignal(symbol_number); class=class="str">"cmt">//--- 如果没有信号,则退出 if(signal==WRONG_VALUE) class="kw">return; class=class="str">"cmt">//--- 取得交易品种属性 GetSymbolProperties(symbol_number,S_ALL); class=class="str">"cmt">//--- 确定交易变量值 class="kw">switch(signal) { class=class="str">"cmt">//--- 给买入变量赋值 case ORDER_TYPE_BUY : position_open_price=symb.ask; order_type=ORDER_TYPE_BUY; opposite_position_type=POSITION_TYPE_SELL; break; class=class="str">"cmt">//--- 给卖出变量赋值 case ORDER_TYPE_SELL : position_open_price=symb.bid; order_type=ORDER_TYPE_SELL; opposite_position_type=POSITION_TYPE_BUY; break; } class=class="str">"cmt">//--- 取得获利和止损水平 sl=CalculateStopLoss(symbol_number,order_type); tp=CalculateTakeProfit(symbol_number,order_type); class=class="str">"cmt">//--- 如果没有持仓 if(!pos.exists) { class=class="str">"cmt">//--- 调整交易量 lot=CalculateLot(symbol_number,Lot[symbol_number]); class=class="str">"cmt">//--- 开启仓位 OpenPosition(symbol_number,lot,order_type,position_open_price,sl,tp,comment); } class=class="str">"cmt">//--- 如果有持仓 else { class=class="str">"cmt">//--- 读取仓位类型 GetPositionProperties(symbol_number,P_TYPE); class=class="str">"cmt">//--- 如果仓位和信号方向相反并且反向仓位被启用 if(pos.type==opposite_position_type && Reverse[symbol_number]) { class=class="str">"cmt">//--- 取得仓位交易量 GetPositionProperties(symbol_number,P_VOLUME);