English Русский 中文 Español Deutsch 日本語
preview
Construindo um Indicador Keltner Channel com Gráficos Canvas Personalizados em MQL5

Construindo um Indicador Keltner Channel com Gráficos Canvas Personalizados em MQL5

MetaTrader 5Sistemas de negociação |
19 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introdução

Neste artigo, construímos um indicador personalizado Keltner Channel com gráficos canvas avançados em MetaQuotes Language 5 (MQL5). O Keltner Channel calcula níveis dinâmicos de suporte e resistência usando os indicadores Moving Average (MA) e Average True Range (ATR), ajudando traders a identificar direções de tendência e possíveis rompimentos. Os tópicos que abordaremos neste artigo incluem:

  1. Compreendendo o Indicador Keltner Channel
  2. Blueprint: Detalhando a Arquitetura do Indicador
  3. Implementação em MQL5
  4. Integração de Gráficos Canvas Personalizados
  5. Backtesting do Indicador Keltner Channel
  6. Conclusão


Compreendendo o Indicador Keltner Channel

O indicador Keltner Channel é uma ferramenta baseada em volatilidade que os traders utilizam, que usa um indicador de Média Móvel (MA) para suavizar os dados de preço e o indicador Average True Range (ATR) para definir níveis dinâmicos de suporte e resistência. Ele possui 3 linhas que formam um canal. A linha do meio do canal é uma média móvel, normalmente escolhida para refletir a tendência predominante. Ao mesmo tempo, as bandas superior e inferior são geradas somando e subtraindo um múltiplo do ATR. Esse método permite que o indicador se ajuste à volatilidade do mercado, facilitando a identificação de possíveis pontos de rompimento ou áreas onde a ação do preço pode reverter.

Na prática, o indicador nos ajuda a identificar níveis-chave no mercado onde o momentum pode mudar. Quando os preços se movem para fora das bandas superior ou inferior, isso sinaliza um mercado estendido demais ou uma possível reversão, fornecendo insights acionáveis tanto para estratégias de seguimento de tendência quanto de reversão à média. Sua natureza dinâmica significa que o indicador se adapta às mudanças de volatilidade, garantindo que os níveis de suporte e resistência permaneçam relevantes conforme as condições de mercado evoluem. Aqui está um exemplo visual.

AMOSTRA DO CANAL


Blueprint: Detalhando a Arquitetura do Indicador

Construiremos a arquitetura do indicador com uma separação clara de responsabilidades: parâmetros de entrada, buffers do indicador e propriedades gráficas. Começaremos definindo as principais entradas, como o período da média móvel, o período do ATR e o multiplicador do ATR, que determinarão o comportamento do indicador. Em seguida, alocaremos três buffers para armazenar os valores do canal superior, da linha central e do canal inferior. Esses buffers serão vinculados a plots gráficos, com propriedades como cor, largura de linha e deslocamento de desenho configuradas usando funções nativas do MQL5. Além disso, utilizaremos funções internas para realizar os cálculos, garantindo que o indicador se adapte dinamicamente à volatilidade do mercado.

Também incorporaremos tratamento de erros para garantir que ambos os handles de indicadores sejam criados com sucesso, fornecendo uma base confiável para os cálculos do indicador. Além da lógica principal do indicador, integraremos gráficos canvas personalizados para aprimorar a apresentação visual, incluindo a criação de um rótulo bitmap sobreposto ao gráfico. Esse design modular não apenas simplificará a depuração e futuras modificações, como também garantirá que cada componente — desde o cálculo dos dados até a saída visual — opere em harmonia, entregando uma ferramenta de trading robusta e visualmente atraente. Em resumo, aqui estão as três coisas que alcançaremos.

ARQUITETURA DO INDICADOR


Implementação em MQL5

Para criar o indicador em MQL5, basta abrir o MetaEditor, ir até o Navigator, localizar a pasta Indicators, clicar na aba "New" e seguir as instruções para criar o arquivo. Depois de criado, no ambiente de codificação definiremos as propriedades e configurações do indicador, como número de buffers, plots e propriedades individuais de linha, como cor, largura e rótulo.

//+------------------------------------------------------------------+
//|                             Keltner Channel Canvas Indicator.mq5 |
//|                        Copyright 2025, Forex Algo-Trader, Allan. |
//|                                 "https://t.me/Forex_Algo_Trader" |
//+------------------------------------------------------------------+
#property copyright "Forex Algo-Trader, Allan"
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property description "Description: Keltner Channel Indicator"
#property indicator_chart_window

//+------------------------------------------------------------------+
//| Indicator properties and settings                                |
//+------------------------------------------------------------------+

// Define the number of buffers used for plotting data on the chart
#property indicator_buffers 3  // We will use 3 buffers: Upper Channel, Middle (MA) line, and Lower Channel

// Define the number of plots on the chart
#property indicator_plots   3  // We will plot 3 lines (Upper, Middle, and Lower)

//--- Plot settings for the Upper Keltner Channel line
#property indicator_type1   DRAW_LINE        // Draw the Upper Channel as a line
#property indicator_color1  clrBlue           // Set the color of the Upper Channel to Blue
#property indicator_label1  "Upper Keltner"   // Label of the Upper Channel line in the Data Window
#property indicator_width1  2                 // Set the line width of the Upper Channel to 2 pixels

//--- Plot settings for the Middle Keltner Channel line (the moving average)
#property indicator_type2   DRAW_LINE        // Draw the Middle (MA) Channel as a line
#property indicator_color2  clrGray           // Set the color of the Middle (MA) Channel to Gray
#property indicator_label2  "Middle Keltner"  // Label of the Middle (MA) line in the Data Window
#property indicator_width2  2                 // Set the line width of the Middle (MA) to 2 pixels

//--- Plot settings for the Lower Keltner Channel line
#property indicator_type3   DRAW_LINE        // Draw the Lower Channel as a line
#property indicator_color3  clrRed            // Set the color of the Lower Channel to Red
#property indicator_label3  "Lower Keltner"   // Label of the Lower Channel line in the Data Window
#property indicator_width3  2                 // Set the line width of the Lower Channel to 2 pixels

Começamos definindo os metadados do indicador, como a versão, usando a palavra-chave #property. Em seguida, alocamos três buffers de indicador usando a propriedade indicator_buffers, que armazenarão e gerenciarão os valores calculados para o "Upper Channel", "Middle Moving Average (MA)" e "Lower Channel". Também definimos "indicator_plots" como 3, especificando que três plots gráficos separados serão desenhados no gráfico. Para cada um deles, configuramos propriedades específicas de visualização:

  • Canal Keltner Superior: Atribuímos a macro DRAW_LINE como seu "tipo de indicador", significando que será desenhado como uma linha contínua. A cor é definida como "Blue" usando clrBlue, e o rótulo "Upper Keltner" ajuda a identificá-lo na Data Window. Definimos a largura da linha como 2 pixels para melhor visibilidade.
  • Canal Keltner Central (Média Móvel): Da mesma forma, definimos seu tipo como "DRAW_LINE", usamos a cor "Gray" e atribuimos o rótulo "Middle Keltner". Essa linha representa a média móvel central, que serve como referência principal para as bandas superior e inferior.
  • Canal Keltner Inferior: Essa linha também é definida como DRAW_LINE, com cor "Red" para diferenciá-la das demais. O rótulo "Lower Keltner" é atribuído, e a largura da linha é definida como 2 pixels.

Com as propriedades definidas, podemos avançar para a definição dos parâmetros de entrada.

//+------------------------------------------------------------------+
//| Input parameters for the indicator                               |
//+------------------------------------------------------------------+

//--- Moving Average parameters
input int    maPeriod=20;                 // Moving Average period (number of bars to calculate the moving average)
input ENUM_MA_METHOD maMethod=MODE_EMA;   // Method of the Moving Average (EMA, in this case)
input ENUM_APPLIED_PRICE maPrice=PRICE_CLOSE; // Price used for the Moving Average (closing price of each bar)

//--- ATR parameters
input int    atrPeriod=10;                // ATR period (number of bars used to calculate the Average True Range)
input double atrMultiplier=2.0;           // Multiplier applied to the ATR value to define the channel distance (upper and lower limits)
input bool   showPriceLabel=true;         // Option to show level price labels on the chart (true/false)

Aqui definimos as propriedades de entrada. Para a média móvel, definimos "maPeriod" (padrão 20) para indicar o número de barras utilizadas. "maMethod", do tipo de dado ENUM_MA_METHOD, é definido como "MODE_EMA", especificando uma média móvel exponencial, e "maPrice", do tipo ENUM_APPLIED_PRICE, é definido como "PRICE_CLOSE", significando que os cálculos são baseados nos preços de fechamento.

Para o "ATR", "atrPeriod" (padrão 10) determina quantas barras são usadas para calcular a volatilidade, enquanto "atrMultiplier" (padrão 2.0) define a distância das bandas superior e inferior em relação à média móvel. Por fim, "showPriceLabel" (padrão true) controla se os rótulos de preço aparecem no gráfico. Essas configurações garantem flexibilidade para adaptar o indicador a diferentes condições de mercado. Por fim, precisamos definir os handles de indicador que utilizaremos.

//+------------------------------------------------------------------+
//| Indicator handle declarations                                    |
//+------------------------------------------------------------------+

//--- Indicator handles for the Moving Average and ATR
int    maHandle = INVALID_HANDLE;   // Handle for Moving Average (used to store the result of iMA)
int    atrHandle = INVALID_HANDLE;  // Handle for ATR (used to store the result of iATR)

//+------------------------------------------------------------------+
//| Indicator buffers (arrays for storing calculated values)         |
//+------------------------------------------------------------------+

//--- Buffers for storing the calculated indicator values
double upperChannelBuffer[];  // Buffer to store the Upper Channel values (Moving Average + ATR * Multiplier)
double movingAverageBuffer[]; // Buffer to store the Moving Average values (middle of the channel)
double lowerChannelBuffer[];  // Buffer to store the Lower Channel values (Moving Average - ATR * Multiplier)

//+------------------------------------------------------------------+
//| Global variables for the parameter values                        |
//+------------------------------------------------------------------+

//--- These variables store the actual input parameter values, if necessary for any further use or calculations
int    maPeriodValue;      // Store the Moving Average period value
int    atrPeriodValue;     // Store the ATR period value
double atrMultiplierValue; // Store the ATR multiplier value

//+------------------------------------------------------------------+

Aqui declaramos os handles de indicador, buffers e algumas variáveis globais necessárias para os cálculos do Keltner Channel. Os handles armazenam referências aos indicadores, permitindo recuperar seus valores dinamicamente. Inicializamos "maHandle" e "atrHandle" como INVALID_HANDLE, garantindo gerenciamento adequado antes da atribuição.

Em seguida, definimos os buffers do indicador, que são arrays usados para armazenar valores calculados para plotagem. "upperChannelBuffer" contém os valores do limite superior, "movingAverageBuffer" armazena a linha central da MA e "lowerChannelBuffer" contém o limite inferior. Esses buffers permitirão uma visualização suave do Keltner Channel no gráfico. Por fim, introduzimos variáveis globais para armazenar parâmetros de entrada para uso posterior. "maPeriodValue" e "atrPeriodValue" mantêm os períodos definidos pelo usuário para "MA" e "ATR", enquanto "atrMultiplierValue" armazena o multiplicador usado para determinar a largura do canal. Agora podemos avançar para o manipulador de evento de inicialização, onde realizamos todos os plots necessários do indicador, o mapeamento e também a inicialização dos handles dos indicadores.

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
   //--- Indicator buffers mapping
   // Indicator buffers are used to store calculated indicator values. 
   // We link each buffer to a graphical plot for visual representation.
   SetIndexBuffer(0, upperChannelBuffer, INDICATOR_DATA);  // Buffer for the upper channel line
   SetIndexBuffer(1, movingAverageBuffer, INDICATOR_DATA); // Buffer for the middle (moving average) line
   SetIndexBuffer(2, lowerChannelBuffer, INDICATOR_DATA);  // Buffer for the lower channel line

   //--- Set the starting position for drawing each plot
   // The drawing for each line will only begin after a certain number of bars have passed
   // This is to avoid showing incomplete calculations at the start
   PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, maPeriod + 1); // Start drawing Upper Channel after 'maPeriod + 1' bars
   PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, maPeriod + 1); // Start drawing Middle (MA) after 'maPeriod + 1' bars
   PlotIndexSetInteger(2, PLOT_DRAW_BEGIN, maPeriod + 1); // Start drawing Lower Channel after 'maPeriod + 1' bars

   //--- Set an offset for the plots
   // This shifts the plotted lines by 1 bar to the right, ensuring that the values are aligned properly
   PlotIndexSetInteger(0, PLOT_SHIFT, 1); // Shift the Upper Channel by 1 bar to the right
   PlotIndexSetInteger(1, PLOT_SHIFT, 1); // Shift the Middle (MA) by 1 bar to the right
   PlotIndexSetInteger(2, PLOT_SHIFT, 1); // Shift the Lower Channel by 1 bar to the right

   //--- Define an "empty value" for each plot
   // Any buffer value set to this value will not be drawn on the chart
   // This is useful for gaps where there are no valid indicator values
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0); // Empty value for Upper Channel
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, 0.0); // Empty value for Middle (MA)
   PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, 0.0); // Empty value for Lower Channel

   //--- Set the short name of the indicator (displayed in the chart and Data Window)
   // This sets the name of the indicator that appears on the chart
   IndicatorSetString(INDICATOR_SHORTNAME, "Keltner Channel");

   //--- Customize the label for each buffer in the Data Window
   // This allows for better identification of the individual plots in the Data Window
   string short_name = "KC:"; // Shortened name of the indicator
   PlotIndexSetString(0, PLOT_LABEL, short_name + " Upper");  // Label for the Upper Channel
   PlotIndexSetString(1, PLOT_LABEL, short_name + " Middle"); // Label for the Middle (MA)
   PlotIndexSetString(2, PLOT_LABEL, short_name + " Lower");  // Label for the Lower Channel

   //--- Set the number of decimal places for the indicator values
   // _Digits is the number of decimal places used in the current chart symbol
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits); // Ensures indicator values match the chart's price format

   //--- Create indicators (Moving Average and ATR)
   // These are handles (IDs) for the built-in indicators used to calculate the Keltner Channel
   // iMA = Moving Average (EMA in this case), iATR = Average True Range
   maHandle = iMA(NULL, 0, maPeriod, 0, maMethod, maPrice); // Create MA handle (NULL = current chart, 0 = current timeframe)
   atrHandle = iATR(NULL, 0, atrPeriod); // Create ATR handle (NULL = current chart, 0 = current timeframe)

   //--- Error handling for indicator creation
   // Check if the handle for the Moving Average (MA) is valid
   if(maHandle == INVALID_HANDLE)
     {
      // If the handle is invalid, print an error message and return failure code
      Print("UNABLE TO CREATE THE MA HANDLE REVERTING NOW!");
      return (INIT_FAILED); // Initialization failed
     }

   // Check if the handle for the ATR is valid
   if(atrHandle == INVALID_HANDLE)
     {
      // If the handle is invalid, print an error message and return failure code
      Print("UNABLE TO CREATE THE ATR HANDLE REVERTING NOW!");
      return (INIT_FAILED); // Initialization failed
     }

   //--- Return success code
   // If everything works correctly, we return INIT_SUCCEEDED to signal successful initialization
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+

Aqui, inicializamos o indicador Keltner Channel no manipulador de evento OnInit configurando buffers, plots, deslocamentos e handles para garantir visualização e cálculo corretos. Começamos vinculando cada buffer do indicador ao seu respectivo plot gráfico usando a função SetIndexBuffer, garantindo que o "Upper Channel", a "Middle Moving Average (MA) Line" e o "Lower Channel" sejam exibidos corretamente.

Em seguida, definimos o comportamento de desenho usando a função PlotIndexSetInteger. Configuramos o desenho para começar somente após "maPeriod + 1" barras, evitando que cálculos incompletos apareçam. Além disso, aplicamos um deslocamento para a direita usando PLOT_SHIFT para alinhar corretamente os valores plotados. Para lidar com dados ausentes, atribuímos um valor vazio de "0.0" a cada buffer usando a função PlotIndexSetDouble.

Em seguida, configuramos as definições de exibição. O nome do indicador é definido usando a função IndicatorSetString, enquanto PlotIndexSetString atribui rótulos para cada linha na "Data Window". A precisão decimal dos valores do indicador é sincronizada com o formato de preço do gráfico usando a função "IndicatorSetInteger". Por fim, criamos os handles do indicador usando as funções iMA e iATR. Se a criação de qualquer handle falhar, tratamos o erro imprimindo uma mensagem usando a função Print e retornando INIT_FAILED. Se tudo for bem-sucedido, INIT_SUCCEEDED é retornado, concluindo o processo de inicialização. Então podemos avançar para o manipulador de evento principal, que trata os cálculos do indicador.

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,             // Total number of bars in the price series
                const int prev_calculated,        // Number of previously calculated bars
                const datetime &time[],           // Array of time values for each bar
                const double &open[],             // Array of open prices for each bar
                const double &high[],             // Array of high prices for each bar
                const double &low[],              // Array of low prices for each bar
                const double &close[],            // Array of close prices for each bar
                const long &tick_volume[],        // Array of tick volumes for each bar
                const long &volume[],             // Array of trade volumes for each bar
                const int &spread[])              // Array of spreads for each bar
{
   //--- Check if this is the first time the indicator is being calculated
   if(prev_calculated == 0) // If no previous bars were calculated, it means this is the first calculation
     {
      //--- Initialize indicator buffers (upper, middle, and lower) with zeros
      ArrayFill(upperChannelBuffer, 0, rates_total, 0);   // Fill the entire upper channel buffer with 0s
      ArrayFill(movingAverageBuffer, 0, rates_total, 0);  // Fill the moving average buffer with 0s
      ArrayFill(lowerChannelBuffer, 0, rates_total, 0);   // Fill the lower channel buffer with 0s

      //--- Copy Exponential Moving Average (EMA) values into the moving average buffer
      // This function requests 'rates_total' values from the MA indicator (maHandle) and copies them into movingAverageBuffer
      if(CopyBuffer(maHandle, 0, 0, rates_total, movingAverageBuffer) < 0)
         return(0); // If unable to copy data, stop execution and return 0

      //--- Copy Average True Range (ATR) values into a temporary array called atrValues
      double atrValues[];
      if(CopyBuffer(atrHandle, 0, 0, rates_total, atrValues) < 0)
         return(0); // If unable to copy ATR data, stop execution and return 0

      //--- Define the starting bar for calculations
      // We need to make sure we have enough data to calculate both the MA and ATR, so we start after the longest required period.
      int startBar = MathMax(maPeriod, atrPeriod) + 1; // Ensure sufficient bars for both EMA and ATR calculations

      //--- Loop from startBar to the total number of bars (rates_total)
      for(int i = startBar; i < rates_total; i++)
        {
         // Calculate the upper and lower channel boundaries for each bar
         upperChannelBuffer[i] = movingAverageBuffer[i] + atrMultiplier * atrValues[i]; // Upper channel = EMA + ATR * Multiplier
         lowerChannelBuffer[i] = movingAverageBuffer[i] - atrMultiplier * atrValues[i]; // Lower channel = EMA - ATR * Multiplier
        }

      //--- Calculation is complete, so we return the total number of rates (bars) calculated
      return(rates_total);
     }
}

Aqui, implementamos a lógica principal de cálculo do indicador Keltner Channel dentro do manipulador de evento OnCalculate, uma função que percorre os dados de preço para calcular e atualizar os buffers do indicador. Primeiro, verificamos se este é o primeiro cálculo avaliando "prev_calculated". Se for "0", inicializamos os buffers "Upper Channel", "Middle Moving Average (MA)" e "Lower Channel" usando a função ArrayFill, garantindo que todos os valores comecem em zero. Em seguida, preenchemos o "movingAverageBuffer" com os valores da MA usando a função CopyBuffer. Se a cópia falhar, interrompemos a execução retornando "0". Da mesma forma, recuperamos os valores de ATR no array temporário "atrValues".

Para garantir que temos dados suficientes tanto para MA quanto para ATR, determinamos a barra inicial usando a função MathMax, que retorna o maior valor entre os períodos dos indicadores, e adicionamos 1 barra para evitar considerar a barra atual incompleta. Em seguida, usamos um for loop para iterar por cada barra de "startBar" até "rates_total", calculando os limites do canal "Upper" e "Lower" usando a fórmula:

  • "UpperChannel = Moving Average + (ATR * Multiplier)"
  • "LowerChannel = Moving Average - (ATR * Multiplier)"

Por fim, retornamos "rates_total", indicando o número de barras calculadas. Se não for a primeira execução do indicador, apenas atualizamos os valores das barras recentes por meio de recálculo.

//--- If this is NOT the first calculation, update only the most recent bars
// This prevents re-calculating all bars, which improves performance
int startBar = prev_calculated - 2; // Start 2 bars back to ensure smooth updating

//--- Loop through the last few bars that need to be updated
for(int i = startBar; i < rates_total; i++)
  {
   //--- Calculate reverse index to access recent bars from the end
   int reverseIndex = rates_total - i; // Reverse indexing ensures we are looking at the most recent bars first

   //--- Copy the latest Exponential Moving Average (EMA) value for this specific bar
   double emaValue[];
   if(CopyBuffer(maHandle, 0, reverseIndex, 1, emaValue) < 0)
      return(prev_calculated); // If unable to copy, return the previous calculated value to avoid recalculation

   //--- Copy the latest Average True Range (ATR) value for this specific bar
   double atrValue[];
   if(CopyBuffer(atrHandle, 0, reverseIndex, 1, atrValue) < 0)
      return(prev_calculated); // If unable to copy, return the previous calculated value to avoid recalculation

   //--- Update the indicator buffers with new values for the current bar
   movingAverageBuffer[i] = emaValue[0]; // Update the moving average buffer for this bar
   upperChannelBuffer[i] = emaValue[0] + atrMultiplier * atrValue[0]; // Calculate the upper channel boundary
   lowerChannelBuffer[i] = emaValue[0] - atrMultiplier * atrValue[0]; // Calculate the lower channel boundary
  }
   
//--- Return the total number of calculated rates (bars)
return(rates_total); // This informs MQL5 that all rates up to 'rates_total' have been successfully calculated

Aqui, otimizamos o desempenho atualizando apenas as barras mais recentes em vez de recalcular todo o indicador a cada tick. Se não for o primeiro cálculo, definimos "startBar" como "prev_calculated - 2", garantindo que atualizemos as últimas barras enquanto mantemos a continuidade. Isso minimiza cálculos desnecessários, pois já possuímos os dados das barras anteriores no gráfico.

Em seguida, iteramos de "startBar" até "rates_total" usando um loop for. Para priorizar as barras recentes, calculamos "reverseIndex = rates_total - i", permitindo buscar primeiro os dados mais recentes. Para cada barra, copiamos o valor mais recente da MA para "emaValue" usando a função CopyBuffer. Se a recuperação de dados falhar, retornamos "prev_calculated", evitando cálculos redundantes. A mesma lógica se aplica ao ATR, armazenando seu valor em "atrValue". Após recuperados, atualizamos os buffers:

  • "movingAverageBuffer[i] = emaValue[0];" atribui a EMA à linha central.
  • "upperChannelBuffer[i] = emaValue[0] + atrMultiplier * atrValue[0];" calcula o limite superior.
  • "lowerChannelBuffer[i] = emaValue[0] - atrMultiplier * atrValue[0];" calcula o limite inferior.

Por fim, retornamos "rates_total", sinalizando que todas as barras necessárias foram processadas. Ao rodar o programa, temos a seguinte saída:

BANDAS DO INDICADOR

Pela imagem, podemos ver que as linhas do indicador estão mapeadas corretamente no gráfico. O que resta agora é plotar os canais e, para isso, precisaremos do recurso de canvas. Isso é tratado na seção seguinte.


Integração de Gráficos Canvas Personalizados

Para integrar o recurso de canvas para gráficos, precisaremos incluir os arquivos de classe canvas necessários para que possamos usar a estrutura já existente. Fazemos isso por meio da seguinte lógica:

#include <Canvas/Canvas.mqh>
CCanvas obj_Canvas;

Incluímos a biblioteca "Canvas.mqh" usando a palavra-chave #include, que fornece funcionalidades de renderização gráfica no gráfico. Essa biblioteca permitirá desenhar elementos personalizados, como visuais do indicador e anotações, diretamente na janela do gráfico. Em seguida, declaramos "obj_Canvas" como uma instância da classe CCanvas. Esse objeto será usado para interagir com o canvas, permitindo criar, modificar e gerenciar elementos gráficos dinamicamente. A classe CCanvas fornecerá métodos para desenhar formas, linhas e textos, aprimorando a representação visual do indicador. Em seguida, precisaremos obter propriedades do gráfico, como a escala, já que apontaremos o gráfico com formas dinâmicas. Fazemos isso no escopo global.

int chart_width         = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
int chart_height        = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
int chart_scale         = (int)ChartGetInteger(0, CHART_SCALE);
int chart_first_vis_bar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);
int chart_vis_bars      = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
double chart_prcmin     = ChartGetDouble(0, CHART_PRICE_MIN, 0);
double chart_prcmax     = ChartGetDouble(0, CHART_PRICE_MAX, 0);

Usamos várias funções ChartGetInteger e ChartGetDouble para recuperar diferentes propriedades da janela atual do gráfico para cálculos adicionais ou posicionamento gráfico de elementos. Primeiro, recuperamos a largura do gráfico usando "ChartGetInteger", com o parâmetro CHART_WIDTH_IN_PIXELS, que retorna a largura do gráfico em pixels. Armazenamos esse valor na variável "chart_width". Da mesma forma, recuperamos a altura do gráfico com "ChartGetInteger" e CHART_HEIGHT_IN_PIXELS, armazenando-a na variável "chart_height".

Em seguida, usamos o parâmetro "CHART_SCALE" para recuperar a escala do gráfico, armazenando esse valor em "chart_scale". Isso representa o nível de zoom do gráfico. Também recuperamos o índice da primeira barra visível usando "CHART_FIRST_VISIBLE_BAR", armazenando-o em "chart_first_vis_bar", o que é útil para cálculos baseados na área visível do gráfico. Para calcular quantas barras estão visíveis na janela do gráfico, usamos o parâmetro CHART_VISIBLE_BARS, armazenando o resultado em "chart_vis_bars". Fazemos o typecast de todos os valores para inteiros.

Por fim, usamos a função "ChartGetDouble" para obter os valores mínimo e máximo de preço visíveis no gráfico com "CHART_PRICE_MIN" e CHART_PRICE_MAX, respectivamente. Esses valores são armazenados nas variáveis "chart_prcmin" e "chart_prcmax", que fornecem a faixa de preço atualmente exibida no gráfico. Munidos dessas variáveis, precisaremos criar um rótulo bitmap no gráfico durante a inicialização, para que possamos ter nossa área de plot pronta.

// Create a obj_Canvas bitmap label for custom graphics on the chart
obj_Canvas.CreateBitmapLabel(0, 0, short_name, 0, 0, chart_width, chart_height, COLOR_FORMAT_ARGB_NORMALIZE);

Aqui, usamos a função "obj_Canvas.CreateBitmapLabel" para criar um rótulo bitmap personalizado no gráfico. Ela recebe parâmetros de posicionamento ("0", "0"), conteúdo ("short_name"), tamanho ("0", "0" para dimensionamento automático) e dimensões do gráfico ("chart_width", "chart_height"). O formato de cor é definido como COLOR_FORMAT_ARGB_NORMALIZE, permitindo transparência e cores personalizadas. Com o rótulo, agora podemos desenhar as formas. No entanto, precisaremos de algumas funções auxiliares que converterão as coordenadas do gráfico e das velas em índices de preço e barra.

//+------------------------------------------------------------------+
//| Converts the chart scale property to bar width/spacing           |
//+------------------------------------------------------------------+
int GetBarWidth(int chartScale) 
{
   // The width of each bar in pixels is determined using 2^chartScale.
   // This calculation is based on the MQL5 chart scale property, where larger chartScale values mean wider bars.
   return (int)pow(2, chartScale); // Example: chartScale = 3 -> bar width = 2^3 = 8 pixels
}
//+------------------------------------------------------------------+
//| Converts the bar index (as series) to x-coordinate in pixels     |
//+------------------------------------------------------------------+
int GetXCoordinateFromBarIndex(int barIndex) 
{
   // The chart starts from the first visible bar, and each bar has a fixed width.
   // To calculate the x-coordinate, we calculate the distance from the first visible bar to the given barIndex.
   // Each bar is shifted by 'bar width' pixels, and we subtract 1 to account for pixel alignment.
   return (chart_first_vis_bar - barIndex) * GetBarWidth(chart_scale) - 1;
}
//+------------------------------------------------------------------+
//| Converts the price to y-coordinate in pixels                     |
//+------------------------------------------------------------------+
int GetYCoordinateFromPrice(double price)
{
   // To avoid division by zero, we check if chart_prcmax equals chart_prcmin.
   // If so, it means that all prices on the chart are the same, so we avoid dividing by zero.
   if(chart_prcmax - chart_prcmin == 0.0)
      return 0; // Return 0 to avoid undefined behavior

   // Calculate the relative position of the price in relation to the minimum and maximum price on the chart.
   // We then convert this to pixel coordinates based on the total height of the chart.
   return (int)round(chart_height * (chart_prcmax - price) / (chart_prcmax - chart_prcmin) - 1);
}
//+------------------------------------------------------------------+
//| Converts x-coordinate in pixels to bar index (as series)         |
//+------------------------------------------------------------------+
int GetBarIndexFromXCoordinate(int xCoordinate)
{
   // Get the width of one bar in pixels
   int barWidth = GetBarWidth(chart_scale);
   
   // Check to avoid division by zero in case barWidth somehow equals 0
   if(barWidth == 0)
      return 0; // Return 0 to prevent errors
   
   // Calculate the bar index using the x-coordinate position
   // This determines how many bar widths fit into the x-coordinate and converts it to a bar index
   return chart_first_vis_bar - (xCoordinate + barWidth / 2) / barWidth;
}
//+------------------------------------------------------------------+
//| Converts y-coordinate in pixels to price                         |
//+------------------------------------------------------------------+
double GetPriceFromYCoordinate(int yCoordinate)
{
   // If the chart height is 0, division by zero would occur, so we avoid it.
   if(chart_height == 0)
      return 0; // Return 0 to prevent errors

   // Calculate the price corresponding to the y-coordinate
   // The y-coordinate is converted relative to the total height of the chart
   return chart_prcmax - yCoordinate * (chart_prcmax - chart_prcmin) / chart_height;
}

Criamos funções para mapear entre dados do gráfico e coordenadas baseadas em pixels. Primeiro, na função "GetBarWidth", calculamos a largura de cada barra em pixels usando a escala do gráfico e aplicando a fórmula 2 elevado à potência da escala do gráfico. Isso ajudará a ajustar a largura da barra com base na escala do gráfico. Para isso, usamos a função pow para calcular potências de 2.

Em seguida, na função "GetXCoordinateFromBarIndex", convertemos um índice de barra em uma coordenada x em pixels. Isso é feito calculando a distância entre a primeira barra visível e o índice de barra especificado. Multiplicamos isso pela largura da barra e subtraímos 1 para considerar o alinhamento de pixels. Para a coordenada y, na função "GetYCoordinateFromPrice", calculamos a posição relativa de um preço no gráfico. Determinamos onde o preço está entre os preços mínimo e máximo do gráfico ("chart_prcmin" e "chart_prcmax") e então escalamos esse valor relativo para caber dentro da altura do gráfico. Tomamos cuidado para evitar divisão por zero caso a faixa de preço seja zero.

De forma semelhante, a função "GetBarIndexFromXCoordinate" funciona ao contrário. Pegamos uma coordenada x e a convertemos de volta em um índice de barra calculando quantas larguras de barra cabem na coordenada x. Isso permite identificar a barra correspondente a uma determinada posição no gráfico. Por fim, na função "GetPriceFromYCoordinate", convertemos uma coordenada y de volta em preço usando a posição relativa da coordenada y dentro da faixa de preço do gráfico. Garantimos que a divisão por zero seja evitada caso a altura do gráfico seja zero.

Juntas, essas funções nos fornecem a capacidade de traduzir entre coordenadas de pixels do gráfico e valores de dados, permitindo posicionar gráficos personalizados no gráfico com alinhamento preciso ao preço e às barras. Assim, agora podemos usar essas funções para criar uma função comum, que utilizaremos para desenhar as formas necessárias entre duas linhas de um canal.

//+------------------------------------------------------------------+
//| Fill the area between two indicator lines                        |
//+------------------------------------------------------------------+
void DrawFilledArea(double &upperSeries[], double &lowerSeries[], color upperColor, color lowerColor, uchar transparency = 255, int shift = 0)
{
   int startBar  = chart_first_vis_bar;      // The first bar that is visible on the chart
   int totalBars = chart_vis_bars + shift;   // The total number of visible bars plus the shift
   uint upperARGB = ColorToARGB(upperColor, transparency); // Convert the color to ARGB with transparency
   uint lowerARGB = ColorToARGB(lowerColor, transparency); // Convert the color to ARGB with transparency
   int seriesLimit = fmin(ArraySize(upperSeries), ArraySize(lowerSeries)); // Ensure series limits do not exceed array size
   int prevX = 0, prevYUpper = 0, prevYLower = 0; // Variables to store the previous bar's x, upper y, and lower y coordinates
   
   for(int i = 0; i < totalBars; i++)
     {
      int barPosition = startBar - i;             // Current bar position relative to start bar
      int shiftedBarPosition = startBar - i + shift; // Apply the shift to the bar position
      int barIndex = seriesLimit - 1 - shiftedBarPosition; // Calculate the series index for the bar

      // Ensure the bar index is within the valid range of the array
      if(barIndex < 0 || barIndex >= seriesLimit || barIndex - 1 < 0)
         continue; // Skip this bar if the index is out of bounds

      // Check if the series contains valid data (not EMPTY_VALUE)
      if(upperSeries[barIndex] == EMPTY_VALUE || lowerSeries[barIndex] == EMPTY_VALUE || shiftedBarPosition >= seriesLimit)
         continue; // Skip this bar if the values are invalid or if the position exceeds the series limit

      int xCoordinate  = GetXCoordinateFromBarIndex(barPosition); // Calculate x-coordinate of this bar
      int yUpper = GetYCoordinateFromPrice(upperSeries[barIndex]); // Calculate y-coordinate for upper line
      int yLower = GetYCoordinateFromPrice(lowerSeries[barIndex]); // Calculate y-coordinate for lower line
      uint currentARGB = upperSeries[barIndex] < lowerSeries[barIndex] ? lowerARGB : upperARGB; // Determine fill color based on which line is higher
            
      // If previous values are valid, draw triangles between the previous bar and the current bar
      if(i > 0 && upperSeries[barIndex - 1] != EMPTY_VALUE && lowerSeries[barIndex - 1] != EMPTY_VALUE)
        {
         if(prevYUpper != prevYLower) // Draw first triangle between the upper and lower parts of the two consecutive bars
            obj_Canvas.FillTriangle(prevX, prevYUpper, prevX, prevYLower, xCoordinate, yUpper, currentARGB);
         if(yUpper != yLower) // Draw the second triangle to complete the fill area
            obj_Canvas.FillTriangle(prevX, prevYLower, xCoordinate, yUpper, xCoordinate, yLower, currentARGB);
        }

      prevX  = xCoordinate; // Store the x-coordinate for the next iteration
      prevYUpper = yUpper;  // Store the y-coordinate of the upper series
      prevYLower = yLower;  // Store the y-coordinate of the lower series
     }
}

Declaramos uma função void "DrawFilledArea", que permitirá preencher a área entre duas linhas do indicador no gráfico. Primeiro, definimos as barras visíveis no gráfico com um deslocamento opcional ("shift") para ajustar o ponto inicial. Também convertemos as cores ("upperColor" e "lowerColor") para o formato ARGB, incluindo transparência, usando a função ColorToARGB. Em seguida, determinamos o limite da série usando a função fmin para evitar exceder os tamanhos dos arrays das séries de dados superior e inferior ("upperSeries" e "lowerSeries"). Inicializamos variáveis para armazenar as coordenadas da barra anterior para as linhas superior e inferior, usadas para desenhar a área.

Depois, percorremos as barras visíveis e calculamos a posição de cada barra no eixo x usando a função "GetXCoordinateFromBarIndex". As coordenadas y das linhas superior e inferior são calculadas usando a função "GetYCoordinateFromPrice", com base nos valores em "upperSeries" e "lowerSeries". Verificamos qual linha está mais alta e atribuímos a cor apropriada para o preenchimento.

Se a barra anterior contiver dados válidos, usamos "obj_Canvas.FillTriangle" para preencher a área entre as duas linhas. Desenhamos dois triângulos para cada par de barras: um triângulo entre as linhas superior e inferior, e outro para completar a área preenchida. Os triângulos são desenhados com a cor determinada anteriormente. Usamos triângulos porque conectam com precisão pontos irregulares entre linhas, especialmente quando não estão perfeitamente alinhadas à grade. Esse método garante preenchimentos mais suaves e melhor eficiência de renderização em comparação com retângulos. Aqui está uma ilustração.

RETÂNGULOS VS TRIÂNGULOS PARA FORMAS IRREGULARES

Por fim, atualizamos as coordenadas x e y anteriores para a próxima iteração, garantindo que a área seja preenchida continuamente entre as linhas para cada barra visível. Com a função pronta, avançamos para utilizá-la para desenhar a quantidade necessária de canais no gráfico, com as respectivas cores solicitadas.

//+------------------------------------------------------------------+
//| Custom indicator redraw function                                 |
//+------------------------------------------------------------------+
void RedrawChart(void)
{
   uint defaultColor = 0; // Default color used to clear the canvas
   color colorUp = (color)PlotIndexGetInteger(0, PLOT_LINE_COLOR, 0); // Color of the upper indicator line
   color colorMid = (color)PlotIndexGetInteger(1, PLOT_LINE_COLOR, 0); // Color of the mid indicator line
   color colorDown = (color)PlotIndexGetInteger(2, PLOT_LINE_COLOR, 0); // Color of the lower indicator line
   
   //--- Clear the canvas by filling it with the default color
   obj_Canvas.Erase(defaultColor);
   
   //--- Draw the area between the upper channel and the moving average
   // This fills the area between the upper channel (upperChannelBuffer) and the moving average (movingAverageBuffer)
   DrawFilledArea(upperChannelBuffer, movingAverageBuffer, colorUp, colorMid, 128, 1);
   
   //--- Draw the area between the moving average and the lower channel
   // This fills the area between the moving average (movingAverageBuffer) and the lower channel (lowerChannelBuffer)
   DrawFilledArea(movingAverageBuffer, lowerChannelBuffer, colorDown, colorMid, 128, 1);
   
   //--- Update the canvas to reflect the new drawing
   obj_Canvas.Update();
}

Declaramos a função "RedrawChart", definimos primeiro cores padrão e depois recuperamos as cores das linhas dos canais superior, central e inferior a partir das propriedades do indicador. Limpamos o canvas com a cor padrão e usamos a função "DrawFilledArea" para preencher as áreas entre o canal superior e a média móvel, e entre a média móvel e o canal inferior, usando as respectivas cores. Por fim, atualizamos o canvas para refletir as mudanças, garantindo que o gráfico seja redesenhado com os novos preenchimentos. Agora podemos chamar a função no manipulador de evento OnCalculate para desenhar o canvas.

RedrawChart(); // This function clears and re-draws the filled areas between the indicator lines

Como temos um objeto de canal do indicador, precisamos excluí-lo quando removemos o indicador.

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
   obj_Canvas.Destroy();
   ChartRedraw();
}

No manipulador de evento OnDeinit, usamos o método "obj_Canvas.Destroy" para limpar e remover quaisquer objetos de desenho personalizados no gráfico quando o indicador é removido. Por fim, chamamos a função ChartRedraw para atualizar e redesenhar o gráfico, garantindo que os gráficos personalizados sejam removidos da exibição. Após executar o programa, temos o seguinte resultado.

RESULTADO FINAL

A partir da visualização, podemos ver que alcançamos nosso objetivo de criar o indicador Keltner Channel avançado com gráficos em canvas. Agora precisamos fazer o backtest do indicador para garantir que ele esteja funcionando corretamente. Isso será feito na próxima seção.


Backtesting do Indicador Keltner Channel

Durante o backtest, observamos que, quando as dimensões do gráfico eram alteradas, a exibição do canal travava e não atualizava para as coordenadas recentes do gráfico. Aqui está o que queremos dizer.

INDICADOR NÃO RESPONSIVO ÀS MUDANÇAS DO GRÁFICO

Para resolver isso, implementamos uma lógica para atualizá-lo no manipulador de evento OnChartEvent.

//+------------------------------------------------------------------+
//| Custom indicator chart event handler function                    |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam){
   if(id != CHARTEVENT_CHART_CHANGE)
      return;
   chart_width          = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   chart_height         = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   chart_scale          = (int)ChartGetInteger(0, CHART_SCALE);
   chart_first_vis_bar  = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);
   chart_vis_bars       = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   chart_prcmin         = ChartGetDouble(0, CHART_PRICE_MIN, 0);
   chart_prcmax         = ChartGetDouble(0, CHART_PRICE_MAX, 0);
   if(chart_width != obj_Canvas.Width() || chart_height != obj_Canvas.Height())
      obj_Canvas.Resize(chart_width, chart_height);
//---
   RedrawChart();
}

Aqui, tratamos a função OnChartEvent, que escuta o evento CHARTEVENT_CHART_CHANGE. Quando as dimensões do gráfico mudam (como ao redimensionar o gráfico), primeiro recuperamos as propriedades atualizadas do gráfico, como a largura ("CHART_WIDTH_IN_PIXELS"). Em seguida, verificamos se a nova largura e altura do gráfico diferem do tamanho atual do canvas usando "obj_Canvas.Width" e "obj_Canvas.Height". Se diferirem, redimensionamos o canvas com "obj_Canvas.Resize". Por fim, chamamos a função "RedrawChart" para atualizar o gráfico e garantir que todos os elementos visuais sejam renderizados corretamente com as novas dimensões. O resultado é o seguinte.

INDICADOR RESPONSIVO ÀS MUDANÇAS DO GRÁFICO

A partir da visualização, podemos ver que as mudanças estão sendo aplicadas dinamicamente quando redimensionamos o gráfico, alcançando assim nosso objetivo.


Conclusão

Em conclusão, este artigo abordou a construção de um indicador MQL5 personalizado usando médias móveis e a ferramenta average true range para criar canais dinâmicos. Focamos no cálculo e na exibição desses canais com um mecanismo de preenchimento, além de tratar melhorias de desempenho para redimensionamento do gráfico e backtesting, garantindo eficiência e precisão para os traders.

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

Criando um Painel Administrador de Trading em MQL5 (Parte IX): Organização de Código (II): Modularização Criando um Painel Administrador de Trading em MQL5 (Parte IX): Organização de Código (II): Modularização
Nesta discussão, damos um passo adiante ao dividir nosso programa MQL5 em módulos menores e mais gerenciáveis. Esses componentes modulares serão então integrados ao programa principal, melhorando sua organização e capacidade de manutenção. Essa abordagem simplifica a estrutura do programa principal e torna os componentes individuais reutilizáveis em outros Expert Advisors (EAs) e no desenvolvimento de indicadores. Ao adotar esse design modular, criamos uma base sólida para melhorias futuras, beneficiando tanto nosso projeto quanto a comunidade mais ampla de desenvolvedores.
Estratégia evolutiva de adaptação da matriz de covariância, Covariance Matrix Adaptation Evolution Strategy (CMA-ES) Estratégia evolutiva de adaptação da matriz de covariância, Covariance Matrix Adaptation Evolution Strategy (CMA-ES)
Vamos explorar um dos algoritmos mais interessantes de otimização sem gradiente, que aprende a compreender a geometria da função objetivo. Consideraremos a implementação clássica do CMA-ES com uma pequena modificação, substituindo a distribuição normal por uma distribuição de potência. Uma análise detalhada da matemática do algoritmo, a implementação prática e uma avaliação honesta, onde o CMA-ES é imbatível e onde é melhor não aplicá-lo.
Desenvolvimento do Toolkit de Análise de Price Action (Parte 13): Ferramenta RSI Sentinel Desenvolvimento do Toolkit de Análise de Price Action (Parte 13): Ferramenta RSI Sentinel
A análise de price action pode ser realizada de forma eficaz por meio da identificação de divergências, utilizando indicadores técnicos como o RSI para fornecer sinais cruciais de confirmação. Neste conteúdo, é explicado como a análise automatizada de divergência do RSI pode identificar continuações de tendência e reversões, oferecendo percepções valiosas sobre o sentimento do mercado.
Redes neurais em trading: Pipeline inteligente de previsões (Mistura esparsa de especialistas) Redes neurais em trading: Pipeline inteligente de previsões (Mistura esparsa de especialistas)
Propomos conhecer a implementação prática do bloco de mistura esparsa de especialistas para séries temporais no ambiente computacional OpenCL. No artigo, é analisado passo a passo o funcionamento da convolução multi-janela mascarada, bem como a organização do aprendizado por gradiente em condições de múltiplos fluxos de informação.