English Русский Español Deutsch 日本語 Português
preview
开发多币种 EA 交易 (第 10 部分):从字符串创建对象

开发多币种 EA 交易 (第 10 部分):从字符串创建对象

MetaTrader 5交易 | 18 十二月 2024, 11:11
95 0
Yuriy Bykov
Yuriy Bykov

概述

在上一篇文章中,我概述了开发 EA 的总体计划,其中包括几个阶段,每个阶段都会生成一定量的信息以供后续阶段使用。我决定将这些信息保存在数据库中,并在其中创建一个表,我们可以在其中放置各种 EA 策略测试器单次通过的结果。

为了能够在接下来的步骤中使用这些信息,我们需要通过某种方式利用数据库中存储的信息来创建必要的对象(交易策略、策略组和 EA)。没有将对象直接保存到数据库的选项。可以建议的最佳方法是将对象的所有属性转换为字符串,将其保存在数据库中,然后从数据库中读取该字符串并从中创建所需的对象。

可以通过不同的方式来实现从字符串创建对象。例如,我们可以创建具有默认参数的所需类的对象,然后使用特殊的方法或函数来解析从数据库读取的字符串并将对应的值分配给对象属性。或者,我们可以创建一个额外的对象构造函数,它只接受一个字符串作为输入。该字符串将在构造函数中被解析为各个部分,并将对应的值分配给对象属性。为了了解哪个选项更好,我们首先看看如何在数据库中存储有关对象的信息。


存储对象信息

让我们打开上一篇文章中填写的数据库中的表,然后查看最后的列。paramsinput 列存储将 CSimpleVolumesStrategy 类交易策略对象转换为字符串的结果以及单次优化通过的输入。

图 1.passes 表的片段,包含有关应用的策略和测试参数的信息


虽然它们是相关的,但它们之间存在差异: inputs 列包含输入的名称(尽管它们与策略对象属性的名称不完全匹配),但缺少一些参数,例如交易品种和周期。因此,我们使用来自 params 列的输入表单来重新创建对象会更加方便。

让我们回想一下将策略对象转换为字符串的实现。在本系列文章的第四部分中,我实现了将 EA 状态保存到文件中,以便在重启后可以恢复。为了防止 EA 意外使用包含另一个类似 EA 数据的文件,我的实现是将有关此 EA 中使用的所有策略实例的参数数据都保存到文件中。

换句话说,原来的任务是确保具有不同参数的交易策略实例生成不同的字符串。因此,我并不特别关心基于此类字符串创建新的交易策略对象的功能。在第九部分中,我采用了现有的字符串转换机制,没有进行任何额外的修改,因为我的目标是调试将此类信息添加到数据库的过程。


进入实现阶段

现在是时候思考如何使用这样的字符串重新创建对象了。假设这样,我们有一个如下所示的字符串:

class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,17,0.70,0.90,50,10000.00,750.00,10000,3)

如果我们将其传递给 CSimpleVolumesStrategy 类对象构造函数,它应该执行以下操作:

  • 删除第一个左括号之前的部分;
  • 用逗号符号将剩余部分拆分直到右括号;
  • 将每个获得的部分分配给相应的对象属性,必要时将它们转换为数字。

查看这些操作的列表,我们可以看到第一个操作可以在更高的层级执行。确实,如果我们首先从这一行获取类名,那么我们就可以定义所创建对象的类。那么构造函数只传递括号内的字符串部分会更方便。

此外,从字符串创建对象的需求并不局限于这个类。首先,我们可能不只有一种交易策略。其次,我们需要创建 CVirtualStrategyGroup 类对象,也就是具有不同参数的多个交易策略实例的组。这对于将几个先前选择的组组合成一个组的阶段非常有用。第三,是什么阻止我们提供从字符串创建 EA 对象(CVirtualAdvisor 类)的能力?这将使我们能够编写一个通用的 EA,可以从文件中加载所有应使用的策略组的文本描述。通过更改文件中的描述,可以在不重新编译 EA 的情况下完全更新其中包含的策略的组成。

如果我们试着想象一下 CVirtualStrategyGroup 类对象的初始化字符串可能是什么样子,那么我们会得到类似这样的内容:

class CVirtualStrategyGroup([
  class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,17,0.70,0.90,50,10000.00,750.00,10000,3),
  class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,27,0.70,0.90,60,10000.00,550.00,10000,3),
  class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,37,0.70,0.90,80,10000.00,150.00,10000,3)
], 0.33)

CVirtualStrategyGroup 类对象构造函数第一个参数是交易策略对象的数组或交易策略组对象的数组。因此,有必要学习如何解析字符串的一部分,它将是一个类似对象描述的数组。如您所见,我使用了 JSON 或 Python 中的标准符号来表示元素列表(数组):元素的条目用逗号分隔,位于一对方括号内。

我们还必须学习如何从字符串中提取部分,不仅是逗号之间的部分,还包括表示另一个嵌套类对象描述的部分。偶然地,为了将交易策略对象转换为字符串,我们使用了 typename() 函数,该函数以 class 词开头的字符串形式返回对象的类名。现在我们可以在解析字符串时使用这个词来表示后面跟着的是描述某个类的对象的字符串,而不是数字或字符串等简单值。

因此,我们理解了实现工厂设计模式的必要性,当一个特殊的对象将根据请求参与创建各种类的对象时。工厂可以生产的对象在类层次结构中通常应该有一个共同的祖先。因此,让我们从创建一个新的公共类开始,所有类(其对象可以从初始化字符串中创建)最终都将从中派生出来。


新的基类

到目前为止,我们参与继承层次结构的基类有:

  • СAdvisorCVirtualAdvisor 类派生自用于创建 EA 的类。
  • CStrategy。用于创建交易策略的类。CSimpleVolumesStrategy 是从它派生的。
  • CVirtualStrategyGroup。交易策略组的类。它没有后代,而且预计也不会有。
  • 那么,就这些了吗?

是的,我不再看到任何具有需要用字符串初始化的后代的基类。这意味着这三个类需要有一个共同的祖先,其中收集所有必要的辅助方法以确保用字符串进行初始化。

我为新祖先选择的名字还没有什么意义。我想以某种方式强调一下,类的后代将能够在工厂中生产,因此它们将是“可工厂化的”。后来,在开发代码时,字母“y”在某处消失了,只剩下名称 CFaсtorable。

最初,该类看起来像这样:

//+------------------------------------------------------------------+
//| Base class of objects created from a string                      |
//+------------------------------------------------------------------+
class CFactorable {
protected:
   virtual void      Init(string p_params) = 0;
public:
   virtual string    operator~() = 0;

   static string     Read(string &p_params);
};

因此,此类的后代需要具有 Init() 方法,该方法将完成将输入的初始化字符串转换为对象属性值所需的所有工作,以及波浪号运算符,它负责将属性反向转换为初始化字符串。还有 Read() 静态方法。它应该能够从初始化字符串中读取一些数据。数据部分是指包含另一个对象的有效初始化字符串、其他数据部分的数组、数字或字符串常量的子字符串。

尽管这个实现已经可以运行,但我还是决定对其进行重大修改。

首先,出现了 Init() 方法,因为我想同时保留旧的对象构造函数和新的构造函数(接受初始化字符串)。为了避免重复代码,我在 Init() 方法中一次实现,并从几个可能的构造函数中调用它。但最终事实证明,并没有必要使用不同的构造函数。我们只需一个新的构造函数就可以了。因此,Init() 方法代码迁移至新的构造函数,而该方法本身被删除。

其次,最初的实现不包含任何检查初始化字符串和错误报告有效性的方法。我们希望自动生成初始化字符串,这几乎完全消除了此类错误的发生,但如果我们突然把生成的初始化字符串搞砸了,最好能及时了解并找到错误。为了这些目的,我添加了一个新的 m_isValid 逻辑属性,它指示所有对象构造函数代码是否成功执行,或者初始化字符串的某些部分是否包含错误。该属性被设为私有,同时添加了对应的 IsValid()SetInvalid() 方法来获取和设置其值。而且,该属性最初始终为 true,而 SetInvalid() 方法只能将其值设置为 false

第三,由于实施了检查和错误处理,Read() 方法变得过于复杂。因此,它被分成几个独立的方法,专门从初始化字符串中读取不同类型的数据。另外还为数据读取方法添加了几个辅助私有方法。值得单独注意的是,数据读取方法会修改传递给它们的初始化字符串。当成功读取下一部分数据时,它将作为方法的结果返回,而传递的初始化字符串将丢失它读取的部分。

第四,如果原始初始化字符串与创建的对象的参数一起被记住,那么对于不同类的对象,将对象转换回初始化字符串的方法可以几乎相同。因此,在基类中添加了 m_params 属性,用于在对象构造函数中存储初始化字符串。

考虑到所做的添加,声明 CFactorable 类如下所示:

//+------------------------------------------------------------------+
//| Base class of objects created from a string                      |
//+------------------------------------------------------------------+
class CFactorable {
private:
   bool              m_isValid;  // Is the object valid?

   // Clear empty characters from left and right in the initialization string 
   static void       Trim(string &p_params);

   // Find a matching closing bracket in the initialization string
   static int        FindCloseBracket(string &p_params, char closeBraket = ')');

   // Clear the initialization string with a check for the current object validity 
   bool              CheckTrimParams(string &p_params);

protected:
   string            m_params;   // Current object initialization string

   // Set the current object to the invalid state 
   void              SetInvalid(string function = NULL, string message = NULL);

public:
                     CFactorable() : m_isValid(true) {}  // Constructor
   bool              IsValid();                          // Is the object valid?

   // Convert object to string
   virtual string    operator~() = 0;

   // Does the initialization string start with the object definition?
   static bool       IsObject(string &p_params, const string className = "");

   // Does the initialization string start with defining an object of the desired class?
   static bool       IsObjectOf(string &p_params, const string className);

   // Read the object class name from the initialization string 
   static string     ReadClassName(string &p_params, bool p_removeClassName = true);

   // Read an object from the initialization string 
   string            ReadObject(string &p_params);
   
   // Read an array from the initialization string as a string 
   string            ReadArrayString(string &p_params);
   
   // Read a string from the initialization string
   string            ReadString(string &p_params);
   
   // Read a number from the initialization string as a string
   string            ReadNumber(string &p_params);
   
   // Read a real number from the initialization string
   double            ReadDouble(string &p_params);
   
   // Read an integer from the initialization string
   long              ReadLong(string &p_params);
};


我不会在这里详细讨论类方法的实现,不过,我想指出的是,所有读取方法的工作都涉及执行一组大致类似的动作。首先,我们检查初始化字符串是否不为空以及对象是否有效。对象可能进入了无效状态,例如,由于先前从实现字符串读取部分数据的操作失败。因此,这样的检查有助于避免对明显有错误的对象执行不必要的操作。

然后检查某些条件以确保初始化字符串包含正确类型的数据(对象、数组、字符串或数字)。如果是这样,那么我们将在初始化字符串中找到该数据块结束的位置。此位置左侧的所有内容都用于获取返回值,右侧的所有内容替换初始化字符串。

如果在检查的某个阶段我们收到否定结果,则调用将当前对象设置为无效状态的方法,同时向其传递有关错误位置和性质的信息。

将该类的代码保存在当前文件夹中的 Factorable.mqh 文件中。


对象工厂

由于对象初始化字符串始终包含类名,我们可以创建一个公共函数或静态方法来充当对象“工厂”。我们将向它传递一个初始化字符串,而接收指向给定类的创建对象的指针。

当然,对于程序中给定位置中名称可以取单个值的类的对象,不需要存在这样的工厂。我们可以使用 new 运算符以标准方式创建一个对象,方法是将包含创建对象的参数的初始化字符串传递给构造函数。但如果我们必须创建类名可以不同的对象(例如,不同的交易策略),那么 new 运算符就无法帮助我们,因为我们首先需要定义我们要创建的对象的类。我们将这项工作委托给工厂,或者更确切地说,委托给它的唯一静态方法 -Create()。

//+------------------------------------------------------------------+
//| Object factory class                                             |
//+------------------------------------------------------------------+
class CVirtualFactory {
public:
   // 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 == "CVirtualStrategyGroup") {
         object = new CVirtualStrategyGroup(p_params);
      } else if(className == "CSimpleVolumesStrategy") {
         object = new CSimpleVolumesStrategy(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:\nclass %s(%s)",
                     className, p_params);
      } else if(!object.IsValid()) {
         PrintFormat(__FUNCTION__
                     " | ERROR: Created object is invalid for:\nclass %s(%s)",
                     className, p_params);
         delete object; // Remove the invalid object
         object = NULL;
      }

      return object;
   }
};

将此代码保存在当前文件夹的 VirtualFactory.mqh 文件中。

创建两个有用的宏,以便我们将来更容易使用工厂。第一个方法将根据初始化字符串创建一个对象,并通过调用 CVirtualFactory::Create() 方法来替换自身:

// Create an object in the factory from a string
#define NEW(Params) CVirtualFactory::Create(Params)

第二个宏将仅从某个其他对象的构造函数运行,该对象应该是 CFactorable 类的后代。换句话说,只有当我们创建主对象,同时从其构造函数内的初始化字符串实现其他(嵌套)对象时,才会发生这种情况。该宏将接收三个参数:创建的对象类名(Class)、接收指向创建对象的指针的变量名称(Object)和初始化字符串(Params)。

一开始,宏会声明一个具有给定名称和类的指针变量,并用 NULL 值来初始化它。然后我们检查主对象是否有效。如果是,则通过 NEW() 宏调用工厂中的对象创建方法。然后尝试将创建的指针转换为所需的类。如果工厂创建的对象与当前所需的类不同,则为此目的使用 dynamic_cast<>() 运算符可以避免运行时错误。在这种情况下,对象指针将保持为 NULL,程序将继续运行。然后我们检查指针的有效性。如果为空或者无效,则设置主对象为无效状态,报告错误并中止主对象构造函数执行。

该宏如下所示:

// Creating a child object in the factory from a string with verification.
// Called only from the current object constructor.
// If the object is not created, the current object becomes invalid
// and exit from the constructor is performed
#define CREATE(Class, Object, Params)                                                                       \
    Class *Object = NULL;                                                                                   \
    if (IsValid()) {                                                                                        \
       Object = dynamic_cast<C*> (NEW(Params));                                                             \
       if(!Object) {                                                                                        \
          SetInvalid(__FUNCTION__, StringFormat("Expected Object of class %s() at line %d in Params:\n%s",  \
                                                #Class, __LINE__, Params));                                 \
          return;                                                                                           \
       }                                                                                                    \
    }                                                                                                       \

将这些宏添加到 Factorable.mqh 文件的开头。


修改以前的基类

CFactorable 类作为基类添加到所有先前的基类中:СAdvisorСStrategyСVirtualStrategyGroup。前两个不需要任何进一步的修改:

//+------------------------------------------------------------------+
//| EA base class                                                    |
//+------------------------------------------------------------------+
class CAdvisor : public CFactorable {
protected:
   CStrategy         *m_strategies[];  // Array of trading strategies
   virtual void      Add(CStrategy *strategy);  // Method for adding a strategy
public:
                    ~CAdvisor();                // Destructor
   virtual void      Tick();                    // OnTick event handler
   virtual double    Tester() {
      return 0;
   }
};
//+------------------------------------------------------------------+
//| Base class of the trading strategy                               |
//+------------------------------------------------------------------+
class CStrategy : public CFactorable {
public:                     
   virtual void      Tick() = 0; // Handle OnTick events
};

СVirtualStrategyGroup 经历了更为重大的变化。由于这不再是一个抽象基类,我们需要在其中编写一个构造函数的实现,该构造函数根据初始化字符串创建一个对象。这样做,我们摆脱了两个单独的构造函数,它们要么采用一系列策略,要么采用一组数组。此外,转换为字符串的方法现在也发生了变化。在该方法中,我们现在只需将类名添加到保存的带有参数的初始化字符串中。Scale() 缩放方法保持不变。

//+------------------------------------------------------------------+
//| Class of trading strategies group(s)                             |
//+------------------------------------------------------------------+
class CVirtualStrategyGroup : public CFactorable {
protected:
   double            m_scale;                // Scaling factor
   void              Scale(double p_scale);  // Scaling the normalized balance
public:
                     CVirtualStrategyGroup(string p_params); // Constructor

   virtual string    operator~() override;      // Convert object to string

   CVirtualStrategy      *m_strategies[];       // Array of strategies
   CVirtualStrategyGroup *m_groups[];           // Array of strategy groups
};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualStrategyGroup::CVirtualStrategyGroup(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the initialization string of the array of strategies or groups
   string items = ReadArrayString(p_params);

// Until the string is empty
   while(items != NULL) {
      // Read the initialization string of one strategy or group object
      string itemParams = ReadObject(items);

      // If this is a group of strategies,
      if(IsObjectOf(itemParams, "CVirtualStrategyGroup")) {
         // Create a strategy group and add it to the groups array
         CREATE(CVirtualStrategyGroup, group, itemParams);
         APPEND(m_groups, group);
      } else {
         // Otherwise, create a strategy and add it to the array of strategies
         CREATE(CVirtualStrategy, strategy, itemParams);
         APPEND(m_strategies, strategy);
      }
   }

// Read the scaling factor
   m_scale = ReadDouble(p_params);

// Correct it if necessary
   if(m_scale <= 0.0) {
      m_scale = 1.0;
   }

   if(ArraySize(m_groups) > 0 && ArraySize(m_strategies) == 0) {
      // If we filled the array of groups, and the array of strategies is empty, then
      // Scale all groups
      Scale(m_scale / ArraySize(m_groups));
   } else if(ArraySize(m_strategies) > 0 && ArraySize(m_groups) == 0) {
      // If we filled the array of strategies, and the array of groups is empty, then
      // Scale all strategies
      Scale(m_scale / ArraySize(m_strategies));
   } else {
      // Otherwise, report an error in the initialization string
      SetInvalid(__FUNCTION__, StringFormat("Groups or strategies not found in Params:\n%s", p_params));
   }
}

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


    ... 

将所做的更改保存到当前文件夹中的 VirtualStrategyGroup.mqh 文件中。

修改 EA 类

在前一篇文章中,CVirtualAdvisor EA 类接收了 Init() 方法,该方法旨在删除不同 EA 构造函数的代码重复。我们有一个以单一策略作为其第一个参数的构造函数,以及一个以策略组对象作为其第一个参数的构造函数。我们可能不难同意,只有一个构造函数 - 接受一组策略的构造函数。如果我们需要使用一个交易策略的实例,我们首先简单地使用这个策略创建一个组,并将创建的组传递给 EA 构造函数。那么就不需要 Init() 方法和额外的构造函数了。因此,我将保留一个从初始化字符串创建 EA 对象的构造函数:

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

public:
                     CVirtualAdvisor(string p_param);    // Constructor
                    ~CVirtualAdvisor();         // Destructor

   virtual string    operator~() override;      // Convert object to string

   ...
};

...

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the initialization string of the strategy group object
   string groupParams = ReadObject(p_params);

// Read the magic number
   ulong p_magic = ReadLong(p_params);

// Read the EA name
   string p_name = ReadString(p_params);

// Read the work flag only at the bar opening
   m_useOnlyNewBar = (bool) ReadLong(p_params);

// If there are no read errors,
   if(IsValid()) {
      // Create a strategy group
      CREATE(CVirtualStrategyGroup, p_group, groupParams);

      // Initialize the receiver with the static receiver
      m_receiver = CVirtualReceiver::Instance(p_magic);

      // Initialize the interface with the static interface
      m_interface = CVirtualInterface::Instance(p_magic);

      m_name = StringFormat("%s-%d%s.csv",
                            (p_name != "" ? p_name : "Expert"),
                            p_magic,
                            (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                           );

      // Save the work (test) start time
      m_fromDate = TimeCurrent();

      // Reset the last save time
      m_lastSaveTime = 0;

      // Add the contents of the group to the EA
      Add(p_group);

      // Remove the group object
      delete p_group;
   }
}

在构造函数中,我们首先从初始化字符串中读取所有数据。如果在此阶段检测到任何差异,当前创建的 EA 对象将进入无效状态。如果一切顺利,构造函数将创建一个策略组,将其策略添加到其策略数组中,并根据从初始化字符串读取的数据设置其余属性。

但是现在,由于构造函数中创建接收器和接口对象之前进行有效性检查,因此可能无法创建这些对象。因此,在析构函数中,我们需要在删除这些对象之前添加一个指向这些对象的指针的正确性检查:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   if(!!m_receiver)  delete m_receiver;         // Remove the recipient
   if(!!m_interface) delete m_interface;        // Remove the interface
   DestroyNewBar();           // Remove the new bar tracking objects 
}

将更改保存在当前文件夹的 VirtualAdvisor.mqh 文件中。

修改交易策略类

CSimpleVolumesStrategy 策略类中,删除具有单独参数的构造函数,并使用 CFactorable 类方法重写接受初始化字符串的构造函数的代码。

在构造函数中,从初始化字符串中读取参数,该字符串先前已在 m_params 属性中保存了其初始状态。如果在读取过程中没有发生导致策略对象无效的错误,请执行基本操作来初始化对象:填充虚拟仓位数组,初始化指标,并在分钟时间范围内注册新柱形的事件处理程序。

将对象转换为字符串的方法也发生了改变。我们不会通过参数来形成它,而是简单地将类名和保存的初始化字符串连接起来,就像我们在之前考虑的两个类中所做的那样。 

//+------------------------------------------------------------------+
//| Trading strategy using tick volumes                              |
//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : public CVirtualStrategy {
   ...

public:
   //--- Public methods
                     CSimpleVolumesStrategy(string p_params); // Constructor

   virtual string    operator~() override;         // Convert object to string

   virtual void      Tick() override;              // OnTick event handler
};


//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSimpleVolumesStrategy::CSimpleVolumesStrategy(string p_params) {
// Save the initialization string
   m_params = p_params;
   
// Read the parameters from the initialization string
   m_symbol = ReadString(p_params);
   m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params);
   m_signalPeriod = (int) ReadLong(p_params);
   m_signalDeviation = ReadDouble(p_params);
   m_signaAddlDeviation = ReadDouble(p_params);
   m_openDistance = (int) ReadLong(p_params);
   m_stopLevel = ReadDouble(p_params);
   m_takeLevel = ReadDouble(p_params);
   m_ordersExpiration = (int) ReadLong(p_params);
   m_maxCountOfOrders = (int) ReadLong(p_params);
   m_fittedBalance = ReadDouble(p_params);

// If there are no read errors,
   if(IsValid()) {
      // Request the required number of virtual positions
      CVirtualReceiver::Get(GetPointer(this), m_orders, m_maxCountOfOrders);

      // Load the indicator to get tick volumes
      m_iVolumesHandle = iVolumes(m_symbol, m_timeframe, VOLUME_TICK);

      // If the indicator is loaded successfully
      if(m_iVolumesHandle != INVALID_HANDLE) {

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

         // Register the event handler for a new bar on the minimum timeframe
         IsNewBar(m_symbol, PERIOD_M1);
      } else {
         // Otherwise, set the object state to invalid
         SetInvalid(__FUNCTION__, "Can't load iVolumes()");
      }
   }
}

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

我们还从类中删除了 Save()Load() 方法,因为事实证明,在 CVirtualStrategy 父类中实现它们足以完成分配给它的任务。

将更改保存在当前文件夹的 CSimpleVolumesStrategy.mqh 文件中。


交易策略单个实例的 EA

为了优化单个交易策略实例的参数,我们只需要修改 OnInit() 初始化函数。在这个函数中,我们应该从 EA 输入参数中生成一个字符串,用于初始化交易策略对象,然后使用它来将 EA 对象替换到初始化字符串中。

由于我们实现了从初始化字符串读取数据的方法,我们可以自由地在其中使用额外的空格和换行符。然后,当输出到日志或者在数据库中进行记录时,我们可以看到初始化字符串的格式大致如下:

Core 1  2023.01.01 00:00:00   OnInit | Expert Params:
Core 1  2023.01.01 00:00:00   class CVirtualAdvisor(
Core 1  2023.01.01 00:00:00       class CVirtualStrategyGroup(
Core 1  2023.01.01 00:00:00          [
Core 1  2023.01.01 00:00:00           class CSimpleVolumesStrategy("EURGBP",16385,17,0.70,0.90,150,10000.00,85.00,10000,3,0.00)
Core 1  2023.01.01 00:00:00          ],1
Core 1  2023.01.01 00:00:00       ),
Core 1  2023.01.01 00:00:00       ,27181,SimpleVolumesSingle,1
Core 1  2023.01.01 00:00:00   )

OnDeinit() 函数中,我们需要确保指向 EA 对象的指针在删除之前是正确的。现在我们不能再保证 EA 对象总是会被创建,因为理论上我们可能有一个不正确的初始化字符串,这会导致工厂过早删除 EA 对象。

...

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   CMoney::FixedBalance(fixedBalance_);

// Prepare the initialization string for a single strategy instance
   string strategyParams = StringFormat(
                              "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d,%.2f)",
                              symbol_, timeframe_,
                              signalPeriod_, signalDeviation_, signaAddlDeviation_,
                              openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                              maxCountOfOrders_, 0
                           );

// Prepare the initialization string for an EA with a group of a single strategy
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    class CVirtualStrategyGroup(\n"
                            "       [\n"
                            "        %s\n"
                            "       ],1\n"
                            "    ),\n"
                            "    ,%d,%s,%d\n"
                            ")",
                            strategyParams, magic_, "SimpleVolumesSingle", true
                         );

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

// Create an EA handling virtual positions
   expert = NEW(expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}

...

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   if(!!expert) delete expert;
}

将获取的代码保存在当前文件夹的SimpleVolumesExpertSingle.mq5 文件中。


多个实例的 EA

为了使用多个交易策略实例测试 EA 创建,请从第八部分中取出 EA,我们用它来执行负载测试。在 OnInit() 函数中,用本文开发的机制替换 EA 创建机制。为此,从 CSV 文件加载策略参数后,我们将根据它们补充策略数组的初始化字符串。然后我们将使用它来生成策略组和 EA 本身的初始化字符串:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Load strategy parameter sets
   int totalParams = LoadParams(fileName_, strategyParams);

// If nothing is loaded, report an error
   if(totalParams == 0) {
      PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n"
                  "Check that it exists in data folder or in common data folder.",
                  fileName_);
      return(INIT_PARAMETERS_INCORRECT);
   }

// Report an error if
   if(count_ < 1) { // number of instances is less than 1
      return INIT_PARAMETERS_INCORRECT;
   }

   ArrayResize(strategyParams, count_);

// Set parameters in the money management class
   CMoney::DepoPart(expectedDrawdown_ / 10.0);
   CMoney::FixedBalance(fixedBalance_);

// Prepare the initialization string for the array of strategy instances
   string strategiesParams;
   FOREACH(strategyParams, strategiesParams += StringFormat(" class CSimpleVolumesStrategy(%s),\n      ",
                                                            strategyParams[i % totalParams]));

// Prepare the initialization string for an EA with the strategy group
   string expertParams = StringFormat("class CVirtualAdvisor(\n"
                                      "   class CVirtualStrategyGroup(\n"
                                      "      [\n"
                                      "      %s],\n"
                                      "      %.2f\n"
                                      "   ),\n"
                                      "   %d,%s,%d\n"
                                      ")",
                                      strategiesParams, scale_,
                                      magic_, "SimpleVolumes_BenchmarkInstances", useOnlyNewBars_);
   
// Create an EA handling virtual positions
   expert = NEW(expertParams);

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}

与之前的 EA 类似,OnDeinit() 函数能够在删除 EA 对象指针之前检查其有效性。

将获取的代码保存在当前文件夹的 BenchmarkInstancesExpert.mq5 文件中。


检查功能

我们使用第八部分中的 BenchmarkInstancesExpert.mq5 EA 以及当前文章中的相同 EA。使用相同的参数启动它们:来自 Params_SV_EURGBP_H1.csv 文件的256个交易策略实例,2022年作为测试时间段。


图 2.两个 EA 版本的测试结果完全相同


结果表明是完全相同的。因此,它们在图像中显示为同一个实例。这非常好,因为我们现在可以使用最新版本进行进一步开发。


结论

就这样,我们设法提供了使用初始化字符串创建所有必要对象的能力。到目前为止,我们几乎都是手动生成这些字符串,但将来我们将能够从数据库中读取它们。总的来说,这就是我们开始对已经可以运行的代码进行如此修改的原因。

对仅在创建对象的方法上有所不同的 EA 进行测试,即使用相同的交易策略实例集,结果相同,这证明了所做的修改是合理的。 

现在我们可以继续进行第一个计划阶段的自动化 - 按顺序启动几个 EA 优化流程来选择交易策略单个实例的参数。我们将在接下来的文章中做到这一点。

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



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

MQL5 简介(第 7 部分):在 MQL5 中构建 EA 交易和使用 AI 生成代码的初级指南 MQL5 简介(第 7 部分):在 MQL5 中构建 EA 交易和使用 AI 生成代码的初级指南
在我们的综合文章中,了解使用 MQL5 构建 EA 交易的终极初学者指南。逐步学习如何使用伪代码构建 EA,并利用 AI(人工智能)生成代码的强大功能。无论你是算法交易的新手,还是想提高自己的技能,本指南都为你提供了创建有效 EA 的清晰路径。
数据科学和机器学习(第 21 部分):解锁神经网络,优化算法揭秘 数据科学和机器学习(第 21 部分):解锁神经网络,优化算法揭秘
深入神经网络的心脏,我们将揭秘神经网络内部所用的优化算法。在本文中,探索解锁神经网络全部潜力的关键技术,把您的模型准确性和效率推向新的高度。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
开发回放系统(第 48 部分):了解服务的概念 开发回放系统(第 48 部分):了解服务的概念
学习些新知识怎么样?在本文中,您将了解如何将脚本转换为服务,以及为什么这样做很有用。