Создание пользовательских индикаторов в MQL5 (Часть 5): Доработка WaveTrend Crossover с помощью Canvas — градиентный туман, сигнальные пузырьки и управление рисками
Введение
В нашей предыдущей статье (Часть 4) мы разработали индикатор Smart WaveTrend Crossover на MetaQuotes Language 5 (MQL5), использующий два осциллятора: один для сигналов, а другой для фильтрации тренда. Он формировал сигналы на покупку и продажу на основе пересечений с возможностью подтверждения по тренду. В пятой части мы улучшим индикатор WaveTrend Crossover, добавив рисование на основе Canvas для градиентных туманных наложений, сигнальные боксы для обнаружения пробоев, метки сигналов в виде пузырьков с надписями BUY/SELL или простых треугольников, а также встроенное управление рисками через динамические уровни Take Profit и Stop Loss. Эта версия добавляет расширенные визуальные элементы, такие как градиентный туман для рыночного контекста, а также параметры фильтрации по тренду, расширения сигнальных боксов и расчётов через свечные множители или проценты с отображением в виде линий и таблиц. Мы рассмотрим следующие темы:
- Понимание улучшенной структуры WaveTrend Crossover на основе Canvas с визуальными функциями и управлением рисками
- Реализация в MQL5
- Бэктестинг
- Заключение
В итоге у вас будет рабочий индикатор MQL5 для улучшенных пересечений WaveTrend с визуальными элементами и инструментами управления рисками, готовый к дальнейшей настройке. Давайте приступим!
Понимание улучшенной структуры WaveTrend Crossover на основе Canvas с визуальными функциями и управлением рисками
Улучшенная структура WaveTrend Crossover на основе Canvas развивает базовый осциллятор моментума, добавляя визуальные наложения и инструменты управления рисками, чтобы обеспечить более наглядный и функциональный торговый интерфейс. Она сохраняет две конфигурации WaveTrend: более чувствительную для обнаружения пересечений, которые указывают на потенциальные входы, и более медленную для фильтрации тренда. При этом добавляется обнаружение пробоев через сигнальные боксы, которые формируются вокруг точек пересечения и закрываются, когда цена пробивает их границы, указывая на подтверждённые изменения импульса. Градиентные туманные наложения отображаются поверх графика и визуально показывают силу тренда через плавно меняющуюся прозрачность, помогая быстро оценивать рыночный контекст. Дополнительно используются метки сигналов в виде пузырьков с надписями BUY/SELL или простых треугольников.
В бычьей конфигурации пересечение вверх в сигнальном осцилляторе, при необходимости подтверждённое восходящим трендом по более медленному осциллятору, создаёт сигнальный бокс вокруг диапазона бара. При пробое верхней границы сигнального бокса срабатывает сигнал покупки, если он соответствует направлению сигнального бокса, а визуальные элементы подчёркивают эту возможность. И наоборот, в медвежьей конфигурации пересечение вниз формирует сигнальный бокс, а пробой вниз генерирует сигнал продажи при выполнении соответствующих условий, позволяя реагировать на развороты или продолжения движения с меньшим уровнем шума. Управление рисками встроено через расчёт уровней Take Profit и Stop Loss на основе средних размеров свечей или процентных изменений; уровни отображаются динамически и помогают с выбором размера позиции и планированием выхода. Таким образом, мы можем оценивать долю успешных срабатываний сигналов.
Мы будем использовать библиотеку MQL5 Canvas для отрисовки градиентного тумана с интерполяцией значений между барами для более плавной визуализации тренда, отслеживать сигнальные боксы для обнаружения пробоев и их закрытия с опциональным продлением через множители средней свечи, предлагать гибкие типы сигналов, например пузырьки с надписями BUY/SELL для лучшей читаемости, и рассчитывать уровни риска с пользовательскими режимами для Take Profit и Stop Loss, обеспечивая при этом эффективную перерисовку при изменениях графика. Ниже кратко показано визуальное представление наших целей.

Реализация в 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, подтверждая успешную настройку. После инициализации получаем следующий результат.

На изображении видно, что 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, то есть сглаживание. Если вам интересно, почему мы выбрали именно этот вариант, он помогает сгладить неровные, ступенчатые края линий. Ниже приведено изображение для лучшего понимания.

По изображению видно, почему был выбран подход со сглаживанием: линии получаются плавными. Теперь можно перейти к использованию этих функций в расчётах и визуализации индикатора. Сначала мы хотим, чтобы табличные торговые уровни оставались на графике, поэтому их нужно сделать статическими между вызовами, если они явно не меняются при появлении новых сигналов.
//+------------------------------------------------------------------+ //| 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, чтобы все изменения стали видимыми. После загрузки индикатора на график видим следующий результат.

На изображении видно, что градиентный туман уже подготовлен на основе диапазона свечей. Теперь нужно отрисовать сигнальные боксы. Эту логику мы безусловно разместим в той же функции перерисовки.
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. Получаем следующий результат.

Видно, что сигнальные боксы отрисованы корректно. Теперь остаётся самая важная часть: нужно условно рисовать пузырьки с надписями 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 с флагами горизонтального и вертикального центрирования. После компиляции получаем следующий результат.

Видно, что теперь сигналы отображаются в пузырьках с надписями 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 и установкой перерисовки при новом сигнале. Так мы получим оповещения при появлении сигнала, как показано ниже.

Для визуализации уровней на графике используется следующая логика.
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 по количеству знаков символа, окрашивая их соответствующим образом и располагая с заданными смещениями от правого края. Вы можете настроить это по своим предпочтениям. После компиляции получаем отрисованные уровни.

Видно, что при разрешённом отображении уровни полностью отрисованы на 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).

Заключение
В заключение мы улучшили индикатор WaveTrend Crossover в MQL5, добавив отрисовку с использованием Canvas для градиентных туманных наложений, сигнальные боксы для обнаружения пробоев, метки сигналов в виде пузырьков с надписями BUY/SELL или простых треугольников, а также встроенное управление рисками через динамические уровни Take Profit и Stop Loss. Это обновление включает расширенные визуальные элементы, такие как градиентный туман для рыночного контекста, а также параметры фильтрации по тренду, расширения сигнальных боксов и расчётов через свечные множители или проценты с отображением в виде линий и таблиц. Такая конфигурация предоставляет более насыщенные визуальные инструменты для анализа изменений импульса, тренда и риска. С этим улучшенным индикатором WaveTrend Crossover вы сможете использовать визуальные функции и инструменты управления рисками для более точного анализа торговых возможностей и дальнейшей оптимизации своей торговли. Удачной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20815
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
MQL5 Bootstrap (I): Набор переиспользуемых функций для работы с позициями и ордерами
Моделирование рынка: Position View (VIII)
Алгоритм бактериальной эволюции — Bacterial Evolutionary Algorithm (BEA)
Оптимизация трендовой торговли: Торговля по направлению тренда и с учётом его силы
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования