English Русский 中文 Español Deutsch 日本語
preview
Indicador de perfil de mercado — Market Profile (Parte 2): Otimização e desenho em canvas

Indicador de perfil de mercado — Market Profile (Parte 2): Otimização e desenho em canvas

MetaTrader 5Exemplos |
219 3
Artyom Trishkin
Artyom Trishkin

Conteúdo


Introdução

Retomando o tema do indicador de Perfil de Mercado iniciado no artigo anterior, vale dizer que a construção do gráfico de perfil de mercado com objetos gráficos convencionais consome muitos recursos. Isso ocorre porque cada ponto de preço do Low ao High da barra diária é preenchido com objetos gráficos-retângulos na quantidade de barras que, ao longo do dia, alcançaram esse nível de preço. E isso para cada ponto, todos contendo múltiplos objetos gráficos, criados e desenhados para cada dia em que se desenha o gráfico de perfil. Quando o indicador cria milhares de objetos gráficos, isso pode causar atrasos significativos ao lidar com outros objetos gráficos e na atualização do gráfico. 

Rodar o indicador no gráfico M30 e construir o Perfil de Mercado para apenas três dias:

leva à criação de 4697 objetos gráficos-retângulos:

Esse é um uso bem ineficiente de recursos. Basta aumentar nas configurações o número de dias exibidos para que a quantidade de objetos criados para desenhar os gráficos de Perfil de Mercado de cada dia mostrado cresça de forma exponencial.

Mas aqui estamos apenas desenhando gráficos com objetos gráficos-retângulos. Um curto segmento de linha do histograma do perfil é um objeto gráfico. Ou seja, é possível desenhar não diretamente no gráfico, mas em um único objeto gráfico-canvas, posicionado no gráfico nas coordenadas necessárias. Assim, teremos apenas um (!) objeto gráfico por dia. E para três dias, seriam três objetos em vez de 4697! Isso faz uma diferença enorme! E isso é possível graças à classe para criação simplificada de desenhos personalizadosCCanvas, fornecida na Biblioteca Padrão do terminal cliente.

A versão do indicador de Perfil de Mercado que desenha o histograma do perfil no canvas está disponível no terminal na pasta \MQL5\Indicators\Free Indicators\ no arquivo MarketProfile Canvas.mq5. Ao estudar o código, percebemos que, diferente da primeira versão (MarketProfile.mq5), a saída gráfica aqui é feita com objetos da classe CCanvas. Ou seja, a lógica do indicador permanece a mesma, já explorada no primeiro artigo na seção Estrutura e princípios, mas agora o desenho é feito com a ajuda da classe especial CMarketProfile, que utiliza desenho em CCanvas.

A lógica de funcionamento é extremamente simples:

  • em um laço que percorre a quantidade de dias especificada,
    • criamos ou obtemos o objeto da classe CMarketProfile para o dia atual no laço,
      • desenhamos ou redesenhamos o perfil do dia no canvas, correspondente ao dia atual no laço.

Ou seja, o trabalho principal de desenho do gráfico de perfil é feito dentro da classe CMarketProfile. Vamos analisar a estrutura e funcionamento dessa classe.


Classe de perfil de mercado CMarketProfile

Abrimos o arquivo \MQL5\Indicators\Free Indicators\MarketProfile Canvas.mq5 e localizamos o código da classe CMarketProfile. Vamos ver o que há nele e discutir para que serve cada parte:

//+------------------------------------------------------------------+
//| Class to store and draw Market Profile for the daily bar         |
//+------------------------------------------------------------------+
class CMarketProfile
  {
public:
                     CMarketProfile() {};
                     CMarketProfile(string prefix, datetime time1, datetime time2, double high, double low, MqlRates &bars[]);
                    ~CMarketProfile(void);

   //--- checks if the object was created for the specified date
   bool              Check(string prefix, datetime time);
   //--- set high/low and array of intraday bars
   void              SetHiLoBars(double high, double low, MqlRates &bars[]);
   //--- set canvas dimensions and drawing options
   void              UpdateSizes(void);
   //--- is the profile in the visible part of the chart?
   bool              isVisibleOnChart(void);
   //--- has the graph scale changed?
   bool              isChartScaleChanged(void);
   //--- calculates profile by sessions
   bool              CalculateSessions(void);
   //--- draws a profile
   void              Draw(double multiplier=1.0);
   //---
protected:
   CCanvas           m_canvas;      // CCanvas class object for drawing profile
   uchar             m_alpha;       // alpha channel value that sets transparency
   string            m_prefix;      // unique prefix of the OBJ_BITMAP object
   string            m_name;        // name of the OBJ_BITMAP object used in m_canvas
   double            m_high;        // day's High
   double            m_low;         // day's Low
   datetime          m_time1;       // start time of the day
   datetime          m_time2;       // end time of the day
   int               m_day_size_pt; // daily bar height in points
   int               m_height;      // daily bar height in pixels on the chart
   int               m_width;       // daily bar width in pixels on the chart
   MqlRates          m_bars[];      // array of bars of the current timeframe between m_time1 and m_time2
   vector            m_asia;        // array of bar counters for the Asian session
   vector            m_europe;      // array of bar counters for the European session
   vector            m_america;     // array of bar counters for the American session
   double            m_vert_scale;  // vertical scaling factor
   double            m_hor_scale;   // horizontal scaling factor
  };

Métodos públicos declarados na classe:
  • O método Check() é usado para verificar a existência do objeto de perfil de mercado criado para um determinado dia;
  • O método SetHiLoBars() é utilizado para definir no objeto de perfil de mercado os valores de preço High e Low do dia e para repassar ao objeto o array de barras intradiárias;
  • O método UpdateSizes() define no objeto de perfil de mercado os tamanhos do canvas e os coeficientes de escala para desenhar os retângulos;
  • O método isVisibleOnChart() retorna um sinalizador indicando que o perfil de mercado está dentro da área visível no gráfico;
  • O método isChartScaleChanged() está declarado na classe, mas não está implementado;
  • O método CalculateSessions() calcula os parâmetros e preenche os arrays das sessões de negociação;
  • O método Draw() desenha no canvas o histograma do perfil de mercado com base nos dados de todas as sessões de negociação.

A finalidade das variáveis declaradas na seção protegida da classe é bem clara. Vale destacar os arrays de contadores de barras das sessões.
Todos eles são declarados como variáveis-vetor, o que permite que sejam usados como arrays de dados, só que de maneira mais simples:

O uso de vetores e matrizes, mais especificamente dos métodos especiais que esses tipos de dados oferecem, permite escrever um código mais simples, mais curto e mais compreensível, mais próximo da notação matemática. Isso livra o programador da necessidade de criar laços aninhados e se preocupar com a indexação correta dos arrays envolvidos nos cálculos. Com isso, aumenta-se a confiabilidade e a velocidade no desenvolvimento de programas complexos.

Vamos examinar a implementação dos métodos declarados da classe.

Construtor:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
void CMarketProfile::CMarketProfile(string prefix, datetime time1, datetime time2, double high, double low, MqlRates &bars[]):
   m_prefix(prefix),
   m_time1(time1),
   m_time2(time2),
   m_high(high),
   m_low(low),
   m_vert_scale(NULL),
   m_hor_scale(NULL)
  {
//--- copy the array of intraday bars to the array of MqlRates structures,
//--- create a name for the graphical object and define the size of the daily candle
   ArrayCopy(m_bars, bars);
   m_name=ExtPrefixUniq+"_MP_"+TimeToString(time1, TIME_DATE);
   m_day_size_pt=(int)((m_high-m_low)/SymbolInfoDouble(Symbol(), SYMBOL_POINT));
//--- set vector sizes for trading sessions
   m_asia=vector::Zeros(m_day_size_pt);
   m_europe=vector::Zeros(m_day_size_pt);
   m_america=vector::Zeros(m_day_size_pt);
//--- set the width and height of the canvas
   UpdateSizes();
//--- if this is the first tick at the beginning of the day, then the canvas dimensions will be zero - set the dimensions to 1 pixel in height and width
   m_height=m_height?m_height:1;
   m_width=m_width?m_width:1;
//--- create a graphical object
   if(m_canvas.CreateBitmap(m_name, m_time1, m_high, m_width, m_height, COLOR_FORMAT_ARGB_NORMALIZE))
      ObjectSetInteger(0, m_name, OBJPROP_BACK, true);
   else
     {
      Print("Error creating canvas: ", GetLastError());
      Print("time1=", m_time1, "  high=", m_high, "  width=", m_width, "  height=", m_height);
     }
  }

O construtor paramétrico da classe recebe como parâmetros o prefixo do nome do objeto-canvas que será criado para desenhar o perfil do dia, o horário de início e fim do dia, os preços máximo e mínimo do dia e o array das barras intradiárias. Os valores dessas variáveis são armazenados nas variáveis correspondentes da classe na linha de inicialização. Em seguida

  • o array passado por referência é copiado para o array da classe, um nome único para o objeto gráfico é criado a partir do prefixo passado nos parâmetros do construtor, da abreviação "_MP_" e do horário de abertura do dia, e o tamanho da vela diária em pontos é calculado;
  • cada um dos arrays das sessões de negociação recebe um tamanho igual ao tamanho da barra diária em pontos e ao mesmo tempo é preenchido com zeros, ou seja, é inicializado;
  • os tamanhos do canvas para desenho do perfil são definidos, sendo que, se for o primeiro tick do dia, o tamanho será zero, e são atribuídos os tamanhos mínimos permitidos de um pixel em ambas as dimensões para largura e altura;
  • o canvas para desenho é criado com os tamanhos especificados.

Método para verificar a existência do objeto de perfil de mercado criado para determinado dia:

//+------------------------------------------------------------------+
//| Checks if CMarketProfile object is for the specified 'time' date |
//+------------------------------------------------------------------+
bool CMarketProfile::Check(string prefix, datetime time)
  {
   string calculated= prefix+"_MP_"+TimeToString(time, TIME_DATE);
   return (m_name==(calculated));
  };

Como o nome de cada objeto-canvas para desenhar o perfil é definido no construtor da classe, e esse nome usa a representação em string do horário de início do dia, então, para verificar se o objeto foi criado para um determinado horário, o método recebe o horário de início do dia, cria-se uma string idêntica ao nome do objeto e essa string criada é comparada com o nome real do objeto. O resultado da verificação é retornado pelo método.

Método para definir no objeto de perfil de mercado os valores de preços High e Low do dia e para repassar ao objeto o array de barras intradiárias:

//+------------------------------------------------------------------+
//| Sets High/Low and a set of current-timeframe bars                |
//+------------------------------------------------------------------+
void CMarketProfile::SetHiLoBars(double high, double low, MqlRates &bars[])
  {
//--- if the maximum of the day has changed, move the OBJ_BITMAP object to the new Y coordinate
   if(high>m_high)
     {
      m_high=high;
      if(!ObjectSetDouble(0, m_name, OBJPROP_PRICE, m_high))
         PrintFormat("Failed to update canvas for %s, error %d", TimeToString(m_time1, TIME_DATE), GetLastError());
     }
   ArrayCopy(m_bars, bars);
   m_high=high;
   m_low=low;
//--- daily range in points
   m_day_size_pt=(int)((m_high-m_low)/SymbolInfoDouble(Symbol(), SYMBOL_POINT));
//--- reset vector sizes for trading sessions
   m_asia=vector::Zeros(m_day_size_pt);
   m_europe=vector::Zeros(m_day_size_pt);
   m_america=vector::Zeros(m_day_size_pt);
  }

O método recebe os valores High e Low da vela diária e, por referência, o array de barras intradiárias no formato da estrutura MqlRates.

  • o preço High é armazenado na variável do objeto e o canvas é deslocado para a nova coordenada;
  • as barras intradiárias do array recebido são copiadas para o array interno;
  • o preço Low do dia é atribuído à variável da classe;
  • o novo tamanho da barra diária em pontos é calculado;
  • os arrays das sessões de negociação são redimensionados de acordo com o valor calculado do tamanho da barra diária em pontos e são preenchidos com zeros, ou seja, inicializados.

Vale destacar que, para inicializar os vetores, é usado o método Zeros() das matrizes e vetores, que ao mesmo tempo define o tamanho do vetor e preenche todo o array com zeros.
Para um array comum, seria necessário fazer duas operações separadas: ArrayResize() e ArrayInitialize().

Método que define no objeto de perfil de mercado os tamanhos do canvas e os coeficientes de escala para desenhar os retângulos:

//+------------------------------------------------------------------+
//|  Sets drawing parameters                                         |
//+------------------------------------------------------------------+
void CMarketProfile::UpdateSizes(void)
  {
//--- convert time/price to x/y coordinates
   int x1, y1, x2, y2;
   ChartTimePriceToXY(0, 0, m_time1, m_high, x1, y1);
   ChartTimePriceToXY(0, 0, m_time2, m_low,  x2, y2);
//--- calculate canvas dimensions
   m_height=y2-y1;
   m_width =x2-x1;
//--- calculate ratios for transforming vertical price levels
//--- and horizontal bar counters to chart pixels
   m_vert_scale=double(m_height)/(m_day_size_pt);
   m_hor_scale =double(m_width*PeriodSeconds(PERIOD_CURRENT))/PeriodSeconds(PERIOD_D1);
   
//--- change the canvas size
   m_canvas.Resize(m_width, m_height);
  }

A lógica do método está comentada no código. Os coeficientes de escala são usados para definir os tamanhos dos retângulos desenhados no canvas, de acordo com a proporção entre o tamanho do canvas e o tamanho da janela do gráfico.
Os coeficientes calculados são adicionados ao cálculo da altura e largura dos retângulos desenhados.

Método que retorna o sinalizador de que o perfil de mercado está dentro da área visível no gráfico:

//+------------------------------------------------------------------+
//|  Checks that the profile is in the visible part of the chart     |
//+------------------------------------------------------------------+
bool CMarketProfile::isVisibleOnChart(void)
  {
   long last_bar=ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);        // last visible bar on the chart on the left
   long first_bar=last_bar+-ChartGetInteger(0, CHART_VISIBLE_BARS);  // first visible bar on the chart on the right
   first_bar=first_bar>0?first_bar:0;
   datetime left =iTime(Symbol(), Period(), (int)last_bar);          // time of the left visible bar on the chart
   datetime right=iTime(Symbol(), Period(), (int)first_bar);         // time of the right visible bar on the chart
   
//--- return a flag that the canvas is located inside the left and right visible bars of the chart
   return((m_time1>= left && m_time1 <=right) || (m_time2>= left && m_time2 <=right));
  }

Aqui são identificados os números dos bares visíveis à esquerda e à direita no gráfico, seus horários são obtidos e é retornado um sinalizador indicando que o horário da borda esquerda e da borda direita do canvas está dentro da área de bares visíveis no gráfico.

Método que calcula os parâmetros e preenche os arrays das sessões de negociação:

//+------------------------------------------------------------------+
//| Prepares profile arrays by sessions                              |
//+------------------------------------------------------------------+
bool CMarketProfile::CalculateSessions(void)
  {
   double point=SymbolInfoDouble(Symbol(), SYMBOL_POINT);   // one point value
//--- if the array of intraday bars is not filled, leave
   if(ArraySize(m_bars)==0)
      return(false);
//---- iterate over all the bars of the current day and mark the cells of the arrays (vectors) that contain the bars being iterated over in the loop
   int size=ArraySize(m_bars);
   for(int i=0; i<size; i++)
     {
      //--- get the bar hour
      MqlDateTime bar_time;
      TimeToStruct(m_bars[i].time, bar_time);
      uint        hour     =bar_time.hour;
      //--- calculate price levels in points from the Low of the day reached by the price on each bar of the loop
      int         start_box=(int)((m_bars[i].low-m_low)/point);   // index of the beginning of price levels reached by the price on the bar
      int         stop_box =(int)((m_bars[i].high-m_low)/point);  // index of the end of price levels reached by the price on the bar

      //--- American session
      if(hour>=InpAmericaStartHour)
        {
         //--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level
         for(int ind=start_box; ind<stop_box; ind++)
            m_america[ind]++;
        }
      else
        {
         //--- European session
         if(hour>=InpEuropeStartHour && hour<InpAmericaStartHour)
            //--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level
            for(int ind=start_box; ind<stop_box; ind++)
               m_europe[ind]++;
         //--- Asian session
         else
            //--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level
            for(int ind=start_box; ind<stop_box; ind++)
               m_asia[ind]++;
        }
     }
//--- vectors of all sessions are ready
   return(true);
  }

No artigo anterior foi detalhada a lógica para determinar a quantidade de barras da sessão de negociação cujos preços atingiram níveis em pontos do Low ao High do dia. Se na versão anterior do indicador isso tudo era feito no laço principal do indicador, aqui todo esse cálculo foi extraído para um método separado do objeto de perfil do dia. A ideia aqui é contar e registrar nas células do array (vetor) a quantidade de barras que atravessaram cada nível de preço, calculado em pontos do Low ao High do dia. Após a execução do método, todos os vetores estarão preenchidos de acordo com o movimento do preço em cada nível. Quantas barras atravessaram cada nível, esse será o valor registrado nas células correspondentes do array (vetor).

Método que desenha no canvas o histograma do perfil de mercado com base nos dados de todas as sessões de negociação:

//+------------------------------------------------------------------+
//|  Draw Market Profile on the canvas                               |
//+------------------------------------------------------------------+
void CMarketProfile::Draw(double multiplier=1.0)
  {
//--- sum up all sessions for rendering
   vector total_profile=m_asia+m_europe+m_america;   // profile that combines all sessions
   vector europe_asia=m_asia+m_europe;               // profile that combines only the European and Asian sessions

//--- set a completely transparent background for the canvas
   m_canvas.Erase(ColorToARGB(clrBlack, 0));

//--- variables for drawing rectangles
   int x1=0;                           // X coordinate of the left corner of the rectangle always starts at zero
   int y1, x2, y2;                     // rectangle coordinates
   int size=(int)total_profile.Size(); // size of all sessions
   
//--- render the American session with filled rectangles
   for(int i=0; i<size; i++)
     {
      //--- skip zero vector values
      if(total_profile[i]==0)
         continue;
      //--- calculate two points to draw a rectangle, x1 is always 0 (X of the lower left corner of the rectangle)
      y1=m_height-int(i*m_vert_scale);                    // Y coordinate of the lower left corner of the rectangle
      y2=(int)(y1+m_vert_scale);                          // Y coordinate of the upper right corner of the rectangle
      x2=(int)(total_profile[i]*m_hor_scale*multiplier);  // X coordinate of the upper right corner of the rectangle 
      //--- draw a rectangle at the calculated coordinates with the color and transparency set for the American session
      m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAmericaSession, InpTransparency));
     }

//--- render the European session with filled rectangles
   for(int i=0; i<size; i++)
     {
      //--- skip zero vector values
      if(total_profile[i]==0)
         continue;
      //--- calculate two points to draw a rectangle
      y1=m_height-int(i*m_vert_scale);
      y2=(int)(y1+m_vert_scale);
      x2=(int)(europe_asia[i]*m_hor_scale*multiplier);
      //--- draw a rectangle over the rendered American session using the calculated coordinates
      //--- with color and transparency set for the European session
      m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpEuropeSession, InpTransparency));
     }

//--- draw the Asian session with filled rectangles
   for(int i=0; i<size; i++)
     {
      //--- skip zero vector values
      if(total_profile[i]==0)
         continue;
      //--- calculate two points to draw a rectangle
      y1=m_height-int(i*m_vert_scale);
      y2=(int)(y1+m_vert_scale);
      x2=(int)(m_asia[i]*m_hor_scale*multiplier);
      //--- draw a rectangle over the rendered European session using the calculated coordinates
      //--- with color and transparency set for the Asian session
      m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAsiaSession, InpTransparency));
     }
//--- update the OBJ_BITMAP object without redrawing the chart
   m_canvas.Update(false);
  }

A lógica do método está detalhada nos comentários do código. Em resumo: temos os arrays (vetores) calculados e preenchidos de três sessões — asiática, europeia e americana. É necessário desenhar o histograma de perfil para cada uma dessas sessões. Primeiro é desenhada a sessão americana, sobre ela é desenhada a sessão europeia e, por fim, sobre as duas anteriores, a sessão asiática.
Por que desenhamos as sessões na ordem inversa ao seu horário de funcionamento?

  • A sessão americana — mais precisamente, seu histograma — abrange tanto o tempo já negociado das duas sessões anteriores quanto o tempo da própria sessão americana, ou seja, é o histograma de perfil mais completo do dia inteiro. Por isso, ela é desenhada primeiro.
  • Em seguida, desenha-se a sessão europeia, que inclui também o tempo já negociado da sessão asiática. Sendo assim, como aqui temos apenas duas sessões — asiática e europeia — o histograma será mais curto no eixo X do que o da sessão americana, portanto deve ser desenhado sobre o da americana. 
  • Por fim, é desenhado o histograma mais curto no eixo X, o da sessão asiática. 
Dessa forma, os histogramas de cada sessão são sobrepostos na ordem correta, compondo uma imagem completa de todo o perfil de mercado do dia.

Vale destacar como é prático unir os dados dos arrays ao se utilizar vetores:

//--- sum up all sessions for rendering
   vector total_profile=m_asia+m_europe+m_america;   // profile that combines all sessions
   vector europe_asia=m_asia+m_europe;               // profile that combines only the European and Asian sessions

Basicamente, trata-se de uma união elemento a elemento de vários arrays com o mesmo tamanho em um único resultado, que pode ser representado por um código como este:

#define SIZE   3

double array_1[SIZE]={0,1,2};
double array_2[SIZE]={3,4,5};
double array_3[SIZE]={6,7,8};

Print("Contents of three arrays:");
ArrayPrint(array_1);
ArrayPrint(array_2);
ArrayPrint(array_3);

for(int i=0; i<SIZE; i++)
  {
   array_1[i]+=array_2[i]+=array_3[i];
  }
  
Print("\nResult of the merge:");
ArrayPrint(array_1);
/*
Contents of three arrays:
0.00000 1.00000 2.00000
3.00000 4.00000 5.00000
6.00000 7.00000 8.00000

Result of the merge:
 9.00000 12.00000 15.00000
*/

O código apresentado faz exatamente o que é realizado pela linha do método discutido acima:

vector total_profile=m_asia+m_europe+m_america;   // profile that combines all sessions

Nem é preciso dizer o quanto o código fica mais conveniente e conciso...

No destrutor da classe, o objeto canvas criado é removido e o gráfico é redesenhado para refletir as alterações:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CMarketProfile::~CMarketProfile(void)
  {
//--- delete all graphical objects after use
   ObjectsDeleteAll(0, m_prefix, 0, OBJ_BITMAP);
   ChartRedraw();
  }

Agora, em vez de desenhar com objetos gráficos dentro do laço do indicador, basta criar uma instância da classe discutida para cada barra diária, calcular os dados de todas as sessões e desenhar no canvas o histograma do perfil de mercado de cada dia. A quantidade de objetos gráficos criados será exatamente igual à quantidade de dias configurada nas opções de exibição do perfil, ao contrário da versão anterior do indicador, onde cada tracinho da linha do histograma era desenhado com um objeto gráfico próprio.


Otimizando o indicador

Vamos agora analisar como foi feito o indicador utilizando a classe de perfil de mercado. Abrimos desde o início o arquivo do indicador \MQL5\Indicators\Free Indicators\MarketProfile Canvas.mq5 e o estudamos.

Logo no início do arquivo do indicador são incluídos os arquivos da classe para criação simplificada de desenhos personalizados CCanvas e o arquivo da classe para criação de listas fortemente tipadas CArrayList<T>:

//+------------------------------------------------------------------+
//|                                         MarketProfile Canvas.mq5 |
//|                              Copyright 2009-2024, MetaQuotes Ltd |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_plots 0

#include <Canvas\Canvas.mqh>
#include <Generic\ArrayList.mqh>

//--- input parameters

Em seguida, são definidos os parâmetros de entrada do indicador, o prefixo único dos objetos gráficos, a declaração da classe de perfil de mercado e a declaração da lista de objetos dessa classe:

//--- input parameters
input uint  InpStartDate       =0;           /* day number to start calculation */  // 0 - current, 1 - previous, etc.
input uint  InpShowDays        =7;           /* number of days to display */        // starting with and including the day in InpStartDate
input int   InpMultiplier      =1;           /* histogram length multiplier */      
input color InpAsiaSession     =clrGold;     /* Asian session */                    
input color InpEuropeSession   =clrBlue;     /* European session */                 
input color InpAmericaSession  =clrViolet;   /* American session */                 
input uchar InpTransparency    =150;         /* Transparency, 0 = invisible */      // market profile transparency, 0 = fully transparent
input uint  InpEuropeStartHour =8;           /* European session opening hour */    
input uint  InpAmericaStartHour=14;          /* American session opening hour */    

//--- unique prefix to identify graphical objects belonging to the indicator
string ExtPrefixUniq;

//--- declare CMarketProfile class
class CMarketProfile;
//--- declare a list of pointers to objects of the CMarketProfile class
CArrayList<CMarketProfile*> mp_list;

Como a classe de perfil de mercado está escrita abaixo do código principal do indicador, é necessária uma declaração antecipada (forward declaration) da classe, para que não ocorra erro de tipo desconhecido na hora da compilação.

'CMarketProfile' - unexpected token

A lista fortemente tipada conterá ponteiros para objetos do tipo da classe CMarketProfile, que está escrita mais abaixo no código.

No manipulador OnInit(), é criado um prefixo para os objetos gráficos usando os 4 últimos dígitos do número de milissegundos desde o início do sistema:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create a prefix for object names
   string number=StringFormat("%I64d", GetTickCount64());
   ExtPrefixUniq=StringSubstr(number, StringLen(number)-4);
   Print("Indicator \"Market Profile Canvas\" started, prefix=", ExtPrefixUniq);

   return(INIT_SUCCEEDED);
  }

Vamos analisar o código completo do manipulador OnCalculate():

//+------------------------------------------------------------------+
//| 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[])
  {
//--- opening time of the current daily bar
   datetime static open_time=0;

//--- number of the last day for calculations
//--- (if InpStartDate = 0 and InpShowDays = 3, lastday = 3)
//--- (if InpStartDate = 1 and InpShowDays = 3, lastday = 4) etc ...
   uint lastday=InpStartDate+InpShowDays;

//--- if the first calculation has already been made
   if(prev_calculated!=0)
     {
      //--- get the opening time of the current daily bar
      datetime current_open=iTime(Symbol(), PERIOD_D1, 0);
      
      //--- if we do not calculate the current day
      if(InpStartDate!=0)
        {
         //--- if the opening time was not received, leave
         if(open_time==current_open)
            return(rates_total);
        }
      //--- update opening time
      open_time=current_open;
      //--- we will only calculate one day from now on, since all other days have already been calculated during the first run
      lastday=InpStartDate+1;
     }

//--- in a loop for the specified number of days (either InpStartDate+InpShowDays on first run, or InpStartDate+1 on each tick)
   for(uint day=InpStartDate; day<lastday; day++)
     {
      //--- get the data of the day with index day into the structure
      MqlRates day_rate[];
      //--- if the indicator is launched on weekends or holidays when there are no ticks, you should first open the daily chart of the symbol
      //--- if we have not received bar data for the day index of the daily period, we leave until the next call to OnCalculate()
      if(CopyRates(Symbol(), PERIOD_D1, day, 1, day_rate)==-1)
         return(prev_calculated);

      //---  get day start and end time
      datetime start_time=day_rate[0].time;
      datetime stop_time=start_time+PeriodSeconds(PERIOD_D1)-1;

      //--- get all intraday bars of the current day
      MqlRates bars_in_day[];
      if(CopyRates(Symbol(), PERIOD_CURRENT, start_time, stop_time, bars_in_day)==-1)
         return(prev_calculated);

      CMarketProfile *market_profile;
      //--- if the Market Profile has already been created and its drawing has been performed earlier
      if(prev_calculated>0)
        {
         //--- find the Market Profile object (CMarketProfile class) in the list by the opening time of the day with the 'day' index
         market_profile=GetMarketProfileByDate(ExtPrefixUniq, start_time);
         //--- if the object is not found, return zero to completely recalculate the indicator
         if(market_profile==NULL)
           {
            PrintFormat("Market Profile not found for %s. Indicator will be recalculated for all specified days",
                        TimeToString(start_time, TIME_DATE));
            return(0);
           }
         //--- CMarketProfile object is found in the list; set it to High and Low values of the day and pass the array of intraday bars
         //--- in this case, the object is shifted to a new coordinate corresponding to the High of the daily candle, and all arrays (vectors) are reinitialized
         market_profile.SetHiLoBars(day_rate[0].high, day_rate[0].low, bars_in_day);
        }
      //--- if this is the first calculation
      else
        {
         //--- create a new object of the CMarketProfile class to store the Market Profile of the day with 'day' index
         market_profile = new CMarketProfile(ExtPrefixUniq, start_time, stop_time, day_rate[0].high, day_rate[0].low, bars_in_day);
         //--- add a pointer to the created CMarketProfile object to the list
         mp_list.Add(market_profile);
        }
      //--- set canvas dimensions and line drawing parameters
      market_profile.UpdateSizes();
      //--- calculate profiles for each trading session
      market_profile.CalculateSessions();
      //--- draw the Market Profile
      market_profile.Draw(InpMultiplier);
     }
//--- redraw the chart after the loop has been completed and all objects have been created and updated
   ChartRedraw(0);

//--- return the number of bars for the next OnCalculate call
   return(rates_total);
  }

A lógica do manipulador está detalhada nos comentários do código. Resumidamente, ela funciona da seguinte forma:

  • No laço que percorre a quantidade de dias do perfil de mercado a serem exibidos;
    • obtém-se, na estrutura, o dia correspondente ao índice do laço;
      • obtém-se a quantidade de barras do período atual do gráfico que pertencem ao dia selecionado no laço;
      • ou se obtém o objeto de perfil de mercado já criado para o dia selecionado, ou se cria um novo, caso ele ainda não exista na lista;
      • obtém-se o tamanho da barra diária do Low ao High em pixels do gráfico e os arrays (vetores) das sessões de negociação são reinicializados;
    • de acordo com o novo tamanho da barra do dia selecionado, alteramos o tamanho do canvas;
    • recalculamos o perfil de mercado do dia para cada sessão;
    • redesenhamos no canvas os perfis de cada sessão de negociação;
  • Ao final do laço, o gráfico é redesenhado.

No manipulador OnDeinit() do indicador, todos os objetos gráficos criados são removidos:

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- delete all Market Profile graphical objects after use
   Print("Indicator \"Market Profile Canvas\" stopped, delete all objects CMarketProfile with prefix=", ExtPrefixUniq);

//--- in a loop by the number of CMarketProfile objects in the list
   int size=mp_list.Count();
   for(int i=0; i<size; i++)
     {
      //--- get the pointer to the CMarketProfile object from the list by the loop index
      CMarketProfile *market_profile;
      mp_list.TryGetValue(i, market_profile);
      //--- if the pointer is valid and the object exists, delete it
      if(market_profile!=NULL)
         if(CheckPointer(market_profile)!=POINTER_INVALID)
            delete market_profile;
     }
//--- redraw the chart to display the result immediately
   ChartRedraw(0);
  }

No manipulador de eventos OnChartEvent(), os tamanhos do canvas de cada dia do perfil de mercado são ajustados:

//+------------------------------------------------------------------+
//| Custom indicator chart's event handler                           |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
//--- if this is a user event, leave
   if(id>=CHARTEVENT_CUSTOM)
      return;

//--- if there is a chart change, update the sizes of all objects of the CMarketProfile class with redrawing the chart
   if(CHARTEVENT_CHART_CHANGE==id)
     {
      //--- in a loop by the number of CMarketProfile objects in the list
      int size=mp_list.Count();
      for(int i=0; i<size; i++)
        {
         //--- get the pointer to the CMarketProfile object by the loop index
         CMarketProfile *market_profile;
         mp_list.TryGetValue(i, market_profile);
         //--- if the object is received and if it is in the visible area of the chart
         if(market_profile)
            if(market_profile.isVisibleOnChart())
              {
               //--- update canvas dimensions and redraw market profile histograms
               market_profile.UpdateSizes();
               market_profile.Draw(InpMultiplier);
              }
        }
      //--- update the chart after recalculating all Profiles
      ChartRedraw();
     }
  }

Como a escala de exibição do gráfico tanto vertical quanto horizontal pode ser alterada, é necessário que os objetos gráficos, onde os histogramas das sessões de negociação são desenhados, também mudem seus tamanhos conforme a nova escala do gráfico. Por isso, no manipulador de eventos, sempre que houver alteração no gráfico, todos os objetos da classe CMarketProfile devem ser atualizados em tamanho e redesenhados no canvas, que passou a ter um novo tamanho conforme a nova escala do gráfico.

Função que retorna o objeto de perfil de mercado criado para o horário de início de dia especificado:

//+------------------------------------------------------------------+
//| Returns CMarketProfile or NULL by the date                       |
//+------------------------------------------------------------------+
CMarketProfile* GetMarketProfileByDate(string prefix, datetime time)
  {
//--- in a loop by the number of CMarketProfile objects in the list
   int size=mp_list.Count();
   for(int i=0; i<size; i++)
     {
      //--- get the pointer to the CMarketProfile object by the loop index
      CMarketProfile *market_profile;
      mp_list.TryGetValue(i, market_profile);
      //--- if the pointer is valid and the object exists,
      if(market_profile!=NULL)
         if(CheckPointer(market_profile)!=POINTER_INVALID)
           {
            //--- if the Market Profile object obtained by the pointer was created for the required time, return the pointer
            if(market_profile.Check(prefix, time))
               return(market_profile);
           }
     }
//--- nothing found - return NULL
   return(NULL);
  }

A função é usada no laço do indicador que percorre os dias de negociação e retorna um ponteiro para o objeto da classe CMarketProfile da lista, que foi criado para a barra diária com determinado horário de abertura do dia. Permite obter o objeto necessário pelo horário, para posterior atualização.

Considerações finais

Analisamos a possibilidade de otimização do código do indicador visando à redução do consumo de recursos. Eliminamos milhares de objetos gráficos, substituindo-os por apenas um objeto gráfico-desenho para cada dia, no qual é desenhado o perfil de mercado.

Como resultado da otimização realizada, cada dia de negociação, na quantidade configurada nas opções (por padrão, 7), é exibido em seu próprio canvas (objeto OBJ_BITMAP), onde são desenhadas como histogramas as três sessões de negociação (asiática, europeia e americana), cada uma com sua cor definida nas configurações. Para três dias de negociação, o perfil de mercado final terá este aspecto:

Aqui temos apenas três objetos gráficos, nos quais os histogramas das sessões foram desenhados com a ajuda da classe CCanvas. É possível notar claramente que a atualização "ao vivo" das imagens, mesmo para apenas três objetos gráficos do tipo "Desenho", causa um leve cintilar e pequenos saltos nas imagens. Isso indica que ainda há espaço para uma nova rodada de otimização do código. De qualquer forma, agora, em vez de milhares de objetos gráficos, temos apenas três. O que representa uma economia significativa no uso de recursos. E os artefatos visuais podem ser corrigidos com uma análise adicional do código para sua eliminação (como, por exemplo, o método isChartScaleChanged() ainda não implementado da classe CMarketProfile, que pode ser usado para tentar redesenhar apenas no momento de uma real mudança de escala do gráfico, e não a cada modificação).

Concluindo, podemos afirmar com segurança que qualquer código pode ser otimizado. Mesmo que, para isso, seja necessário recorrer a uma abordagem diferente para a construção do componente visual, como foi feito neste indicador.

O artigo é acompanhado pelo arquivo completo e comentado do indicador, que pode ser baixado e estudado por conta própria, permitindo, se desejado, continuar com sua otimização.

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/16579

Arquivos anexados |
Últimos Comentários | Ir para discussão (3)
__zeus__
__zeus__ | 8 jan. 2025 em 06:32
Por que não escrever o perfil de volume perfeito?
Ihor Herasko
Ihor Herasko | 8 jan. 2025 em 08:38
__zeus__ #:
Por que não escrever um perfil de volume perfeito?

O que significa "perfeito"?

Artyom Trishkin
Artyom Trishkin | 8 jan. 2025 em 10:58
__zeus__ #:
Por que não escrever o perfil de volume perfeito?
Apoio o Igor em sua pergunta
Redes neurais em trading: Conjunto de agentes com uso de mecanismos de atenção (MASAAT) Redes neurais em trading: Conjunto de agentes com uso de mecanismos de atenção (MASAAT)
Apresentamos a estrutura adaptativa multiagente para otimização de portfólio financeiro (MASAAT), que integra mecanismos de atenção e análise de séries temporais. O MASAAT forma um conjunto de agentes que analisam séries de preços e mudanças direcionais, permitindo identificar variações significativas nos preços dos ativos em diferentes níveis de detalhamento.
Redes neurais em trading: Modelo adaptativo multiagente (Conclusão) Redes neurais em trading: Modelo adaptativo multiagente (Conclusão)
No artigo anterior, conhecemos o framework adaptativo multiagente MASA, que combina abordagens de aprendizado por reforço com estratégias adaptativas, garantindo um equilíbrio harmônico entre lucratividade e riscos em condições turbulentas de mercado. Implementamos o funcional de agentes individuais deste framework, e neste artigo continuaremos o trabalho iniciado, levando-o à sua conclusão lógica.
ADAM Populacional (estimativa adaptativa de momentos) ADAM Populacional (estimativa adaptativa de momentos)
Este artigo apresenta a transformação do conhecido e popular método de otimização por gradiente ADAM em um algoritmo populacional e sua modificação com a introdução de indivíduos híbridos. A nova abordagem permite criar agentes que combinam elementos de soluções bem-sucedidas usando uma distribuição probabilística. A principal inovação é a formação de indivíduos híbridos populacionais, que acumulam de forma adaptativa informações das soluções mais promissoras, aumentando a eficácia da busca em espaços multidimensionais complexos.
Introdução ao MQL5 (Parte 9): Compreendendo e Usando Objetos no MQL5 Introdução ao MQL5 (Parte 9): Compreendendo e Usando Objetos no MQL5
Aprenda a criar e personalizar objetos de gráfico no MQL5 usando dados atuais e históricos. Este guia baseado em projetos ajuda você a visualizar negociações e aplicar conceitos do MQL5 na prática, facilitando a criação de ferramentas adaptadas às suas necessidades de negociação.