English Русский Español Deutsch 日本語 Português
preview
开发多币种 EA 交易(第 1 部分):多种交易策略的协作

开发多币种 EA 交易(第 1 部分):多种交易策略的协作

MetaTrader 5交易 | 28 八月 2024, 10:28
98 0
Yuriy Bykov
Yuriy Bykov

在工作期间,我不得不面对各种交易策略。通常情况下,EA 只执行一种交易思路。由于难以确保一个终端上的多个 EA 稳定协作,我们通常只能选择少数最优秀的 EA。但如果因此而放弃完全可行的策略,那还是很可惜的。如何才能让它们协同工作?


确定问题

我们需要决定我们想要什么,以及我们拥有什么。

我们有(或几乎有):

  • 以现成 EA 代码的形式,或仅仅是一套用于执行交易操作的规则,在不同交易品种和时间框架上运行的一些不同的交易策略
  • 起始存款
  • 最大允许回撤

我们想要:

  • 在多个交易品种和时间框架上,在一个账户中协作使用所有选定的策略
  • 在每个策略之间平均分配或按照规定的比例分配起始存款
  • 自动计算已开立头寸的数量,以符合最大允许回撤量的要求
  • 正确处理终端重启
  • 能够在 MetaTrader 5 和 4 中运行

我们将使用面向对象的方法、MQL5 和 MetaTrader 5 中的标准测试器。

手头的任务相当繁重,因此我们将逐步解决。

在第一阶段,让我们以一个简单的交易思路为例。让我们用它制作一个简单的 EA,对其进行优化,选择两组最佳参数。再创建一个包含两个原始简单 EA 副本的 EA,并查看其结果。


从交易思路到交易策略

让我们把下面的思路作为一个实验。 

假设某个交易品种开始密集交易时,单位时间内的价格变化可能比交易低迷时更大。然后,如果我们看到交易已经加剧,价格已经朝着某个方向变化,那么也许在不久的将来,它也会朝着同样的方向变化。让我们试着从中获利吧。

交易策略 是一套基于交易思路的开仓和平仓规则。它不包含任何未知参数。通过这套规则,我们可以确定在策略运行的任何时刻是否应该开仓,如果应该开仓,应该开哪些仓。

让我们试着把想法变成策略。首先,我们需要以某种方式检测交易强度的增加。没有这一点,我们就无法确定何时开仓。为此,我们将使用交易量,即在当前烛形中终端接收到的新价格的数量。交易量越大,表明交易越活跃。但对于不同的交易品种,其强度会有很大差异。因此,我们不能为分时成交量设定一个单一水平,超过分时成交量我们就认为开始了密集交易。那么,要确定这一水平,我们可以从几根烛形的平均交易量入手。经过一番思考,我们可以做出如下描述:

当烛形的分时成交量超过当前烛形方向的平均成交量时下达挂单。每个订单都有有效期,过期后将被删除。如果挂单已变成头寸,则只有在达到指定的止损和止盈水平后才会平仓。如果交易量比平均值超出更多,那么除了已经打开的挂单外,还可能会发出额外的订单。

这是一个更详细的描述,但并不完整。因此,我们要再读一遍,把所有不清楚的地方都标出来,那里需要更详细的解释。 

下面是提出的问题:

  • "下挂单......" - 我们应该下什么挂单?
  • "平均成交量......" - 如何计算烛形中的平均成交量?
  • "......超过平均...... " - 如何确定超过平均量?
  • "...如果分时交易量超出平均值更多......" - 如何确定更多的超出量?
  • "......可以下额外订单 " - 总共可以下多少订单?

我们将下哪些挂单?基于这一想法,我们希望价格继续沿着烛形开始时的方向移动。例如,如果当前价格高于烛形期间开始时的价格,那么我们就应该开挂单买入。如果我们开启 BUY_LIMIT,那么要让它起作用,价格应该先返回(下跌)一点,然后要让开仓头寸获利,价格应该再次上涨。如果我们开启的是 BUY_STOP,那么要建立头寸,价格就应该继续再移动(上涨)一些,然后再涨得更高,这样才能获利。

目前还不清楚哪种方案更好 。因此,为了简单起见,让我们始终打开止损单(BUY_STOP 和 SELL_STOP)。今后,可以将其作为一个策略参数,由其值决定哪些订单将被开启。

如何计算烛形的平均交易量?要计算平均交易量,我们需要选择交易量将纳入平均计算的烛形。我们来选取一定数量的最近关闭的连续烛形。然后,如果我们设置了烛形数量,就可以计算出平均交易量。

如何确定平均交易量的超出部分?如果我们选择条件

V > V_avr ,

其中
V 是当前烛形的交易量,
V_avr 是平均交易量,
那么大约一半的烛形都能满足这一条件。根据交易思路,我们只有在成交量明显超过平均值时才下单。否则,与之前的烛形不同,这根烛形还不能被视为交易更加活跃的迹象。例如,我们可以使用下面的公式:

V > V_avr + D * V_avr,

其中 D 是一个数值比率。如果 D = 1,则在当前成交量超过平均值 2 倍时开启,如果 D = 2,则在当前成交量超过平均值3 倍时开启

不过,该条件只能用于开启一个订单,因为如果用于开启第二个和后续订单,那么它们将紧接着第一个订单开启。只需打开一单更大交易量的订单,就可以替代这部分订单。

如何确定更大的超出量? 为此,让我们在条件等式中再添加一个参数 - 未结订单数 N:

V > V_avr + D * V_avr + N * D * V_avr。

那么,为了让第二个订单在第一个订单之后开启(即 N = 1),必须满足以下条件:

V > V_avr + 2 * D * V_avr。

要开启第一个订单(N = 0),方程的形式已经为我们所知:

V > V_avr + D * V_avr。

最后,对开头的方程进行最后一次修正。让我们用两个独立的参数 D 和 D_add,来代替第一个订单和后续订单中的同一个 D:

V > V_avr + D * V_avr + N * D_add * V_avr、

V > V_avr * (1 + D + N * D_add)

这样看来,我们在选择策略的最佳参数时就有了更大的自由度。

如果我们的条件使用 N 值作为订单和头寸的总数,那么我们的意思是每个挂单都会变成一个单独的头寸,而不会增加已开仓头寸的交易量。因此,目前我们只能将这种策略的适用范围限制在对头寸进行独立核算("对冲")的账户上。  

一切都清楚后,让我们列出可以取不同值的变量,而不是只有一个值的变量。这些将是我们策略的输入参数。让我们考虑一下,要打开订单,我们还需要知道成交量、与当前价格的距离、到期时间以及止损和止盈水平。然后我们会得到如下描述:

EA 在对冲账户的特定交易品种和周期(时间框架)上运行

设置输入参数:

  • 用于计算平均交易量的烛形数量 (K)
  • 打开第一个订单时与平均值的相对偏差 (D)
  • 第二个订单和后续订单开仓时与平均值的相对偏差 (D_add)
  • 从价格到挂单的距离
  • 止损(点数)
  • 止盈(点数)
  • 挂单到期时间(分钟)
  • 同时打开的最大订单数 (N_max)
  • 单个订单交易量

找出未结订单和头寸的数量 (N)。
如果小于 N_max,则:
        计算最近 K 个关闭烛形的平均交易量,得到 V_avr 值。
        如果满足 V > V_avr * (1 + D + N * D_add) 条件,则:
                确定当前烛形的价格变化方向:如果价格上涨,则下达 BUY_STOP 挂单,否则下达 SELL_STOP 挂单
                在参数中指定的距离、到期时间、止损和止盈水平下挂单。


实现交易策略

让我们开始编写代码吧。首先,让我们列出所有参数,将它们分成几组,使其更加清晰,并为每个参数提供注释。这些注释(如果有)将在启动 EA 时显示在参数对话框中,并显示在策略测试器的参数选项卡中,而不是我们为其选择的变量名称。

现在,我们只需设置一些默认值。我们将在优化过程中寻找最佳设置。

input group "===  Opening signal parameters"
input int         signalPeriod_        = 48;    // Number of candles for volume averaging 
input double      signalDeviation_     = 1.0;   // Relative deviation from the average to open the first order 
input double      signaAddlDeviation_  = 1.0;   // Relative deviation from the average for opening the second and subsequent orders

input group "===  Pending order parameters"
input int         openDistance_        = 200;   // Distance from price to pending order
input double      stopLevel_           = 2000;  // Stop Loss (in points)
input double      takeLevel_           = 75;    // Take Profit (in points)
input int         ordersExpiration_    = 6000;  // Pending order expiration time (in minutes)

input group "===  Money management parameters"
input int         maxCountOfOrders_    = 3;     // Maximum number of simultaneously open orders
input double      fixedLot_            = 0.01;  // Single order volume

input group "===  EA parameters"
input ulong       magicN_              = 27181; // Magic

由于 EA 将执行交易操作,我们将创建一个 CTrade 类的全局对象。我们将通过调用该对象的方法来下挂单。

CTrade            trade;            // Object for performing trading operations 

请记住,全局变量(或对象),是在 EA 代码中函数的 之外声明的变量(或对象)。因此,我们的所有 EA 函数中都可以使用它们。它们不应与全局终端变量混淆。

为了计算开仓订单的参数,我们需要获取当前价格和 EA 将在其上启动的其他交易品种属性。为此,要创建一个 CSymbolInfo 类的全局对象。

CSymbolInfo       symbolInfo;       // Object for obtaining data on the symbol properties

此外,我们还需要计算未结订单和头寸的数量。为此,让我们创建 COrderInfo 和 CPositionInfo 类的全局对象,用于获取未结订单和仓位的数据。我们将在两个全局变量 - countOrders 和 countPositions 中存储数量本身。

COrderInfo        orderInfo;        // Object for receiving information about placed orders
CPositionInfo     positionInfo;     // Object for receiving information about open positions

int               countOrders;      // Number of placed pending orders
int               countPositions;   // Number of open positions

例如,我们可以使用 iVolumes 技术指标来计算多个烛形的平均交易量。要获取其值,我们需要一个变量来存储该指标的句柄(一个整数,用于存储 EA 中使用的所有其他指标中该指标的序列号)。要找出平均交易量,我们首先要将指标缓冲区中的数值复制到一个预先准备好的数组中。我们还将这个数组设为全局可用。

int               iVolumesHandle;   // Tick volume indicator handle
double            volumes[];        // Receiver array of indicator values (volumes themselves) 

现在,我们可以继续使用 OnInit() EA 初始化函数和 OnTick() 分时处理函数。

在初始化过程中,我们可以执行以下操作:

  • 加载指标以获取分时交易量并记住其句柄
  • 根据烛形的数量设置接收数组的大小,以计算平均量,并设置其寻址方式为时间序列 
  • 设置通过交易对象下单的幻数

这就是我们的初始化函数:

int OnInit() {
   // Load the indicator to get tick volumes
   iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK);
   
   // Set the size of the tick volume receiving array and the required addressing
   ArrayResize(volumes, signalPeriod_);
   ArraySetAsSeries(volumes, true);

   // Set Magic Number for placing orders via 'trade'
   trade.SetExpertMagicNumber(magicN_);
   
   return(INIT_SUCCEEDED);
}

根据策略说明,我们应该首先在分时处理函数中找到未结订单和仓位的数量。让我们用一个单独的 UpdateCounts() 函数来实现这个功能。在这个函数中,我们将查看所有未结头寸和订单,只计算那些幻数与我们的 EA 幻数相匹配的头寸和订单。

void UpdateCounts() {
// Reset position and order counters
   countPositions = 0;
   countOrders = 0;

// Loop through all positions
   for(int i = 0; i < PositionsTotal(); i++) {
      // If the position with index i is selected successfully and its Magic is ours, then we count it 
      if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) {
         countPositions++;
      }
   }

// Loop through all orders
   for(int i = 0; i < OrdersTotal(); i++) {
      // If the order with index i is selected successfully and its Magic is the one we need, then we consider it 
      if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) {
         countOrders++;
      }
   }
}

然后,确保未结头寸和订单的数量不超过设置中指定的数量。在这种情况下,我们需要检查是否满足开立新订单的条件。让我们用一个单独的 SignalForOpen() 函数来实现这种检查。它将返回三个可能值中的一个:

  • +1 - 打开 BUY_STOP 订单的信号
  •  0 - 无信号
  • -1 - 打开 SELL_STOP 订单的信号

为了下挂单,我们还将编写两个独立的函数: OpenBuyOrder() 和 OpenSellOrder()。 

现在,我们可以编写 OnTick() 函数的完整实现了。

void OnTick() {
// Count open positions and orders
   UpdateCounts();

// If their number is less than allowed
   if(countOrders + countPositions < maxCountOfOrders_) {
      // Get an open signal
      int signal = SignalForOpen();

      if(signal == 1) {          // If there is a buy signal, then 
         OpenBuyOrder();         // open the BUY_STOP order
      } else if(signal == -1) {  // If there is a sell signal, then
         OpenSellOrder();        // open the SELL_STOP order
      }
   }
}

之后,我们再添加其余函数的实现,这样 EA 代码就完成了。让我们将其保存到当前文件夹中的 SimpleVolumes.mq5 文件中。

#include <Trade\OrderInfo.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\Trade.mqh>

input group "===  Opening signal parameters"
input int         signalPeriod_        = 48;    // Number of candles for volume averaging 
input double      signalDeviation_     = 1.0;   // Relative deviation from the average to open the first order 
input double      signaAddlDeviation_  = 1.0;   // Relative deviation from the average for opening the second and subsequent orders

input group "===  Pending order parameters"
input int         openDistance_        = 200;   // Distance from price to pending order
input double      stopLevel_           = 2000;  // Stop Loss (in points)
input double      takeLevel_           = 75;    // Take Profit (in points)
input int         ordersExpiration_    = 6000;  // Pending order expiration time (in minutes)

input group "===  Money management parameters"
input int         maxCountOfOrders_    = 3;     // Maximum number of simultaneously open orders
input double      fixedLot_            = 0.01;  // Single order volume

input group "===  EA parameters"
input ulong       magicN_              = 27181; // Magic


CTrade            trade;            // Object for performing trading operations 

COrderInfo        orderInfo;        // Object for receiving information about placed orders
CPositionInfo     positionInfo;     // Object for receiving information about open positions

int               countOrders;      // Number of placed pending orders
int               countPositions;   // Number of open positions

CSymbolInfo       symbolInfo;       // Object for obtaining data on the symbol properties

int               iVolumesHandle;   // Tick volume indicator handle
double            volumes[];        // Receiver array of indicator values (volumes themselves) 

//+------------------------------------------------------------------+
//| Initialization function of the expert                            |
//+------------------------------------------------------------------+
int OnInit() {
// Load the indicator to get tick volumes
   iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK);

// Set the size of the tick volume receiving array and the required addressing
   ArrayResize(volumes, signalPeriod_);
   ArraySetAsSeries(volumes, true);

// Set Magic Number for placing orders via 'trade'
   trade.SetExpertMagicNumber(magicN_);

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| "Tick" event handler function                                    |
//+------------------------------------------------------------------+
void OnTick() {
// Count open positions and orders
   UpdateCounts();

// If their number is less than allowed
   if(countOrders + countPositions < maxCountOfOrders_) {
      // Get an open signal
      int signal = SignalForOpen();

      if(signal == 1) {          // If there is a buy signal, then 
         OpenBuyOrder();         // open the BUY_STOP order
      } else if(signal == -1) {  // If there is a sell signal, then
         OpenSellOrder();        // open the SELL_STOP order
      }
   }
}

//+------------------------------------------------------------------+
//| Calculate the number of open orders and positions                |
//+------------------------------------------------------------------+
void UpdateCounts() {
// Reset position and order counters
   countPositions = 0;
   countOrders = 0;

// Loop through all positions
   for(int i = 0; i < PositionsTotal(); i++) {
      // If the position with index i is selected successfully and its Magic is ours, then we count it 
      if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) {
         countPositions++;
      }
   }

// Loop through all orders
   for(int i = 0; i < OrdersTotal(); i++) {
      // If the order with index i is selected successfully and its Magic is the one we need, then we consider it 
      if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) {
         countOrders++;
      }
   }
}

//+------------------------------------------------------------------+
//| Open the BUY_STOP order                                          |
//+------------------------------------------------------------------+
void OpenBuyOrder() {
// Update symbol current price data
   symbolInfo.Name(Symbol());
   symbolInfo.RefreshRates();

// Retrieve the necessary symbol and price data
   double point = symbolInfo.Point();
   int digits = symbolInfo.Digits();
   double bid = symbolInfo.Bid();
   double ask = symbolInfo.Ask();
   int spread = symbolInfo.Spread();

// Let's make sure that the opening distance is not less than the spread
   int distance = MathMax(openDistance_, spread);

// Opening price
   double price = ask + distance * point; 
   
// StopLoss and TakeProfit levels
   double sl = NormalizeDouble(price - stopLevel_ * point, digits);
   double tp = NormalizeDouble(price + (takeLevel_ + spread) * point, digits);
   
// Expiration time
   datetime expiration = TimeCurrent() + ordersExpiration_ * 60; 
   
// Order volume
   double lot = fixedLot_; 
   
// Set a pending order
   bool res = trade.BuyStop(lot,
                            NormalizeDouble(price, digits),
                            Symbol(),
                            NormalizeDouble(sl, digits),
                            NormalizeDouble(tp, digits),
                            ORDER_TIME_SPECIFIED,
                            expiration);

   if(!res) {
      Print("Error opening order");
   }
}

//+------------------------------------------------------------------+
//| Open the SELL_STOP order                                         |
//+------------------------------------------------------------------+
void OpenSellOrder() {
// Update symbol current price data
   symbolInfo.Name(Symbol());
   symbolInfo.RefreshRates();

// Retrieve the necessary symbol and price data
   double point = symbolInfo.Point();
   int digits = symbolInfo.Digits();
   double bid = symbolInfo.Bid();
   double ask = symbolInfo.Ask();
   int spread = symbolInfo.Spread();

// Let's make sure that the opening distance is not less than the spread
   int distance = MathMax(openDistance_, spread);

// Opening price
   double price = bid - distance * point;
   
// StopLoss and TakeProfit levels
   double sl = NormalizeDouble(price + stopLevel_ * point, digits);
   double tp = NormalizeDouble(price - (takeLevel_ + spread) * point, digits);

// Expiration time
   datetime expiration = TimeCurrent() + ordersExpiration_ * 60;

// Order volume
   double lot = fixedLot_;

// Set a pending order
   bool res = trade.SellStop(lot,
                             NormalizeDouble(price, digits),
                             Symbol(),
                             NormalizeDouble(sl, digits),
                             NormalizeDouble(tp, digits),
                             ORDER_TIME_SPECIFIED,
                             expiration);

   if(!res) {
      Print("Error opening order");
   }
}

//+------------------------------------------------------------------+
//| Signal for opening pending orders                                |
//+------------------------------------------------------------------+
int SignalForOpen() {
// By default, there is no signal
   int signal = 0;

// Copy volume values from the indicator buffer to the receiving array
   int res = CopyBuffer(iVolumesHandle, 0, 0, signalPeriod_, volumes);

// If the required amount of numbers have been copied
   if(res == signalPeriod_) {
      // Calculate their average value
      double avrVolume = ArrayAverage(volumes);

      // If the current volume exceeds the specified level, then
      if(volumes[0] > avrVolume * (1 + signalDeviation_ + (countOrders + countPositions) * signaAddlDeviation_)) {
         // if the opening price of the candle is less than the current (closing) price, then 
         if(iOpen(Symbol(), PERIOD_CURRENT, 0) < iClose(Symbol(), PERIOD_CURRENT, 0)) {
            signal = 1; // buy signal
         } else {
            signal = -1; // otherwise, sell signal
         }
      }
   }

   return signal;
}

//+------------------------------------------------------------------+
//| Number array average value                                       |
//+------------------------------------------------------------------+
double ArrayAverage(const double &array[]) {
   double s = 0;
   int total = ArraySize(array);
   for(int i = 0; i < total; i++) {
      s += array[i];
   }

   return s / MathMax(1, total);
}
//+------------------------------------------------------------------+

让我们开始优化 EURGBP H1 的 EA 参数,以 MetaQuotes 报价为基础,从 2018-01-01 至 2023-01-01 期间,起始存款为 100 000 美元,最小手数为 0.01 手。请注意,在对不同经纪商的报价进行测试时,同一 EA 显示的结果可能略有不同。有时,这些结果可能会大相径庭。

让我们选择两组不错的参数,结果如下:


图 1.[130,0.9,1.4,231,3750,50,600,3,0.01] 的测试结果 



图 2. 159,1.7,0.8,248,3600,495,39000,3,0.01] 的测试结果 

在一大笔起始存款上进行测试不是偶然的,原因是,如果 EA 开立的仓位有固定的交易量,那么如果回撤大于可用资金,运行可能会提前结束。在这种情况下,我们将无法知道,在使用相同参数的情况下,是否有可能合理地减少未结头寸的数量(或等同于增加起始存款)以避免损失。

让我们回顾一个例子。假设我们的起始存款为 1,000 美元。在测试器中运行时,我们得到了以下结果:

  • 最终存款为 11,000 美元(利润 1,000%,EA 赚取 + 10,000 美元,再加上初始的 1,000 美元)
  • 最高绝对回撤额为 2,000 美元

显然,我们只是幸运,在 EA 将存款增加到 2,000 多美元之后,才出现了这样的回撤。因此,测试器运行完成后,我们就能看到这些结果。如果这种回撤发生得更早(例如,我们选择了不同的测试期开始时间),那么我们就会损失全部存款。

如果我们手动运行,那么我们可以更改参数中的交易量或增加起始存款,然后重新开始运行。但如果在优化过程中进行运行,则无法做到这一点。在这种情况下,由于资金管理设置选择错误,一组可能很好的参数就可能会被拒绝。为了降低出现这种结果的可能性,我们可以在运行优化时,初始存款额都非常大,而交易量则设为最低。

回到例子中,如果起始存款是 100,000 美元,那么在重复回撤 2,000 美元的情况下,不会出现损失全部存款的情况,测试者将得到这些结果。我们可以计算出,如果我们的最大允许回撤额为 10%,那么初始存款至少应为 20,000 美元。在这种情况下,利润率将只有 50%(EA 赚取的 + 10,000 美元与最初的 20,000 美元之比)。

让我们对所选的两个参数组合进行类似计算,起始存款为 10,000 美元,允许回撤为起始存款的 10%。

参数 手数  回撤 利润  可以接受的
回撤
可以接受的
手数
可以接受的
收益
   L  D  P  Da La = L * (Da / D) Pa = P * (Da / D)
[130, 0.9, 1.4, 231,
3750, 50, 600, 3, 0.01]

0.01 28.70 (0.04%)  260.41 1000 (10%) 0.34 9073 (91%)
[159, 1.7, 0.8, 248、
3600,495,39000,3,0.01
]
0.01 92.72 (0.09%)  666.23 1000 (10%)
0.10 7185 (72%)

我们可以看到,两种输入参数选项都能产生大致相同的收益(约 80%)。第一种选项的绝对收益较低,但回撤幅度较小。因此,在这种情况下,我们可以增加开仓量,而不是第二种选项,因为第二种选项虽然赚得更多,但会有更大的回撤。

因此,我们找到了几种有前景的输入参数组合。让我们开始将它们合并为一个 EA。


基本策略类

让我们创建 CStrategy 类,在其中收集所有策略固有的属性和方法。例如,任何策略都会有某个交易品种和时间框架,无论其与指标的关系如何。我们还将为每种策略分配自己的开仓幻数和单个仓位大小。为简单起见,我们暂不考虑仓位大小可变的策略操作。我们以后一定会这样做的。

在必要的方法中,只有初始化策略参数的构造函数、初始化方法和 OnTick 事件处理程序可以确定。我们得到以下代码:

class CStrategy : public CObject {
protected:
   ulong             m_magic;          // Magic
   string            m_symbol;         // Symbol (trading instrument)
   ENUM_TIMEFRAMES   m_timeframe;      // Chart period (timeframe)
   double            m_fixedLot;       // Size of opened positions (fixed)

public:
   // Constructor
   CStrategy(ulong p_magic,
             string p_symbol,
             ENUM_TIMEFRAMES p_timeframe,
             double p_fixedLot);

   virtual int       Init() = 0; // Strategy initialization - handling OnInit events
   virtual void      Tick() = 0; // Main method - handling OnTick events
};

Init() 和 Tick() 方法被声明为纯虚拟方法(方法头后面的 = 0)。这意味着我们不会在 CStrategy 类中编写这些方法的实现。在该类的基础上,我们将创建子类,其中必须包含 Init() 和 Tick() 方法,并包含具体交易规则的实现。

类的说明已准备就绪。之后,我们将添加必要的构造函数实现。由于这是一个在创建策略对象时自动调用的方法函数,因此我们需要在此方法中确保策略参数已初始化。构造函数将接收四个参数,并通过初始化列表将其赋值给相应的类成员变量。

CStrategy::CStrategy(
   ulong p_magic,
   string p_symbol,
   ENUM_TIMEFRAMES p_timeframe,
   double p_fixedLot) :
// Initialization list
   m_magic(p_magic),
   m_symbol(p_symbol),
   m_timeframe(p_timeframe),
   m_fixedLot(p_fixedLot)
{}

将代码保存到当前文件夹的 Strategy.mqh 文件中。


交易策略类

让我们把原始简单 EA 的逻辑移植到一个新的子类 CSimpleVolumesStrategy 中。为此,应将所有输入变量和全局变量设为类的成员。我们将用从 CStrategy 基类继承的 m_fixedLot 和 m_magic 基类成员替换 fixedLot_ 和 magicN_ 变量。

#include "Strategy.mqh"

class CSimpleVolumeStrategy : public CStrategy {
   //---  Open signal parameters
   int               signalPeriod_;       // Number of candles for volume averaging
   double            signalDeviation_;    // Relative deviation from the average to open the first order
   double            signaAddlDeviation_; // Relative deviation from the average for opening the second and subsequent orders

   //---  Pending order parameters
   int               openDistance_;       // Distance from price to pending order
   double            stopLevel_;          // Stop Loss (in points)
   double            takeLevel_;          // Take Profit (in points)
   int               ordersExpiration_;   // Pending order expiration time (in minutes)

   //---  Money management parameters
   int               maxCountOfOrders_;   // Maximum number of simultaneously open orders

   CTrade            trade;               // Object for performing trading operations

   COrderInfo        orderInfo;           // Object for receiving information about placed orders
   CPositionInfo     positionInfo;        // Object for receiving information about open positions

   int               countOrders;         // Number of placed pending orders
   int               countPositions;      // Number of open positions

   CSymbolInfo       symbolInfo;          // Object for obtaining data on the symbol properties

   int               iVolumesHandle;      // Tick volume indicator handle
   double            volumes[];           // Receiver array of indicator values (volumes themselves)  
};

OnInit() 和 OnTick() 函数成为 Init() 和 Tick() 公共方法,所有其他函数成为 CSimpleVolumesStrategy 类的新私有方法。公有方法可以从外部代码中调用,例如从 EA 对象方法中调用。而私有方法只能从指定类的方法中调用。让我们在类描述中添加方法头。

class CSimpleVolumeStrategy : public CStrategy {
private:
   //---  ... previous code
   double            volumes[];           // Receiver array of indicator values (volumes themselves)

   //--- Methods
   void              UpdateCounts();      // Calculate the number of open orders and positions
   int               SignalForOpen();     // Signal for opening pending orders
   void              OpenBuyOrder();      // Open the BUY_STOP order
   void              OpenSellOrder();     // Open the SELL_STOP order
   double            ArrayAverage(
      const double &array[]);             // Average value of the number array

public:
   //--- Public methods
   virtual int       Init();              // Strategy initialization method
   virtual void      Tick();              // OnTick event handler
};

在这些函数的实现位置,在它们的名称前添加 "CSimpleVolumesStrategy::" 前缀,以便让编译器清楚地知道,这些不再只是函数,而是我们类的函数方法。 

class CSimpleVolumeStrategy : public CStrategy {
   // Class description listing properties and methods...
};

int CSimpleVolumeStrategy::Init() {
// Function code ...
}

void CSimpleVolumeStrategy::Tick() {
// Function code ...
}

void CSimpleVolumeStrategy::UpdateCounts() {
// Function code ...
}

int CSimpleVolumeStrategy::SignalForOpen() {
// Function code ...
}

void CSimpleVolumeStrategy::OpenBuyOrder() {
// Function code ...
}

void CSimpleVolumeStrategy::OpenSellOrder() {
// Function code ...
}

double CSimpleVolumeStrategy::ArrayAverage(const double &array[]) {
// Function code ...
}

在最初的简单 EA 中,输入参数值是在声明时分配的。在启动已编译的 EA 时,输入参数对话框中的数值(而不是代码中设置的数值)会被分配给它们。在类描述中无法做到这一点,因此构造函数在此发挥作用。

让我们创建一个带有必要参数列表的构造函数。构造函数也应该是公有的,否则我们将无法从外部代码中创建策略对象。

class CSimpleVolumeStrategy : public CStrategy {
private:
   //---  ... previous code   

public:
   //--- Public methods
   CSimpleVolumeStrategy(
      ulong            p_magic,
      string           p_symbol,
      ENUM_TIMEFRAMES  p_timeframe,
      double           p_fixedLot,
      int              p_signalPeriod,
      double           p_signalDeviation,
      double           p_signaAddlDeviation,
      int              p_openDistance,
      double           p_stopLevel,
      double           p_takeLevel,
      int              p_ordersExpiration,
      int              p_maxCountOfOrders
   );                                     // Constructor

   virtual int       Init();              // Strategy initialization method
   virtual void      Tick();              // OnTick event handler
};

类的说明已准备就绪。除了构造函数外,它的所有方法都已经有了实现。就让我们加进去吧。在最简单的情况下,该类的构造函数只会将接收到的参数值分配给该类的相应成员。此外,前四个参数将通过调用基类构造函数来实现这一点。

CSimpleVolumeStrategy::CSimpleVolumeStrategy(
   ulong            p_magic,
   string           p_symbol,
   ENUM_TIMEFRAMES  p_timeframe,
   double           p_fixedLot,
   int              p_signalPeriod,
   double           p_signalDeviation,
   double           p_signaAddlDeviation,
   int              p_openDistance,
   double           p_stopLevel,
   double           p_takeLevel,
   int              p_ordersExpiration,
   int              p_maxCountOfOrders) : 
   // Initialization list
   CStrategy(p_magic, p_symbol, p_timeframe, p_fixedLot), // Call the base class constructor
   signalPeriod_(p_signalPeriod),
   signalDeviation_(p_signalDeviation),
   signaAddlDeviation_(p_signaAddlDeviation),
   openDistance_(p_openDistance),
   stopLevel_(p_stopLevel),
   takeLevel_(p_takeLevel),
   ordersExpiration_(p_ordersExpiration),
   maxCountOfOrders_(p_maxCountOfOrders)
{}

要做的事情已经所剩无几。在所有遇到 fixedLot_ 和 magicN_ 的地方,将它们重命名 为 m_fixedLot 和 m_magic。用 m_symbol 基类变量代替获取 Symbol() 当前交易品种的函数,用 m_timeframe 代替 PERIOD_CURRENT 常量。将此代码保存在当前文件夹的 SimpleVolumesStrategy.mqh 文件中。


EA 类

让我们创建 CAdvisor 基类。其目的是存储特定交易策略的对象列表,并启动其事件处理程序。对于这个类来说,CExpert 这个名字更合适,但它已经在标准库中使用,所以我们将使用 CAdvisor 来代替。

#include "Strategy.mqh"

class CAdvisor : public CObject {
protected:
   CStrategy         *m_strategies[];  // Array of trading strategies
   int               m_strategiesCount;// Number of strategies

public:
   virtual int       Init();           // EA initialization method
   virtual void      Tick();           // OnTick event handler
   virtual void      Deinit();         // Deinitialization method

   void              AddStrategy(CStrategy &strategy);   // Strategy adding method
};

在 Init() 和 Tick() 方法中,会循环使用 m_strategies[] 数组中的所有策略,并调用相应的事件处理方法。

void CAdvisor::Tick(void) {
   // Call OnTick handling for all strategies
   for(int i = 0; i < m_strategiesCount; i++) {
      m_strategies[i].Tick();
   }
}

在策略添加方法中,情况正是如此。

void CAdvisor::AddStrategy(CStrategy &strategy) {
   // Increase the strategy number counter by 1
   m_strategiesCount = ArraySize(m_strategies) + 1;
   
   // Increase the size of the strategies array
   ArrayResize(m_strategies, m_strategiesCount);
   // Write a pointer to the strategy object to the last element
   m_strategies[m_strategiesCount - 1] = GetPointer(strategy);
}

让我们把这段代码保存到当前文件夹的 Advisor.mqh 文件中。在该类的基础上,可以创建实现管理多个策略的任何特定方法的子类。但现在,我们将只局限于这个基类,而不会以任何方式干涉单个策略的工作。


使用多种策略进行交易的 EA

要编写交易 EA,我们只需创建一个全局 EA 对象(属于 CAdvisor 类)。

在 OnInit() 初始化事件处理程序中,我们将使用所选参数创建策略对象,并将其添加到 EA 对象中。之后,我们会调用 EA 对象的 Init() 方法,以便初始化其中的所有策略。

OnTick() 和 OnDeinit() 事件处理程序只需调用 EA 对象的相应方法。

#include "Advisor.mqh"
#include "SimpleVolumesStartegy.mqh"

input double depoPart_  = 0.8;      // Part of the deposit for one strategy
input ulong  magic_     = 27182;    // Magic

CAdvisor     expert;                // EA object

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   expert.AddStrategy(...);
   expert.AddStrategy(...);

   int res = expert.Init();   // Initialization of all EA strategies

   return(res);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   expert.Tick();
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   expert.Deinit();
}
//+------------------------------------------------------------------+

现在,让我们来详细了解创建策略对象。由于每个策略实例都会打开并考虑到自己的订单和仓位,因此它们应该具有不同的幻数。幻数是策略构造函数的第一个参数。因此,为了保证不同的幻数,我们将在 magic_ 参数指定的原始幻数上添加不同的数字。

   expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 1, ...));
   expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 2, ...));

构造函数的第二和第三个参数是交易品种和周期数。由于我们对 EURGBP H1 进行了优化,因此我们指定了这些特定值。

   expert.AddStrategy(new CSimpleVolumeStrategy(
                         magic_ + 1, "EURGBP", PERIOD_H1, ...));
   expert.AddStrategy(new CSimpleVolumeStrategy(
                         magic_ + 2, "EURGBP", PERIOD_H1, ...));

下一个重要参数是 开仓头寸的大小。我们已经计算出两种策略的适当大小(0.34 和 0.10)。但这是策略单独运行时处理 10%(10,000 美元)回撤的大小。如果两个策略同时起作用,第一个策略的回撤可能会与第二个策略的回撤相加。在最坏的情况下,为了不超过规定的 10%,我们将不得不把建仓规模减半。但是,两种策略的回撤情况可能并不一致,甚至在一定程度上相互抵消。在这种情况下,我们可以稍微减少仓位,但仍不能超过 10%。因此,我们将还原乘数设为 EA 参数 (depoPart_),然后选择最佳值。

策略构造器 的其余参数是我们在优化简单 EA 后选择的数值集。最终结果如下:

   expert.AddStrategy(new CSimpleVolumeStrategy(
                         magic_ + 1, "EURGBP", PERIOD_H1,
                         NormalizeDouble(0.34 * depoPart_, 2),
                         130, 0.9, 1.4, 231, 3750, 50, 600, 3)
                     );
   expert.AddStrategy(new CSimpleVolumeStrategy(
                         magic_ + 2, "EURGBP", PERIOD_H1,
                         NormalizeDouble(0.10 * depoPart_, 2),
                         159, 1.7, 0.8, 248, 3600, 495, 39000, 3)
                     );

将生成的代码保存到当前文件夹的 SimpleVolumesExpert.mq5 文件中。


测试结果

在测试组合 EA 之前,让我们记住,使用第一组参数的策略应产生约 91% 的利润,而使用第二组参数的策略应产生 72% 的利润(对于 10,000 美元的起始存款和 10%(1,000 美元)的最大回撤,以及最佳手数)。

让我们根据维持给定回撤的标准来选择depoPart_ 参数的最佳值,结果如下。

图 3.组合 EA 操作结果

测试期结束时的余额约为 22 400 美元,利润率为 124%。这比我们在运行该策略的单个实例时得到的结果要多。我们只需利用现有的交易策略,而无需对其进行任何修改,就能改善交易结果。


结论

在实现目标的道路上,我们只迈出了一小步。这让我们更加相信,这种方法可以提高交易质量。到目前为止,EA 还缺乏许多重要方面。

例如,我们看了一个非常简单的策略,它不以任何方式控制平仓,不需要准确确定柱形的起始点,也不使用任何繁琐的计算。要在重启终端后恢复状态,除了计算未结头寸和订单(EA 可以做到这一点)外,您不需要做任何额外的努力 。但并非每个策略都如此简单。此外,该 EA 无法在净额结算账户上运行,并且可以同时打开相反的仓位。我们还没有考虑在不同的交易品种上下功夫。等等等等......

在真正开始交易之前,一定要考虑到这些方面。敬请期待新文章。


    本文由MetaQuotes Ltd译自俄文
    原文地址: https://www.mql5.com/ru/articles/14026

    附加的文件 |
    SimpleVolumes.mq5 (21.22 KB)
    Strategy.mqh (3.78 KB)
    Advisor.mqh (6.29 KB)
    交易策略 交易策略
    各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
    开发回放系统(第 41 部分):启动第二阶段(二) 开发回放系统(第 41 部分):启动第二阶段(二)
    如果到目前为止,你觉得一切都很好,那就说明你在开始开发应用程序时,并没有真正考虑到长远的问题。随着时间的推移,你将不再需要为新的应用程序编程,只需让它们协同工作即可。让我们看看如何完成鼠标指标的组装。
    新手在交易中的10个基本错误 新手在交易中的10个基本错误
    新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
    开发回放系统(第 38 部分):铺路(II) 开发回放系统(第 38 部分):铺路(II)
    许多认为自己是 MQL5 程序员的人,其实并不具备我在本文中将要概述的基础知识。许多人认为 MQL5 是一个有限的工具,但实际原因是他们尚未具备所需的知识。所以,如果您有啥不知道,不要为此感到羞愧。最好是因为不去请教而感到羞愧。简单地强制 MetaTrader 5 禁用指标重叠,并不能确保指标和智能系统之间的双向通信。我们离这个目标还很远,但指标在图表上没有重叠的事实给了我们一些信心。