创建多币种多系统 EA 交易·进阶篇
📘

创建多币种多系统 EA 交易·进阶篇

第 2/2 篇

EA 启动先把账户和品种摸清

多策略 EA 在 OnInit 里第一件事是拿账户级数值,比如杠杆率。它跟具体品种和策略无关,直接 Leverage=AccountInfo.Leverage() 存下来就行,没必要塞进数组里反复传。 外部参数建议统一倒进数组。像 BBPeriod_A0 这种用 uint 声明是为了堵住用户填负数,但指标函数 iBands 要的是 int,不转类型编译器会警告,所以写成 (int)BBPeriod_A0 再存数组最省事。 品种合法性必须查。下面这段代码遍历策略数组,若 IsTrade_A[i] 为 false 就跳过,否则调 IsSymbolInMarketWatch 确认该品种在“市场报价”里;找不到就 Print 报错并 ExpertRemove 终止 EA。 别让同一品种在多个策略里重复挂。Strategy_A>1 时会跑双重循环比对 Symbol_A[i]==Symbol_A[j],撞车就直接移除 EA。外汇和贵金属波动剧烈,这种初始化冲突若漏检,实盘可能倾向出现重复开仓和保证金占用翻倍。 通用动作放在 for 循环里做:校验输入参数、建指标句柄(例如 iBands 取 PRICE_HIGH)、算批量数据,最后用 CTrade 的 Trade_A 对象设交易参数。每套策略重复同一套流程即可。

MQL5 / C++
class=class="str">"cmt">//--- Get the leverage for the account
  Leverage=AccountInfo.Leverage();
class=class="str">"cmt">//--- Copy external variables to arrays
  Symbol_A[class="num">0]     =Symbol_A0;
  IsTrade_A[class="num">0]    =IsTrade_A0;
  Period_A[class="num">0]     =Period_A0;
  BBPeriod_A[class="num">0]   =(class="type">int)BBPeriod_A0;
  BBShift_A[class="num">0]    =BBShift_A0;
  BBDeviation_A[class="num">0]=BBDeviation_A0;
class=class="str">"cmt">//--- Check for the symbol in the Market Watch
  for(class="type">int i=class="num">0; i<Strategy_A; i++)
   {
     if(IsTrade_A[i]==class="kw">false) class="kw">continue;
     if(IsSymbolInMarketWatch(Symbol_A[i])==class="kw">false)
       {
        Print(Symbol_A[i]," could not be found on the server!");
        ExpertRemove();
       }
   }
class=class="str">"cmt">//--- Check whether the symbol is used more than once
  if(Strategy_A>class="num">1)
   {
     for(class="type">int i=class="num">0; i<Strategy_A-class="num">1; i++)
       {
         if(IsTrade_A[i]==class="kw">false) class="kw">continue;
         for(class="type">int j=i+class="num">1; j<Strategy_A; j++)
           {
             if(IsTrade_A[j]==class="kw">false) class="kw">continue;
             if(Symbol_A[i]==Symbol_A[j])
               {
                Print(Symbol_A[i]," is used more than once!");
                ExpertRemove();
               }
           }
       }
   }
class=class="str">"cmt">//--- The IsSymbolInMarketWatch() function
class="type">bool IsSymbolInMarketWatch(class="type">class="kw">string f_Symbol)
  {
   for(class="type">int s=class="num">0; s<SymbolsTotal(class="kw">false); s++)
     {
       if(f_Symbol==SymbolName(s,class="kw">false))
         class="kw">return(true);
     }
   class="kw">return(class="kw">false);
  }
class=class="str">"cmt">//--- General actions
  for(class="type">int i=class="num">0; i<Strategy_A; i++)
   {
     if(IsTrade_A[i]==class="kw">false) class="kw">continue;
     class=class="str">"cmt">//--- Check for errors in input parameters
     class=class="str">"cmt">//...
     class=class="str">"cmt">//--- Set indicator handles
     BB_handle_high_A[i]=iBands(Symbol_A[i],Period_A[i],BBPeriod_A[i],BBShift_A[i],BBDeviation_A[i],
                                 PRICE_HIGH);
     if(BB_handle_high_A[i]<class="num">0)
       {

「多品种EA的句柄失败与交易参数装配」

在把布林带指标挂到多个交易品种上时,若某个品种的高价轨句柄返回 INVALID_HANDLE,说明该品种可能不在当前经纪商可见品种列表里。代码里用 Print 打出 Symbol_A[i] 和 GetLastError(),随后直接 ExpertRemove() 卸载EA,避免后续对空句柄计算仓位。 对每个允许交易的品种,要先把 SymbolInfo 指向具体符号,再取 LotsMin、LotsMax、Point、ContractSize 存进数组。外汇与贵金属杠杆品种的最小手数常是 0.01,最大手数随经纪商从 10 到 1000 不等,Point 在 XAUUSD 常见为 0.01,这些数值开 MT5 品种规格窗口就能核对。 Trade_A 对象初始化时,SetExpertMagicNumber 区分策略,SetDeviationInPoints 给滑点容差,SetTypeFilling 用 ORDER_FILLING_RETURN 适配服务器,SetAsyncMode(true) 走 OrderSendAsync 非阻塞发单。LogLevel(1) 只保留关键日志,类内部其实会自选最优模式。 最后一段双重循环比对 Strategy_A 与 Strategy_B 里启用的符号,若同一品种出现在两套策略就 Print 报警并卸载。多策略共锁一个品种可能在同根账户里自相成交,概率上会增加无故回撤,实盘前应在策略配置里手动排重。

MQL5 / C++
Print("Failed to create a handle for Bollinger Bands based on High prices for ",Symbol_A[i]," . Handle=",INVALID_HANDLE,
 "\n Error=",GetLastError());
 ExpertRemove();
 }
 class=class="str">"cmt">//...
 class=class="str">"cmt">//--- Calculate data for the Lot
 class=class="str">"cmt">//--- set the name of the symbol for which the information will be obtained
 SymbolInfo.Name(Symbol_A[i]);
 class=class="str">"cmt">//--- minimum and maximum volume size in trading operations
 MinLot_A[i]=SymbolInfo.LotsMin();
 MaxLot_A[i]=SymbolInfo.LotsMax();
 class=class="str">"cmt">//--- point value
 Point_A[i]=SymbolInfo.Point();
 class=class="str">"cmt">//--- contract size
 ContractSize_A[i]=SymbolInfo.ContractSize();
 class=class="str">"cmt">//--- Set some additional parameters
 }
class=class="str">"cmt">//--- Set parameters for trading operations
class=class="str">"cmt">//--- set the magic number
 Trade_A.SetExpertMagicNumber(MagicNumber_A);
class=class="str">"cmt">//--- set the permissible slippage in points upon deal execution
 Trade_A.SetDeviationInPoints(Slippage_A);
class=class="str">"cmt">//--- order filling mode, use the mode that is allowed by the server
 Trade_A.SetTypeFilling(ORDER_FILLING_RETURN);
class=class="str">"cmt">//--- logging mode, it is advisable not to call this method as the class will set the optimal mode by itself
 Trade_A.LogLevel(class="num">1);
class=class="str">"cmt">//--- the function to be used for trading: true - OrderSendAsync(), class="kw">false - OrderSend().
 Trade_A.SetAsyncMode(true);
class=class="str">"cmt">//--- Check whether one and the same symbol is used in several strategies
 for(class="type">int i=class="num">0; i<Strategy_A; i++)
 {
 if(IsTrade_A[i]==class="kw">false) class="kw">continue;
 for(class="type">int j=class="num">0; j<Strategy_B; j++)
 {
 if(IsTrade_B[j]==class="kw">false) class="kw">continue;
 if(Symbol_A[i]==Symbol_B[j])
 {
 Print(Symbol_A[i]," is used in several strategies!");
 ExpertRemove();
 }
 }
 }

◍ 多品种循环里 continue 与 break 的分工

在 OnTimer() 里跑多交易品种扫描时,单品种 EA 常用 return 直接掐掉后续计算;但多品种框架下这么做会连累其他品种,正确做法是当前品种不满足条件就 continue,只跳过这一轮迭代。 想给多策略 EA 加新策略,且新策略内部本身带“终止全部计算”的 for 循环,可以套一层标志位:内层 break 置 IsInterrupt=true,出内层循环后判断若为真就 continue,从而只放弃当前策略的本次迭代而非整个定时器逻辑。 变量替换是移植旧 EA 的关键一步:把 _Symbol 换成 Symbol_A[i]、_Point 换成 Point_A[i],这些品种特有数值在初始化阶段写进数组即可。下面这段框架演示了连接检测、A/B 策略双循环以及内层中断模式。 平仓逻辑里有个可验证细节:当 Bid_price>=布林下轨[0] 或 DealNumber_A[A]==0 时倾向平掉 BUY 仓,CopyBuffer 返回值<=0 则 continue 跳过,避免脏数据下单。外汇与贵金属品种波动剧烈,这类定时器轮询策略仍存在滑点及断连风险。

MQL5 / C++
class="type">void OnTimer()
  {
class=class="str">"cmt">//--- Check if the terminal is connected to the trade server
   if(TerminalInfoInteger(TERMINAL_CONNECTED)==class="kw">false) class="kw">return;
class=class="str">"cmt">//--- Section A: Main loop of the FOR class="kw">operator for strategy A -----------
   for(class="type">int A=class="num">0; A<Strategy_A; A++)
     {
     class=class="str">"cmt">//--- A.class="num">1: Check whether the symbol is allowed to be traded
      if(IsTrade_A[A]==class="kw">false)
        class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration
     }
class=class="str">"cmt">//--- Section В: Main loop of the FOR class="kw">operator for strategy В -----------
   for(class="type">int B=class="num">0; B<Strategy_B; B++)
     {
     class=class="str">"cmt">//--- B.class="num">1: Check whether the symbol is allowed to be traded
      if(IsTrade_B[B]==class="kw">false)
        class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration
     }
}
class=class="str">"cmt">//--- Section N: Main loop of the FOR class="kw">operator for strategy N -----------
for(class="type">int N=class="num">0; N<Strategy_N; N++)
  {
   class=class="str">"cmt">//...
   class="type">bool IsInterrupt=class="kw">false;
   for(class="type">int i=class="num">0; i<Number; i++)
     {
      if(...) class=class="str">"cmt">// terminate all calculations
        {
         IsInterrupt=true;
         class="kw">break;
        }
     }
   if(IsInterrupt=true)
      class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration
   class=class="str">"cmt">//...
  }
class=class="str">"cmt">//--- A.class="num">3: Lower band of BB calculated based on High prices
 if(CopyBuffer(BB_handle_high_A[A],LOWER_BAND,BBShift_A[A],class="num">1,BB_lower_band_high)<=class="num">0)
    class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration
 ArraySetAsSeries(BB_lower_band_high,true);
class=class="str">"cmt">//--- A.class="num">7.1: Calculate the current Ask and Bid prices
SymbolInfo.Name(Symbol_A[A]);
SymbolInfo.RefreshRates();
class="type">class="kw">double Ask_price=SymbolInfo.Ask();
class="type">class="kw">double Bid_price=SymbolInfo.Bid();
if(PositionSelect(Symbol_A[A]))
   {
   class=class="str">"cmt">//--- A.class="num">7.2: Closing a BUY position
    if(PositionInfo.PositionType()==POSITION_TYPE_BUY)
     {
      if(Bid_price>=BB_lower_band_high[class="num">0] || DealNumber_A[A]==class="num">0)
        {
         if(!Trade_A.PositionClose(Symbol_A[A]))
           {
            Print("Failed to close the Buy ",Symbol_A[A]," position. Code=",Trade_A.ResultRetcode(),
                  " (",Trade_A.ResultRetcodeDescription(),")");
            class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration
           }
         else
           {

下穿下轨后的买入执行与日志

当 Ask 价格小于等于当前 K 线的布林带下轨(BB_lower_band_low[0])时,策略触发 A.9.1 的买入分支。这里先用 Trade_A.Buy(OrderLot, Symbol_A[A]) 尝试以计算手数在下标为 A 的交易品种上开多,返回布尔值决定后续流向。 若 Buy 返回 false,说明下单被拒绝或执行失败,代码会打印品种名、ResultRetcode 错误码及 ResultRetcodeDescription 文字说明,然后 continue 跳出本次 FOR 迭代,避免对同一品种重复尝试。外汇与贵金属杠杆高,实盘里这类拒单常源于保证金不足或报价过期,需结合终端‘专家’标签页核对。 若 Buy 返回 true,则打印成功日志并同样 continue 结束本轮循环。注意两路分支都靠 continue 终止当前迭代,意味着每个品种每根满足条件 K 线最多只下一单,不会在循环内连续加仓。 下面这段是原文里买卖平仓与开仓日志的拼接片段,可直接贴进 MT5 脚本看输出:

MQL5 / C++
Print("The Buy ",Symbol_A[A]," position closed successfully. Code=",Trade_A.ResultRetcode(),
             " (",Trade_A.ResultRetcodeDescription(),")");
class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration
}
}
}
class=class="str">"cmt">//...
}
class=class="str">"cmt">//--- A.class="num">9.1: for a Buy
if(Ask_price<=BB_lower_band_low[class="num">0])
  {
  class=class="str">"cmt">//...
  class=class="str">"cmt">//--- A.class="num">9.1.class="num">3: Execute a deal
  if(!Trade_A.Buy(OrderLot,Symbol_A[A]))
    {
     Print("The Buy ",Symbol_A[A]," has been unsuccessful. Code=",Trade_A.ResultRetcode(),
           " (",Trade_A.ResultRetcodeDescription(),")");
     class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration
    }
  else
    {
     Print("The Buy ",Symbol_A[A]," has been successful. Code=",Trade_A.ResultRetcode(),
           " (",Trade_A.ResultRetcodeDescription(),")");
     class="kw">continue; class=class="str">"cmt">// terminate the current FOR iteration
    }
  }

「分项跑完再叠总盘」

EA 成型以后,先把策略与品种拆开单独回测,再跑一遍全策略全品种的混合盘,两边对照才有意义。前提是你已经把输入参数调到各自的最优值,否则对比结论会失真。 策略测试器里按图 2 的设定走:单策略单品种各自出一份报告。策略 A 在 EURUSD 上的权益曲线见图表 3,换到 GBPUSD 见图表 4;策略 B 在 AUDUSD 见图表 5,在 EURJPY 见图表 6。 把四个组合并成「所有策略与交易品种」统一跑,得到图表 7 的总结果。外汇与贵金属杠杆高、滑点跳空频繁,回测盈利不代表实盘概率同构,MT5 里用真实点差重跑一遍才作数。

◍ 一点提醒

多币种多系统 EA 的结构并不复杂,把不同品种和策略塞进同一个 mq5 文件里,就能在单一账户里统算整体效益。实测中即便券商只允许挂一个 EA,这种聚合思路也跑得通,附带的 2multi_en.mq5 源码约 30 KB,直接拖进 MT5 就能编译研读。 外汇和贵金属杠杆高、滑点诡异,多系统同跑会放大连带回撤,参数没验之前别直接上真仓。 想改定时器节奏的,看 OnTimer 实际耗时:若平均超 1 秒,OnInit 里的 EventSetTimer(1) 就该调大,否则事件堆积可能拖慢整体。 剩下要做的只是打开策略测试器,挨个品种跑一遍,看哪些组合在样本内不互蚀。

MQL5 / C++
class="type">int OnInit()
{
class=class="str">"cmt">//--- 设置事件发生频率
EventSetTimer(class="num">1); class=class="str">"cmt">// class="num">1 秒
class=class="str">"cmt">// ...
class="kw">return(class="num">0);
}

常见问题

在EA初始化时读取账户杠杆与品种交易权限,过滤掉不可交易或点值异常的品种,再进入主循环,避免中途报错退出。
多是品种未加载或参数越界。创建句柄后立刻判空,失败则跳过该品种并记录日志,不要强行后续取数。
可以。小布能按品种聚合EA运行状态,把句柄失败和信号触发标出来,你只盯异常项就行。
单品种不满足条件用continue跳过去跑下一个;遇到账户断连等全局故障才用break退出整轮,别拿break躲单个品种错误。
方便回查某笔单是哪个品种、什么价位进的,分项跑完叠总盘时能对账,不至于混单。