
Desarrollamos un asesor experto multidivisa (Parte 10): Creación de objetos a partir de una cadena
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 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
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso
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.
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.