Desarrollamos un asesor experto multidivisas (Parte 24): Añadimos una nueva estrategia (I)
Introducción
En el artículo anterior, continuamos desarrollando un sistema para optimizar automáticamente las estrategias de trading en MetaTrader 5. El núcleo del sistema es una base de datos de optimización que contiene información sobre proyectos de optimización. Para crear proyectos, se escribió un script de creación de proyectos. A pesar de que el script se escribió para crear un proyecto destinado a optimizar una estrategia comercial específica (SimpleVolumes), puede utilizarse como plantilla adaptable a otras estrategias comerciales.
Creamos la capacidad de exportar automáticamente grupos seleccionados de estrategias comerciales en la etapa final del proyecto. La exportación se realizó a una base de datos independiente, denominada base de datos. El EA final puede utilizarlo para actualizar la configuración de los sistemas de negociación sin necesidad de recompilar. Esto nos permite simular el trabajo del EA en el probador durante un intervalo de tiempo, en el que pueden aparecer varias veces nuevos resultados de optimización del proyecto.
Finalmente, también hemos adoptado una estructura significativa para organizar los archivos del proyecto, dividiendo todos los archivos en dos partes. La primera parte, denominada librería Advisor, se trasladó a la carpeta MQL5/Include, mientras que el resto permaneció en la carpeta de trabajo dentro de MQL5/Experts. Hemos trasladado todos los archivos que admiten el sistema de optimización automática y que son independientes de los tipos de estrategias comerciales que se están optimizando a la sección de librerías.. La carpeta de trabajo del proyecto contiene los EA de etapa, un EA final y un script para crear un proyecto de optimización.
Sin embargo, dejé la estrategia de trading del modelo SimpleVolumes en la sección de la librería, ya que en ese momento era más importante para nosotros probar cómo funcionaría el mecanismo de actualización automática de los parámetros de la estrategia en el EA final. En realidad, no importaba dónde se conectara exactamente el archivo con el código fuente de la estrategia comercial durante la compilación.
Ahora imaginemos que queremos adoptar una nueva estrategia comercial y conectarla a un sistema de optimización automática, creando un EA provisional y un EA definitivo para ella. ¿Qué necesitamos para esto?
Trazando el camino
En primer lugar, tomemos una estrategia sencilla y apliquémosla en código para utilizarla con nuestra librería Advisor. Colocamos su código en la carpeta de trabajo del proyecto. Una vez creada la estrategia, se puede crear un Asesor Experto de primera fase, que se utilizará para optimizar los parámetros de instancias individuales de esta estrategia de trading. Aquí encontraremos algunas dificultades relacionadas con la necesidad de separar los códigos de la librería y del proyecto.
Podemos utilizar prácticamente los mismos EA para la segunda y tercera etapas que se escribieron en la parte anterior, ya que el la librería no contiene ninguna mención a las clases de estrategias de trading utilizadas. Y tendrás que añadir un comando para incluir el nuevo archivo de estrategia en el código de la carpeta de trabajo del proyecto.
Para la nueva estrategia, tendremos que realizar algunos cambios en el script de creación de proyectos en la base de datos de optimización. Como mínimo, los cambios afectarán a la plantilla de parámetros de entrada para la primera etapa, ya que la composición de los parámetros de entrada en la nueva estrategia comercial diferirá de la de la estrategia anterior.
Después de modificar la EA de creación de proyectos en la base de datos de optimización, podremos ejecutarla. Se creará la base de datos de optimización y se añadirán a ella las tareas de optimización necesarias para este proyecto. A continuación, podemos ejecutar el transportador de optimización automática y esperar a que termine de funcionar. Es un proceso bastante largo. Su duración depende del intervalo de tiempo de optimización seleccionado (cuanto más largo sea, más tiempo tardará), de la complejidad de la propia estrategia de trading (cuanto más compleja sea, más tiempo tardará) y, por supuesto, del número de agentes de prueba disponibles para la optimización (cuantos más haya, más rápido será).
El último paso es ejecutar el EA final o probarlo en el probador de estrategias para evaluar los resultados de la optimización.
¡Empecemos!
Estrategia SimpleCandles
Cree una nueva carpeta para el proyecto en la carpeta MQL5/Experts. Llamémoslo, por ejemplo, Article.17277. Probablemente valga la pena hacer una aclaración desde el principio para evitar confusiones en el futuro. Utilizaré el término «proyecto» en dos sentidos. En un caso, simplemente significará una carpeta con archivos que se utilizarán para optimizar automáticamente una determinada estrategia comercial. El código para estos EAs utilizará archivos de inclusión de la librería Advisor. Por lo tanto, en este contexto, un proyecto es simplemente una carpeta de trabajo dentro de la carpeta «Experts» del terminal. En otro caso, la palabra «proyecto» significará una estructura de datos creada en la base de datos de optimización, que describe las tareas de optimización que deben realizarse automáticamente para obtener resultados que luego se utilizan en el EA final destinado a funcionar en una cuenta de trading. En este contexto, un proyecto consiste esencialmente en llenar una base de datos de optimización, antes de que comience la optimización propiamente dicha.
Ahora estamos hablando de un proyecto en el primer sentido. Por lo tanto, creemos una subcarpeta llamada Strategies en la carpeta de trabajo del proyecto. Colocaremos en él archivos de diversas estrategias comerciales. Por ahora, solo crearemos una nueva estrategia allí.
Repitamos el camino seguido en la parte 1 al desarrollar la estrategia de trading SimpleVolumes. Comencemos también con la formulación de la idea de negociación.
Supongamos que cuando se producen varias velas consecutivas en la misma dirección para un determinado símbolo, la probabilidad de que la siguiente vela tenga una dirección diferente aumenta ligeramente. Entonces, si abrimos una posición en la dirección opuesta después de tales velas, podríamos obtener ganancias.
Intentemos convertir esta idea en una estrategia. Para ello, necesitamos formular un conjunto de reglas para abrir y cerrar posiciones que no contenga ningún parámetro desconocido. Este conjunto de reglas debería permitirnos determinar, en cualquier momento en que la estrategia esté en funcionamiento, si se deben abrir posiciones y, en caso afirmativo, cuáles.
En primer lugar, especifiquemos el concepto de dirección de la vela. Consideraremos que una vela es alcista si el precio de cierre de la vela es superior al precio de apertura. Una vela cuyo precio de cierre sea inferior al precio de apertura se denominará bajista. Dado que queremos evaluar la dirección de varias velas consecutivas pasadas, aplicaremos el concepto de dirección de la vela solo a las velas ya cerradas. De esto podemos concluir que el momento de una posible apertura de una posición llegará con la llegada de una nueva barra, es decir, la aparición de una nueva vela.
Entonces, hemos decidido el momento de abrir posiciones, pero ¿qué pasa con cerrarlas? Utilizaremos la opción más sencilla: al abrir una posición, se establecerán los niveles StopLoss y TakeProfit, en los que se cerrará la posición.
Ahora podemos describir nuestra estrategia de la siguiente manera:
Una señal para abrir una posición será una situación en la que, al inicio de una nueva barra (vela), todas las velas anteriores estén orientadas en la misma dirección (al alza o a la baja). Si las velas apuntan hacia arriba, entonces abrimos una posición de VENTA. De lo contrario, abrimos una posición de COMPRA.
Cada posición tiene niveles de StopLoss y TakeProfit y solo se cerrará cuando se alcancen dichos niveles. Si ya hay una posición abierta y se recibe de nuevo una señal para abrir una posición, se pueden abrir posiciones adicionales si su número no es demasiado grande.
Esta es una descripción más detallada, pero aún incompleta. Por lo tanto, lo leemos de nuevo y resaltamos todos los lugares donde algo no está claro. Se requieren explicaciones más detalladas al respecto.
Estas son las preguntas que surgieron:
- «... de las varias velas anteriores ...» — ¿Cuántas son «varias»?
- «... se pueden abrir posiciones adicionales...» — ¿Cuántas posiciones se pueden abrir en total?
- «... tiene niveles StopLoss y TakeProfit ...» — ¿Cómo se utilizan esos valores? ¿Cómo se calculan?
¿Cuántas son «varias» velas? Esta es la pregunta más fácil. Esta cantidad será simplemente uno de los parámetros estratégicos que se pueden modificar para encontrar el valor óptimo. Solo puede ser un número entero y no muy grande, probablemente no más de 10, ya que, a juzgar por los gráficos, las secuencias largas de velas unidireccionales son poco frecuentes.
¿Cuántas posiciones pueden estar abiertas en total? Esto también puede convertirse en un parámetro estratégico y los mejores valores pueden seleccionarse durante la optimización.
¿Cómo utilizar los valores para StopLoss y TakeProfit? ¿Cómo calcularlos? Esta es una pregunta un poco más compleja, pero en el caso más sencillo podemos responderla de la misma manera que las anteriores: StopLoss y TakeProfit en puntos se convertirán en parámetros de la estrategia. Al abrir una posición, nos alejaremos del precio de apertura por el número de puntos especificado en estos parámetros en las direcciones deseadas. Sin embargo, también se puede utilizar un enfoque ligeramente más complejo. Podríamos establecer estos parámetros no en puntos, sino como un porcentaje de algún valor promedio de la volatilidad del precio del instrumento de negociación (símbolo) expresado en puntos. Esto plantea la siguiente pregunta.
¿Cómo se calcula este valor de volatilidad? Hay varias formas de hacerlo. Por ejemplo, puede utilizar el indicador de volatilidad ATR (Average True Range) ya preparado o idear e implementar su propio método para calcular la volatilidad. Pero lo más probable es que uno de los parámetros en dichos cálculos sea el número de períodos durante los cuales se considera el rango de fluctuaciones de precios de un instrumento de negociación y el tamaño de un período. Si añadimos estos valores a los parámetros de la estrategia, podemos utilizarlos para calcular la volatilidad.
Dado que no imponemos restricciones sobre el hecho de que, tras abrir la primera posición, las siguientes deban abrirse en la misma dirección, pueden darse situaciones en las que la estrategia de trading mantenga posiciones abiertas en diferentes direcciones. En una implementación normal, nos veríamos obligados a limitar el ámbito de aplicación de dicha estrategia para trabajar únicamente con cuentas con contabilidad de posiciones independientes («cobertura»). Pero con el uso de posiciones virtuales, no existe tal limitación.
Ahora que todo está claro, enumeremos todos los valores que ya hemos mencionado como parámetros estratégicos. Debemos tener en cuenta que, para recibir una señal para abrir posiciones, debemos seleccionar qué símbolo y qué marco temporal utilizaremos para seguir las velas. Entonces obtenemos la siguiente descripción:
El EA se inicia en un símbolo y un período específicos (marco temporal).
Configure la entrada:
- Símbolo
- Marco temporal para el recuento de velas unidireccionales
- Número de velas en la misma dirección (signalSeqLen)
- Período ATR (periodATR)
- Stop Loss (en puntos o % ATR) (stopLevel)
- Take Profit (en puntos o % ATR) (takeLevel)
- Número máximo de posiciones abiertas simultáneamente (maxCountOfOrders)
- Tamaño de la posición
Cuando llega una nueva barra, comprobamos las direcciones de las últimas velas cerradas signalSeqLen.
Si las instrucciones son las mismas y el número de posiciones abiertas es inferior a maxCountOfOrders, entonces:
- Calcular StopLoss y TakeProfit. Si periodATR = 0, simplemente incrementamos el precio actual por el número de puntos tomado de los parámetros stopLevel y takeLevel. Si periodATR > 0, calculamos el valor ATR utilizando el parámetro periodATR para el marco temporal diario. Nos retiramos del precio actual por los valores ATR * stopLevel y ATR * takeLevel.
- Abrimos una posición de VENTA si la dirección de la vela era alcista y una posición de COMPRA si la dirección de la vela era bajista. Al abrir la posición, establezca los niveles de StopLoss y TakeProfit calculados previamente.
Esta descripción es ya suficiente para comenzar la implementación. Resolveremos cualquier problema que surja en el camino.
También me gustaría llamar la atención sobre el hecho de que, al describir la estrategia, no mencionamos el tamaño de las posiciones abiertas. Aunque formalmente hemos añadido dicho parámetro a la lista de parámetros, dada la utilización de la estrategia desarrollada en el sistema de optimización automática, podemos utilizar simplemente el lote mínimo para las pruebas. Durante la optimización automática, se seleccionarán los valores adecuados del multiplicador del tamaño de la posición que garanticen una reducción específica del 10 % durante todo el intervalo de prueba. Por lo tanto, no tendremos que establecer manualmente el tamaño de las posiciones en ningún sitio.
Implementación de la estrategia
Usemos la clase existente CSimpleVolumesStrategy y creemos la clase CSimpleCandlesStrategy basada en ella. Debe declararse como descendiente de la clase CVirtualStrategy. Enumeremos los parámetros de estrategia necesarios como campos de clase, teniendo en cuenta que nuestra nueva clase hereda algunos campos y métodos más de sus antecesores.
//+------------------------------------------------------------------+ //| Trading strategy using unidirectional candlesticks | //+------------------------------------------------------------------+ class CSimpleCandlesStrategy : public CVirtualStrategy { protected: string m_symbol; // Symbol (trading instrument) ENUM_TIMEFRAMES m_timeframe; // Chart period (timeframe) //--- Open signal parameters int m_signalSeqLen; // Number of unidirectional candles int m_periodATR; // ATR period //--- Position parameters double m_stopLevel; // Stop Loss (in points or % ATR) double m_takeLevel; // Take Profit (in points or % ATR) //--- Money management parameters int m_maxCountOfOrders; // Max number of simultaneously open positions CSymbolInfo *m_symbolInfo; // Object for getting information about the symbol properties // ... public: // Constructor CSimpleCandlesStrategy(string p_params); virtual string operator~() override; // Convert object to string virtual void Tick() override; // OnTick event handler };
Para obtener de forma centralizada información sobre las propiedades de un instrumento de negociación (símbolo), incluiremos el puntero al objeto de clase CSymbolInfo en los campos de clase.
La clase de nuestra nueva estrategia comercial es descendiente de la clase CFactorable. De esta forma, podemos implementar un constructor en la nueva clase que lea los valores de los parámetros de la cadena de inicialización utilizando los métodos de lectura implementados en la clase CFactorable. Si no se han producido errores durante la lectura, el método IsValid() devuelve «true».
Para trabajar con posiciones virtuales, en el antecesor CVirtualStrategy, se declara la matriz m_orders con el fin de almacenar punteros a los objetos de la clase CVirtualOrder, es decir, posiciones virtuales. Por lo tanto, en el constructor pediremos que se creen tantas instancias de objetos de posición virtual como se especifique en el parámetro m_maxCountOfOrders y las colocaremos en la matriz m_orders. El método estático CVirtualReceiver::Get() realizará esta tarea.
Dado que nuestra estrategia solo abrirá posiciones cuando se abra una nueva barra en un marco temporal determinado, crea un objeto para comprobar la aparición de una nueva barra para un símbolo y un marco temporal determinados.
Y lo último que debemos hacer en el constructor es pedirle al monitor de símbolos que cree un objeto de información para nuestra clase CSymbolInfo.
El código completo del constructor tendrá el siguiente aspecto:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CSimpleCandlesStrategy::CSimpleCandlesStrategy(string p_params) { // Read parameters from the initialization string m_params = p_params; m_symbol = ReadString(p_params); m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params); m_signalSeqLen = (int) ReadLong(p_params); m_periodATR = (int) ReadLong(p_params); m_stopLevel = ReadDouble(p_params); m_takeLevel = ReadDouble(p_params); m_maxCountOfOrders = (int) ReadLong(p_params); if(IsValid()) { // Request the required number of objects for virtual positions CVirtualReceiver::Get(&this, m_orders, m_maxCountOfOrders); // Add tracking a new bar on the required timeframe IsNewBar(m_symbol, m_timeframe); // Create an information object for the desired symbol m_symbolInfo = CSymbolsMonitor::Instance()[m_symbol]; } }
A continuación, debemos implementar el operador virtual abstracto tilde (~), que devuelve la cadena de inicialización del objeto de estrategia. Su implementación es estándar:
//+------------------------------------------------------------------+ //| Convert an object to a string | //+------------------------------------------------------------------+ string CSimpleCandlesStrategy::operator~() { return StringFormat("%s(%s)", typename(this), m_params); }
Otro método virtual obligatorio que debe implementarse es el método de gestión de ticks Tick(). En el método, comprobamos si se inicia una nueva barra y si el número de posiciones abiertas aún no ha alcanzado el valor máximo. Si se cumplen estas condiciones, comprobamos la presencia de una señal de apertura. Si hay una señal, entonces abrimos una posición en la dirección correspondiente. Los métodos restantes que añadimos a la clase desempeñan un papel secundario.
//+------------------------------------------------------------------+ //| "Tick" event handler function | //+------------------------------------------------------------------+ void CSimpleCandlesStrategy::Tick() override { // If a new bar has arrived for a given symbol and timeframe if(IsNewBar(m_symbol, m_timeframe)) { // If the number of open positions is less than the allowed number if(m_ordersTotal < m_maxCountOfOrders) { // Get an open signal int signal = SignalForOpen(); if(signal == 1) { // If there is a buy signal, then OpenBuy(); // open a BUY position } else if(signal == -1) { // If there is a sell signal, then OpenSell(); // open a SELL_STOP position } } } }
Hemos trasladado la comprobación de la presencia de una señal de apertura al método independiente SignalForOpen(). En este método, recibimos una serie de cotizaciones de velas anteriores y comprobamos una por una si todas ellas apuntan hacia abajo o hacia arriba:
//+------------------------------------------------------------------+ //| Signal for opening pending orders | //+------------------------------------------------------------------+ int CSimpleCandlesStrategy::SignalForOpen() { // By default, there is no signal int signal = 0; MqlRates rates[]; // Copy the quote values (candles) to the destination array int res = CopyRates(m_symbol, m_timeframe, 1, m_signalSeqLen, rates); // If the required number of candles has been copied if(res == m_signalSeqLen) { signal = 1; // buy signal // Loop through all the candles for(int i = 0; i < m_signalSeqLen; i++) { // If at least one upward candle occurs, cancel the signal if(rates[i].open < rates[i].close ) { signal = 0; break; } } if(signal == 0) { signal = -1; // otherwise, sell signal // Loop through all the candles for(int i = 0; i < m_signalSeqLen; i++) { // If at least one downward candle occurs, cancel the signal if(rates[i].open > rates[i].close ) { signal = 0; break; } } } } return signal; }
Los métodos creados OpenBuy() y OpenSell() se encargan de abrir posiciones. Dado que son muy similares, proporcionaremos el código solo para uno de ellos. Los puntos clave de este método son llamar al método para actualizar los niveles StopLoss y TakeProfit, que actualiza los valores de los dos campos de clase correspondientes; m_sl y m_tp, así como llamar al método para abrir la primera posición virtual sin abrir de la matriz m_orders.
//+------------------------------------------------------------------+ //| Open BUY order | //+------------------------------------------------------------------+ void CSimpleCandlesStrategy::OpenBuy() { // Retrieve the necessary symbol and price data double point = m_symbolInfo.Point(); int digits = m_symbolInfo.Digits(); // Opening price double price = m_symbolInfo.Ask(); // Update SL and TP levels by calculating ATR UpdateLevels(); // StopLoss and TakeProfit levels double sl = NormalizeDouble(price - m_sl * point, digits); double tp = NormalizeDouble(price + m_tp * point, digits); bool res = false; for(int i = 0; i < m_maxCountOfOrders; i++) { // Iterate through all virtual positions if(!m_orders[i].IsOpen()) { // If we find one that is not open, then open it // Open a virtual SELL position res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot, 0, NormalizeDouble(sl, digits), NormalizeDouble(tp, digits)); break; // and exit } } if(!res) { PrintFormat(__FUNCTION__" | ERROR opening BUY virtual order", 0); } }
El método de actualización de nivel comprueba primero si se ha establecido algún valor distinto de cero para el período de cálculo del ATR. Si es así, se llama a la función de cálculo ATR. El resultado se almacena en la variable channelWidth. Cuando el valor del período es 0, se asigna 1 a esta variable. En este caso, los valores de las entradas m_stopLevel y m_takeLevel se interpretan como valores en puntos y se incluyen en m_sl y m_tp sin cambios. De lo contrario, se interpretan como una fracción del valor ATR y se multiplican por el valor ATR calculado:
//+------------------------------------------------------------------+ //| Update SL and TP levels based on calculated ATR | //+------------------------------------------------------------------+ void CSimpleCandlesStrategy::UpdateLevels() { // Calculate ATR double channelWidth = (m_periodATR > 0 ? ChannelWidth() : 1); // Update SL and TP levels m_sl = m_stopLevel * channelWidth; m_tp = m_takeLevel * channelWidth; }
El último método que necesitaremos para nuestra nueva estrategia de trading es el método de cálculo del ATR. Como ya se ha mencionado, se puede implementar de diferentes maneras, incluyendo el uso de soluciones ya preparadas. Para simplificar, utilizaremos una de las posibles opciones de implementación disponibles:
//+------------------------------------------------------------------+ //| Calculate the ATR value (non-standard implementation) | //+------------------------------------------------------------------+ double CSimpleCandlesStrategy::ChannelWidth(ENUM_TIMEFRAMES p_tf = PERIOD_D1) { int n = m_periodATR; // Number of bars for calculation MqlRates rates[]; // Array for quotes // Copy quotes from the daily (default) timeframe int res = CopyRates(m_symbol, p_tf, 1, n, rates); // If the required amount has been copied if(res == n) { double tr[]; // Array for price ranges ArrayResize(tr, n); // Change its size double s = 0; // Sum for calculating the average FOREACH(rates, { tr[i] = rates[i].high - rates[i].low; // Remember the bar size }); ArraySort(tr); // Sort the sizes // Sum the inner two quarters of the bar sizes for(int i = n / 4; i < n * 3 / 4; i++) { s += tr[i]; } // Return the average size in points return 2 * s / n / m_symbolInfo.Point(); } return 0.0; }
Guarde los cambios realizados en el archivo Strategies/SimpleCandlesStrategy.mqh en la carpeta de trabajo del proyecto.
Conectando la estrategia
Por lo tanto, la estrategia en su conjunto está lista y ahora tenemos que conectarla al archivo del EA. Comencemos con la primera etapa. Le recordamos que su código ahora está dividido en dos archivos:
- MQL5/Experts/Article.17277/Stage1.mq5 — archivo del proyecto actual para investigar la estrategia SimpleCandles;
- MQL5/Include/antekov/Advisor/Experts/Stage1.mqh — archivo de la librería común a todos los proyectos.
En el archivo de proyecto actual, debe hacer lo siguiente:
- Defina la constante __NAME__ asignándole un valor único que difiera de los nombres utilizados en otros proyectos.
- Adjunte un archivo con la clase de estrategia comercial desarrollada.
- Conecta la parte común de la primera etapa del EA a la librería del asesor.
- Enumera los datos necesarios para la estrategia comercial.
- Cree una función denominada GetStrategyParams(), que convierta los valores de las entradas en una cadena de inicialización para el objeto de estrategia.
// 1. Define a constant with the EA name #define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) // 2. Connect the required strategy #include "Strategies/SimpleCandlesStrategy.mqh"; // 3. Connect the general part of the first stage EA from the Advisor library #include <antekov/Advisor/Experts/Stage1.mqh> //+------------------------------------------------------------------+ //| 4. Strategy inputs | //+------------------------------------------------------------------+ sinput string symbol_ = "GBPUSD"; sinput ENUM_TIMEFRAMES period_ = PERIOD_H1; input group "=== Opening signal parameters" input int signalSeqLen_ = 5; // Number of unidirectional candles input int periodATR_ = 30; // ATR period input group "=== Pending order parameters" input double stopLevel_ = 3750; // Stop Loss (in points) input double takeLevel_ = 50; // Take Profit (in points) input group "=== Money management parameters" input int maxCountOfOrders_ = 3; // Maximum number of simultaneously open orders //+------------------------------------------------------------------+ //| 5. Strategy initialization string generation function | //| from the inputs | //+------------------------------------------------------------------+ string GetStrategyParams() { return StringFormat( "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d)", symbol_, period_, signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_ ); } //+------------------------------------------------------------------+
Sin embargo, si compilamos el archivo del EA de la primera etapa (la compilación se realiza sin errores), al ejecutarlo obtenemos el siguiente error en la función OnInit(), lo que provoca que el EA se detenga:
2018.01.01 00:00:00 CVirtualFactory::Create | ERROR: Constructor not found for: 2018.01.01 00:00:00 class CSimpleCandlesStrategy("GBPUSD",16385,5,30,2.95,3.92,3)
El motivo es que, para crear objetos de todas las clases descendientes de CFactorable, utilizamos una función CVirtualFactory::Create() independiente del archivo Virtual/VirtualFactory.mqh. Se llama en las macros NEW(C) y CREATE(C, O, P) declaradas en Base/Factorable.mqh.
Esta función lee el nombre de la clase de objeto de la cadena de inicialización y lo asigna a la variable className. La parte leída se elimina de la cadena de inicialización. A continuación, se realiza una simple iteración a través de todos los nombres de clase posibles (descendientes de CFactorable) hasta que se encuentra una coincidencia con el nombre que se acaba de leer. En este caso, se crea un nuevo objeto de la clase deseada y el puntero a él a través de la variable object se devuelve como resultado de la función de creación:
// Create an object from the initialization string static CFactorable* Create(string p_params) { // Read the object class name string className = CFactorable::ReadClassName(p_params); // Pointer to the object being created CFactorable* object = NULL; // Call the corresponding constructor depending on the class name if(className == "CVirtualAdvisor") { object = new CVirtualAdvisor(p_params); } else if(className == "CVirtualRiskManager") { object = new CVirtualRiskManager(p_params); } else if(className == "CVirtualStrategyGroup") { object = new CVirtualStrategyGroup(p_params); } else if(className == "CSimpleVolumesStrategy") { object = new CSimpleVolumesStrategy(p_params); } else if(className == "CHistoryStrategy") { object = new CHistoryStrategy(p_params); } // If the object is not created or is created in the invalid state, report an error if(!object) { ... } return object; }
Cuando todo nuestro código estaba en una sola carpeta, simplemente añadimos aquí ramas de sentencias condicionales adicionales para las nuevas clases hijas CFactorable que estábamos utilizando. Por ejemplo, así es como se creó la parte responsable de crear los objetos de nuestra primera estrategia de modelo SimpleVolumes:
} else if(className == "CSimpleVolumesStrategy") { object = new CSimpleVolumesStrategy(p_params); }
Siguiendo el enfoque anterior, deberíamos añadir aquí un bloque similar para nuestra nueva estrategia de modelo SimpleCandles:
} else if(className == "CSimpleCandlesStrategy") { object = new CSimpleCandlesStrategy(p_params); }
Pero ahora esto ya viola el principio de separar el código en partes de librería y proyecto. La parte del código correspondiente a la biblioteca no necesita saber qué otras estrategias nuevas se crearán al utilizarla. Ahora incluso la creación de CSimpleVolumesStrategy de esta manera parece incorrecta.
Intentemos encontrar una forma de garantizar, por un lado, la creación de todos los objetos necesarios y, por otro, una separación clara del código.
Mejorando CFactorable
Debo admitir que esta tarea no es tan sencilla. Me obligó a pensar mucho en la solución y a probar más de una opción de implementación antes de encontrar finalmente la que se seguirá utilizando por ahora. Si el lenguaje MQL5 tuviera la capacidad de ejecutar código desde una cadena en un programa ya compilado, entonces todo se resolvería de forma muy sencilla. Sin embargo, por motivos de seguridad, no disponemos de una función similar a la función eval() de otros lenguajes de programación. Por lo tanto, tuvimos que conformarnos con las oportunidades que se nos presentaban.
En general, la idea es la siguiente: cada descendiente de CFactorable debe tener una función estática que cree un objeto de la clase dada. Por lo tanto, estamos ante una especie de constructor estático. En este caso, el constructor normal puede convertirse en no público, y solo se puede utilizar el constructor estático para crear objetos. A continuación, tendremos que asociar de alguna manera los nombres de las clases con estas funciones, de modo que podamos entender qué función constructora debemos llamar en función del nombre de la clase obtenido de la cadena de inicialización.
Para resolver este problema, necesitaremos punteros de función. Se trata de un tipo especial de variable que nos permite almacenar un puntero a un código de función en una variable y llamar al código de función utilizando ese puntero. Como habrás notado, todos los constructores estáticos de objetos de diferentes clases descendientes de CFactorable se pueden declarar con la siguiente firma:
static CFactorable* Create(string p_params)
Por lo tanto, podemos crear una matriz estática donde colocar punteros a dichas funciones para todas las clases descendientes. Las clases que forman parte de la librería Advisor (CVirtualAdvisor, CVirtualStrategyGroup, CVirtualRiskManager) se añadirán de alguna manera a esta matriz dentro del código de la librería. Al mismo tiempo, las clases de estrategia comercial se añadirán a esta matriz desde el código ubicado en la carpeta de trabajo del proyecto. De esta manera se logrará la separación de códigos deseada.
La siguiente pregunta es: ¿cómo logramos todo esto? ¿En qué clase se debe declarar esta matriz estática y cómo se puede rellenar? ¿Cómo podemos conservar la asociación de un nombre de clase con un elemento de matriz?
Al principio, parecía más adecuado crear esta matriz estática como parte de la clase CFactorable. Para la vinculación, podemos crear otra matriz estática de cadenas: nombres de clases. Si la reposición añade simultáneamente un nombre de clase a una matriz y un puntero a un constructor estático de objetos de esa clase a otra matriz, obtendremos una relación de índice entre los elementos de las dos matrices. En otras palabras, una vez encontrado el índice de un elemento igual al nombre de clase requerido en una matriz, podemos utilizar este índice para obtener el puntero a una función constructora de otra matriz y, a continuación, llamarla pasando la cadena de inicialización.
Pero, ¿cómo rellenamos estas matrices? Realmente no quería crear ninguna función que tuviera que ser llamada desde OnInit(). Aunque, al final, este enfoque resulta bastante viable. Pero al final, tomé una decisión diferente.
La idea básica era que nos gustaría poder llamar a algún código no desde OnInit(), sino directamente desde los archivos que describen las clases de los objetos descendientes de CFactorable. Sin embargo, si simplemente colocas el código fuera de la definición de la clase, no se ejecutará. Pero si declaras una variable global fuera de la definición de clase que es un objeto de alguna clase, ¡entonces su constructor se llamará en este lugar!
Por lo tanto, creemos una clase separada CFactorableCreator específicamente para este fin. Sus objetos almacenarán el nombre de la clase y un puntero al constructor estático de los objetos de la clase dada. Esta clase también tendrá una matriz estática de punteros a objetos de la misma clase. Al mismo tiempo, el constructor CFactorableCreator se asegurará de que todos los objetos que cree terminen en esta matriz:
// Preliminary class definition class CFactorable; // Type declaration - pointer to the function for creating objects of the CFactorable class typedef CFactorable* (*TCreateFunc)(string); //+------------------------------------------------------------------+ //| Class of creators that bind names and static | //| constructors of CFactorable descendant classes | //+------------------------------------------------------------------+ class CFactorableCreator { public: string m_className; // Class name TCreateFunc m_creator; // Static constructor for the class // Creator constructor CFactorableCreator(string p_className, TCreateFunc p_creator); // Static array of all created creator objects static CFactorableCreator* creators[]; }; // Static array of all created creator objects CFactorableCreator* CFactorableCreator::creators[]; //+------------------------------------------------------------------+ //| Creator constructor | //+------------------------------------------------------------------+ CFactorableCreator::CFactorableCreator(string p_className, TCreateFunc p_creator) : m_className(p_className), m_creator(p_creator) { // Add the current creator object to the static array APPEND(creators, &this); } //+------------------------------------------------------------------+
Veamos cómo podemos organizar la reposición de la matriz CFactorableCreator::creators utilizando la clase CVirtualAdvisor como ejemplo. Transferiremos el constructor CVirtualAdvisor a la sección «protegida» y añadiremos la función constructora estática Create(). Después de describir la clase, Create() el objeto global de la clase CFactorableCreator denominado CVirtualAdvisorCreator. Es precisamente ahí, al llamar al constructor CFactorableCreator, donde se rellena la matriz CFactorableCreator::creators.
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: //... CVirtualAdvisor(string p_param); // Private constructor public: static CFactorable* Create(string p_params) { return new CVirtualAdvisor(p_params) }; //... }; CFactorableCreator CVirtualAdvisorCreator("CVirtualAdvisor", CVirtualAdvisor::Create);
Tendremos que realizar las mismas tres modificaciones en todas las clases de los objetos descendientes de CFactorable. Para simplificar un poco las cosas, declararemos dos macros auxiliares en el archivo que contiene la clase CFactorable:
// Declare a static constructor inside the class #define STATIC_CONSTRUCTOR(C) static CFactorable* Create(string p) { return new C(p); } // Add a static constructor for the new CFactorable descendant class // to a special array by creating a global object of the CFactorableCreator class #define REGISTER_FACTORABLE_CLASS(C) CFactorableCreator C##Creator(#C, C::Create);
Simplemente repiten la plantilla de código que ya hemos desarrollado para la clase CVirtualAdvisor. Ahora podemos realizar ediciones como esta:
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: // ... CVirtualAdvisor(string p_param); // Constructor public: STATIC_CONSTRUCTOR(CVirtualAdvisor); // ... }; REGISTER_FACTORABLE_CLASS(CVirtualAdvisor);
Es necesario realizar cambios similares en los tres archivos de clase de la librería Advisor (CVirtualAdvisor, CVirtualStrategyGroup, CVirtualRiskManager), pero esto solo tuvo que hacerse una vez. Ahora que estos cambios están en la biblioteca, podemos olvidarnos de ellos.
En los archivos de las clases de estrategia comercial ubicados en la carpeta de trabajo del proyecto, estas adiciones son obligatorias para cada nueva clase. Añadámoslos a nuestra nueva estrategia, tras lo cual el código de descripción de la clase tendrá este aspecto:
//+------------------------------------------------------------------+ //| Trading strategy using unidirectional candlesticks | //+------------------------------------------------------------------+ class CSimpleCandlesStrategy : public CVirtualStrategy { protected: string m_symbol; // Symbol (trading instrument) ENUM_TIMEFRAMES m_timeframe; // Chart period (timeframe) //--- Open signal parameters int m_signalSeqLen; // Number of unidirectional candles int m_periodATR; // ATR period //--- Position parameters double m_stopLevel; // Stop Loss (in points or % ATR) double m_takeLevel; // Take Profit (in points or % ATR) //--- Money management parameters int m_maxCountOfOrders; // Max number of simultaneously open positions CSymbolInfo *m_symbolInfo; // Object for getting information about the symbol properties double m_tp; // Stop Loss in points double m_sl; // Take Profit in points //--- Methods int SignalForOpen(); // Signal to open a position void OpenBuy(); // Open a BUY position void OpenSell(); // Open a SELL position double ChannelWidth(ENUM_TIMEFRAMES p_tf = PERIOD_D1); // Calculate the ATR value void UpdateLevels(); // Update SL and TP levels // Private constructor CSimpleCandlesStrategy(string p_params); public: // Static constructor STATIC_CONSTRUCTOR(CSimpleCandlesStrategy); virtual string operator~() override; // Convert object to string virtual void Tick() override; // OnTick event handler }; // Register the CFactorable descendant class REGISTER_FACTORABLE_CLASS(CSimpleCandlesStrategy);
Permítanme enfatizar una vez más que las partes resaltadas deben estar presentes en cualquier nueva clase de estrategia comercial.
Solo queda aplicar la matriz rellenada de creadores de objetos en la función general de creación de objetos desde la cadena de inicialización CVirtualFactory::Create(). Aquí también cambiaremos algo. Al final, ya no necesitamos colocar esta función en una clase separada. Anteriormente, esto se hacía porque, formalmente, la clase CFactorable no está obligada a conocer los nombres de todos sus descendientes. Una vez realizados los cambios, es posible que no conozcamos los nombres de todos los descendientes, pero podemos crear cualquiera de ellos accediendo a los constructores estáticos a través de los elementos de una única matriz CFactorableCreator::creators. Por lo tanto, movamos el código de esta función a un nuevo método estático de la clase CFactorable::Create():
//+------------------------------------------------------------------+ //| Base class of objects created from a string | //+------------------------------------------------------------------+ class CFactorable { // ... public: // ... // Create an object from the initialization string static CFactorable* Create(string p_params); }; //+------------------------------------------------------------------+ //| Create an object from the initialization string | //+------------------------------------------------------------------+ CFactorable* CFactorable::Create(string p_params) { // Pointer to the object being created CFactorable* object = NULL; // Read the object class name string className = CFactorable::ReadClassName(p_params); // Find and call the corresponding constructor depending on the class name int i; SEARCH(CFactorableCreator::creators, className == CFactorableCreator::creators[i].m_className, i); if(i != -1) { object = CFactorableCreator::creators[i].m_creator(p_params); } // If the object is not created or is created in the invalid state, report an error if(!object) { PrintFormat(__FUNCTION__" | ERROR: Constructor not found for:\n%s", p_params); } else if(!object.IsValid()) { PrintFormat(__FUNCTION__ " | ERROR: Created object is invalid for:\n%s", p_params); delete object; // Remove the invalid object object = NULL; } return object; }
Como puede ver, primero obtenemos el nombre de clase de la cadena de inicialización, después de lo cual buscamos el índice del elemento en la matriz de creadores cuyo nombre de clase coincide con el requerido. El índice requerido se coloca en la variable i. Si se encuentra el índice, se llama al constructor estático del objeto de la clase requerida a través del puntero correspondiente a la función. Ya no hay referencias a los nombres de las clases descendientes de CFactorable en este código. El archivo que contiene la clase CVirtualFactory ha quedado obsoleto. Se excluirá de la librería.
Comprobación de la primera etapa del EA
Compilemos la primera etapa del EA y ejecutemos la optimización manualmente (por ahora). Tomemos como ejemplo el intervalo de optimización de 2018 a 2023, ambos inclusive, el símbolo GBPUSD y el marco temporal H4. La optimización comienza con éxito y, tras un tiempo, podemos ver los resultados obtenidos:

Figura 1. Configuración de optimización y visualización de resultados de optimización para el EA Stage1.mq5
Veamos un par de pasadas individuales que parecieron más o menos buenas.


Figura 2. Resultados del pase con los siguientes parámetros: Clase CSimpleCandlesStrategy("GBPUSD",16388,4,23,2.380,4.950,19)
En los resultados presentados en la figura 2, la apertura se produjo después de cuatro velas en la misma dirección, y la relación entre los niveles StopLoss y TakeProfit fue de aproximadamente 1:2.


Figura 3. Resultados del pase con los siguientes parámetros: clase CSimpleCandlesStrategy("GBPUSD",16388,7,9,0.090,3.840,1)
La figura 3 muestra los resultados de un pase donde la apertura se produjo después de siete velas en la misma dirección. En este caso, se utilizó un StopLoss muy corto y un TakeProfit grande. Esto se ve claramente en el gráfico, donde la gran mayoría de las operaciones se cierran con una pequeña pérdida, y solo una docena de operaciones en 6 años se cerraron con ganancias, aunque estas fueron cuantiosas.
Por lo tanto, aunque esta estrategia de trading es muy sencilla, puede probar a trabajar con ella para obtener mejores resultados tras combinar muchas instancias en un EA final.
Conclusión
Aún no hemos completado el proceso de conectar la nueva estrategia al sistema de optimización automática, pero hemos dado pasos importantes que nos permitirán continuar por el camino previsto. En primer lugar, ya tenemos una nueva estrategia de negociación implementada como una clase independiente que es descendiente de CVirtualStrategy. En segundo lugar, pudimos conectarlo al EA de primera etapa y verificamos que era posible iniciar el proceso de optimización de este EA.
La optimización de una única instancia de una estrategia de negociación, realizada en la primera fase, comienza cuando la base de datos de optimización aún no contiene los resultados de ninguna ejecución. Para la segunda y tercera etapas, ya es necesario disponer de los resultados de optimización de la primera etapa en la base de datos. Por lo tanto, aún no es posible conectar y probar la estrategia en los EAs de segunda y tercera etapa. En primer lugar, debemos crear un proyecto en la base de datos de optimización y ejecutarlo para acumular los resultados de la primera fase. En la siguiente parte, continuaremos el trabajo que hemos iniciado considerando la modificación del EA de creación de proyectos.
¡Gracias por su atención! ¡Hasta pronto!
Advertencia importante
Todos los resultados presentados en este artículo y en todos los artículos anteriores de la serie se basan únicamente en datos históricos de pruebas y no garantizan ningún beneficio en el futuro. El trabajo dentro de este proyecto es de naturaleza investigativa. Todos los resultados publicados pueden ser utilizados por cualquier persona bajo su propia responsabilidad.
Contenido del archivo
| # | Nombre | Versión | Descripción | Cambios recientes |
|---|---|---|---|---|
| MQL5/Experts/Article.17277 | Carpeta de trabajo del proyecto | |||
| 1 | CreateProject.mq5 | 1.01 | Script para crear un proyecto con etapas, trabajos y tareas de optimización. | Parte 23 |
| 2 | Optimization.mq5 | 1.00 | EA para optimización automática de proyectos | Parte 23 |
| 3 | SimpleCandles.mq5 | 1.00 | EA final para el funcionamiento en paralelo de varios grupos de estrategias de modelado. Los parámetros se tomarán de la librería de grupos integrada. | Parte 24 |
| 4 | Stage1.mq5 | 1.22 | EA para optimización de una única instancia de una estrategia de trading (etapa 1). | Parte 24 |
| 5 | Stage2.mq5 | 1.00 | EA para optimización grupal de instancias de estrategias de trading (etapa 2). | Parte 23 |
| 6 | Stage3.mq5 | 1.00 | EA que guarda un grupo estandarizado de estrategias generado en una base de datos de EAs, con un nombre determinado. | Parte 23 |
| MQL5/Experts/Article.17277/Strategies | Carpeta de estrategias del proyecto | |||
| 7 | SimpleCandlesStrategy.mqh | 1.01 | Parte 24 | |
| MQL5/Include/antekov/Advisor/Base | Clases base de las que heredan las demás clases del proyecto | |||
| 8 | Advisor.mqh | 1.04 | Clase base | Parte 10 |
| 9 | Factorable.mqh | 1.05 | Clase base de objetos creados a partir de una cadena string. | Parte 24 |
| 10 | FactorableCreator.mqh | 1.00 | Parte 24 | |
| 11 | Interface.mqh | 1.01 | Clase básica para visualizar varios objetos. | Parte 4 |
| 12 | Receiver.mqh | 1.04 | Clase base para convertir volúmenes abiertos en posiciones de mercado. | Parte 12 |
| 13 | Strategy.mqh | 1.04 | Clase base de la estrategia comercial. | Parte 10 |
| MQL5/Include/antekov/Advisor/Database | Archivos para manejar todos los tipos de bases de datos utilizadas por los EAs del proyecto. | |||
| 14 | Database.mqh | 1.10 | Clase para el manejo de la base de datos. | Parte 22 |
| 15 | db.adv.schema.sql | 1.00 | Estructura de la base de datos del EA final. | Parte 22 |
| 16 | db.cut.schema.sql | 1.00 | Estructura de la base de datos de optimización truncada. | Parte 22 |
| 17 | db.opt.schema.sql | 1.05 | Estructura de la base de datos de optimización. | Parte 22 |
| 18 | Storage.mqh | 1.01 | Clase para gestionar el almacenamiento clave-valor del EA final en el EA de la base de datos. | Parte 23 |
| MQL5/Include/antekov/Advisor/Experts | Archivos que contienen componentes comunes usados por EAs de diferentes tipos. | |||
| 19 | Expert.mqh | 1.22 | Librería para el EA final. Los parámetros del grupo se pueden obtener de la base de datos del EA. | Parte 23 |
| 20 | Optimization.mqh | 1.04 | Librería que gestiona el lanzamiento de tareas de optimización. | Parte 23 |
| 21 | Stage1.mqh | 1.19 | Libreria de optimización de estrategia de trading de instancia única (etapa 1). | Parte 23 |
| 22 | Stage2.mqh | 1.04 | Librería que optimiza un grupo de instancias de estrategias de trading (etapa 2) | Parte 23 |
| 23 | Stage3.mqh | 1.04 | Libreria que guarda un grupo estandarizado de estrategias generado en una base de datos con un nombre determinado. | Parte 23 |
| MQL5/Include/antekov/Advisor/Optimization | Clases responsables de la optimización automática. | |||
| 24 | Optimizer.mqh | 1.03 | Clase para el gestor de proyectos de optimización automática. | Parte 22 |
| 25 | OptimizerTask.mqh | 1.03 | Clase de tarea de optimización. | Parte 22 |
| MQL5/Include/antekov/Advisor/Strategies | Ejemplos de estrategias comerciales utilizadas para demostrar cómo funciona el proyecto. | |||
| 26 | HistoryStrategy.mqh | 1.00 | Clase de estrategia comercial para reproducir el historial de transacciones. | Parte 16 |
| 27 | SimpleVolumesStrategy.mqh | 1.11 | Clase de estrategia comercial que utiliza volúmenes de ticks. | Parte 22 |
| MQL5/Include/antekov/Advisor/Utils | Utilidades auxiliares, macros para la reducción de código. | |||
| 28 | ExpertHistory.mqh | 1.00 | Clase para exportar el historial comercial a un archivo. | Parte 16 |
| 29 | Macros.mqh | 1.05 | Macros útiles para operaciones con matrices. | Parte 22 |
| 30 | NewBarEvent.mqh | 1.00 | Clase para definir una nueva barra para un símbolo específico. | Parte 8 |
| 31 | SymbolsMonitor.mqh | 1.00 | Clase para obtener información sobre instrumentos comerciales (símbolos). | Parte 21 |
| MQL5/Include/antekov/Advisor/Virtual | Clase para crear diversos objetos unidos mediante el uso de un sistema de órdenes y posiciones comerciales virtuales. | |||
| 32 | Money.mqh | 1.01 | Clase básica sobre gestión del dinero | Parte 12 |
| 33 | TesterHandler.mqh | 1.07 | Clase de gestión de eventos de optimización | Parte 23 |
| 34 | VirtualAdvisor.mqh | 1.10 | Clase que maneja posiciones virtuales (órdenes). | Parte 24 |
| 35 | VirtualChartOrder.mqh | 1.01 | Clase de posición virtual gráfica. | Parte 18 |
| 36 | VirtualHistoryAdvisor.mqh | 1.00 | Clase de repetición del historial comercial. | Parte 16 |
| 37 | VirtualInterface.mqh | 1.00 | Clase GUI. | Parte 4 |
| 38 | VirtualOrder.mqh | 1.09 | Clase de órdenes y posiciones virtuales. | Parte 22 |
| 39 | VirtualReceiver.mqh | 1.04 | Clase para convertir volúmenes abiertos en posiciones de mercado (receptor). | Parte 23 |
| 40 | VirtualRiskManager.mqh | 1.05 | Clase de gestión de riesgos (gestor de riesgos). | Parte 24 |
| 41 | VirtualStrategy.mqh | 1.09 | Clase de estrategia comercial con posiciones virtuales. | Parte 23 |
| 42 | VirtualStrategyGroup.mqh | 1.03 | Clase de grupo(s) de estrategias comerciales. | Parte 24 |
| 43 | VirtualSymbolReceiver.mqh | 1.00 | Clase receptora de símbolos. | Parte 3 |
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/17277
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.
Características del Wizard MQL5 que debe conocer (Parte 56): Fractales de Bill Williams
Análisis de múltiples símbolos con Python y MQL5 (Parte 3): Tipos de cambio triangulares
Kit de herramientas de negociación MQL5 (Parte 8): Cómo implementar y utilizar la librería History Manager en sus proyectos
Introducción a las curvas ROC (Receiver Operating Characteristic)
- 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