Русский
preview
Developing a multi-currency Expert Advisor (Part 10): Creating objects from a string

Developing a multi-currency Expert Advisor (Part 10): Creating objects from a string

MetaTrader 5Trading | 13 September 2024, 18:38
358 2
Yuriy Bykov
Yuriy Bykov

Introduction

In the previous article, I have outlined a general plan for developing the EA, which includes several stages. Each stage generates a certain amount of information to be used in the stages that follow. I decided to save this information in a database and created a table in it, in which we can place the results of single passes of the strategy tester for various EAs.

In order to be able to use this information in the next steps, we need to have some way of creating the necessary objects (trading strategies, their groups and EAs) from the information stored in the database. There is no option to save objects directly to the database. The best thing that can be suggested is to convert all the properties of objects into a string, save it in the database, then read this string from the database and create the required object from it.

Creating an object from a string can be implemented in different ways. For example, we can create an object of the desired class with default parameters, and then use a special method or function to parse the string read from the database and assign the corresponding values to the object properties. Alternatively, we can create an additional object constructor that will accept only one string as an input. This string will be parsed into parts inside the constructor and the corresponding values will be assigned to the object properties there. To understand which option is better, let's first look at how we store information about objects in the database.


Storing information about objects

Let's open the table in the database that we filled in the previous article and look at the last columns. The params and inputs columns store the result of converting the CSimpleVolumesStrategy class trading strategy object into a string and the inputs of a single optimization pass.

Fig. 1. The fragment of the passes table with information about the applied strategy and test parameters


Although they are related, there are differences between them: the inputs column features the names of inputs (although they do not exactly match the names of the strategy object properties), but some parameters, such as the symbol and period, are missing. Therefore, it will be more convenient for us to use the entry form from the params column to recreate the object.

Let's recall where we got the implementation of converting a strategy object into a string. In the fourth part of the article series, I implemented saving the EA state to a file so that it can be restored after a restart. To prevent the EA from accidentally using a file featuring data from another similar EA, I implemented saving data about the parameters of all instances of strategies used in this EA to the file.

In other words, the original task was to ensure that instances of trading strategies with different parameters generated different strings. Therefore, I was not particularly concerned about the possibility to create a new trading strategy object based on such strings. In the ninth part, I took the existing string conversion mechanism without any additional modification, since my objective was to debug the process of adding such information to the database.


Moving on to implementation

Now is the time to think about how we can recreate objects using such strings. So, we have a string that looks something like this:

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

If we pass it to the CSimpleVolumesStrategy class object constructor, it should do the following:

  • remove the part that comes before the first opening parenthesis;
  • split the remaining part up to the closing parenthesis by comma symbols;
  • assign each obtained part to the corresponding object properties converting them into numbers if necessary.

Looking at the list of these actions, we can see that the first action can be performed at a higher level. Indeed, if we first take the class name from this line, then we can define the class of the created object. Then it is more convenient for the constructor to pass only that part of the string that is inside the parentheses.

Moreover, the needs for creating objects from a string are not limited to this single class. First, we may not have just one trading strategy. Secondly, we will need to create CVirtualStrategyGroup class objects, that is, groups of several instances of trading strategies with different parameters. This will be useful for the stage of combining several previously selected groups into one group. Third, what prevents us from providing the ability to create an EA object (the CVirtualAdvisor class) from a string? This will allow us to write a universal EA that can load from a file a text description of all groups of strategies that should be used. By changing the description in the file, it will be possible to completely update the composition of the strategies included in it without recompiling the EA.

If we try to imagine what the initialization string of CVirtualStrategyGroup class objects might look like, then we get something like this:

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)

The first parameter of the CVirtualStrategyGroup class object constructor is an array of trading strategy objects or an array of trading strategy group objects. Therefore, it is necessary to learn how to parse a part of a string, which will be an array of similar object descriptions. As you can see, I have used the standard notation applied in JSON or Python to represent a list (array) of elements: the element entries are separated by commas and located inside a pair of square brackets.

We will also have to learn how to extract parts from a string not only between commas, but also those that represent a description of another nested class object. By chance, to convert the trading strategy object to a string, we used the typename() function, which returns the class name of an object as a string preceded by the class words. We can now use this word when parsing a string as a sign that what follows is a string describing an object of a certain class, and not a simple value such as a number or string.

Thus, we come to an understanding of the need to implement the Factory design pattern, when a special object will be engaged in the creation of objects of various classes upon request. The objects that a factory can produce should typically have a common ancestor in the class hierarchy. So let's start by creating a new common class all classes (whose objects can be created from the initialization string) will eventually be derived from.


New base class

So far, our base classes participating in the inheritance hierarchy have been:

  • СAdvisor. The class for creating EAs the CVirtualAdvisor class is derived from.
  • CStrategy. The class for creating trading strategies. CSimpleVolumesStrategy is derived from it
  • CVirtualStrategyGroup. The class for groups of trading strategies. It has no descendants and none are expected.
  • So, is that all?

Yes, I don't see any more base classes having descendants that need the ability to be initialized with a string. This means that these three classes need to have a common ancestor, in which to collect all the necessary auxiliary methods to ensure initialization with a string.

The name I have chosen for the new ancestor is not very meaningful yet. I would like to somehow emphasize that the class descendants will be able to be produced in the Factory, so they will be "factoryable". Further on, while developing the code, the letter "y" disappeared somewhere, and only the name CFaсtorable remained.

Initially, the class looked something like this:

//+------------------------------------------------------------------+
//| 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);
};

So, the descendants of this class were required to have the Init() method, which will do all the work necessary to convert the input initialization string into object property values, and the tilde operator, which deals with the reverse conversion of properties into an initialization string. The existence of the Read() static method is also stated. It should be able to read some of the data from the initialization string. By a data part, we mean a substring that contains either a valid initialization string of another object, or an array of other data parts, or a number, or a string constant.

Although this implementation was brought to a working state, I decided to make significant changes to it.

First, the Init() method appeared because I wanted to keep both the old object constructors and the new constructor (which accepts the initialization string). To avoid duplicating code, I implemented it once in the Init() method and called it from several possible constructors. But in the end it turned out that there was no need for different constructors. We can get by with just one new constructor. Therefore the Init() method code moved to the new constructor, while the method itself was removed.

Second, the initial implementation did not contain any means of checking the validity of initialization strings and error reports. We expect to generate initialization strings automatically, which almost completely eliminates the occurrence of such errors, but if suddenly we mess up something with the generated initialization strings, it would be nice to know about it in a timely manner and be able to find the error. For these purposes, I have added a new m_isValid logical property, which indicates whether all of the object constructor code executed successfully, or whether some parts of the initialization string contained errors. The property is made private, while the appropriate IsValid() and SetInvalid() methods are added to get and set its value. Moreover, the property is always true initially, while the SetInvalid() method can only set its value to false.

Third, the Read() method became too cumbersome because of the implemented checks and error handling. So, it was split into several separate methods specializing in reading different types of data from the initialization string. Several auxiliary private methods have also been added for data reading methods. It is worth noting separately that the data reading methods modify the initialization string that is passed to them. When the next part of the data is successfully read, it is returned as the result of the method, and the passed initialization string loses the part it read.

Fourth, the method of converting an object back to an initialization string can be made almost identical for objects of different classes if the original initialization string is remembered with the parameters of the created object. Therefore, the m_params property was added to the base class to store the initialization string in the object constructor.

Considering the additions made, declaring the CFactorable class looks like this:

//+------------------------------------------------------------------+
//| 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);
};


I will not dwell on the implementation of the class methods here. However, I would like to note that the work of all reading methods involves performing a roughly similar set of actions. First, we check that the initialization string is not empty and the object is valid. The object could have entered an invalid state, for example, as a result of a previous unsuccessful operation to read part of the data from the implementation string. Therefore, such a check helps to avoid performing unnecessary actions on an obviously faulty object.

Then certain conditions are checked to ensure that the initialization string contains data of the correct type (object, array, string or number). If so, then we find the location where that piece of data ends in the initialization string. Everything located to the left of this place is used to get the return value, and everything to the right replaces the initialization string.

If at some stage of the checks we receive a negative result, then call the method of setting the current object to the invalid state, while passing information about the error location and nature to it.

Save the code of the class in the Factorable.mqh file in the current folder.


Object factory

Since the object initialization strings always include the class name, we can make a public function or static method that will act as an object "factory". We will pass an initialization string to it receiving a pointer to the created object of the given class.

Of course, for objects of the classes whose name in a given place in the program can take on a single value, the presence of such a factory is not necessary. We can create an object in the standard way using the new operator by passing the initialization string with the parameters of the created object to the constructor. But if we have to create objects whose class name can be different (for example, different trading strategies), then the new operator is unable to help us, since we first need to define the class of the object we are about to create. Let's entrust this work to the factory, or rather, to its only static method - 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;
   }
};

Save this code in the VirtualFactory.mqh file of the current folder.

Create two useful macros to make it easier for us to use the factory in the future. The first one will create an object from the initialization string replacing itself with calling the CVirtualFactory::Create() method:

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

The second macro will only be run from the constructor of some other object, which should be the CFactorable class descendant. In other words, this will happen only if we create the main object, while implementing other (nested) objects from the initialization string inside its constructor. The macro is to receive three parameters: created object class name (Class), name of the variable receiving the pointer to the created object (Object) and initialization string (Params).

At the beginning, the macro will declare a pointer variable with the given name and class and initialize it with the NULL value. Then we check whether the main object is valid. If yes, then call the object creation method in the factory via the NEW() macro. Then try to cast the created pointer to the required class. Using the dynamic_cast<>() operator for this purpose avoids a runtime error if the factory creates an object of a different Class than the one currently required. In this case, the Object pointer will simply remain equal to NULL, and the program will continue running. Then we check the pointer validity. If it is empty or invalid, set the main object to the invalid state, report an error and abort the main object constructor execution.

This is what the macro looks like:

// 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;                                                                                           \
       }                                                                                                    \
    }                                                                                                       \

Add these macros to the beginning of the Factorable.mqh file.


Modification of the previous base classes

Add the CFactorable class as a base one to all the previous base classes: СAdvisor, СStrategy and СVirtualStrategyGroup. The first two will not require any further changes:

//+------------------------------------------------------------------+
//| 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 has undergone more serious changes. Since this is no longer an abstract base class, we needed to write an implementation of the constructor in it that creates an object from the initialization string. In doing so, we got rid of two separate constructors that took either an array of strategies or an array of groups. Also, the method of converting to a string has now changed. In the method, we now simply add the class name to the saved initialization string with parameters. The Scale() scaling method has remained unchanged.

//+------------------------------------------------------------------+
//| 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);
}


    ... 

Save the changes made to the VirtualStrategyGroup.mqh file in the current folder.

Modification of the EA class

In the previous article, the CVirtualAdvisor EA class received the Init() method, which was supposed to remove code duplication for different EA constructors. We had a constructor that took a single strategy as its first argument, and a constructor that took a strategy group object as its first argument. It probably will not be difficult for us to agree that there will be only one constructor - the one which accepts a group of strategies. If we need to use one instance of a trading strategy, we first simply create a group with this one strategy and pass the created group to the EA constructor. Then there is no need for the Init() method and additional constructors. Therefore, I will leave one constructor that creates an EA object from the initialization string:

//+------------------------------------------------------------------+
//| 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;
   }
}

In the constructor, we first read all the data from the initialization string. If any discrepancy is detected at this stage, the currently created EA object will go into an invalid state. If all is well, the constructor will create a strategy group, add its strategies to its strategy array, and set the remaining properties based on the data read from the initialization string.

But now, due to the validity check before creating the receiver and interface objects in the constructor, these objects may not be created. Therefore, in the destructor, we need to add a check for the correctness of pointers to these objects before deleting them:

//+------------------------------------------------------------------+
//| 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 
}

Save the changes in the VirtualAdvisor.mqh file of the current folder.

Modification of the trading strategy class

In the CSimpleVolumesStrategy strategy class, remove the constructor with separate parameters and rewrite the code of the constructor that accepts the initialization string using the CFactorable class methods.

In the constructor, read the parameters from the initialization string having previously saved its initial state in the m_params property. If no errors occurred during reading that would cause the strategy object to become invalid, perform the basic actions to initialize the object: fill the array of virtual positions, initialize the indicator and register the event handler for a new bar on the minute timeframe.

The method of converting an object to a string has also changed. Instead of forming it from the parameters, we will simply concatenate the class name and the saved initialization string, as we did in the two previous considered classes. 

//+------------------------------------------------------------------+
//| 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);
}

We also removed the Save() and Load() methods from the class since their implementation in the CVirtualStrategy parent class proved to be quite sufficient to carry out the tasks assigned to it.

Save the changes in the CSimpleVolumesStrategy.mqh file of the current folder.


The EA for a single instance of the trading strategy

To optimize the parameters of a single trading strategy instance, we need to change only the OnInit() initialization function. In this function, we should form a string for initializing the trading strategy object from the EA inputs and then use it to substitute the EA object into the initialization string.

Thanks to our implementation of methods for reading data from the initialization string, we are free to use additional spaces and line feeds inside it. Then when outputting to the log or making an entry in the database, we can see the initialization string formatted approximately like this:

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   )

In the OnDeinit() function, we need to make sure the pointer to the EA object is correct before removing it. Now we can no longer guarantee that the EA object will always be created, since theoretically we could have an incorrect initialization string, which would lead to the early deletion of the EA object by the factory.

...

//+------------------------------------------------------------------+
//| 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;
}

Save the obtained code in the file SimpleVolumesExpertSingle.mq5 of the current folder.


EA for multiple instances

To test the EA creation with multiple trading strategy instances, take the EA from the eighth part, which we used to perform load testing. In the OnInit() function, replace the EA creation mechanism with the one developed in this article. To do this, after loading the strategy parameters from the CSV file, we will supplement the initialization string of the strategy array based on them. Then we will use it to form the initialization string for the strategy group and the EA itself:

//+------------------------------------------------------------------+
//| 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);
}

Similar to the previous EA, the OnDeinit() function receives the ability to check the validity of the pointer to the EA object before deleting it.

Save the obtained code in the BenchmarkInstancesExpert.mq5 file of the current folder.


Checking functionality

Let's use the BenchmarkInstancesExpert.mq5 EA from the eighth part and the same EA from the current article. Launch them with the same parameters: 256 instances of trading strategies from the Params_SV_EURGBP_H1.csv file, the year of 2022 serves as a test period.


Fig. 2. The test results of the two EA versions are identical


The results have turned out to be identical. Therefore, they are displayed as one instance in the image. This is very good, as we can now use the latest version for its further development.


Conclusion

So, we have managed to provide the ability to create all the necessary objects using initialization strings. So far we have been generating these lines almost manually, but in the future we will be able to read them from the database. This is, in general, why we started such a revision of the already working code.

Identical results of testing EAs that differ only in the method of creating objects, i.e. working with the same sets of trading strategy instances, justify the changes made. 

Now we can move on and proceed to the automation of the first planned stage - the sequential launch of several processes of EA optimization to select the parameters of a single instance of the trading strategy. We will do this in the coming articles.

Thank you for your attention! See you soon!



Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/14739

Last comments | Go to discussion (2)
Viktor Kudriavtsev
Viktor Kudriavtsev | 16 May 2024 at 14:05

Yuri hello. Thank you for the interesting series of articles.

Yuri, could you post the strategy file with which you tested the Expert Advisor from the current article? This is the one you got the screenshot at the bottom of the article. If it is posted somewhere, please tell me where, I have not found it under other articles. Should I put it in the folder C:\Users\Admin/AppData\Roaming\MetaQuotes\Terminal\Common\Files or in the terminal folder? I want to see if I get the same results in the terminal as in your screenshot.

Yuriy Bykov
Yuriy Bykov | 16 May 2024 at 18:35

Hello Victor.

This file can be obtained by running the EA optimisation with one strategy instance and after finishing it, saving its results first in XML and then saving it to CSV from Excel. This was explained in Part 6.

Creating a Trading Administrator Panel in MQL5 (Part III): Enhancing the GUI with Visual Styling (I) Creating a Trading Administrator Panel in MQL5 (Part III): Enhancing the GUI with Visual Styling (I)
In this article, we will focus on visually styling the graphical user interface (GUI) of our Trading Administrator Panel using MQL5. We’ll explore various techniques and features available in MQL5 that allow for customization and optimization of the interface, ensuring it meets the needs of traders while maintaining an attractive aesthetic.
How to Implement Auto Optimization in MQL5 Expert Advisors How to Implement Auto Optimization in MQL5 Expert Advisors
Step by step guide for auto optimization in MQL5 for Expert Advisors. We will cover robust optimization logic, best practices for parameter selection, and how to reconstruct strategies with back-testing. Additionally, higher-level methods like walk-forward optimization will be discussed to enhance your trading approach.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Creating an MQL5-Telegram Integrated Expert Advisor (Part 6): Adding Responsive Inline Buttons Creating an MQL5-Telegram Integrated Expert Advisor (Part 6): Adding Responsive Inline Buttons
In this article, we integrate interactive inline buttons into an MQL5 Expert Advisor, allowing real-time control via Telegram. Each button press triggers specific actions and sends responses back to the user. We also modularize functions for handling Telegram messages and callback queries efficiently.