English Deutsch 日本語
preview
Создаем индикатор канал Кельтнера с помощью пользовательской графики Canvas на MQL5

Создаем индикатор канал Кельтнера с помощью пользовательской графики Canvas на MQL5

MetaTrader 5Торговые системы |
387 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В настоящей статье мы создаем пользовательский индикатор канал Кельтнера с расширенной графикой canvas на MetaQuotes Language 5 (MQL5). Канал Кельтнера рассчитывает динамические уровни поддержки и сопротивления с помощью индикаторов Скользящая средняя (MA) и Средний истинный диапазон (ATR), помогая трейдерам определять направления тренда и потенциальные пробои. Темы, которые мы рассмотрим в настоящей статье:

  1. Знакомство с индикатором канал Кельтнера
  2. План: Анализ архитектуры индикатора
  3. Реализация средствами MQL5
  4. Интеграция пользовательской графики Canvas
  5. Тестирование на истории индикатора канал Кельтнера
  6. Заключение


Знакомство с индикатором канал Кельтнера

Индикатор канал Кельтнера - это инструмент, основанный на волатильности, применяемый трейдерами и использующий индикатор скользящей средней (MA) для сглаживания ценовых данных и индикатор среднего истинного диапазона (ATR) для определения динамических уровней поддержки и сопротивления. У него есть 3 линии, формирующих канал. Средняя линия канала - это скользящая средняя, обычно выбираемая для отражения преобладающего тренда. В то же время верхний и нижний диапазоны генерируются путем сложения и вычитания значения, кратного ATR. Этот метод позволяет индикатору адаптироваться к волатильности рынка, что облегчает определение потенциальных точек пробоя или областей, где ценовое движение может развернуться.

На практике индикатор помогает нам определить ключевые уровни на рынке, где динамика может измениться. Когда цены выходят за пределы верхнего или нижнего диапазонов, это сигнализирует о перенапряжении рынка или потенциальном развороте, предоставляя полезную информацию как для стратегии следования тренду, так и для стратегии возврата к среднему значению. Динамичный характер индикатора означает, что он адаптируется к изменениям волатильности, гарантируя, что уровни поддержки и сопротивления остаются актуальными по мере изменения рыночных условий. Вот наглядный пример.

CHANNEL SAMPLE


План: Анализ архитектуры индикатора

Мы построим архитектуру индикатора на основе четкого разделения обязанностей: входные параметры, буферы индикатора и графические свойства. Начнем с определения ключевых входных данных, таких как период скользящей средней, период ATR и множитель ATR, которые будут определять поведение индикатора. Затем выделим три буфера для хранения значений для верхнего канала, средней линии и нижнего канала. Эти буферы будут связаны с графическими диграммами, а такие свойства, как цвет, ширина линии и сдвиг при рисовании, будут настроены с помощью встроенных функций MQL5. Кроме того, мы будем использовать встроенные функции для выполнения расчетов, обеспечивая динамичнуюое адаптирование индикатора к волатильности рынка.

Кроме того, мы включим обработку ошибок, чтобы гарантировать успешное создание обоих хэндлов индикатора, обеспечивая надежную основу для расчетов индикатора. Помимо основной логики индикатора, мы интегрируем пользовательскую графику canvas для улучшения визуального представления, включая создание графической метки, накладываемой на график. Такая модульная конструкция не только упростит отладку и внесение изменений в будущем, но и обеспечит согласованную работу каждого компонента — от расчета данных до визуального вывода —, обеспечивая надежный и визуально привлекательный торговый инструмент. В двух словах, вот три вещи, которых мы достигнем.

INDICATOR'S ARCHITECTURE


Реализация средствами MQL5

Для создания индикатора на MQL5, просто откройте MetaEditor, перейдите в навигатор, найдите папку "Индикаторы» (Indicators), перейдите на вкладку "Создать" (New) и следуйте инструкциям по созданию файла. Как только он будет создан, в среде программирования мы определим свойства и настройки индикатора, такие как количество буферов, графики и отдельные свойства линий, такие как цвет, ширина и метка.

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

Мы начинаем с настройки индикатора метаданных metadata, таких как версия, с помощью ключевого слова #property. Далее мы выделяем три буфера индикатора, используя свойство indicator_buffers, которое будет хранить и управлять вычисленными значениями для "Верхнего канала", "Средней скользящей средней (MA)" и "Нижнего канала". Мы также установили для параметра "indicator_plots" значение 3, определив, что на графике будут отображаться три отдельных графических построения. Для каждого из них мы настраиваем определенные свойства визуализации:

  • Верхний канал Кельтнера Мы назначаем макрос DRAW_LINE в качестве его "типа индикатора", что означает, что он будет отображаться в виде непрерывной линии. Цвет устанавливаем на "синий" с помощью clrBlue, а метка "Upper Keltner" помогает идентифицировать его в окне данных. Устанавливаем ширину линии равной 2 пикселям для лучшей видимости.
  • Средний Канал Кельтнера (скользящая средняя): Аналогично, устанавливаем его тип на "DRAW_LINE", используем "Серый" цвет и присваиваем метку "Middle Keltner". Эта линия представляет собой центральную скользящую среднюю, служащую основным ориентиром для верхней и нижней полос.
  • Нижний канал Кельтнера: Эта линия также определяется как DRAW_LINE и имеет "красный" цвет, чтобы отличать ее от других. Присваиваем метку "Lower Keltner", а ширину линии устанавливаем равной 2 пикселям.

Разобравшись со свойствами, можно перейти к определению входных параметров.

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

Здесь мы определяем входные свойства. Для скользящей средней устанавливаем "maPeriod" (по умолчанию 20), чтобы определить количество используемых баров. Для параметра "maMethod" с типом данных ENUM_MA_METHOD  задается значение "MODE_EMA", указывающее экспоненциальную скользящую среднюю, а для параметра "maPrice" с типом данных ENUM_APPLIED_PRICE задается значение "PRICE_CLOSE", что означает, что расчеты основаны на ценах закрытия.

Для параметра "ATR" значение "atrPeriod" (по умолчанию 10) определяет, сколько баров используется для расчета волатильности, в то время как "atrMultiplier" (по умолчанию 2.0) устанавливает расстояние верхней и нижней полос от скользящей средней. И последнее, "showPriceLabel" (по умолчанию - значение true) контролирует, будут ли отображаться ценовые метки на графике. Эти настройки обеспечат гибкость при адаптации индикатора к различным рыночным условиям. Наконец, нам нужно определить хэндлы индикаторов, которые мы будем использовать.

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

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

Здесь мы объявляем хэндлы индикаторов, буферы и некоторые глобальные переменные , которые нам потребуются для расчетов канала Кельтнера. В хэндлах хранятся ссылки на индикаторы, что позволяет нам динамически извлекать их значения. Мы инициализируем "maHandle" и "atrHandle" как INVALID_HANDLE, обеспечивая надлежащее управление хэндлом перед назначением.

Далее мы определяем индикаторные буферы, которые представляют собой массивы, используемые для хранения вычисленных значений для построения графиков. "upperChannelBuffer" содержит значения верхней границы, "movingAverageBuffer" хранит среднюю линию MA, а "lowerChannelBuffer" содержит нижнюю границу. Эти буферы обеспечат плавную визуализацию канала Кельтнера на графике. Наконец, вводим глобальные переменные для хранения входных параметров для дальнейшего использования. "maPeriodValue" и "atrPeriodValue" содержат заданные пользователем периоды для "MA" и "ATR", в то время как "atrMultiplierValue" хранит множитель, используемый для определения ширины канала. Теперь можно перейти к обработчику событий инициализации, где мы выполняем все необходимые построения индикаторов и отображение, а также инициализацию хэндлов индикаторов.

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

Здесь мы инициализируем индикатор канала Кельтнера в обработчике событий OnInit , настраивая буферы, графики, смещения и хэндлы для обеспечения корректной визуализации и расчета. Начинаем с привязки каждого индикаторного буфера к соответствующему графическому изображению с помощью функции SetIndexBuffer, гарантируя надлежащее отображение "Верхнего канала", "Средней скользящей средней (MA)" и "Нижнего канала".

Далее определяем поведение рисунка с помощью функции PlotIndexSetInteger. Устанавливаем начало черчения только после баров "maPeriod + 1", чтобы исключить появление неполных вычислений. Кроме того, применяем сдвиг вправо с помощью PLOT_SHIFT, чтобы корректно выставить отображаемые значения. Чтобы обработать недостающие данные, присваиваем каждому буферу пустое значение "0.0" с помощью функции PlotIndexSetDouble .

Затем настраиваем параметры отображения. Название индикатора задается с помощью функции IndicatorSetString , в то время как PlotIndexSetString  назначает метки для каждой линии в "Окне данных". Десятичная точность значений индикатора синхронизируется с форматом цены на графике с помощью функции "IndicatorSetInteger". Наконец, мы создаем хэндлы индикатора с помощью функций iMA и iATR . Если создание какого-либо хэндла завершается неудачей, мы обрабатываем ошибки, выводя сообщение об ошибке с помощью функции Print  и возвращая INIT_FAILED. Если все проходит успешно, возвращается INIT_SUCCEEDED , завершая процесс инициализации. Затем мы можем перейти к основному обработчику событий, обрабатывающему вычисления индикатора.

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

Здесь мы реализуем основную логику расчета индикатора канал Кельтнера в обработчике событий OnCalculate , функции, выполняющей итерацию ценовых данных для вычисления и обновления буферов индикатора. Сначала проверяем, является ли это первым вычислением, оценивая "prev_calculated". Если значение равно "0", инициализируем буферы "Верхнего канала", "Средней скользящей средней (MA)" и "Нижнего канала" с помощью функции ArrayFill , гарантирующей, что все значения начинаются с нуля. Далее заполняем "movingAverageBuffer" значениями MA с помощью функции CopyBuffer . Если копирование завершается неудачей, останавливаем выполнение, возвращая "0". Аналогично, извлекаем значения ATR во временный массив "atrValues".

Чтобы убедиться, что у нас достаточно данных как для скользящей средней, так и для ATR, определяем начальный бар с помощью функции MathMax,  возвращающей максимальное значение между периодами индикатора, а также добавляем 1 бар, чтобы не учитывать текущий неполный бар. Затем мы используем цикл "for"  для перебора каждого бара от "startBar" до "rates_total", вычисляя "Верхнюю" и "Нижнюю" границы канала по формуле:

  • "Верхний канал = Скользящая средняя + (ATR * множитель)"
  • «Нижний канал = Скользящая средняя - (ATR * множитель)"

Наконец, возвращаем "rates_total", указывающий количество рассчитанных баров. Если это не первый запуск индикатора, мы просто обновляем значения последних баров путем пересчета.

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

Здесь мы оптимизируем эффективность путем обновления только самых последних баров вместо того, чтобы пересчитывать весь индикатор на каждом тике. Если это не первое вычисление, определяем "startBar" как "prev_calculated - 2", гарантируя, что мы обновим последние несколько баров, сохраняя непрерывность. Это сводит к минимуму ненужные вычисления, так как у нас уже есть данные для предыдущих баров на графике.

Затем выполняем перебор от "startBar" до "rates_total" с помощью цикла for. Чтобы определить приоритетность последних баров, вычисляем "reverseIndex = rates_total - i", что позволяет нам получить последние данные первыми. Для каждого бара копируем самое последнее значение MA в "emaValue" с помощью функции CopyBuffer . Если поиск данных завершается неудачей, мы возвращаем "prev_calculated", избегая избыточных вычислений. Та же логика применяется и к ATR, сохраняющему свое значение в "atrValue". После поиска выполняем обновление буферов.

  • "movingAverageBuffer[i] = emaValue[0];" назначает ЕМА средней линии.
  • "upperChannelBuffer[i] = emaValue[0] + atrMultiplier * atrValue[0];" вычисляет верхнюю границу.
  • "lowerChannelBuffer[i] = emaValue[0] - atrMultiplier * atrValue[0];" вычисляет нижнюю границу.

Наконец, возвращаем "rates_total", сигнализируя о том, что все необходимые бары обработаны. После запуска программы получаем следующий результат.

INDICATOR BANDS

На изображении видно, что линии индикатора корректно нанесены на график. Теперь остается только графически представить каналы, а для этого нам понадобится признак canvas. Этот вопрос рассматривается в следующем разделе.


Интеграция пользовательской графики Canvas

Для интегрирования функции canvas для графики, нам нужно будет включить необходимые файлы класса canvas, чтобы можно было использовать уже существующую встроенную структуру. Для этого мы используем следующую логику.

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

Включаем библиотеку "Canvas.mqh" с помощью ключевого слова #include , которое предоставляет функциональные возможности для графического отображения на диаграмме. Эта библиотека позволит нам создавать пользовательские элементы, такие как визуальные элементы индикаторов и аннотации, непосредственно в окне графика. Затем объявляем "obj_Canvas" как экземпляр класса CCanvas . Этот объект будет использоваться для взаимодействия с canvas, позволяя нам динамически создавать, изменять графические элементы и управлять ими. Класс CCanvas  предоставит методы для рисования фигур, линий и текста, улучшая визуальное представление индикатора. Далее нам нужно получить свойства диаграммы, такие как масштаб, поскольку мы будем создаавать диаграмму с помощью динамических фигур. Мы делаем это в глобальном масштабе.

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

Мы используем различные функции ChartGetInteger и ChartGetDouble  для извлечения различных свойств текущего окна графика для дальнейших вычислений или графического размещения элементов. Сначала получаем ширину диаграммы с помощью "ChartGetInteger" с параметром CHART_WIDTH_IN_PIXELS, который возвращает ширину диаграммы в пикселях. Сохраняем это значение в переменной "chart_width". Аналогично, извлекаем высоту диаграммы с помощью "ChartGetInteger" и CHART_HEIGHT_IN_PIXELS, сохраняя ее в переменной "chart_height".

Далее используем параметр "CHART_SCALE" для получения масштаба диаграммы, сохраняя это значение в "chart_scale". Это соответствует уровню масштабирования диаграммы. Мы также получаем индекс первого видимого бара с помощью "CHART_FIRST_VISIBLE_BAR", сохраняя его в "chart_first_vis_bar", что полезно для вычислений на основе видимой области диаграммы. Чтобы рассчитать, сколько баров видно в окне графика, мы используем параметр CHART_VISIBLE_BARS , сохраняя результат в "chart_vis_bars". Мы "преобразуем" все значения в целые числа.

Наконец, мы используем функцию "ChartGetDouble", чтобы получить минимальное и максимальное значения цены, отображаемые на графике с помощью "CHART_PRICE_MIN" и CHART_PRICE_MAX соответственно. Эти значения хранятся в переменных "chart_prcmin" и "chart_prcmax", которые определяют диапазон цен, отображаемый в данный момент на графике. Вооружившись этими переменными, нам нужно будет создать графическую метку на диаграмме при инициализации, чтобы наша область построения была готова.

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

Здесь мы используем функцию "obj_Canvas.CreateBitmapLabel" для создания пользовательской графической метки на диаграмме. Она принимает параметры для позиционирования ("0", "0"), содержимого ("short_name"), размера ("0", "0" для автоматического определения размера) и размеров диаграммы ("chart_width", "chart_height"). Цветовой формат установлен на COLOR_FORMAT_ARGB_NORMALIZE, что позволяет настраивать прозрачность и цвет. Поскольку у нас есть метка, теперь мы можем рисовать фигуры. Однако нам понадобятся некоторые вспомогательные функции, которые преобразуют координаты диаграммы и свечи в индексы цен и баров.

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

Мы создаем функции для сопоставления данных диаграммы с пиксельными координатами. Сначала в функции "GetBarWidth" мы вычисляем ширину каждого бара в пикселях, используя масштаб диаграммы и применяя формулу 2, возведенную в степень масштаба диаграммы. Это поможет нам настроить ширину бара в зависимости от масштаба диаграммы. Для этого мы используем функцию pow  для вычисления степеней 2.

Далее, в функции "GetXCoordinateFromBarIndex" мы преобразуем индекс бара в координату x в пикселях. Это делается путем вычисления расстояния между первым видимым баром и указанным индексом бара. Умножаем это значение на ширину бара и вычитаем 1, чтобы учесть выравнивание пикселей. Для координаты y в функции "GetYCoordinateFromPrice" вычисляем относительное положение цены на графике. Определяем, где находится цена между минимальной и максимальной ценами графика ("chart_prcmin" и "chart_prcmax"), затем масштабируем это относительное значение так, чтобы оно соответствовало высоте графика. Обращаем внимание, что необходимо избежать деления на ноль, если диапазон цен равен нулю.

Аналогично, функция "GetBarIndexFromXCoordinate" работает в обратном порядке. Мы берем координату x и преобразуем ее обратно в индекс бара, вычисляя, сколько раз ширина бара помещается в координату x. Это позволяет нам определить бар, соответствующий заданной позиции на графике. Наконец, в функции "GetPriceFromYCoordinate" преобразуем координату y обратно в цену, используя относительное положение координаты y в пределах ценового диапазона на графике. Если высота графика равна нулю, следим за тем, чтобы избежать деления на ноль.

В совокупности эти функции дают возможность изменять пиксельные координаты графика и значения данных, что позволяет размещать пользовательскую графику на диаграмме с точным соответствием по цене и барам. Таким образом, теперь мы можем использовать эти функции для создания общей функции, которую будем использовать для рисования необходимых фигур между двумя заданными линиями канала.

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

Объявляем пустую функцию "DrawFilledArea", которая позволит заполнить область между двумя линиями индикатора на диаграмме. Сначала определяем видимые бары на графике с помощью опционального сдвига ("shift"), чтобы скорректировать начальную точку. Также преобразуем цвета ("upperColor" и "lowerColor") в формат ARGB, включая прозрачность, с помощью функции ColorToARGB . Затем определяем предел ряда с помощью функции fmin,  чтобы избежать превышения размеров массива верхних и нижних рядов данных индикатора ("Верхние ряды" и "нижние ряды"). Инициализируем переменные для хранения координат предыдущего бара для верхней и нижней линий, которые используются для рисования области.

Затем перебираем видимые бары и вычисляем положение каждого бара на оси x с помощью функции "GetXCoordinateFromBarIndex". Координаты Y верхней и нижней линий рассчитываются с помощью функции "GetYCoordinateFromPrice" на основе значений в "upperSeries" и "lowerSeries". Проверяем, какая линия находится выше, и назначаем соответствующий цвет для заливки.

Если предыдущий бар содержит корректные данные, мы используем "obj_Canvas.FillTriangle" для заполнения области между двумя линиями. Мы рисуем два треугольника для каждой пары баров: один треугольник между верхней и нижней линиями, а другой - для завершения области с заливкой. Треугольники рисуются определенным ранее цветом. Мы используем треугольники, потому что они точно соединяют асимметричные точки между линиями, особенно когда линии не идеально совпадают с сеткой. Этот метод обеспечивает более плавную заливку и более высокую эффективность отрисовки по сравнению с прямоугольниками. Вот иллюстрация.

RECTANGLES VS TRIANGLES FOR IRREGULAR SHAPES

Наконец, обновляем предыдущие координаты x и y для следующей итерации, гарантируя непрерывное заполнение области между линиями для каждого видимого бара. Вооружившись этой функцией, мы переходим к ее использованию для отображения количества необходимых каналов на графике соответствующими цветами в соответствии с запросом.

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

Объявляем функцию "RedrawChart" и сначала определяем цвета по умолчанию, а затем извлекаем цвета линий для верхнего, среднего и нижнего каналов из свойств индикатора. Выделяем «полотно» цветом по умолчанию и используем функцию "DrawFilledArea" для заполнения области между верхним каналом и скользящей средней, а также между скользящей средней и нижним каналом соответствующими цветами. Наконец, мы обновляем «полотно», чтобы отразить изменения, и убеждаемся, что диаграмма отображается с новыми заливками. Теперь для рисования «полотна» мы можем вызвать функцию в обработчике событий OnCalculate.

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

Поскольку у нас есть объект канал индикатора, необходимо удалить его после избавления от индикатора.

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

В обработчике событий OnDeinit  мы используем метод "obj_Canvas.Destroy" для очистки и удаления любых пользовательских графических объектов на диаграмме при удалении индикатора. Наконец, вызываем функцию ChartRedraw , чтобы обновить и перерисовать диаграмму, убедившись, что пользовательская графика удалена с экрана. После запуска программы мы получаем следующий результат.

FINAL OUTCOME

Из визуализации видно, что мы достигли нашей цели - создать усовершенствованный индикатор канал Кельтнера с графикой canvas. Теперь чтобы убедиться, что индикатор работает корректно, нам нужно протестировать его на истории. Мы это сделаем в следующем разделе.


Тестирование на истории индикатора канал Кельтнера

Во время тестирования на истории мы заметили, что при изменении размеров диаграммы отображение канала зависало и не обновлялось до последних значений координат диаграммы. Вот то, что мы имеем в виду.

INDICATOR UNRESPONSIVE TO CHART CHANGES

Для решения этой проблемы мы реализовали логику для обновления диаграммы в обработчике событий 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();
}

Здесь мы обрабатываем функцию OnChartEvent , которая отслеживает событие CHARTEVENT_CHART_CHANGE . Когда размеры диаграммы изменяются (например, при изменении масштабирования диаграммы), сначала извлекаем обновленные свойства диаграммы, такие как ширина ("CHART_WIDTH_IN_PIXELS"). Затем с помощью функций "obj_Canvas.Width" и "obj_Canvas.Height» проверяем, отличаются ли новые ширина и высота диаграммы от текущего размера «полотна». Если они действительно отличаются, изменяем размер «полотна» с помощью "obj_Canvas.Resize". Наконец, вызываем функцию "RedrawChart", чтобы обновить диаграмму и убедиться, что все визуальные элементы отображаются корректно с учетом новых размеров. Результат показан ниже.

INDICATOR RESPONSIVE TO CHART CHANGES

Из визуализации видно, что изменения вступают в силу динамически при изменении размера диаграммы. Тем самым мы достигаем нашей цели.


Заключение

В заключение, в данной статье мы рассмотрели создание пользовательского индикатора MQL5 с помощью инструментов скользящие средние и средний истинный диапазон для создания динамических каналов. Мы сосредоточились на расчете и отображении этих каналов с помощью механизма заливки, а также на улучшении производительности при изменении размера графиков и тестировании на истории, обеспечивая эффективность и точность для трейдеров.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17155

Прикрепленные файлы |
Возможности Мастера MQL5, которые вам нужно знать (Часть 50): Осциллятор Awesome Возможности Мастера MQL5, которые вам нужно знать (Часть 50): Осциллятор Awesome
Осциллятор Awesome — еще один индикатор Билла Вильямса, используемый для измерения импульса. Он может генерировать несколько сигналов. Как и в предыдущих статьях, мы рассмотрим его на основе паттернов, используя классы и сборку Мастера MQL5.
Трейдинг с экономическим календарем MQL5 (Часть 3): Добавление сортировки по валюте, важности и времени Трейдинг с экономическим календарем MQL5 (Часть 3): Добавление сортировки по валюте, важности и времени
В этой статье мы реализуем фильтры на панели инструментов экономического календаря MQL5 для лучшего отображения новостей по валюте, важности и времени. Сначала мы установим критерии сортировки для каждой категории, а затем интегрируем их в панель управления, чтобы отображать только релевантные события. Наконец, мы обеспечим динамическое обновление каждого фильтра, чтобы предоставлять трейдерам необходимую экономическую информацию в реальном времени.
Разработка инструментария для анализа движения цен (Часть 3): Советник Analytics Master Разработка инструментария для анализа движения цен (Часть 3): Советник Analytics Master
Переход от простого торгового скрипта к полнофункциональному советнику может значительно улучшить ваш торговый опыт. Представьте себе систему, которая автоматически отслеживает графики, выполняет основные вычисления в фоновом режиме и предоставляет регулярные обновления каждые два часа. Советник способен анализировать ключевые показатели, имеющие решающее значение для принятия обоснованных торговых решений, гарантируя вам доступ к самой актуальной информации для эффективной корректировки ваших стратегий.
Пользовательские символы MQL5: Создаем символ 3D-баров Пользовательские символы MQL5: Создаем символ 3D-баров
В данной статье представлено детальное руководство по созданию инновационного индикатора 3DBarCustomSymbol.mq5, который генерирует пользовательские символы в MetaTrader 5, объединяющие цену, время, объем и волатильность в единое трехмерное представление. Рассматриваются математические основы, архитектура системы, практические аспекты реализации и применения в торговых стратегиях.