English Русский 中文 Deutsch 日本語
preview
Desarrollo de asesores expertos autooptimizables en MQL5 (Parte 8): Análisis de múltiples estrategias (2)

Desarrollo de asesores expertos autooptimizables en MQL5 (Parte 8): Análisis de múltiples estrategias (2)

MetaTrader 5Ejemplos |
39 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

En esta serie, exploramos formas creativas de combinar diferentes estrategias de trading en un conjunto único y coherente. El objetivo es superar las limitaciones de las estrategias individuales fusionándolas en algo más potente.

En nuestra última conversación, creamos una superclase que serviría de base para todas nuestras estrategias de trading. Esta superclase nos permitió implementar nuestra primera estrategia, un cruce de medias móviles, en MQL5. Posteriormente, comparamos nuestra clase de estrategia flexible con una versión codificada de forma rígida de la misma estrategia para verificar que el rendimiento coincidiera.

Utilizando el probador de estrategias de MetaTrader 5, pudimos encontrar una configuración de parámetros sólida. Encontrar buenos parámetros por cuenta propia puede resultar complicado. Por eso, el optimizador genético de MetaTrader 5 es una herramienta tan valiosa. Ayuda a automatizar el proceso, ahorrando tiempo y esfuerzo. 

También utilizamos técnicas de pruebas prospectivas para filtrar los conjuntos de parámetros estables de nuestros resultados. Esto confirmó que nuestra implementación fue precisa y eficiente.

Hoy vamos un paso más allá. Crearemos una segunda estrategia basada en el Índice de Fuerza Relativa (RSI) y luego la combinaremos con nuestra estrategia de cruce de medias móviles. Al combinarlas, nuestro objetivo es crear una estrategia combinada más sólida y potencialmente más rentable. También utilizaremos el probador de estrategias de MetaTrader 5 para optimizar esta nueva estrategia combinada. Pero antes de profundizar, es importante hablar de un concepto clave: la minimización de parámetros.

A medida que añadimos más componentes a nuestra estrategia, el número de parámetros puede aumentar rápidamente. Una estrategia con demasiados elementos variables se vuelve más difícil, y a veces casi imposible, de optimizar de manera efectiva. Por eso es crucial limitar el número de parámetros siempre que sea posible. A menudo, fijar ciertos parámetros a valores constantes nos permite centrarnos en optimizar los que tienen mayor impacto.

Para poner las cosas en perspectiva, nuestra estrategia original de media móvil logró un índice de Sharpe de 1,29 en las pruebas prospectivas, con una ganancia de 133,51 dólares en 101 operaciones. En cambio, la nueva estrategia basada en el RSI que vamos a desarrollar hoy logró un índice de Sharpe de 2,68 y una ganancia de 214,08 dólares, con tan solo 52 operaciones.

Eso significa que la nueva estrategia no solo generó más dinero, sino que lo hizo con menos operaciones, menor exposición al mercado y menos riesgo. Este es precisamente el tipo de rendimiento que buscamos al diseñar estrategias de trading.

Trabajando en estrecha colaboración con la herramienta de prueba de estrategias y utilizándola con criterio, podemos descubrir configuraciones más rentables. Dicho esto, es importante gestionar las expectativas. El probador de estrategias de MetaTrader 5 no puede corregir mágicamente una mala estrategia. Si una estrategia tiene fallos fundamentales, ninguna optimización la hará rentable.

Incluso con una buena estrategia, la optimización no garantiza mejores resultados, pero puede mejorar significativamente tus posibilidades si se utiliza correctamente. Con una base sólida y las herramientas adecuadas, no hay límite al rendimiento que podemos obtener.


Primeros pasos en MQL5

Comenzamos creando primero la clase para nuestra estrategia de punto medio RSI. El primer objetivo que debemos alcanzar es cargar las dependencias que requiere nuestra clase de estrategia. La primera dependencia es la clase RSI que creamos para nuestros indicadores de búfer único. Además, también necesitaremos la superclase para todas nuestras estrategias, también llamada clase padre.

//+------------------------------------------------------------------+
//|                                                  RSIMidPoint.mqh |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <VolatilityDoctor\Indicators\RSI.mqh>
#include <VolatilityDoctor\Strategies\Parent\Strategy.mqh>

Ahora que hemos cargado nuestras dependencias, podemos comenzar a definir la clase de estrategia de punto medio del RSI y sus miembros. Tenga en cuenta que esta estrategia de punto medio hereda de la clase base para todas nuestras estrategias, que denotamos utilizando la sintaxis de dos puntos. A partir de ahí, definimos los miembros de nuestra clase de estrategia de punto medio. Observe que solo tenemos que definir unos pocos miembros. Además, tenga en cuenta que todos los métodos virtuales que creamos en la clase de estrategia principal deben volver a aparecer como virtuales en nuestra clase de punto medio RSI.

class RSIMidPoint : public Strategy
  {
private:
                     //--- The instance of the RSI used in this strategy
                     RSI *my_rsi;

public:
                     //--- Class constructor 
                     RSIMidPoint(string user_symbol,ENUM_TIMEFRAMES user_timeframe,int user_period,ENUM_APPLIED_PRICE user_price);
                     
                     //--- Class destructor
                    ~RSIMidPoint();
                    
                    //--- Class overrides
                    virtual bool Update(void);
                    virtual bool BuySignal(void);
                    virtual bool SellSignal(void);
  };
 

Ahora podemos empezar a considerar cómo se implementará cada método específicamente para nuestra estrategia RSI. El método de actualización solo necesita actualizar el valor del indicador RSI y luego asegurarse de que la lectura actual del indicador RSI no sea cero. Si ese es el caso, entonces todo salió bien; de lo contrario, algo salió mal.

//+------------------------------------------------------------------+
//| Our strategy update method                                       |
//+------------------------------------------------------------------+
bool RSIMidPoint::Update(void)
   {
      //--- Set the indicator value
      my_rsi.SetIndicatorValues(Strategy::GetIndicatorBufferSize(),true);
      
      //--- Check readings are valid
      if(my_rsi.GetCurrentReading() != 0) return(true);
      
      //--- Something went wrong
      return(false);
   }  

A continuación, analizaremos los casos que justifican nuestras señales de compra y venta. Nuestras señales de compra se generan cuando la lectura del RSI está por debajo de 50, mientras que, por otro lado, nuestras señales de venta se generan cuando el RSI está por encima de 50.

//+------------------------------------------------------------------+
//| Check for our buy signal                                         |
//+------------------------------------------------------------------+
bool RSIMidPoint::BuySignal(void)
   {
      //--- Buy signals when the RSI is below 50
      return(my_rsi.GetCurrentReading()<50);
   }

//+------------------------------------------------------------------+
//| Check for our sell signal                                        |
//+------------------------------------------------------------------+
bool RSIMidPoint::SellSignal(void)
   {
      //--- Sell signals when the RSI is above 50
      return(my_rsi.GetCurrentReading()>50);
   }

Por último, debemos considerar el constructor y el destructor de la clase. El constructor de la clase recibirá los detalles requeridos por nuestro indicador RSI. En concreto, nuestro constructor requiere que el usuario especifique sobre qué símbolo, qué marco temporal, qué período y sobre qué precio se debe aplicar el RSI. Una vez especificados todos esos detalles, cargamos la información y creamos una nueva instancia de nuestro indicador RSI.

Sin embargo, tenga en cuenta que esta versión particular del indicador RSI que utilizamos no es la misma que viene preinstalada en MetaTrader 5. Se trata de un tipo personalizado que hemos definido y que cuenta con muchas otras funciones útiles que utilizaremos. No obstante, la funcionalidad general es la misma, y los lectores pueden implementar algunos de estos métodos desde cero si lo necesitan.

//+------------------------------------------------------------------+
//| Our class constructor                                            |
//+------------------------------------------------------------------+
RSIMidPoint::RSIMidPoint(string user_symbol,ENUM_TIMEFRAMES user_timeframe,int user_period,ENUM_APPLIED_PRICE user_price)
  {
   my_rsi = new RSI(user_symbol,user_timeframe,user_period,user_price);
   Print("RSI-Mid-Point Strategy Loaded.");
  }

Finalmente, en el destructor de nuestra clase, eliminamos el puntero al objeto RSI personalizado que creamos.

//+------------------------------------------------------------------+
//| Our class destructor                                             |
//+------------------------------------------------------------------+
RSIMidPoint::~RSIMidPoint()
  {
   delete my_rsi;
  }
//+------------------------------------------------------------------+

Ahora que hemos definido la clase que encapsula nuestra estrategia del punto medio del RSI, debemos asegurarnos de que la clase se haya construido sin errores. Por lo tanto, primero debemos establecer un nivel de rendimiento de referencia utilizando una versión codificada de forma rígida de la estrategia para poder probar nuestra clase y ver si puede recuperar los mismos niveles de rendimiento que se obtuvieron con la versión codificada de forma rígida. 

Si estas dos estrategias son equivalentes, entonces deberían tener los mismos niveles de rentabilidad y producir las mismas estadísticas cuando se prueban durante el mismo período. Así pues, al definir nuestra línea base, lo primero que debemos hacer es enumerar las constantes del sistema que necesitamos. Por ejemplo, el precio sobre el que se debe aplicar el RSI, el período del RSI y el marco temporal. Es necesario fijar todas estas constantes en ambas pruebas para garantizar que las comparaciones sean justas.

//+------------------------------------------------------------------+
//|                                          MSA Test 2 Baseline.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Define system constants                                          |
//+------------------------------------------------------------------+
#define RSI_PRICE        PRICE_CLOSE
#define RSI_PERIOD       15
#define RSI_TIME_FRAME   PERIOD_D1
#define HOLDING_PERIOD   5

A continuación, cargaremos las dependencias que necesitamos. En este caso, solo necesitamos unas tres bibliotecas.

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>
#include <VolatilityDoctor\Time\Time.mqh>

Además, también necesitaremos definir algunas variables globales. Observe que podemos dividir nuestras variables globales en dos tipos: tipos personalizados y tipos del sistema. Los tipos personalizados son definidos por el usuario. En todas las instalaciones de MetaTrader 5 están disponibles tipos de datos definidos por el sistema, como por ejemplo los números de doble precisión y los números de coma flotante. Esta es una forma de agrupar nuestras variables globales, simplemente para que nuestro código sea más fácil de mantener.

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+

//--- Custom Types
CTrade    Trade;
TradeInfo *TradeInformation;
Time      *TradeTime;

//--- System Types
double rsi[],ma_close[];
int    rsi_handler;
int    position_timer;

A partir de ahí, pasamos a construir el proceso de inicialización para nuestro asesor experto. Cuando se inicializa nuestro asesor experto, observe que crearemos nosotros mismos el manejador del RSI y, a continuación, también crearemos instancias dinámicas de algunos de los tipos personalizados que necesitamos. Necesitamos estos tipos personalizados, en particular, para controlar el tiempo y obtener información operativa esencial, como el volumen mínimo permitido. Y a partir de ahí, nos aseguraremos de que el manejador del RSI se haya cargado de forma segura.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Our technical indicator
   rsi_handler = iRSI(Symbol(),RSI_TIME_FRAME,RSI_PERIOD,RSI_PRICE);

//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),RSI_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),RSI_TIME_FRAME);


//--- Safety checks
   if(rsi_handler == INVALID_HANDLE)
      return(false);
   
//--- Everything was fine
   return(INIT_SUCCEEDED);
  }
//--- End of OnInit Scope

Cuando ya no utilicemos nuestro asesor experto, liberaremos el indicador que ya no utilizamos y también eliminaremos los objetos dinámicos que hayamos creado.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete the indicators and dynamic objects
   IndicatorRelease(rsi_handler);

   delete TradeTime;
   delete TradeInformation;

  }
//--- End of Deinit Scope

Durante la función OnTick, primero comprobaremos si se ha formado una nueva vela diaria, utilizando la biblioteca que hemos escrito para ello. Si se ha formado una nueva vela, actualizaremos nuestros indicadores técnicos y comprobaremos si tenemos alguna posición abierta. Si no tenemos ninguna posición abierta, nos aseguraremos de que el tiempo de la posición se haya reiniciado y, a continuación, comprobaremos si hay alguna señal de trading. De lo contrario, si tenemos operaciones abiertas, comprobaremos si nuestra operación ha llegado a su vencimiento y debe cerrarse. De lo contrario, seguiremos manteniendo la posición.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new daily candle has formed
   if(TradeTime.NewCandle())
     {

      //--- Update our technical indicators
      Update();

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Reset the position timer
         position_timer = 0;

         //--- Check for a trading signal
         CheckSignal();
        }

      //--- Otherwise
      else
        {
         //--- The position has reached maturity
         if(position_timer == HOLDING_PERIOD)
            Trade.PositionClose(Symbol());

         //--- Otherwise keep holding
         else
            position_timer++;
        }
     }
  }
//--- End of OnTick Scope

También debemos considerar la implementación de algunos de los métodos que hemos comentado. Por ejemplo, el método de actualización simplemente llama a copy buffer, y copia los valores del indicador desde nuestro manejador del RSI en la matriz que especificamos para este propósito. Solo estamos copiando la lectura actual del indicador RSI. No necesitamos las lecturas anteriores.

//+------------------------------------------------------------------+
//| Update our technical indicators                                  |
//+------------------------------------------------------------------+
void Update(void)
  {
//--- Call the CopyBuffer method to get updated indicator values
   CopyBuffer(rsi_handler,0,0,1,rsi);
  }
//--- End of Update Scope

Nuevamente, el método de señal de verificación (el método de señal de verificación simplemente busca las señales de trading que definimos anteriormente) indica que nuestro RSI comprará cuando la lectura esté por debajo de 50; de lo contrario, venderemos si nuestra lectura está por encima de 50.

//+------------------------------------------------------------------+
//| Check for a trading signal using our cross-over strategy         |
//+------------------------------------------------------------------+
void CheckSignal(void)
  {
//--- Buy signals when the RSI is below 50
   if(rsi[0] < 50)
     {
      Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
      return;
     }

//--- Sell signals when the RSI is above 50
   else
      if(rsi[0] > 50)
        {
         Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
         return;
        }

  }
//--- End of CheckSignal Scope

Por último, unificaremos las constantes del sistema que definimos al principio.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef RSI_PRICE        
#undef RSI_PERIOD       
#undef RSI_TIME_FRAME   
#undef HOLDING_PERIOD   
//+------------------------------------------------------------------+

Para comenzar con nuestra prueba retrospectiva, primero seleccionaremos el período de entrenamiento. Nuestra prueba se desarrollará desde el 1 de enero de 2022 hasta el 1 de mayo de 2025. Tenga en cuenta que realizaremos este ejercicio en el marco temporal diario.

Figura 1: Los días del backtest que seleccionamos para nuestra medición de referencia. 

Nuestra simulación nos proporcionará pruebas sólidas si configuramos nuestros retrasos en "Retraso aleatorio". Esto simula la latencia del mercado real, el deslizamiento y otros factores que generan tensión al operar con cuentas reales.

Figura 2: Configuración de la prueba que utilizamos para nuestra prueba retrospectiva. 

La curva de capital que se muestra en la Figura 3 a continuación es el objetivo que debe reproducir la clase de estrategia de negociación RSI.

Figura 3: Curva de capital generada por la versión programada de nuestra estrategia de negociación. 

Podemos consultar estadísticas detalladas sobre el rendimiento de nuestra estrategia de referencia. Queremos que nuestra clase obtenga las mismas estadísticas al ser evaluada durante el mismo período, para demostrar que el método es sólido.


Figura 4: Estadísticas detalladas del rendimiento de nuestra estrategia de negociación. 

A continuación, tenemos que aplicar la misma prueba para determinar si nuestra clase de estrategia RSI es válida. Por lo tanto, la mayoría de los elementos globales definidos previamente, como las constantes del sistema y las variables globales, permanecerán prácticamente sin cambios. Las principales modificaciones que introduciremos en la aplicación incluyen la inicialización de nuestra clase de estrategia durante la fase de inicialización del Asesor Experto. 

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Time\Time.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>
#include <VolatilityDoctor\Strategies\RSIMidPoint.mqh>

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+

//--- Custom Types
CTrade               Trade;
TradeInfo            *TradeInformation;
Time                 *TradeTime;
RSIMidPoint          *RSIMid;

//--- System Types
int    position_timer;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),RSI_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),RSI_TIME_FRAME);
   RSIMid           = new RSIMidPoint(Symbol(),RSI_TIME_FRAME,RSI_PERIOD,RSI_PRICE);

//--- Everything was fine
   return(INIT_SUCCEEDED);
  }
//--- End of OnInit Scope
Además, debemos actualizar la función OnDeinit para garantizar que el objeto recién creado se elimine correctamente.
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete the dynamic objects
   delete TradeTime;
   delete TradeInformation;
   delete RSIMid;
  }
//--- End of Deinit Scope
Por último, debemos asegurarnos de que el método CheckSignal se invoque correctamente.
//+------------------------------------------------------------------+
//| Check for a trading signal using our cross-over strategy         |
//+------------------------------------------------------------------+
void CheckSignal(void)
  {
//--- Long positions when RSI is below 50
   if(RSIMid.BuySignal())
     {
      Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
      return;
     }

//--- Otherwise short positions when the RSI is above 50
   else
      if(RSIMid.SellSignal())
        {
         Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
         return;
        }
  }
//--- End of CheckSignal Scope

Ahora procederemos a probar la clase de estrategia RSI que hemos creado, durante el mismo período que utilizamos en nuestra prueba de referencia anterior.

Figura 5: Selección de las fechas de prueba para nuestra versión de clase de la estrategia de negociación RSI. 

Ambas pruebas se realizaron bajo condiciones de "Retraso aleatorio" como se especifica en la Figura 2, y produjeron curvas de capital similares bajo la configuración de la prueba retrospectiva.

Figura 6: La curva de capital obtenida por la versión de clase de nuestra estrategia de negociación coincide con la línea base. 

Además, ambas pruebas obtuvieron las mismas estadísticas en el análisis detallado. Esto indica que hemos implementado la clase correctamente y sin errores. Por lo tanto, procederemos ahora a fusionar las clases de estrategias de Media Móvil e Índice de Fuerza Relativa.

Figura 7: Estadísticas detalladas que analizan el rendimiento de la versión de clase de nuestra estrategia de negociación en comparación con la línea base. 

Ahora que hemos comprobado que nuestras versiones de la estrategia de negociación, tanto la basada en clases como la codificada, producen resultados idénticos, podemos proceder a fusionar la estrategia de cruce de medias móviles con la estrategia basada en el RSI para crear una nueva estrategia combinada. Primero, debemos seleccionar algunos parámetros que mantendremos fijos. Es importante controlar el crecimiento del espacio de parámetros, ya que un espacio de parámetros demasiado grande puede hacer que una optimización significativa sea prácticamente imposible. 

//+------------------------------------------------------------------+
//|                                                   MSA Test 1.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
//--- Fix any parameters that can afford to remain fixed
#define MA_SHIFT         0
#define MA_TYPE          MODE_EMA
#define RSI_PRICE        PRICE_CLOSE

Además, ahora que estamos ejecutando dos estrategias simultáneamente, debemos definir cómo interactuarán estas estrategias dentro de MQL5. Para gestionar esto, podemos utilizar una enumeración para definir la lógica que puede existir en varios estados relacionados.

En consecuencia, hemos creado un enumerador llamado Modo Estrategia, que tiene tres configuraciones posibles. La primera configuración, representada por 0, es la política de votación. Según esta política, una operación solo se realiza si tanto la estrategia de media móvil como la del RSI coinciden en la misma dirección. En otras palabras, ambas estrategias deben indicar una posición larga para que se abra una operación de compra.

El primer modo alternativo asigna a la estrategia RSI la responsabilidad de abrir posiciones largas, mientras que la estrategia de media móvil gestiona las posiciones cortas. El modo final invierte esta asignación, otorgando la responsabilidad de las operaciones de compra a la estrategia de media móvil y la responsabilidad de las operaciones de venta a la estrategia RSI.

//+------------------------------------------------------------------+
//| User defined enumerator                                          |
//+------------------------------------------------------------------+

enum STRATEGY_MODE
  {
   MODE_ONE   = 0, //Voting Policy
   MODE_TWO   = 1, //RSI Buy & MA Sell
   MODE_THREE = 2  //MA Sell & RSI Buy
  };
Al estructurar la estrategia de esta manera, hemos reducido el número total de parámetros de entrada a un conjunto manejable de cinco. A partir de ahí, cargamos las dependencias previamente comentadas e inicializamos las variables con las que ya estamos familiarizados. 

//+------------------------------------------------------------------+
//| User Inputs                                                      |
//+------------------------------------------------------------------+
input   group          "Moving Average Strategy Parameters"
input   int             MA_PERIOD                       =        10;//Moving Average Period


input   group          "RSI Strategy Parameters"
input   int             RSI_PERIOD                      =         15;//RSI Period

input   group          "Global Strategy Parameters"
input   ENUM_TIMEFRAMES STRATEGY_TIME_FRAME             = PERIOD_D1;//Strategy Timeframe
input   int             HOLDING_PERIOD                  =         5;//Position Maturity Period
input   STRATEGY_MODE   USER_MODE                       =         0;//Operation Mode For Our Strategy

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Time\Time.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>
#include <VolatilityDoctor\Strategies\OpenCloseMACrossover.mqh>
#include <VolatilityDoctor\Strategies\RSIMidPoint.mqh>

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+

//--- Custom Types
CTrade               Trade;
Time                 *TradeTime;
TradeInfo            *TradeInformation;
RSIMidPoint          *RSIMid;
OpenCloseMACrossover *MACross;

//--- System Types
int                  position_timer;

Durante la función de inicialización, cargamos las mismas clases que se utilizan a lo largo de todo el proceso de desarrollo. Cuando se finaliza el Asesor Experto, nos aseguramos de que todos los objetos creados dinámicamente se eliminen correctamente.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),STRATEGY_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),STRATEGY_TIME_FRAME);
   MACross          = new OpenCloseMACrossover(Symbol(),STRATEGY_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE);
   RSIMid           = new RSIMidPoint(Symbol(),STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE);
//--- Everything was fine
   return(INIT_SUCCEEDED);
  }
//--- End of OnInit Scope

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete the dynamic objects
   delete TradeTime;
   delete TradeInformation;
   delete MACross;
   delete RSIMid;
  }
//--- End of Deinit Scope

En la función OnTick, aplicamos la misma lógica que presentamos anteriormente. Dado que esta lógica ya se ha tratado en profundidad, la función OnTick se omite en esta sección de la discusión. La función de actualización se ha modificado ligeramente, ya que ahora llama a dos métodos de actualización independientes, uno para cada estrategia. Por último, el método CheckSignal ha experimentado el cambio más significativo. Ahora, antes de iniciar cualquier operación, debe determinar primero el modo de estrategia actual.

//+------------------------------------------------------------------+
//| Update our technical indicators                                  |
//+------------------------------------------------------------------+
void Update(void)
  {
//--- Update the strategy
   RSIMid.Update();
   MACross.Update();
  }
//--- End of Update Scope

//+------------------------------------------------------------------+
//| Check for a trading signal using our cross-over strategy         |
//+------------------------------------------------------------------+
void CheckSignal(void)
  {

//--- Both Stratetgies Should Cast The Same Vote
   if(USER_MODE == 0)
     {
      //--- Long positions when the close moving average is above the open
      if(MACross.BuySignal() && RSIMid.BuySignal())
        {
         Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
         return;
        }

      //--- Otherwise short
      else
         if(MACross.SellSignal()  && RSIMid.SellSignal())
           {
            Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
            return;
           }
     }

//--- RSI Opens All Long Positions & The Moving Average Opens Short Positions
   else
      if(USER_MODE == 1)
        {

         if(RSIMid.BuySignal())
           {
            Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
            return;
           }

         //--- Otherwise short
         else
            if(MACross.SellSignal())
              {
               Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
               return;
              }
        }

      //--- RSI Opens All Short Positions & The Moving Average Opens Long Positions
      else
         if(USER_MODE == 2)
           {

            if(MACross.BuySignal())
              {
               Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
               return;
              }

            //--- Otherwise short
            else
               if(RSIMid.SellSignal())
                 {
                  Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
                  return;
                 }
           }

  }
//--- End of CheckSignal Scope

Finalmente, una vez ejecutada toda la lógica, eliminamos la definición de todas las constantes del sistema para concluir el programa de forma limpia.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef MA_SHIFT
#undef RSI_PRICE
#undef MA_TYPE
//+------------------------------------------------------------------+

Comencemos ahora a buscar la configuración de parámetros adecuada para nuestra aplicación. Seleccione la aplicación que acabamos de crear y, a continuación, establezca el campo "Adelante" en "1/2". Esto permitirá que nuestro optimizador genético utilice la mitad de los datos históricos que tenemos para aprender buenos parámetros, y la otra mitad permanecerá oculta para el optimizador y se utilizará para validar la configuración que el optimizador consideró viable. 

Figura 8: Configuración de nuestra prueba directa.

Después de configurar "Retraso aleatorio" como lo hicimos en las 2 pruebas anteriores, recuerde que esta vez también queremos seleccionar un optimizador. Si dispone de mayor capacidad de procesamiento, no dude en probar también las demás optimizaciones que se ofrecen, como la opción "Lenta (repaso completo de parámetros)".

Figura 9: Seleccione la opción "Rápida (algoritmo genético)" en el campo Optimización.

Seleccione la pestaña “Parámetros de entrada” en la parte inferior de la pantalla para que aparezca el panel que controla los parámetros de nuestra estrategia que se van a probar. Seleccione la casilla de verificación junto a cada parámetro y, a continuación, elija una ubicación de inicio y una ubicación final para los valores que se van a buscar. Tenga en cuenta que el parámetro de paso también es crucial y debe tenerse en cuenta. Si elige un tamaño de paso de 1, sus resultados serán muy detallados; sin embargo, como consecuencia, el número total de pasos necesarios podría ser astronómico. En general, se recomiendan intervalos de 2, 4, 5 o incluso 10 para búsquedas especialmente grandes.

Figura 10: Selección de los intervalos de ajuste sobre los que se deben buscar nuestros parámetros.

Cuando inicie la prueba, los resultados de la prueba forward aparecerán como un diagrama de dispersión, donde cada punto del gráfico representa una prueba completa realizada con un conjunto único de parámetros de estrategia.

Figura 11: Podemos visualizar los resultados producidos por el probador de estrategias como un diagrama de dispersión bidimensional, con el beneficio representado en el eje Y.

Nuestra estrategia siguió siendo rentable incluso fuera de la muestra de entrenamiento que nuestro algoritmo genético utilizó para seleccionar estos ajustes. Este es un buen indicador de la posible estabilidad futura si decidimos aplicar el algoritmo a capital real.

Figura 12: La curva de capital generada por nuestra estrategia de negociación presenta una tendencia positiva fuera de la muestra.

Nuestro nuevo sistema es mucho más rentable que la versión anterior. Podemos ver claramente que la combinación "Voting Policy" parece ser la más estable. 

Figura 13: El rendimiento de nuestra aplicación mejoró después de incorporarle una estrategia adicional.

Estos fueron los mejores resultados que obtuvimos de los resultados de Forward en nuestra primera discusión. Nuestro nuevo sistema está superando en rendimiento a la versión anterior que teníamos, la cual solo tenía en cuenta una estrategia.

Figura 14: Los mejores resultados que obtuvimos cuando nuestra aplicación solo tenía una estrategia.



Conclusión

Este artículo ha guiado al lector a través de los numerosos beneficios de aplicar los principios de la programación orientada a objetos (POO) en MQL5, combinados con la creatividad humana. Ha demostrado que existen múltiples maneras de integrar diferentes estrategias en un sistema cohesionado. La programación orientada a objetos es una herramienta potente y versátil para este tipo de proyectos creativos, ya que permite un desarrollo estructurado, controlado y predecible.

Partiendo de los conceptos fundamentales presentados aquí, los lectores ahora están capacitados para perfeccionar y optimizar sus propias estrategias de trading. Gracias al uso del Probador de Estrategias integrado en MetaTrader 5, los lectores pueden comenzar a probar y mejorar sus implementaciones.

Este debate también ha puesto de relieve la importancia de minimizar el número de parámetros de entrada en una estrategia. Una estrategia con demasiados parámetros resulta cada vez más difícil de optimizar eficazmente. Si bien es posible delegar parte de la carga computacional a los servicios de MQL5 Cloud Network, mantener la simplicidad en el diseño suele conducir a mejores resultados y estrategias más sólidas.

Al aplicar los principios analizados en este artículo, los lectores deberían disponer ahora de conocimientos prácticos para crear sus propios sistemas de negociación basados en conjuntos de modelos. Esperamos contar con su continua participación en futuras discusiones, donde introduciremos una tercera capa en nuestra estrategia de trading. Este siguiente paso nos ayudará a construir una base sólida para futuros modelos estadísticos.

Nombre del archivo Descripción del archivo 
MSA Test 2 Baseline.mq5 Versión de prueba codificada de nuestra estrategia de trading RSI. 
MSA Test 2 Class.mq5 Versión de prueba de nuestra clase de estrategia RSI.
MSA Test 2.ex5 Versión completa de nuestra estrategia de trading. 
MSA Test 2.mq5 Código fuente de la aplicación de conjunto MA y RSI que desarrollamos.
RSIMidPoint.mqh Clase MQL5 para nuestra estrategia de punto medio del RSI. 

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/18471

Archivos adjuntos |
MSA_Test_2.ex5 (47.2 KB)
MSA_Test_2.mq5 (7.64 KB)
RSIMidPoint.mqh (3.51 KB)
Del básico al intermedio: Sobrecarga de operadores (IV) Del básico al intermedio: Sobrecarga de operadores (IV)
En este artículo, haremos una primera aproximación para mostrar cómo podemos implementar la sobrecarga del operador de subíndice y también del operador de asignación, intentando ofrecer un enfoque práctico e interesante para todos. Lo que veremos aquí es solo una parte de lo que aún pretendo mostrar y que está directamente relacionado con la sobrecarga de estos operadores.
Simulación de mercado: La unión hace la fuerza (II) Simulación de mercado: La unión hace la fuerza (II)
Hasta ahora, la aplicación desarrollada a lo largo de esta serie de artículos se centraba exclusivamente en simular la parte gráfica. Sin embargo, para disponer de un sistema más completo, en el que podamos probar un Asesor Experto dentro del servicio de repetición/simulador, también debemos simular el servidor de trading. Observarás que esta simulación incluirá únicamente los elementos imprescindibles. No obstante, tú, estimado lector, podrás completar las partes que falten. Como esos componentes adicionales no influyen en lo que pretendo mostrar, ya contamos con más que suficiente para desarrollar lo que hemos planificado.
Simulación de mercado: La unión hace la fuerza (III) Simulación de mercado: La unión hace la fuerza (III)
En este artículo presentaré nuestro sistema para simular operaciones a mercado. Aunque está prácticamente terminado, todavía quedan algunas cosas por implementar y unos pocos cambios por realizar. Sin embargo, confieso que, después de todo lo que ya hemos desarrollado, estoy cansado de seguir atrapado en la implementación de este sistema.
Del básico al intermedio: Sobrecarga de operadores (III) Del básico al intermedio: Sobrecarga de operadores (III)
En este artículo veremos cómo implementar la sobrecarga tanto de operadores lógicos como de operadores relacionales. Hacerlo requiere cierto cuidado y una buena dosis de atención. Un simple descuido al implementar la sobrecarga de estos operadores puede hacer que todo el código termine siendo completamente inservible. Si la sobrecarga presenta algún problema, toda una base de datos creada a partir de los resultados generados por el código deberá descartarse por completo o, como mínimo, revisarse íntegramente.