Создание пользовательских индикаторов в MQL5 (Часть 1): Построение трендового индикатора на основе пивотов с градиентной заливкой на Canvas
Введение
В этой статье мы разработаем трендовый индикатор на основе пивотов в MetaQuotes Language 5 (MQL5), который рассчитывает быстрые/медленные пивотные линии, определяет тренды и отмечает их направленными стрелками, продлевает пивотные линии вперед на графике и для лучшей читаемости поддерживает необязательную градиентную заливку на Canvas для выделения бычьих или медвежьих областей. В этой статье мы рассмотрим следующие темы:
- Понимание архитектуры трендового индикатора на основе пивотов
- Реализация в MQL5
- Бэктестирование
- Заключение
К концу статьи у вас будет полностью рабочий индикатор MQL5 для обнаружения тренда на основе пивотов с гибкими визуальными настройками. Приступим к разработке.
Понимание архитектуры трендового индикатора на основе пивотов
Индикатор Pivot Trend Detector — это технический инструмент, который использует быстрые и медленные пивотные линии, построенные по диапазонам максимумов/минимумов за заданные периоды, чтобы определять направление тренда и потенциальные развороты. Он сглаживает ценовые данные и при этом выделяет изменения с помощью цветных линий и стрелок. Индикатор состоит из трех основных линий: медленной линии, которая служит главным ориентиром тренда (вверх или вниз в зависимости от положения цены), быстрой пунктирной линии, меняющей цвет при развороте тренда, и стрелок, отмечающих начало новых трендов при пересечении ценой обеих линий.
Такая конфигурация помогает отслеживать изменения импульса: медленная линия выступает как поддержка/сопротивление, а быстрая линия дает ранние сигналы, которые можно адаптировать к волатильности за счет настройки периодов. На практике индикатор помогает следовать за трендом: он подтверждает восходящий тренд, когда цена остается выше медленной линии (она рисуется цветом восходящего тренда), и нисходящий тренд, когда цена остается ниже медленной линии (она рисуется цветом нисходящего тренда). Стрелки отмечают точки входа при пересечениях, а дополнительные продления линий уходят вправо, формируя проекции на будущие бары. Динамическая заливка между линиями визуализирует силу тренда с помощью градиентной прозрачности, плавно исчезая от медленной линии к быстрой и интуитивно выделяя область.
Архитектуру индикатора мы построим на четком разделении ответственности: входные параметры, буферы индикатора и графические свойства. Начнем с определения ключевых входных параметров: быстрых/медленных периодов, цветов, прозрачности, кода стрелки и продлений, которые будут задавать поведение индикатора. Затем выделим восемь буферов для хранения медленных линий вверх/вниз, быстрых линий с цветами, трендовых стрелок с цветами, а также внутренних расчетов для значений тренда и медленной линии. Эти буферы будут связаны с отображаемыми сериями, свойства которых — тип (линия/цветная линия/стрелка), цвет, ширина и сдвиг — настраиваются с помощью встроенных функций MQL5. Кроме того, мы используем класс CCanvas для градиентной заливки пространства между линиями, чтобы индикатор динамически адаптировался к рыночной волатильности. Вкратце, ниже показан пример того, что мы получим.

Реализация в MQL5
Чтобы создать индикатор в MQL5, откройте MetaEditor, перейдите в Навигатор, найдите папку Indicators (Индикаторы), нажмите кнопку "New" (Создать) и следуйте подсказкам для создания файла. После его создания в среде программирования мы определим свойства и настройки индикатора, такие как количество буферов, отображаемых серий и отдельные свойства линий, включая цвет, ширину и метку.
//+------------------------------------------------------------------+ //| 1. Pivot Trend Detector.mq5 | //| Copyright 2025, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property indicator_chart_window #property indicator_buffers 8 #property indicator_plots 4 #property indicator_label1 "PTD slow line up" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDodgerBlue #property indicator_width1 2 #property indicator_label2 "PTD slow line down" #property indicator_type2 DRAW_LINE #property indicator_color2 clrCrimson #property indicator_width2 2 #property indicator_label3 "PTD fast line" #property indicator_type3 DRAW_COLOR_LINE #property indicator_color3 clrDodgerBlue,clrCrimson #property indicator_style3 STYLE_DOT #property indicator_label4 "PTD trend start" #property indicator_type4 DRAW_COLOR_ARROW #property indicator_color4 clrDodgerBlue,clrCrimson #property indicator_width4 2
Реализацию начнем с определения метаданных индикатора с помощью директив property, указав, что он рисуется в основном окне графика через "indicator_chart_window", выделив 8 буферов с помощью indicator_buffers, и определив 4 графических построения через "indicator_plots". Для первого графического построения задаем метку "PTD slow line up" и тип DRAW_LINE, цвет DodgerBlue и ширину 2. Второе графическое построение получает метку "PTD slow line down", тип линии, цвет Crimson и ширину 2. Третье графическое построение имеет метку "PTD fast line", тип цветной линии, цвета DodgerBlue и Crimson, а также пунктирный стиль. Четвертое графическое построение получает метку "PTD trend start", тип цветной стрелки, цвета DodgerBlue и Crimson, ширину 2. Эти свойства задают визуальную структуру медленных линий вверх/вниз, быстрых линий с изменяемым цветом и стрелок начала тренда. Затем определим входные параметры и глобальные переменные для использования в программе.
#include <Canvas/Canvas.mqh> //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ CCanvas obj_Canvas; //--- Canvas object //--- input parameters input int fastPeriod = 5; // Fast period input int slowPeriod = 10; // Slow period input color upColor = clrDodgerBlue; // Up trend color input color downColor = clrCrimson; // Down trend color input int fillOpacity = 128; // Fill opacity (0-255) input int arrowCode = 77; // Arrow code for trend start input bool showExtensions = true; // Show line extensions input bool enableFilling = true; // Enable canvas fill (disable for speed) input int extendBars = 1; // Extension bars to protrude lines/fill //--- indicator buffers double slowLineUpBuffer[],slowLineDownBuffer[],slowLineBuffer[],fastLineBuffer[],fastLineColorBuffer[],trendArrowColorBuffer[],trendArrowBuffer[],trendBuffer[]; //--- Indicator buffers //--- chart properties int currentChartWidth = 0; //--- Current chart width int currentChartHeight = 0; //--- Current chart height int currentChartScale = 0; //--- Current chart scale int firstVisibleBarIndex = 0; //--- First visible bar index int visibleBarsCount = 0; //--- Visible bars count double minPrice = 0.0; //--- Minimum price double maxPrice = 0.0; //--- Maximum price //--- optimization flags static datetime lastRedrawTime = 0; //--- Last redraw time static double previousTrend = -1; //--- Previous trend string objectPrefix = "PTD_"; //--- Object prefix
Здесь мы подключаем библиотеку Canvas с помощью "#include <Canvas/Canvas.mqh>", чтобы задействовать пользовательскую графическую отрисовку, например градиентную заливку между линиями индикатора для более наглядной визуализации. Затем объявляем "obj_Canvas" как глобальный экземпляр класса CCanvas для управления объектом Canvas при заливке областей. Мы задаем входные параметры для настройки: "fastPeriod" со значением по умолчанию 5 для окна расчета быстрого пивота, "slowPeriod" со значением 10 для медленного, "upColor" как DodgerBlue для восходящих трендов, "downColor" как Crimson для нисходящих, "fillOpacity" со значением 128 (полупрозрачность) для заливки области в диапазоне 0-255, "arrowCode" со значением 77 для символов начала тренда из Wingdings, "showExtensions" со значением true для продления линий за текущий бар, "enableFilling" со значением true для включения заливки Canvas (можно отключить ради производительности), "extendBars" со значением 1 как количество баров для продления. Код стрелки можно изменить и использовать любой из определенных в MQL5 символов Wingdings как показано ниже.

Затем выделяем восемь глобальных массивов как буферы индикатора: "slowLineUpBuffer" и "slowLineDownBuffer" для отдельных медленных линий вверх/вниз, "slowLineBuffer" для внутренних расчетов медленной линии, "fastLineBuffer" для быстрой линии, "fastLineColorBuffer" для ее цветов, "trendArrowColorBuffer" и "trendArrowBuffer" для позиций и цветов стрелок, а также "trendBuffer" для состояний тренда. Мы задаем глобальные переменные для свойств графика: "currentChartWidth"/"Height" со значением 0 для начального размера, "currentChartScale" со значением 0, "firstVisibleBarIndex" со значением 0 для крайнего левого бара, "visibleBarsCount" со значением 0, а также "minPrice" и "maxPrice" со значением 0.0 для диапазона. Для оптимизации мы используем статическую переменную "lastRedrawTime" со значением 0 для ограничения частоты перерисовок, статическую "previousTrend" со значением -1 для обнаружения изменений и "objectPrefix" со значением "PTD_" для именования продлений. После компиляции получаем следующее окно входных параметров.

После завершения работы с входными параметрами можно перейти к обработчику события инициализации и инициализировать программу. Ниже приведена используемая логика.
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { // Set chart properties currentChartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width currentChartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height currentChartScale = (int)ChartGetInteger(0, CHART_SCALE); //--- Get chart scale firstVisibleBarIndex = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); //--- Get first visible bar visibleBarsCount = (int)ChartGetInteger(0, CHART_VISIBLE_BARS); //--- Get visible bars minPrice = ChartGetDouble(0, CHART_PRICE_MIN, 0); //--- Get min price maxPrice = ChartGetDouble(0, CHART_PRICE_MAX, 0); //--- Get max price // Indicator buffers SetIndexBuffer(0,slowLineUpBuffer,INDICATOR_DATA); //--- Set slow up buffer SetIndexBuffer(1,slowLineDownBuffer,INDICATOR_DATA); //--- Set slow down buffer SetIndexBuffer(2,fastLineBuffer,INDICATOR_DATA); //--- Set fast buffer SetIndexBuffer(3,fastLineColorBuffer,INDICATOR_COLOR_INDEX); //--- Set fast color buffer SetIndexBuffer(4,trendArrowBuffer,INDICATOR_DATA); //--- Set arrow buffer SetIndexBuffer(5,trendArrowColorBuffer,INDICATOR_COLOR_INDEX); //--- Set arrow color buffer SetIndexBuffer(6,trendBuffer,INDICATOR_CALCULATIONS); //--- Set trend buffer SetIndexBuffer(7,slowLineBuffer,INDICATOR_CALCULATIONS); //--- Set slow buffer // Plot settings PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,slowPeriod); //--- Set slow draw begin PlotIndexSetInteger(1,PLOT_DRAW_BEGIN,slowPeriod); //--- Set slow draw begin PlotIndexSetInteger(2,PLOT_DRAW_BEGIN,fastPeriod); //--- Set fast draw begin PlotIndexSetInteger(3,PLOT_DRAW_BEGIN,fastPeriod); //--- Set fast draw begin PlotIndexSetInteger(4,PLOT_DRAW_BEGIN,slowPeriod); //--- Set arrow draw begin PlotIndexSetInteger(3,PLOT_ARROW,arrowCode); //--- Set arrow code // Line extensions PlotIndexSetInteger(0,PLOT_SHIFT,extendBars); //--- Set slow up shift PlotIndexSetInteger(1,PLOT_SHIFT,extendBars); //--- Set slow down shift PlotIndexSetInteger(2,PLOT_SHIFT,extendBars); //--- Set fast shift PlotIndexSetInteger(3,PLOT_SHIFT,0); //--- Set arrow shift // Set plot colors dynamically PlotIndexSetInteger(0, PLOT_LINE_COLOR, 0, upColor); //--- Set slow up color PlotIndexSetInteger(1, PLOT_LINE_COLOR, 0, downColor); //--- Set slow down color PlotIndexSetInteger(2, PLOT_LINE_COLOR, 0, upColor); //--- Set fast up color PlotIndexSetInteger(2, PLOT_LINE_COLOR, 1, downColor); //--- Set fast down color PlotIndexSetInteger(4, PLOT_LINE_COLOR, 0, upColor); //--- Set arrow up color PlotIndexSetInteger(4, PLOT_LINE_COLOR, 1, downColor); //--- Set arrow down color // Short name string shortName = "PTD(" + IntegerToString(fastPeriod) + "," + IntegerToString(slowPeriod) + ")"; //--- Set short name IndicatorSetString(INDICATOR_SHORTNAME, shortName); //--- Set indicator short name return(INIT_SUCCEEDED); //--- Return success }
В обработчике OnInit, который выполняется при добавлении индикатора на график или его перезагрузке, сначала получаем и сохраняем текущие размеры графика и параметры отображения, поскольку они понадобятся позже при отрисовке на Canvas: ширину в пикселях получаем через ChartGetInteger с использованием CHART_WIDTH_IN_PIXELS и записываем в "currentChartWidth", высоту через CHART_HEIGHT_IN_PIXELS и записываем в "currentChartHeight", масштаб через CHART_SCALE и записываем в "currentChartScale", первый видимый бар через CHART_FIRST_VISIBLE_BAR и записываем в "firstVisibleBarIndex", количество видимых баров через CHART_VISIBLE_BARS и записываем в "visibleBarsCount", минимальную цену через ChartGetDouble и "CHART_PRICE_MIN" записываем в "minPrice", а максимум через CHART_PRICE_MAX записываем в "maxPrice". Эти значения позволят адаптивно выполнять отрисовку в зависимости от текущего вида графика.
Затем связываем восемь буферов с отображаемыми сериями: "slowLineUpBuffer" назначаем индексу 0 как данные, "slowLineDownBuffer" — индексу 1 как данные, "fastLineBuffer" — индексу 2 как данные, "fastLineColorBuffer" — индексу 3 как индекс цвета, "trendArrowBuffer" — индексу 4 как данные, "trendArrowColorBuffer" — индексу 5 как индекс цвета, "trendBuffer" — индексу 6 как расчеты, "slowLineBuffer" — индексу 7 как расчеты, используя SetIndexBuffer с соответствующими типами. Начало отрисовки графических построений настраиваем с помощью PlotIndexSetInteger и PLOT_DRAW_BEGIN: медленные серии — от "slowPeriod", быстрые и стрелочные — от "fastPeriod" или "slowPeriod". Символ стрелочной серии задаем через PLOT_ARROW и устанавливаем его равным "arrowCode". Для продлений применяем сдвиги через "PLOT_SHIFT": extendBars для медленных линий вверх/вниз и быстрой линии, 0 для стрелок. Цвета отображаемых серий задаем динамически с помощью "PlotIndexSetInteger" и "PLOT_LINE_COLOR": индекс 0 — "upColor", индекс 1 — "downColor", для быстрой линии с индексом 2 — "upColor" в позиции 0 и "downColor" в позиции 1, аналогично для стрелок с индексом 4. Создаем строку короткого имени как "PTD(" плюс быстрый и медленный периоды через запятую плюс ")" и устанавливаем ее с помощью IndicatorSetString и INDICATOR_SHORTNAME. Возвращаем INIT_SUCCEEDED для подтверждения успешной инициализации. После компиляции получаем следующий результат.

На изображении видно, что индикатор корректно настраивается при загрузке. В окне данных отображаются буферы, и теперь нужно заполнить их, а также выполнить расчеты индикатора, чтобы получить значения по нашей стратегии. Сделаем это в обработчике OnCalculate следующим образом.
//+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int& spread[]) { // Always calculate buffers int startBar = prev_calculated - 1; //--- Set start bar if(startBar < 0) startBar = 0; //--- Adjust start bar for(int barIndex = startBar; barIndex < rates_total && !_StopFlag; barIndex++) { int fastStartBar = barIndex - fastPeriod + 1; //--- Calc fast start if(fastStartBar < 0) fastStartBar = 0; //--- Adjust fast start int slowStartBar = barIndex - slowPeriod + 1; //--- Calc slow start if(slowStartBar < 0) slowStartBar = 0; //--- Adjust slow start double slowHigh = high[ArrayMaximum(high, slowStartBar, slowPeriod)]; //--- Get slow high double slowLow = low[ArrayMinimum(low, slowStartBar, slowPeriod)]; //--- Get slow low double fastHigh = high[ArrayMaximum(high, fastStartBar, fastPeriod)]; //--- Get fast high double fastLow = low[ArrayMinimum(low, fastStartBar, fastPeriod)]; //--- Get fast low if(barIndex > 0) { slowLineBuffer[barIndex] = (close[barIndex] > slowLineBuffer[barIndex-1]) ? slowLow : slowHigh; //--- Set slow line fastLineBuffer[barIndex] = (close[barIndex] > fastLineBuffer[barIndex-1]) ? fastLow : fastHigh; //--- Set fast line trendBuffer[barIndex] = trendBuffer[barIndex-1]; //--- Set trend if(close[barIndex] < slowLineBuffer[barIndex] && close[barIndex] < fastLineBuffer[barIndex]) trendBuffer[barIndex] = 1; //--- Set up trend if(close[barIndex] > slowLineBuffer[barIndex] && close[barIndex] > fastLineBuffer[barIndex]) trendBuffer[barIndex] = 0; //--- Set down trend trendArrowBuffer[barIndex] = (trendBuffer[barIndex] != trendBuffer[barIndex-1]) ? slowLineBuffer[barIndex] : EMPTY_VALUE; //--- Set arrow slowLineUpBuffer[barIndex] = (trendBuffer[barIndex] == 0) ? slowLineBuffer[barIndex] : EMPTY_VALUE; //--- Set slow up slowLineDownBuffer[barIndex] = (trendBuffer[barIndex] == 1) ? slowLineBuffer[barIndex] : EMPTY_VALUE; //--- Set slow down } else { trendArrowBuffer[barIndex] = slowLineUpBuffer[barIndex] = slowLineDownBuffer[barIndex] = EMPTY_VALUE; //--- Set empties trendBuffer[barIndex] = fastLineColorBuffer[barIndex] = trendArrowColorBuffer[barIndex] = 0; //--- Set zeros fastLineBuffer[barIndex] = slowLineBuffer[barIndex] = close[barIndex]; //--- Set first lines } fastLineColorBuffer[barIndex] = trendArrowColorBuffer[barIndex] = trendBuffer[barIndex]; //--- Set colors } return(rates_total); //--- Return total rates }
В обработчике OnCalculate, который вызывается на каждом новом тике или баре, мы обновляем буферы индикатора свежими ценовыми данными, чтобы графические построения отражали текущие рыночные условия. Начальный бар для расчетов определяем как "prev_calculated - 1" и корректируем его до 0, если значение отрицательное, чтобы избежать недопустимых индексов. Затем выполняем цикл от "startBar" до "rates_total - 1", пока выполнение не остановлено: для каждого "barIndex" рассчитываем начало быстрого периода как "barIndex - fastPeriod + 1" (ограничиваем снизу нулем), а медленного — как "barIndex - slowPeriod + 1" (также ограничиваем нулем). Медленный максимум находим как максимальное значение High за медленный период с помощью ArrayMaximum по массиву high от "slowStartBar", медленный минимум — как минимальное значение Low с помощью ArrayMinimum, быстрый максимум — как максимальное значение High за быстрый период, быстрый минимум — как минимальное значение Low.
Для "barIndex > 0" мы записываем в "slowLineBuffer[barIndex]" медленный минимум, если close находится выше предыдущей медленной линии, иначе — медленный максимум; в "fastLineBuffer[barIndex]" записываем быстрый минимум, если close находится выше предыдущей быстрой линии, иначе — быстрый максимум. Предыдущее состояние тренда копируем в "trendBuffer[barIndex]", затем обновляем его: при close ниже обеих линий переменной trend присваивается 1, а при close выше обеих — 0; далее эти числовые состояния используются в коде для выбора цвета и отображаемой линии. В "trendArrowBuffer[barIndex]" помещаем стрелку на уровне медленной линии, если тренд изменился относительно предыдущего значения, иначе оставляем пустое значение. В "slowLineUpBuffer[barIndex]" записываем медленную линию, если trend равен 0, иначе пустое значение; в "slowLineDownBuffer[barIndex]" — медленную линию, если trend равен 1, иначе пустое значение. Для первого бара ("barIndex == 0") задаем стрелки и медленные линии вверх/вниз как пустые значения, trend/цвет быстрой линии/цвет стрелки — как 0, а быстрые/медленные линии — как close[0] для инициализации. Значения "fastLineColorBuffer[barIndex]" и "trendArrowColorBuffer[barIndex]" устанавливаем равными значению тренда для индексирования цвета. Возвращаем "rates_total", чтобы указать, что обработаны все бары. Теперь после компиляции получаем следующий результат.

На изображении видно, что индикатор корректно рассчитывается и отображается на графике, а массивы буферов заполняются значениями данных. Осталось добавить цены справа от линий индикатора, чтобы видеть точные ценовые уровни линий. Это несложно. Для модульности вынесем логику в отдельную функцию.
//+------------------------------------------------------------------+ //| Draw right price extension line/label | //+------------------------------------------------------------------+ bool drawRightPrice(string objectName, datetime lineTime, double linePrice, color lineColor, ENUM_LINE_STYLE lineStyle = STYLE_SOLID) { bool objectExists = (ObjectFind(0, objectName) >= 0); //--- Check exists if(!objectExists) { if(!ObjectCreate(0, objectName, OBJ_ARROW_RIGHT_PRICE, 0, lineTime, linePrice)) { Print("Failed to create ", objectName); //--- Log failure return false; //--- Return failure } } else { ObjectSetInteger(0, objectName, OBJPROP_TIME, 0, lineTime); //--- Set time ObjectSetDouble(0, objectName, OBJPROP_PRICE, 0, linePrice); //--- Set price } long currentScale = ChartGetInteger(0, CHART_SCALE); //--- Get scale int lineWidth = 1; //--- Init width if(currentScale <= 1) lineWidth = 1; //--- Set width small else if(currentScale <= 3) lineWidth = 2; //--- Set width medium else lineWidth = 3; //--- Set width large ObjectSetInteger(0, objectName, OBJPROP_COLOR, lineColor); //--- Set color ObjectSetInteger(0, objectName, OBJPROP_WIDTH, lineWidth); //--- Set width ObjectSetInteger(0, objectName, OBJPROP_STYLE, lineStyle); //--- Set style ObjectSetInteger(0, objectName, OBJPROP_BACK, false); //--- Set foreground ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false); //--- Set not selectable ObjectSetInteger(0, objectName, OBJPROP_SELECTED, false); //--- Set not selected ChartRedraw(0); //--- Redraw chart return true; //--- Return success } // we then call this function in the "OnCalculate" event handler // Draw line extensions if enabled if(showExtensions && rates_total > 0) { int latestBarIndex = rates_total - 1; //--- Get latest index double slowLineValue = slowLineBuffer[latestBarIndex]; //--- Get slow value double fastLineValue = fastLineBuffer[latestBarIndex]; //--- Get fast value double currentTrend = trendBuffer[latestBarIndex]; //--- Get trend color lineColor = (currentTrend == 0.0) ? upColor : downColor; //--- Set line color datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current time long timeOffset = (long)extendBars * PeriodSeconds(_Period); //--- Calc offset datetime extensionTime = currentBarTime + (datetime)timeOffset; //--- Calc extension time drawRightPrice(objectPrefix + "SLOW", extensionTime, slowLineValue, lineColor, STYLE_SOLID); //--- Draw slow extension drawRightPrice(objectPrefix + "FAST", extensionTime, fastLineValue, lineColor, STYLE_DOT); //--- Draw fast extension }
Для логики отрисовки цены справа определяем функцию "drawRightPrice", которая создает или обновляет объект правой ценовой отметки (OBJ_ARROW_RIGHT_PRICE). Он визуально показывает цену справа от линии; благодаря этому линии индикатора продлеваются горизонтально вправо и выводятся на будущие бары в соответствии с входными настройками. Сначала проверяем наличие объекта с помощью ObjectFind — если его нет, создаем OBJ_ARROW_RIGHT_PRICE в заданных "lineTime" и "linePrice" с помощью ObjectCreate, при неудаче записываем сообщение в журнал и возвращаем false. Если объект существует, обновляем его временную и ценовую привязки с помощью ObjectSetInteger для OBJPROP_TIME и ObjectSetDouble для "OBJPROP_PRICE". Текущий масштаб графика получаем с помощью ChartGetInteger и CHART_SCALE и записываем в "currentScale", затем задаем "lineWidth" в зависимости от масштаба: 1 для scale <=1, 2 для <=3, 3 для большего масштаба, чтобы обеспечить видимость при разных уровнях приближения.
Настраиваем объект: задаем цвет с помощью OBJPROP_COLOR равным "lineColor", ширину — "lineWidth", стиль — "lineStyle" (по умолчанию сплошной), передний план через "OBJPROP_BACK" false, а также запрет выделения и выбора через "OBJPROP_SELECTABLE" и "OBJPROP_SELECTED" false. Перерисовываем график с помощью ChartRedraw и при успешном выполнении возвращаем true. Эту функцию вызываем в обработчике OnCalculate если "showExtensions" равно true и бары существуют: получаем последний индекс как "rates_total - 1", извлекаем медленное и быстрое значения из буферов, тренд из "trendBuffer", выбираем "lineColor" как "upColor", если trend равен 0.0, иначе "downColor", получаем время текущего бара с помощью iTime со сдвигом 0, рассчитываем смещение как "extendBars * PeriodSeconds(_Period)", время продления — как текущее время плюс смещение, затем вызываем "drawRightPrice" для медленной линии со сплошным стилем и для быстрой линии с пунктирным стилем, используя "objectPrefix + "SLOW"" или "FAST". После компиляции получаем следующий результат.

После отрисовки правой цены основная часть индикатора уже завершена. Осталось выполнить отрисовку на Canvas, чтобы залить область между линиями индикатора так, как мы планировали. Для этого определим несколько вспомогательных функций.
//+------------------------------------------------------------------+ //| Convert chart scale to bar width | //+------------------------------------------------------------------+ int BarWidth(int chartScale) { return (int)MathPow(2.0, chartScale); //--- Return bar width } //+------------------------------------------------------------------+ //| Convert bar shift to x pixel | //+------------------------------------------------------------------+ int ShiftToX(int barShift) { return (int)((firstVisibleBarIndex - barShift) * BarWidth(currentChartScale) - 1); //--- Return x pixel } //+------------------------------------------------------------------+ //| Convert price to y pixel | //+------------------------------------------------------------------+ int PriceToY(double price) { if(maxPrice - minPrice == 0.0) return 0; //--- Return zero if no range return (int)MathRound(currentChartHeight * (maxPrice - price) / (maxPrice - minPrice) - 1); //--- Return y pixel }
Сначала определяем функцию "BarWidth" для расчета ширины каждого бара в пикселях на основе текущего масштаба графика. Она возвращает целое число из "MathPow(2.0, chartScale)" — это дает экспоненциальную оценку (1 при масштабе 0, 2 при 1, 4 при 2 и т. д.) для позиционирования в координатах Canvas. Затем реализуем функцию "ShiftToX", которая преобразует сдвиг бара (относительно крайнего левого видимого бара) в x-координату на графике в пикселях, вычисляя "(firstVisibleBarIndex - barShift) * BarWidth(currentChartScale) - 1" с приведением к int. Это размещает элементы справа (более свежие) налево (более старые) с корректировкой на 1 для выравнивания. Наконец, создаем функцию "PriceToY", которая сопоставляет цену с y-координатой Canvas в пикселях: возвращает 0, если ценовой диапазон отсутствует ("maxPrice - minPrice == 0.0"), иначе округляет "currentChartHeight * (maxPrice - price) / (maxPrice - minPrice) - 1" с помощью MathRound с приведением к int. Это инвертирует ось Y (более высокие цены находятся сверху) и корректирует значение на 1 для точной отрисовки. Теперь используем эти функции, чтобы создать основную функцию, выполняющую основную работу.
//+-----------------------------------------------------------------------------------------+ //| Fill area between two lines using trend for color with gradient alpha from slow to fast | //+-----------------------------------------------------------------------------------------+ void DrawFilling(const double &slowLineValues[], const double &fastLineValues[], const double &trendValues[], color fillUpColor, color fillDownColor, uchar fillAlpha = 255, int extendShift = 0) { int firstVisibleBar = firstVisibleBarIndex; //--- Get first visible int totalBarsToDraw = visibleBarsCount + extendShift; //--- Calc bars to draw int bufferSize = (int)ArraySize(slowLineValues); //--- Get buffer size if(bufferSize == 0 || bufferSize != ArraySize(fastLineValues) || bufferSize != ArraySize(trendValues)) return; //--- Return if invalid int previousX = -1; //--- Init previous X int previousY1 = -1; //--- Init previous Y1 int previousY2 = -1; //--- Init previous Y2 for(int offset = 0; offset < totalBarsToDraw; offset++) { int barPosition = firstVisibleBar - offset; //--- Calc bar position int x = ShiftToX(barPosition); //--- Calc x if(x >= currentChartWidth) break; //--- Break if beyond width int dataBarShift = firstVisibleBar - offset + extendShift; //--- Calc data shift int bufferBarIndex = bufferSize - 1 - dataBarShift; //--- Calc buffer index if(bufferBarIndex < 0 || bufferBarIndex >= bufferSize) { previousX = -1; //--- Reset previous X continue; //--- Continue } double value1 = slowLineValues[bufferBarIndex]; //--- Get value1 double value2 = fastLineValues[bufferBarIndex]; //--- Get value2 if(value1 == EMPTY_VALUE || value2 == EMPTY_VALUE) { previousX = -1; //--- Reset previous X continue; //--- Continue } int y1 = PriceToY(value1); //--- Calc y1 int y2 = PriceToY(value2); //--- Calc y2 double currentTrend = trendValues[bufferBarIndex]; //--- Get trend uint baseColorRGB = (currentTrend == 0.0) ? (ColorToARGB(fillUpColor, 255) & 0x00FFFFFF) : (ColorToARGB(fillDownColor, 255) & 0x00FFFFFF); //--- Set base RGB if(previousX != -1 && x > previousX) { double deltaX = x - previousX; //--- Calc delta X int endColumn = MathMin(x, currentChartWidth - 1); //--- Calc end column double maxT = (double)(endColumn - previousX) / deltaX; //--- Calc max T for(int column = previousX; column <= endColumn; column++) { double t = (column - previousX) / deltaX; //--- Calc t double interpolatedY1 = previousY1 + t * (y1 - previousY1); //--- Interpolate Y1 double interpolatedY2 = previousY2 + t * (y2 - previousY2); //--- Interpolate Y2 int upperY = (int)MathRound(MathMin(interpolatedY1, interpolatedY2)); //--- Calc upper Y int lowerY = (int)MathRound(MathMax(interpolatedY1, interpolatedY2)); //--- Calc lower Y if(upperY > lowerY) continue; //--- Continue if invalid double slowLineY = interpolatedY1; //--- Set slow Y double height = MathAbs(interpolatedY1 - interpolatedY2); //--- Calc height if(height == 0.0) continue; //--- Continue if no height // Fill per row with gradient from slow (opaque) to fast (transparent) for(int row = upperY; row <= lowerY; row++) { double distanceFromSlow = MathAbs(row - slowLineY); //--- Calc distance double gradientFraction = distanceFromSlow / height; //--- Calc fraction uchar alphaValue = (uchar)(fillAlpha * (1.0 - gradientFraction)); //--- Calc alpha if(alphaValue > fillAlpha) alphaValue = fillAlpha; //--- Cap alpha uint pixelColor = ((uint)alphaValue << 24) | baseColorRGB; //--- Set pixel color obj_Canvas.FillRectangle(column, row, column, row, pixelColor); //--- Fill pixel } } } previousX = x; //--- Update previous X previousY1 = y1; //--- Update previous Y1 previousY2 = y2; //--- Update previous Y2 } } //+------------------------------------------------------------------+ //| Redraw the canvas | //+------------------------------------------------------------------+ void Redraw(void) { if(currentChartWidth <= 0 || currentChartHeight <= 0) return; //--- Return if invalid size uint defaultColor = 0; //--- Default color obj_Canvas.Erase(defaultColor); //--- Erase canvas DrawFilling(slowLineBuffer, fastLineBuffer, trendBuffer, upColor, downColor, (uchar)fillOpacity, extendBars); //--- Draw filling obj_Canvas.Update(); //--- Update canvas }
Определяем функцию "DrawFilling" для отрисовки области между медленной и быстрой линиями на Canvas с градиентной заливкой. Тренд используется для выбора цвета восходящего или нисходящего движения, а прозрачность плавно уменьшается от медленной линии (полное значение "fillAlpha") к быстрой линии (прозрачно), создавая плавное визуальное затухание с продлением на "extendShift" баров, если оно включено. Сначала получаем первый видимый бар и рассчитываем "totalBarsToDraw" как количество видимых баров плюс "extendShift", затем получаем размер буфера из "slowLineValues" и досрочно выходим, если данные недействительны или не совпадают с быстрым/трендовым буферами. Инициализируем предыдущие X/Y1/Y2 значением -1 для отслеживания интерполяции, затем выполняем цикл по смещениям от 0 до "totalBarsToDraw - 1": для каждого смещения вычисляем позицию бара как "firstVisibleBar - offset", x-пиксель через "ShiftToX" и прерываем цикл, если значение выходит за ширину графика; сдвиг данных как видимый бар минус смещение плюс "extendShift", индекс буфера как размер минус 1 минус сдвиг данных — пропускаем значения вне диапазона или пустые значения, сбрасывая предыдущий X.
Затем получаем медленное value1 и быстрое value2 из буферов, преобразуем их в y1/y2 через "PriceToY", определяем тренд из "trendValues" и задаем базовый RGB из "fillUpColor" или "fillDownColor" с помощью ColorToARGB с маской до RGB. Если предыдущий X действителен и текущий X больше предыдущего, выполняем интерполяцию: рассчитываем delta X, конечный столбец как минимум из X и ширины минус 1, максимальный t как (end - previous) / delta. Для каждого столбца от предыдущего до конечного вычисляем t как (column - previous) / delta, интерполируем y1 и y2, округляем min/max до верхнего/нижнего Y и пропускаем, если upper > lower. Устанавливаем "slowLineY" равным интерполированному y1, высоту — как abs y1 минус y2, пропуская нулевую высоту. Для каждой строки от upper до lower рассчитываем расстояние от медленной линии, долю как distance / height, alpha как "fillAlpha * (1.0 - fraction)" с приведением к uchar, ограничиваем alpha значением "fillAlpha", объединяем цвет пикселя как alpha, сдвинутую на 24 бита, с OR по базовому RGB, и заполняем один пиксель в столбце/строке с помощью "obj_Canvas.FillRectangle" (1x1). Обновляем previous X до текущего X, Y1 до Y1 и Y2 до Y2 для следующей итерации.
Реализуем функцию "Redraw", чтобы при необходимости обновлять отрисовку Canvas; если ширина или высота недействительны (<=0), выходим досрочно. Устанавливаем цвет по умолчанию равным 0 (прозрачный), очищаем Canvas через "obj_Canvas.Erase", вызываем "DrawFilling" и передаем медленный/быстрый/трендовый буферы, цвета вверх/вниз, "fillOpacity" с приведением к uchar и "extendBars", затем обновляем отображение Canvas через "obj_Canvas.Update". Именно эту функцию будем вызывать, когда нужно отрисовать градиентную заливку области между линиями индикатора, как показано ниже в обработчике расчета.
if(!enableFilling) return(rates_total); //--- Return if no filling // Canvas logic only if enabled bool isNewBar = (rates_total > prev_calculated); //--- Check new bar bool hasTrendChanged = false; //--- Init trend changed if(rates_total > 0 && trendBuffer[rates_total-1] != previousTrend) { hasTrendChanged = true; //--- Set changed previousTrend = trendBuffer[rates_total-1]; //--- Update previous trend } // Update chart properties (only if changed) bool hasChartChanged = false; //--- Init chart changed int newChartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get new width int newChartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get new height int newChartScale = (int)ChartGetInteger(0, CHART_SCALE); //--- Get new scale int newFirstVisibleBar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); //--- Get new first visible int newVisibleBars = (int)ChartGetInteger(0, CHART_VISIBLE_BARS); //--- Get new visible bars double newMinPrice = ChartGetDouble(0, CHART_PRICE_MIN, 0); //--- Get new min price double newMaxPrice = ChartGetDouble(0, CHART_PRICE_MAX, 0); //--- Get new max price if(newChartWidth != currentChartWidth || newChartHeight != currentChartHeight) { obj_Canvas.Resize(newChartWidth, newChartHeight); //--- Resize canvas currentChartWidth = newChartWidth; //--- Update width currentChartHeight = newChartHeight; //--- Update height hasChartChanged = true; //--- Set changed } if(newChartScale != currentChartScale || newFirstVisibleBar != firstVisibleBarIndex || newVisibleBars != visibleBarsCount || newMinPrice != minPrice || newMaxPrice != maxPrice) { currentChartScale = newChartScale; //--- Update scale firstVisibleBarIndex = newFirstVisibleBar; //--- Update first visible visibleBarsCount = newVisibleBars; //--- Update visible bars minPrice = newMinPrice; //--- Update min price maxPrice = newMaxPrice; //--- Update max price hasChartChanged = true; //--- Set changed } // Redraw only on: new bar, trend change, or chart resize/scroll. Debounce to 1x/sec max. datetime currentTime = TimeCurrent(); //--- Get current time if((isNewBar || hasTrendChanged || hasChartChanged) && (currentTime - lastRedrawTime >= 1)) { Redraw(); //--- Redraw canvas lastRedrawTime = currentTime; //--- Update last redraw }
Здесь мы досрочно возвращаем "rates_total", если "enableFilling" равно false, пропуская логику Canvas для повышения производительности при отключенной заливке. Затем выполняем операции, связанные с Canvas, только если заливка включена: проверяем появление нового бара через "rates_total > prev_calculated" и записываем результат в "isNewBar", обнаруживаем изменения тренда, сравнивая "trendBuffer[rates_total-1]" с "previousTrend", если бары существуют, устанавливаем "hasTrendChanged" в true и обновляем "previousTrend", если значение отличается. Отслеживаем изменения графика: инициализируем "hasChartChanged" как false, получаем новые ширину/высоту/масштаб/первый видимый бар/количество видимых баров/min price/max price с помощью функций ChartGetInteger и ChartGetDouble. Если ширина или высота изменились, изменяем размер Canvas через "obj_Canvas.Resize" до новых размеров, обновляем "currentChartWidth" и "currentChartHeight" и устанавливаем "hasChartChanged" в true. Если изменились масштаб, первый видимый бар, количество видимых баров, минимальная или максимальная цена, соответствующим образом обновляем глобальные переменные и устанавливаем "hasChartChanged" в true.
Наконец, оптимизируем перерисовки: получаем текущее время с помощью TimeCurrent и записываем в "currentTime". Если появился новый бар, изменился тренд или изменился график, а с момента "lastRedrawTime" прошла как минимум 1 секунда, вызываем "Redraw" для обновления Canvas и устанавливаем "lastRedrawTime" равным текущему времени. Это ограничивает частоту перерисовки максимум одним разом в секунду и снижает лишние вычисления. Теперь остается только перерисовывать изменения при обнаружении событий графика и удалять их при деинициализации, как показано ниже.
//+------------------------------------------------------------------+ //| Chart event handler | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { if(id != CHARTEVENT_CHART_CHANGE || !enableFilling) return; int newChartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get new width int newChartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get new height if(newChartWidth != currentChartWidth || newChartHeight != currentChartHeight) { obj_Canvas.Resize(newChartWidth, newChartHeight); //--- Resize canvas currentChartWidth = newChartWidth; //--- Update width currentChartHeight = newChartHeight; //--- Update height Redraw(); //--- Redraw canvas return; //--- Return } int newChartScale = (int)ChartGetInteger(0, CHART_SCALE); //--- Get new scale int newFirstVisibleBar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); //--- Get new first visible int newVisibleBars = (int)ChartGetInteger(0, CHART_VISIBLE_BARS); //--- Get new visible bars double newMinPrice = ChartGetDouble(0, CHART_PRICE_MIN, 0); //--- Get new min price double newMaxPrice = ChartGetDouble(0, CHART_PRICE_MAX, 0); //--- Get new max price if(newChartScale != currentChartScale || newFirstVisibleBar != firstVisibleBarIndex || newVisibleBars != visibleBarsCount || newMinPrice != minPrice || newMaxPrice != maxPrice) { currentChartScale = newChartScale; //--- Update scale firstVisibleBarIndex = newFirstVisibleBar; //--- Update first visible visibleBarsCount = newVisibleBars; //--- Update visible bars minPrice = newMinPrice; //--- Update min price maxPrice = newMaxPrice; //--- Update max price Redraw(); //--- Redraw canvas } } //+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(enableFilling) obj_Canvas.Destroy(); //--- Destroy canvas if enabled ObjectsDeleteAll(0,objectPrefix,0,OBJ_ARROW_RIGHT_PRICE); //--- Delete right price arrows ChartRedraw(0); //--- Redraw chart }
Здесь мы вызываем обработчик OnChartEvent для обработки событий, связанных с графиком. В частности, реагируем на изменения только если заливка включена, то есть "enableFilling" равно true; в противном случае выходим досрочно. Сначала получаем новую ширину и высоту графика с помощью ChartGetInteger с использованием CHART_WIDTH_IN_PIXELS и CHART_HEIGHT_IN_PIXELS. Если какое-либо из этих значений отличается от "currentChartWidth" или "currentChartHeight", изменяем размер Canvas до новых размеров через "obj_Canvas.Resize", обновляем глобальные переменные, вызываем "Redraw" для обновления заливки и выходим. Затем получаем новый масштаб через CHART_SCALE, первый видимый бар через "CHART_FIRST_VISIBLE_BAR", видимые бары через "CHART_VISIBLE_BARS", минимальную цену через ChartGetDouble и "CHART_PRICE_MIN", максимум через CHART_PRICE_MAX. Если масштаб, первый видимый бар, количество видимых баров, минимальная или максимальная цена отличаются от сохраненных глобальных значений, обновляем "currentChartScale", "firstVisibleBarIndex", "visibleBarsCount", "minPrice", "maxPrice" и вызываем "Redraw", чтобы адаптировать заливку Canvas к новому виду графика.
В обработчике OnDeinit, который выполняется при удалении индикатора или закрытии терминала для очистки ресурсов: если "enableFilling" равно true, уничтожаем Canvas через "obj_Canvas.Destroy"; удаляем все объекты правых ценовых меток, начинающиеся с "objectPrefix", с помощью ObjectsDeleteAll, указывая график 0, окно 0 и тип OBJ_ARROW_RIGHT_PRICE; затем перерисовываем график с помощью функции ChartRedraw После компиляции получаем следующий результат.

По визуализации видно, что мы рассчитываем индикатор и выполняем заливку Canvas, когда она разрешена, тем самым достигая поставленных целей. Осталось выполнить бэктестирование программы — этим займемся в следующем разделе.
Бэктестирование
Мы провели тестирование, и ниже показана итоговая визуализация в виде единого растрового изображения формата Graphics Interchange Format (GIF).

Заключение
В заключение отметим, что мы создали трендовый индикатор на основе пивотов в MQL5, который рассчитывает быстрые и медленные пивотные линии по диапазонам максимумов и минимумов, определяет направление тренда с помощью цветных линий и стрелок, при необходимости продлевает линии для проекций и заполняет области с помощью градиентной заливки средствами Canvas для большей наглядности, одновременно оптимизируя перерисовку на новых барах или при изменениях графика. Этот индикатор предоставляет гибкий инструмент для обнаружения тренда с настраиваемыми входными параметрами. В следующих частях мы рассмотрим более сложные индикаторы, такие как каналы волатильности или осцилляторы импульса с элементами машинного обучения. Следите за продолжением.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20610
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Реализация модели гауссовской смеси в MQL5
Моделирование рынка: Position View (VII)
Создание пользовательских индикаторов в MQL5 (Часть 2): Разработка индикатора RSI в виде шкалы со стрелкой на Canvas
От начального до среднего уровня: Навигация по песочнице
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования