MQL5 Cookbook: 多币种EA交易 - 简洁而快速的途径·进阶篇
📘

MQL5 Cookbook: 多币种EA交易 - 简洁而快速的途径·进阶篇

第 2/3 篇

◍ 多品种 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 策略测试器逐品种验证句柄返回。

MQL5 / C++
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。 外汇和贵金属波动受杠杆与消息面影响大,这类多周期监听只解决「何时触发」,不预示方向,实盘前务必在策略测试器跑历史数据验证事件漏触发概率。

MQL5 / C++
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 就收不到信号。

MQL5 / C++
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。

MQL5 / C++
      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 数据先跑一遍确认。

MQL5 / C++
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);

常见问题

用结构体数组存 symbol、timeframe、magic,在 OnInit 里循环调用 SeriesInfoInteger 预加载时序,失败就 Print 并 return INIT_FAILED,避免运行时才报错。
给每个品种分配一个 bit 位,新柱或订单事件触发时把对应位写 1,主循环用位与判断哪几个品种需要处理,清位即可,CPU 占用明显低于轮询。
可以,小布能按你设定的品种和周期做AIGC诊断,新柱和信号事件直接在品种页推送,你只看结论不用扒代码日志。
跨周末或断线重连时时间跳变会错位,应存每个品种上一根柱的 open time,用 iTime 取前值严格比对,才能稳定抓首根。
下单前必须填好 lotSize、slTick、tpTick、comment,且用 SymbolInfoDouble 校验最小手数和止损距离,否则订单会被经纪商拒绝。