English Русский 中文 Español Deutsch 日本語
preview
Criação de Indicadores complexos de maneira fácil usando objetos

Criação de Indicadores complexos de maneira fácil usando objetos

MetaTrader 5Exemplos | 8 novembro 2022, 09:30
509 1
Manuel Alejandro Cercos Perez
Manuel Alejandro Cercos Perez

1. Introdução

Se você já tentou criar ou modificar um indicador complexo, você deve conhecer alguns dos problemas que surgem ao aumentar a quantidade de buffers: você precisará declarar toneladas de arrays do tipo double para os buffers, defini-los como buffers, configurar todos eles ...

Então vem as plotagens: você tem que declarar que tipo de gráficos você está usando, configurar todas as suas propriedades e, em seguida, certificar-se de que tudo se encaixa corretamente e que você declarou a quantidade correta de buffers e gráficos (se houver menos, você terá um erro do tipo "Array Out Of Range" (array fora do intervalo) ou um gráfico invisível, até que você perceba).

Finalmente, chega a hora de lidar com os dados dos buffers: se você quiser combinar os dados de uma grande quantidade de buffers (por exemplo, obter a média/máxima/mínima de 10 buffers em outro), precisará escrever linhas muito longas de código repetido comparando/combinando cada buffer um por um, ou confiar em alguns truques inteligentes com macros ou funções para economizar espaço. O resultado é provavelmente o mesmo: uma bagunça muito complexa propensa a bugs com toneladas de linhas e funcionalidades repetidas. Se você cometeu um erro de digitação em qualquer lugar, seria um pesadelo detectá-lo e corrigi-lo!

Esses tipos de situações podem atrapalhar iniciantes (e até programadores avançados) ao fazer indicadores complexos em funcionalidades ou visualmente. No entanto, há um pequeno truque que não é aparente para todos que pode tornar o desenvolvimento de indicadores de maneira mais rápida e fácil:

Você pode definir como os arrays de buffers que estão contidos dentro dos objetos

Neste artigo, nós exploraremos as opções que este truque nos dará e apresentaremos as soluções para poder e aplicar esse truque em qualquer outra situação usando a Programação Orientada a Objetos.


2. O primeiro exemplo

Antes de começarmos a fazer o indicador, vamos ver como é a forma mais básica do objeto que irá conter os arrays do buffer:

class CIndicatorPlot
{
public:
   double            array[];
};

Ele só tem um array público. Por enquanto é importante que seja público para que nós possamos acessá-lo ao configurá-lo como buffer ou configurar/acessar os dados que ele terá (como qualquer outro indicador).

Agora vamos ao indicador: para aplicar alguns dos conceitos, vamos criar um indicador que mostrará 10 indicadores RSI com diferentes períodos e sua média. Nós começaremos com as propriedades, entradas e a função OnInit.

#property indicator_buffers 11
#property indicator_plots 11

input int firstPeriod = 6;
input int increment = 2;

CIndicatorPlot indicators[];
int handles[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{

   ArrayResize(indicators, 11);
//--- indicator buffers mapping

   for (int i=0; i<11; i++)
   {
      SetIndexBuffer(i, indicators[i].array, INDICATOR_DATA);
      PlotIndexSetInteger(i, PLOT_DRAW_TYPE, DRAW_LINE);
   }

   for (int i=0; i<10; i++)
      PlotIndexSetInteger(i, PLOT_LINE_COLOR, clrRed);


   PlotIndexSetInteger(10, PLOT_LINE_COLOR, clrCyan);
   PlotIndexSetInteger(10, PLOT_LINE_STYLE, STYLE_DASH);
   PlotIndexSetInteger(10, PLOT_LINE_WIDTH, 2);

   ArrayResize(handles, 10);
   for (int i=0; i<10; i++)
      handles[i] = iRSI(NULL, PERIOD_CURRENT, firstPeriod+i*increment, PRICE_CLOSE);


//---
   return(INIT_SUCCEEDED);
}

Observe que nós usamos apenas 2 propriedades: buffer_indicador e indicadores_plots. Além das propriedades comuns (copyright, link, version, separate/chart window...) essas duas sempre serão necessárias. As outras propriedades (line color, draw type...) são opcionais, mas para ter um código mais compacto vamos configurá-las com PlotIndexSetInteger em um loop.
Para este indicador, nós precisaremos de 10 buffers para cada RSI com períodos diferentes e mais um para a sua média. Vamos colocar todos eles dentro de um array. Também criamos os manipuladores do indicador na OnInit. 

Agora, alguns cálculos e a cópia dos dados...

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
//---
   int limit = MathMax(0, prev_calculated-1);

   for (int i=0; i<10; i++)
   {
      if (limit==0)
         CopyBuffer(handles[i], 0, 0, rates_total-limit, indicators[i].array);
      else
      {
         double newValues[];
         CopyBuffer(handles[i], 0, 0, rates_total-limit, newValues);

         for (int k=0; k<rates_total-limit; k++)
         {
            indicators[i].array[limit+k] = newValues[k];
         }
      }
   }

   for (int i=limit; i<rates_total; i++)
   {
      indicators[10].array[i] = 0.0;
      for (int j=0; j<10; j++)                            
         indicators[10].array[i] +=indicators[j].array[i];

      indicators[10].array[i]/=10.0;
   }


//--- return value of prev_calculated for next call
   return(rates_total);
}

Observe que calcular a média de todos os buffers agora é tão fácil quanto fazer um loop. Se cada buffer fosse declarado no escopo global como um array do tipo double (como de costume), adicioná-los não seria tão fácil ou feito em poucas linhas.

Por último, não se esqueça de liberar os manipuladores...

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   for (int i=0; i<10; i++)
      IndicatorRelease(handles[i]);
}
//+------------------------------------------------------------------+

E este é o nosso resultado final:

Nada mal para o tamanho do código, mas há espaço para melhorar, como nós veremos na seção a seguir.


3. Adicionando mais opções

Apesar de termos economizado algum espaço usando esta classe e configurando as propriedades na inicialização (ao invés de #property), nós ainda tínhamos que configurar manualmente os buffers e os gráficos, e nem sempre isso seria tão fácil. Não existe uma maneira mais eficaz de fazer isso? A resposta é sim, e nós fazemos isso delegando a funcionalidade à classe.

Primeiro, nós adicionaremos algumas funções extras à classe que nós precisaremos mais tarde.

class CIndicatorPlot
{
private:
   int               indicator_plot;

public:
   double            array[];

   void              SetBuffer(int &buffer, int &plot);
   void              SetLineWidth(int width);
   void              SetLineStyle(ENUM_LINE_STYLE style);
   void              SetLineColor(color line_color);
   void              SetLabel(string label);
};

A função SetBuffer irá definir o buffer do indicador e plotar. Com duas variáveis passadas por referência, as mudanças que um objeto faz nelas refletirão nas próximas chamadas de outros objetos. O índice de plotagem é salvo para definir outras propriedades.

O restante das funções do conjunto são setters simples das propriedades do gráfico.

//+------------------------------------------------------------------+
void CIndicatorPlot::SetBuffer(int &buffer,int &plot)
{
   indicator_plot = plot;

   SetIndexBuffer(buffer, array, INDICATOR_DATA);
   PlotIndexSetInteger(indicator_plot, PLOT_DRAW_TYPE, DRAW_LINE);

   buffer++; //Increment for other steps (One buffer in this case)
   plot++;   //Increment one plot in any case
}

//+------------------------------------------------------------------+
void CIndicatorPlot::SetLineWidth(int width)
{
   PlotIndexSetInteger(indicator_plot, PLOT_LINE_WIDTH, width);
}

//---
//...

Para ajudar o indicador a se tornar mais atrativo visualmente, nós também criaremos uma função para interpolar as cores que usaremos posteriormente:

//+------------------------------------------------------------------+
//| Function to linearly interpolate 2 colors                        |
//+------------------------------------------------------------------+
color InterpolateColors(color colorA, color colorB, double factor)
{
   if (factor<=0.0) return colorA;
   if (factor>=1.0) return colorB;

   int result = 0;

   for (int i=0; i<3; i++) //R-G-B
   {
      int subcolor = int(
                        ((colorA>>(8*i))&(0xFF))*(1.0-factor) +
                        ((colorB>>(8*i))&(0xFF))*factor
                     );

      subcolor = subcolor>0xFF?0xFF:(
                    subcolor<0x00?0x00:
                    subcolor);

      result |= subcolor<<(8*i);
   }
   return (color)result;
}

Agora a função OnInit terá o seguinte aspecto:

CIndicatorPlot* indicators[];
CIndicatorPlot average;
int handles[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
   ArrayResize(indicators, 10);
   ArrayResize(handles, 10);


   int index=0, plot=0;

   for (int i=0; i<10; i++)
   {
        indicators[i] = new CIndicatorPlot();
   
      indicators[i].SetBuffer(index, plot);
      indicators[i].SetLineColor(InterpolateColors(clrYellow, clrRed, i/9.0));
      indicators[i].SetLabel("RSI ("+IntegerToString(firstPeriod+i*increment)+")");

      handles[i] = iRSI(NULL, PERIOD_CURRENT, firstPeriod+i*increment, PRICE_CLOSE);
   }

   average.SetBuffer(index, plot);
   average.SetLineColor(clrBlue);
   average.SetLineStyle(STYLE_DASH);
   average.SetLineWidth(2);
   average.SetLabel("Average");

//---
   return(INIT_SUCCEEDED);
}

Observe que em nenhum lugar nos referimos a qualquer buffer ou gráfico pelo seu número: as classes lidaram com esse problema. Agora é mais fácil definir corretamente qualquer propriedade de plotagem ou alterar a ordem dos buffers, pois nós podemos nos referir a eles com um objeto em vez de um índice. Aproveitamos também para adicionar algumas cores e rótulos às plotagens.

Neste exemplo nós alteramos também a estrutura do indicador usando um array de ponteiros para os RSIs (para provar que também é possível usar objetos criados dinamicamente) e separando a média do array. Por isso, nós precisamos alterar as referências da média na OnCalculate e excluir os indicadores no array de ponteiros na OnDeInit.

void OnDeinit(const int reason)
{
   for (int i=0; i<10; i++)
      IndicatorRelease(handles[i]);
   for (int i=0; i<10; i++)
        delete indicators[i];
}

Agora ele ficará assim:

A única mudança visual está nas cores (e nos rótulos na janela de dados). Internamente nós melhoramos o nosso fluxo de trabalho facilitando o manuseio das plotagens e buffers, mas ainda há mais espaço para melhorar a sua organização interna.

Se você olhar mais de perto, verá que cada manipulador é usado apenas por um dos buffers: cada buffer RSI pode ser calculado independentemente para que nós possamos fazer a classe fazer isso dentro (em vez de ter os cálculos diretamente na OnCalculate). A média precisa acessar o restante dos buffers, mas esses cálculos também podem ser delegados à classe. Podemos usar a herança para adicionar funcionalidades específicas sem alterar as funções ou adicionar condicionais na classe base.

Primeiro, nós adicionaremos os manipuladores de eventos vazios virtuais à classe base:

class CIndicatorPlot
{
   //...

public:
   
   //...

   virtual void      Init() { }
   virtual void      DeInit() { }
   virtual void      Update(const int start, const int rates_total) { }
};

Como visto anteriormente neste exemplo, a atualização só precisa do start e rates_total para realizar os seus cálculos, então o resto dos valores são omitidos.

Agora vamos criar uma classe RSI individual. Ele pode criar e excluir o identificador necessário. Além disso, incluímos uma função para definir o período desse manipulador, mas também é possível incluir esse parâmetro na Init().

class CRSIIndividual : public CIndicatorPlot
{
private:
   int               handle;
   int               rsi_period;

public:

   void              SetPeriodRSI(int period);

   virtual void      Init();
   virtual void      DeInit();
   virtual void      Update(const int start, const int rates_total);
};

//+------------------------------------------------------------------+
void CRSIIndividual::SetPeriodRSI(int period)
{
   rsi_period = period;
}

//+------------------------------------------------------------------+
void CRSIIndividual::Init(void)
{
   handle = iRSI(NULL, PERIOD_CURRENT, rsi_period, PRICE_CLOSE);
}

//+------------------------------------------------------------------+
void CRSIIndividual::Update(const int start,const int rates_total)
{
   if (start==0)
      CopyBuffer(handle, 0, 0, rates_total-start, array);
   else
   {
      double newValues[];
      CopyBuffer(handle, 0, 0, rates_total-start, newValues);

      for (int k=0; k<rates_total-start; k++)
      {
         array[start+k] = newValues[k];
      }
   }
}

//+------------------------------------------------------------------+
void CRSIIndividual::DeInit(void)
{
   IndicatorRelease(handle);
}

Para a classe Average, nós precisamos armazenar os ponteiros para acessar o restante dos objetos da plotagem do indicador (o RSI individual). Neste caso, Init() e DeInit() não são necessários.

class CRSIAverage : public CIndicatorPlot
{
private:
   CRSIIndividual*   rsi_indicators[];

public:
   void              SetRSIPointers(const CRSIIndividual &rsi_objects[]);

   virtual void      Update(const int start, const int rates_total);
};

//+------------------------------------------------------------------+
void CRSIAverage::SetRSIPointers(const CRSIIndividual &rsi_objects[])
{
   int total = ArraySize(rsi_objects);
   ArrayResize(rsi_indicators, total);

   for (int i=0; i<total; i++)
      rsi_indicators[i] = (CRSIIndividual*)GetPointer(rsi_objects[i]);
}

//+------------------------------------------------------------------+
void CRSIAverage::Update(const int start,const int rates_total)
{
   for (int i=start; i<rates_total; i++)
   {
      array[i] = 0.0;
      for (int j=0; j<10; j++)
         array[i] +=rsi_indicators[j].array[i];

      array[i]/=10.0;
   }
}

Criar um array de ponteiros pode parecer complicar demais o problema quando você pode acessar os objetos diretamente do escopo global, mas isso facilitará a reutilização da classe em outros indicadores sem fazer mais alterações. Observe também que neste exemplo nós usaremos novamente um array de objetos em vez de ponteiros para os indicadores do RSI, então nós precisamos obter os ponteiros deles.

Finalmente, a função OnInit (e as declarações de objeto acima) ficarão assim...

CRSIIndividual indicators[];
CRSIAverage average;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
   ArrayResize(indicators, 10);


   int index=0, plot=0;

   for (int i=0; i<10; i++)
   {
      indicators[i].SetBuffer(index, plot);
      indicators[i].SetLineColor(InterpolateColors(clrYellow, clrRed, i/9.0));
      indicators[i].SetLabel("RSI ("+IntegerToString(firstPeriod+i*increment)+")");

      indicators[i].SetPeriodRSI(firstPeriod+i*increment);
      indicators[i].Init();                               
   }

   average.SetBuffer(index, plot);
   average.SetLineColor(clrBlue);
   average.SetLineStyle(STYLE_DASH);
   average.SetLineWidth(2);
   average.SetLabel("Average");

   average.SetRSIPointers(indicators);                    

//---
   return(INIT_SUCCEEDED);
}

... e nós poderemos deixar muito mais limpas as outras funções de manipulação de eventos:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
//---
   int limit = MathMax(0, prev_calculated-1);

   for (int i=0; i<10; i++)
      indicators[i].Update(limit, rates_total);

   average.Update(limit, rates_total);

//--- return value of prev_calculated for next call
   return(rates_total);
}
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   for (int i=0; i<10; i++)
      indicators[i].DeInit();
}
//+------------------------------------------------------------------+

Visualmente, o indicador será exatamente igual ao segundo exemplo.



4. Expandindo a classe

A partir de agora, todas essas classes são boas em fazer o seu trabalho, mas são muito específicas para o tipo de indicador com o qual estamos lidando: usamos apenas algumas propriedades de plotagem e apenas desenhos em linha, mas e se eu quiser usar um gráfico que tenha buffers de cores? Ou um histograma, ou um zig-zag...? Para podermos reutilizar o que fizemos, nós precisaremos generalizar as classes. Para isso, nós precisamos preencher três condições:

  • Você deve ser capaz de criar qualquer tipo de plot/buffer, ou alterar as propriedades da plotagem, sem ter que sair da classe ou conhecer os detalhes dos índices de buffer/plot.
  • Você deve poder adicionar gráficos com qualquer estilo de desenho (linha, histograma, velas...) sem ter que se preocupar com a quantidade e os tipos de buffers que cada um possui (no entanto, você sempre será responsável pelos dados que colocar nesses arrays).
  • Você deve ser capaz de adicionar funcionalidades específicas às classes facilmente com herança (opcionalmente).

Com isso em mente, nós vamos primeiro explicar como as classes são implementadas e como a herança é estruturada.

Primeiro, as classes estão estruturadas assim:

  • CIndicatorBufferBase
    • CIndicatorCalculations
    • CIndicatorPlotBase
      • CIndicator_1Data
        • CIndicatorPlotLine
        • CIndicatorPlotHistogram
        • ...
        • CIndicator_1Data1Color
          • CIndicatorPlotColorLine
          • ...
      • CIndicator_2Data
        • CIndicatorPlotHistogram2
        • ...
        • CIndicator_2Data1Color
          • CIndicatorPlotColorHistogram2
          • ...
      • CIndicator_4Data
        • CIndicatorPlotCandles
        • ...
        • CIndicator_4Data1Color
          • CIndicatorPlotColorCandles
          • ...

Alguns pontos-chave:

  • Os três pontos significam que há mais classes que herdam da mesma que a anterior (só diferem no estilo de desenho, que está implícito em cada classe).
  • As classes em vermelho são as classes abstratas que não podem ser instanciadas, mas elas podem armazenar os ponteiros de outras classes que derivam delas (polimorfismo).
  • O restante das classes herda de uma classe base que possui a quantidade correspondente dos buffers de dados/cor. Isso também permite o polimorfismo, pois você pode ter um indicador que precisa acessar uma classe que tenha 1 buffer de dados, independentemente de ser uma linha, um histograma etc.
  • As classes de cores herdam dos buffers de dados pelo mesmo motivo do ponto acima.
  • CIndicatorCalculations é usado para os buffers de cálculo auxiliares, que não possuem plotagem.

A implementação, resumida, fica assim:

//+------------------------------------------------------------------+
//| Base class for plots and calculation buffers                     |
//+------------------------------------------------------------------+
class CIndicatorBufferBase
{
public:
   virtual void      SetBuffer(int &buffer, int &plot)=NULL;
   virtual void      SetAsSeries(bool set)=NULL;

   virtual void      Init() { }
   virtual void      DeInit() { }
   virtual void      Update(const int rates_total,
                            const int prev_calculated,
                            const datetime &time[],
                            const double &open[],
                            const double &high[],
                            const double &low[],
                            const double &close[],
                            const long &tick_volume[],
                            const long &volume[],
                            const int &spread[]) { }
};

//+------------------------------------------------------------------+
//| Calculations Buffer (with no plot)                               |
//+------------------------------------------------------------------+
class CIndicatorCalculations : public CIndicatorBufferBase
{
public:
   double            array[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};

//+------------------------------------------------------------------+
void CIndicatorCalculations::SetBuffer(int &index, int &plot)
{
   SetIndexBuffer(index, array, INDICATOR_CALCULATIONS);

   index++;
//No plot is used
}

//+------------------------------------------------------------------+
void CIndicatorCalculations::SetAsSeries(bool set)
{
   ArraySetAsSeries(array, set);
}

//+------------------------------------------------------------------+
//| Base indicator plot class                                        |
//+------------------------------------------------------------------+
class CIndicatorPlotBase : public CIndicatorBufferBase
{
protected:

   int               indicator_plot;

   virtual void      SetDrawType()=NULL; //Implicit in each class

public:

   void              SetArrow(uchar arrow);
   void              SetArrowShift(int shift);
   void              SetDrawBegin(int begin);
   void              SetShowData(bool show);
   void              SetShift(int shift);
   void              SetLineStyle(ENUM_LINE_STYLE style);
   void              SetLineWidth(int width);
   void              SetColorIndexes(color &color_array[]);
   void              SetLineColor(color line_color);
   void              SetLineColor(color line_color, int index);
   void              SetEmptyValue(double empty);
   void              SetLabel(string label);

   int               GetInteger(ENUM_PLOT_PROPERTY_INTEGER property_id, int property_modifier=0);
};

//...

//...

//+------------------------------------------------------------------+
//| Base for indicators with 1 Data Buffer                           |
//+------------------------------------------------------------------+
class CIndicator_1Data : public CIndicatorPlotBase
{
public:

   double            array[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};

//+------------------------------------------------------------------+
void CIndicator_1Data::SetBuffer(int &buffer,int &plot)
{
   indicator_plot = plot;

   SetIndexBuffer(buffer, array, INDICATOR_DATA);
   SetDrawType();

   buffer++;
   plot++;
}

//...

//+------------------------------------------------------------------+
//| Plot Line (1 data buffer)                                        |
//+------------------------------------------------------------------+
class CIndicatorPlotLine : public CIndicator_1Data
{
protected:

   virtual void      SetDrawType() final;
};

//+------------------------------------------------------------------+
void CIndicatorPlotLine::SetDrawType(void)
{
   PlotIndexSetInteger(indicator_plot, PLOT_DRAW_TYPE, DRAW_LINE);
}

//...

//...

//+------------------------------------------------------------------+
//| Base for indicators with 2 Data Buffers                          |
//+------------------------------------------------------------------+
class CIndicator_2Data : public CIndicatorPlotBase
{
public:

   double            first_array[];
   double            second_array[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};


//+------------------------------------------------------------------+
void CIndicator_2Data::SetBuffer(int &buffer, int &plot)
{
   indicator_plot = plot;

   SetIndexBuffer(buffer, first_array, INDICATOR_DATA);
   SetIndexBuffer(buffer+1, second_array, INDICATOR_DATA);
   SetDrawType();

   buffer+=2;
   plot++;
}

//...

//...

//+------------------------------------------------------------------+
//| Base for indicators with 1 Data Buffer & 1 Color Buffer          |
//+------------------------------------------------------------------+
class CIndicator_1Data1Color : public CIndicator_1Data
{
public:

   double            color_buffer[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};

//+------------------------------------------------------------------+
void CIndicator_1Data1Color::SetBuffer(int &buffer, int &plot)
{
   CIndicator_1Data::SetBuffer(buffer, plot);

   SetIndexBuffer(buffer, color_buffer, INDICATOR_COLOR_INDEX);

   buffer++; //Add color buffer
}

//+------------------------------------------------------------------+
void CIndicator_1Data1Color::SetAsSeries(bool set)
{
   CIndicator_1Data::SetAsSeries(set);
   ArraySetAsSeries(color_buffer, set);
}

//...

Cada classe contém (e define) a quantidade de buffers necessários. CIndicatorBufferBase tem os manipuladores de eventos que podem ser substituídos opcionalmente por qualquer tipo de classe de buffer, CIndicatorPlotBase contém setters para todas as propriedades de plotagem (e um getter), cada classe de dados base (com ou sem cor) contém as declarações do array e funções de configuração para os buffers, e cada classe específica substitui a função SetDrawType() e o declara como final para que ela não possa ser substituída novamente (se você precisar de uma classe com um tipo de desenho indefinido, poderá herdar da classe de dados base e substituir essa função).

Como nota, nesta implementação Update tem todos os valores que são usados no evento da OnCalculate, mas eles podem ser substituídos por menos parâmetros se você não precisar usar polimorfismo.

ArraySetAsSeries também foi incluída, pois ela é uma função muito comum e quase sempre exige que todos os buffers sejam configurados da mesma maneira.


Agora que nós temos as classes, nós podemos criar um indicador. Vamos adicionar algumas coisas juntos como exemplo:

  • Primeiro, nós criaremos as bandas com base no indicador ATR e as exibiremos como um gráfico de preenchimento.
  • Em seguida, nós criaremos 10 médias móveis com diferentes períodos e as exibiremos no gráfico como gráficos de linha.
  • Por fim, nós usaremos um gráfico de velas coloridas para alterar a coloração das velas dependendo de quantas MAs estão acima/abaixo das bandas.

Primeiro nós vamos declarar as entradas e incluir os arquivos para as classes de indicadores e a interpolação de cores que fizemos na seção 3:

#property indicator_buffers 19
#property indicator_plots 13

#include <OOPIndicators/IndicatorClass.mqh>
#include <OOPIndicators/ColorLerp.mqh>

input int atr_period = 10; //ATR Period
input double atr_band_multiplier = 0.8; //ATR Multiplier for bands
input bool show_bands = true; //Show Bands
input bool show_data = false; //Show Extra Data

input int ma_faster_period = 14; //MA Faster Period
input int ma_step = 2; //MA Step
input ENUM_MA_METHOD ma_method = MODE_SMA; //MA Method

Nós já definimos a quantidade de buffers e plots necessários. Você não deveria ter que saber desde o início quantos de cada são necessários, mas como você verá mais abaixo pode ser mais fácil descobrir os valores (na OnInit()).

Em seguida, nós criaremos as classes para cada parte do indicador.

Começando com as bandas do ATR:

//+------------------------------------------------------------------+
//| ATR Bands class (inherit from Filling Plot)                      |
//+------------------------------------------------------------------+
class CATRBand : public CIndicatorPlotFilling
{
private:

   int               handle;

public:

   virtual void      Init();
   virtual void      DeInit();
   virtual void      Update(const int limit, const int rates_total, const double &close[]);
};

//+------------------------------------------------------------------+
void CATRBand::Init(void)
{
   handle = iATR(NULL, PERIOD_CURRENT, atr_period);
}

//+------------------------------------------------------------------+
void CATRBand::Update(const int limit,const int rates_total,const double &close[])
{
   double atr[];
   CopyBuffer(handle, 0, 0, rates_total-limit, atr);

   for (int i=limit; i<rates_total; i++)
   {
      first_array[i] = close[i]+atr[i-limit]*atr_band_multiplier;
      second_array[i] = close[i]-atr[i-limit]*atr_band_multiplier;
   }
}

//+------------------------------------------------------------------+
void CATRBand::DeInit(void)
{
   IndicatorRelease(handle);
}

A classe MA, que possui os parâmetros na Init() para o período e método:

//+------------------------------------------------------------------+
//| Moving Averages class (inherit from Line Plot)                   |
//+------------------------------------------------------------------+
class CMA : public CIndicatorPlotLine
{
private:

   int               handle;

public:
   virtual void      Init(int period, ENUM_MA_METHOD mode);
   virtual void      DeInit();
   virtual void      Update(const int limit, const int rates_total);
};

//+------------------------------------------------------------------+
void CMA::Init(int period, ENUM_MA_METHOD mode)
{
   handle = iMA(NULL, PERIOD_CURRENT, period, 0, mode, PRICE_CLOSE);
}

//+------------------------------------------------------------------+
void CMA::Update(const int limit,const int rates_total)
{
   if (limit==0) CopyBuffer(handle, 0, 0, rates_total, array);
   else
   {
      double newVals[];
      CopyBuffer(handle, 0, 0, rates_total-limit, newVals);

      for (int i=limit; i<rates_total; i++)
         array[i] = newVals[i-limit];
   }
}

//+------------------------------------------------------------------+
void CMA::DeInit(void)
{
   IndicatorRelease(handle);
}

E a classe de velas. Nesse caso, e para evitar complexidades extras para o exemplo, nós acessaremos os objetos do escopo global. No entanto, isso não é recomendado se você planeja reutilizar qualquer classe.

Ele também contém macros que vamos declarar abaixo também. Nota: as funções estão abaixo da macro no código, mas as funções foram reordenadas para serem exibidas aqui.

//+------------------------------------------------------------------+
//| Color Candles class (inherit from Color Candles Plot)            |
//+------------------------------------------------------------------+
class CColorCandles : public CIndicatorPlotColorCandles
{
public:
   virtual void      Update(const int limit,
                            const int rates_total,
                            const double &open[],
                            const double &high[],
                            const double &low[],
                            const double &close[]);
};

//+------------------------------------------------------------------+
void CColorCandles::Update(const int limit,
                           const int rates_total,
                           const double &open[],
                           const double &high[],
                           const double &low[],
                           const double &close[])
{
   for (int i=limit; i<rates_total; i++)
   {
      open_array[i] = open[i];
      high_array[i] = high[i];
      low_array[i] = low[i];
      close_array[i] = close[i];

      int count_ma = TOTAL_MA;

      for (int m=0; m<TOTAL_MA; m++)
      {
         if (maIndicators[m].array[i] > bands.first_array[i]) count_ma++;
         if (maIndicators[m].array[i] < bands.second_array[i]) count_ma--;
      }

      color_buffer[i] = count_ma;

      //Update inside of this other object (to avoid making an extra inheritance, or an external loop)
      showIndex.array[i] = TOTAL_MA - count_ma;
   }
}

Agora nós temos que declarar os objetos e configurar os visuais dos buffers e as plotagens:

#define TOTAL_MA 10

CMA maIndicators[TOTAL_MA];
CATRBand bands;
CColorCandles candles;
CIndicatorPlotNone showIndex; //To show MAs above/below

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
   int buffer=0, plot=0;

   bands.SetBuffer(buffer, plot);
   candles.SetBuffer(buffer, plot);
   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].SetBuffer(buffer, plot);
   showIndex.SetBuffer(buffer, plot);

//Print("Buffers: ", buffer, "  Plots: ", plot);

//--- plot settings
   if (show_bands) bands.SetLineColor(clrDarkSlateGray);
   else bands.SetLineColor(clrNONE);
   bands.SetShowData(show_data);
   if (show_data)
      bands.SetLabel("Close + ATR;Close - ATR");


   for (int i=0; i<TOTAL_MA; i++)
   {
      maIndicators[i].SetLineColor(InterpolateColors(clrAqua, clrRoyalBlue, i/(TOTAL_MA-1.0)));
      maIndicators[i].SetLabel("MA("+IntegerToString(ma_faster_period+i*ma_step)+")");
      maIndicators[i].SetShowData(show_data);
      if (i>0 && i <TOTAL_MA-1) maIndicators[i].SetLineStyle(STYLE_DOT);
      else maIndicators[i].SetLineWidth(2);
   }

   color arrow_colors[TOTAL_MA*2+1];

   for (int i=0; i<TOTAL_MA; i++)
      arrow_colors[i] = InterpolateColors(clrGreenYellow, clrGray, i/double(TOTAL_MA));
   arrow_colors[TOTAL_MA] = clrGray;
   for (int i=TOTAL_MA+1; i<TOTAL_MA*2+1; i++)
      arrow_colors[i] = InterpolateColors(clrGray, clrOrange, (i-TOTAL_MA)/double(TOTAL_MA));

   candles.SetColorIndexes(arrow_colors);
   candles.SetLabel("Open;High;Low;Close");
   candles.SetShowData(false);

   showIndex.SetLabel("MAs above/below");
   showIndex.SetShowData(true);

//--- initialize classes
   bands.Init();
   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].Init(ma_faster_period+i*ma_step, ma_method);

   return(INIT_SUCCEEDED);
}

Em ordem, primeiro os buffers são configurados, depois as propriedades do gráfico e depois os subindicadores são inicializados (conforme especificado em suas classes).

Como dito antes, você pode saber facilmente a quantidade de buffers e gráficos que você precisa imprimindo os valores das variáveis buffer e plot. Então você pode definir as propriedades corretamente (no início, você também pode defini-las para um número maior do que o necessário para evitar erros).

Observe também que nós incluímos uma instância da classe Plot None. Esse objeto é atualizado pelo objeto de velas, portanto, não precisa de manipuladores de eventos específicos. Ele exibe a quantidade de MAs que estão acima ou abaixo das bandas na janela de dados.

Por fim, não há muita funcionalidade nos outros manipuladores de eventos, pois tudo está dentro dos objetos, apenas é necessário chamar as funções dos objetos na ordem correta:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
//---
   int limit = MathMax(0, prev_calculated-1);

   bands.Update(limit, rates_total, close);

   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].Update(limit, rates_total);

   candles.Update(limit, rates_total, open, high, low, close);

//--- return value of prev_calculated for next call
   return(rates_total);
}

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   bands.DeInit();
   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].DeInit();
}

O resultado final do indicador fica assim:


5. Limitações deste método

Apesar de ser mais conveniente, usar funções ao invés de propriedades traz alguns inconvenientes: o mais notável é a interferência ao mudar as cores/estilo de qualquer plotagem, às vezes eles ficam e às vezes são reescritos pela inicialização.

Esse problema pode ser evitado usando entradas para as cores (em vez de alterá-las na guia Cores) ou verificando se há uma cor diferente do preto (0x000000), que é o padrão. Isso funciona com todas as cores, exceto a cor preto.

if (obj.GetInteger(PLOT_LINE_COLOR)==clrBlack)
   obj.SetLineColor(clrYellow);

Além disso, neste artigo nós não analisamos os impactos no desempenho do uso dessas classes. Em teoria, usar as propriedades diretamente e menos funções deve ser mais rápido, mas não muito significativo na maioria dos casos.

Finalmente, como você deve ter notado, as classes não contêm manipuladores para os eventos do gráfico ou na OnTimer. A razão é que os eventos do gráfico são melhor processados diretamente na OnChartEvent, chamando as funções específicas depois que isso é feito (em vez de chamar um manipulador para cada indicador sempre que houver um evento e processar todos os eventos várias vezes). Para o timer, você pode usar o manipulador Update de forma diferente se o seu indicador for multi-período ou multi-moeda (você não teria acesso aos arrays OnCalculate diretamente). Outra decisão de projeto com a qual alguns podem discordar foi declarar os arrays que são usados como buffers com visibilidade pública: é possível definir os arrays com visibilidade protegida e o indicador ainda funcionariam, mas pode ser necessário adicionar getters para poder acessar os dados externamente.


6. Conclusão

Neste artigo nós pensamos e desenvolvemos um método para criar os indicadores complexos com mais facilidade e em menos linhas. Começamos com pequenos truques de organização para um caso específico, depois implementamos uma estrutura de classes que permite a reutilização e a personalização das funcionalidades e por fim reunimos tudo em um indicador de exemplo que utiliza a maioria dos recursos descritos no artigo.


Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/11233

Arquivos anexados |
MQL5.zip (10.17 KB)
Últimos Comentários | Ir para discussão (1)
Daniel Jose
Daniel Jose | 8 nov 2022 em 11:45

Seu artigo ficou muito BOM 😁 ... mas como você mencionou, existe sim um leve problema envolvido na questão de performance. Na maior parte das vezes, não irá influenciar em nada, podendo conviver com isto sem problemas. Mas em momentos de grande volatilidade, o sistema pode vim a travar, ou ficar muito lento, isto por conta da quantidade de chamadas que OnCalculate  poderá vim a receber. Mas o problema não está na função, e sim nas interações que acontecem nela. Já que a cada chamada, o MetaTrader será forçado a ler o buffer dos indicadores, e se a quantidade for grande, pode haver problemas. Uma forma de resolver isto seria usar uma chamada a OnTime, de maneira a aliviar um pouco a carga, já que as chamadas seriam executada não a cada evento de calculo, mas sim em um espaço de tempo predefinido. Se a ideia é operar manualmente, ou de forma semi automática, isto não seria problema, pois cada calculo poderia ser executado em um espaço de 200 milissegundos por exemplo. Mas não é muito adequado usar eventos OnTime em indicadores, devido a possibilidade dele influir no Thread de calculo, principalmente quando o mercado se encontra com muita volatilidade. Uma forma de fazer isto, seria forçando os cálculos na própria OnCalculate, de maneira a não precisar fazer chamadas de a CopyBuffer. Uma outra coisa que irá reduzir em alguns ciclos de máquina o consumo do processador é você modificar na função OnCalculate, a seguinte linha:

int OnCalculate(const int rates_total, 
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
//---
   int limit = MathMax(0, prev_calculated-1);

por esta daqui:

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
//---
   int limit = (prev_calculated <= 0 ? 0 : prev_calculated - 1);

Pode parecer pouco, mas usar o operador ternário, neste caso, irá ser mais eficiente do que fazer uma chamada a função. Estes pequenos detalhes, fazem bastante diferença em momentos de alta volatilidade ... 😁👍

Aprendendo a construindo um EA que opera de forma automática (Parte 05): Gatilhos manuais (II) Aprendendo a construindo um EA que opera de forma automática (Parte 05): Gatilhos manuais (II)
Aprenda como criar um EA que opera de forma automática, isto de forma simples e o mais seguro possível. No final daquele artigo, pensei que seria adequado permitir o uso do EA, de uma maneira manual, pelo menos por um tempo.
Indicador CCI. Três etapas de transformação Indicador CCI. Três etapas de transformação
Neste artigo, eu farei alterações adicionais no CCI afetando a própria lógica desse indicador. Além disso, nós poderemos vê-lo na janela principal do gráfico.
Ciência de Dados e Aprendizado de Máquina — Redes Neurais (Parte 01): Entendendo as Redes Neurais Feed Forward Ciência de Dados e Aprendizado de Máquina — Redes Neurais (Parte 01): Entendendo as Redes Neurais Feed Forward
Muitas pessoas as amam, mas apenas alguns entendem todas as operações por trás das Redes Neurais. Neste artigo, eu tentarei explicar tudo o que acontece por trás dos bastidores de um perceptron multicamadas feed-forward de maneira simples.
Ciência de Dados e Aprendizado de Máquina (Parte 06): Gradiente Descendente Ciência de Dados e Aprendizado de Máquina (Parte 06): Gradiente Descendente
O gradiente descendente desempenha um papel significativo no treinamento das redes neurais e muitos algoritmos de aprendizado de máquina. Ele é um algoritmo rápido e inteligente, apesar do seu trabalho impressionante, ele ainda é mal interpretado por muitos cientistas de dados, vamos ver do que ele se trata.