Padrões de reversão: Testando o padrão 'topo/fundo duplo'

Dmitriy Gizlyk | 15 janeiro, 2019

Sumário

Introdução

A análise realizada no artigo Quanto dura a tendência? mostra que 60% do tempo o preço permanece em tendência. Isso significa que abrir uma posição no início de uma tendência produz os melhores resultados. A busca por pontos de inversão de tendência gera inúmeros padrões de reversão. Um dos padrões mais famosos e usados é o de 'topo/fundo duplo'. 

1. Aspectos teóricos da formação de padrões

O padrão 'topo/fundo duplo' pode ser encontrado com bastante frequência. Sua formação está intimamente ligada à teoria dos níveis de comércio. O padrão é formado no final de uma tendência quando o preço atinge um nível de suporte ou resistência (dependendo do movimento anterior). Após uma correção durante um teste repetido do nível, ele reverte novamente em vez de rompê-lo.

Neste ponto, os traders de contra-tendência entram em jogo negociando com base no recuo em relação ao nível e empurrando o preço para correção. Enquanto o movimento de correção ganha impulso, os traders que seguem a tendência começam a sair do mercado fixando o lucro ou fechando posições deficitárias que visam romper o nível. Isso fortalece ainda mais o movimento, levando ao surgimento de uma nova tendência.

Padrão topo duplo

Ao procurar esse padrão, não faz sentido procurar uma correspondência exata de topos/fundos. O desvio dos níveis superior/inferior é considerado normal. O mais importante é que os topos estejam dentro do mesmo nível de suporte/resistência. A confiabilidade do padrão depende da força do nível em que ocorre sua formação.


2. Estratégia de negociação usando o padrão

A popularidade do padrão deu origem a várias estratégias baseadas nele. Na Internet, existem pelo menos três pontos de entrada diferentes para negociar usando esse padrão.

2.1. Cenário 1

O primeiro ponto de entrada é baseado no rompimento da linha do pescoço. Um stop-loss é definido além da linha superior/inferior. Existem diferentes abordagens para identificar o rompimento da linha do pescoço. Os traders podem usar um fechamento de barra sob a linha do pescoço, bem como o rompimento da linha do pescoço a uma distância fixa. Ambas as abordagens têm seus prós e contras. No caso de um movimento brusco, o fechamento da vela pode ocorrer a uma distância suficiente da linha do pescoço, tornando o uso do padrão ineficiente.

Primeiro ponto de entrada

A desvantagem dessa abordagem é um nível de stop loss relativamente alto, o que reduz a relação lucro/risco da estratégia utilizada.

2.2. Cenário 2

O segundo ponto de entrada é baseado na teoria dos níveis espelhados, quando a linha do pescoço sendo resistência se transforma em suporte e vice-versa. Aqui a entrada é feita quando o preço, depois de ter sido quebrado, retorna à linha do pescoço. Nesse caso, um stop-loss é definido além do extremo da última correção, reduzindo significativamente o nível de stop-loss. Infelizmente, o preço nem sempre testa a linha do pescoço após ter sido quebrado, reduzindo assim o número de entradas.

Segundo ponto de entrada 


2.3. Cenário 3

O terceiro ponto de entrada é baseado na teoria da tendência. É definido pelo rompimento da linha de tendência construída a partir do ponto inicial do movimento até o extremo da linha do pescoço. O stop-loss, como no primeiro caso, é colocado atrás da linha de topos/fundos. A entrada antecipada fornece um nível de stop-loss menor que o primeiro ponto de entrada. Também dá mais sinais em comparação ao segundo cenário. Ao mesmo tempo, tal ponto de entrada dá mais sinais falsos, uma vez que é possível formar um canal entre as linhas extremas e o pescoço, ou pode haver o padrão de flâmula. Ambos os casos indicam a continuação da tendência.

Terceiro ponto de entrada. 


Todas as três estratégias oferecem sair no nível igual à distância entre o extremo e a linha do pescoço.

Take-Profit

Além disso, ao determinar o padrão no gráfico, você deve observar que o topo/fundo duplo deve se destacar claramente do movimento do preço. Ao descrever o padrão, frequentemente é adicionada uma restrição: deve haver pelo menos seis barras entre dois topos/fundos.

Além disso, como a formação de padrões é baseada na teoria dos níveis de preços, a negociação usando padrões não deve contradizê-lo. Portanto, com base na finalidade procurada, a linha do pescoço não deve ser inferior ao nível 50 de Fibo do movimento inicial. Além disso, a fim de filtrar os sinais falsos, podemos adicionar o nível mínimo da primeira correção (que forma a linha do pescoço) como um indicador da força do nível de preços.


3. Criemos o EA

3.1. Buscando extremos

A criação de um EA começa com a criação do bloco de busca do padrão. Vamos usar o indicador ZigZag fornecido por padrão com o MetaTrader 5 para procurar extremos. Movemos a parte de cálculo do indicador para a classe, conforme descrito no artigo [1]. O indicador contém dois buffers de indicador contendo valor de preço em pontos de extremos. Os buffers de indicador contêm valores vazios entre os extremos. Para não criar dois buffers de indicador contendo vários valores vazios, eles foram substituídos por uma matriz de estruturas contendo informações sobre o extremo. A estrutura para armazenar informações sobre o extremo é a seguinte.

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

Se você usou o indicador ZigZag pelo menos uma vez, você sabe quantas coisas você precisa fazer ao buscar parâmetros ideais. Parâmetros muito pequenos dividem movimentos grandes em partes pequenas, enquanto parâmetros muito grandes fazem com que movimentos curtos sejam ignorados. O algoritmo para busca de padrões gráficos é muito exigente quanto à qualidade de encontrar extremos. Enquanto tentava encontrar um meio-termo, decidi usar o indicador com pequenos valores de parâmetros e criar uma superestrutura adicional combinando movimentos unidirecionais com curtas correções num único movimento.

Para resolver esse problema, foi criada a classe CTrends. O cabeçalho da classe é apresentado abaixo. Durante a inicialização, para a classe são passados uma referência ao objeto da classe de indicador e o tamanho do movimento mínimo em que um novo movimento será considerado uma continuação da tendência.

class CTrends : public CObject
  {
private:
   CZigZag          *C_ZigZag;         // Referência para o objeto do indicador ZigZag
   s_Extremum        Trends[];         // Matriz de extremos
   int               i_total;          // Número total de extremos armazenados
   double            d_MinCorrection;  // Tamanho mínimo do movimento para continuar a tendência

public:
                     CTrends();
                    ~CTrends();
//--- Método de inicialização de classe
   virtual bool      Create(CZigZag *pointer, double min_correction);
//--- Obtendo informações sobre o extremo
   virtual bool      IsHigh(s_Extremum &pointer) const;
   virtual bool      Extremum(s_Extremum &pointer, const int position=0);
   virtual int       ExtremumByTime(datetime time);
//--- Obtendo informações gerais
   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 obter informações sobre o extremo na classe são fornecidos os métodos:

O bloco de informações gerais contém métodos que retornam o número total de extremos armazenados, o símbolo e o período gráfico usados.

A lógica principal da classe é implementada no método Calculate. Vamos dar uma olhada mais de perto.

No início do método, verificamos a referência para o objeto da classe de indicador e a presença de extremos encontrados pelo indicador.

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

Em seguida, determinamos o número de extremos não processados. Caso todos os extremos sejam processados, saímos do método com o 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;
     }

Depois disso, solicitamos o número necessário de extremos a partir da classe de indicador.

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

Se, até o momento, não houver um único extremo no banco de dados, adicionamos o extremo mais antigo ao banco de dados chamando o método AddTrendPoint.

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

Em seguida, realizamos um ciclo com iteração de todos os extremos carregados. Extremos antes do último salvo são ignorados.

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

Na próxima etapa, verificamos se os topos são unidirecionais. Se o novo extremo redesenhar o anterior, atualizamos as informações.

      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 topos multidirecionais, verificamos se o novo movimento é uma continuação da tendência anterior. Se for assim, atualizamos as informações sobre extremos. Se o resultado da verificação for negativo, adicionamos informações sobre o extremo, chamando o 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]);
           }
        }

O código completo das classes pode ser encontrado no anexo.

3.2. Pesquisa de padrões

Depois de determinar o preço extremo, construiremos um bloco para encontrar pontos para posições de abertura. Dividimos esse trabalho em 2 sub-etapas:

  1. Busca de padrões para possíveis posições de abertura.
  2. Diretamente pontos de abertura de posição.

Esta funcionalidade será atribuída à classe CPttern. O cabeçalho da classe é apresentado abaixo.

class CPattern : public CObject
  {
private:
   s_Extremum     s_StartTrend;        //Ponto de início da tendência
   s_Extremum     s_StartCorrection;   //Ponto de início da correção
   s_Extremum     s_EndCorrection;     //Ponto de conclusão da correção
   s_Extremum     s_EndTrend;          //Ponto de conclusão da tendência
   double         d_MinCorrection;     //Correção mínima
   double         d_MaxCorrection;     //Correção máxima
//---
   bool           b_found;             //Sinalizador "Padrão encontrado"
//---
   CTrends       *C_Trends;
public:
                     CPattern();
                    ~CPattern();
//--- Inicialização da classe
   virtual bool      Create(CTrends *trends, double min_correction, double max_correction);
//--- Métodos de busca de padrão e do ponto 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 para obter informações sobre os extremos do padrão
   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;  }
  };

Vamos definir o padrão usando quatro extremos próximos, cujas informações armazenaremos nas quatro estruturas s_StartTrend, s_StartCorrection, s_EndCorrection e s_EndTrend. Para identificar o padrão, também precisamos de níveis de correção mínimo e máximo, que serão armazenados nas variáveis d_MinCorrection e d_MaxCorrection. Vamos obter os extremos numa instância da classe CTrends criada.

Ao inicializar a classe, passaremos para ela um ponteiro para um objeto da classe CTrends e os níveis de limite da correção. Dentro do método, verificamos se o ponteiro passado é válido, salvamos as informações recebidas e limpamos as estruturas dos 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;
  }

Vamos buscar padrões potenciais usando o método Search(). Nos parâmetros esse método recebe a data de início da pesquisa de padrões e retorna um valor booleano informando sobre os resultados da pesquisa. Consideremos o algoritmo do método em mais detalhes.

No início do método, verificamos o ponteiro para o objeto da classe CTrends e a presença de extremos salvos. Em caso de resultado negativo, saímos do método com o resultado false.

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

Em seguida, determinamos o extremo correspondente à data obtida nos parâmetros de entrada. Se o extremo não for encontrado, saímos do método com o resultado false.

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

Em seguida, realizamos um ciclo para percorrer todos os extremos, a partir da data especificada e até a última encontrada. Primeiro nós obtemos 4 extremos consecutivos. Se pelo menos um dos extremos não for recebido, avançamos para o próximo 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;

Na próxima etapa, verificamos os extremos correspondem com o padrão desejado. Se os extremos não satisfizerem o padrão desejado, avançamos para os extremos seguintes. Ao encontrar um padrão, definimos o sinalizador como true e saímos do método com o mesmo 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;
  }

O próximo passo, depois de determinar o padrão, procuraremos um ponto de entrada. Vamos procurar o ponto de entrada de acordo com o segundo cenário. Mas, para minimizar o risco de o preço não retornar à linha do pescoço, procuraremos uma confirmação do sinal num período gráfico menor.

Para implementar essa funcionalidade, criamos o método CheckSignal(). Esse método, além do próprio sinal, retornará os níveis de negociação de stop-loss e take profit, portanto, nos parâmetros do método, utilizamos ponteiros para variáveis.

No início do método, verificamos o sinalizador de um padrão encontrado anteriormente e, se o padrão não for encontrado, saímos do método com o resultado false.

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

Logo, determinamos o tempo de fechamento da vela de formação do padrão e carregamos os dados do período gráfico de interesse desde o início da formação do padrão até o momento atual.

   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;

Depois disso, realizaremos um ciclo no qual a cada barra verificaremos o rompimento da linha do pescoço, a correção e o fechamento da vela atrás da linha do pescoço na direção do movimento esperado.

Repare que aqui adicionei mais algumas restrições:

Se um dos eventos que cancelam o padrão for detectado, sairemos do método com o 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;

Depois de detectar um sinal para abrir uma posição, indicamos o tipo de sinal ("-1" - Venda, "1" - Compra) e os níveis de negociação. Definimos o stop-loss na profundidade máxima de correção em relação à linha do pescoço após ter sido quebrada. Para o take-profit, definimos 2 níveis:

1. Em um nível igual a 90% da distância entre o extremo e o pescoço na direção da abertura de posição.

2. No nível igual a 90% do movimento de tendência anterior.

Ao mesmo tempo, adicionamos a restrição de que o nível do primeiro take-profit não pode exceder o nível do 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;
        }

O código completo das classes pode ser encontrado no anexo.

3.3. Montando o EA

Após preparar tudo, reuniremos todos os blocos num único EA. Declaramos as variáveis externas que dividimos em três blocos:

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

No bloco de variáveis globais, declararemos uma matriz para armazenar ponteiros para objetos padrão, uma instância da classe de operações de negociação, uma instância de uma classe para busca de padrões que armazenará um ponteiro para a instância que processa a classe e uma variável para armazenar a hora inicial da busca do próximo padrão.

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

Para definir simultaneamente dois take-profit para a posição, usaremos o método proposto no artigo [2]

Na função OnInit(), inicializaremos todos os objetos necessários. Além disso, como não declaramos globalmente instâncias das classes CZigZag e CTrends, simplesmente as inicializamos e adicionamos ponteiros esses objetos em nossa matriz. No caso de um erro de inicialização em qualquer uma das etapas, saímos da função com o resultado INIT_FAILED.

int OnInit()
  {
//--- Inicialização da matriz de objetos
   ar_Objects=new CArrayObj();
   if(CheckPointer(ar_Objects)==POINTER_INVALID)
      return INIT_FAILED;
//--- Inicialização da classe do 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);
//--- Inicialização da classe de busca de tendências
   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;
//--- Inicialização da classe de operações de negociação
   Trade=new CTrade();
   if(CheckPointer(Trade)==POINTER_INVALID)
      return INIT_FAILED;
   Trade.SetAsyncMode(false);
   Trade.SetDeviationInPoints(l_Slippage);
   Trade.SetTypeFillingBySymbol(_Symbol);
//--- Inicialização de variáveis auxiliares
   start_search=0;
   CLimitTakeProfit::OnlyOneSymbol(true);
//---
   return(INIT_SUCCEEDED);
  }

Na função OnDeinit(), vamos limpar as instâncias dos objetos usados.

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 sempre, a funcionalidade principal é realizada na função OnTick. A funcionalidade dessa função pode ser dividida em dois blocos:

1. Verificação de sinais para abrir posições nos padrões encontrados anteriormente. É iniciado em cada abertura de uma nova vela num timeframe pequeno para procurar por confirmação de sinal.

2. Busca de novos padrões. É iniciado em cada abertura de uma nova vela no timeframe de trabalho (especificado para o indicador).

No início da função, verificaremos o início da nova barra no timeframe de confirmação do ponto de entrada. Se a barra não estiver formada, saímos da função antes do próximo tick. Repare que esta abordagem só funcionará corretamente se o timeframe para confirmar o ponto de entrada não for maior do que o timeframe de trabalho. Caso contrário, em vez de sair da função, será preciso ir para o bloco de busca de padrões.

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;

No caso de uma nova barra surgir, realizaremos um ciclo para verificar se em todos os padrões previamente salvos há um sinal para abrir uma posição. Observe aqui que não verificaremos que nos dois primeiros objetos da matriz existem sinais, pois nessas células armazenamos ponteiros para instâncias das classes de busca de extremos. Nos casos em que o ponteiro armazenado é inválido ou a função de teste de sinal retorna um valor false, o ponteiro será removido da matriz. A verificação dos sinais padrão será realizada diretamente na função CheckPattern(), cujo algoritmo será discutido abaixo.

   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;
           }
        }
     }

Após verificar os padrões encontrados anteriormente, avançamos para o segundo bloco — a busca por novos padrões. Para fazer isso, verificamos se há uma nova barra no timeframe de trabalho. Se uma nova barra não for formada, sairemos da função antecipando um novo tick.

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

Quando uma nova barra aparecer, determinaremos a data inicial da busca de padrões (levando em conta a profundidade do histórico analisado nos parâmetros). Em seguida, verificaremos se o ponteiro para um objeto da classe CPattern é relevante e, no caso de um ponteiro inválido, criaremos uma nova instância da classe.

   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;

Depois disso, no ciclo, chamamos o método de busca de potenciais padrões. No caso de uma pesquisa bem-sucedida, deslocaremos a data de início da pesquisa de um novo padrão e verificaremos se o padrão encontrado existe na matriz de padrões anteriormente encontrados. Se o padrão já estiver na matriz, avançamos para uma nova pesquisa.

   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;

Se um novo padrão for encontrado, verificamos se o sinal pode abrir uma posição, chamando a função CheckPattern(). Depois disso, se necessário, salvamos o padrão em nossa matriz e inicializamos uma nova instância da classe para a próxima pesquisa. O ciclo continua até que, durante a próxima pesquisa, o método Search() retorne o 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 complementar, proponho considerar as funções do algoritmo CheckPattern(). Nos parâmetros, esse método recebe um ponteiro para uma instância da classe CPatern e retorna o valor lógico do resultado das operações. Lembre-se que ao obter o resultado false a partir da função em questão, o padrão analisado é removido da matriz de objetos armazenados.

No início da função, chamaremos o método de busca de um sinal para abrir uma posição da classe CPattern. Se a verificação for mal-sucedida, saímos da função com o 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;

Se o sinal para abrir a posição for encontrado com sucesso, definimos níveis de negociação e enviamos uma ordem para abrir uma posição de acordo com o sinal recebido.

   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;
  }

Repare que se uma posição for aberta com sucesso, saímos da função com o resultado false. Isso não se deve a um erro, mas, sim, à necessidade de remover o padrão trabalhado da matriz. Este passo permite evitar a reabertura de uma posição com o mesmo padrão.

O código completo de todos os métodos e funções pode ser encontrado no anexo.

4. Teste de estratégia

Após a criação do nosso EA, é hora de verificar seu trabalho usando dados históricos. Vamos realizar testes num período de 9 meses de 2018 no par EURUSD. Vamos buscar padrões no timeframe M30, e vamos procurar os pontos de abertura de posição no timeframe M5.

Teste do expert advisorTeste do expert advisor

Os resultados dos testes mostraram a capacidade do EA de gerar lucros. Durante o período de testes, o EA concluiu 90 trades, dos quais 70 foram lucrativos. O EA apresentou um fator de lucro de 2,02 e um fator de recuperação de 4,77, indicando que o EA se pode usar em contas reais. Os resultados completos dos testes estão listados abaixo.

Resultados do testeResultados do teste

Fim do artigo

Neste artigo, criamos um Expert Advisor trabalhando no padrão de reversão de tendência "Topo/fundo duplo". O teste do EA usando dados históricos mostrou resultados aceitáveis e a capacidade do EA de gerar lucros. Esse trabalho confirma a possibilidade de usar o padrão "Topo/fundo duplo" para buscar pontos de abertura de posição como um bom sinal de reversão da tendência.

Links

  1. Como transferir a parte de cálculo de qualquer indicador para o código do EA
  2. Implementando Take Profit na forma de ordens limitadas sem alterar o código original do EA

Programas utilizados no artigo:

#
Nome
Tipo
Descrição
1ZigZag.mqhBiblioteca de classeClasse do indicador Zig Zag
2Trends.mqh Biblioteca de classeClasse de busca de tendências
3Pattern.mqhBiblioteca de classeClasse de trabalho com padrões
4LimitTakeProfit.mqhBiblioteca de classeClasse para substituir take-profit por ordens limitadas
5Header.mqhBibliotecaArquivo de cabeçalho do EA
6DoubleTop.mq5Expert AdvisorEA da estratégia "Topo/fundo duplo".