Recetas MQL5 - Señales comerciales de pivotes

Denis Kirichenko | 24 abril, 2017


Introducción

El presente artículo continúa la serie en la que se describen los indicadores y configuraciones que generan las señales comerciales. Esta vez, hablaremos de los pivotes, niveles o puntos de reversa. Usaremos de nuevo la Librería estándar. Primero, trabajaremos con el indicador de los niveles de reversa, y luego basándose en él, crearemos una estrategia base que seguiremos modificando.

Es deseable que el lector conozca la clase base para la creación de los generadores de señales comerciales CExpertSignal.


1. Indicador de pivotes — niveles de reversa

Para la estrategia va a usarse el indicador que dibuja los niveles de una posible reversa. En este caso, el dibujado va a realizarse sólo a través de los medios de la construcción gráfica, sin usar los objetos gráficos. La ventaja principal de este enfoque es la posibilidad de acceder al indicador en el modo de optimización. Su desventaja consiste en el hecho de que las construcciones gráficas no pueden salir fuera de los límites de los búferes indicadores, lo que significa que en las barras no habrá líneas en el futuro.

Los niveles se puede calcular de varias maneras diferentes. Para aclarar mejor este asunto, lea el artículo «Estrategia de trading basadas en el análisis de los puntos pivote (Pivot Points)».

Por ahora, nos centraremos en el enfoque clásico cuando los niveles se definen de acuerdo con las fórmulas siguientes:



RES es el nivel i de resistencia, y SUP es el nivel i de soporte. En total habrá: 1 nivel base de reversa (PP), 6 niveles de resistencia (RES) y 6 niveles de soporte (SUP).

Pues, el indicador tiene aspecto de un conjunto de niveles horizontales construidos a base de diferentes precios. La primera ejecución del indicador en el gráfico construirá los niveles solamente para el día actual (Fig. 1).


Fig. 1 Indicador de pivotes: dibujado para el día actual

Fig. 1 Indicador de pivotes: dibujado para el día actual


Examinaremos el código del indicador bloque por bloque, empezando de la parte de cálculo.

Entonces, cuando empieza un día nuevo, es necesario volver a calcular todos los niveles de reversa.

//--- si hay un día nuevo
   if(gNewDay.isNewBar(today))
     {
      PrintFormat("Nuevo día: %s",TimeToString(today));
      //--- normalización de precios
      double d_high=NormalizeDouble(daily_rates[0].high,_Digits);
      double d_low=NormalizeDouble(daily_rates[0].low,_Digits);
      double d_close=NormalizeDouble(daily_rates[0].close,_Digits);
      //--- memorizar los precios
      gYesterdayHigh=d_high;
      gYesterdayLow=d_low;
      gYesterdayClose=d_close;
      //--- 1) pivote: PP = (HIGH + LOW + CLOSE) / 3        
      gPivotVal=NormalizeDouble((gYesterdayHigh+gYesterdayLow+gYesterdayClose)/3.,_Digits);
      //--- 4) RES1.0 = 2*PP - LOW
      gResVal_1_0=NormalizeDouble(2.*gPivotVal-gYesterdayLow,_Digits);
      //--- 5) SUP1.0 = 2*PP – HIGH
      gSupVal_1_0=NormalizeDouble(2.*gPivotVal-gYesterdayHigh,_Digits);
      //--- 8) RES2.0 = PP + (HIGH -LOW)
      gResVal_2_0=NormalizeDouble(gPivotVal+(gYesterdayHigh-gYesterdayLow),_Digits);
      //--- 9) SUP2.0 = PP - (HIGH – LOW)
      gSupVal_2_0=NormalizeDouble(gPivotVal-(gYesterdayHigh-gYesterdayLow),_Digits);
      //--- 12) RES3.0 = 2*PP + (HIGH – 2*LOW)
      gResVal_3_0=NormalizeDouble(2.*gPivotVal+(gYesterdayHigh-2.*gYesterdayLow),_Digits);
      //--- 13) SUP3.0 = 2*PP - (2*HIGH – LOW)
      gSupVal_3_0=NormalizeDouble(2.*gPivotVal-(2.*gYesterdayHigh-gYesterdayLow),_Digits);
      //--- 2) RES0.5 = (PP + RES1.0) / 2
      gResVal_0_5=NormalizeDouble((gPivotVal+gResVal_1_0)/2.,_Digits);
      //--- 3) SUP0.5 = (PP + SUP1.0) / 2
      gSupVal_0_5=NormalizeDouble((gPivotVal+gSupVal_1_0)/2.,_Digits);
      //--- 6) RES1.5 = (RES1.0 + RES2.0) / 2
      gResVal_1_5=NormalizeDouble((gResVal_1_0+gResVal_2_0)/2.,_Digits);
      //--- 7) SUP1.5 = (SUP1.0 + SUP2.0) / 2
      gSupVal_1_5=NormalizeDouble((gSupVal_1_0+gSupVal_2_0)/2.,_Digits);
      //--- 10) RES2.5 = (RES2.0 + RES3.0) / 2
      gResVal_2_5=NormalizeDouble((gResVal_2_0+gResVal_3_0)/2.,_Digits);
      //--- 11) SUP2.5 = (SUP2.0 + SUP3.0) / 2
      gSupVal_2_5=NormalizeDouble((gSupVal_2_0+gSupVal_3_0)/2.,_Digits);
      //--- barra del inicio del día actual
      gDayStart=today;
      //--- encontrar la barra inicial del timeframe activo
      //--- como serie temporal
      for(int bar=0;bar<rates_total;bar++)
        {
         //--- hora de la barra seleccionada
         datetime curr_bar_time=time[bar];
         user_date.DateTime(curr_bar_time);
         //--- día de la barra seleccionada
         datetime curr_bar_time_of_day=user_date.DateOfDay();
         //--- si la barra actual fue el día anterior
         if(curr_bar_time_of_day<gDayStart)
           {
            //--- fijar la barra inicial
            gBarStart=bar-1;
            break;
           }
        }
      //--- resetear el contador local
      prev_calc=0;
       }

He utilizado el color rojo para destacar las líneas donde exactamente se recalculan los niveles. Luego hay que encontrar el número de la barra para el timeframe actual, que servirá de inicio para el dibujado de los niveles. La variable gBarStart se encarga de su valor. Durante la búsqueda, se activa la estructura SUserDateTimepara trabajar con fechas y horas, siendo el descendiente de la estructura CDateTime.

Ahora diré algunas palabras sobre el bloque donde se realiza el llenado de los valores de búfer para las barras del timeframe actual.

//--- si hay nueva barra en el timeframe actual
   if(gNewMinute.isNewBar(time[0]))
     {
      //--- hasta que barra contamos 
      int bar_limit=gBarStart;
      //--- si no es la primer ejecución
      if(prev_calc>0)
         bar_limit=rates_total-prev_calc;
      //--- cálculo de búferes 
      for(int bar=0;bar<=bar_limit;bar++)
        {
         //--- 1) пивот
         gBuffers[0].data[bar]=gPivotVal;
         //--- 2) RES0.5
         if(gToPlotBuffer[1])
            gBuffers[1].data[bar]=gResVal_0_5;
         //--- 3) SUP0.5
         if(gToPlotBuffer[2])
            gBuffers[2].data[bar]=gSupVal_0_5;
         //--- 4) RES1.0
         if(gToPlotBuffer[3])
            gBuffers[3].data[bar]=gResVal_1_0;
         //--- 5) SUP1.0
         if(gToPlotBuffer[4])
            gBuffers[4].data[bar]=gSupVal_1_0;
         //--- 6) RES1.5
         if(gToPlotBuffer[5])
            gBuffers[5].data[bar]=gResVal_1_5;
         //--- 7) SUP1.5
         if(gToPlotBuffer[6])
            gBuffers[6].data[bar]=gSupVal_1_5;
         //--- 8) RES2.0
         if(gToPlotBuffer[7])
            gBuffers[7].data[bar]=gResVal_2_0;
         //--- 9) SUP2.0
         if(gToPlotBuffer[8])
            gBuffers[8].data[bar]=gSupVal_2_0;
         //--- 10) RES2.5
         if(gToPlotBuffer[9])
            gBuffers[9].data[bar]=gResVal_2_5;
         //--- 11) SUP2.5
         if(gToPlotBuffer[10])
            gBuffers[10].data[bar]=gSupVal_2_5;
         //--- 12) RES3.0
         if(gToPlotBuffer[11])
            gBuffers[11].data[bar]=gResVal_3_0;
         //--- 13) SUP3.0
         if(gToPlotBuffer[12])
            gBuffers[12].data[bar]=gSupVal_3_0;
        }
       }

El cálculo de los búferes empieza cuando aparece nueva barra en el gráfico en el que ha sido iniciado el indicador. Con el color amarillo se marca la definición del número de la barra hasta que vamos a calcular los búferes. Para eso se utiliza el contador local de barras contadas. Lo necesitamos porque el inicio del nuevo día no reseteará el valor de la constante prev_calculated a cero, mientras que este reseteo es necesario.

El código completo del indicador de pivotes se encuentra en el archivo Pivots.mq5.


2. Estrategia básica

Intentaremos crear una estrategia básica simple basándose en el indicador descrito. Supongamos que la señal de apertura depende de la posición del precio de apertura del día respecto al pivote central. Cuando el precio toque el nivel del pivote, la señal se quedará confirmada.

Así, en el gráfico EURUSD, M15 ( Рис.2 ) se muestra la situación cuando la sesión de 14 de enero de 2015 fue abierta por debajo del pivote central, y luego el precio intradía tocó su nivel de abajo arriba. De esta manera, apareció una señal de venta. Si no se ha activado el Stop Loss ni el Take Profit, vamos a cerrar al comenzar el nuevo día.

Fig. 2. Estrategia básica: señal de venta

Fig. 2. Estrategia básica: señal de venta


Los niveles de Stop Loss y Take Profit los vamos a vincular a los niveles de reversa del indicador de pivote. Del Stop para la venta nos servirá el nivel intermedio de resistencia Res0.5 ubicado en $1.18153. Como el Stop para el beneficio, usaremos el nivel base de soporte Sup1.0 en $1.17301 . Más tarde volveremos a la sesión de 14 de enero. Mientras tanto hablaremos del código que será la base algorítmica de la estrategia básica.


2.1 Clase de señal CSignalPivots

Crearemos la clase de señal que va a generar las señales de parte de varios modelos formados a base de la dinámica de precios y el indicador de los niveles de reversa.

//+------------------------------------------------------------------+
//| Class CSignalPivots                                              |
//| Purpose: Clase de señales comerciales a base de pivotes.         |
//| Descendiente de la clase CExpertSignal.                          |
//+------------------------------------------------------------------+
class CSignalPivots : public CExpertSignal
  {
   //--- === Data members === --- 
protected:
   CiCustom          m_pivots;            // objeto-indicador "Pivots"   
   //--- parámetros personalizados
   bool              m_to_plot_minor;     // dibujado de niveles secundarios
   double            m_pnt_near;          // tolerancia 
   //--- de cálculo
   double            m_pivot_val;         // valor del pivote
   double            m_daily_open_pr;     // precio de apertura del día actual   
   CisNewBar         m_day_new_bar;       // nueva barra del timeframe diario
   //--- modelos del mercado  
   //--- 1) Modelo 0 «primer toque del nivel PP» (encima - buy, abajo - sell)
   int               m_pattern_0;         // peso
   bool              m_pattern_0_done;    // indicio del modelo modificado
   //--- === Methods === --- 
public:
   //--- constructor/destructor
   void              CSignalPivots(void);
   void             ~CSignalPivots(void){};
   //--- métodos del establecimiento de parámetros personalizados
   void              ToPlotMinor(const bool _to_plot) {m_to_plot_minor=_to_plot;}
   void              PointsNear(const uint _near_pips);
   //--- métodos de configuración de los «pesos» de los modelos de mercado
   void              Pattern_0(int _val) {m_pattern_0=_val;m_pattern_0_done=false;}
   //--- método de verificación de configuraciones
   virtual bool      ValidationSettings(void);
   //--- método de la creación del indicador y serie temporal
   virtual bool      InitIndicators(CIndicators *indicators);
   //--- métodos de verificación, si los modelos del mercado están formados
   virtual int       LongCondition(void);
   virtual int       ShortCondition(void);
   virtual double    Direction(void);
   //--- métodos de definición de los niveles de la entrada en el mercado
   virtual bool      OpenLongParams(double &price,double &sl,double &tp,datetime &expiration);
   virtual bool      OpenShortParams(double &price,double &sl,double &tp,datetime &expiration);
   //---
protected:
   //--- método de inicialización del indicador
   bool              InitCustomIndicator(CIndicators *indicators);
   //--- obtención del valor del nivel del pivote
   double            Pivot(void) {return(m_pivots.GetData(0,0));}
   //--- obtención del valor del nivel principal de resistencia
   double            MajorResistance(uint _ind);
   //--- obtención del valor del nivel secudario de resistencia
   double            MinorResistance(uint _ind);
   //--- obtención del valor del nivel principal de soporte
   double            MajorSupport(uint _ind);
   //--- obtención del valor del nivel secundario de soporte
   double            MinorSupport(uint _ind);
  };
  //+------------------------------------------------------------------+


En mi artículo «Recetas MQL5 - Señales comerciales de los canales móviles» ya utilizaba el enfoque cuando el contacto del precio con una línea se fija, si el precio entra en los alrededores de esta línea. El miembro de datos m_pnt_near definirá la tolerancia para el nivel de reversa.

Tal vez el papel más importante en la clase deba asignarse al modelo de señal que se atiende por esta clase. En la clase base habrá un modelo. Además del peso (m_pattern_0), también va a tener el indicio del procesamiento durante el día de trading (m_pattern_0_done).

La clase base de señal CExpertSignal abunda de métodos virtuales. Y esta riqueza permite implementar una configuración fina de la clase derivada.

En particular, para calcular los niveles de trading, he redefinido 2 métodos OpenLongParams() y OpenShortParams().

Examinaremos el código del primer método: determinación de los valores para los niveles comerciales durante la compra.

//+------------------------------------------------------------------+
//| Determinación de niveles comerciales para la compra              |
//+------------------------------------------------------------------+
bool CSignalPivots::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration)
  {
   bool params_set=false;
   sl=tp=WRONG_VALUE;
//--- si el Modelo 0 se considera
   if(IS_PATTERN_USAGE(0))
      //--- si el Modelo 0 no está procesado
      if(!m_pattern_0_done)
        {
         //--- precio de apertura - al mercado
         double base_price=m_symbol.Ask();
         price=m_symbol.NormalizePrice(base_price-m_price_level*PriceLevelUnit());
         //--- precio sl - nivel Sup0.5
         sl=this.MinorSupport(0);
         if(sl==DBL_MAX)
            return false;
         //--- si se establece el precio sl
         sl=m_symbol.NormalizePrice(sl);
         //--- precio tp - nivel Res1.0         
         tp=this.MajorResistance(0);
         if(tp==DBL_MAX)
            return false;
         //--- si se establece el precio tp
         tp=m_symbol.NormalizePrice(tp);
         expiration+=m_expiration*PeriodSeconds(m_period);
         //--- si los precios están establecidos
         params_set=true;
         //--- modelo procesado
         m_pattern_0_done=true;
        }
//---
   return params_set;
  }
  //+------------------------------------------------------------------+


El precio de Stop Loss se calcula como el valor del primer nivel secundario de soporte usando el método MinorSupport(). El Profit se define según el precio del primer nivel base de resistencia usando a través del método MajorResistance(). Para la venta, estos métodos serán sustituidos por MinorResistance() y MajorSupport(), respectivamente.

Para que se activen los métodos de definición de niveles comerciales en la clase derivada, hay que hacer que la señal personalizada sea principal. El método de definición de los niveles comerciales de la clase principal tiene el siguiente aspecto:

//+------------------------------------------------------------------+
//| Detecting the levels for buying                                  |
//+------------------------------------------------------------------+
bool CExpertSignal::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration)
  {
   CExpertSignal *general=(m_general!=-1) ? m_filters.At(m_general) : NULL;
//---
   if(general==NULL)
     {
      //--- if a base price is not specified explicitly, take the current market price
      double base_price=(m_base_price==0.0) ? m_symbol.Ask() : m_base_price;
      price      =m_symbol.NormalizePrice(base_price-m_price_level*PriceLevelUnit());
      sl         =(m_stop_level==0.0) ? 0.0 : m_symbol.NormalizePrice(price-m_stop_level*PriceLevelUnit());
      tp         =(m_take_level==0.0) ? 0.0 : m_symbol.NormalizePrice(price+m_take_level*PriceLevelUnit());
      expiration+=m_expiration*PeriodSeconds(m_period);
      return(true);
     }
//---
   return(general.OpenLongParams(price,sl,tp,expiration));
  }
  //+------------------------------------------------------------------+

Si el índice de la señal principal no está establecido, los niveles obtendrán los valores predefinidos. Para evitar esta situación, durante la inicialización de la señal se puede escribir lo siguiente en el código del Asesor Experto:

//--- filtro CSignalPivots
   CSignalPivots *filter0=new CSignalPivots;
   if(filter0==NULL)
     {
      //--- error
      PrintFormat(__FUNCTION__+": error creating filter0");
      return INIT_FAILED;
     }
   signal.AddFilter(filter0);
     signal.General(0);  


El método de comprobación de la condición de compra está presentado como sigue:

//+------------------------------------------------------------------+
//| Comprobación de la condición de compra                           |
//+------------------------------------------------------------------+
int CSignalPivots::LongCondition(void)
  {
   int result=0;
//--- si el Modelo 0 se considera
   if(IS_PATTERN_USAGE(0))
      //--- si el Modelo 0 no está procesado
      if(!m_pattern_0_done)
         //--- si el día se ha abierto encima del pivote
         if(m_daily_open_pr>m_pivot_val)
           {
            //--- precio mínimo en la barra actual
            double last_low=m_low.GetData(1);
            //--- si el precio se ha obtenido
            if((last_low>WRONG_VALUE) && (last_low<DBL_MAX))
               //--- si el toque ha sido arriba (teniendo en cuenta la tolerancia)
               if(last_low<=(m_pivot_val+m_pnt_near))
                 {
                  result=m_pattern_0;
                  //--- en el Diario
                  Print("\n---== El precio toca el nivel del pivote arriba ==---");
                  PrintFormat("Precio: %0."+IntegerToString(m_symbol.Digits())+"f",last_low);
                  PrintFormat("Pivote: %0."+IntegerToString(m_symbol.Digits())+"f",m_pivot_val);
                  PrintFormat("Tolerancia: %0."+IntegerToString(m_symbol.Digits())+"f",m_pnt_near);
                 }
           }
//---
   return result;
  }
  //+------------------------------------------------------------------+

Es fácil de notar que el toque arriba se comprueba tomando en cuenta la tolerancia last_low<=(m_pivot_val+m_pnt_near).

El método de definición de la dirección «ponderada» Direction(), entre otras cosas, comprueba el procesamiento del modelo base.

//+------------------------------------------------------------------+
//| Definición de la dirección «ponderada»                           |
//+------------------------------------------------------------------+
double CSignalPivots::Direction(void)
  {
   double result=0.;
//--- obtener datos históricos diarios
   MqlRates daily_rates[];
   if(CopyRates(_Symbol,PERIOD_D1,0,1,daily_rates)<0)
      return 0.;
//--- si el Modelo 0 ha sido procesado
   if(m_pattern_0_done)
     {
      //--- comprobar la aparición de nuevo día
      if(m_day_new_bar.isNewBar(daily_rates[0].time))
        {
         //--- resetear la bandera del procesamiento del modelo
         m_pattern_0_done=false;
         return 0.;
        }
     }
//--- si el Modelo 0 no ha sido procesado
   else
     {
      //--- precio de apertura del día
      if(m_daily_open_pr!=daily_rates[0].open)
         m_daily_open_pr=daily_rates[0].open;
      //--- pivote
      double curr_pivot_val=this.Pivot();
      if(curr_pivot_val<DBL_MAX)
         if(m_pivot_val!=curr_pivot_val)
            m_pivot_val=curr_pivot_val;
     }
//--- resultado
   result=m_weight*(this.LongCondition()-this.ShortCondition());
//---
   return result;
  }
  //+------------------------------------------------------------------+


En cuanto a las señales para el cierre, vamos a redefinir los métodos de la clase principal CloseLongParams() y CloseShortParams(). Aquí tenemos el ejemplo del código para el bloque de la compra:

//+------------------------------------------------------------------+
//| Determinación del nivel comercial para la compra                 |
//+------------------------------------------------------------------+
bool CSignalPivots::CloseLongParams(double &price)
  {
   price=0.;
//--- si el Modelo 0 se considera
   if(IS_PATTERN_USAGE(0))
      //--- si el Modelo 0 no está procesado
      if(!m_pattern_0_done)
        {
         price=m_symbol.Bid();
         //--- en el Registro
         Print("\n---== Señal para cerrar la compra ==---");
         PrintFormat("Precio del mercado: %0."+IntegerToString(m_symbol.Digits())+"f",price);
         return true;
        }
//--- return the result
   return false;
  }
  //+------------------------------------------------------------------+

El límite de la señal para el cierre debe ser puesto a cero en el código del propio Asesor Experto.

signal.ThresholdClose(0);

Entonces, en la clase principal no habrá comprobación según la dirección.

//+------------------------------------------------------------------+
//| Generating a signal for closing of a long position               |
//+------------------------------------------------------------------+
bool CExpertSignal::CheckCloseLong(double &price)
  {
   bool   result   =false;
//--- the "prohibition" signal
   if(m_direction==EMPTY_VALUE)
      return(false);
//--- check of exceeding the threshold value
   if(-m_direction>=m_threshold_close)
     {
      //--- there's a signal
      result=true;
      //--- try to get the level of closing
      if(!CloseLongParams(price))
         result=false;
     }
//--- zeroize the base price
   m_base_price=0.0;
//--- return the result
   return(result);
  }
  //+------------------------------------------------------------------+

Surge la pregunta, ¿a base de qué en este caso va a verificarse la señal del cierre? En primer lugar, va a verificarse gracias a la presencia de la posición (en el método Processing() ), y segundo, gracias a la característica m_pattern_0_done (en los métodos redifinidos CloseLongParams() y CloseShortParams() ). En total, en cuanto el EA detecte la presencia de una posición cuando el Modelo 0 no está procesado, enseguida intentará cerrar la posoción. Eso ocurre al iniciarse la sesión de trading.

Hemos considerado los principios de la clase personalizada de señal CSignalPivots, ahora hablaremos sobre la clase de la estrategia.


2.2 Clase de la estrategia comercial CPivotsExpert

La clase derivada de la estrategia parece a la clase análoga para los canales móviles. Las diferencias son las siguientes: en primer lugar, no se utiliza el modo de trading a base de ticks, sino el modo basado en minutos. Eso permitirá testear rápidamente la estrategia a base del historial bastante profundo. En segundo lugar, hay una verificación para cerrar la posición. Más arriba ya hemos determinado la razón por la que el Asesor Experto puede cerrar la posición.

El principal método procesador es el siguiente:

//+------------------------------------------------------------------+
//| Módulo principal                                                 |
//+------------------------------------------------------------------+
bool CPivotsExpert::Processing(void)
  {
//--- nueva barra en minutos
   if(!m_minute_new_bar.isNewBar())
      return false;
//--- cálculo de dirección
   m_signal.SetDirection();
//--- si no hay posición
   if(!this.SelectPosition())
     {
      //--- módulo de apertura de posición
      if(this.CheckOpen())
         return true;
     }
//--- si hay posición
   else
     {
      //--- módulo del cierre de posición
      if(this.CheckClose())
         return true;
     }
//--- si no hay operaciones comerciales
   return false;
  }
  //+------------------------------------------------------------------+

Pues, eso es todo. Ahora podemos ejecutar la estrategia básica. Su código está disponible en el archivo BasePivotsTrader.mq5.


Fig. 3. Estrategia básica: venta

Fig. 3. Estrategia básica: venta


Volvamos al ejemplo de 14 de enero de 2015. En este caso, el modelo fue procesado perfectamente. Abrimos hacia abajo en el pivote, y cerramos en el nivel del soporte principal Sup1.0.

Fue realizado un repaso en el Probador de Estrategias de 07.01.2013 a 07.01.2017 en el período de tiempo de 15 minutos para el símbolo EURUSD con los siguientes valores de parámetros:

  • Límite de señal para la apertura, [0...100] = 10;
  • Peso, [0...1.0] = 1,0;
  • Volumen fijo = 0,1;
  • Tolerancia, pp = 15.

Al final, la estrategia tradeaba con un resultado estable. La verdad es que este resultado fue negativo (Fig. 4).

Fig.4 EURUSD: resultados de la primera estrategia básica para 2013-2016.

Fig. 4  EURUSD: resultados de la primera estrategia básica para 2013-2016.


A juzgar por el gráfico de la eficacia, lo hicimos todo erróneamente. Había que comprar cuando llegaba la señal de venta, y vender en caso de la señal de compra. ¿Es así? Intentaremos de comprobarlo. Para eso crearemos la segunda estrategia básica.

Introduciremos algunas modificaciones en la parte de las señales. Entonces, como ejemplo, la condición de compra será como sigue:

//+------------------------------------------------------------------+
//| Comprobación de la condición de venta                            |
//+------------------------------------------------------------------+
int CSignalPivots::LongCondition(void)
  {
   int result=0;
//--- si el Modelo 0 se considera
   if(IS_PATTERN_USAGE(0))
      //--- si el Modelo 0 no está procesado
      if(!m_pattern_0_done)
         //--- si el día se ha abierto por debajo del pivote
         if(m_daily_open_pr<m_pivot_val)
           {
            //--- precio máximo en la barra actual
            double last_high=m_high.GetData(1);
            //--- si el precio se ha obtenido
            if((last_high>WRONG_VALUE) && (last_high<DBL_MAX))
               //--- si el toque ha sido abajo (teniendo en cuenta la tolerancia)
               if(last_high>=(m_pivot_val-m_pnt_near))
                 {
                  result=m_pattern_0;
                  //--- en el Diario
                  Print("\n---== El precio toca el nivel del pivote abajo ==---");
                  PrintFormat("Цена: %0."+IntegerToString(m_symbol.Digits())+"f",last_high);
                  PrintFormat("Pivote: %0."+IntegerToString(m_symbol.Digits())+"f",m_pivot_val);
                  PrintFormat("Tolerancia: %0."+IntegerToString(m_symbol.Digits())+"f",m_pnt_near);
                 }
           }
//---
   return result;
  }
  //+------------------------------------------------------------------+

Volvemos a repasar la estrategia en el Probador y obtenemos el siguiente resultado:

Рис.5 EURUSD: resultados de la segunda estrategia básica para 2013-2016.

Fig. 5  EURUSD: resultados de la segunda estrategia básica para 2013-2016.

Es evidente que no hubo «efecto espejado» de la primera parte. Obviamente, eso se debe al tamaño del Stop Loss y Take Profit. Aparte de eso, con el inicio del nuevo día se cerraban las posiciones para las que no se activó el Stop ni Profit durante la sesión de trading.

Intentaremos de nuevo modificar la estrategia básica, en particular, su segunda versión. Hagamos que para la compra el nivel de Stop Loss se coloque más allá -hasta el soporte principal Sup1.0- y el tamaño del Take Profit se limite por el nivel intermedio de resistencia Res0.5. Para la venta, colocaremos el Stop en el nivel Res1.0, y el Profit, en Sup0.5.

De esta manera, por ejemplo, los niveles comerciales para la compra van a determinarse así:

//+------------------------------------------------------------------+
//| Determinación de niveles comerciales para la compra              |
//+------------------------------------------------------------------+
bool CSignalPivots::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration)
  {
   bool params_set=false;
   sl=tp=WRONG_VALUE;
//--- si el Modelo 0 se considera
   if(IS_PATTERN_USAGE(0))
      //--- si el Modelo 0 no está procesado
      if(!m_pattern_0_done)
        {
         //--- precio de apertura — al mercado
         double base_price=m_symbol.Ask();
         price=m_symbol.NormalizePrice(base_price-m_price_level*PriceLevelUnit());
         //--- precio sl - nivel Sup1.0
         sl=this.MajorSupport(0);
         if(sl==DBL_MAX)
            return false;
         //--- si se establece el precio sl
         sl=m_symbol.NormalizePrice(sl);
         //--- precio tp - nivel Res0.5         
         tp=this.MinorResistance(0);
         if(tp==DBL_MAX)
            return false;
         //--- si se establece el precio tp
         tp=m_symbol.NormalizePrice(tp);
         expiration+=m_expiration*PeriodSeconds(m_period);
         //--- si los precios están establecidos
         params_set=true;
         //--- modelo procesado
         m_pattern_0_done=true;
        }
//---
   return params_set;
  }
  //+------------------------------------------------------------------+


En el Probador, se obtendrá el siguiente resultado para la tercera versión:

Fig.6 EURUSD: resultados de la tercera estrategia básica para 2013-2016.

Fig. 6  EURUSD: resultados de la tercera estrategia básica para 2013-2016 гг.


Se ha obtenido una imagen más o menos parecida a la imagen espejada para la primera versión. A primera vista, parece que el Grial ha sido encontrado. Pero hay unos escollos que serán discutidos en la siguiente sección.


3. Sobre la cuestión de robustez

Si nos fijamos atentamente en la Fig. 6, es fácil de notar que la curva del balance iba creciendo de forma desigual. Había áreas donde el balance acumulaba el beneficio de manera constante. Había áreas de reducción.  Además, también había áreas donde la curva del balance se movía directamente a la derecha.

La robustez (robustness) es la estabilidad del sistema comercial que refleja la relativa constancia y eficacia del sistema durante un largo período de tiempo.

En general, se puede decir que a la estrategia le falta la robustez. ¿Se puede aumentarla? Lo intentaremos.


3.1 Indicador de tendencia

En mi opinión, las reglas comerciales trabajan mejor cuando en el mercado hay un movimiento dirigido, es decir una tendencia. Así, la estrategia mostraba el mejor resultado para EURUSD en 2014 y el principio de 2015, cuando el par estaba disminuyendo progresivamente.

Resulta que hace falta algún tipo del filtro que permite evitar el movimiento lateral. Respecto al tema sobre la definición de una tendencia estable hay un montón de materiales, inclusive en la sección "Artículos" en mql5.com. Personalmente a mí, me ha gustado el artículo «Distintas maneras para averiguar la tendencia en MQL5». Su autor ofrece una manera cómoda y, lo más importante, universal para buscar la tendencia.

He creado el indicador semejante MaTrendCatcher. En él se usan dos medias móviles, una rápida y otra lenta. Si la diferencia entre los valores es positiva, consideramos que la tendencia alcista. Entonces, en el indicador, las columnas del histograma serán iguales a 1. Si esta diferencia será negativa, entonces en el mercado reina una tendencia bajista. Las columnas serán iguales a menos 1 (Fig. 7).


Fig. 7. Indicador de la tendencia MaTrendCatcher

Fig. 7  Indicador de la tendencia MaTrendCatcher


Además de eso, si en relación a la barra anterior, la diferencia entre las MA se aumenta (la tendencia se fortalece), la columna será verde, si se reduce, roja.

Existe un recurso más añadido al indicador: ahí donde la diferencia entre las MA es pequeña, las columnas no van a visualizarse.El parámetro personalizado del indicador «Corte, pp» (Fig. 8) establece el grado de la diferencia necesaria para ocultar su visualización.


Fig. 8. Indicador de tendencia MaTrendCatcher, corte de diferencias pequeñas

Fig. 8  Indicador de tendencia MaTrendCatcher, corte de diferencias pequeñas


Así, para los efectos de la filtración, usaremos el indicador MaTrendCatcher.

Para activar el indicador, es necesario introducir algunas modificaciones en el código de los archivos del proyecto. Nótese que la última versión del Asesor Experto va a guardarse en la carpeta Model..

Para los efectos de la estrategia era necesario obtener el valor calculado de la dirección «ponderada». Por eso, se ha tenido que crear una clase descendiente personalizada de la clase básica de señal.

class CExpertUserSignal : public CExpertSignal

Luego, en la clase actualizada de señal para los niveles de reversa, ha aparecido un nuevo modelo— Modelo 1 «tendencia-flat-contratendencia».

La verdad es que completa el Modelo 0. Por eso, puede llamarse el submodelo. Más tarde, reflejaremos este momento en el código.

Ahora, el método para comprobar la condición de compra es el siguiente:

//+------------------------------------------------------------------+
//| Comprobación de la condición de compra                           |
//+------------------------------------------------------------------+
int CSignalPivots::LongCondition(void)
  {
   int result=0;
//--- si el Modelo 0 se considera
   if(IS_PATTERN_USAGE(0))
      //--- si el Modelo 0 no está procesado
      if(!m_pattern_0_done)
        {
         m_is_signal=false;
         //--- si el día se ha abierto por debajo del pivote
         if(m_daily_open_pr<m_pivot_val)
           {
            //--- precio máximo en la barra anterior
            double last_high=m_high.GetData(1);
            //--- si el precio se ha obtenido
            if(last_high>WRONG_VALUE && last_high<DBL_MAX)
               //--- si el toque ha sido abajo (teniendo en cuenta la tolerancia)
               if(last_high>=(m_pivot_val-m_pnt_near))
                 {
                  result=m_pattern_0;
                  m_is_signal=true;
                  //--- en el Diario
                  this.Print(last_high,ORDER_TYPE_BUY);
                 }
           }
        //--- si el Modelo 1 se considera
         if(IS_PATTERN_USAGE(1))
           {
            //--- si en la barra anterior había la tendencia alcista
            if(m_trend_val>0. && m_trend_val!=EMPTY_VALUE)
              {
               //--- si hay aceleración
               if(m_trend_color==0. && m_trend_color!=EMPTY_VALUE)
                  result+=(m_pattern_1+m_speedup_allowance);
               //--- si no hay aceleración
               else
                  result+=(m_pattern_1-m_speedup_allowance);
              }
           }
        }
//---
   return result;
    }

He marcado en verde el bloque donde el submodelo entra en el juego.

La finalidad del cálculo es la siguiente: si la entrada en el mercado ha sido realizada sin considerar el submodelo, el resultado de la señal será igual al peso del Modelo 0. Si el submodelo se toma en cuenta, las opciones son las siguientes:

  1. la entrada según la tendencia con aceleración (premio por la tendencia y premio por la aceleración);
  2. la entrada según la tendencia sin aceleración (premio por la tendencia y multa por la aceleración);
  3. la entrada contra la tendencia con aceleración (multa por la contratendencia y multa por la aceleración);
  4. la entrada contra la tendencia sin aceleración (multa por la contratendencia y premio por la aceleración);

Este enfoque permitirá no reaccionar a una señal débil. Si el peso de la señal supera el valor del límite, entonces influye en el tamaño del volumen comercial. En la clase del Asesor Experto para los pivotes existe el método CPivotsExpert::LotCoefficient():

//+------------------------------------------------------------------+
//| Coeficiente del lote                                             |
//+------------------------------------------------------------------+
double CPivotsExpert::LotCoefficient(void)
  {
   double lot_coeff=1.;
//--- señal común
   CExpertUserSignal *ptr_signal=this.Signal();
   if(CheckPointer(ptr_signal)==POINTER_DYNAMIC)
     {
      double dir_val=ptr_signal.GetDirection();
      lot_coeff=NormalizeDouble(MathAbs(dir_val/100.),2);
     }
//---
   return lot_coeff;
  }
  //+------------------------------------------------------------------+

Si la señal ha reunido 120 puntos, el volumen inicial se corregirá a 1,2, si ha reunido 70, a 0,7.

Para aplicar el coeficiente, hay que redefinir los métodos OpenLong() y OpenShort(). En particular, el método para la compra se presenta así:

//+------------------------------------------------------------------+
//| Long position open or limit/stop order set                       |
//+------------------------------------------------------------------+
bool CPivotsExpert::OpenLong(double price,double sl,double tp)
  {
   if(price==EMPTY_VALUE)
      return(false);
//--- get lot for open
   double lot_coeff=this.LotCoefficient();
   double lot=LotOpenLong(price,sl);
   lot=this.NormalLot(lot_coeff*lot);
//--- check lot for open
   lot=LotCheck(lot,price,ORDER_TYPE_BUY);
   if(lot==0.0)
      return(false);
//---
   return(m_trade.Buy(lot,price,sl,tp));
  }
  //+------------------------------------------------------------------+

La idea con la formación dinámica del tamaño del lote es bastante sencilla: cuanto más fuerte sea la señal, mayor será el riesgo.


3.2 Tamaño del intervalo

También es fácil de notar que cuando los niveles de reversa (pivotes) se encuentran cerca uno de otro, eso indica en una baja volatilidad en el mercado. Para abstenerse de operar durante estos días, ha sido introducido el parámetro «Límite del ancho, pp». El Modelo 0 (y también el submodelo) va a considerarse como procesado, si el umbral del límite no ha sido superado. Este límite se comprueba en el cuerpo del método Direction(). Aquí tenemos una parte del código:

//--- si el límite está establecido
   if(m_wid_limit>0.)
     {
      //--- límite superior de cálculo 
      double norm_upper_limit=m_symbol.NormalizePrice(m_wid_limit+m_pivot_val);
      //--- límite superior de real 
      double res1_val=this.MajorResistance(0);
      if(res1_val>WRONG_VALUE && res1_val<DBL_MAX)
        {
         //--- si el límite no está superado 
         if(res1_val<norm_upper_limit)
           {
            //--- el Modelo 0 ha sido procesado
            m_pattern_0_done=true;
            //--- en el Diario
            Print("\n---== El límite superior no está superado ==---");
            PrintFormat("De cálculo: %0."+IntegerToString(m_symbol.Digits())+"f",norm_upper_limit);
            PrintFormat("Real: %0."+IntegerToString(m_symbol.Digits())+"f",res1_val);
            //---
            return 0.;
           }
        }
      //--- límite inferior de cálculo 
      double norm_lower_limit=m_symbol.NormalizePrice(m_pivot_val-m_wid_limit);
      //--- límite inferior real 
      double sup1_val=this.MajorSupport(0);
      if(sup1_val>WRONG_VALUE && sup1_val<DBL_MAX)
        {
         //--- si el límite no está superado 
         if(norm_lower_limit<sup1_val)
           {
            //--- el Modelo 0 ha sido procesado
            m_pattern_0_done=true;
            //--- en el Diario
            Print("\n---== El límite inferior no está superado ==---");
            PrintFormat("De cálculo: %0."+IntegerToString(m_symbol.Digits())+"f",norm_lower_limit);
            PrintFormat("Real: %0."+IntegerToString(m_symbol.Digits())+"f",sup1_val);
            //---
            return 0.;
           }
        }
       }

Y si la verificación del ancho del intervalo no ha sido superada, en el diario aparecerá, por ejemplo, la siguiente entrada:

2015.08.19 00:01:00   ---== El límite superior no está superado ==---
2015.08.19 00:01:00   De cálculo: 1.10745
2015.08.19 00:01:00   Real: 1.10719

O sea, para ser completo, a la señal le ha faltado 26 pp.


Ejecutamos la estrategia en el Probador de Estrategias en el modo de optimización. He utilizado los siguientes parámetros de optimización:

  1. «Límite del ancho, pp»;
  2. «Tolerancia, pp»;
  3. «MA rápida»;
  4. «MA lenta»;
  5. «Corte, pp»;

En términos de rentabilidad, el repaso más acertado es el siguiente:

Fig.9 EURUSD: resultados de la estrategia con uso de filtros para 2013-2016.

Fig. 9 EURUSD: resultados de la estrategia con uso de filtros para 2013-2016.

Como era de esperar, algunas señales han sido eliminadas. La curva del balance se ha hecho más suave.

Pero cabe mencionar lo que no ha salido. Como se puede observar en el gráfico, desde el año 2015 la estrategia genera las áreas en las que la curva del balance fluctúa en una franja estrecha, sin un crecimiento explícito del beneficio. Los resultados de la optimización se encuentran en el archivo EURUSD_model.xml.

Vamos a ver los resultados en otros símbolos.

Así, en el par USDJPY, el mejor repaso se muestra en la Fig.10.

Fig. 10 USDJPY: resultados de la estrategia con uso de filtros para 2013-2016.

Fig. 10 USDJPY: resultados de la estrategia con uso de filtros para 2013-2016.

Ahora, echamos un vistazo en el oro «spot». El mejor resultado se muestra en la Fig. 11.

Fig. 11 XAUUSD: resultados de la estrategia con uso de filtros para 2013-2016.

Fig. 11 XAUUSD: resultados de la estrategia con uso de filtros para 2013-2016.

Durante este período, el metal precioso se negociaba en una franja estrecha, por eso la estrategia no ha tenido un resultado positivo.

Lo que se refiere a la libra esterlina, el mejor de los repasos se muestra en la Fig. 12.

Fig. 12 GBPUSD: resultados de la estrategia con uso de filtros para 2013-2016.

Fig. 12 GBPUSD: resultados de la estrategia con uso de filtros para 2013-2016.

La libra se negociaba bastante bien de acuerdo con la tendencia. Pero la corrección al principio del 2015 ha estropeado el resultado final.

En un todo, la estrategia trabaja mejor cuando en el mercado hay una tendencia.

Conclusión

En conclusión, cabe mencionar que el proceso de la creación de una estrategia de trading se compone de varias etapas. En la fase inicial, se formula la idea de trading. Normalmente, ésta es una hipótesis que debe formalizarse como código, y luego comprobarse en el Probador. A menudo es necesario corregir y mejorar algo ya en el proceso del testeo. En eso consiste el trabajo del desarrollador. En este caso, para codificar la estrategia de pivotes, ha sido utilizado este enfoque. Según mi parecer, el uso de la POO facilita considerablemente la tarea del desarrollador.

Voy a mencionar que el testeo completo en modo de optimización ha sido realizado en MQL5 Cloud Network. Usando la tecnología basada en la nube, fue posible evaluar la eficiencia de las estrategias de manera rápida y barata.


Ubicación de archivos

Ubicación de archivos

Es más conveniente colocar los archivos con las estrategias en la carpeta Pivots. Después de la compilación, es necesario colocar los archivos de los indicadores (Pivots.ex5 y MaTrendCatcher.ex5) en la carpeta de indicadores %MQL5\Indicators.