创建多币种多系统 EA 交易

Maxim Khrolenko | 27 五月, 2014

简介

我相信,很多交易人都会交易不止一个交易品种并使用多种交易策略。依靠高效的资金管理,这种方法不仅能帮你提高潜在利润,还能最大程度降低大笔资金亏损风险。创建 EA 交易时,检查程序策略是否有效的第一步就是优化,以确定最佳输入参数。

确认参数值后,便从技术上做好了 EA 交易准备。但是还存在一个重要问题尚未解决。如果交易人将所有策略都放在一个 EA 中,那么其测试结果如何?在一些交易品种或策略上亏损可能会在某种程度上同时发生并造成可怕的整体亏损,甚至要求追加预付款有时也会让人措手不及。

本文引入了创建多币种多系统 EA 交易的概念,帮我们回答这个重要问题。

1. “EA 交易”的结构

一般来说,“EA 交易”的结构如下:

图1 多币种多系统“EA 交易”的结构

图1 多币种多系统“EA 交易”的结构

可以看到,程序是以一个 for 循环为基础。各项策略都安排在一个循环中,其中每个迭代都单独负责一个交易品种。你可以在循环中安排无限多项策略。重要的是,你的计算机要有足够的资源能够“处理”这样的程序。

请记住,在MetaTrader 5 中,每个交易品种可能只有一个仓位。该仓位代表之前完成的众多买入与卖出的总和 。因此,对一个交易品种的多策略测试结果与对同一交易品种的相同策略的分别测试结果的总和不同。

为了进一步了解“EA 交易”的结构,我们会采取 2 项策略,其中每项策略都交易两个交易品种:

策略 A:

策略 B:

为了独立于将在其中测试“EA 交易”或进行交易的交易品种的新的价格变动,建议用 OnTimer() 函数在多币种模式下进行交易。

为此,在初始化“EA 交易”时,我们会用 EventSetTimer() 函数指定调用程序计算事件的生成频率,而在去初始化时,我们用 EventKillTimer() 函数通知终端停止生成事件:

// Include standard libraries
// Create external parameters
// Create arrays, variables, indicator handles, etc.

//--- Initialization of the Expert Advisor
int OnInit()
  {
   //--- Set event generation frequency
   EventSetTimer(1); // 1 second
   // ...
   return(0);
  }
void OnTimer()
  {
   // ...
  }
//--- Deinitialization of the Expert Advisor
void OnDeinit(const int reason)
  {
   //--- Stop event generation
   EventKillTimer();
   // ...
  }

你还可以用其它函数取代 EventSetTimer(),比如可以用 EventSetMillisecondTimer(),在这个函数中,频率被精确到 毫秒,但不得通过过度频繁的程序计算调用来滥用函数。

如需访问账户、仓位和交易品种设置以及交易函数,我们会分别使用 CAccountInfoCPositionInfoCSymbolInfoCTrade 类。我们将它们加到“EA 交易”中:

//--- Include standard libraries
#include <Trade\AccountInfo.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\Trade.mqh>

“EA 交易”是以 for 循环为基础,因此我们需要为其外部参数创建数组。首先,我们来创建一个与每项策略的交易品种数量相等的常量:

//--- Number of traded symbols for each strategy
#define Strategy_A 2
#define Strategy_B 2

然后创建外部参数。我们用这些常量来确定数组的大小,这些常量将来会被复制到数组中。另外,我们还要创建指标句柄和其它全局变量。

下面以策略 A 的一个交易品种为例:

//------------------- External parameters of strategy A
input string          Data_for_Strategy_A="Strategy A -----------------------";
//--- Symbol 0
input string          Symbol_A0      = "EURUSD";   // Symbol
input bool            IsTrade_A0     = true;       // Permission for trading
//--- Bollinger Bands (BB) parameters
input ENUM_TIMEFRAMES Period_A0      = PERIOD_H1;  // ВВ period
input uint            BBPeriod_A0    = 20;         // Period for calculation of the moving average of BB
input int             BBShift_A0     = 0;          // Horizontal shift of ВВ
input double          BBDeviation_A0 = 2.0;        // Number of standard deviations of BB
//...
//--- General parameters of strategy A
input double          DealOfFreeMargin_A = 1.0;    // Percent of free margin for a deal
input uint            MagicNumber_A      = 555;    // Magic number
input uint            Slippage_A         = 100;    // Permissible slippage for a deal
//...
//------------- Set variables of strategy A -----
//--- Arrays for external parameters
string          Symbol_A[Strategy_A];
bool            IsTrade_A[Strategy_A];
ENUM_TIMEFRAMES Period_A[Strategy_A];
int             BBPeriod_A[Strategy_A];
int             BBShift_A[Strategy_A];
double          BBDeviation_A[Strategy_A];
//--- Arrays for global variables
double          MinLot_A[Strategy_A],MaxLot_A[Strategy_A];
double          Point_A[Strategy_A],ContractSize_A[Strategy_A];
uint            DealNumber_A[Strategy_A];
datetime        Locked_bar_time_A[Strategy_A],time_arr_A[];
//--- Indicator handles
int             BB_handle_high_A[Strategy_A];
int             BB_handle_low_A[Strategy_A];
//--- Arrays for indicator values
double          BB_upper_band_high[],BB_lower_band_high[];
double          BB_upper_band_low[],BB_lower_band_low[];
//--- Class
CTrade          Trade_A;
//...
//--- Set global variables for all strategies
long            Leverage;
//--- Classes
CAccountInfo    AccountInfo;
CPositionInfo   PositionInfo;
CSymbolInfo     SymbolInfo;

为了有可能禁用某个交易品种的交易,我们创建了一个布尔变量 IsTrade_A0,这个变量将被放在 for 循环的开头。


2. 初始化“EA 交易”

首先,我们要获取所有策略所需的数值,比如杠杆率。由于杠杆率应用于交易账户,与策略或交易品种无关,因此无需将其数值复制到数组中:

//--- Get the leverage for the account
   Leverage=AccountInfo.Leverage();

然后我们要将外部变量复制到数组。

//--- Copy external variables to arrays
   Symbol_A[0]     =Symbol_A0;
   IsTrade_A[0]    =IsTrade_A0;
   Period_A[0]     =Period_A0;
   BBPeriod_A[0]   =(int)BBPeriod_A0;
   BBShift_A[0]    =BBShift_A0;
   BBDeviation_A[0]=BBDeviation_A0;

如果某个外部参数的类型要求将其 转换 到另一类型,可将其复制到数组更加方便完成转换。

在这里,我们看到已创建 BBPeriod_A0 作为 uint 以防止用户设置负值。在此我们将它转化为 int 并将其复制到同样作为 int创建的数组。否则,当你试图在指标句柄中插入 uint 型参数时,编译器会发出警告。

我们来进一步看看“市场报价”中是否提供了已交易的交易品种,且在一项策略中是否多次使用了这一交易品种:

//--- Check for the symbol in the Market Watch
   for(int i=0; i<Strategy_A; i++)
     {
      if(IsTrade_A[i]==false) continue;
      if(IsSymbolInMarketWatch(Symbol_A[i])==false)
        {
         Print(Symbol_A[i]," could not be found on the server!");
         ExpertRemove();
        }
     }

//--- Check whether the symbol is used more than once
   if(Strategy_A>1)
     {
      for(int i=0; i<Strategy_A-1; i++)
        {
         if(IsTrade_A[i]==false) continue;
         for(int j=i+1; j<Strategy_A; j++)
           {
            if(IsTrade_A[j]==false) continue;
            if(Symbol_A[i]==Symbol_A[j])
              {
               Print(Symbol_A[i]," is used more than once!");
               ExpertRemove();
              }
           }
        }
     }
//--- The IsSymbolInMarketWatch() function
bool IsSymbolInMarketWatch(string f_Symbol)
  {
   for(int s=0; s<SymbolsTotal(false); s++)
     {
      if(f_Symbol==SymbolName(s,false))
         return(true);
     }
   return(false);
  }

如果选择的交易品种无误,检查每个交易品种的输入参数是否有误,创建指标句柄,获取批量计算的所需数据,而且如有必要,完成既定策略所规定的其它事项。

我们会在 for 循环中实现上述动作。

//--- General actions
   for(int i=0; i<Strategy_A; i++)
     {
      if(IsTrade_A[i]==false) continue;
      //--- Check for errors in input parameters
      //...
      //--- 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]<0)
        {
         Print("Failed to create a handle for Bollinger Bands based on High prices for ",Symbol_A[i]," . Handle=",INVALID_HANDLE,
               "\n Error=",GetLastError());
         ExpertRemove();
        }
      //...
      //--- Calculate data for the Lot
      //--- set the name of the symbol for which the information will be obtained
      SymbolInfo.Name(Symbol_A[i]);
      //--- minimum and maximum volume size in trading operations
      MinLot_A[i]=SymbolInfo.LotsMin();
      MaxLot_A[i]=SymbolInfo.LotsMax();
      //--- point value
      Point_A[i]=SymbolInfo.Point();
      //--- contract size
      ContractSize_A[i]=SymbolInfo.ContractSize();

      //--- Set some additional parameters
     }

然后,我们会用 CTrade 类的 Trade_A 对象设置策略 A 的交易运算参数。

//--- Set parameters for trading operations
//--- set the magic number
   Trade_A.SetExpertMagicNumber(MagicNumber_A);
//--- set the permissible slippage in points upon deal execution
   Trade_A.SetDeviationInPoints(Slippage_A);
//--- order filling mode, use the mode that is allowed by the server
   Trade_A.SetTypeFilling(ORDER_FILLING_RETURN);
//--- logging mode, it is advisable not to call this method as the class will set the optimal mode by itself
   Trade_A.LogLevel(1);
//--- the function to be used for trading: true - OrderSendAsync(), false - OrderSend().
   Trade_A.SetAsyncMode(true);

对各项策略重复同一流程,即

  1. 将外部变量复制到数组;
  2. 检查交易品种选择是否正确;
  3. 检查错误,设置指标句柄,计算批量数据,计算既定策略所需的所有事项;
  4. 设置交易运算参数。

最后,最好检查一下在多个策略中是否使用了某种相同的交易品种(下面以两项策略为例):

//--- Check whether one and the same symbol is used in several strategies
   for(int i=0; i<Strategy_A; i++)
     {
      if(IsTrade_A[i]==false) continue;
      for(int j=0; j<Strategy_B; j++)
        {
         if(IsTrade_B[j]==false) continue;
         if(Symbol_A[i]==Symbol_B[j])
           {
            Print(Symbol_A[i]," is used in several strategies!");
            ExpertRemove();
           }
        }
     }

3. 交易“For”循环

OnTimer() 函数内的 for 循环框架如下所示:

void OnTimer()
  {
//--- Check if the terminal is connected to the trade server
   if(TerminalInfoInteger(TERMINAL_CONNECTED)==false) return;

//--- Section A: Main loop of the FOR operator for strategy A -----------
   for(int A=0; A<Strategy_A; A++)
     {
      //--- A.1: Check whether the symbol is allowed to be traded
      if(IsTrade_A[A]==false)
         continue; // terminate the current FOR iteration

     }

//--- Section В: Main loop of the FOR operator for strategy В -----------
   for(int B=0; B<Strategy_B; B++)
     {
      //--- B.1: Check whether the symbol is allowed to be traded
      if(IsTrade_B[B]==false)
         continue; // terminate the current FOR iteration

     }
  }

如果基于单一策略的一个单一交易品种“EA 交易”要求停止所有后续计算,我们会使用 return 运算符。在我们的例子中,我们只需终止当前迭代并继续下一交易品种迭代。为此,最好使用 continue 运算符。

如果想要通过添加策略来加强多策略“EA 交易”,且该策略具有以终止所有后续计算为条件的 for 循环,可以采用以下模式:

//--- Section N: Main loop of the FOR operator for strategy N -----------
for(int N=0; N<Strategy_N; N++)
  {

   //...
   bool IsInterrupt=false;
   for(int i=0; i<Number; i++)
     {
      if(...) // terminate all calculations
        {
         IsInterrupt=true;
         break;
        }
     }
   if(IsInterrupt=true)
      continue; // terminate the current FOR iteration
   //...

  }

创建 for 循环框架后,我们只需将来自其它 EA 的代码插入其中,然后用数组元素替换一些变量。

比如,将预先定义的变量 _Symbol 改为 Symbol_A[i] 或将 _Point 改为 Point_A[i]。这些变量的数值是既定交易品种所特有的数值,因此可在初始化时将其复制到数组。

例如,我们来看这个指标值:

 //--- A.3: Lower band of BB calculated based on High prices
 if(CopyBuffer(BB_handle_high_A[A],LOWER_BAND,BBShift_A[A],1,BB_lower_band_high)<=0)
    continue; // terminate the current FOR iteration
 ArraySetAsSeries(BB_lower_band_high,true);

为实现买入仓位平仓,我们会编写以下代码:

 //--- A.7.1: Calculate the current Ask and Bid prices
 SymbolInfo.Name(Symbol_A[A]);
 SymbolInfo.RefreshRates();
 double Ask_price=SymbolInfo.Ask();
 double Bid_price=SymbolInfo.Bid();

 if(PositionSelect(Symbol_A[A]))
   {
    //--- A.7.2: Closing a BUY position
    if(PositionInfo.PositionType()==POSITION_TYPE_BUY)
      {
       if(Bid_price>=BB_lower_band_high[0] || DealNumber_A[A]==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(),")");
             continue; // terminate the current FOR iteration
            }
          else
            {
             Print("The Buy ",Symbol_A[A]," position closed successfully. Code=",Trade_A.ResultRetcode(),
                   " (",Trade_A.ResultRetcodeDescription(),")");
             continue; // terminate the current FOR iteration
            }
         }
      }

    //...
   }

建一个买入仓位:

 //--- A.9.1: for a Buy
 if(Ask_price<=BB_lower_band_low[0])
   {
    //...

    //--- A.9.1.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(),")");
       continue; // terminate the current FOR iteration
      }
    else
      {
       Print("The Buy ",Symbol_A[A]," has been successful. Code=",Trade_A.ResultRetcode(),
             " (",Trade_A.ResultRetcodeDescription(),")");
       continue; // terminate the current FOR iteration
      }
   }

去初始化时请务必终止生成计时器事件并删除指标句柄。

4. 测试结果

准备好 EA 后,我们会分别测试每项策略和每个交易品种,并将测试结果与同时以所有策略与交易品种进行交易的测试结果进行比较。

假设用户已经确定了输入参数的最佳值。


下面是策略测试程序的设置:

图 2. 策略测试程序的设置

图 2. 策略测试程序的设置

策略 A 的测试结果,EURUSD:

图3 策略 A 的测试结果,EURUSD

图3 策略 A 的测试结果,EURUSD

策略 A 的测试结果,GBPUSD:

图4 策略 A 的测试结果,GBPUSD

图4 策略 A 的测试结果,GBPUSD

策略 B 的测试结果,AUDUSD:

图5 策略 B 的测试结果,AUDUSD

图5 策略 B 的测试结果,AUDUSD

策略 B 的测试结果,EURJPY:

图6 策略 B 的测试结果,EURJPY

图6 策略 B 的测试结果,EURJPY

所有策略与交易品种的测试结果:

图7 所有策略与交易品种的测试结果

图7 所有策略与交易品种的测试结果


总结

因此,多币种多系统 EA 的结构简单方便,几乎可在其中放置你的所有策略。

EA 能让你更好地评估用所有策略进行交易的效益。实践证明,如果在既定账户中只允许使用一个 EA 时,该 EA 同样有效。本文附上 EA 的源代码助您学习以上信息。