English Русский 中文 Deutsch 日本語 Português
preview
Desarrollamos un asesor experto multidivisa (Parte 10): Creación de objetos a partir de una cadena

Desarrollamos un asesor experto multidivisa (Parte 10): Creación de objetos a partir de una cadena

MetaTrader 5Trading |
302 2
Yuriy Bykov
Yuriy Bykov

Introducción

En el artículo anterior esbozamos un plan general para el desarrollo del EA, que incluye varios pasos. Cada paso genera cierta cantidad de información que debe utilizarse en las etapas siguientes. Acto seguido, hemos decidido almacenar esta información en la base de datos y hacer una tabla en ella donde podemos poner los resultados de pasadas individuales del simulador de estrategias para diferentes asesores expertos.

Para poder usar esta información en pasos posteriores, necesitaremos disponer de algún método de creación de los objetos necesarios (estrategias comerciales, sus grupos y expertos) a partir de la información almacenada en la base de datos. No es posible guardar objetos directamente en la base de datos. Lo mejor que podemos proponer es convertir todas las propiedades del objeto en una cadena, almacenarla en la base de datos, luego leer esta cadena de la base de datos y crear el objeto deseado a partir de ella.

El proceso de creación de un objeto a partir de una cadena puede implementarse de muchas maneras diferentes. Por ejemplo, podemos crear un objeto de la clase requerida con parámetros por defecto y, a continuación, utilizar un método o función especial para analizar la cadena leída de la base de datos y asignar los valores correspondientes a las propiedades del objeto. O podemos crear un constructor de objeto adicional que solo acepte una cadena como parámetro de entrada. Esta cadena será analizada dentro del constructor y los valores correspondientes se asignarán a las propiedades del objeto. Para entender cuál es la mejor opción, primero veremos cómo almacenamos la información sobre los objetos en la base de datos.


Almacenamiento de información sobre objetos

Vamos ahora a abrir la tabla de la base de datos que rellenamos en el artículo de la semana pasada y a echar un vistazo a las últimas columnas. Las columnas params y inputs almacenan el resultado de la conversión a una cadena del objeto de estrategia comercial de la clase CSimpleVolumesStrategy y los parámetros de entrada de una pasada de optimización.

Fig. 1. Fragmento de la tabla passes con la información sobre la estrategia utilizada y los parámetros de la prueba


Aunque están relacionados, existen diferencias entre ellos: la columna de inputs contiene los nombres de los parámetros de entrada (aunque no son exactamente los mismos que los nombres de las propiedades del objeto de estrategia), pero faltan algunos parámetros como el símbolo y el periodo. Por lo tanto, nos resultará más cómodo utilizar el formulario de registro de la columna params para crear nuevamente el objeto.

Recordemos de dónde hemos sacado nuestra implementación para convertir un objeto de estrategia en una cadena. En la cuarta parte de la serie de artículos, implementamos el almacenamiento del estado del EA en un archivo, para que pudiera ser restaurado después de un reinicio. Para evitar que el EA use accidentalmente el archivo de datos de otro EA similar, hemos añadido al archivo información sobre los parámetros de todas las instancias de estrategia utilizadas en este EA.

Es decir, el objetivo original era que las instancias de las estrategias comerciales con distintos parámetros generaran cadenas distintas. Por lo tanto, no nos importaba mucho si luego podíamos crear un nuevo objeto de estrategia comercial utilizando dichas cadenas. En la novena parte, tomamos el mecanismo ya existente de conversión a una cadena sin modificaciones adicionales, porque la tarea consistía en depurar el propio proceso de adición de dicha información a la base de datos.


Procedemos a la implementación

Ha llegado el momento de pensar en cómo podemos recrear objetos a partir de esas cadenas. Por lo tanto, imaginemos que tenemos una cadena de algo como esto:

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

Si lo pasamos al constructor de un objeto de la clase CSimpleVolumesStrategy, este debería hacer lo siguiente:

  • eliminar la parte anterior al primer paréntesis de apertura;
  • dividir el resto hasta el paréntesis de cierre por comas;
  • asignar cada parte obtenida a las propiedades correspondientes del objeto, con conversión a números si fuera necesario.

Observando la lista de estas acciones, podemos ver que la primera acción se puede realizar a un nivel superior. De hecho, si primero tomamos el nombre de la clase de esta cadena, podemos entender a partir de ella qué objeto de clase se va a crear. Entonces será más cómodo para el constructor transmitir solo la parte de la cadena que esté dentro de los paréntesis.

Además, la necesidad de crear objetos a partir de una cadena no se limitará a esta única clase. En primer lugar, es posible que no tengamos una única estrategia comercial. En segundo lugar, necesitaremos crear objetos de la clase CVirtualStrategyGroup de forma similar, es decir, grupos de varias instancias de estrategias comerciales con diferentes parámetros. Esto resultará útil en la etapa de fusión de varios grupos previamente seleccionados en un solo grupo. Y en tercer lugar, ¿por qué no implementar la posibilidad de crear un objeto experto propio (clase CVirtualAdvisor)? Esto nos permitirá escribir un asesor experto universal que cargue desde un archivo una descripción textual de todos los grupos de estrategias que se deben utilizar. Cambiando la descripción en este archivo, será posible actualizar completamente la composición de las estrategias incluidas en él sin recompilar el asesor experto.

Si tratamos de imaginar cómo podría ser la cadena de inicialización de los objetos de la clase CVirtualStrategyGroup, obtendremos aproximadamente lo siguiente:

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)

El primer parámetro del constructor de objetos de la clase CVirtualStrategyGroup es un array de objetos de estrategia comercial o un array de objetos de grupo de estrategias comerciales. Por lo tanto, deberemos aprender a analizar una parte de una cadena, que será una array de descripciones uniformes de objetos. Como podemos ver, hemos utilizado la notación estándar utilizada en JSON o Python para representar una lista (array) de elementos: las entradas de los elementos están separadas por comas, y en general se encontrarán dentro de un par de corchetes.

También tendremos que aprender a extraer de la cadena partes no solo entre comas, sino también las que representan una descripción de otro objeto anidado de la clase. Casualmente, para convertir el objeto de estrategias comerciales en una cadena, hemos utilizado la función typename(), que retorna el nombre de la clase del objeto como una cadena precedida por la palabra class. Ahora podremos utilizar esta palabra al analizar una cadena como indicación de que se trata de una cadena que describe un objeto de alguna clase y no un simple valor de tipo número o cadena.

Así es como nos daremos cuenta de la necesidad de implementar el patrón de diseño «fábrica» (factory), donde un objeto especial se encargará de crear objetos de diferentes clases según las solicitudes. Los objetos que Factory podrá generar deberán tener generalmente un antepasado común en la jerarquía de clases. Así que empezaremos por crear una nueva clase genérica de la que eventualmente derivarán todas las clases cuyos objetos puedan crearse a partir de la cadena inicializadora.


Nueva clase básica

Hasta ahora, nuestras clases básicas implicadas en la jerarquía de herencia han sido las siguientes:

  • СAdvisor. Clase para crear expertos, la clase CVirtualAdvisor se heredará de ella.
  • CStrategy. Clase para crear estrategias comerciales, CSimpleVolumesStrategy eventualmente se heredará de ella.
  • CVirtualStrategyGroup. Clase para grupos de estrategias comerciales. No tiene herederos ni se espera que los tenga.
  • ... ¿y ya está?

Sí, ya no resultan visibles las clases básicas con herederos que necesitan la capacidad de inicializar a partir de una cadena. Por lo tanto, estas tres clases necesitarán crear un antepasado común en el que recopilar todos los métodos de ayuda necesarios para posibilitar la inicialización de cadenas.

El nombre para el nuevo antepasado que hemos elegido hasta ahora no es demasiado adecuado. Quería destacar de alguna manera que los descendientes de esta clase podrán ser producidos en la fábrica (Factory), es decir, serán "factoryable". Lo interesante es que el traductor en línea destaca esta palabra como errónea, pero su traducción es la esperada: "apto para la producción en una fábrica". Luego, durante el proceso de escritura, la letra "y" desapareció en algún lugar y solo quedó el nombre CFaсtorable.

Originalmente, esta clase tenía este aspecto:

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

Es decir, se clasificaban los descendientes de esta clase que tuvieran un método Init(), que haría todo el trabajo necesario para convertir la cadena de inicialización de entrada en los valores de las propiedades del objeto, y un operador de tilde, que se encargaría de la conversión inversa de las propiedades a la cadena de inicialización. También se indica que hay un método estático Read() que debería ser capaz de leer algunos datos de la cadena de inicialización. Entenderemos por «datos» una subcadena que contenga una cadena de inicialización válida de otro objeto, una array de otras partes de datos, un número o una constante de cadena.

Aunque era posible llevar esta aplicación a un estado viable, decidimos introducir cambios significativos en ella.

En primer lugar, el método Init() surgía porque queríamos mantener tanto los antiguos constructores del objeto como el nuevo constructor (que acepta la cadena de inicialización). Para evitar duplicar código, lo implementamos una vez en el método Init() y lo llamamos desde varios constructores posibles. Pero al final resultó que no había necesidad de diferentes constructores. Podemos arreglárnoslas bastante bien con un nuevo constructor. Por lo tanto, el código del método Init() se ha trasladado al nuevo constructor, mientras que el propio método ha sido eliminado.

En segundo lugar, la implementación inicial no contenía ningún medio para controlar la corrección de las cadenas de inicialización y los mensajes de error. Sí, esperamos formar cadenas de inicialización automáticamente, lo que excluye casi por completo la aparición de tales errores, pero si de repente nos equivocamos en algo con las cadenas de inicialización que se están formando, estaría bien saberlo a tiempo y poder encontrar un lugar específico del error. Para ello, hemos añadido una nueva propiedad booleana m_isValid, que indicará si el código completo del constructor del objeto se ha ejecutado correctamente, o si algunas partes de la cadena de inicialización contenían errores. Esta propiedad se ha hecho privada, y se han añadido los métodos correspondientes para obtener y establecer su valor: IsValid() y SetInvalid(). E inicialmente esta propiedad es siempre true, mientras que el método SetInvalid() solo puede establecer su valor en false.

En tercer lugar, el método Read(), tras incluir las comprobaciones y el procesamiento de errores, se ha vuelto demasiado engorroso y se ha divido en varios métodos separados especializados en la lectura de distintos tipos de datos desde la cadena de inicialización. También se han añadido varios métodos privados que cumplen una función de servicio para los métodos de lectura de datos. Cabe destacar que los métodos de lectura de datos modifican la cadena de inicialización que se les transmite. Cuando el siguiente dato se lee correctamente, se retorna como resultado del método, mientras que la cadena de inicialización transmitida pierde la parte de lectura.

En cuarto lugar, el método de conversión de un objeto de nuevo a una cadena de inicialización puede hacerse casi idéntico para objetos de clases diferentes si se recuerda la cadena de inicialización original con los parámetros del objeto que estamos creando. Por lo tanto, la propiedad m_params se añadirá a la clase básica para almacenar la cadena de inicialización en el constructor del objeto.

Con estas adiciones, la declaración de la clase CFactorable tendrá este aspecto:

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


No vamos a entrar en la implementación de los métodos de esta clase en el marco de este artículo. Observe únicamente que el funcionamiento de todos los métodos de lectura implica la realización de un conjunto de acciones bastante similar. Primero comprobamos que la cadena de inicialización no esté vacía y que el objeto se encuentre en un estado útil. El objeto puede encontrarse en un estado defectuoso, por ejemplo, como resultado de una operación previa fallida para leer parte de los datos de una cadena de implementación. Por ello, esta comprobación ayudará a evitar la ejecución de acciones innecesarias en un objeto defectuoso conocido.

A continuación, comprobamos determinadas condiciones para garantizar que la cadena de inicialización vaya seguida de datos del tipo correcto (objeto, array, cadena o número). Si es así, buscaremos el lugar donde termina ese dato en la cadena de inicialización. Lo que se encuentra a la izquierda de este espacio se utiliza para obtener el valor de retorno, y lo se haya a la derecha sustituye a la cadena de inicialización.

Si en alguna fase de las comprobaciones obtenemos un resultado negativo, llamaremos al método que permite establecer el estado del objeto actual como defectuoso transmitiéndole información sobre el lugar y la naturaleza del error que se ha producido.

Vamos a guardar el código de esta clase en el archivo Factorable.mqh en la carpeta actual.


Fábrica de objetos

Como las cadenas de inicialización de objetos contienen necesariamente el nombre de la clase, podremos crear una función o método estático de acceso público que actúe como fábrica de objetos. Luego le transmitiremos una cadena de inicialización, y como resultado obtendremos un puntero al objeto creado de la clase dada.

Por supuesto, para los objetos de aquellas clases cuyo nombre puede tomar un único valor en un lugar determinado del programa, la presencia de dicha fábrica no resulta necesaria. Podemos crear un objeto de la forma estándar usando el operador new pasando al constructor una cadena de inicialización con los parámetros del objeto a crear. Pero si tenemos que crear objetos cuyo nombre de clase puede ser diferente (por ejemplo, distintas estrategias comerciales), entonces el nuevo operador no nos ayudará aquí, porque tenemos que entender primero qué clase de objeto se va a crear. Confiaremos este trabajo a la fábrica, o más bien a su único método estático 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;
   }
};

Guardaremos este código en el archivo VirtualFactory.mqh en la carpeta actual.

Para facilitarnos el uso de la fábrica en el futuro, crearemos dos macros útiles. La primera creará el objeto a partir de la cadena de inicialización, sustituyéndola por una llamada al método CVirtualFactory::Create():

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

La segunda solo se ejecutará desde el constructor de algún otro objeto, necesariamente descendiente de la clase CFactorable. Es decir, solo cuando, al crear un objeto (el principal), necesitemos crear otros objetos (anidados) a partir de la cadena de inicialización dentro de su constructor. A esta macro le transmitiremos tres parámetros: el nombre de la clase del objeto a crear (Class), el nombre de la variable donde se escribirá el puntero al objeto creado (Object), y la cadena de inicialización (Params).

La macro declarará primero una variable de puntero con el nombre y la clase dados y la inicializará con el valor NULL. A continuación, se comprobará si el objeto principal se encuentra en estado operativo. En caso afirmativo, llamaremos al método de creación de un objeto en la fábrica mediante la macro NEW(). Intentaremos convertir el puntero creado a la clase requerida. El uso del operador de conversión dinámica de tipos dynamic_cast<>() para este propósito evitará un error de tiempo de ejecución en caso de que la fábrica haya creado un objeto de clase Class distinto del requerido actualmente. En este caso, el puntero Object simplemente permanecerá igual a NULL y el programa proseguirá su ejecución. Y luego comprobaremos si el puntero es correcto. Si está vacío o no es válido, el objeto principal se establecerá en un estado defectuoso, se informará del error y se abortará la ejecución del constructor del objeto principal.

Esta macro tendrá el aspecto siguiente:

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

Añadiremos estas macros al principio del archivo Factorable.mqh.


Modificación de clases básicas anteriores

Ahora añadiremos la clase CFactorable como clase básica a todas las clases básicas anteriores: СAdvisor, СStrategy, СVirtualStrategyGroup. En los dos primeros no será necesario realizar más cambios:

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


    ... 

Pero СVirtualStrategyGroup ha sufrido cambios más significativos. Como ya no se trata de una clase básica abstracta, necesitábamos escribir una implementación del constructor que crease un objeto a partir de la cadena de inicialización. Al hacerlo, nos deshicimos de dos constructores separados que aceptaban una array de estrategias o una array de grupos. También hemos modificado el método de conversión a cadena. En él, ahora simplemente añadiremos el nombre de la clase a la cadena de inicialización almacenada con los parámetros. Y el método de escalado Scale() permanecerá inalterado.

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

Ahora guardaremos los cambios realizados en el archivo VirtualStrategyGroup.mqh en la carpeta actual.

Modificación de la clase de experto

En el último artículo, en la clase de experto CVirtualAdvisor, precisamente añadimos el método Init(), que debía eliminar la duplicación de código para diferentes constructores del experto. Teníamos un constructor que tomaba una única estrategia como primer argumento, y un constructor que tomaba un objeto de grupo de estrategias como primer argumento. Probablemente estaremos de acuerdo en que solo deberá haber un constructor: el que tome un grupo de estrategias. Si tenemos que utilizar una instancia de una estrategia comercial, simplemente crearemos primero un grupo con una de estas estrategias, y el grupo creado se pasará al constructor del asesor experto. Entonces no habrá necesidad tanto del método Init() como de los constructores adicionales. Por lo tanto, dejaremos un único constructor que cree el objeto asesor experto a partir de la cadena de inicialización:

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

En el constructor, primero leeremos todos los datos de la cadena de inicialización. Si se detecta alguna incoherencia en esta fase, el objeto asesor experto actualmente creado pasará al estado defectuoso. Si todo va bien, el constructor creará el grupo de estrategias, añadirá sus estrategias a su array de estrategias y establecerá el resto de propiedades según los datos leídos de la cadena de inicialización.

Pero ahora, debido a la comprobación de la utilidad realizada antes de crear los objetos receptores y de interfaz en el constructor, es posible que estos objetos no se creen. Por eso deberíamos añadir una comprobación de la corrección de los punteros a estos objetos en el destructor antes de borrarlos:

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

Guardaremos los cambios en el archivo VirtualAdvisor.mqh de la carpeta actual.


Modificación de la clase de estrategia comercial

En nuestra clase de estrategia comercial CSimpleVolumesStrategy, también eliminaremos el constructor con parámetros independientes y reescribiremos el código del constructor que acepta la cadena de inicialización usando los métodos de la clase CFactorable.

En el constructor leeremos los parámetros de la cadena de inicialización, recordado previamente su estado inicial en la propiedad m_params. Si durante la lectura no se han producido errores que pongan el objeto estrategia en un estado defectuoso, entonces realizaremos las acciones principales para inicializar el objeto: rellenaremos el array de posiciones virtuales, inicializaremos el indicador y registraremos el manejador de eventos de una nueva barra en el marco temporal de minutos.

También hemos cambiado el método para convertir un objeto en una cadena. En lugar de formarla a partir de parámetros, simplemente concatenaremos el nombre de la clase y la cadena de inicialización recordada, al igual que hicimos en las dos clases anteriores comentadas. 

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   )

También hemos eliminado los métodos Save() y Load() de esta clase, ya que su implementación en la clase padre CVirtualStrategy resultaba suficiente para cumplir las tareas asignadas.

Ahora guardaremos los cambios realizados en el archivo CSimpleVolumesStrategy.mqh en la carpeta actual.


Asesor experto para una única instancia de una estrategia comercial

En el asesor experto, para optimizar los parámetros de una única instancia de estrategia comercial, solo tenemos que cambiar la función de inicialización OnInit(). En ella, deberemos formar la cadena de inicialización del objeto de estrategia comercial a partir de los parámetros de entrada del asesor experto y, a continuación, utilizarla para sustituirla en la cadena de inicialización del objeto de experto.

Gracias a nuestra implementación de los métodos para leer los datos de la cadena de inicialización, somos libres de utilizar espacios extra y caracteres de avance de línea dentro de ella. Entonces, cuando se envíe al registro o se escriba en la base de datos, podremos ver la cadena de inicialización aproximadamente con este formato:

...

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

En la función OnDeinit(), necesitaremos, antes de borrar el objeto de experto, añadir una comprobación de que el puntero al mismo es válido. Ahora ya no podremos garantizar que el objeto de experto se creará siempre, porque teóricamente podríamos tener una cadena de inicialización incorrecta, lo cual provocaría el borrado anticipado del objeto de experto por parte de la fábrica.

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

Guardaremos el código obtenido en el archivo SimpleVolumesExpertSingle.mq5 en la carpeta actual.


Asesor para varias instancias

Para probar la creación de un objeto de experto con muchas instancias de estrategias comerciales, tomaremos el asesor experto de la parte 8, que utilizamos para las pruebas de carga. En él, en la función OnInit(), cambiaremos el mecanismo de creación de un asesor experto por el desarrollado en el marco de este artículo. Para ello, tras cargar los parámetros de las estrategias desde el archivo CSV, completaremos la cadena de inicialización del array de estrategias basándonos en ellos. Luego la utilizaremos para formar la cadena de inicialización del grupo de estrategias y del propio asesor experto:

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

De forma similar al EA anterior, añadiremos a la función OnDeinit() una comprobación de la corrección del puntero al objeto de experto antes de borrarlo.

Guardaremos el código obtenido en el archivo BenchmarkInstancesExpert.mq5 de la carpeta actual.


Comprobación de funcionamiento

Tomaremos el asesor experto BenchmarkInstancesExpert.mq5 de la Parte 8 y el propio asesor experto de este artículo. Lo ejecutaremos con los mismos parámetros: 256 instancias de estrategias comerciales del archivo CSV Params_SV_EURGBP_H1.csv, periodo de prueba - 2022.


Fig. 2. Los resultados de las pruebas de las dos variantes del asesor han resultado completamente idénticos


Los resultados de las pruebas de las dos variantes del asesor han resultado completamente idénticos. Por lo tanto, en la figura se muestran en un solo caso. Esto es algo muy bueno, ya que ahora podremos utilizar la última opción para seguir desarrollándola.


Conclusión

Hoy hemos conseguido implementar la posibilidad de crear todos los objetos necesarios utilizando cadenas de inicialización. Hasta ahora hemos generado estas cadenas casi manualmente, pero en el futuro podremos leerlas desde la base de datos. Por eso, en general, hemos empezado a rehacer el código que ya funcionaba.

Los resultados idénticos en las pruebas de asesores expertos que difieren solo en la forma de creación de objetos, es decir, que trabajan con los mismos conjuntos de instancias de estrategias comerciales, confirman la corrección de los cambios implementados. 

Ahora podremos avanzar y proceder a automatizar la primera etapa planificada: el inicio secuencial de varios procesos de optimización del EA para seleccionar los parámetros de una instancia de estrategia comercial. Pero de esto hablaremos ya en los siguientes artículos.

Gracias por su atención, ¡hasta pronto!

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/14739

Viktor Kudriavtsev
Viktor Kudriavtsev | 16 may 2024 en 14:05

Hola Yuri. Gracias por la interesante serie de artículos.

Yuri, ¿podrías publicar el archivo de estrategia con el que probaste el Asesor Experto del artículo actual? Este es el que tienes la captura de pantalla en la parte inferior del artículo. Si está colgado en algún sitio, por favor dime dónde, no lo he encontrado en otros artículos. ¿Debo ponerlo en la carpeta C:\Users\Admin/AppData/Roaming\MetaQuotes\Terminal\Common\Files o en la carpeta del terminal? Quiero ver si obtengo los mismos resultados en el terminal como en su captura de pantalla.

Yuriy Bykov
Yuriy Bykov | 16 may 2024 en 18:35

Hola Victor.

Este archivo se puede obtener ejecutando la optimización del EA con una instancia de estrategia y tras finalizarla, guardando sus resultados primero en XML y luego guardándolo en CSV desde Excel. Esto se explicó en la parte 6.

Algoritmo de evolución del caparazón de tortuga (Turtle Shell Evolution Algorithm, TSEA) Algoritmo de evolución del caparazón de tortuga (Turtle Shell Evolution Algorithm, TSEA)
Hoy hablaremos sobre un algoritmo de optimización único inspirado en la evolución del caparazón de las tortugas. El algoritmo TSEA emula la formación gradual de los sectores de piel queratinizada que representan soluciones óptimas a un problema. Las mejores soluciones se vuelven más "duras" y se encuentran más cerca de la superficie exterior, mientras que las menos exitosas permanecen "blandas" y se hallan en el interior. El algoritmo utiliza la clusterización de soluciones según su calidad y distancia, lo cual permite conservar las opciones menos acertadas y aporta flexibilidad y adaptabilidad.
Creación de un modelo de restricción de tendencia de velas (Parte 5): Sistema de notificaciones (Parte III) Creación de un modelo de restricción de tendencia de velas (Parte 5): Sistema de notificaciones (Parte III)
Esta parte de la serie de artículos está dedicada a la integración de WhatsApp con MetaTrader 5 para las notificaciones. Hemos incluido un diagrama de flujo para simplificar la comprensión y analizaremos la importancia de las medidas de seguridad en la integración. El objetivo principal de los indicadores es simplificar el análisis mediante la automatización, y deben incluir métodos de notificación para alertar a los usuarios cuando se cumplan determinadas condiciones. Descubra más en este artículo.
Creación de una interfaz gráfica de usuario interactiva en MQL5 (Parte 1): Creación del panel Creación de una interfaz gráfica de usuario interactiva en MQL5 (Parte 1): Creación del panel
Este artículo explora los pasos fundamentales en la elaboración e implementación de un panel de Interfaz Gráfica de Usuario (GUI) utilizando MetaQuotes Language 5 (MQL5). Los paneles de utilidades personalizados mejoran la interacción del usuario en la negociación simplificando las tareas habituales y visualizando la información esencial de la negociación. Al crear paneles personalizados, los operadores pueden agilizar su flujo de trabajo y ahorrar tiempo durante las operaciones.
El criterio de homogeneidad de Smirnov como indicador de la no estacionariedad de las series temporales El criterio de homogeneidad de Smirnov como indicador de la no estacionariedad de las series temporales
El artículo analiza uno de los criterios de homogeneidad no paramétricos más famosos: el criterio de Smirnov. Asimismo, se consideran tanto datos modelo como cotizaciones reales, y se ofrece un ejemplo de construcción de un indicador de no estacionariedad (iSmirnovDistance).