English Русский 中文 Deutsch 日本語
preview
Creación de un indicador canal de Keltner con gráficos personalizados en Canvas en MQL5

Creación de un indicador canal de Keltner con gráficos personalizados en Canvas en MQL5

MetaTrader 5Sistemas comerciales |
147 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introducción

En este artículo, creamos un indicador personalizado canal de Keltner con gráficos avanzados Canvas en MetaQuotes Language 5 (MQL5). El canal de Keltner calcula los niveles dinámicos de soporte y resistencia utilizando los indicadores media móvil (Moving Average, MA) y rango medio verdadero (Average True Range, ATR), lo que ayuda a los operadores a detectar las direcciones de las tendencias y las posibles rupturas. Los temas que trataremos en este artículo incluyen:

  1. Comprender el indicador del canal de Keltner
  2. Plan: Desglose de la arquitectura del indicador
  3. Implementación en MQL5
  4. Integración de gráficos personalizados en Canvas
  5. Pruebas retrospectivas del indicador del canal de Keltner
  6. Conclusión


Comprender el indicador del canal de Keltner

El indicador canal de Keltner es una herramienta basada en la volatilidad que utilizan los operadores y que emplea un indicador de media móvil (MA) para suavizar los datos de precios y el indicador de rango verdadero medio (ATR) para establecer niveles dinámicos de soporte y resistencia. Tiene 3 líneas que forman un canal. La línea media del canal es una media móvil, que normalmente se elige para reflejar la tendencia predominante. Al mismo tiempo, las bandas superior e inferior se generan sumando y restando un múltiplo del ATR. Este método permite que el indicador se ajuste a la volatilidad del mercado, lo que facilita la detección de posibles puntos de ruptura o áreas en las que la acción del precio podría revertirse.

En la práctica, el indicador nos ayuda a identificar niveles clave en el mercado donde el impulso puede cambiar. Cuando los precios se mueven fuera de las bandas superior o inferior, esto indica un mercado sobreextendido o una posible reversión, lo que brinda información útil para estrategias de seguimiento de tendencias y de reversión a la media. Su naturaleza dinámica significa que el indicador se adapta a los cambios en la volatilidad, garantizando que los niveles de soporte y resistencia sigan siendo relevantes a medida que evolucionan las condiciones del mercado. He aquí un ejemplo visual.

MUESTRA DEL CANAL


Plan: Desglose de la arquitectura del indicador

Construiremos la arquitectura del indicador sobre una clara separación de responsabilidades: parámetros de entrada, búferes del indicador y propiedades gráficas. Comenzaremos definiendo los parámetros clave, como el período de la media móvil, el período ATR y el multiplicador ATR, que determinarán el comportamiento del indicador. A continuación, asignaremos tres búferes para almacenar los valores del canal superior, la línea media y el canal inferior. Estos buffers estarán vinculados a gráficos, con propiedades como el color, el grosor de la línea y el desplazamiento del dibujo configurados mediante las funciones integradas de MQL5. Además, utilizaremos las funciones integradas para realizar los cálculos, asegurándonos de que el indicador se adapte dinámicamente a la volatilidad del mercado.

Además, incorporaremos el manejo de errores para garantizar que ambos indicadores se creen correctamente, proporcionando una base fiable para los cálculos del indicador. Más allá de la lógica de los indicadores principales, integraremos gráficos personalizados en lienzo para mejorar la presentación visual, incluida la creación de una etiqueta de mapa de bits que se superpone al gráfico. Este diseño modular no solo simplificará la depuración y las modificaciones futuras, sino que también garantizará que cada componente, desde el cálculo de datos hasta la salida visual, funcione en armonía, lo que dará como resultado una herramienta de negociación robusta y visualmente atractiva. En resumen, estas son las tres cosas que lograremos.

ARQUITECTURA DEL INDICADOR


Implementación en MQL5

Para crear el indicador en MQL5, solo tiene que abrir MetaEditor, ir al Navegador, localizar la carpeta Indicators, hacer clic en la pestaña «Nuevo» y seguir las instrucciones para crear el archivo. Una vez creado, en el entorno de codificación, definiremos las propiedades y la configuración del indicador, como el número de buffers, los plots y las propiedades individuales de las líneas, como el color, el grosor y la etiqueta.

//+------------------------------------------------------------------+
//|                             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

Comenzamos estableciendo los metadatos del indicador, como la versión, utilizando la palabra clave #property. A continuación, asignamos tres buffers de indicadores utilizando la propiedad indicator_buffers, que almacenará y gestionará los valores calculados para «Upper Channel», «Middle Moving Average (MA)» y «Lower Channel». También establecemos «indicator_plots» en 3, lo que define que se dibujarán tres gráficos separados en el gráfico. Para cada uno de ellos, configuramos propiedades de visualización específicas:

  • Upper Channel: Asignamos la macro DRAW_LINE como su «tipo de indicador», lo que significa que se dibujará como una línea continua. El color se establece en «Azul» utilizando clrBlue, y la etiqueta «Upper Keltner» ayuda a identificarlo en la ventana de datos. Hemos establecido el ancho de la línea en 2 píxeles para mejorar la visibilidad.
  • Middle Moving Average (MA): Del mismo modo, establecemos su tipo en «DRAW_LINE», utilizamos el color «Gris» y le asignamos la etiqueta «Middle Keltner». Esta línea representa la media móvil central, que sirve como referencia principal para las bandas superior e inferior.
  • Lower Channel: Esta línea también se define como DRAW_LINE, con un color «rojo» para diferenciarla de las demás. Se asigna la etiqueta «Lower Keltner» y se establece el ancho de la línea en 2 píxeles.

Con las propiedades, podemos pasar a definir los 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)

Aquí definimos las propiedades de entrada. Para la media móvil, establecemos «maPeriod» (por defecto 20) para definir el número de barras utilizadas. El «maMethod», de tipo de datos ENUM_MA_METHOD, se establece en «MODE_EMA», lo que especifica una media móvil exponencial, y el «maPrice», de tipo de datos ENUM_APPLIED_PRICE, se establece en «PRICE_CLOSE», lo que significa que los cálculos se basan en los precios de cierre.

Para «ATR», «atrPeriod» (por defecto 10) determina cuántas barras se utilizan para calcular la volatilidad, mientras que «atrMultiplier» (por defecto 2,0) establece la distancia de las bandas superior e inferior con respecto a la media móvil. Por último, «showPriceLabel» (por defecto true) controla si las etiquetas de precios aparecen en el gráfico. Estos ajustes garantizarán la flexibilidad a la hora de adaptar el indicador a las diferentes condiciones del mercado. Por último, debemos definir los indicadores que vamos a utilizar.

//+------------------------------------------------------------------+
//| 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

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

Aquí declaramos los indicadores, los búferes y algunas variables globales que necesitaremos para los cálculos del canal de Keltner. Los manejadores almacenan referencias a los indicadores, lo que nos permite recuperar sus valores de forma dinámica. Inicializamos «maHandle» y «atrHandle» a INVALID_HANDLE, lo que garantiza una gestión adecuada de los identificadores antes de la asignación.

A continuación, definimos los búferes de indicadores, que son matrices utilizadas para almacenar valores calculados para su representación gráfica. «upperChannelBuffer» contiene los valores límite superiores, «movingAverageBuffer» almacena la línea MA media y «lowerChannelBuffer» contiene el límite inferior. Estos amortiguadores permitirán una visualización fluida del canal de Keltner en el gráfico. Por último, introducimos las variables globales para almacenar los parámetros de entrada para su uso posterior. «maPeriodValue» y «atrPeriodValue» contienen los períodos definidos por el usuario para «MA» y «ATR», mientras que «atrMultiplierValue» almacena el multiplicador utilizado para determinar el ancho del canal. Ahora podemos pasar al controlador de eventos de inicialización, donde realizamos todos los trazados y mapeos de indicadores necesarios, así como la inicialización de los manejadores de 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);
}
//+------------------------------------------------------------------+

Aquí, inicializamos el indicador de canal Keltner en el controlador de eventos OnInit configurando búferes, gráficos, compensaciones y manejadores para garantizar una visualización y un cálculo correctos. Comenzamos vinculando cada búfer indicador a su gráfico correspondiente utilizando la función SetIndexBuffer, asegurándonos de que «Upper Channel», «Middle Moving Average (MA) Line» y «Lower Channel» se muestren correctamente.

A continuación, definimos el comportamiento del dibujo utilizando la función PlotIndexSetInteger. Configuramos el dibujo para que comience solo después de «maPeriod + 1» barras para evitar que aparezcan cálculos incompletos. Además, aplicamos un desplazamiento hacia la derecha utilizando PLOT_SHIFT para alinear correctamente los valores representados. Para gestionar los datos que faltan, asignamos un valor vacío de «0.0» a cada búfer utilizando la función PlotIndexSetDouble.

A continuación, configuramos los ajustes de pantalla. El nombre del indicador se establece mediante la función IndicatorSetString, mientras que PlotIndexSetString asigna etiquetas a cada línea de la «Ventana de datos». La precisión decimal de los valores del indicador se sincroniza con el formato de precios del gráfico mediante la función «IndicatorSetInteger». Por último, creamos los indicadores utilizando las funciones iMA e iATR. Si falla la creación de cualquier identificador, gestionamos los errores imprimiendo un mensaje de error mediante la función Print y devolviendo INIT_FAILED. Si todo sale bien, se devuelve INIT_SUCCEEDED, completando así el proceso de inicialización. A continuación, podemos pasar al controlador de eventos principal, que se encarga de los cálculos del 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);
     }
}

Aquí, implementamos la lógica de cálculo central del indicador de canal de Keltner dentro del controlador de eventos OnCalculate, una función que itera sobre los datos de precios para calcular y actualizar los búferes del indicador. En primer lugar, comprobamos si se trata del primer cálculo evaluando «prev_calculated». Si es «0», inicializamos los búferes «Upper Channel», «Middle Moving Average (MA)» y «Lower Channel» utilizando la función ArrayFill, asegurándonos de que todos los valores comiencen en cero. A continuación, rellenamos el «movingAverageBuffer» con los valores MA utilizando la función CopyBuffer. Si la copia falla, detenemos la ejecución devolviendo «0». Del mismo modo, recuperamos los valores ATR en la matriz temporal «atrValues».

Para asegurarnos de que tenemos datos suficientes tanto para MA como para ATR, determinamos la barra inicial utilizando la función MathMax, que devuelve el valor máximo entre los periodos del indicador, y añadimos 1 barra para evitar tener en cuenta la barra incompleta actual. A continuación, utilizamos un bucle for para iterar a través de cada barra desde «startBar» hasta «rates_total», calculando los límites «Upper» y «Lower» del canal utilizando la fórmula:

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

Por último, devolvemos «rates_total», indicando el número de barras calculadas. Si no es la primera ejecución del indicador, simplemente actualizamos los valores de las barras recientes mediante un nuevo cá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

Aquí optimizamos el rendimiento actualizando solo las barras más recientes en lugar de recalcular todo el indicador en cada tick. Si no se trata del primer cálculo, definimos «startBar» como «prev_calculated - 2», lo que garantiza que actualicemos las últimas barras y mantengamos la continuidad. Esto minimiza los cálculos innecesarios, ya que ya disponemos de los datos de las barras anteriores del gráfico.

A continuación, iteramos desde «startBar» hasta «rates_total» utilizando un bucle for. Para dar prioridad a las barras recientes, calculamos «reverseIndex = rates_total - i», lo que nos permite obtener primero los datos más recientes. Para cada barra, copiamos el valor MA más reciente en «emaValue» utilizando la función CopyBuffer. Si la recuperación de datos falla, devolvemos «prev_calculated», evitando cálculos redundantes. La misma lógica se aplica a ATR, almacenando su valor en «atrValue». Una vez recuperados, actualizamos los búferes:

  • «movingAverageBuffer[i] = emaValue[0];» asigna la EMA a la línea media.
  • «upperChannelBuffer[i] = emaValue[0] + atrMultiplier * atrValue[0];» calcula el límite superior.
  • «lowerChannelBuffer[i] = emaValue[0] - atrMultiplier * atrValue[0];» calcula el límite inferior.

Por último, devolvemos «rates_total», lo que indica que se han procesado todas las barras necesarias. Al ejecutar el programa, obtenemos el siguiente resultado.

INDICADOR

En la imagen podemos ver que las líneas indicadoras se han representado correctamente en el gráfico. Ahora solo queda trazar los canales, y para ello necesitaremos la función lienzo. Esto se trata en la siguiente sección.


Integración de gráficos personalizados en Canvas

Para integrar la función Canvas para gráficos, tendremos que incluir los archivos de clase canvas necesarios para poder utilizar la estructura integrada ya existente. Lo conseguimos mediante la siguiente lógica.

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

Incluimos la biblioteca «Canvas.mqh» utilizando la palabra clave #include, que proporciona funcionalidades para la representación gráfica en el gráfico. Esta biblioteca nos permitirá dibujar elementos personalizados, como indicadores visuales y anotaciones, directamente en la ventana del gráfico. A continuación, declaramos «obj_Canvas» como una instancia de la clase CCanvas. Este objeto se utilizará para interactuar con el lienzo, lo que nos permitirá crear, modificar y gestionar elementos gráficos de forma dinámica. La clase CCanvas proporcionará métodos para dibujar formas, líneas y texto, mejorando la representación visual del indicador. A continuación, necesitaremos obtener las propiedades del gráfico, como la escala, ya que vamos a señalar el gráfico con formas dinámicas. Lo hacemos en las variables globales.

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

Utilizamos varias funciones ChartGetInteger y ChartGetDouble para recuperar diferentes propiedades de la ventana del gráfico actual para realizar cálculos adicionales o colocar elementos gráficos. En primer lugar, recuperamos el ancho del gráfico utilizando «ChartGetInteger», con el parámetro CHART_WIDTH_IN_PIXELS, que devuelve el ancho del gráfico en píxeles. Almacenamos este valor en la variable «chart_width». Del mismo modo, recuperamos la altura del gráfico con «ChartGetInteger» y CHART_HEIGHT_IN_PIXELS, almacenándola en la variable «chart_height».

A continuación, utilizamos el parámetro «CHART_SCALE» para recuperar la escala del gráfico y almacenamos este valor en «chart_scale». Esto representa el nivel de zoom del gráfico. También recuperamos el índice de la primera barra visible utilizando «CHART_FIRST_VISIBLE_BAR» y lo almacenamos en «chart_first_vis_bar», lo cual resulta útil para realizar cálculos basados en el área visible del gráfico. Para calcular cuántas barras son visibles en la ventana del gráfico, utilizamos el parámetro CHART_VISIBLE_BARS, almacenando el resultado en «chart_vis_bars». «Convertimos» todos los valores a enteros.

Por último, utilizamos la función «ChartGetDouble» para obtener los valores mínimos y máximos de los precios visibles en el gráfico con «CHART_PRICE_MIN» y CHART_PRICE_MAX, respectivamente. Estos valores se almacenan en las variables «chart_prcmin» y «chart_prcmax», que proporcionan el rango de precios que se muestra actualmente en el gráfico. Con estas variables, tendremos que crear una etiqueta de mapa de bits en el gráfico durante la inicialización, para que podamos tener lista nuestra área de trazado.

// 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);

Aquí utilizamos la función «obj_Canvas.CreateBitmapLabel» para crear una etiqueta de mapa de bits personalizada en el gráfico. Requiere parámetros para el posicionamiento («0», «0»), el contenido («short_name»), el tamaño («0», «0» para el tamaño automático) y las dimensiones del gráfico («chart_width», «chart_height»). El formato de color está establecido en COLOR_FORMAT_ARGB_NORMALIZE, lo que permite personalizar la transparencia y el color. Con la etiqueta, ahora podemos dibujar las formas. Sin embargo, necesitaremos algunas funciones auxiliares que conviertan las coordenadas del gráfico y las velas en índices de precios y barras.

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

Creamos funciones para establecer correspondencias entre los datos de los gráficos y las coordenadas basadas en píxeles. En primer lugar, en la función «GetBarWidth», calculamos el ancho de cada barra en píxeles utilizando la escala del gráfico y aplicando la fórmula 2 elevado a la potencia de la escala del gráfico. Esto nos ayudará a ajustar el ancho de la barra en función de la escala del gráfico. Para ello, utilizamos la función pow para calcular potencias de 2.

A continuación, en la función «GetXCoordinateFromBarIndex», convertimos un índice de barra en una coordenada x en píxeles. Esto se hace calculando la distancia entre la primera barra visible y el índice de barra especificado. Multiplicamos esto por el ancho de la barra y restamos 1 para tener en cuenta la alineación de píxeles. Para la coordenada y, en la función «GetYCoordinateFromPrice», calculamos la posición relativa de un precio en el gráfico. Determinamos dónde se encuentra el precio entre los precios mínimo y máximo del gráfico («chart_prcmin» y «chart_prcmax») y, a continuación, escalamos este valor relativo para que se ajuste a la altura del gráfico. Nos aseguramos de evitar la división por cero si el rango de precios es cero.

Del mismo modo, la función «GetBarIndexFromXCoordinate» funciona a la inversa. Tomamos una coordenada x y la convertimos de nuevo en un índice de barra calculando cuántos anchos de barra caben en la coordenada x. Esto nos permite identificar la barra correspondiente a una posición determinada en el gráfico. Por último, en la función «GetPriceFromYCoordinate», convertimos una coordenada y de nuevo en un precio utilizando la posición relativa de la coordenada y dentro del rango de precios del gráfico. Nos aseguramos de evitar la división por cero si la altura del gráfico es cero.

En conjunto, estas funciones nos permiten traducir las coordenadas de píxeles del gráfico y los valores de los datos, lo que nos permite colocar gráficos personalizados en el gráfico con una alineación precisa con respecto al precio y las barras. Por lo tanto, ahora podemos utilizar las funciones para crear una función común, que utilizaremos para dibujar las formas necesarias entre dos líneas dadas de un 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 una función vacía «DrawFilledArea», que permitirá rellenar el área entre dos líneas indicadoras en el gráfico. En primer lugar, definimos las barras visibles en el gráfico con un desplazamiento opcional («shift») para ajustar el punto de inicio. También convertimos los colores («upperColor» y «lowerColor») al formato ARGB, incluyendo la transparencia, utilizando la función ColorToARGB. A continuación, determinamos el límite de la serie utilizando la función fmin para evitar exceder los tamaños de las matrices de las series de datos indicadoras superior e inferior («upperSeries» y «lowerSeries»). Inicializamos variables para almacenar las coordenadas de la barra anterior para las líneas superior e inferior, que se utilizan para dibujar el área.

A continuación, recorremos las barras visibles y calculamos la posición de cada barra en el eje x utilizando la función «GetXCoordinateFromBarIndex». Las coordenadas y de las líneas superior e inferior se calculan utilizando la función «GetYCoordinateFromPrice», basándose en los valores de «upperSeries» y «lowerSeries». Comprobamos qué línea es más alta y asignamos el color adecuado para el relleno.

Si la barra anterior contiene datos válidos, utilizamos «obj_Canvas.FillTriangle» para rellenar el área entre las dos líneas. Dibujamos dos triángulos para cada par de barras: un triángulo entre las líneas superior e inferior, y otro para completar el área rellena. Los triángulos se dibujan con el color determinado anteriormente. Utilizamos triángulos porque conectan con precisión puntos irregulares entre líneas, especialmente cuando las líneas no están perfectamente alineadas con la cuadrícula. Este método garantiza rellenos más uniformes y una mayor eficiencia de renderizado en comparación con los rectángulos. Aquí hay una ilustración.

RECTÁNGULOS VS TRIÁNGULOS PARA FORMAS IRREGULARES

Por último, actualizamos las coordenadas x e y anteriores para la siguiente iteración, asegurándonos de que el área se rellene continuamente entre las líneas de cada barra visible. Una vez familiarizados con la función, pasamos a utilizarla para dibujar el número de canales necesarios en el gráfico, con los colores respectivos según lo solicitado.

//+------------------------------------------------------------------+
//| 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 la función «RedrawChart» y, en primer lugar, definimos los colores predeterminados y, a continuación, recuperamos los colores de las líneas de los canales superior, medio e inferior de las propiedades del indicador. Borramos el lienzo con el color predeterminado y utilizamos la función «DrawFilledArea» para rellenar las áreas entre el canal superior y la media móvil, y entre la media móvil y el canal inferior, utilizando los colores respectivos. Por último, actualizamos el lienzo para reflejar los cambios, asegurándonos de que el gráfico se vuelva a dibujar con los nuevos rellenos. Ahora podemos llamar a la función en el controlador de eventos OnCalculate para dibujar el lienzo.

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

Dado que tenemos un objeto de canal indicador, debemos eliminarlo una vez que nos hayamos deshecho del indicador.

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

En el controlador de eventos OnDeinit, utilizamos el método «obj_Canvas.Destroy» para limpiar y eliminar cualquier objeto de dibujo personalizado del gráfico cuando se elimina el indicador. Por último, llamamos a la función ChartRedraw para actualizar y volver a dibujar el gráfico, asegurándonos de que los gráficos personalizados se borren de la pantalla. Una vez ejecutado el programa, obtenemos el siguiente resultado.

RESULTADO FINAL

A partir de la visualización, podemos ver que hemos logrado nuestro objetivo, que era crear el indicador avanzado del canal de Keltner con los gráficos de Canvas. Ahora tenemos que realizar una prueba retrospectiva del indicador para asegurarnos de que funciona correctamente. Esto se hace en la siguiente sección.


Pruebas retrospectivas del indicador del canal de Keltner

Durante las pruebas retrospectivas, observamos que cuando se modificaban las dimensiones del gráfico, la visualización del canal se bloqueaba y no se actualizaba con las coordenadas recientes del gráfico. Esto es lo que queremos decir.

INDICADOR QUE NO RESPONDE A LOS CAMBIOS EN EL GRÁFICO

Para solucionar esto, implementamos una lógica para actualizarlo en el controlador de eventos 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();
}

Aquí, manejamos la función OnChartEvent, que escucha el evento CHARTEVENT_CHART_CHANGE. Cuando cambian las dimensiones del gráfico (por ejemplo, cuando se cambia el tamaño del gráfico), primero recuperamos las propiedades actualizadas del gráfico, como el ancho («CHART_WIDTH_IN_PIXELS»). A continuación, comprobamos si el nuevo ancho y alto del gráfico difieren del tamaño actual del lienzo utilizando «obj_Canvas.Width» y «obj_Canvas.Height». Si difieren, cambiamos el tamaño del lienzo con «obj_Canvas.Resize». Por último, llamamos a la función «RedrawChart» para actualizar el gráfico y asegurarnos de que todos los elementos visuales se representen correctamente con las nuevas dimensiones. El resultado es el siguiente.

INDICADOR QUE RESPONDE A LOS CAMBIOS EN LOS GRÁFICOS

A partir de la visualización, podemos ver que los cambios se aplican dinámicamente cuando cambiamos el tamaño del gráfico, con lo que se logra nuestro objetivo.


Conclusión

En conclusión, este artículo ha tratado sobre la creación de un indicador MQL5 personalizado utilizando medias móviles y el ATR para crear canales dinámicos. Nos centramos en calcular y mostrar estos canales con un mecanismo de relleno, al tiempo que abordamos las mejoras de rendimiento para el redimensionamiento de gráficos y las pruebas retrospectivas, garantizando la eficiencia y la precisión para los operadores.

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/17155

Creación de una estrategia de retorno a la media basada en el aprendizaje automático Creación de una estrategia de retorno a la media basada en el aprendizaje automático
Este artículo propone otro enfoque original para crear sistemas comerciales basados en el aprendizaje automático, usando la clusterización y el etiquetado de transacciones para estrategias de retorno a la media.
Algoritmo de optimización del billar — Billiards Optimization Algorithm (BOA) Algoritmo de optimización del billar — Billiards Optimization Algorithm (BOA)
El método BOA, inspirado en el clásico juego del billar, modela el proceso de búsqueda de soluciones óptimas como un juego de bolas que intentan acertar en las troneras que representan los mejores resultados. En este artículo revisaremos los fundamentos del BOA, su modelo matemático y su eficacia para resolver diversos problemas de optimización.
Creación de un Panel de administración de operaciones en MQL5 (Parte IX): Organización del código (II): Modularización Creación de un Panel de administración de operaciones en MQL5 (Parte IX): Organización del código (II): Modularización
En este debate, damos un paso más allá al desglosar nuestro programa MQL5 en módulos más pequeños y manejables. Estos componentes modulares se integrarán posteriormente en el programa principal, mejorando su organización y facilidad de mantenimiento. Este enfoque simplifica la estructura de nuestro programa principal y permite reutilizar los componentes individuales en otros asesores expertos (EA) y desarrollos de indicadores. Al adoptar este diseño modular, creamos una base sólida para futuras mejoras, lo que beneficia tanto a nuestro proyecto como a la comunidad de desarrolladores en general.
Redes neuronales en el trading: Integración de la teoría del caos en la previsión de series temporales (Attraos) Redes neuronales en el trading: Integración de la teoría del caos en la previsión de series temporales (Attraos)
El framework de Attraos integra la teoría del caos en la previsión de series temporales a largo plazo tratándolas como proyecciones de sistemas dinámicos caóticos multidimensionales. Usando la invarianza de los atractores, el modelo aplica la reconstrucción del espacio de fases y la memoria dinámica con varias resoluciones para preservar las estructuras históricas.