English Deutsch 日本語
preview
Создание пользовательских индикаторов в MQL5 (Часть 5): Доработка WaveTrend Crossover с помощью Canvas — градиентный туман, сигнальные пузырьки и управление рисками

Создание пользовательских индикаторов в MQL5 (Часть 5): Доработка WaveTrend Crossover с помощью Canvas — градиентный туман, сигнальные пузырьки и управление рисками

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

Введение

В нашей предыдущей статье (Часть 4) мы разработали индикатор Smart WaveTrend Crossover на MetaQuotes Language 5 (MQL5), использующий два осциллятора: один для сигналов, а другой для фильтрации тренда. Он формировал сигналы на покупку и продажу на основе пересечений с возможностью подтверждения по тренду. В пятой части мы улучшим индикатор WaveTrend Crossover, добавив рисование на основе Canvas для градиентных туманных наложений, сигнальные боксы для обнаружения пробоев, метки сигналов в виде пузырьков с надписями BUY/SELL или простых треугольников, а также встроенное управление рисками через динамические уровни Take Profit и Stop Loss. Эта версия добавляет расширенные визуальные элементы, такие как градиентный туман для рыночного контекста, а также параметры фильтрации по тренду, расширения сигнальных боксов и расчётов через свечные множители или проценты с отображением в виде линий и таблиц. Мы рассмотрим следующие темы:

  1. Понимание улучшенной структуры WaveTrend Crossover на основе Canvas с визуальными функциями и управлением рисками
  2. Реализация в MQL5
  3. Бэктестинг
  4. Заключение

В итоге у вас будет рабочий индикатор MQL5 для улучшенных пересечений WaveTrend с визуальными элементами и инструментами управления рисками, готовый к дальнейшей настройке. Давайте приступим!


Понимание улучшенной структуры WaveTrend Crossover на основе Canvas с визуальными функциями и управлением рисками

Улучшенная структура WaveTrend Crossover на основе Canvas развивает базовый осциллятор моментума, добавляя визуальные наложения и инструменты управления рисками, чтобы обеспечить более наглядный и функциональный торговый интерфейс. Она сохраняет две конфигурации WaveTrend: более чувствительную для обнаружения пересечений, которые указывают на потенциальные входы, и более медленную для фильтрации тренда. При этом добавляется обнаружение пробоев через сигнальные боксы, которые формируются вокруг точек пересечения и закрываются, когда цена пробивает их границы, указывая на подтверждённые изменения импульса. Градиентные туманные наложения отображаются поверх графика и визуально показывают силу тренда через плавно меняющуюся прозрачность, помогая быстро оценивать рыночный контекст. Дополнительно используются метки сигналов в виде пузырьков с надписями BUY/SELL или простых треугольников.

В бычьей конфигурации пересечение вверх в сигнальном осцилляторе, при необходимости подтверждённое восходящим трендом по более медленному осциллятору, создаёт сигнальный бокс вокруг диапазона бара. При пробое верхней границы сигнального бокса срабатывает сигнал покупки, если он соответствует направлению сигнального бокса, а визуальные элементы подчёркивают эту возможность. И наоборот, в медвежьей конфигурации пересечение вниз формирует сигнальный бокс, а пробой вниз генерирует сигнал продажи при выполнении соответствующих условий, позволяя реагировать на развороты или продолжения движения с меньшим уровнем шума. Управление рисками встроено через расчёт уровней Take Profit и Stop Loss на основе средних размеров свечей или процентных изменений; уровни отображаются динамически и помогают с выбором размера позиции и планированием выхода. Таким образом, мы можем оценивать долю успешных срабатываний сигналов.

Мы будем использовать библиотеку MQL5 Canvas для отрисовки градиентного тумана с интерполяцией значений между барами для более плавной визуализации тренда, отслеживать сигнальные боксы для обнаружения пробоев и их закрытия с опциональным продлением через множители средней свечи, предлагать гибкие типы сигналов, например пузырьки с надписями BUY/SELL для лучшей читаемости, и рассчитывать уровни риска с пользовательскими режимами для Take Profit и Stop Loss, обеспечивая при этом эффективную перерисовку при изменениях графика. Ниже кратко показано визуальное представление наших целей.

MQL5 CANVAS-BASED EVOLUTION FRAMEWORK


Реализация в MQL5

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

//+------------------------------------------------------------------+
//|                           1. Smart WaveTrend Crossover PART2.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"

#property indicator_chart_window
#property indicator_buffers 28
#property indicator_plots 3
#property indicator_label1 "Colored Candles"
#property indicator_type1 DRAW_COLOR_CANDLES
#property indicator_color1 clrTeal, clrRed
#property indicator_style1 STYLE_SOLID
#property indicator_width1 1
#property indicator_label2 "Buy Signals"
#property indicator_type2 DRAW_ARROW
#property indicator_color2 clrForestGreen
#property indicator_style2 STYLE_SOLID
#property indicator_width2 1
#property indicator_label3 "Sell Signals"
#property indicator_type3 DRAW_ARROW
#property indicator_color3 clrOrangeRed
#property indicator_style3 STYLE_SOLID
#property indicator_width3 1

Здесь мы увеличиваем количество буферов индикатора с 23 до 28, чтобы обработать дополнительные расчёты для добавленных функций. Для наглядности мы выделили конкретные строки, в которые были внесены изменения. Далее мы подключаем библиотеку Canvas для пользовательской отрисовки на графике. Она нужна для расширенных графических элементов, таких как градиентный туман, пользовательские сигнальные боксы и метки, которые не поддерживаются стандартными функциями построения MQL5. Это улучшает визуальное представление сигналов и трендов. Для этого используется следующий подход.

#include <Canvas/Canvas.mqh>

Мы подключаем библиотеку Canvas с помощью "#include <Canvas/Canvas.mqh>". Она предоставляет классы и функции для пользовательской отрисовки на графике, позволяя программно отображать расширенные визуальные элементы, такие как градиентный туман, сигнальные боксы и метки, без опоры на стандартные типы построений. Далее нужно добавить больше входных параметров, чтобы расширить управление индикатором из интерфейса.

input group "Signal Settings"
input bool use_trend_filter = true;        // Use Trend Filter for Boxes?

enum signal_options {
   Triangles,                              // Triangles
   Labels_Buy_Sell                         // Labels Buy Sell
};
input signal_options signal_type = Labels_Buy_Sell; // Signal Type
input color signal_buy_col = clrForestGreen; // Buy Signal Color
input color signal_sell_col = clrOrangeRed; // Sell Signal Color
input bool show_only_matching = true;      // Show Only Matching Signals?
input bool use_box_multiplier = false;     // Extend Box by Average Candle Size?
input double box_multiplier = 1.0;         // Box Extension Multiplier
input int base_offset = 10;                // Base Signal Offset from Candle

input group "Box Settings"
input color box_bull_fill = clrBlue;       // Box Bull Fill Color
input color box_bear_fill = clrGold;       // Box Bear Fill Color
input int box_fill_transp = 80;            // Box Fill Transparency (0-100)

input group "Fog"
input bool show_fog = true;                // Fog
input double offset_mult = 0.7;            // Fog Height × Avg Candle
input int base_transp = 80;                // Base Transparency
input int transp_inc = 4;                  // Transparency Increment

input group "Risk Management"
input bool showTPSL = true;                // Show TP/SL Levels

enum tp_sl_modes {
   Candle_Multiplier,                      // Candle Multiplier
   Percentage                              // Percentage
};
input tp_sl_modes tpSlMode = Candle_Multiplier; // TP/SL Calculation Mode
input int tp_sl_length = 50;               // Average Candle Length Period
input double tp1Multiplier = 2.0;          // TP1×
input double tp2Multiplier = 3.0;          // TP2×
input double tp3Multiplier = 4.0;          // TP3×
input double slMultiplier = 2.0;           // SL×
input double tp1Percent = 2.0;             // TP1 %
input double tp2Percent = 3.0;             // TP2 %
input double tp3Percent = 4.0;             // TP3 %
input double slPercent = 1.5;              // SL %

Мы продолжаем определять пользовательские входные параметры в сгруппированных секциях, чтобы обеспечить настройку расширенных функций. В группе "Signal Settings" добавляется булев параметр со значением true по умолчанию для применения фильтра тренда к сигналам на основе сигнальных боксов. Затем следует перечисление "signal_options" с вариантами "Triangles" — для отображения сигналов в виде треугольников и "Labels_Buy_Sell" для пузырьков с надписями BUY/SELL; по умолчанию выбран второй вариант, определяющий тип визуализации сигналов. Также добавляются входные параметры цвета для сигналов покупки и продажи, по умолчанию ForestGreen и OrangeRed, булев параметр со значением true для показа только сигналов, совпадающих с направлением сигнального бокса, ещё один булев параметр со значением false для расширения сигнальных боксов с использованием средних размеров свечей, множитель double со значением 1.0 для такого продления и целочисленное смещение 10 для позиционирования сигналов относительно свечей.

Далее в группе "Box Settings" мы добавляем входные параметры цвета для заливки бычьих и медвежьих сигнальных боксов, по умолчанию синий и золотой, а также целочисленный параметр прозрачности заливки от 0 до 100, установленный на 80 для управления непрозрачностью рисуемых сигнальных боксов. В группе "Fog" (настройки тумана) добавляется булев параметр, по умолчанию включающий отображение градиентных туманных наложений, множитель double 0.7 для масштабирования высоты туманного слоя на основе среднего размера свечи, базовая прозрачность 80 и шаг 4 для постепенного изменения прозрачности в градиентном эффекте. Наконец, в группе "Risk Management" мы предлагаем булев параметр, по умолчанию включающий отображение уровней Take Profit и Stop Loss, перечисление "tp_sl_modes" с вариантами "Candle_Multiplier" и "Percentage", где по умолчанию выбран первый метод расчёта, целочисленный период 50 для усреднения длины свечей, а также значения double для множителей или процентов по трём уровням take-profit и одному stop-loss, например 2.0 для первого множителя take-profit и 1.5 для процента stop-loss. Это позволяет настраивать параметры риска.

Затем мы расширим глобальные буферы, добавив средние размеры свечей. Они нужны для новых функций, таких как продление сигнальных боксов, высота туманного слоя и расчёты TP/SL, поскольку эти элементы опираются на показатели волатильности для динамического масштабирования визуальных объектов и уровней.

double avg_candle_size[];     //--- Average candle size buffer

После этого нужно расширить глобальные переменные для работы с объектами Canvas, свойствами графика для динамической отрисовки, временной меткой перерисовки, структурами для хранения данных сигнальных боксов и сигналов, массивами для их хранения, именами линий и таблицы TP/SL, периодом продления, префиксом объектов и размером шрифта. Эти переменные нужны для управления пользовательской графикой, отслеживания видимой области графика для оптимизации, хранения постоянных данных по сигнальным боксам и сигналам, а также работы с визуальными элементами TP/SL, что обеспечивает эффективную перерисовку и масштабирование.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
CCanvas obj_Canvas;           //--- Canvas object

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

static datetime lastRedrawTime = 0; //--- Last redraw time

//+------------------------------------------------------------------+
//| Box information structure                                        |
//+------------------------------------------------------------------+
struct BoxInfo {
   datetime left_time;        // Store left time
   datetime right_time;       // Store right time
   double   top;              // Store top price
   double   bottom;           // Store bottom price
   int      dir;              // Store direction
};
BoxInfo all_boxes[];          //--- All boxes array

//+------------------------------------------------------------------+
//| Signal information structure                                     |
//+------------------------------------------------------------------+
struct SignalInfo {
   datetime time;             // Store signal time
   int      dir;              // Store signal direction
};
SignalInfo all_signals[];     //--- All signals array

string slLine = "SL_Line";    //--- SL line name
string tp1Line = "TP1_Line";  //--- TP1 line name
string tp2Line = "TP2_Line";  //--- TP2 line name
string tp3Line = "TP3_Line";  //--- TP3 line name
string tpSlTableObjects[11];  //--- TP/SL table objects

long extendSeconds = PeriodSeconds() * 100; //--- Extend seconds

string objPrefix = "SWTC_";   //--- Object prefix

int current_font_size;        //--- Current font size

Здесь мы объявляем глобальные переменные для управления состоянием индикатора и пользовательской графикой, начиная с экземпляра класса CCanvas с именем "obj_Canvas", который будет обрабатывать операции рисования на Canvas во всей программе. Затем мы задаём целочисленные переменные для отслеживания текущей ширины, высоты и масштаба графика, индекса первого видимого бара и количества видимых баров, а также переменные double для минимальной и максимальной цены в видимой области графика. Это позволяет динамически корректировать отображение при перерисовке. Статическая переменная datetime "lastRedrawTime" инициализируется нулём, чтобы хранить временную метку последнего обновления Canvas и оптимизировать частоту перерисовки.

Мы определяем структуру "BoxInfo" для хранения сведений о каждом сигнальном боксе: левой и правой временной метки, верхней и нижней цены, а также целочисленного направления. Затем создаём массив "all_boxes", который будет хранить несколько таких структур для управления сигнальными боксами пробоя.

Аналогично мы создаём структуру "SignalInfo" с полями времени и направления, а также массив "all_signals" для хранения списка сформированных сигналов, используемых при отображении. Мы инициализируем строковые переменные для имён линий Take Profit и Stop Loss, например "SL_Line" для stop-loss, а также массив "tpSlTableObjects" размером 11 для ссылок на объекты таблицы управления рисками. Переменная long "extendSeconds" задаётся через PeriodSeconds, умноженный на 100, чтобы определить период продления для некоторых визуальных элементов. В конце задаётся строковый префикс "SWTC_" для имён объектов, чтобы избежать конфликтов, и целочисленная переменная "current_font_size" для динамической настройки размера текста в зависимости от масштаба графика. Затем мы определим несколько вспомогательных функций для визуализации объектов.

//+------------------------------------------------------------------+
//| Darken color                                                     |
//+------------------------------------------------------------------+
color DarkenColor(color c, double factor = 0.5) {
   uchar r = uchar((c & 0xFF) * factor);         //--- Compute red component
   uchar g = uchar(((c >> 8) & 0xFF) * factor);  //--- Compute green component
   uchar b = uchar(((c >> 16) & 0xFF) * factor); //--- Compute blue component
   return (color)((b << 16) | (g << 8) | r);     //--- Return darkened color
}

//+------------------------------------------------------------------+
//| Draw rectangle label                                             |
//+------------------------------------------------------------------+
bool drawRectangleLabel(string objectName, int xDistance, int yDistance, int xSize, int ySize, color rectColor, int borderType = BORDER_FLAT, bool back = true) {
   bool objectExists = (ObjectFind(0, objectName) >= 0);                //--- Check if object exists
   if (!objectExists) {                                                 //--- Handle new object
      if (!ObjectCreate(0, objectName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { //--- Create rectangle label
         Print("Failed to create ", objectName);                        //--- Log failure
         return false;                                                  //--- Return failure
      }
   }
   ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, xDistance);       //--- Set x distance
   ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, yDistance);       //--- Set y distance
   ObjectSetInteger(0, objectName, OBJPROP_XSIZE, xSize);               //--- Set x size
   ObjectSetInteger(0, objectName, OBJPROP_YSIZE, ySize);               //--- Set y size
   ObjectSetInteger(0, objectName, OBJPROP_COLOR, rectColor);           //--- Set color
   ObjectSetInteger(0, objectName, OBJPROP_BORDER_TYPE, borderType);    //--- Set border type
   ObjectSetInteger(0, objectName, OBJPROP_BACK, back);                 //--- Set background
   ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);          //--- Disable selectable
   ObjectSetInteger(0, objectName, OBJPROP_SELECTED, false);            //--- Disable selected
   return true;                                                         //--- Return success
}

//+------------------------------------------------------------------+
//| Draw label                                                       |
//+------------------------------------------------------------------+
bool drawLabel(string objectName, int xDistance, int yDistance, string text, color labelColor) {
   bool objectExists = (ObjectFind(0, objectName) >= 0);                //--- Check if object exists
   if (!objectExists) {                                                 //--- Handle new object
      if (!ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0)) {           //--- Create label
         Print("Failed to create ", objectName);                        //--- Log failure
         return false;                                                  //--- Return failure
      }
   }
   ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, xDistance);       //--- Set x distance
   ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, yDistance);       //--- Set y distance
   ObjectSetString(0, objectName, OBJPROP_TEXT, text);                  //--- Set text
   ObjectSetInteger(0, objectName, OBJPROP_COLOR, labelColor);          //--- Set color
   ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, 10);               //--- Set font size
   ObjectSetString(0, objectName, OBJPROP_FONT, "Arial");               //--- Set font
   ObjectSetInteger(0, objectName, OBJPROP_BACK, false);                //--- Disable background
   ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);          //--- Disable selectable
   ObjectSetInteger(0, objectName, OBJPROP_SELECTED, false);            //--- Disable selected
   return true;                                                         //--- Return success
}

Мы определяем функцию "DarkenColor", которая создаёт более тёмный оттенок заданного цвета. Она принимает исходный цвет и необязательный коэффициент, по умолчанию равный 0.5, извлекает красную, зелёную и синюю компоненты с помощью побитовых операций, таких как "& 0xFF" для красной компоненты, сдвиг вправо на 8 и "& 0xFF" для зелёной, а также сдвиг вправо на 16 и "& 0xFF" для синей. Затем каждая компонента умножается на коэффициент, приводится к типу uchar и снова объединяется в новое значение цвета с помощью сдвигов влево и побитового OR.

Далее мы создаём функцию "drawRectangleLabel" для рисования или обновления прямоугольной метки на графике. Сначала проверяется наличие объекта через ObjectFind, а если объект отсутствует, он создаётся через ObjectCreate с типом OBJ_RECTANGLE_LABEL. При ошибке выводится сообщение через Print и возвращается false. В противном случае задаются такие свойства, как расстояния x и y, размеры, цвет, тип границы со значением BORDER_FLAT по умолчанию, флаг фона со значением true по умолчанию, а также отключаются возможность выбора и выделение объекта, после чего функция возвращает true.

Аналогично реализуется функция "drawLabel" для текстовых меток: она проверяет наличие объекта через "ObjectFind" и при необходимости создаёт его через "ObjectCreate" с типом OBJ_LABEL. Если создание не удалось, выводится ошибка и возвращается false. Затем настраиваются расстояния x и y, текст, цвет, размер шрифта 10, шрифт Arial, отключаются фон, возможность выбора и выделение, после чего функция возвращает true. Теперь при инициализации нам нужно привязать новый буфер для расчётов новых средних размеров свечей. Также нужно инициализировать Canvas и задать метки таблицы.

//+------------------------------------------------------------------+
//| Initialize indicator                                             |
//+------------------------------------------------------------------+
int OnInit() {
   IndicatorSetString(INDICATOR_SHORTNAME, "Smart WaveTrend Crossover"); //--- Set short name

   PlotIndexSetInteger(1, PLOT_ARROW, 233);                              //--- Set buy arrow symbol
   PlotIndexSetInteger(1, PLOT_SHOW_DATA, signal_type == Triangles);     //--- Set buy visibility
   PlotIndexSetInteger(1, PLOT_LINE_COLOR, 0, signal_buy_col);           //--- Set buy color

   PlotIndexSetInteger(2, PLOT_ARROW, 234);                              //--- Set sell arrow symbol
   PlotIndexSetInteger(2, PLOT_SHOW_DATA, signal_type == Triangles);     //--- Set sell visibility
   PlotIndexSetInteger(2, PLOT_LINE_COLOR, 0, signal_sell_col);          //--- Set sell color

   SetIndexBuffer(23, avg_candle_size, INDICATOR_CALCULATIONS);          //--- Bind avg candle size

   currentChartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);   //--- Get chart width
   currentChartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height

   string canvas_name = "SWTC_Canvas";                                   //--- Set Canvas name
   if (!obj_Canvas.CreateBitmapLabel(0, 0, canvas_name, 0, 0, currentChartWidth, currentChartHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create canvas
      Print("Failed to create canvas");                                  //--- Log failure
      return(INIT_FAILED);                                               //--- Return failure
   }

   tpSlTableObjects[0] = objPrefix + "Table_Frame";                      //--- Set table frame
   tpSlTableObjects[1] = objPrefix + "Table_Level";                      //--- Set table level
   tpSlTableObjects[2] = objPrefix + "Table_Price";                      //--- Set table price
   tpSlTableObjects[3] = objPrefix + "Table_TP1";                        //--- Set TP1 label
   tpSlTableObjects[4] = objPrefix + "Table_TP1_Price";                  //--- Set TP1 price
   tpSlTableObjects[5] = objPrefix + "Table_TP2";                        //--- Set TP2 label
   tpSlTableObjects[6] = objPrefix + "Table_TP2_Price";                  //--- Set TP2 price
   tpSlTableObjects[7] = objPrefix + "Table_TP3";                        //--- Set TP3 label
   tpSlTableObjects[8] = objPrefix + "Table_TP3_Price";                  //--- Set TP3 price
   tpSlTableObjects[9] = objPrefix + "Table_SL";                         //--- Set SL label
   tpSlTableObjects[10] = objPrefix + "Table_SL_Price";                  //--- Set SL price

   current_font_size = 10;                                               //--- Initialize font size

   return(INIT_SUCCEEDED);                                               //--- Return success
}

В обработчике событий OnInit мы настраиваем дополнительные свойства индикатора, задавая его короткое имя через IndicatorSetString; мы изменили его для единообразия названия. Для графического построения сигналов покупки мы, как и раньше, задаём символ стрелки 233 через PlotIndexSetInteger, но включаем его видимость только тогда, когда тип сигналов равен "Triangles", поскольку пузырьки с надписями BUY/SELL будут рисоваться отдельно на Canvas. Также применяется заданный пользователем цвет покупки. Аналогично для сигналов продажи задаётся стрелка 234, управление видимостью выполняется тем же способом, и назначается цвет продажи. Буфер среднего размера свечи привязывается к индексу 23 как расчётный буфер для поддержки градиентного тумана и расширения сигнальных боксов.

Мы получаем текущие размеры графика с помощью ChartGetInteger для ширины и высоты, чтобы правильно инициализировать Canvas. Затем задаём имя Canvas и создаём на нём bitmap label через метод "CreateBitmapLabel" объекта Canvas, указывая подокно, позицию, размер и формат цвета "COLOR_FORMAT_ARGB_NORMALIZE". Если создание не удалось, выводим ошибку через Print и возвращаем INIT_FAILED. Мы заполняем массив объектов таблицы именами с префиксом для элементов отображения управления рисками: рамки, меток уровней и цен, а также отдельных записей для Take Profit и Stop Loss. Переменная размера шрифта инициализируется значением 10 для вывода текста. В конце возвращается INIT_SUCCEEDED, подтверждая успешную настройку. После инициализации получаем следующий результат.

INITIALIZED CANVAS

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

//+------------------------------------------------------------------+
//| Draw right price label                                           |
//+------------------------------------------------------------------+
bool drawRightPrice(string objectName, datetime lineTime, double linePrice, color lineColor, ENUM_LINE_STYLE lineStyle = STYLE_SOLID, int lineWidth = 1) {
   bool objectExists = (ObjectFind(0, objectName) >= 0);           //--- Check if object exists
   if (!objectExists) {                                            //--- Handle new object
      if (!ObjectCreate(0, objectName, OBJ_ARROW_RIGHT_PRICE, 0, lineTime, linePrice)) { //--- Create right price
         Print("Failed to create ", objectName);                   //--- Log failure
         return false;                                             //--- Return failure
      }
   } else {                                                        //--- Handle existing object
      ObjectSetInteger(0, objectName, OBJPROP_TIME, 0, lineTime);  //--- Set time
      ObjectSetDouble(0, objectName, OBJPROP_PRICE, 0, linePrice); //--- Set price
   }

   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_FONTSIZE, 10);          //--- Set font size
   ObjectSetString(0, objectName, OBJPROP_FONT, "Arial");          //--- Set font
   ObjectSetInteger(0, objectName, OBJPROP_BACK, false);           //--- Disable background
   ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);     //--- Disable selectable
   ObjectSetInteger(0, objectName, OBJPROP_SELECTED, false);       //--- Disable selected
   ChartRedraw(0);                                                 //--- Redraw chart
   return true;                                                    //--- Return success
}

//+------------------------------------------------------------------+
//| Update font sizes                                                |
//+------------------------------------------------------------------+
void UpdateFontSizes() {
   long scale = 0;                                                 //--- Initialize scale
   if (ChartGetInteger(0, CHART_SCALE, 0, scale)) {                //--- Get chart scale
      current_font_size = (int)(8 + scale * 1.5);                  //--- Compute font size
      current_font_size = MathMax(8, MathMin(18, current_font_size)); //--- Clamp font size
      ChartRedraw(0);                                              //--- Redraw chart
   }
}

//+------------------------------------------------------------------+
//| Convert bar width                                                |
//+------------------------------------------------------------------+
int BarWidth(int chartScale) {
   return (int)MathPow(2.0, chartScale);                           //--- Return bar width
}

//+------------------------------------------------------------------+
//| Convert shift to x                                               |
//+------------------------------------------------------------------+
int ShiftToX(int bar_index) {
   return (firstVisibleBarIndex - bar_index) * BarWidth(currentChartScale); //--- Return x position
}

//+------------------------------------------------------------------+
//| Convert price to y                                               |
//+------------------------------------------------------------------+
int PriceToY(double price) {
   if (maxPrice - minPrice == 0.0) return 0;                       //--- Handle zero range
   return (int)MathRound(currentChartHeight * (maxPrice - price) / (maxPrice - minPrice)); //--- Return y position
}

//+------------------------------------------------------------------+
//| Draw box on canvas                                               |
//+------------------------------------------------------------------+
void DrawBoxOnCanvas(int x_left, int y_top, int x_right, int y_bottom, color fillColor, int fillTransp) {
   int x1 = MathMin(x_left, x_right);                              //--- Set min x
   int x2 = MathMax(x_left, x_right);                              //--- Set max x
   int y1 = MathMin(y_top, y_bottom);                              //--- Set min y
   int y2 = MathMax(y_top, y_bottom);                              //--- Set max y

   uchar alpha_fill = (uchar)(255 * (100 - fillTransp) / 100);     //--- Compute fill alpha
   uint argb_fill = ColorToARGB(fillColor, alpha_fill);            //--- Get fill ARGB
   obj_Canvas.FillRectangle(x1, y1, x2, y2, argb_fill);            //--- Fill rectangle

   color borderColor = DarkenColor(fillColor, 0.7);                //--- Get border color
   uint argb_border = ColorToARGB(borderColor, 255);               //--- Get border ARGB

   obj_Canvas.LineAA(x1, y1, x2, y1, argb_border);                 //--- Draw top border
   obj_Canvas.LineAA(x1, y1 + 1, x2, y1 + 1, argb_border);         //--- Draw top inner

   obj_Canvas.LineAA(x1, y2, x2, y2, argb_border);                 //--- Draw bottom border
   obj_Canvas.LineAA(x1, y2 - 1, x2, y2 - 1, argb_border);         //--- Draw bottom inner

   obj_Canvas.LineAA(x1, y1, x1, y2, argb_border);                 //--- Draw left border
   obj_Canvas.LineAA(x1 + 1, y1, x1 + 1, y2, argb_border);         //--- Draw left inner

   obj_Canvas.LineAA(x2, y1, x2, y2, argb_border);                 //--- Draw right border
   obj_Canvas.LineAA(x2 - 1, y1, x2 - 1, y2, argb_border);         //--- Draw right inner
}

Сначала мы создаём функцию "drawRightPrice" для рисования или обновления ценовой метки, выровненной по правому краю графика. Она проверяет наличие объекта через ObjectFind и, если объект отсутствует, создаёт его через ObjectCreate с типом OBJ_ARROW_RIGHT_PRICE; при ошибке выводится сообщение через "Print" и возвращается false. Для существующих объектов корректируются свойства времени и цены, затем задаются цвет, ширина, стиль со значением "STYLE_SOLID" по умолчанию, размер шрифта 10, шрифт "Arial", отключаются фон, возможность выбора и выделение, после чего график перерисовывается через ChartRedraw, и функция возвращает true. Далее мы определяем функцию "UpdateFontSizes" для динамической настройки размеров текста: она получает масштаб графика через ChartGetInteger, вычисляет новый размер шрифта как 8 плюс 1.5, умноженное на масштаб, ограничивает его диапазоном от 8 до 18 с помощью MathMax и MathMin, а затем запускает перерисовку графика.

Мы реализуем функцию "BarWidth", которая рассчитывает ширину баров в пикселях на основе масштаба графика, возвращая 2 в степени масштаба через MathPow с приведением к целому типу. Функция "ShiftToX" преобразует индекс бара в x-координату на Canvas, умножая разницу относительно первого видимого бара на ширину бара. Аналогично функция "PriceToY" преобразует цену в y-координату: если диапазон равен нулю, она возвращает 0, иначе вычисляет пропорциональное положение цены от максимума к минимуму с использованием MathRound и масштабированием по высоте графика.

Наконец, мы разрабатываем функцию "DrawBoxOnCanvas", которая рисует на Canvas заполненный прямоугольник с границами. Она определяет минимальные и максимальные координаты с помощью "MathMin" и "MathMax", рассчитывает alpha-канал заливки по прозрачности, преобразует цвета в формат ARGB через ColorToARGB, заполняет область через "FillRectangle", получает более тёмный цвет границы через "DarkenColor" и рисует сглаженные линии внешней и внутренней границы со всех сторон с помощью "LineAA". "AA" означает anti-aliased, то есть сглаживание. Если вам интересно, почему мы выбрали именно этот вариант, он помогает сгладить неровные, ступенчатые края линий. Ниже приведено изображение для лучшего понимания.

ANTI-ALIASING (AA) COMPARISON

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

//+------------------------------------------------------------------+
//| Calculate indicator values                                       |
//+------------------------------------------------------------------+
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[]) {
   static int last_dir = 0;                      //--- Last direction
   static double last_sl = 0.0;                  //--- Last SL
   static double last_tp1 = 0.0;                 //--- Last TP1
   static double last_tp2 = 0.0;                 //--- Last TP2
   static double last_tp3 = 0.0;                 //--- Last TP3
   static datetime last_signal_time = 0;         //--- Last signal time

   if (prev_calculated == 0) {                   //--- Handle initial calc

      //--- Existing initializations

      ArrayInitialize(avg_candle_size, EMPTY_VALUE); //--- Init avg candle
      ArrayInitialize(buyArrowBuf, EMPTY_VALUE); //--- Init buy arrows
      ArrayInitialize(sellArrowBuf, EMPTY_VALUE); //--- Init sell arrows

      ArrayResize(all_boxes, 0);                 //--- Clear boxes
      ArrayResize(all_signals, 0);               //--- Clear signals

      last_dir = 0;                              //--- Reset direction
      last_sl = 0.0;                             //--- Reset SL
      last_tp1 = 0.0;                            //--- Reset TP1
      last_tp2 = 0.0;                            //--- Reset TP2
      last_tp3 = 0.0;                            //--- Reset TP3
      last_signal_time = 0;                      //--- Reset signal time
   }
}

В обработчике событий OnCalculate мы объявляем статические переменные для сохранения состояния между пересчётами: целочисленную переменную для направления последнего сигнала, значения double для последнего stop-loss и трёх уровней take-profit, а также datetime для временной метки последнего сигнала. Это обеспечивает непрерывность отображения управления рисками. Когда "prev_calculated" равен нулю, что означает первый расчёт или полный сброс, мы расширяем инициализацию: задаём буфер среднего размера свечи и буферы стрелок значением EMPTY_VALUE через ArrayInitialize, изменяем размер массивов сигнальных боксов и сигналов до нуля через ArrayResize, чтобы очистить предыдущие данные, и сбрасываем все статические переменные к начальным значениям, например к нулю или 0.0, для чистого старта. Чтобы запускать обновления Canvas только при необходимости, добавим флаг перерисовки следующим образом.

bool new_signal_redraw = false;               //--- New redraw flag

В цикле расчётов нужно добавить логику вычисления среднего размера свечи для работы с волатильностью следующим образом.

double sum_co = 0;                         //--- Init sum
int cnt_co = 0;                            //--- Init count
for (int k = 0; k < 50; k++) {             //--- Loop candles
   if (i - k < 0) break;                   //--- Skip invalid
   sum_co += MathAbs(close[i - k] - open[i - k]); //--- Accumulate
   cnt_co++;                               //--- Increment
}
if (cnt_co > 0) avg_candle_size[i] = sum_co / cnt_co; //--- Set average
else avg_candle_size[i] = MathAbs(close[i] - open[i]); //--- Set default

Внутри цикла инициализируем переменную double sum нулём и целочисленный count нулём, затем перебираем до 50 предыдущих баров, начиная с текущего индекса и двигаясь назад. Для каждого допустимого бара добавляем в сумму абсолютный размер тела, рассчитанный как разница между close и open через MathAbs, и увеличиваем счётчик; если индекс становится отрицательным, цикл прерывается. Если count больше нуля, средний размер свечи для текущего бара задаётся делением суммы на count; в противном случае используется только абсолютный размер тела текущего бара.

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

//--- BEFORE

// buyArrowBuf[i] = EMPTY_VALUE;              //--- Reset buy arrow
// sellArrowBuf[i] = EMPTY_VALUE;             //--- Reset sell arrow
// if (signal_bull_cross[i] == 1 && (!use_trend_filter || trend_is_bull[i] == 1)) { //--- Check buy condition
//    buyArrowBuf[i] = low[i] - _Point * base_offset; //--- Place buy arrow
// }
// if (signal_bear_cross[i] == 1 && (!use_trend_filter || trend_is_bear[i] == 1)) { //--- Check sell condition
//    sellArrowBuf[i] = high[i] + _Point * base_offset; //--- Place sell arrow
// }

//--- AFTER

double box_top = use_box_multiplier ? high[i] + avg_candle_size[i] * box_multiplier : high[i]; //--- Set top
double box_bottom = use_box_multiplier ? low[i] - avg_candle_size[i] * box_multiplier : low[i]; //--- Set bottom

if (signal_bull_cross[i] == 1 && (!use_trend_filter || trend_is_bull[i] == 1)) { //--- Check bull signal
   BoxInfo b;                              //--- Create box
   b.left_time = time[i];                  //--- Set left
   b.right_time = 0;                       //--- Set right
   b.top = box_top;                        //--- Set top
   b.bottom = box_bottom;                  //--- Set bottom
   b.dir = 1;                              //--- Set dir
   ArrayResize(all_boxes, ArraySize(all_boxes) + 1); //--- Resize boxes
   all_boxes[ArraySize(all_boxes) - 1] = b; //--- Add box
}

if (signal_bear_cross[i] == 1 && (!use_trend_filter || trend_is_bear[i] == 1)) { //--- Check bear signal
   BoxInfo b;                              //--- Create box
   b.left_time = time[i];                  //--- Set left
   b.right_time = 0;                       //--- Set right
   b.top = box_top;                        //--- Set top
   b.bottom = box_bottom;                  //--- Set bottom
   b.dir = -1;                             //--- Set dir
   ArrayResize(all_boxes, ArraySize(all_boxes) + 1); //--- Resize boxes
   all_boxes[ArraySize(all_boxes) - 1] = b; //--- Add box
}

bool buy_event = false;                    //--- Buy event flag
bool sell_event = false;                   //--- Sell event flag

for (int j = ArraySize(all_boxes) - 1; j >= 0; j--) { //--- Loop boxes
   if (all_boxes[j].right_time == 0) {     //--- Check active
      if (close[i] > all_boxes[j].top) {   //--- Check break up
         if (!show_only_matching || all_boxes[j].dir == 1) buy_event = true; //--- Set buy
         all_boxes[j].right_time = time[i]; //--- Close box
      }
      if (close[i] < all_boxes[j].bottom) { //--- Check break down
         if (!show_only_matching || all_boxes[j].dir == -1) sell_event = true; //--- Set sell
         all_boxes[j].right_time = time[i]; //--- Close box
      }
   }
}

while (ArraySize(all_boxes) > 500) {       //--- Limit boxes
   bool removed = false;                   //--- Removed flag
   for (int j = 0; j < ArraySize(all_boxes); j++) { //--- Loop to remove
      if (all_boxes[j].right_time != 0) {  //--- Check closed
         ArrayRemove(all_boxes, j, 1);     //--- Remove box
         removed = true;                   //--- Set removed
         break;                            //--- Exit loop
      }
   }
   if (!removed) break;                    //--- No more to remove
}

if (signal_type == Triangles) {            //--- Check triangles
   if (buy_event) {                        //--- Handle buy
      buyArrowBuf[i] = low[i] - _Point * base_offset; //--- Set arrow
   }
   if (sell_event) {                       //--- Handle sell
      sellArrowBuf[i] = high[i] + _Point * base_offset; //--- Set arrow
   }
} else {                                   //--- Handle labels
   if (buy_event) {                        //--- Handle buy
      SignalInfo s;                        //--- Create signal
      s.time = time[i];                    //--- Set time
      s.dir = 1;                           //--- Set dir
      ArrayResize(all_signals, ArraySize(all_signals) + 1); //--- Resize signals
      all_signals[ArraySize(all_signals) - 1] = s; //--- Add signal
      new_signal_redraw = (i == rates_total - 1); //--- Set redraw
   }
   if (sell_event) {                       //--- Handle sell
      SignalInfo s;                        //--- Create signal
      s.time = time[i];                    //--- Set time
      s.dir = -1;                          //--- Set dir
      ArrayResize(all_signals, ArraySize(all_signals) + 1); //--- Resize signals
      all_signals[ArraySize(all_signals) - 1] = s; //--- Add signal
      new_signal_redraw = (i == rates_total - 1); //--- Set redraw
   }
}

while (ArraySize(all_signals) > 500) {     //--- Limit signals
   ArrayRemove(all_signals, 0, 1);         //--- Remove oldest
}

Мы определяем верхнюю и нижнюю границы потенциальных сигнальных боксов, устанавливая их по максимуму и минимуму бара или, если включено продление, расширяя границы вверх и вниз на средний размер свечи, умноженный на множитель сигнального бокса, чтобы добавить буфер вокруг ценового диапазона. Когда обнаружено бычье пересечение и выполнено условие фильтра тренда, создаётся структура "BoxInfo", её поля заполняются текущим временем слева, нулём справа для обозначения активного состояния, рассчитанными верхней и нижней ценами и направлением 1 для бычьего сигнального бокса. Затем массив "all_boxes" расширяется через ArrayResize с размером ArraySize плюс один, и новая структура добавляется в конец. Аналогично при медвежьем пересечении и похожих условиях создаётся другой экземпляр "BoxInfo", поля задаются соответствующим образом с направлением -1 для медвежьего сигнального бокса, массив расширяется, и сигнальный бокс добавляется.

Мы инициализируем булевы флаги событий покупки и продажи значением false, затем перебираем массив "all_boxes" в обратном порядке, начиная с последнего индекса. Для каждого активного сигнального бокса, у которого правое время равно нулю, проверяем, превышает ли текущая цена закрытия верхнюю границу для пробоя вверх. Если направление совпадает или фильтр совпадения отключён, событие покупки устанавливается в true, а сигнальный бокс закрывается присвоением текущего времени правой границе. Аналогично, если закрытие опускается ниже нижней границы, событие продажи устанавливается при выполнении условий, и сигнальный бокс закрывается. Для управления размером массива, пока "all_boxes" содержит более 500 элементов, мы сканируем массив с начала, находим и удаляем первый закрытый сигнальный бокс через ArrayRemove, устанавливаем флаг успешного удаления и выходим из цикла, а если удалить больше нечего, прекращаем очистку.

Если тип сигналов равен "Triangles", при событии покупки мы размещаем стрелку покупки ниже минимума на величину offset, умноженную на _Point, а при событии продажи — выше максимума аналогичным образом. В противном случае, для типов с метками, при событии покупки создаётся структура "SignalInfo", её время задаётся текущим, направление — 1, затем массив "all_signals" расширяется, и структура добавляется в него; если это последний бар, устанавливается флаг перерисовки. Для событий продажи выполняются те же действия с направлением -1. Наконец, пока "all_signals" превышает 500 элементов, самый старый элемент удаляется из начала через функцию "ArrayRemove". Теперь всё готово к рисованию на Canvas. Начнём с градиентного тумана. Логику вынесем в отдельную функцию для модульности.

//+------------------------------------------------------------------+
//| Redraw canvas                                                    |
//+------------------------------------------------------------------+
void Redraw(int rates_total) {
   if (currentChartWidth <= 0 || currentChartHeight <= 0) return; //--- Handle invalid size

   double h[], l[], c[], acs[], th[];            //--- Declare arrays
   datetime t[];                                 //--- Declare time

   if (CopyHigh(_Symbol, _Period, 0, rates_total, h) != rates_total) return;  //--- Copy high
   if (CopyLow(_Symbol, _Period, 0, rates_total, l) != rates_total) return;   //--- Copy low
   if (CopyClose(_Symbol, _Period, 0, rates_total, c) != rates_total) return; //--- Copy close
   if (CopyTime(_Symbol, _Period, 0, rates_total, t) != rates_total) return;  //--- Copy time

   ArrayCopy(acs, avg_candle_size, 0, 0, rates_total); //--- Copy avg size
   ArrayCopy(th, trend_hist, 0, 0, rates_total); //--- Copy hist

   uint default_color = 0;                       //--- Default color
   obj_Canvas.Erase(default_color);              //--- Erase canvas

   current_font_size = (int)(10 + currentChartScale * 1.5);         //--- Compute font
   current_font_size = MathMax(10, MathMin(24, current_font_size)); //--- Clamp font

   if (show_fog) {                               //--- Check fog
      int total = visibleBarsCount;              //--- Set total
      int previousX = -1;                        //--- Prev x
      double previous_hl2 = 0.0;                 //--- Prev hl2
      double previous_offset = 0.0;              //--- Prev offset
      int previous_dir = 0;                      //--- Prev dir
      color previous_fog_color = clrNONE;        //--- Prev color

      for (int i = 0; i < total; i++) {                         //--- Loop visible
         int bar_index = firstVisibleBarIndex - i;              //--- Compute index
         if (bar_index < 0 || bar_index >= rates_total) continue; //--- Skip invalid

         int x = ShiftToX(bar_index);                           //--- Get x
         if (x >= currentChartWidth) continue;                  //--- Skip offscreen

         int buffer_index = rates_total - 1 - bar_index;        //--- Compute buffer
         double hl2 = (h[buffer_index] + l[buffer_index]) / 2.0; //--- Compute hl2
         double offset_val = acs[buffer_index] * offset_mult;   //--- Compute offset
         int dir = th[buffer_index] >= 0 ? -1 : 1;              //--- Set dir
         color fog_color = th[buffer_index] >= 0 ? col_up : col_dn; //--- Set color

         if (previousX != -1 && x > previousX) {                //--- Check previous
            double deltaX = x - previousX;                      //--- Compute delta
            int endColumn = MathMin(x, currentChartWidth - 1);  //--- Set end

            for (int column = previousX + 1; column <= endColumn; column++) { //--- Loop columns
               double t_val = (column - previousX) / deltaX;     //--- Compute t
               double interp_hl2 = previous_hl2 + t_val * (hl2 - previous_hl2); //--- Interp hl2
               double interp_offset = previous_offset + t_val * (offset_val - previous_offset); //--- Interp offset
               int interp_dir = previous_dir;                    //--- Interp dir
               color interp_fog_color = previous_fog_color;      //--- Interp color

               double full_offset = 6.0 * interp_offset;         //--- Full offset
               double edge_price = interp_hl2 + interp_dir * full_offset; //--- Edge price

               int slow_y = PriceToY(interp_hl2);                //--- Slow y
               int fast_y = PriceToY(edge_price);                //--- Fast y

               int upperY = MathMin(slow_y, fast_y);             //--- Upper y
               int lowerY = MathMax(slow_y, fast_y);             //--- Lower y
               upperY = MathMax(0, upperY);                      //--- Clamp upper
               lowerY = MathMin(currentChartHeight - 1, lowerY); //--- Clamp lower

               double height_pixels = MathAbs(slow_y - fast_y);  //--- Height
               if (height_pixels == 0.0) continue;               //--- Skip zero

               double total_inc = transp_inc * 6.0;              //--- Total inc

               for (int row = upperY; row <= lowerY; row++) {                 //--- Loop rows
                  double distanceFromSlow_pixels = MathAbs(row - slow_y);     //--- Distance
                  double gradientFraction = distanceFromSlow_pixels / height_pixels; //--- Fraction
                  double transp = base_transp + total_inc * gradientFraction; //--- Transp
                  if (transp > 100.0) transp = 100.0;                         //--- Clamp transp

                  uchar alpha = (uchar)(255 * (100 - transp) / 100.0);        //--- Alpha
                  uint argb = ColorToARGB(interp_fog_color, alpha);           //--- ARGB
                  obj_Canvas.PixelSet(column, row, argb);                     //--- Set pixel
               }
            }
         }

         previousX = x;                             //--- Update prev x
         previous_hl2 = hl2;                        //--- Update prev hl2
         previous_offset = offset_val;              //--- Update prev offset
         previous_dir = dir;                        //--- Update prev dir
         previous_fog_color = fog_color;            //--- Update prev color
      }
   }

   obj_Canvas.Update();                          //--- Update canvas
}

В функции "Redraw" мы сначала проверяем, что текущая ширина и высота графика положительные; если одно из значений равно нулю или меньше, выходим раньше, чтобы избежать некорректных операций. Затем объявляем локальные массивы для максимумов, минимумов, цен закрытия, средних размеров свечей, гистограмм тренда и времени. После этого заполняем их, копируя данные символа через CopyHigh, "CopyLow", "CopyClose" и CopyTime от начала до общего количества баров, выходя из функции, если любое копирование не возвращает ожидаемое количество. Данные из буферов среднего размера свечи и гистограммы тренда переносятся в локальные массивы через ArrayCopy для использования при отрисовке. Canvas очищается цветом 0 через метод "Erase", чтобы подготовиться к новой отрисовке. Текущий размер шрифта вычисляется как 10 плюс 1.5, умноженное на масштаб графика, затем ограничивается диапазоном от 10 до 24 через MathMax и MathMin для стабильного вывода текста.

Если отображение градиентного тумана включено, мы задаём общее количество итераций равным числу видимых баров и инициализируем предыдущие значения для x-позиции, цены hl2, смещения, направления и цвета. Затем перебираем каждый видимый бар слева направо: вычисляем индекс бара как первый видимый минус счётчик цикла, пропускаем бар, если он выходит за границы или соответствующий индекс rates недействителен, получаем x-координату через "ShiftToX" и пропускаем её, если она выходит за ширину графика. Далее рассчитываем индекс буфера как общее количество rates минус один минус индекс бара, вычисляем hl2 как середину между high и low, смещение как средний размер свечи, умноженный на множитель, направление как -1, если гистограмма тренда неотрицательна, или 1 в противном случае, а цвет тумана выбираем по знаку гистограммы с использованием заданных пользователем цветов роста или падения.

Если предыдущая x-координата существует и текущая x больше неё, мы определяем разницу в пикселях и задаём конечный столбец как минимум между текущей x и шириной графика минус один. Затем перебираем промежуточные столбцы: интерполируем коэффициент t как относительное положение внутри разницы, линейно интерполируем hl2 и смещение между предыдущим и текущим значениями, сохраняем предыдущее направление и цвет. Далее вычисляем полное смещение как 6, умноженное на интерполированное смещение, и получаем цену края, добавляя это смещение, масштабированное направлением, к интерполированному hl2. Интерполированный hl2 и край преобразуются в y-координаты через "PriceToY"; верхняя и нижняя y-границы задаются как минимум и максимум этих координат, при этом верхняя ограничивается минимумом 0, а нижняя — максимумом высоты графика минус один через MathMax и "MathMin". Высота в пикселях рассчитывается как абсолютная разница y; если она равна нулю, цикл по строкам пропускается. Общее приращение задаётся как шаг прозрачности, умноженный на 6.

Для каждой строки от верхней до нижней y вычисляется расстояние пикселя от медленной y-координаты, то есть позиции hl2, затем определяется доля градиента как отношение расстояния к высоте. Прозрачность рассчитывается как базовое значение плюс общее приращение, умноженное на эту долю, и ограничивается максимумом 100. Затем она преобразуется в alpha как uchar от 255, умноженного на (100 минус прозрачность) и делённого на 100. Цвет ARGB получается через ColorToARGB с интерполированным цветом тумана и alpha, после чего пиксель в заданных столбце и строке устанавливается функцией PixelSet. После обработки каждого бара предыдущие значения обновляются текущими. Наконец, отображение Canvas обновляется через "Update", чтобы применить все нарисованные элементы. Чтобы наглядно видеть прогресс, мы вызовем эту функцию в конце после основных обновлений в обработчике расчёта, чтобы перерисовать график с учётом новой информации.

bool hasChartChanged = false;                                         //--- Change flag
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 bar
int newVisibleBars = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);     //--- Get new visible
double newMinPrice = ChartGetDouble(0, CHART_PRICE_MIN, 0);           //--- Get new min
double newMaxPrice = ChartGetDouble(0, CHART_PRICE_MAX, 0);           //--- Get new max

if (newChartWidth != currentChartWidth || newChartHeight != currentChartHeight) { //--- Check size change
   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) { //--- Check other changes
   currentChartScale = newChartScale;          //--- Update scale
   firstVisibleBarIndex = newFirstVisibleBar;  //--- Update first bar
   visibleBarsCount = newVisibleBars;          //--- Update visible
   minPrice = newMinPrice;                     //--- Update min
   maxPrice = newMaxPrice;                     //--- Update max
   hasChartChanged = true;                     //--- Set changed
}

datetime currentTime = TimeCurrent();         //--- Get current time
if (hasChartChanged || rates_total > prev_calculated || new_signal_redraw) { //--- Check redraw
   Redraw(rates_total);                        //--- Call redraw
   lastRedrawTime = currentTime;               //--- Update time
}

ChartRedraw(0);                               //--- Redraw chart

В обработчике событий OnCalculate мы инициализируем булев флаг для отслеживания изменений графика, затем получаем обновлённые свойства графика: новую ширину и высоту через ChartGetInteger с CHART_WIDTH_IN_PIXELS и "CHART_HEIGHT_IN_PIXELS", новый масштаб через CHART_SCALE, первый видимый бар через "CHART_FIRST_VISIBLE_BAR", количество видимых баров через "CHART_VISIBLE_BARS", а также минимальную и максимальную цены через ChartGetDouble с "CHART_PRICE_MIN" и CHART_PRICE_MAX. Если новая ширина или высота отличается от текущих значений, объект Canvas изменяет размер через метод "Resize" с передачей новых размеров, глобальные переменные ширины и высоты обновляются, а флаг изменения устанавливается в true.

Аналогично, если изменились масштаб, первый видимый бар, количество видимых баров, минимальная или максимальная цена, мы обновляем соответствующие глобальные переменные и устанавливаем флаг в true. Текущее серверное время получаем через TimeCurrent, затем проверяем, равен ли флаг true, появились ли новые бары при сравнении rates_total с prev_calculated или требует ли новый сигнал перерисовки. Если выполняется любое из условий, вызываем функцию "Redraw" с общим количеством rates и обновляем временную метку последней перерисовки. Наконец, принудительно обновляем график через ChartRedraw, чтобы все изменения стали видимыми. После загрузки индикатора на график видим следующий результат.

FOG REDRAW

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

for (int j = 0; j < ArraySize(all_boxes); j++) { //--- Loop boxes
   int left_bar = iBarShift(_Symbol, _Period, all_boxes[j].left_time); //--- Get left bar
   int x_left = ShiftToX(left_bar);              //--- Get left x
   int x_right;                                  //--- Declare right x

   if (all_boxes[j].right_time == 0) {           //--- Check active
      x_right = currentChartWidth - 1;           //--- Set to end
   } else {                                      //--- Handle closed
      int right_bar = iBarShift(_Symbol, _Period, all_boxes[j].right_time); //--- Get right bar
      x_right = ShiftToX(right_bar);             //--- Get right x
   }

   int y_top = PriceToY(all_boxes[j].top);       //--- Get top y
   int y_bottom = PriceToY(all_boxes[j].bottom); //--- Get bottom y

   color fill_col = all_boxes[j].dir == 1 ? box_bull_fill : box_bear_fill;       //--- Set fill

   DrawBoxOnCanvas(x_left, y_top, x_right, y_bottom, fill_col, box_fill_transp); //--- Draw box
}

Для отрисовки сигнальных боксов мы перебираем каждый элемент массива "all_boxes", используя ArraySize для определения общего количества, и обрабатываем элементы от индекса 0 до конца. Для каждого сигнального бокса получаем индекс левого бара через iBarShift, передавая символ, период и левое время сигнального бокса, чтобы преобразовать временную метку в позицию бара. Затем вычисляем левую x-координату на Canvas через "ShiftToX".

Мы объявляем переменную для правой x-координаты. Если правое время сигнального бокса равно нулю, то он всё ещё активен, поэтому правой x-координате присваивается ширина графика минус один, чтобы расширить сигнальный бокс до края. В противном случае, для закрытых сигнальных боксов, правый индекс бара аналогично получается через "iBarShift", а его x-позиция рассчитывается через "ShiftToX". Верхняя и нижняя цены сигнального бокса преобразуются в y-координаты через "PriceToY". Цвет заливки выбирается по направлению: бычья заливка при направлении 1 или медвежья при -1. Затем вызывается "DrawBoxOnCanvas" с рассчитанными x- и y-позициями, выбранным цветом и прозрачностью, чтобы визуально отрисовать сигнальный бокс на Canvas. Получаем следующий результат.

CANVAS BOXES

Видно, что сигнальные боксы отрисованы корректно. Теперь остаётся самая важная часть: нужно условно рисовать пузырьки с надписями BUY/SELL, когда выбран соответствующий режим. Для этого используется следующая логика.

if (signal_type == Labels_Buy_Sell) {                             //--- Check labels
   for (int j = 0; j < ArraySize(all_signals); j++) {             //--- Loop signals
      int bar = iBarShift(_Symbol, _Period, all_signals[j].time); //--- Get bar
      if (bar > firstVisibleBarIndex || bar < firstVisibleBarIndex - visibleBarsCount) continue; //--- Skip off view

      int x = ShiftToX(bar);                                      //--- Get x
      int buffer_index = rates_total - 1 - bar;                   //--- Get buffer
      double price = (all_signals[j].dir == 1) ? l[buffer_index] : h[buffer_index]; //--- Set price
      int y = PriceToY(price);                                    //--- Get y

      string text = (all_signals[j].dir == 1) ? "BUY" : "SELL";   //--- Set text
      color bg_col = (all_signals[j].dir == 1) ? signal_buy_col : signal_sell_col; //--- Set bg
      color border_col = DarkenColor(bg_col, 0.5);                //--- Set border
      color text_col = clrWhite;                                  //--- Set text color

      obj_Canvas.FontSet("Arial Bold", (uint)current_font_size, FW_BOLD); //--- Set font
      int text_width = obj_Canvas.TextWidth(text);                //--- Get width
      int text_height = obj_Canvas.TextHeight(text);              //--- Get height

      int padding_width = 6 + current_font_size / 3;              //--- Compute width pad
      int padding_height = 4 + current_font_size / 4;             //--- Compute height pad

      int rect_width = text_width + padding_width;                //--- Set rect width
      int rect_height = text_height + padding_height;             //--- Set rect height
      int label_offset = base_offset + currentChartScale;         //--- Set offset
      int tri_base = 6 + currentChartScale * 2;                   //--- Set tri base
      tri_base = (tri_base / 2) * 2;                              //--- Ensure even
      int tri_height = (int)(tri_base * 0.5);                     //--- Set tri height

      int x_rect = x - rect_width / 2;                            //--- Set rect x
      int y_rect = (all_signals[j].dir == 1) ? y + label_offset : y - rect_height - label_offset; //--- Set rect y

      uchar alpha = (uchar)(255 * (100 - 20) / 100);              //--- Set alpha
      uint argb_fill = ColorToARGB(bg_col, alpha);                //--- Get fill
      obj_Canvas.FillRectangle(x_rect, y_rect, x_rect + rect_width, y_rect + rect_height, argb_fill); //--- Fill rect

      uint argb_border = ColorToARGB(border_col, 255);            //--- Get border
      obj_Canvas.LineAA(x_rect, y_rect, x_rect + rect_width, y_rect, argb_border); //--- Draw top
      obj_Canvas.LineAA(x_rect + rect_width, y_rect, x_rect + rect_width, y_rect + rect_height, argb_border); //--- Draw right
      obj_Canvas.LineAA(x_rect + rect_width, y_rect + rect_height, x_rect, y_rect + rect_height, argb_border); //--- Draw bottom
      obj_Canvas.LineAA(x_rect, y_rect + rect_height, x_rect, y_rect, argb_border); //--- Draw left

      int x_center = x_rect + rect_width / 2;     //--- Set center
      if (all_signals[j].dir == 1) {              //--- Handle buy
         int tri_left = x_center - tri_base / 2;  //--- Set left
         int tri_right = x_center + tri_base / 2; //--- Set right
         int tri_tip_y = y_rect - tri_height;     //--- Set tip
         obj_Canvas.FillTriangle(tri_left, y_rect, tri_right, y_rect, x_center, tri_tip_y, argb_fill); //--- Fill tri
         obj_Canvas.LineAA(tri_left, y_rect, x_center, tri_tip_y, argb_border); //--- Draw left slant
         obj_Canvas.LineAA(tri_right, y_rect, x_center, tri_tip_y, argb_border); //--- Draw right slant
      } else {                                    //--- Handle sell
         int tri_bottom_y = y_rect + rect_height; //--- Set bottom
         int tri_left = x_center - tri_base / 2;  //--- Set left
         int tri_right = x_center + tri_base / 2; //--- Set right
         int tri_tip_y = tri_bottom_y + tri_height; //--- Set tip
         obj_Canvas.FillTriangle(tri_left, tri_bottom_y, tri_right, tri_bottom_y, x_center, tri_tip_y, argb_fill); //--- Fill tri
         obj_Canvas.LineAA(tri_left, tri_bottom_y, x_center, tri_tip_y, argb_border); //--- Draw left slant
         obj_Canvas.LineAA(tri_right, tri_bottom_y, x_center, tri_tip_y, argb_border); //--- Draw right slant
      }

      int text_x = x_rect + rect_width / 2;   //--- Set text x
      int text_y = y_rect + rect_height / 2;  //--- Set text y
      uint argb_text = ColorToARGB(text_col, 255); //--- Get text ARGB
      obj_Canvas.TextOut(text_x, text_y, text, argb_text, TA_CENTER | TA_VCENTER); //--- Draw text
   }
}

Сначала проверяем, равен ли тип сигналов "Labels_Buy_Sell", чтобы отрисовывать пузырьки с надписями BUY/SELL. Затем перебираем каждый элемент массива "all_signals" с использованием ArraySize для определения количества. Для каждого сигнала его временная метка преобразуется в индекс бара через iBarShift с передачей символа и периода. Если бар находится вне видимого диапазона, итерация пропускается по сравнению с первым видимым индексом и количеством видимых баров. x-координата вычисляется через "ShiftToX", индекс буфера получается как общее количество rates минус один минус бар, опорная цена задаётся как low для сигналов покупки (направление 1) или high для продаж, после чего эта цена преобразуется в y через "PriceToY". Текст метки определяется как "BUY" или "SELL" в зависимости от направления, цвет фона выбирается из пользовательских входных параметров, цвет границы получается затемнением фона через "DarkenColor" с коэффициентом 0.5, а цвет текста задаётся белым.

Мы настраиваем шрифт Canvas через "FontSet" на "Arial Bold" с текущим размером шрифта и флагом bold, измеряем размеры текста через "TextWidth" и "TextHeight", рассчитываем отступы по ширине и высоте на основе размера шрифта и получаем размеры прямоугольника, добавляя отступы к размерам текста. Смещение метки задаётся как сумма базового смещения и масштаба графика. Основание треугольника рассчитывается как 6 плюс удвоенный масштаб, затем делается чётным через целочисленное деление и умножение, а высота треугольника задаётся как половина основания с приведением к int. Прямоугольник центрируется по x путём вычитания половины его ширины из x-координаты сигнала, а y-позиция смещается выше или ниже в зависимости от направления. Alpha заливки вычисляется как uchar от 255, умноженного на (100 минус 20) и делённого на 100, что соответствует непрозрачности 80%. Цвет заливки ARGB получается через ColorToARGB, после чего область прямоугольника заполняется через FillRectangle. Далее получаем ARGB для границы и рисуем сглаженные линии LineAA по верхней, правой, нижней и левой сторонам прямоугольника.

Для сигналов покупки мы рассчитываем точки треугольника со смещением влево и вправо от центра на половину основания ниже прямоугольника, а вершину — выше на высоту. Треугольник заполняется через FillTriangle с ARGB-заливкой, а наклонные стороны рисуются через "LineAA". Теперь это уже не новый для вас подход, и вы понимаете, почему мы выбрали его вместо стандартного варианта. Для сигналов продажи точки треугольника располагаются так, что основание находится у нижней части прямоугольника, левая и правая точки задаются аналогично, а вершина смещается вниз на высоту. Затем треугольник заполняется, и его наклонные стороны рисуются. В конце x и y текста центрируются внутри прямоугольника, цвет текста преобразуется в ARGB, а текст выводится через TextOut с флагами горизонтального и вертикального центрирования. После компиляции получаем следующий результат.

BUBBLED SIGNALS

Видно, что теперь сигналы отображаются в пузырьках с надписями BUY/SELL. Далее нужно рассчитать уровни stop-loss и take-profit.

if (showTPSL && (buy_event || sell_event) && i == rates_total - 1) { //--- Check TP/SL
   int lastSignal = buy_event ? 1 : -1;     //--- Set signal
   double lastClose = close[i];             //--- Set close
   double sum_range = 0;                    //--- Init sum
   int cnt_range = 0;                       //--- Init count
   for (int k = 0; k < tp_sl_length; k++) { //--- Loop range
      if (i - k < 0) break;                 //--- Skip invalid
      sum_range += high[i - k] - low[i - k]; //--- Accumulate
      cnt_range++;                          //--- Increment
   }
   double avgRange = (cnt_range > 0) ? sum_range / cnt_range : 0; //--- Compute avg

   double sl_val = (lastSignal == 1) ? 
                   (tpSlMode == Candle_Multiplier ? lastClose - avgRange * slMultiplier : lastClose * (1 - slPercent / 100)) : 
                   (tpSlMode == Candle_Multiplier ? lastClose + avgRange * slMultiplier : lastClose * (1 + slPercent / 100)); //--- Compute SL

   double tp1_val = (lastSignal == 1) ? 
                    (tpSlMode == Candle_Multiplier ? lastClose + avgRange * tp1Multiplier : lastClose * (1 + tp1Percent / 100)) : 
                    (tpSlMode == Candle_Multiplier ? lastClose - avgRange * tp1Multiplier : lastClose * (1 - tp1Percent / 100)); //--- Compute TP1

   double tp2_val = (lastSignal == 1) ? 
                    (tpSlMode == Candle_Multiplier ? lastClose + avgRange * tp2Multiplier : lastClose * (1 + tp2Percent / 100)) : 
                    (tpSlMode == Candle_Multiplier ? lastClose - avgRange * tp2Multiplier : lastClose * (1 - tp2Percent / 100)); //--- Compute TP2

   double tp3_val = (lastSignal == 1) ? 
                    (tpSlMode == Candle_Multiplier ? lastClose + avgRange * tp3Multiplier : lastClose * (1 + tp3Percent / 100)) : 
                    (tpSlMode == Candle_Multiplier ? lastClose - avgRange * tp3Multiplier : lastClose * (1 - tp3Percent / 100)); //--- Compute TP3

   last_dir = lastSignal;                  //--- Update dir
   last_sl = sl_val;                       //--- Update SL
   last_tp1 = tp1_val;                     //--- Update TP1
   last_tp2 = tp2_val;                     //--- Update TP2
   last_tp3 = tp3_val;                     //--- Update TP3
   last_signal_time = time[i];             //--- Update time
   new_signal_redraw = true;               //--- Set redraw
}

if (i == rates_total - 1) {                //--- Check last bar
   static bool last_buy = false;           //--- Last buy
   static bool last_sell = false;          //--- Last sell

   if (buy_event && !last_buy) {           //--- Handle new buy
      Alert("WaveTrend BUY " + _Symbol + " @" + DoubleToString(close[i], _Digits)); //--- Alert buy
      last_buy = true;                     //--- Set last buy
      new_signal_redraw = true;            //--- Set redraw
   } else last_buy = buy_event;            //--- Update last buy

   if (sell_event && !last_sell) {         //--- Handle new sell
      Alert("WaveTrend SELL " + _Symbol + " @" + DoubleToString(close[i], _Digits)); //--- Alert sell
      last_sell = true;                    //--- Set last sell
      new_signal_redraw = true;            //--- Set redraw
   } else last_sell = sell_event;          //--- Update last sell
}

Здесь мы проверяем, включено ли отображение Take Profit и Stop Loss, произошло ли событие покупки или продажи и обрабатывается ли последний бар в rates. Если да, определяем направление сигнала как 1 для покупки или -1 для продажи, фиксируем текущую цену закрытия, затем инициализируем сумму и счётчик нулём и перебираем прошлые бары до tp_sl_length, пропуская недопустимые индексы, накапливая диапазон high-low в сумме и увеличивая счётчик.

Средний диапазон вычисляется делением суммы на счётчик, если он больше нуля; иначе используется значение по умолчанию 0. В зависимости от направления сигнала и выбранного "tpSlMode" из перечисления "Candle_Multiplier" или "Percentage" рассчитывается значение stop-loss: для покупок в режиме множителя это close минус средний диапазон, умноженный на slMultiplier, а в процентном режиме — close, умноженный на (1 минус slPercent, делённый на 100). Для продаж применяется обратная логика с добавлением или умножением на (1 плюс slPercent, делённый на 100). Аналогичные условные расчёты выполняются для трёх значений take-profit с использованием соответствующих множителей или процентов и с учётом направления через сложение или вычитание.

Мы обновляем статическое последнее направление, stop-loss, уровни take-profit и время сигнала этими значениями, а также устанавливаем флаг перерисовки в true для обновления визуальных элементов. Кроме того, если это последний бар, мы используем статические булевы переменные для отслеживания предыдущих состояний покупки и продажи. Для нового события покупки, которое ранее не было отмечено, вызывается Alert с сообщением, объединяющим "WaveTrend BUY", символ, "@" и цену закрытия, отформатированную по количеству знаков символа через DoubleToString. Затем last buy устанавливается в true и включается перерисовка; в противном случае обновляется флаг last buy. События продажи обрабатываются аналогично с оповещением "WaveTrend SELL", обновлением флага last sell и установкой перерисовки при новом сигнале. Так мы получим оповещения при появлении сигнала, как показано ниже.

SIGNAL ALERT

Для визуализации уровней на графике используется следующая логика.

if (showTPSL) {                               //--- Check TP/SL
   if (last_dir == 0) {                       //--- Handle no dir
      ObjectDelete(0, slLine);                //--- Delete SL
      ObjectDelete(0, tp1Line);               //--- Delete TP1
      ObjectDelete(0, tp2Line);               //--- Delete TP2
      ObjectDelete(0, tp3Line);               //--- Delete TP3
      for (int i = 0; i < ArraySize(tpSlTableObjects); i++) { //--- Loop table
         ObjectDelete(0, tpSlTableObjects[i]); //--- Delete object
      }
   } else {                                   //--- Handle dir
      datetime extension_time = last_signal_time; //--- Set time
      color tp_color = (last_dir == 1) ? col_up : col_dn; //--- Set TP color
      color sl_color = (last_dir == 1) ? col_dn : col_up; //--- Set SL color

      drawRightPrice(slLine, extension_time, last_sl, sl_color, STYLE_SOLID, 2); //--- Draw SL
      drawRightPrice(tp1Line, extension_time, last_tp1, tp_color, STYLE_SOLID, 1); //--- Draw TP1
      drawRightPrice(tp2Line, extension_time, last_tp2, tp_color, STYLE_SOLID, 1); //--- Draw TP2
      drawRightPrice(tp3Line, extension_time, last_tp3, tp_color, STYLE_SOLID, 1); //--- Draw TP3

      currentChartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Update width
      drawRectangleLabel(tpSlTableObjects[0], currentChartWidth - 150, 20, 120, 120, clrGray, BORDER_FLAT, true); //--- Draw frame
      drawLabel(tpSlTableObjects[1], currentChartWidth - 140, 30, "Level", clrBlack); //--- Draw level
      drawLabel(tpSlTableObjects[2], currentChartWidth - 80, 30, "Price", clrBlack); //--- Draw price
      drawLabel(tpSlTableObjects[3], currentChartWidth - 140, 50, "TP1", tp_color); //--- Draw TP1
      drawLabel(tpSlTableObjects[4], currentChartWidth - 80, 50, DoubleToString(last_tp1, _Digits), tp_color); //--- Draw TP1 price
      drawLabel(tpSlTableObjects[5], currentChartWidth - 140, 70, "TP2", tp_color); //--- Draw TP2
      drawLabel(tpSlTableObjects[6], currentChartWidth - 80, 70, DoubleToString(last_tp2, _Digits), tp_color); //--- Draw TP2 price
      drawLabel(tpSlTableObjects[7], currentChartWidth - 140, 90, "TP3", tp_color); //--- Draw TP3
      drawLabel(tpSlTableObjects[8], currentChartWidth - 80, 90, DoubleToString(last_tp3, _Digits), tp_color); //--- Draw TP3 price
      drawLabel(tpSlTableObjects[9], currentChartWidth - 140, 110, "SL", sl_color); //--- Draw SL
      drawLabel(tpSlTableObjects[10], currentChartWidth - 80, 110, DoubleToString(last_sl, _Digits), sl_color); //--- Draw SL price
   }
}

Мы проверяем, включено ли отображение Take Profit и Stop Loss. Если да, а последнее направление равно нулю, то есть активного сигнала нет, удаляем все связанные объекты, вызывая ObjectDelete для линий stop-loss и take-profit, затем перебираем массив объектов таблицы через ArraySize и удаляем каждый объект отдельно. В противном случае, при активном направлении, задаём время продления как временную метку последнего сигнала, выбираем цвет take-profit как цвет роста для покупок или цвет падения для продаж, а цвет stop-loss — противоположным. Затем вызываем "drawRightPrice" для отображения линии stop-loss со сплошным стилем и шириной 2, а также каждой линии take-profit со сплошным стилем и шириной 1, все на времени продления и с соответствующими ценами и цветами.

Мы обновляем текущую ширину графика через ChartGetInteger с CHART_WIDTH_IN_PIXELS, рисуем рамку таблицы через "drawRectangleLabel" с позиционированием в 150 пикселях от правого края при y = 20, размером 120 на 120, серым цветом, плоской границей и включённым фоном. Затем размещаем метки через "drawLabel" для заголовков "Level" и "Price" чёрным цветом, а также для каждого Take Profit и Stop Loss с метками вроде "TP1" и отформатированными ценами через DoubleToString по количеству знаков символа, окрашивая их соответствующим образом и располагая с заданными смещениями от правого края. Вы можете настроить это по своим предпочтениям. После компиляции получаем отрисованные уровни.

COMPLETE CANVAS DRAWING WITH SL/TP LEVELS

Видно, что при разрешённом отображении уровни полностью отрисованы на Canvas. Теперь остаётся обработать изменения графика, чтобы индикатор плавно обновлялся при действиях пользователя. Для обнаружения изменений графика мы будем использовать обработчик событий OnChartEvent.

//+------------------------------------------------------------------+
//| Handle chart event                                               |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if (id == CHARTEVENT_CHART_CHANGE) {          //--- Check change
      Redraw(Bars(_Symbol, _Period));            //--- Redraw
      UpdateFontSizes();                         //--- Update fonts
   }
}

В обработчике событий OnChartEvent мы обрабатываем входящие параметры, включая идентификатор события, значение long, значение double и строку, чтобы обнаруживать действия пользователя с графиком и реагировать на них. Если ID равен CHARTEVENT_CHART_CHANGE, что означает изменения вроде масштабирования, прокрутки или изменения размера, мы запускаем обновление, вызывая функцию "Redraw" с общим количеством баров, полученным через "Bars" по текущему символу и таймфрейму, а также выполняем "UpdateFontSizes" для перенастройки текстовых элементов под оптимальную читаемость. Кроме того, нужно удалить объекты графика при удалении индикатора с графика.

//+------------------------------------------------------------------+
//| Deinitialize indicator                                           |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   obj_Canvas.Destroy();                         //--- Destroy canvas

   ArrayResize(all_boxes, 0);                    //--- Clear boxes
   ArrayResize(all_signals, 0);                  //--- Clear signals

   ObjectDelete(0, slLine);                      //--- Delete SL line
   ObjectDelete(0, tp1Line);                     //--- Delete TP1 line
   ObjectDelete(0, tp2Line);                     //--- Delete TP2 line
   ObjectDelete(0, tp3Line);                     //--- Delete TP3 line

   for (int i = 0; i < ArraySize(tpSlTableObjects); i++) { //--- Loop through table objects
      ObjectDelete(0, tpSlTableObjects[i]);      //--- Delete object
   }

   ChartRedraw(0);                               //--- Redraw chart
}

В обработчике событий OnDeinit при удалении индикатора мы очищаем ресурсы: сначала уничтожаем объект Canvas через его метод Destroy, чтобы освободить графические элементы. Затем уменьшаем размер массивов "all_boxes" и "all_signals" до нуля с помощью ArrayResize, очищая сохранённые данные и освобождая память. Линии stop-loss и take-profit удаляются вызовом ObjectDelete для каждого именованного объекта линии. Затем мы перебираем массив объектов таблицы по его размеру из ArraySize и удаляем каждый объект через "ObjectDelete", чтобы убрать отображение управления рисками. В конце вызываем ChartRedraw, чтобы обновить график и отразить все удаления. После этого наши цели достигнуты. Остаётся выполнить бэктестинг программы, и это рассматривается в следующем разделе.


Бэктестинг

Мы провели тестирование, и ниже приведена итоговая визуализация в формате Graphics Interchange Format (GIF).

BACKTEST GIF


Заключение

В заключение мы улучшили индикатор WaveTrend Crossover в MQL5, добавив отрисовку с использованием Canvas для градиентных туманных наложений, сигнальные боксы для обнаружения пробоев, метки сигналов в виде пузырьков с надписями BUY/SELL или простых треугольников, а также встроенное управление рисками через динамические уровни Take Profit и Stop Loss. Это обновление включает расширенные визуальные элементы, такие как градиентный туман для рыночного контекста, а также параметры фильтрации по тренду, расширения сигнальных боксов и расчётов через свечные множители или проценты с отображением в виде линий и таблиц. Такая конфигурация предоставляет более насыщенные визуальные инструменты для анализа изменений импульса, тренда и риска. С этим улучшенным индикатором WaveTrend Crossover вы сможете использовать визуальные функции и инструменты управления рисками для более точного анализа торговых возможностей и дальнейшей оптимизации своей торговли. Удачной торговли!

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

Прикрепленные файлы |
MQL5 Bootstrap (I): Набор переиспользуемых функций для работы с позициями и ордерами MQL5 Bootstrap (I): Набор переиспользуемых функций для работы с позициями и ордерами
В статье представлен компактный вспомогательный слой MQL5 для типовых торговых операций. Он включает функции проверки наличия позиций, счётчики позиций, вспомогательные функции для группового закрытия позиций, а также функции получения наиболее недавно открытой или самой старой позиции по символу, magic-номеру или типу. Интеграция демонстрируется на простом советнике с пересечением SMA. В результате советники становятся чище, уменьшается количество различий в реализации между проектами и упрощается сопровождение кода.
Моделирование рынка: Position View (VIII) Моделирование рынка: Position View (VIII)
В предыдущей статье мы рассмотрели, как можно реализовать индикатор позиции для закрытия открытой позиции непосредственно с графика, взаимодействуя с доступным на нем объектом. После завершения и проверки работы первого механизма мы приступили к внесению изменений, чтобы обеспечить возможность удаления линий тейк-профита и стоп-лосса для открытой позиции. Однако, поскольку необходимые изменения требовали подробных объяснений, в той же статье я показал только те изменения, которые необходимо было внести в советник, но всё ещё требовалось показать изменения, которые необходимо было внести в индикатор позиции.
Алгоритм бактериальной эволюции — Bacterial Evolutionary Algorithm (BEA) Алгоритм бактериальной эволюции — Bacterial Evolutionary Algorithm (BEA)
Статья посвящена бактериальному эволюционному алгоритму Нава — Фурухаси (BEA) и его двум ключевым операторам: посегментной бактериальной мутации для локальной оптимизации и горизонтальному переносу генов для обмена удачными фрагментами решений. Мы реализуем BEA в каркасе C_AO как конечный автомат под фиксированный бюджет оценок, тестируем на Hilly, Forest и Megacity и поясняем терминологическую развилку с моделью Нумаоки.
Оптимизация трендовой торговли: Торговля по направлению тренда и с учётом его силы Оптимизация трендовой торговли: Торговля по направлению тренда и с учётом его силы
Это специализированный трендовый советник, который выполняет краткосрочный и долгосрочный анализ, принимает торговые решения и исполняет сделки на основе общего тренда и его силы. В статье подробно рассматривается советник, специально разработанный для терпеливых и дисциплинированных трейдеров, способных последовательно придерживаться выбранного торгового направления, открывать сделки и удерживать позиции только тогда, когда рынок движется уверенно и по тренду, не меняя часто своего направленного взгляда на рынок, особенно против тренда, пока не будут достигнуты цели по тейк-профиту.