English Русский Español Português
preview
开发多币种 EA 交易(第 24 部分):添加新策略(一)

开发多币种 EA 交易(第 24 部分):添加新策略(一)

MetaTrader 5测试者 |
119 0
Yuriy Bykov
Yuriy Bykov

概述

在上一篇文章中,我们继续开发了用于在 MetaTrader 5 中自动优化交易策略的系统。该系统的核心是一个优化数据库,其中包含有关优化项目的信息。为了创建项目,编写了项目创建脚本。尽管该脚本是为了优化特定交易策略( SimpleVolumes )而编写的项目,但它可以用作模板,并可进行调整以适应其他交易策略。

我们在项目的最后阶段创建了自动导出选定交易策略组的功能。导出内容到一个名为 EA 数据库的独立数据库中。最终的 EA 程序可以使用它来更新交易系统的设置,而无需重新编译。这样我们就可以在测试器中模拟 EA 在一段时间内的工作情况,在此期间,新的项目优化结果可能会出现多次。

我们也终于采用了更有意义的项目文件组织结构,将所有文件分为两部分。第一部分,即 Advisor 库,被移到了 MQL5/Include 文件夹,而其余部分则保留在 MQL5/Experts 工作文件夹中。我们已将所有支持自动优化系统且独立于正在优化的交易策略类型的文件移至库的部分。项目工作文件夹包含阶段 EA、最终 EA 和用于创建优化项目的脚本。 

然而,我还是把 SimpleVolumes 模型交易策略留在了库部分,因为当时对我们来说更重要的是测试自动更新策略参数的机制在最终 EA 中是如何工作的。在编译过程中,包含交易策略源代码的文件究竟连接到哪里并不重要。

现在我们试着想象一下,我们想要采取一个新的交易策略,并将其连接到一个自动优化系统,为其创建阶段 EA 和最终 EA。我们需要什么?


规划路径

首先,让我们选择一个简单的策略,并将其用代码实现,以便在我们的 Advisor 库中使用。让我们将它的代码放在项目的工作文件夹中。策略创建完成后,即可创建第一阶段的 EA 交易系统,该系统将用于优化此交易策略单个实例的参数。在这里,我们将遇到一些与需要分离库代码和项目代码相关的困难。

我们可以对前一部分中编写的第二和第三阶段使用几乎相同的 EA,因为其库部分的代码没有提及所使用的交易策略类别。您需要添加一个命令,将新策略文件包含到项目工作文件夹中的代码中。

对于新策略,我们需要对优化数据库中的项目创建 EA 脚本进行一些修改。至少,这些变化会影响第一阶段 EA 的输入参数模板,因为新交易策略中的输入参数组成将与之前的策略不同。

在优化数据库中修改项目创建 EA 后,我们就可以运行它了。我们将创建优化数据库,并将本项目所需的优化任务添加到该数据库中。接下来,我们可以运行自动优化输送机,然后等待它完成工作。这是一个相当漫长的过程。其持续时间取决于所选的优化时间间隔(时间间隔越长,所需时间越长)、交易策略本身的复杂性(交易策略越复杂,所需时间越长),当然,还取决于可用于优化的测试代理的数量(测试代理越多,所需时间越短)。

最后一步是运行最终 EA 或在策略测试器中对其进行测试,以评估优化结果。

让我们开始吧!


SimpleCandles 策略

MQL5/Experts 文件夹中为项目创建一个新文件夹。例如,我们称其为 Article.17277 。为了避免日后产生误解,最好立即发布免责声明。我将从两个方面使用“项目”一词。其中一种情况是,它仅仅意味着一个文件夹,其中包含用于自动优化特定交易策略的 EA 文件。这些EA的代码将使用来自 Advisor 库的包含文件。因此,在这种情况下,项目仅仅是终端专家文件夹中的一个工作文件夹。在另一种情况下,“项目”一词指的是在优化数据库中创建的数据结构,它描述了必须自动执行的优化任务,以获得最终用于交易账户的 EA 程序的结果。在这种情况下,项目本质上是在优化本身开始之前填充优化数据库。

现在我们谈论的是第一种意义上的项目。所以,让我们在项目工作文件夹中创建一个名为 Strategies 的 子文件夹。我们将把各种交易策略的文件放在里面。目前,我们只会在那里制定一项新策略。

让我们重复第一部分中开发 SimpleVolumes 交易策略的步骤。我们也从交易理念的制定开始。  

让我们假设,当某个交易品种在同一方向上出现多个连续烛形时,下一根烛形将具有不同方向的概率会稍微高一些。那么,如果我们在这种烛形出现后反向建仓,我们或许能够从中获利。

让我们试着把这个想法变成一种策略。要做到这一点,我们需要制定一套不包含任何未知参数的开盘和收盘持仓规则。通过这套规则,我们可以确定在策略运行的任何时刻是否应该开仓,如果应该开仓,应该开哪些仓。

首先,让我们明确一下烛形方向的概念。如果烛形的收盘价高于开盘价,我们就称这根烛形为上涨烛形。收盘价低于开盘价的烛形称为下跌烛形。由于我们要评估几个连续的过去烛形的方向,我们将只对已经关闭的烛形应用烛形方向的概念。由此我们可以得出结论,开仓的时机将随着新烛形的出现而到来,也就是新烛形的出现。

我们已经确定了开仓的时间,但是平仓的时间呢?我们将采用最简单的方案:开仓时,设置止损位和止盈位,并在达到这些止损止盈位时平仓。

现在我们可以对我们的策略作如下描述:

当新的一根柱形开始时,之前几根柱形都朝着同一个方向(上涨或下跌)发展,这就是开仓的信号。如果烛形向上波动,我们就开立卖单。否则,我们就开立买入仓位。 

每个仓位都有止损位和止盈位,只有达到这些止损位和止盈位时才会平仓。如果已经有一个未平仓位,并且再次收到开仓信号,那么如果仓位数量不太大,就可以开立额外的仓位。

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

下面是提出的问题:

  • “……前几根蜡烛…… ”——“几根”到底是多少?
  • “……可以开立额外仓位…… ”—— 总共可以开立多少个仓位?
  • ……设有止损和止盈位…… ”—— 如何使用这些值?如何计算它们?

“几根”蜡烛是多少? 这是最简单的问题。这个数值只是众多策略参数之一,可以通过改变这些参数来找到最佳值。它只能是整数,而且不能很大,可能不超过 10,因为从图表来看,长时间的单向烛形序列很少见。

总共可以开立多少个仓位? 这也可以作为策略参数,并在优化过程中选择最佳值。 

如何使用止损和止盈值?如何计算它们? 这是一个稍微复杂一些的问题,但最简单的情况下,我们可以用与之前问题相同的方法来回答:止损和止盈点数将作为策略参数。开仓时,我们将按照这些参数中指定的点数,沿所需方向偏离开仓价格。然而,也可以采用稍微复杂一些的方法。我们可能不会以点数来设置这些参数,而是以交易工具(交易品种)价格波动率的某个平均值的百分比来设置,该波动率以点数表示。这就引出了下一个问题。

如何找到这种波动率值?有很多方法可以做到这一点。例如,您可以使用现成的 ATR(平均真实范围)波动率指标,或者提出并实现您自己的波动率计算方法。但最有可能的是,此类计算中的参数之一可能是考虑交易工具价格波动幅度的周期数和一个周期的大小。如果我们把这些值加到策略参数中,我们可以用它们来计算波动率。 

由于我们没有对开仓后第一个仓位后续一定要向同一方向开仓进行限制,可能会出现交易策略会向不同方向持仓的情况。在正常情况下,我们将被迫限制这种策略的应用范围,使其仅适用于具有独立头寸核算(“对冲”)的账户。但使用虚拟仓位就没有这种限制了。

现在一切都清楚了,让我们列出所有我们已经提到的策略参数值。我们应该考虑到,为了收到开仓信号,我们需要选择要用来跟踪烛形的交易品种和时间周期。然后我们会得到如下描述:

该 EA 会在特定的交易品种和周期(时间周期)上启动。

设置输入参数:

  • 交易品种(Symbol)
  • 单向烛形计数的时间周期(Timeframe)
  • 同一方向的蜡烛数量(signalSeqLen)
  • ATR 周期数(periodATR)
  • 止损(以点数或 %ATR 表示)(stopLevel)
  • 止盈(以点数或 %ATR 表示)(takeLevel)
  • 同时持仓的最大数量(maxCountOfOrders)
  • 仓位规模

当新的柱形出现时,我们会检查最后收盘的 signalSeqLen 长度的烛形的方向。

如果方向相同且未平仓位数量小于 maxCountOfOrders,则:

  • 计算 StopLoss(止损)和 TakeProfit(止盈)。如果 periodATR = 0,我们只需将当前价格增加从 stopLevel 和 takeLevel 参数中取出的点数即可。如果 periodATR > 0,则使用 periodATR 参数计算日线时间周期的 ATR 值。我们从当前价格后撤 ATR * stopLevel 和 ATR * takeLevel。

  • 如果烛形方向向上,我们就开立卖单;如果烛形方向向下,我们就开立买单。开仓时,设置之前计算出的 StopLoss 和 TakeProfit 水平。

这份描述已经足以开始实现了,我们将解决过程中出现的任何问题。

我还想指出,在描述该策略时,我们没有提及所开仓位的大小。虽然我们正式地将这样的参数添加到了参数列表中,但考虑到在自动优化系统中使用了已开发的策略,我们可以简单地使用最小手数进行测试。在自动优化过程中,将选择合适的仓位大小倍增值,以确保在整个测试期间实现 10% 的指定回撤。因此,我们无需在任何地方手动设置仓位大小。


实现策略

让我们使用现有的 CSimpleVolumesStrategy 类,并在此基础上创建 CSimpleCandlesStrategy 类。它必须被声明为 CVirtualStrategy 类的子类。让我们将所需的策略参数列为类字段,同时记住我们的新类会从其父类继承一些字段和方法。

//+------------------------------------------------------------------+
//| Trading strategy using unidirectional candlesticks               |
//+------------------------------------------------------------------+
class CSimpleCandlesStrategy : public CVirtualStrategy {
protected:
   string            m_symbol;            // Symbol (trading instrument)
   ENUM_TIMEFRAMES   m_timeframe;         // Chart period (timeframe)

   //---  Open signal parameters
   int               m_signalSeqLen;      // Number of unidirectional candles
   int               m_periodATR;         // ATR period

   //---  Position parameters
   double            m_stopLevel;         // Stop Loss (in points or % ATR)
   double            m_takeLevel;         // Take Profit (in points or % ATR)

   //---  Money management parameters
   int               m_maxCountOfOrders;  // Max number of simultaneously open positions

   CSymbolInfo       *m_symbolInfo;       // Object for getting information about the symbol properties

  // ...   

public:
   // Constructor
                     CSimpleCandlesStrategy(string p_params);
   
   virtual string    operator~() override;   // Convert object to string
   virtual void      Tick() override;        // OnTick event handler
};

为了集中获取有关交易工具(交易品种)属性的信息,我们将把指向 CSymbolInfo 类对象的指针包含在类字段中。 

我们新交易策略的类是 CFactorable 类的后代。这样,我们就可以在新类中实现一个构造函数,该构造函数将使用 CFactorable 类中实现的读取方法从初始化字符串中读取参数值。如果在读取过程中没有发生错误,则 IsValid() 方法返回 “true”。

为了处理虚拟仓位,在 CVirtualStrategy 中声明了 m_orders 数组,其目的是存储指向 CVirtualOrder 类对象的指针,即虚拟仓位。因此,在构造函数中,我们将要求创建与 m_maxCountOfOrders 参数中指定的虚拟仓位对象实例数量相同的实例,并将它们放入 m_orders 数组中。CVirtualReceiver::Get() 静态方法将完成这项工作。

由于我们的策略只会在给定时间周期内出现新柱时才会开仓,因此创建一个对象来检查给定交易品种和时间周期内出现新柱的事件。

最后,我们需要在构造函数中请求交易品种监视器为我们的 CSymbolInfo 类创建一个信息对象。

完整的构造函数代码如下所示:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSimpleCandlesStrategy::CSimpleCandlesStrategy(string p_params) {
   // Read parameters from the initialization string
   m_params = p_params;
   m_symbol = ReadString(p_params);
   m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params);
   m_signalSeqLen = (int) ReadLong(p_params);
   m_periodATR = (int) ReadLong(p_params);
   m_stopLevel = ReadDouble(p_params);
   m_takeLevel = ReadDouble(p_params);
   m_maxCountOfOrders = (int) ReadLong(p_params);

   if(IsValid()) {
      // Request the required number of objects for virtual positions
      CVirtualReceiver::Get(&this, m_orders, m_maxCountOfOrders);

      // Add tracking a new bar on the required timeframe
      IsNewBar(m_symbol, m_timeframe);
      
      // Create an information object for the desired symbol
      m_symbolInfo = CSymbolsMonitor::Instance()[m_symbol];
   }
}

接下来,我们需要实现抽象的虚拟波浪号(~)运算符,它返回策略对象的初始化字符串。其实现方式是标准的:

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CSimpleCandlesStrategy::operator~() {
   return StringFormat("%s(%s)", typename(this), m_params);
}

另一个需要实现的虚拟方法是 Tick() 分时报价处理方法。在该方法中,我们检查新柱的出现,以及未平仓头寸的数量是否已达到最大值。如果满足这些条件,那么我们就检查是否存在开仓信号。如果有信号,我们就按相应方向开仓。我们添加到类中的其余方法起到了辅助作用。 

//+------------------------------------------------------------------+
//| "Tick" event handler function                                    |
//+------------------------------------------------------------------+
void CSimpleCandlesStrategy::Tick() override {
// If a new bar has arrived for a given symbol and timeframe
   if(IsNewBar(m_symbol, m_timeframe)) {
// If the number of open positions is less than the allowed number
      if(m_ordersTotal < m_maxCountOfOrders) {
         // Get an open signal
         int signal = SignalForOpen();

         if(signal == 1) {          // If there is a buy signal, then 
            OpenBuy();              // open a BUY position
         } else if(signal == -1) {  // If there is a sell signal, then
            OpenSell();             // open a SELL_STOP position
         }
      }
   }
}

我们将检查是否存在开仓信号的操作移到了单独的 SignalForOpen() 方法中。在这个方法中,我们接收一个包含先前烛形报价的数组,并依次检查它们是否全部向下或向上:

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

   MqlRates rates[];
// Copy the quote values (candles) to the destination array
   int res = CopyRates(m_symbol, m_timeframe, 1, m_signalSeqLen, rates);

// If the required number of candles has been copied
   if(res == m_signalSeqLen) {
      signal = 1; // buy signal

      // Loop through all the candles
      for(int i = 0; i < m_signalSeqLen; i++) {
         // If at least one upward candle occurs, cancel the signal
         if(rates[i].open < rates[i].close ) {
            signal = 0;
            break;
         }
      }

      if(signal == 0) {
         signal = -1; // otherwise, sell signal

         // Loop through all the candles
         for(int i = 0; i < m_signalSeqLen; i++) {
            // If at least one downward candle occurs, cancel the signal
            if(rates[i].open > rates[i].close ) {
               signal = 0;
               break;
            }
         }
      }

   }

   return signal;
}

创建的 OpenBuy()OpenSell() 方法负责开仓,由于它们非常相似,我们将只提供其中一个的代码。该方法的关键在于调用更新止损和止盈水平的方法,该方法会更新两个相应的类字段 - m_slm_tp 的值,以及调用从 m_orders 数组中打开第一个未开立虚拟仓位的方法

//+------------------------------------------------------------------+
//| Open BUY order                                                   |
//+------------------------------------------------------------------+
void CSimpleCandlesStrategy::OpenBuy() {
// Retrieve the necessary symbol and price data
   double point = m_symbolInfo.Point();
   int digits = m_symbolInfo.Digits();

// Opening price
   double price = m_symbolInfo.Ask();

// Update SL and TP levels by calculating ATR
   UpdateLevels();

// StopLoss and TakeProfit levels
   double sl = NormalizeDouble(price - m_sl * point, digits);
   double tp = NormalizeDouble(price + m_tp * point, digits);

   bool res = false;
   for(int i = 0; i < m_maxCountOfOrders; i++) {   // Iterate through all virtual positions
      if(!m_orders[i].IsOpen()) {                  // If we find one that is not open, then open it
         // Open a virtual SELL position
         res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot,
                                0,
                                NormalizeDouble(sl, digits),
                                NormalizeDouble(tp, digits));

         break; // and exit
      }
   }

   if(!res) {
      PrintFormat(__FUNCTION__" | ERROR opening BUY virtual order", 0);
   }
}

水平更新方法首先检查 ATR 计算周期是否设置了非零值。如果答案是肯定的,则调用 ATR 计算函数。它的结果会赋值给 channelWidth 变量。当周期值为 0 时,该变量赋值为 1。在这种情况下, m_stopLevelm_takeLevel 输入的值被解释为点数值,并直接包含在 m_slm_tp 中,没有做任何更改。否则,它们将被解释为 ATR 值的一部分,并相乘以计算出的 ATR 值:

//+------------------------------------------------------------------+
//| Update SL and TP levels based on calculated ATR                  |
//+------------------------------------------------------------------+
void CSimpleCandlesStrategy::UpdateLevels() {
// Calculate ATR
   double channelWidth = (m_periodATR > 0 ? ChannelWidth() : 1);

// Update SL and TP levels
   m_sl = m_stopLevel * channelWidth;
   m_tp = m_takeLevel * channelWidth;
}

我们新交易策略所需的最后一个方法是 ATR 计算方法。如前所述,它可以以不同的方式实现,包括使用现成的解决方案。为简单起见,我们将使用手边可能的实现选项之一:

//+------------------------------------------------------------------+
//| Calculate the ATR value (non-standard implementation)            |
//+------------------------------------------------------------------+
double CSimpleCandlesStrategy::ChannelWidth(ENUM_TIMEFRAMES p_tf = PERIOD_D1) {
   int n = m_periodATR; // Number of bars for calculation
   MqlRates rates[];    // Array for quotes

   // Copy quotes from the daily (default) timeframe
   int res = CopyRates(m_symbol, p_tf, 1, n, rates);

   // If the required amount has been copied
   if(res == n) {
      double tr[];         // Array for price ranges
      ArrayResize(tr, n);  // Change its size
   
      double s = 0;        // Sum for calculating the average
      FOREACH(rates, {
         tr[i] = rates[i].high - rates[i].low; // Remember the bar size
      });
      
      ArraySort(tr); // Sort the sizes

      // Sum the inner two quarters of the bar sizes
      for(int i = n / 4; i < n * 3 / 4; i++) {
         s += tr[i];
      }
      
      // Return the average size in points
      return 2 * s / n / m_symbolInfo.Point();
   }

   return 0.0;
}

将对 Strategies/SimpleCandlesStrategy.mqh 文件所做的更改保存到项目工作文件夹中。


连接策略

所以,整个策略已经准备就绪,现在我们需要将其连接到 EA 文件。让我们从第一阶段 EA 开始。我们在此提醒您,它的代码现在已拆分为两个文件:

  • MQL5/Experts/Article.17277/Stage1.mq5 — 当前研究 SimpleCandles 策略的项目文件;
  • MQL5/Include/antekov/Advisor/Experts/Stage1.mqh — 所有项目通用的库文件。

在当前项目文件中,您需要执行以下操作:

  1. 定义 __NAME__ 常量,为其分配一个与其他项目中的名称不同的唯一值。
  2. 请附上已开发的交易策略类文件。
  3. 连接 Advisor 库中第一阶段 EA 的公共部分。
  4. 列出交易策略的输入参数。
  5. 创建一个名为 GetStrategyParams() 的函数,该函数将输入值转换为策略对象的初始化字符串。
在代码中看起来可能是这样的:

// 1. Define a constant with the EA name
#define  __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME)

// 2. Connect the required strategy
#include "Strategies/SimpleCandlesStrategy.mqh";

// 3. Connect the general part of the first stage EA from the Advisor library
#include <antekov/Advisor/Experts/Stage1.mqh>

//+------------------------------------------------------------------+
//| 4. Strategy inputs                                               |
//+------------------------------------------------------------------+
sinput string     symbol_              = "GBPUSD";
sinput ENUM_TIMEFRAMES period_         = PERIOD_H1;

input group "===  Opening signal parameters"
input int         signalSeqLen_        = 5;     // Number of unidirectional candles
input int         periodATR_           = 30;    // ATR period

input group "===  Pending order parameters"
input double      stopLevel_           = 3750;  // Stop Loss (in points)
input double      takeLevel_           = 50;    // Take Profit (in points)

input group "===  Money management parameters"
input int         maxCountOfOrders_    = 3;     // Maximum number of simultaneously open orders


//+------------------------------------------------------------------+
//| 5. Strategy initialization string generation function            |
//|    from the inputs                                               |
//+------------------------------------------------------------------+
string GetStrategyParams() {
   return StringFormat(
             "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d)",
             symbol_, period_,
             signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_
          );
}
//+------------------------------------------------------------------+

但是,如果我们编译第一阶段 EA 文件(编译过程没有错误),那么运行时 OnInit() 函数中会出现以下错误,导致 EA 停止运行:

2018.01.01 00:00:00   CVirtualFactory::Create | ERROR: Constructor not found for:
2018.01.01 00:00:00   class CSimpleCandlesStrategy("GBPUSD",16385,5,30,2.95,3.92,3)

这样做的原因是,为了创建所有 CFactorable 子类的对象,我们使用 Virtual/VirtualFactory.mqh 文件中单独的 CVirtualFactory::Create() 函数。在 Base/Factorable.mqh 中声明的 NEW(C)CREATE(C, O, P) 宏中调用它。

此函数从初始化字符串中读取对象类名到 className 变量中。从初始化字符串中移除读取到的部分。接下来,对所有可能的类名( CFactorable 后代)进行简单的迭代,直到找到与刚刚读取的名称匹配的类名为止。在这种情况下,会创建一个所需类的新对象,并通过 object 变量返回指向该对象的指针作为创建函数的结果:

// Create an object from the initialization string
   static CFactorable* Create(string p_params) {
      // Read the object class name
      string className = CFactorable::ReadClassName(p_params);
      
      // Pointer to the object being created
      CFactorable* object = NULL;

      // Call the corresponding constructor  depending on the class name
      if(className == "CVirtualAdvisor") {
         object = new CVirtualAdvisor(p_params);
      } else if(className == "CVirtualRiskManager") {
         object = new CVirtualRiskManager(p_params);
      } else if(className == "CVirtualStrategyGroup") {
         object = new CVirtualStrategyGroup(p_params);
      } else if(className == "CSimpleVolumesStrategy") {
         object = new CSimpleVolumesStrategy(p_params);
      } else if(className == "CHistoryStrategy") {
         object = new CHistoryStrategy(p_params);
      } 
            
      // If the object is not created or is created in the invalid state, report an error
      if(!object) {
         ...
      }

      return object;
   }

当我们的所有代码都在一个文件夹中时,我们只需在这里为我们使用的新 CFactorable 子类添加额外的条件语句分支即可。例如,我们第一个 SimpleVolumes 模型策略中负责创建对象的组件就是这样诞生的:

} else if(className == "CSimpleVolumesStrategy") {
   object = new CSimpleVolumesStrategy(p_params);
}

沿用之前的做法,我们应该在这里添加一个类似的模块,用于我们新的 SimpleCandles 模型策略:

} else if(className == "CSimpleCandlesStrategy") {
   object = new CSimpleCandlesStrategy(p_params);
}

但现在这已经违反了将代码分成库和项目部分的原则。代码的库部分不需要知道在使用它时会创建哪些其他新策略。现在,即使是以这种方式创建 CSimpleVolumesStrategy 也显得不妥。

让我们尝试找到一种方法,一方面确保创建所有必要的对象,另一方面实现代码的清晰分离。


提高 CFactorable

我必须承认,这项任务并不简单。它迫使我认真思考它的解决方案,并尝试了不止一个实现选项,最终找到了目前仍在使用的选项。如果 MQL5 语言能够从已经编译的程序中的字符串执行代码,那么所有问题都将非常简单地解决。但出于安全考虑,我们没有类似其他编程语言中的 eval() 函数的功能。因此,我们只能利用手头的机会。

总的来说,其思路是这样的:每个 CFactorable 子类都应该有一个静态函数来创建给定类的对象。所以,我们处理的是一种静态构造函数。在这种情况下,可以将常规构造函数设为非公有,并且只能使用静态构造函数来创建对象。接下来,我们需要以某种方式将类的字符串名称与这些函数关联起来,以便我们能够根据从初始化字符串中获得的类名来了解我们需要调用哪个构造函数。

要解决这个问题,我们需要函数指针。这是一种特殊类型的变量,允许我们在变量中存储指向函数代码的指针,并使用该指针调用函数代码。如您所见,所有不同 CFactorable 子类的对象的静态构造函数都可以使用以下签名声明:

static CFactorable* Create(string p_params)

因此,我们可以创建一些静态数组,在其中为所有子类放置指向此类函数的指针。构成 Advisor 库的类( CVirtualAdvisor、CVirtualStrategyGroup、CVirtualRiskManager )将以某种方式添加到库代码中的此数组中。同时,交易策略类将从项目工作文件夹中的代码添加到此数组中。这样就能实现所需的代码分离。

下一个问题是 —— 我们如何实现这一切?这个静态数组应该在哪个类中声明?如何对其进行填充?如何保持类名与数组元素的关联?

起初,似乎最合适的做法是将此静态数组创建为 CFactorable 类的一部分。对于绑定,我们可以创建另一个静态字符串数组 —— 类名。如果同时向一个数组添加一个类名,并向另一个数组添加指向该类对象的静态构造函数的指针,我们将得到两个数组元素之间的索引关系。换言之,在一个数组中找到一个元素的索引等于所需类名后,可以使用该索引从另一个数组中获取指向构造函数的指针,然后通过初始化字符串调用它。

但是我们如何填充这些数组呢?我真的不想创建任何需要在 OnInit() 中调用的函数。虽然事实证明,这种方法是完全可行的。但最终,我做出了不同的决定。

基本思路是,我们希望能够从描述 CFactorable 后代对象类的文件中,而不是从 OnInit() 中调用一些代码。但是,如果只是将代码放在类定义之外,它将不会被执行。但是如果你在类定义之外声明了一个全局变量,它是某个类的对象,那么它的构造函数就会在这里被调用!

因此,让我们专门为此目的创建一个单独的类 CFactorableCreator 。它的对象将存储类名和指向给定类的静态构造函数的指针。这个类还会有一个指向同一类对象的指针的静态数组。同时, CFactorableCreator 构造函数会确保它创建的每个对象最终都会进入这个数组:

// Preliminary class definition
class CFactorable;

// Type declaration - pointer to the function for creating objects of the CFactorable class
typedef CFactorable* (*TCreateFunc)(string);

//+------------------------------------------------------------------+
//| Class of creators that bind names and static                     |
//| constructors of CFactorable descendant classes                   |
//+------------------------------------------------------------------+
class CFactorableCreator {
public:
   string            m_className;   // Class name
   TCreateFunc       m_creator;     // Static constructor for the class

   // Creator constructor
                     CFactorableCreator(string p_className, TCreateFunc p_creator);

   // Static array of all created creator objects
   static CFactorableCreator* creators[];
};

// Static array of all created creator objects
CFactorableCreator* CFactorableCreator::creators[];

//+------------------------------------------------------------------+
//| Creator constructor                                              |
//+------------------------------------------------------------------+
CFactorableCreator::CFactorableCreator(string p_className, TCreateFunc p_creator) :
   m_className(p_className),
   m_creator(p_creator) {
// Add the current creator object to the static array
   APPEND(creators, &this);
}
//+------------------------------------------------------------------+

让我们以 CVirtualAdvisor 类为例,看看如何组织 CFactorableCreator::creators 数组的补充。 我们将把 CVirtualAdvisor 构造函数转移到“受保护”部分,并添加 Create() 静态构造函数。描述完类之后,创建名为 CVirtualAdvisorCreatorCFactorableCreator 类的全局对象。就在调用 CFactorableCreator 构造函数时, CFactorableCreator::creators 数组会被补充。

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {

protected:
   //...
                     CVirtualAdvisor(string p_param);    // Private constructor
public:
                     static CFactorable* Create(string p_params) { return new CVirtualAdvisor(p_params) };
                    //...
};

CFactorableCreator CVirtualAdvisorCreator("CVirtualAdvisor", CVirtualAdvisor::Create);

我们需要对 CFactorable 的所有子类对象进行相同的三个修改。为了简化操作,我们将在包含 CFactorable 类的文件中声明两个辅助宏:

// Declare a static constructor inside the class
#define STATIC_CONSTRUCTOR(C) static CFactorable* Create(string p) { return new C(p); }

// Add a static constructor for the new CFactorable descendant class
// to a special array by creating a global object of the CFactorableCreator class 
#define REGISTER_FACTORABLE_CLASS(C) CFactorableCreator C##Creator(#C, C::Create);

他们只是重复了我们已经为 CVirtualAdvisor 类开发的代码模板。现在我们可以这样修改:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   // ...
                     CVirtualAdvisor(string p_param);    // Constructor
public:
                     STATIC_CONSTRUCTOR(CVirtualAdvisor);
                    // ...
};

REGISTER_FACTORABLE_CLASS(CVirtualAdvisor);

需要对 Advisor 库中的三个类文件( CVirtualAdvisor、CVirtualStrategyGroup、CVirtualRiskManager )进行类似的更改,但这只需要做一次。既然这些更改已经写入库中,我们就可以忘记它们了。 

在项目工作文件夹中的交易策略类文件中,每增加一个新类都必须添加此类内容。让我们将它们添加到我们的新策略中,之后其类描述代码将如下所示:

//+------------------------------------------------------------------+
//| Trading strategy using unidirectional candlesticks               |
//+------------------------------------------------------------------+
class CSimpleCandlesStrategy : public CVirtualStrategy {
protected:
   string            m_symbol;            // Symbol (trading instrument)
   ENUM_TIMEFRAMES   m_timeframe;         // Chart period (timeframe)

   //---  Open signal parameters
   int               m_signalSeqLen;      // Number of unidirectional candles
   int               m_periodATR;         // ATR period

   //---  Position parameters
   double            m_stopLevel;         // Stop Loss (in points or % ATR)
   double            m_takeLevel;         // Take Profit (in points or % ATR)

   //---  Money management parameters
   int               m_maxCountOfOrders;  // Max number of simultaneously open positions

   CSymbolInfo       *m_symbolInfo;       // Object for getting information about the symbol properties

   double            m_tp;                // Stop Loss in points
   double            m_sl;                // Take Profit in points

   //--- Methods
   int               SignalForOpen();     // Signal to open a position
   void              OpenBuy();           // Open a BUY position
   void              OpenSell();          // Open a SELL position

   double            ChannelWidth(ENUM_TIMEFRAMES p_tf = PERIOD_D1); // Calculate the ATR value
   void              UpdateLevels();      // Update SL and TP levels

   // Private constructor
                     CSimpleCandlesStrategy(string p_params);

public:
   // Static constructor
                     STATIC_CONSTRUCTOR(CSimpleCandlesStrategy);

   virtual string    operator~() override;   // Convert object to string
   virtual void      Tick() override;        // OnTick event handler
};

// Register the CFactorable descendant class
REGISTER_FACTORABLE_CLASS(CSimpleCandlesStrategy);

我再次强调,任何新的交易策略类都应该包含上述重点内容

剩下的就是将填充好的对象创建器数组应用到 CVirtualFactory::Create() 初始化字符串中的通用对象创建函数中。这里我们也会做一些改动。事实证明,我们不再需要将此函数放在单独的类中。以前这样做是因为从形式上讲, CFactorable 类不需要知道它所有后代的名称。更改完成后,我们可能不知道所有后代的名称,但我们可以通过访问单个数组 CFactorableCreator::creators 的元素来访问静态构造函数,从而创建其中任何一个后代。所以,让我们把这个函数的代码移到 CFactorable::Create() 类的一个新静态方法中:

//+------------------------------------------------------------------+
//| Base class of objects created from a string                      |
//+------------------------------------------------------------------+
class CFactorable {
 // ...

public:
   // ...

   // Create an object from the initialization string
   static CFactorable* Create(string p_params);
};


//+------------------------------------------------------------------+
//| Create an object from the initialization string                  |
//+------------------------------------------------------------------+
CFactorable* CFactorable::Create(string p_params) {
// Pointer to the object being created
   CFactorable* object = NULL;

// Read the object class name
   string className = CFactorable::ReadClassName(p_params);

// Find and call the corresponding constructor depending on the class name
   int i;
   SEARCH(CFactorableCreator::creators, className == CFactorableCreator::creators[i].m_className, i);
   if(i != -1) {
      object = CFactorableCreator::creators[i].m_creator(p_params);
   }

// If the object is not created or is created in the invalid state, report an error
   if(!object) {
      PrintFormat(__FUNCTION__" | ERROR: Constructor not found for:\n%s",
                  p_params);
   } else if(!object.IsValid()) {
      PrintFormat(__FUNCTION__
                  " | ERROR: Created object is invalid for:\n%s",
                  p_params);
      delete object; // Remove the invalid object
      object = NULL;
   }

   return object;
}

如您所见,我们首先从初始化字符串中获取类名,然后搜索创建者数组中类名与所需类名匹配的元素的索引。所需的索引值被放入变量 i 中。如果找到索引,则通过指向函数的相应指针调用所需类对象的静态构造函数。此代码中不再引用 CFactorable 子类的名称。包含 CVirtualFactory 类的文件已变得多余。它将被从库中移除。


检查第一阶段 EA

让我们编译第一阶段 EA,并(目前)手动运行优化。以 2018 年至 2023 年(含)为优化区间,以 GBPUSD 为交易品种,以 H4 时间为时间周期。优化过程顺利启动,一段时间后我们可以查看获得的结果:

图 1.Stage1.mq5 EA 的优化设置和优化结果可视化

我们来看几个看起来还算不错的单个通过。

图 2.使用以下参数进行测试的结果:class CSimpleCandlesStrategy("GBPUSD",16388,4,23,2.380,4.950,19)

如图 2 所示,开盘价出现在同一方向的四根蜡烛线之后,止损位和止盈位的比率约为 1:2。 

图 3.使用以下参数进行测试的结果:class CSimpleCandlesStrategy("GBPUSD",16388,7,9,0.090,3.840,1)

图 3 显示了开盘价在同一方向上出现七根烛形后出现的交易结果。在这种情况下,使用了非常短的止损位和很大的止盈位。这一点在图表上清晰可见,绝大多数交易以小幅亏损收盘,6 年来只有十几个交易以盈利收盘,尽管亏损较大。

所以,即使这种交易策略非常简单,你也可以尝试将其多次合并到一个最终 EA 中,以获得更好的结果。


结论

我们尚未完成将新策略与自动优化系统连接起来的过程,但我们已经采取了重要步骤,这将使我们能够继续按照既定路线前进。首先,我们已经实现了一个新的交易策略,它是一个单独的类,是 CVirtualStrategy 的子类。其次,我们能够将其连接到第一阶段 EA,并验证了启动该 EA 的优化过程是可能的。

在第一阶段,对单个交易策略实例进行优化,此时优化数据库还不包含任何运行结果。对于第二阶段和第三阶段,已经需要将第一阶段的优化结果存储在数据库中。因此,目前还无法将策略连接到第二阶段和第三阶段的 EA 上进行测试。首先,我们需要在优化数据库中创建一个项目并运行它,以累积第一阶段的结果。在下一部分中,我们将继续我们开始的工作,考虑修改项目创建 EA。

感谢您的关注!期待很快与您见面!


重要警告

本文和本系列之前的所有文章中的所有结果仅基于历史测试数据,并不保证未来会有任何利润。该项目中的工作具有研究性质。所有已发表的结果都可以由任何人使用,风险自负。


存档内容

#
 名称
版本  描述  最近修改
  MQL5/Experts/Article.17277   项目工作文件夹  
1 CreateProject.mq5 1.01
用于创建具有阶段、作业和优化任务的项目的 EA 脚本。
第 23 部分
2 Optimization.mq5
1.00 用于项目自动优化的 EA  第 23 部分
3 SimpleCandles.mq5
1.00 最终 EA,用于并行运行多组模型策略。参数将从内置组库中获取。
第 24 部分
4 Stage1.mq5 1.22  交易策略单实例优化 EA(第一阶段)
第 24 部分
5 Stage2.mq5
1.00 交易策略实例组优化 EA(第二阶段)
第 23 部分
Stage3.mq5
1.00 EA 将生成的标准化策略组保存到具有给定名称的 EA 数据库中。
第 23 部分
  MQL5/Experts/Article.17277/Strategies   项目策略文件夹  
7 SimpleCandlesStrategy.mqh 1.01   第 24 部分
  MQL5/Include/antekov/Advisor/Base
  其他项目类所继承的基类    
8 Advisor.mqh 1.04 EA 基类 第 10 部分
9 Factorable.mqh
1.05
从字符串创建的对象的基类
第 24 部分
10 FactorableCreator.mqh
1.00   第 24 部分
11 Interface.mqh 1.01
可视化各种对象的基类
第 4 部分
12 Receiver.mqh
1.04  将未平仓交易量转换为市场仓位的基类
第 12 部分
13 Strategy.mqh
1.04
交易策略基类
第 10 部分
  MQL5/Include/antekov/Advisor/Database
  用于处理项目 EA 使用的所有类型数据库的文件
 
14 Database.mqh 1.10 处理数据库的类 第 22 部分
15 db.adv.schema.sql 1.00
最终 EA 的数据库结构 第 22 部分
16 db.cut.schema.sql
1.00 截断优化数据库的结构
第 22 部分
17 db.opt.schema.sql
1.05  优化数据库结构
第 22 部分
18 Storage.mqh   1.01
用于处理 EA 数据库中最终 EA 的键值存储的类
第 23 部分
  MQL5/Include/antekov/Advisor/Experts
  包含不同类型已使用 EA 的公共部分的文件
 
19 Expert.mqh  1.22 最终 EA 的库文件。组参数可以从 EA 数据库中获取。
第 23 部分
20 Optimization.mqh  1.04 用于管理优化任务启动 EA 的库文件
第 23 部分
21 Stage1.mqh
1.19 单实例交易策略优化 EA(第一阶段)的库文件
第 23 部分
22 Stage2.mqh 1.04 用于优化一组交易策略实例的 EA 的库文件(第二阶段)   第 23 部分
23 Stage3.mqh
1.04 EA 库文件,用于将生成的标准化策略组保存到具有给定名称的 EA 数据库中。 第 23 部分
  MQL5/Include/antekov/Advisor/Optimization
  负责自动优化的类
 
24 Optimizer.mqh
1.03  项目自动优化管理器类
第 22 部分
25 OptimizerTask.mqh
1.03
优化任务类
第 22 部分
  MQL5/Include/antekov/Advisor/Strategies    用于演示项目如何运作的交易策略示例
 
26 HistoryStrategy.mqh 
1.00 用于回放交易历史的交易策略类
第 16 部分
27 SimpleVolumesStrategy.mqh
1.11
使用分时交易量的交易策略类
第 22 部分
  MQL5/Include/antekov/Advisor/Utils
  辅助工具、用于代码简化的宏  
28 ExpertHistory.mqh 1.00 用于将交易历史导出到文件的类 第 16 部分
29 Macros.mqh 1.05 用于数组操作的有用的宏 第 22 部分
30 NewBarEvent.mqh 1.00  用于定义特定交易品种的新柱形的类  第 8 部分
31 SymbolsMonitor.mqh  1.00 用于获取交易工具(交易品种)信息的类 第 21 部分
  MQL5/Include/antekov/Advisor/Virtual
  通过使用虚拟交易订单和头寸系统创建各种对象的类
 
32 Money.mqh 1.01  资金管理基类
第 12 部分
33 TesterHandler.mqh  1.07 优化事件处理类  第 23 部分
34 VirtualAdvisor.mqh  1.10  处理虚拟仓位(订单)的 EA 类 第 24 部分
35 VirtualChartOrder.mqh  1.01  图形虚拟仓位类 第 18 部分
36 VirtualHistoryAdvisor.mqh 1.00  交易历史回放 EA 类  第 16 部分
37 VirtualInterface.mqh  1.00  EA GUI 类  第 4 部分
38 VirtualOrder.mqh 1.09  虚拟订单和仓位类  第 22 部分
39 VirtualReceiver.mqh 1.04 将未平仓交易量转换为市场仓位的类(接收方) 第 23 部分
40 VirtualRiskManager.mqh  1.05 风险管理类(风险管理器) 第 24 部分
41 VirtualStrategy.mqh 1.09  具有虚拟仓位的交易策略类 第 23 部分
42 VirtualStrategyGroup.mqh  1.03  交易策略组类 第 24 部分
43 VirtualSymbolReceiver.mqh  1.00 交易品种接收器类  第 3 部分

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

附加的文件 |
MQL5.zip (102.06 KB)
用于MetaTrader 5的WebSocket:借助Windows API实现异步客户端连接 用于MetaTrader 5的WebSocket:借助Windows API实现异步客户端连接
本文详细介绍了开发一款自定义动态链接库的过程,该库旨在为MetaTrader程序提供异步WebSocket客户端连接功能。
在 MQL5 中构建自定义市场状态检测系统(第二部分):智能交易系统(EA) 在 MQL5 中构建自定义市场状态检测系统(第二部分):智能交易系统(EA)
本文详细介绍如何利用第一篇开发的状态检测器,构建一个自适应的智能交易系统(MarketRegimeEA)。该系统能够根据趋势、震荡或高波动市场,自动切换交易策略与风险参数。文中涵盖了实用的参数优化、状态过渡处理以及多时间周期指标的应用。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
在MQL5中构建自定义市场状态检测系统(第一部分):指标 在MQL5中构建自定义市场状态检测系统(第一部分):指标
本文详细介绍了如何使用自相关和波动性等统计方法,在MQL5中创建一个市场状态检测系统。文中提供了用于分类趋势、盘整和波动行情的类代码,以及一个自定义指标。