Patrones de viraje: Poniendo a prueba el patrón "Pico/valle doble"

Dmitriy Gizlyk | 8 enero, 2019

Contenido

Introducción

El análisis realizado en el artículo "¿Cuánto dura una tendencia?", demuestra que cerca de un 60% del movimiento del precio se encuentra dentro de la tendencia. Y precisamente la apertura de la posición al inicio de la tendencia da la posibilidad de obtener el resultado máximo. La búsqueda de los puntos de viraje ha generado una gran cantidad de patrones de viraje. Uno de los más famosos y más utilizados en el patrón del pico/valle doble. 

1. Aspectos teóricos de la formación del patrón

En el gráfico de precios, el patrón "Pico/valle doble" se puede encontrar con bastante frecuencia. La naturaleza de su formación está estrechamente relacionada con la teoría de niveles comerciales. El patrón se forma al final de la tendencia cuando el moviemiento de precio toca en el nivel de resistencia o apoyo, dependiendo del movimiento anterior, y después de la corrección durante la simulación repetida no rompe el nivel, sino que retrocede.

En este momento, entran en juego los tráders anti-tendencia, que comercian con el rebote contra el nivel y empujan el precio hacia el lado de la corrección. Con el aumento del movimiento de tendencia, comienzan a salir del mercado los tráders que comercian con la tendencia. Estos, o bien fijan su beneficio, o bien cierran las transacciones con pérdidas abiertas en la ruptura del nivel de resistencia/apoyo. Estos efectos fortalecen aún más el movimiento, lo que provoca el surgimiento de una nueva tendencia.

Patrón del pico doble

Al buscar el patrón en el gráfico, no conviene intentar localizar una coincidencia exacta de picos/valles. La desviación del nivel de picos/valles se considera normal. Lo más importante es que los picos se encuentren dentro de un nivel de apoyo/resistencia. La fiabilidad del patrón es directamente proporcional a la fuerza del nivel en cuya zona tiene lugar su formación.


2. Estrategia comercial según el patrón

Cuanto más amplia sea la difusión que ha obtenido el patrón, más diversas serán las estrategias para su comercio. En los vastos especios de internet, existen como mínimo tres puntos de entrada para el procesamiento de este patrón.

2.1. Caso 1

El primer punto de entrada se basa en la ruptura de la línea de cuello. El stop-loss se coloca más allá de la línea de picos / valles. En este caso, además, existen distintos enfoques para definir la "ruptura de la línea de cuello". Aquí puede usarse tanto el cierre de la barra por debajo de la línea de cuello, como la superación de la línea de cuello en una distancia fija. Ambos enfoques tienen sus ventajas y desventajas. En el caso de un movimiento brusco, el cierre de la vela puede tener lugar a una distancia suficiente de la línea de cuello, lo que hace inefectivo el uso del patrón.

Primer punto de entrada

Entre las desventajas del enfoque podemos nombrar el nivel relativamente elevado de stop-loss, lo que reduce el coeficiente de beneficio/riesgo de la estrategia utilizada.

2.2. Caso 2

El segundo punto de entrada se basa en la teoría de los niveles inversos, cuando la línea de cuello de apoyo se transforma en resistencia y viceversa. Aquí la entrada se realiza en el retroceso del precio hacia la línea después de su ruptura. En esta variante, el stop-loss se coloca más allá del extremo de la última corrección, lo que reduce significativamente el nivel de stop-loss. En este caso, además, conviene destacar que, por desgracia, ni mucho menos siempre el precio pone a prueba la línea de cuello tras la ruptura de esta. Asimismo, este hecho reduce significativamente el número de entradas.

Segundo punto de entrada 


2.3. Caso 3

El tercer punto de entrada se basa en la teoría de tendencia. Y se determina según la línea de tendencia, construida a partir del punto de comienzo del movimiento hasta el extremo en la línea de cuello. El stop-loss, como en el primer caso, se coloca más allá de la línea de picos/valles. La entrada temprana da un menor nivel de stop-loss en compración con el primer punto de entrada. Asimismo, da más señales en comparación con el segundo caso. Al mismo tiempo, este punto de entrada da más señales falsas, puesto que es posible la formación de un canal entre las líneas de extremo y el cuello, o podría darse un patrón de banderín. Ambos casos indican la continuación de la tendencia.

Tercer punto de entrada. 


En las tres estrategias se ofrece la salida en un nivel igual a la distancia desde el extremo y la línea de cuello.

Take Profit

Asimismo, al determinar el patrón en el gráfico, deberemos prestar atención a que el pico/valle doble destaque claramente en el movimiento de precio. Con frecuencia, al describir el patrón, se añade lo siguiente: entre dos picos/valles, deberá haber no menos de 6 barras.

Lo que es más, puesto que la formación del patrón se basa en la teoría de niveles de precio, el comercio según el patrón, como mínimo, no deberá contradecirla. Por eso, partiendo del supuesto objetivo, la línea de cuello no deberá estar por debajo del nivel de Fibonacci 50 del movimiento inicial. Además, para filtrar las señales falsas, podemos añadir el nivel mínimo de la primera corrección (que forma la línea de cuello), como indicador de fuerza del nivel de precio.


3. Creando el asesor

3.1. Buscando los extremos

Comenzaremos la creación del asesor comercial creando el bloque de búsqueda del patrón. Para buscar los extremos en el gráfico, usaremos el indicador zig-zag del paquete estádar de MetaTrader 5. Vamos a trasladar la parte de los cálculos a la clase tecnológica descrita en el artículo [1]. Como ya sabemos, este indicador contiene dos búferes de indicador, que contienen el valor del precio en los puntos de extremo. Entre los extremos, los búferes de indicador contienen valores vacíos. Para no crear dos búferes de indicador que contengan multitud de valores vacíos, estos han sido sustituidos por una matriz de estructuras que contienen información sobre el extremo. La estructura para guardar la información sobre el extremo tiene el aspecto que mostramos abajo.

   struct s_Extremum
     {
      datetime          TimeStartBar;
      double            Price;
      
      s_Extremum(void)  :  TimeStartBar(0),
                           Price(0)
         {
         }
      void Clear(void)
        {
         TimeStartBar=0;
         Price=0;
        }
     };

Es de suponer que cualquiera que haya usado el indicador zig-zag aunque sea una vez, sabe cuántos compromisos tiene que hacer al buscar los parámetros óptimos. Los valores pequeños de los parámetros conllevan la división de un gran movimiento en partes pequeñas. Y al contrario, valores demasiado altos de los parámetros provocarán la omisión de los movimientos cortos. El algoritmo de búsqueda de los patrones gráficos es muy exigente en cuanto a la calidad de localización de los extremos. En un intento de "hacer compatible lo incompatible", se tomó la decisión de usar el indicador con valores pequeños de los parámetros, en este caso, además, creando un ajuste adicional que combinará los movimientos en una misma dirección con las correcciones cortas en un mismo movimiento.

Para resolver esta tarea, se ha construido la clase CTrends. El encabezado de la clase se muestra más abajo. En la inicialización, a la clase se le transmite un enlace al objeto de la clase de indicador y el tamaño del movimiento mínimo, con el cual un nuevo movimiento se considerará la continuación de la tendencia.

class CTrends : public CObject
  {
private:
   CZigZag          *C_ZigZag;         // Enlace al objeto del indicador ZigZag
   s_Extremum        Trends[];         // Matriz de los extremos
   int               i_total;          // Número total de extremos guardados
   double            d_MinCorrection;  // Tamaño mínimo del movimiento para continuar la tendencia

public:
                     CTrends();
                    ~CTrends();
//--- Método de inicialización de la clase
   virtual bool      Create(CZigZag *pointer, double min_correction);
//--- Obteniendo información sobre el extremo
   virtual bool      IsHigh(s_Extremum &pointer) const;
   virtual bool      Extremum(s_Extremum &pointer, const int position=0);
   virtual int       ExtremumByTime(datetime time);
//--- Obteniendo información general
   virtual int       Total(void)          {  Calculate(); return i_total;   }
   virtual string    Symbol(void) const   {  if(CheckPointer(C_ZigZag)==POINTER_INVALID) return "Not Initilized"; return C_ZigZag.Symbol();  }
   virtual ENUM_TIMEFRAMES Timeframe(void) const   {  if(CheckPointer(C_ZigZag)==POINTER_INVALID) return PERIOD_CURRENT; return C_ZigZag.Timeframe();  }
   
protected:
   virtual bool      Calculate(void);
   virtual bool      AddTrendPoint(s_Extremum &pointer);
  };

Para obtener la información sobre los extremos se han creado los siguientes métodos en la clase:

En el bloque de información general se han previsto métodos que retornan el número total de extremos guardados y el símbolo y marco temporal usados.

La lógica principal de la clase está implementada en el método Calculate. Vamos a analizarlo con más detalle.

Al principio del método, comprobamos la actualidad del enlace al objeto de la clase de indicador y la presencia de los extremos encontrados por el indicador.

bool CTrends::Calculate(void)
  {
   if(CheckPointer(C_ZigZag)==POINTER_INVALID)
      return false;
//---
   if(C_ZigZag.Total()==0)
      return true;

A continuación, definimos el número de extremos no procesados. En el caso de que todos los extremos hayan sido procesados, salimos del método con el resultado true.

   int start=(i_total<=0 ? C_ZigZag.Total() : C_ZigZag.ExtremumByTime(Trends[i_total-1].TimeStartBar));
   switch(start)
     {
      case 0:
        return true;
        break;
      case -1:
        start=(i_total<=1 ? C_ZigZag.Total() : C_ZigZag.ExtremumByTime(Trends[i_total-2].TimeStartBar));
        if(start<0 || ArrayResize(Trends,i_total-1)<=0)
          {
           ArrayFree(Trends);
           i_total=0;
           start=C_ZigZag.Total();
          }
        else
           i_total=ArraySize(Trends);
        if(start==0)
           return true;
        break;
     }

Después de ello, solicitamos el número necesario de extremos desde la clase de indicador.

   s_Extremum  base[];
   if(!C_ZigZag.Extremums(base,0,start))
      return false;
   int total=ArraySize(base);
   if(total<=0)
      return true;

Si hasta el momento no ha habido ni un solo extremo en la base de datos, añadimos a la base de datos el extremo más antiguo, llamando a AddTrendPoint.

   if(i_total==0)
      if(!AddTrendPoint(base[total-1]))
         return false;

A continuación, organizamos un ciclo con iteración de todos los extremos cargados. Los extremos previos al último guardado son omitidos.

   for(int i=total-1;i>=0;i--)
     {
      int trends_pos=i_total-1;
      if(Trends[trends_pos].TimeStartBar>=base[i].TimeStartBar)
         continue;

En el siguiente paso, comprobamos si los picos son unidireccionales. Si un nuevo extremo redibuja el anterior, actualizamos la información.

      if(IsHigh(Trends[trends_pos]))
        {
         if(IsHigh(base[i]))
           {
            if(Trends[trends_pos].Price<base[i].Price)
              {
               Trends[trends_pos].Price=base[i].Price;
               Trends[trends_pos].TimeStartBar=base[i].TimeStartBar;
              }
            continue;
           }

Para los picos en direcciones opuestas, comprobamos si el nuevo movimiento es la continuación de la tendencia anterior. En el caso de que la respuesta sea positiva, acutalizamos la información sobre los extremos. Si la comprobación es negativa, añadimos la información sobre el extremo llamando al método AddTrendPoint;

         else
           {
            if(trends_pos>1 && Trends[trends_pos-1].Price>base[i].Price  && Trends[trends_pos-2].Price>Trends[trends_pos].Price)
              {
               double trend=fabs(Trends[trends_pos].Price-Trends[trends_pos-1].Price);
               double correction=fabs(Trends[trends_pos].Price-base[i].Price);
               if(fabs(1-correction/trend)>d_MinCorrection)
                 {
                  Trends[trends_pos-1].Price=base[i].Price;
                  Trends[trends_pos-1].TimeStartBar=base[i].TimeStartBar;
                  i_total--;
                  ArrayResize(Trends,i_total);
                  continue;
                 }
              }
            AddTrendPoint(base[i]);
           }
        }

Podrá familiarizarse con el código completo de todas las clases y sus métodos en los anexos.

3.2. Buscando los patrones

Después de definir los extremos de precio, construimos el bloque de búsqueda de los puntos de apertura de posición. Dividiremos este trabajo en 2 subetapas:

  1. La búsqueda de un patrón para la apertura potencial de posición.
  2. El punto de apertura de posición propiamente dicho.

Esta funcionalidad se implementará en la clase CPttern, cuyo encabezado se muestra más abajo.

class CPattern : public CObject
  {
private:
   s_Extremum     s_StartTrend;        //Punto de comienzo de la tendencia
   s_Extremum     s_StartCorrection;   //Punto de comienzo de la corrección
   s_Extremum     s_EndCorrection;     //Punto de finalización de la corrección
   s_Extremum     s_EndTrend;          //Punto de finalización de la tendencia
   double         d_MinCorrection;     //Corrección mínima
   double         d_MaxCorrection;     //Corrección máxima
//---
   bool           b_found;             //Bandera "Patrón encontrado"
//---
   CTrends       *C_Trends;
public:
                     CPattern();
                    ~CPattern();
//--- Inicializando clase
   virtual bool      Create(CTrends *trends, double min_correction, double max_correction);
//--- Métodos de búsqueda del patrón y el punto de entrada
   virtual bool      Search(datetime start_time);
   virtual bool      CheckSignal(int &signal, double &sl, double &tp1, double &tp2);
//--- method of comparing the objects
   virtual int       Compare(const CPattern *node,const int mode=0) const;
//--- Métodos de obtención de información sobre los extremos del patrón
   s_Extremum        StartTrend(void)        const {  return s_StartTrend;       }
   s_Extremum        StartCorrection(void)   const {  return s_StartCorrection;  }
   s_Extremum        EndCorrection(void)     const {  return s_EndCorrection;    }
   s_Extremum        EndTrend(void)          const {  return s_EndTrend;         }
   virtual datetime  EndTrendTime(void)            {  return s_EndTrend.TimeStartBar;  }
  };

Determinaremos el patrón según los cuatro extremos colindantes, cuya información guardaremos en cuatro estructuras s_StartTrend, s_StartCorrection, s_EndCorrection y s_EndTrend. Para identificar el patrón, también necesitaremos los niveles del mínimo y el máximo de la corrección, que se guardarán en las variables d_MinCorrection y d_MaxCorrection. Obtendremos los extremos a partir del ejemplar de la clase CTrends, anteriormente creada.

Al inicializar la clase, le transmitiremos el puntero al objeto de la clase CTrends y los niveles de límite de corrección. Dentro del método, comprobamos que el puntero transmitido sea verdadero, guardamos la información obtenida y limpiamos la estructura de los extremos.

bool CPattern::Create(CTrends *trends,double min_correction,double max_correction)
  {
   if(CheckPointer(trends)==POINTER_INVALID)
      return false;
//---
   C_Trends=trends;
   b_found=false;
   s_StartTrend.Clear();
   s_StartCorrection.Clear();
   s_EndCorrection.Clear();
   s_EndTrend.Clear();
   d_MinCorrection=min_correction;
   d_MaxCorrection=max_correction;
//---
   return true;
  }

Realizaremos la búsqueda de patrones potenciales en el método Search(). Este método recibe en los parámetros la fecha de inicio de la búsqueda del patrón, y retorna un valor lógico que informa sobre los resutados de la búsqueda. Vamos a analizar con mayor detalle el algoritmo del método.

Al comienzo del método, comprobamos la actualidad del puntero al objeto de la clase CTrends y la presencia de los extremos guardados. Si el resultado es negativo, salimos del método con el resultado false.

bool CPattern::Search(datetime start_time)
  {
   if(CheckPointer(C_Trends)==POINTER_INVALID || C_Trends.Total()<4)
      return false;

A continuación, determinamos el punto extremo que se corresponda con la fecha obtenida en los parámetros de entrada. Si el extremo no ha sido encontrado, salimos del método con el resultado false.

   int start=C_Trends.ExtremumByTime(start_time);
   if(start<0)
      return false;

A continuación, organizamos un ciclo de iteración por todos los extremos, comenzando por la fecha indicada y hasta el último encontrado. Primero obtenemos 4 extremos consecutivos. Si aunque sea uno solo de ellos no ha sido obtenido, pasamos al siguiente extremo.

   b_found=false;
   for(int i=start;i>=0;i--)
     {
      if((i+3)>=C_Trends.Total())
         continue;
      if(!C_Trends.Extremum(s_StartTrend,i+3) || !C_Trends.Extremum(s_StartCorrection,i+2) ||
         !C_Trends.Extremum(s_EndCorrection,i+1) || !C_Trends.Extremum(s_EndTrend,i))
         continue;

En la siguiente etapa, comprobamos que los extremos se correspondan con el patrón buscado. Si los extremos no satisfacen el patrón buscado, pasamos a los siguientes extremos. Si detectamos un patrón, establecemos la bandera en la posición true y salimos del método con el mismo resultado.

      double trend=s_StartCorrection.Price-s_StartTrend.Price;
      double correction=s_StartCorrection.Price-s_EndCorrection.Price;
      double re_trial=s_EndTrend.Price-s_EndCorrection.Price;
      double koef=correction/trend;
      if(koef<d_MinCorrection || koef>d_MaxCorrection || (1-fmin(correction,re_trial)/fmax(correction,re_trial))>=d_MaxCorrection)
         continue;
      b_found= true; 
//---
      break;
     }
//---
   return b_found;
  }

El siguiente paso tras determinar el patrón, será buscar el punto de entrada. El punto de entrada lo buscaremos de acuerdo con el segundo caso. Pero para minimizar el riesgo de que el precio no retorne a la línea de cuello, buscaremos la confirmación de la señal en un marco temporal menor.

Para implementar esta funcionalidad, crearemos el método CheckSignal(). Este método, aparte de la propia señal, retornará los niveles comerciales del stop-loss y el take-profit, por eso en los parámetros del método utilizaremos punteros a las variables.

Al comienzo del método, comprobamos la bandera que indica la localización previa de algún patrón; si el patrón no ha sido encontrado, salimos del método con el resultado false.

bool CPattern::CheckSignal(int &signal, double &sl, double &tp1, double &tp2)
  {
   if(!b_found)
      return false;

A continuación, determinamos la hora de cierre de la vela de formación del patrón y cargamos los datos del marco temporal que nos interese desde el comienzo de la formación del patrón hasta el momento actual.

   string symbol=C_Trends.Symbol();
   if(symbol=="Not Initilized")
      return false;
   datetime start_time=s_EndTrend.TimeStartBar+PeriodSeconds(C_Trends.Timeframe());
   int shift=iBarShift(symbol,e_ConfirmationTF,start_time);
   if(shift<0)
      return false;
   MqlRates rates[];
   int total=CopyRates(symbol,e_ConfirmationTF,0,shift+1,rates);
   if(total<=0)
      return false;

Después de ello, organizamos un ciclo en el que podamos comprobar barra a barra la ruptura de la línea de cuello, la corrección y el cierre de la vela tras la línea de cuello en la dirección del movimiento esperado.

Merece la pena notar que aquí hemos añadido algunas otras limitaciones:

Si se detecta uno de los eventos que cancelan el patrón, salimos del método con el resultado false.

   signal=0;
   sl=tp1=tp2=-1;
   bool up_trend=C_Trends.IsHigh(s_EndTrend);
   double extremum=(up_trend ? fmax(s_StartCorrection.Price,s_EndTrend.Price) : fmin(s_StartCorrection.Price,s_EndTrend.Price));
   double exit_level=2*s_EndCorrection.Price - extremum;
   bool break_neck=false;
   for(int i=0;i<total;i++)
     {
      if(up_trend)
        {
         if(rates[i].low<=exit_level || rates[i].high>extremum)
            return false;
         if(!break_neck)
           {
            if(rates[i].close>s_EndCorrection.Price)
               continue;
            break_neck=true;
            continue;
           }
         if(rates[i].high>s_EndCorrection.Price)
           {
            if(sl==-1)
               sl=rates[i].high;
            else
               sl=fmax(sl,rates[i].high);
           }
         if(rates[i].close<s_EndCorrection.Price || sl==-1)
            continue;
         if((total-i)>2)
            return false;

Después de detectar la señal de apertura de posición, indicamos el tipo de señal ("-1" - Venta, "1" - Compra) y los niveles comerciales. Estableceremos el stop-loss en el nivel de la profundidad máxima de la corrección con respecto a la línea de cuello después de su ruptura. Para el take-profit estableceremos 2 niveles:

1. En un nivel igual a un 90% de la distancia desde la línea de los extremos hasta el cuello, en dirección de la apertura de la posición.

2. En un nivel igual a un 90% del anterior movimiento de tendencia.

En este caso, además, añadiremos una limitación: el nivel del primer take-profit no podrá superar el nivel del segundo take-profit.

         signal=-1;
         double top=fmax(s_StartCorrection.Price,s_EndTrend.Price);
         tp1=s_EndCorrection.Price-(top-s_EndCorrection.Price)*0.9;
         tp2=top-(top-s_StartTrend.Price)*0.9;
         tp1=fmax(tp1,tp2);
         break;
        }

Podrá familiarizarse con el código completo de todos los métodos y clases en los anexos.

3.3. Desarrollando el asesor

Después de realizar el trabajo preparatorio, reunimos todos los bloques en un solo asesor. Declaramos las variable externas, que dividimos en tres bloques:

sinput   string            s1             =  "---- ZigZag Settings ----";     //---
input    int               i_Depth        =  12;                              // Depth
input    int               i_Deviation    =  100;                             // Deviation
input    int               i_Backstep     =  3;                               // Backstep
input    int               i_MaxHistory   =  1000;                            // Max history, bars
input    ENUM_TIMEFRAMES   e_TimeFrame    =  PERIOD_M30;                      // Work Timeframe
sinput   string            s2             =  "---- Pattern Settings ----";    //---
input    double            d_MinCorrection=  0.118;                           // Minimal Correction
input    double            d_MaxCorrection=  0.5;                             // Maximal Correction
input    ENUM_TIMEFRAMES   e_ConfirmationTF= PERIOD_M5;                       // Timeframe for confirmation
sinput   string            s3             =  "---- Trade Settings ----";      //---
input    double            d_Lot          =  0.1;                             // Trade Lot
input    ulong             l_Slippage     =  10;                              // Slippage
input    uint              i_SL           =  350;                             // Stop Loss Backstep, points

En el bloque de variables globales, declaramos la matriz para guardar los punteros a los objetos de los patrones, el ejemplar de clase de las operaciones comerciales, el ejemplar de clase para la búsqueda de patrones, en el que se guardará el puntero al ejemplar de clase procesado, y la variable para guardar la hora de comienzo de la búsqueda del próximo patrón.

CArrayObj         *ar_Objects;
CTrade            *Trade;
CPattern          *Pattern;
datetime           start_search;

Para implementar la posibilidad de colocar dos take-profit simultáneos a una posición, utilizaremos la tecnología ofrecida en el artículo [2]

En la función OnInit() inicializamos todos los objetos necesarios. En este caso, además, no declaramos de forma global los ejemplares de las clases CZigZag y CTrends, simplemente las inicializamos y añadimos a nuestra matriz los punteros a estos objetos. En caso de obtener un error de inicialización en cualquiera de las etapas, salimos de la función con el resultado INIT_FAILED.

int OnInit()
  {
//--- Inicializando la matriz de objetos
   ar_Objects=new CArrayObj();
   if(CheckPointer(ar_Objects)==POINTER_INVALID)
      return INIT_FAILED;
//--- Inicializando la clase de indicador zig-zag
   CZigZag *zig_zag=new CZigZag();
   if(CheckPointer(zig_zag)==POINTER_INVALID)
      return INIT_FAILED;
   if(!ar_Objects.Add(zig_zag))
     {
      delete zig_zag;
      return INIT_FAILED;
     }
   zig_zag.Create(_Symbol,i_Depth,i_Deviation,i_Backstep,e_TimeFrame);
   zig_zag.MaxHistory(i_MaxHistory);
//--- Inicializando la clase para la búsqueda de movimientos de tendencia
   CTrends *trends=new CTrends();
   if(CheckPointer(trends)==POINTER_INVALID)
      return INIT_FAILED;
   if(!ar_Objects.Add(trends))
     {
      delete trends;
      return INIT_FAILED;
     }
   if(!trends.Create(zig_zag,d_MinCorrection))
      return INIT_FAILED;
//--- Inicializando la clase de las operaciones comerciales
   Trade=new CTrade();
   if(CheckPointer(Trade)==POINTER_INVALID)
      return INIT_FAILED;
   Trade.SetAsyncMode(false);
   Trade.SetDeviationInPoints(l_Slippage);
   Trade.SetTypeFillingBySymbol(_Symbol);
//--- Inicializando las variables auxiliares
   start_search=0;
   CLimitTakeProfit::OnlyOneSymbol(true);
//---
   return(INIT_SUCCEEDED);
  }

En la función OnDeinit(), limpiamos los ejemplares de los objetos utilizados.

void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(ar_Objects)!=POINTER_INVALID)
     {
      for(int i=ar_Objects.Total()-1;i>=0;i--)
         delete ar_Objects.At(i);
      delete ar_Objects;
     }
   if(CheckPointer(Trade)!=POINTER_INVALID)
      delete Trade;
   if(CheckPointer(Pattern)!=POINTER_INVALID)
      delete Pattern;
  }

Como siempre, la funcionalidad básica se ha implementado en la función OnTick. La funcionaidad de esta función se puede dividir en dos bloques:

1. Comprobación de la señales de apertura de posición en los patrones detectados anteriormente. Se inicia cada vez que una nueva vela aparece en un marco temporal menor de búsqueda de confirmación de la señal.

2. Búsqueda de nuevos patrones. Se inicia cada vez que se abre una nueva vela en el marco temporal de trabajo (establecido para el indicador).

Al principio de la función, comprobamos la llegada de una nueva barra en el marco temporal de confirmación del punto de entrada. Si la barra no se ha formado, salimos de la función hasta el próximo tick. Conviene destacar que este enfoque funcionará correctamente solo si el marco temporal de confirmación del punto de entrada no es superior al marco temporal de trabajo. En caso contrario, en lugar de salir de la función, deberemos pasar al bloque de búsqueda de patrones.

void OnTick()
  {
//---
   static datetime Last_CfTF=0;
   datetime series=(datetime)SeriesInfoInteger(_Symbol,e_ConfirmationTF,SERIES_LASTBAR_DATE);
   if(Last_CfTF>=series)
      return;
   Last_CfTF=series;

Si llega una nueva barra, organizamos el ciclo para comprobar en todos los patrones guardados anteriormente la presencia de una señal de apertura de posición. Aquí debemos prestar atención a que no vamos a comprobar la presencia de señales en los dos primeros objetos de la matriz, puesto que en estas celdas guardamos los punteros a los ejemplares de las clases de búsqueda de los extremos. En el caso de que el puntero guardado sea inválido o la función de comprobación de la señal retorne el valor false, el puntero será eliminado de la matriz. La comprobación de las señales propiamente dicha se realizará en la función CheckPattern(), cuyo algoritmo se analizará más abajo.

   int total=ar_Objects.Total();
   for(int i=2;i<total;i++)
     {
      if(CheckPointer(ar_Objects.At(i))==POINTER_INVALID)
         if(ar_Objects.Delete(i))
           {
            i--;
            total--;
            continue;
           }
//---
      if(!CheckPattern(ar_Objects.At(i)))
        {
         if(ar_Objects.Delete(i))
           {
            i--;
            total--;
            continue;
           }
        }
     }

Después de comprobar los patrones anteriormente encontrados, pasamos al segundo bloque: la búsqueda de nuevos patrones. Para ello, comprobamos la presencia de una nueva barra en el marco temporal de trabajo. Si la nueva barra no se ha formado, salimos de la función a la espera de un nuevo tick.

   static datetime Last_WT=0;
   series=(datetime)SeriesInfoInteger(_Symbol,e_TimeFrame,SERIES_LASTBAR_DATE);
   if(Last_WT>=series)
      return;

Al aparecer una nueva barra, determinamos la fecha inicial de búsqueda de patrones (teniendo en cuenta la profundidad de la historia a analizar establecida en los parámetros). A continuación, comprobamos el puntero al objeto de la clase CPattern y, si el puntero no es válido, creamos un nuevo ejemplar de clase.

   start_search=iTime(_Symbol,e_TimeFrame,fmin(i_MaxHistory,Bars(_Symbol,e_TimeFrame)));
   if(CheckPointer(Pattern)==POINTER_INVALID)
     {
      Pattern=new CPattern();
      if(CheckPointer(Pattern)==POINTER_INVALID)
         return;
      if(!Pattern.Create(ar_Objects.At(1),d_MinCorrection,d_MaxCorrection))
        {
         delete Pattern;
         return;
        }
     }
   Last_WT=series;

Después de ello, llamamos en el ciclo al método de búsqueda de patrones potenciales. Si la búsqueda tiene éxito, desplazamos la fecha de comienzo de la búsqueda del patrón y comprobamos la presencia del patrón encontrado en la matriz de los patrones anteriormente encontrados. Si el patrón ya está en la matriz, pasamos a una nueva búsqueda.

   while(!IsStopped() && Pattern.Search(start_search))
     {
      start_search=fmax(start_search,Pattern.EndTrendTime()+PeriodSeconds(e_TimeFrame));
      bool found=false;
      for(int i=2;i<ar_Objects.Total();i++)
         if(Pattern.Compare(ar_Objects.At(i),0)==0)
           {
            found=true;
            break;
           }
      if(found)
         continue;

Si ya se ha encontrado un nuevo patrón, comprobamos la señal de apertura de posición, llamando a la función CheckPattern(). Después de ello, en caso necesario, guardamos el patrón en nuestra matriz e inicializamos el nuevo ejemplar de la clase para la siguiente búsqueda. El ciclo continúa hasta que en la próxima búsqueda, el método Search() retorne el valor false.

      if(!CheckPattern(Pattern))
         continue;
      if(!ar_Objects.Add(Pattern))
         continue;
      Pattern=new CPattern();
      if(CheckPointer(Pattern)==POINTER_INVALID)
         break;
      if(!Pattern.Create(ar_Objects.At(1),d_MinCorrection,d_MaxCorrection))
        {
         delete Pattern;
         break;
        }
     }
//---
   return;
  }

Para tener una imagen completa, proponemos analizar el algortimo de la función CheckPattern(). En los parámetros, este método obtiene el puntero al ejemplar de la clase CPatern y retorna el valor lógico del resultado de la realización de las operaciones. Recordemos que al obtener el resultado false de la función estudiada, se elimina el patrón analizado de la matriz de objetos guardados.

Al inicio de la función, llamamos al método de búsqueda de la señal de apertura de posición de la clase CPattern. Si se da un error de comprobación, salimos de la función con el resultado false.

bool CheckPattern(CPattern *pattern)
  {
   int signal=0;
   double sl=-1, tp1=-1, tp2=-1;
   if(!pattern.CheckSignal(signal,sl,tp1,tp2))
      return false;

Si la búsqueda de la señal de apertura de posición tiene éxito, establecemos los niveles comerciales y enviamos la orden de apertura de posición de acuerdo con la señal recibida.

   double price=0;
   double to_close=100;
//---
   switch(signal)
     {
      case 1:
        price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
        CLimitTakeProfit::Clear();
        if((tp1-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(CLimitTakeProfit::AddTakeProfit((uint)((tp1-price)/_Point),(fabs(tp1-tp2)>=_Point ? 50 : 100)))
              to_close-=(fabs(tp1-tp2)>=_Point ? 50 : 100);
        if(to_close>0 && (tp2-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(!CLimitTakeProfit::AddTakeProfit((uint)((tp2-price)/_Point),to_close))
              return false;
        if(Trade.Buy(d_Lot,_Symbol,price,sl-i_SL*_Point,0,NULL))
           return false;
        break;
      case -1:
        price=SymbolInfoDouble(_Symbol,SYMBOL_BID);
        CLimitTakeProfit::Clear();
        if((price-tp1)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(CLimitTakeProfit::AddTakeProfit((uint)((price-tp1)/_Point),(fabs(tp1-tp2)>=_Point ? 50 : 100)))
              to_close-=(fabs(tp1-tp2)>=_Point ? 50 : 100);
        if(to_close>0 && (price-tp2)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(!CLimitTakeProfit::AddTakeProfit((uint)((price-tp2)/_Point),to_close))
              return false;
        if(Trade.Sell(d_Lot,_Symbol,price,sl+i_SL*_Point,0,NULL))
           return false;
        break;
     }
//---
   return true;
  }

Debemos prestar atención a que, en el caso de abrir la posición con éxito, saldremos de la función con el resultado false. Esto se relaciona, no con el error, sino con la necesidad de eliminar el patrón procesado de la matriz. Este paso permite evitar una nueva apertura de posición según el mismo patrón.

Podrá familiarizarse con el código de todos los métodos y funciones en los anexos.

4. Simulando la estrategia

Después de crear nuestro asesor, ha llegado el momento de comprobar su funcionamiento con datos históricos. Realizaremos la simulación en un periodo de 9 meses de 2018 para la pareja EURUSD. Vamos a realizar la búsqueda de patrones en el marco temporal М30, y a buscar los puntos de entrada de posición en el marco М5.

Simulando el asesorSimulando el asesor

Los resultados de la simulación han mostrado que el asesor tiene la posibilidad de generar beneficios. En el periodo simulado, el asesor ha realizado 90 transacciones, 70 de las cuales han sido rentables. El asesor ha mostrado un factor de beneficio de 2.02 y un factor de recuperación de 4.77, lo que indica la posibilidad de usar el asesor en cuentas reales. Más abajo se muestran los resultados de la simulación.

Resultados de la simulaciónResultados de la simulación

Conclusión

En este artículo hemos creado un asesor que trabaja con el patrón de viraje de tendencia "Pico/valle doble". La simulación del asesor con datos históricos ha mostrado unos resultados aceptables, así como la capacidad del asesor de generar beneficios. Este trabajo confirma la posibilidad de usar el patrón "Pico/valle doble" para buscar los puntos de entrada de posición como una buena señal de viraje de tendencia.

Enlaces

  1. Cómo transferir los cálculos de cualquier indicador al código de un asesor experto
  2. Implementación de Take Profit en forma de órdenes limitadas sin cambiar el código fuente del EA

Programas usados en el artículo:

#
Nombre
Tipo
Descripción
1ZigZag.mqhBiblioteca de claseClase de indicador de Zig Zag
2Trends.mqh Biblioteca de claseClase de búsqueda de tendencias
3Pattern.mqhBiblioteca de claseClase de trabajo con patrones
4LimitTakeProfit.mqhBiblioteca de claseClase para sustitución de órdenes take-profit por órdenes límite
5Header.mqhBibliotecaArchivo de encabezados del asesor
6DoubleTop.mq5AsesorAsesor de la estrategia "Pico/valle doble"