English
preview
Торговые инструменты на MQL5 (Часть 20): Построение графиков на Canvas с использованием статистической корреляции и регрессионного анализа

Торговые инструменты на MQL5 (Часть 20): Построение графиков на Canvas с использованием статистической корреляции и регрессионного анализа

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

Введение

В своей предыдущей статье (Часть 19) мы разработали интерактивную палитру инструментов в MetaQuotes Language 5 (MQL5) для рисования графиков, включающую в себя перетаскиваемые панели, изменение размера, переключение тем и кнопки для различных инструментов анализа. В Части 20 мы создаем графический инструмент на основе Canvas для статистической корреляции и линейной регрессии между двумя переменными с перетаскиваемыми и масштабируемыми элементами, динамическими тиками и статистическим отображением. Эта визуализация предоставляет сведения о парной торговле с помощью линий регрессии, точек данных и таких показателей, как наклон и R-квадрат. В статье рассмотрим следующие темы:

  1. Изучение статистической корреляции и регрессии на графиках Canvas
  2. Реализация средствами MQL5
  3. Тестирование на истории
  4. Заключение

В итоге у вас будет интерактивный регрессионный график, готовый к проведению анализа рынка. Перейдём к реализации!


Изучение статистической корреляции и регрессии на графиках Canvas

Статистическая корреляция измеряет силу и направление связи между двумя переменными, такими как цены символов, используя такие показатели, как коэффициент Пирсона, варьирующийся от -1 (обратная корреляция) до 1 (прямая корреляция), в то время как линейная регрессия строит прямую линию по точкам данных для прогнозирования тенденций по наклону и свободному члену. На графиках Canvas эти данные визуализируются с помощью точек рассеяния для корреляций и линий регрессии для прогнозов, а коэффициент детерминации R² используется для оценки качества построения, что помогает нам выявлять парные зависимости или расхождения. Это графическое представление на перемещаемых объектах Canvas позволяет интерактивно изучать рыночные взаимосвязи, а панели статистики обеспечивают быстрый доступ к аналитической информации. Наш план состоит в том, чтобы загрузить данные символов, вычислить регрессию с использованием ALGLIB, отобразить графики с динамическими тиками и сглаженными точками/линиями, а также вывести статистические данные, такие как наклон и коэффициент детерминации R², на графических накладках. Вкратце, ниже представлено наглядное представление наших целей.

CORRELATION & REGRESSION CANVAS GRAPHING ROADMAP


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

Чтобы создать программу на MQL5, откройте MetaEditor, перейдите в Навигатор, найдите папку «Советники» (Experts), щелкните кнопкой мыши на вкладке "Создать" (New) и следуйте инструкциям по созданию файла. Как только это будет сделано, в среде программирования нужно будет объявить некоторые входные параметры и глобальные переменные, которые будем использовать в программе.

//+------------------------------------------------------------------+
//|              Canvas Graphing PART 1 - Statistical Regression.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 strict

#include <Math\Alglib\alglib.mqh>
#include <Canvas\Canvas.mqh>

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ResizeDirection {
   NO_RESIZE,                                           // No resize
   RESIZE_BOTTOM_EDGE,                                  // Resize bottom edge
   RESIZE_RIGHT_EDGE,                                   // Resize right edge
   RESIZE_CORNER                                        // Resize corner
};

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
sinput group "=== REGRESSION SETTINGS ==="
input int                maxHistoryBars = 200;          // Maximum History Bars
input ENUM_TIMEFRAMES    chartTimeframe = PERIOD_CURRENT; // Chart Timeframe
input string             primarySymbol = "AUDUSDm";     // Primary Symbol (X-axis)
input string             secondarySymbol = "EURUSDm";   // Secondary Symbol (Y-axis)

sinput group "=== CANVAS DISPLAY SETTINGS ==="
input int                initialCanvasX = 20;           // Initial Canvas X Position
input int                initialCanvasY = 30;           // Initial Canvas Y Position
input int                initialCanvasWidth = 600;      // Initial Canvas Width
input int                initialCanvasHeight = 400;     // Initial Canvas Height
input int                plotPadding = 10;              // Plot Area Internal Padding (px)

sinput group "=== THEME COLOR (SINGLE CONTROL!) ==="
input color              themeColor = clrDodgerBlue;    // Master Theme Color
input bool               showBorderFrame = true;        // Show Border Frame

sinput group "=== REGRESSION LINE SETTINGS ==="
input color              regressionLineColor = clrBlue; // Regression Line Color
input int                regressionLineWidth = 2;       // Regression Line Width
input color              dataPointsColor = clrRed;      // Data Points Color
input int                dataPointSize = 3;             // Data Point Size

sinput group "=== BACKGROUND SETTINGS ==="
input bool               enableBackgroundFill = true;   // Enable Background Fill
input color              backgroundTopColor = clrWhite; // Background Top Color
input double             backgroundOpacityLevel = 0.95; // Background Opacity (0-1)

sinput group "=== TEXT AND LABELS ==="
input int                titleFontSize = 14;            // Title Font Size
input color              titleTextColor = clrBlack;     // Title Text Color
input int                labelFontSize = 11;            // Label Font Size
input color              labelTextColor = clrBlack;     // Label Text Color
input int                axisLabelFontSize = 12;        // Axis Labels Font Size
input bool               showStatistics = true;         // Show Statistics & Legend

sinput group "=== STATS & LEGEND PANEL SETTINGS ==="
input int                statsPanelX = 70;              // Stats Panel X Position
input int                statsPanelY = 10;              // Stats Panel Y Offset (from header)
input int                statsPanelWidth = 130;         // Stats Panel Width
input int                statsPanelHeight = 65;         // Stats Panel Height
input int                panelFontSize = 13;            // Stats & Legend Font Size
input int                legendHeight = 35;             // Legend Panel Height

sinput group "=== INTERACTION SETTINGS ==="
input bool               enableDragging = true;         // Enable Canvas Dragging
input bool               enableResizing = true;         // Enable Canvas Resizing
input int                resizeGripSize = 8;            // Resize Grip Size (pixels)

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CCanvas mainCanvas;                                     //--- Declare main canvas
string canvasObjectName = "RegressionCanvas_Main";      //--- Set canvas object name

int currentPositionX = initialCanvasX;                  //--- Initialize current X position
int currentPositionY = initialCanvasY;                  //--- Initialize current Y position
int currentWidthPixels = initialCanvasWidth;            //--- Initialize current width
int currentHeightPixels = initialCanvasHeight;          //--- Initialize current height

bool isDraggingCanvas = false;                          //--- Initialize dragging flag
bool isResizingCanvas = false;                          //--- Initialize resizing flag
int dragStartX = 0, dragStartY = 0;                     //--- Initialize drag start coordinates
int canvasStartX = 0, canvasStartY = 0;                 //--- Initialize canvas start coordinates

int resizeStartX = 0, resizeStartY = 0;                 //--- Initialize resize start coordinates
int resizeInitialWidth = 0, resizeInitialHeight = 0;    //--- Initialize resize initial dimensions
ResizeDirection activeResizeMode = NO_RESIZE;           //--- Initialize active resize mode
ResizeDirection hoverResizeMode = NO_RESIZE;            //--- Initialize hover resize mode

bool isHoveringCanvas = false;                          //--- Initialize canvas hover flag
bool isHoveringHeader = false;                          //--- Initialize header hover flag
bool isHoveringResizeZone = false;                      //--- Initialize resize hover flag
int lastMouseX = 0, lastMouseY = 0;                     //--- Initialize last mouse coordinates
int previousMouseButtonState = 0;                       //--- Initialize previous mouse state

const int MIN_CANVAS_WIDTH = 300;                       //--- Set minimum canvas width
const int MIN_CANVAS_HEIGHT = 200;                      //--- Set minimum canvas height
const int HEADER_BAR_HEIGHT = 35;                       //--- Set header bar height

double regressionSlope = 0.0;                           //--- Initialize regression slope
double regressionIntercept = 0.0;                       //--- Initialize regression intercept
double correlationCoefficient = 0.0;                    //--- Initialize correlation coefficient
double rSquared = 0.0;                                  //--- Initialize R-squared

double primaryClosePrices[];                            //--- Declare primary close prices array
double secondaryClosePrices[];                          //--- Declare secondary close prices array
bool dataLoadedSuccessfully = false;                    //--- Initialize data loaded flag

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

Глобальные переменные включают в себя основной объект Canvas "mainCanvas" с именем "RegressionCanvas_Main"; отслеживание текущего положения и размеров; флаги и координаты для перетаскивания/изменения размера; состояния при наведении курсора и отслеживание курсора мыши; константы для минимальных размеров и высоты заголовка; метрики регрессии, такие как наклон и коэффициент детерминации R²; массивы цен для символов; и флаг загрузки данных. Далее мы определим несколько вспомогательных функций для цветовых тем, которые помогут в нанесении цветов.

//+------------------------------------------------------------------+
//| Theme Color Helper Functions                                     |
//+------------------------------------------------------------------+
color LightenColor(color baseColor, double factor) {
   uchar r = (uchar)((baseColor >> 16) & 0xFF);                //--- Extract red component
   uchar g = (uchar)((baseColor >> 8) & 0xFF);                 //--- Extract green component
   uchar b = (uchar)(baseColor & 0xFF);                        //--- Extract blue component
   
   r = (uchar)MathMin(255, r + (255 - r) * factor);            //--- Lighten red
   g = (uchar)MathMin(255, g + (255 - g) * factor);            //--- Lighten green
   b = (uchar)MathMin(255, b + (255 - b) * factor);            //--- Lighten blue
   
   return (r << 16) | (g << 8) | b;                            //--- Return lightened color
}

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

Здесь мы реализуем две вспомогательные функции, "LightenColor" и "DarkenColor", для динамической корректировки основного цвета темы для визуальных эффектов, таких как градиенты и всплывающие подсказки на графике регрессии. В функции "LightenColor" мы извлекаем компоненты RGB из базового цвета с помощью битовых сдвигов, затем осветляем каждый из них, добавляя масштабированную по коэффициенту часть оставшейся интенсивности к значению 255, ограничивая значение с помощью MathMin во избежание переполнения за границы, и объединяем их в значение цвета.

Аналогично, функция "DarkenColor" извлекает компоненты и умножает каждый из них на (1 - коэффициент), чтобы уменьшить интенсивность, получая оттенки для рамок или фона. Эти функции необходимы для обеспечения единообразия темы, поскольку они определяют вариации цвета на основе одного входного значения цвета, что позволяет создавать плавные градиенты и адаптивные элементы пользовательского интерфейса без жесткого кодирования нескольких цветов. Для продолжения мы инициализируем объект Canvas и загрузим данные символов, которые будем использовать для анализа. Мы будем использовать функции, чтобы сделать наш код модульным и организованным для дальнейшего расширения. Для достижения этого результата мы использовали следующий подход.

//+------------------------------------------------------------------+
//| Create Regression Canvas                                         |
//+------------------------------------------------------------------+
bool CreateCanvas() {
   if (!mainCanvas.CreateBitmapLabel(0, 0, canvasObjectName, 
       currentPositionX, currentPositionY, currentWidthPixels, currentHeightPixels, 
       COLOR_FORMAT_ARGB_NORMALIZE)) {                          //--- Create bitmap label
      return false;                                             //--- Return failure
   }
   return true;                                                 //--- Return success
}

//+------------------------------------------------------------------+
//| Load Price Data for Regression Analysis                          |
//+------------------------------------------------------------------+
bool loadSymbolClosePrices() {
   if (!SymbolSelect(primarySymbol, true)) {                     //--- Select primary symbol
      Print("ERROR: Primary symbol not found: ", primarySymbol); //--- Print error
      return false;                                              //--- Return failure
   }

   if (!SymbolSelect(secondarySymbol, true)) {                   //--- Select secondary symbol
      Print("ERROR: Secondary symbol not found: ", secondarySymbol); //--- Print error
      return false;                                              //--- Return failure
   }

   int copiedPrimary = CopyClose(primarySymbol, chartTimeframe, 1, maxHistoryBars, primaryClosePrices); //--- Copy primary closes
   if (copiedPrimary <= 0) {                                     //--- Check copy success
      Print("ERROR: Failed to copy data for ", primarySymbol, ". Error: ", GetLastError()); //--- Print error
      return false;                                              //--- Return failure
   }

   int copiedSecondary = CopyClose(secondarySymbol, chartTimeframe, 1, maxHistoryBars, secondaryClosePrices); //--- Copy secondary closes
   if (copiedSecondary <= 0) {                                   //--- Check copy success
      Print("ERROR: Failed to copy data for ", secondarySymbol, ". Error: ", GetLastError()); //--- Print error
      return false;                                              //--- Return failure
   }

   int actualBars = MathMin(copiedPrimary, copiedSecondary);     //--- Get min bars
   ArrayResize(primaryClosePrices, actualBars);                  //--- Resize primary array
   ArrayResize(secondaryClosePrices, actualBars);                //--- Resize secondary array

   dataLoadedSuccessfully = true;                                //--- Set loaded flag
   Print("SUCCESS: Loaded ", actualBars, " bars for both symbols"); //--- Print success
   return true;                                                  //--- Return success
}

Сначала реализуем функцию "CreateCanvas" для настройки основной графической области для графика регрессии, используя метод CreateBitmapLabel на "mainCanvas" с указанием текущей позиции, размеров и параметра COLOR_FORMAT_ARGB_NORMALIZE для поддержки альфа-канала, возвращая false в случае неудачи или true в случае успеха. Этот метод мы вызовем во время инициализации для создания визуальной основы.

Далее мы создаём функцию "loadSymbolClosePrices" для получения исторических данных для анализа, сначала выбирая символы с помощью SymbolSelect и выводя сообщение об ошибках, если они не найдены, затем копируя цены закрытия для основных и второстепенных символов с помощью CopyClose в массивы, проверяя наличие положительных значений и обрабатывая ошибки с помощью GetLastError. Для обеспечения согласованности мы берем минимальное количество баров между копиями, соответствующим образом изменяем размер массивов, устанавливаем флаг "dataLoadedSuccessfully", выводим сообщение об успешной загрузке баров и возвращаем true, что позволяет выполнять вычисления регрессии только с действительными данными. Теперь можно вызвать эту функцию в обработчике события инициализации, чтобы выполнить начальную настройку.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   currentPositionX = initialCanvasX;                           //--- Set current X from input
   currentPositionY = initialCanvasY;                           //--- Set current Y from input
   currentWidthPixels = initialCanvasWidth;                     //--- Set current width from input
   currentHeightPixels = initialCanvasHeight;                   //--- Set current height from input

   if (!CreateCanvas()) {                                       //--- Create canvas or fail
      Print("ERROR: Failed to create regression canvas");       //--- Print error
      return(INIT_FAILED);                                      //--- Return failure
   }

   if (!loadSymbolClosePrices()) {                              //--- Load prices or fail
      Print("ERROR: Failed to load price data for symbols");    //--- Print error
      return(INIT_FAILED);                                      //--- Return failure
   }

   ChartRedraw();                                               //--- Redraw chart

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

В обработчике OnInit устанавливаем текущее положение и размеры на основе входных параметров, обеспечивая начало работы объекта Canvas в указанном пользователем месте и размере. Далее мы вызываем функцию "CreateCanvas" для инициализации основной графической области, выводя сообщение об ошибке и возвращая INIT_FAILED в случае неудачи, после чего загружаем данные о ценах с помощью "loadSymbolClosePrices", обрабатывая сбои аналогичным образом, чтобы предотвратить продолжение работы без корректных входных данных. Наконец, мы перерисовываем диаграмму, чтобы отобразить график, и возвращаем INIT_SUCCEEDED, завершая настройку для выполнения интерактивного регрессионного анализа. Теперь мы можем определить уравнение для вычисления линии регрессии, чтобы использовать его при визуализации.

//+------------------------------------------------------------------+
//| Calculate Linear Regression Parameters                           |
//+------------------------------------------------------------------+
bool computeLinearRegression() {
   int dataSize = ArraySize(primaryClosePrices);                //--- Get data size
   if (dataSize <= 0 || ArraySize(secondaryClosePrices) != dataSize) { //--- Check valid size
      return false;                                             //--- Return failure
   }

   double tempPrimary[], tempSecondary[];                       //--- Declare temp arrays
   ArraySetAsSeries(tempPrimary, true);                         //--- Set primary as series
   ArraySetAsSeries(tempSecondary, true);                       //--- Set secondary as series
   ArrayCopy(tempPrimary, primaryClosePrices);                  //--- Copy primary
   ArrayCopy(tempSecondary, secondaryClosePrices);              //--- Copy secondary

   CMatrixDouble regressionMatrix(dataSize, 2);                 //--- Create regression matrix

   for (int i = 0; i < dataSize; i++) {                         //--- Loop over data
      regressionMatrix.Set(i, 0, tempPrimary[i]);               //--- Set X value
      regressionMatrix.Set(i, 1, tempSecondary[i]);             //--- Set Y value
   }

   CLinReg linearRegression;                                    //--- Declare linear regression
   CLinearModel linearModel;                                    //--- Declare linear model
   CLRReport regressionReport;                                  //--- Declare report
   int returnCode;                                              //--- Declare return code

   linearRegression.LRBuild(regressionMatrix, dataSize, 1, returnCode, linearModel, regressionReport); //--- Build regression

   if (returnCode != 1) {                                       //--- Check success
      Print("ERROR: Linear regression calculation failed with code: ", returnCode); //--- Print error
      return false;                                             //--- Return failure
   }

   int numberOfVars;                                            //--- Declare vars count
   double coefficientsArray[];                                  //--- Declare coefficients
   linearRegression.LRUnpack(linearModel, coefficientsArray, numberOfVars); //--- Unpack model

   regressionSlope = coefficientsArray[0];                      //--- Set slope
   regressionIntercept = coefficientsArray[1];                  //--- Set intercept

   computeStatistics();                                         //--- Compute statistics

   PrintFormat("Regression Equation: Y = %.6f + %.6f * X", regressionIntercept, regressionSlope); //--- Print equation
   PrintFormat("Correlation: %.4f | R-Squared: %.4f", correlationCoefficient, rSquared); //--- Print stats

   return true;                                                 //--- Return success
}

//+------------------------------------------------------------------+
//| Calculate Regression Statistics                                  |
//+------------------------------------------------------------------+
void computeStatistics() {
   int n = ArraySize(primaryClosePrices);                       //--- Get size
   if (n <= 0) return;                                          //--- Return if empty

   double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0; //--- Initialize sums

   for (int i = 0; i < n; i++) {                                //--- Loop over data
      double x = primaryClosePrices[i];                         //--- Get X
      double y = secondaryClosePrices[i];                       //--- Get Y

      sumX += x;                                                //--- Accumulate X
      sumY += y;                                                //--- Accumulate Y
      sumXY += x * y;                                           //--- Accumulate XY
      sumX2 += x * x;                                           //--- Accumulate X2
      sumY2 += y * y;                                           //--- Accumulate Y2
   }

   double meanX = sumX / n;                                     //--- Compute mean X
   double meanY = sumY / n;                                     //--- Compute mean Y

   double numerator = n * sumXY - sumX * sumY;                  //--- Compute numerator
   double denominatorX = MathSqrt(n * sumX2 - sumX * sumX);     //--- Compute denominator X
   double denominatorY = MathSqrt(n * sumY2 - sumY * sumY);     //--- Compute denominator Y

   if (denominatorX != 0 && denominatorY != 0) {                //--- Check denominators
      correlationCoefficient = numerator / (denominatorX * denominatorY); //--- Compute correlation
      rSquared = correlationCoefficient * correlationCoefficient; //--- Compute R-squared
   } else {                                                     //--- Handle zero denominators
      correlationCoefficient = 0;                               //--- Set correlation to 0
      rSquared = 0;                                             //--- Set R-squared to 0
   }
}

Мы реализуем функцию "computeLinearRegression" для выполнения линейного регрессионного анализа с использованием библиотеки ALGLIB, сначала получая размер данных из "primaryClosePrices" и проверяя, соответствует ли он "secondaryClosePrices", возвращая false, если размер данных недействителен или пуст, чтобы предотвратить ошибки. Далее мы подготавливаем временные массивы "tempPrimary" и "tempSecondary", заданные как серии с помощью ArraySetAsSeries для обеспечения надлежащего порядка, копируем данные о ценах и создаем матрицу регрессии "CMatrixDouble" размером dataSize x 2, заполняя столбец 0 первичными ценами (X), а столбец 1 — вторичными (Y) в цикле.

Мы объявляем объекты ALGLIB, включая "CLinReg" для регрессии, "CLinearModel" для модели, "CLRReport" для результатов и код возврата, затем вызываем "linearRegression.LRBuild" с матрицей, размером и переменной 1, проверяя, равен ли returnCode 1 для успешного выполнения; если нет, выводим сообщение об ошибке и возвращаем false. В случае успеха мы распаковываем модель с помощью функции "linearRegression.LRUnpack" в массив "coefficientsArray", присваивая значение наклона переменной "regressionSlope" (индекс 0) и значение пересечения "regressionIntercept" (индекс 1), вызываем функцию "computeStatistics" для вычисления дополнительных метрик, выводим уравнение регрессии и статистику с помощью функции PrintFormat и возвращаем значение true.

Функция "computeStatistics" вычисляет корреляцию и коэффициент детерминации R-квадрат для проверки, получая n из размера массива и инициализируя суммы для X, Y, XY, X2, Y2, а затем в цикле накапливая эти значения из массивов цен. Мы вычисляем средние значения "meanX" и "meanY" как суммы, деленные на n, затем числитель как nsumXY - sumXsumY, а знаменатели как квадратные корни из (n*sumX2 - sumX^2), и аналогично для Y, устанавливая "correlationCoefficient" равным произведению числителя и знаменателей, если оно не равно нулю (коэффициент корреляции Пирсона r, измеряющий силу линейной зависимости от -1 до 1), в противном случае -0; R-квадрат, как его квадрат указывает на дисперсию, объясняемую моделью. Этот статистический расчет имеет решающее значение для количественной оценки парных взаимосвязей, где высокая положительная корреляция указывает на схожие движения, что помогает в таких стратегиях, как хеджирование, в то время как низкий коэффициент детерминации (R-квадрат) предупреждает о плохом соответствии. Фактически, мы можем вызвать эту функцию при инициализации, чтобы выполнить вычисления на бэкенде следующим образом.

if (!computeLinearRegression()) {                            //--- Compute regression or fail
   Print("ERROR: Failed to calculate regression parameters"); //--- Print error
   return(INIT_FAILED);                                      //--- Return failure
}

Это дает следующий результат.

INITIAL RUN OUTCOME

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

//+------------------------------------------------------------------+
//| Render Regression Visualization                                  |
//+------------------------------------------------------------------+
void renderVisualization() {
   mainCanvas.Erase(0);                                         //--- Erase canvas

   if (enableBackgroundFill) {                                  //--- Check background fill
      drawGradientBackground();                                 //--- Draw gradient background
   }

   drawCanvasBorder();                                          //--- Draw border
   drawHeaderBar();                                             //--- Draw header bar

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

//+------------------------------------------------------------------+
//| Draw Gradient Background                                         |
//+------------------------------------------------------------------+
void drawGradientBackground() {
   color bottomColor = LightenColor(themeColor, 0.85);          //--- Compute bottom color
   
   for (int y = HEADER_BAR_HEIGHT; y < currentHeightPixels; y++) { //--- Loop over rows
      double gradientFactor = (double)(y - HEADER_BAR_HEIGHT) / (currentHeightPixels - HEADER_BAR_HEIGHT); //--- Compute factor
      color currentRowColor = InterpolateColors(backgroundTopColor, bottomColor, gradientFactor); //--- Interpolate color
      uchar alphaChannel = (uchar)(255 * backgroundOpacityLevel); //--- Compute alpha
      uint argbColor = ColorToARGB(currentRowColor, alphaChannel); //--- Get ARGB

      for (int x = 0; x < currentWidthPixels; x++) {            //--- Loop over columns
         mainCanvas.PixelSet(x, y, argbColor);                  //--- Set pixel
      }
   }
}

//+------------------------------------------------------------------+
//| Draw Canvas Border                                               |
//+------------------------------------------------------------------+
void drawCanvasBorder() {
   if (!showBorderFrame) return;                                //--- Return if no border

   color borderColor = isHoveringResizeZone ? DarkenColor(themeColor, 0.2) : themeColor; //--- Get border color
   uint argbBorder = ColorToARGB(borderColor, 255);             //--- Get ARGB border

   mainCanvas.Rectangle(0, 0, currentWidthPixels - 1, currentHeightPixels - 1, argbBorder); //--- Draw outer border
   mainCanvas.Rectangle(1, 1, currentWidthPixels - 2, currentHeightPixels - 2, argbBorder); //--- Draw inner border
}

//+------------------------------------------------------------------+
//| Draw Header Bar                                                  |
//+------------------------------------------------------------------+
void drawHeaderBar() {
   color headerColor;                                           //--- Declare header color
   if (isDraggingCanvas) {                                      //--- Check dragging
      headerColor = DarkenColor(themeColor, 0.1);               //--- Set darker color
   } else if (isHoveringHeader) {                               //--- Check hovering
      headerColor = LightenColor(themeColor, 0.4);              //--- Set medium light
   } else {                                                     //--- Default
      headerColor = LightenColor(themeColor, 0.7);              //--- Set very light
   }
   uint argbHeader = ColorToARGB(headerColor, 255);             //--- Get ARGB header

   mainCanvas.FillRectangle(0, 0, currentWidthPixels - 1, HEADER_BAR_HEIGHT, argbHeader); //--- Fill header

   if (showBorderFrame) {                                       //--- Check show border
      uint argbBorder = ColorToARGB(themeColor, 255);           //--- Get ARGB border
      mainCanvas.Rectangle(0, 0, currentWidthPixels - 1, HEADER_BAR_HEIGHT, argbBorder); //--- Draw outer
      mainCanvas.Rectangle(1, 1, currentWidthPixels - 2, HEADER_BAR_HEIGHT - 1, argbBorder); //--- Draw inner
   }

   mainCanvas.FontSet("Arial Bold", titleFontSize);             //--- Set title font
   uint argbText = ColorToARGB(titleTextColor, 255);            //--- Get ARGB text

   string titleText = StringFormat("%s vs %s - Linear Regression", secondarySymbol, primarySymbol); //--- Format title
   mainCanvas.TextOut(currentWidthPixels / 2, (HEADER_BAR_HEIGHT - titleFontSize) / 2, 
                                titleText, argbText, TA_CENTER); //--- Draw title
}


//--- We call the visualization function in the initialization event

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   currentPositionX = initialCanvasX;                           //--- Set current X from input
   currentPositionY = initialCanvasY;                           //--- Set current Y from input
   currentWidthPixels = initialCanvasWidth;                     //--- Set current width from input
   currentHeightPixels = initialCanvasHeight;                   //--- Set current height from input

   if (!CreateCanvas()) {                                       //--- Create canvas or fail
      Print("ERROR: Failed to create regression canvas");       //--- Print error
      return(INIT_FAILED);                                      //--- Return failure
   }

   if (!loadSymbolClosePrices()) {                              //--- Load prices or fail
      Print("ERROR: Failed to load price data for symbols");    //--- Print error
      return(INIT_FAILED);                                      //--- Return failure
   }

   if (!computeLinearRegression()) {                            //--- Compute regression or fail
      Print("ERROR: Failed to calculate regression parameters"); //--- Print error
      return(INIT_FAILED);                                      //--- Return failure
   }

   renderVisualization();                                       //--- Render visualization

   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);            //--- Enable mouse events
   ChartRedraw();                                               //--- Redraw chart

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

Здесь мы реализуем функцию "renderVisualization" для компоновки всего графика на Canvas, начиная с его очистки с помощью Erase, установленного на 0 для получения «чистого листа», затем условно рисуем градиентный фон, если "enableBackgroundFill" имеет значение true, после чего следует контур и бар заголовка, и завершаем функцией Update для отображения содержимого. Можно использовать любой предпочитаемый вами стиль границы или цвет. Мы просто придумали произвольный способ демонстрации.

Далее функция "drawGradientBackground" создает вертикальный градиент от заголовка вниз, осветляя цвет темы внизу с помощью функции "LightenColor", перебирая строки для вычисления коэффициентов интерполяции, смешивая цвета с помощью "InterpolateColors", применяя прозрачность к ARGB и устанавливая каждый пиксель построчно с помощью функции PixelSet для плавных переходов. Для обрамления объекта Canvas функция "drawCanvasBorder" проверяет значение параметра "showBorderFrame" и возвращается досрочно, если оно равно false, в противном случае затемняет цвет темы при наведении курсора и изменении размера с помощью параметра "DarkenColor", преобразует его в ARGB и рисует внешние и внутренние прямоугольники с помощью параметра Rectangle для создания эффекта обводки.

Для верхней части панели "drawHeaderBar" выбирает цвет заливки в зависимости от перетаскивания (затемненный), наведения курсора (средне осветленный) или по умолчанию (очень осветленный) с помощью параметров "DarkenColor" или "LightenColor", заполняет прямоугольник бара, добавляет границы, если они включены, устанавливает жирный шрифт "Arial", форматирует заголовок символами и центрирует его с помощью TextOut в текстовом цвете ARGB. В обработчике OnInit, после настройки и обработки данных, мы вызываем функцию "renderVisualization" для генерации начального графика, включаем события перемещения мыши с помощью ChartSetInteger и перерисовываем график для немедленного просмотра. В программировании принято всегда выполнять компиляцию и тестирование прогресса на каждом этапе. После компиляции получаем следующий результат.

CANVAS HEADER BAR AND BODY

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

//+------------------------------------------------------------------+
//| Calculate optimal ticks with AGGRESSIVE spacing (fills space!)   |
//+------------------------------------------------------------------+
int calculateOptimalTicks(double minValue, double maxValue, int pixelRange, double &tickValues[]) {
   double range = maxValue - minValue;                          //--- Compute range
   if (range == 0 || pixelRange <= 0) {                         //--- Check invalid
      ArrayResize(tickValues, 1);                               //--- Resize to 1
      tickValues[0] = minValue;                                 //--- Set single tick
      return 1;                                                 //--- Return 1
   }
   
   int targetTickCount = (int)(pixelRange / 50.0);              //--- Compute target count
   if (targetTickCount < 3) targetTickCount = 3;                //--- Min 3
   if (targetTickCount > 20) targetTickCount = 20;              //--- Max 20
   
   double roughStep = range / (double)(targetTickCount - 1);    //--- Compute rough step
   
   double magnitude = MathPow(10.0, MathFloor(MathLog10(roughStep))); //--- Compute magnitude
   
   double normalized = roughStep / magnitude;                   //--- Normalize
   
   double niceNormalized;                                       //--- Declare nice normalized
   if (normalized <= 1.0) niceNormalized = 1.0;                 //--- Set 1.0
   else if (normalized <= 1.5) niceNormalized = 1.0;            //--- Set 1.0
   else if (normalized <= 2.0) niceNormalized = 2.0;            //--- Set 2.0
   else if (normalized <= 2.5) niceNormalized = 2.0;            //--- Set 2.0
   else if (normalized <= 3.0) niceNormalized = 2.5;            //--- Set 2.5
   else if (normalized <= 4.0) niceNormalized = 4.0;            //--- Set 4.0
   else if (normalized <= 5.0) niceNormalized = 5.0;            //--- Set 5.0
   else if (normalized <= 7.5) niceNormalized = 5.0;            //--- Set 5.0
   else niceNormalized = 10.0;                                  //--- Set 10.0
   
   double step = niceNormalized * magnitude;                    //--- Compute step
   
   double tickMin = MathFloor(minValue / step) * step;          //--- Compute tick min
   double tickMax = MathCeil(maxValue / step) * step;           //--- Compute tick max
   
   int numTicks = (int)MathRound((tickMax - tickMin) / step) + 1; //--- Compute num ticks
   
   if (numTicks > 25) {                                         //--- Check too many
      step *= 2.0;                                              //--- Double step
      tickMin = MathFloor(minValue / step) * step;              //--- Recalc min
      tickMax = MathCeil(maxValue / step) * step;               //--- Recalc max
      numTicks = (int)MathRound((tickMax - tickMin) / step) + 1; //--- Recalc num
   }
   
   if (numTicks < 3) {                                          //--- Check too few
      step /= 2.0;                                              //--- Halve step
      tickMin = MathFloor(minValue / step) * step;              //--- Recalc min
      tickMax = MathCeil(maxValue / step) * step;               //--- Recalc max
      numTicks = (int)MathRound((tickMax - tickMin) / step) + 1; //--- Recalc num
   }
   
   ArrayResize(tickValues, numTicks);                           //--- Resize array
   for (int i = 0; i < numTicks; i++) {                         //--- Loop to set ticks
      tickValues[i] = tickMin + i * step;                       //--- Set tick value
   }
   
   return numTicks;                                             //--- Return count
}

//+------------------------------------------------------------------+
//| Format tick label with appropriate precision                     |
//+------------------------------------------------------------------+
string formatTickLabel(double value, double range) {
   if (range > 100) return DoubleToString(value, 0);            //--- Format no decimals
   else if (range > 10) return DoubleToString(value, 1);        //--- Format 1 decimal
   else if (range > 1) return DoubleToString(value, 2);         //--- Format 2 decimals
   else if (range > 0.1) return DoubleToString(value, 3);       //--- Format 3 decimals
   else return DoubleToString(value, 4);                        //--- Format 4 decimals
}

//+------------------------------------------------------------------+
//| Draw Regression Plot WITH CUSTOMIZABLE INTERNAL PADDING          |
//+------------------------------------------------------------------+
void drawRegressionPlot() {
   if (!dataLoadedSuccessfully) return;                         //--- Return if no data

   int plotAreaLeft = 60;                                       //--- Set plot left
   int plotAreaRight = currentWidthPixels - 40;                 //--- Set plot right
   int plotAreaTop = HEADER_BAR_HEIGHT + 10;                    //--- Set plot top
   int plotAreaBottom = currentHeightPixels - 50;               //--- Set plot bottom

   int drawAreaLeft = plotAreaLeft + plotPadding;               //--- Set draw left
   int drawAreaRight = plotAreaRight - plotPadding;             //--- Set draw right
   int drawAreaTop = plotAreaTop + plotPadding;                 //--- Set draw top
   int drawAreaBottom = plotAreaBottom - plotPadding;           //--- Set draw bottom

   int plotWidth = drawAreaRight - drawAreaLeft;                //--- Compute plot width
   int plotHeight = drawAreaBottom - drawAreaTop;               //--- Compute plot height

   if (plotWidth <= 0 || plotHeight <= 0) return;               //--- Return if invalid

   double minX = primaryClosePrices[0];                         //--- Init min X
   double maxX = primaryClosePrices[0];                         //--- Init max X
   double minY = secondaryClosePrices[0];                       //--- Init min Y
   double maxY = secondaryClosePrices[0];                       //--- Init max Y

   int dataPoints = ArraySize(primaryClosePrices);              //--- Get data points
   for (int i = 1; i < dataPoints; i++) {                       //--- Loop over points
      if (primaryClosePrices[i] < minX) minX = primaryClosePrices[i]; //--- Update min X
      if (primaryClosePrices[i] > maxX) maxX = primaryClosePrices[i]; //--- Update max X
      if (secondaryClosePrices[i] < minY) minY = secondaryClosePrices[i]; //--- Update min Y
      if (secondaryClosePrices[i] > maxY) maxY = secondaryClosePrices[i]; //--- Update max Y
   }

   double rangeX = maxX - minX;                                 //--- Compute range X
   double rangeY = maxY - minY;                                 //--- Compute range Y

   if (rangeX == 0) rangeX = 1;                                 //--- Set min range X
   if (rangeY == 0) rangeY = 1;                                 //--- Set min range Y

   uint argbAxisColor = ColorToARGB(clrBlack, 255);             //--- Get axis ARGB
   
   for (int thick = 0; thick < 2; thick++) {                    //--- Loop for thick Y-axis
      mainCanvas.Line(plotAreaLeft - thick, plotAreaTop, plotAreaLeft - thick, plotAreaBottom, argbAxisColor); //--- Draw Y-axis line
   }
   
   for (int thick = 0; thick < 2; thick++) {                    //--- Loop for thick X-axis
      mainCanvas.Line(plotAreaLeft, plotAreaBottom + thick, plotAreaRight, plotAreaBottom + thick, argbAxisColor); //--- Draw X-axis line
   }

   mainCanvas.FontSet("Arial", axisLabelFontSize);              //--- Set tick font
   uint argbTickLabel = ColorToARGB(clrBlack, 255);             //--- Get tick label ARGB
   
   double yTickValues[];                                        //--- Declare Y ticks
   int numYTicks = calculateOptimalTicks(minY, maxY, plotHeight, yTickValues); //--- Compute Y ticks
   
   for (int i = 0; i < numYTicks; i++) {                        //--- Loop over Y ticks
      double yValue = yTickValues[i];                           //--- Get Y value
      if (yValue < minY || yValue > maxY) continue;             //--- Skip out of range
      
      int yPos = drawAreaBottom - (int)((yValue - minY) / rangeY * plotHeight); //--- Compute Y pos
      
      mainCanvas.Line(plotAreaLeft - 5, yPos, plotAreaLeft, yPos, argbAxisColor); //--- Draw tick
      
      string yLabel = formatTickLabel(yValue, rangeY);          //--- Format label
      mainCanvas.TextOut(plotAreaLeft - 8, yPos - axisLabelFontSize/2, yLabel, argbTickLabel, TA_RIGHT); //--- Draw label
   }

   double xTickValues[];                                        //--- Declare X ticks
   int numXTicks = calculateOptimalTicks(minX, maxX, plotWidth, xTickValues); //--- Compute X ticks
   
   for (int i = 0; i < numXTicks; i++) {                        //--- Loop over X ticks
      double xValue = xTickValues[i];                           //--- Get X value
      if (xValue < minX || xValue > maxX) continue;             //--- Skip out of range
      
      int xPos = drawAreaLeft + (int)((xValue - minX) / rangeX * plotWidth); //--- Compute X pos
      
      mainCanvas.Line(xPos, plotAreaBottom, xPos, plotAreaBottom + 5, argbAxisColor); //--- Draw tick
      
      string xLabel = formatTickLabel(xValue, rangeX);          //--- Format label
      mainCanvas.TextOut(xPos, plotAreaBottom + 7, xLabel, argbTickLabel, TA_CENTER); //--- Draw label
   }

   uint argbPoints = ColorToARGB(dataPointsColor, 255);         //--- Get points ARGB

   for (int i = 0; i < dataPoints; i++) {                       //--- Loop over points
      int screenX = drawAreaLeft + (int)((primaryClosePrices[i] - minX) / rangeX * plotWidth); //--- Compute screen X
      int screenY = drawAreaBottom - (int)((secondaryClosePrices[i] - minY) / rangeY * plotHeight); //--- Compute screen Y

      drawCirclePoint(screenX, screenY, dataPointSize, argbPoints); //--- Draw point
   }

   double lineStartY = regressionIntercept + regressionSlope * minX; //--- Compute start Y
   double lineEndY = regressionIntercept + regressionSlope * maxX; //--- Compute end Y

   int lineStartScreenX = drawAreaLeft;                         //--- Set start screen X
   int lineStartScreenY = drawAreaBottom - (int)((lineStartY - minY) / rangeY * plotHeight); //--- Compute start screen Y
   int lineEndScreenX = drawAreaRight;                          //--- Set end screen X
   int lineEndScreenY = drawAreaBottom - (int)((lineEndY - minY) / rangeY * plotHeight); //--- Compute end screen Y

   uint argbLine = ColorToARGB(regressionLineColor, 255);       //--- Get line ARGB

   for (int w = 0; w < regressionLineWidth; w++) {              //--- Loop for width
      mainCanvas.LineAA(lineStartScreenX, lineStartScreenY + w, 
                                  lineEndScreenX, lineEndScreenY + w, argbLine); //--- Draw line
   }

   mainCanvas.FontSet("Arial Bold", labelFontSize);             //--- Set axis label font
   uint argbAxisLabel = ColorToARGB(clrBlack, 255);             //--- Get axis label ARGB

   string xAxisLabel = primarySymbol + " (X-axis)";             //--- Set X label
   mainCanvas.TextOut(currentWidthPixels / 2, currentHeightPixels - 20, xAxisLabel, argbAxisLabel, TA_CENTER); //--- Draw X label

   string yAxisLabel = secondarySymbol + " (Y-axis)";           //--- Set Y label
   mainCanvas.FontAngleSet(900);                                //--- Set vertical angle
   mainCanvas.TextOut(12, currentHeightPixels / 2, yAxisLabel, argbAxisLabel, TA_CENTER); //--- Draw Y label
   mainCanvas.FontAngleSet(0);                                  //--- Reset angle
}

//+------------------------------------------------------------------+
//| Draw Circle Point with Anti-Aliasing (smooth like CGraphic)      |
//+------------------------------------------------------------------+
void drawCirclePoint(int centerX, int centerY, int radius, uint argbColor) {
   uchar srcAlpha = (uchar)((argbColor >> 24) & 0xFF);          //--- Extract source alpha
   uchar srcRed = (uchar)((argbColor >> 16) & 0xFF);            //--- Extract source red
   uchar srcGreen = (uchar)((argbColor >> 8) & 0xFF);           //--- Extract source green
   uchar srcBlue = (uchar)(argbColor & 0xFF);                   //--- Extract source blue
   
   double radiusDouble = (double)radius + 0.5;                  //--- Adjust radius
   int extent = radius + 2;                                     //--- Compute extent
   
   for (int dy = -extent; dy <= extent; dy++) {                 //--- Loop over dy
      for (int dx = -extent; dx <= extent; dx++) {              //--- Loop over dx
         double distance = MathSqrt((double)(dx * dx + dy * dy)); //--- Compute distance
         
         if (distance <= radiusDouble) {                        //--- Check within radius
            double coverage = 1.0;                              //--- Set full coverage
            if (distance > radiusDouble - 1.0) {                //--- Check edge
               coverage = radiusDouble - distance;              //--- Compute coverage
               if (coverage < 0) coverage = 0;                  //--- Clamp min
               if (coverage > 1.0) coverage = 1.0;              //--- Clamp max
            }
            
            uchar finalAlpha = (uchar)(srcAlpha * coverage);    //--- Compute final alpha
            if (finalAlpha == 0) continue;                      //--- Skip if transparent
            
            uint pixelColor = ((uint)finalAlpha << 24) | ((uint)srcRed << 16) | 
                             ((uint)srcGreen << 8) | (uint)srcBlue; //--- Compose color
            
            int px = centerX + dx;                              //--- Compute pixel X
            int py = centerY + dy;                              //--- Compute pixel Y
            if (px >= 0 && px < currentWidthPixels && py >= 0 && py < currentHeightPixels) { //--- Check bounds
               blendPixelSet(mainCanvas, px, py, pixelColor);   //--- Blend pixel
            }
         }
      }
   }
}

Для построения графика мы реализуем функцию "drawRegressionPlot", которая визуализирует результаты регрессионного анализа на объекте Canvas. Сначала функция возвращается к исходному состоянию, если данные не загружены, затем определяет границы области построения графика с фиксированными полями и применяет "plotPadding" для внутреннего интервала, вычисляет эффективные размеры отрисовки и завершает работу, если данные недействительны. Далее, мы находим минимум/максимум для X (первичные цены) и Y (вторичные цены), проходя по массивам в цикле, устанавливаем нулевые значения равными 1 для масштабирования, преобразуем черный цвет в ARGB для осей и рисуем утолщенные линии по осям Y и X, используя Line в циклах для удвоения ширины.

Для маркировки осей мы устанавливаем шрифт "Arial" с помощью параметра FontSet, подготавливаем ARGB для тиков, вычисляем тики по оси Y с помощью параметра "calculateOptimalTicks" и преобразуем их в значения "yTickValues", выполняем цикл для позиционирования каждого значения, рисуем короткие тики с помощью параметра "Line" и добавляем метки, выровненные по правому краю, используя параметр "formatTickLabel" на основе диапазона. Аналогично для тиков по оси X с метками, расположенными по центру снизу. Мы отображаем точки данных, преобразуя цены в экранные координаты, масштабированные в соответствии с диапазонами и размерами, и вызываем функцию "drawCirclePoint" с радиусом и цветом ARGB из входных данных для каждой точки.

Для построения линии регрессии мы вычисляем начальную/конечную точку Y, используя точку пересечения с осью Y и наклон относительно минимальной/максимальной точки по оси X, сопоставляем ее с позициями на экране, подготавливаем ARGB-каналы и рисуем сглаженные сегменты с помощью функции LineAA, зацикленной по ширине. Наконец, мы добавляем жирную метку оси X по центру внизу и метку оси Y, повернутую вертикально на 90 градусов с помощью FontAngleSet  в левом центре, после чего угол сбрасывается. В функции "drawCirclePoint" извлекаем компоненты ARGB, корректируем радиус для сглаживания, выполняем цикл по расширенной области, вычисляем расстояния с помощью MathSqrt, устанавливаем полное или краевое покрытие (затухание на границе), вычисляем окончательный альфа-канал и цвет пикселя, а затем смешиваем ограниченные пиксели с помощью "blendPixelSet" для создания плавных кругов, имитирующих качество CGraphic. При вызове этой функции в базовой функции рендеринга получаем следующий результат.

CANVAS REGRESSION PLOT

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

//+------------------------------------------------------------------+
//| Draw Statistics Panel AS OVERLAY                                 |
//+------------------------------------------------------------------+
void drawStatisticsPanel() {
   int panelX = statsPanelX;                                    //--- Set panel X
   int panelY = HEADER_BAR_HEIGHT + statsPanelY;                //--- Set panel Y
   int panelWidth = statsPanelWidth;                            //--- Set panel width
   int panelHeight = statsPanelHeight;                          //--- Set panel height

   color panelBgColor = LightenColor(themeColor, 0.9);          //--- Compute bg color
   uchar bgAlpha = 153;                                         //--- Set alpha
   uint argbPanelBg = ColorToARGB(panelBgColor, bgAlpha);       //--- Get panel bg ARGB
   uint argbBorder = ColorToARGB(themeColor, 255);              //--- Get border ARGB
   uint argbText = ColorToARGB(clrBlack, 255);                  //--- Get text ARGB

   for (int y = panelY; y <= panelY + panelHeight; y++) {       //--- Loop over rows
      for (int x = panelX; x <= panelX + panelWidth; x++) {     //--- Loop over columns
         blendPixelSet(mainCanvas, x, y, argbPanelBg);          //--- Blend bg pixel
      }
   }

   for (int x = panelX; x <= panelX + panelWidth; x++) {        //--- Draw top border
      blendPixelSet(mainCanvas, x, panelY, argbBorder);         //--- Blend border pixel
   }
   for (int y = panelY; y <= panelY + panelHeight; y++) {       //--- Draw right border
      blendPixelSet(mainCanvas, panelX + panelWidth, y, argbBorder); //--- Blend border pixel
   }
   for (int y = panelY; y <= panelY + panelHeight; y++) {       //--- Draw left border
      blendPixelSet(mainCanvas, panelX, y, argbBorder);         //--- Blend border pixel
   }

   mainCanvas.FontSet("Arial", panelFontSize);                  //--- Set stats font

   int textY = panelY + 8;                                      //--- Set text Y
   int lineSpacing = panelFontSize;                             //--- Set line spacing

   string equationText = StringFormat("Y = %.3f + %.3f * X", regressionIntercept, regressionSlope); //--- Format equation
   mainCanvas.TextOut(panelX + 8, textY, equationText, argbText, TA_LEFT); //--- Draw equation
   textY += lineSpacing;                                        //--- Update Y

   string correlationText = StringFormat("Correlation: %.4f", correlationCoefficient); //--- Format correlation
   mainCanvas.TextOut(panelX + 8, textY, correlationText, argbText, TA_LEFT); //--- Draw correlation
   textY += lineSpacing;                                        //--- Update Y

   string rSquaredText = StringFormat("R-Squared: %.4f", rSquared); //--- Format R-squared
   mainCanvas.TextOut(panelX + 8, textY, rSquaredText, argbText, TA_LEFT); //--- Draw R-squared
   textY += lineSpacing;                                        //--- Update Y

   string dataPointsText = StringFormat("Points: %d", ArraySize(primaryClosePrices)); //--- Format points
   mainCanvas.TextOut(panelX + 8, textY, dataPointsText, argbText, TA_LEFT); //--- Draw points
}

Мы реализуем функцию "drawStatisticsPanel" для наложения полупрозрачной панели, отображающей регрессионные показатели, на объект Canvas, позиционируя ее по входным данным, таким как "statsPanelX", и смещая от высоты заголовка, с фиксированными шириной и высотой. Затем мы осветляем цвет фона темы с помощью "LightenColor", устанавливаем альфа-значение на 153 для тонкости, преобразуем в ARGB и заполняем область панели попиксельно, используя вложенные циклы и "blendPixelSet" для плавной интеграции с существующим контентом.

Для обрамления мы рисуем верхнюю, правую, левую и нижнюю границы, смешивая пиксели границ с цветовой гаммой ARGB в циклах, создавая простой контур без сплошных прямоугольников. Мы устанавливаем шрифт "Arial" равным "panelFontSize", инициализируем текст по оси Y отступами и межстрочным интервалом, заданными размером шрифта, затем форматируем и отображаем уравнение, используя StringFormat и TextOut с выравниванием по левому краю, обновляя значение по оси Y; аналогично для корреляции, коэффициента детерминации R-квадрат и количества точек данных, заданных размером массива. Эта панель компактно предоставляет ключевые статистические данные, такие как "Y = точка пересечения + наклон * X", что повышает интерпретируемость и не перегружает основной график. Для панели легенды использовался аналогичный подход.

//+------------------------------------------------------------------+
//| Draw Legend                                                      |
//+------------------------------------------------------------------+
void drawLegend() {
   int legendX = statsPanelX;                                   //--- Set legend X
   int legendY = HEADER_BAR_HEIGHT + statsPanelY + statsPanelHeight; //--- Set legend Y
   int legendWidth = statsPanelWidth;                           //--- Set legend width
   int legendHeightThis = legendHeight;                         //--- Set legend height

   color legendBgColor = LightenColor(themeColor, 0.9);         //--- Compute bg color
   uchar bgAlpha = 153;                                         //--- Set alpha
   uint argbLegendBg = ColorToARGB(legendBgColor, bgAlpha);     //--- Get legend bg ARGB
   uint argbBorder = ColorToARGB(themeColor, 255);              //--- Get border ARGB
   uint argbText = ColorToARGB(clrBlack, 255);                  //--- Get text ARGB

   for (int y = legendY; y <= legendY + legendHeightThis; y++) { //--- Loop over rows
      for (int x = legendX; x <= legendX + legendWidth; x++) {  //--- Loop over columns
         blendPixelSet(mainCanvas, x, y, argbLegendBg);         //--- Blend bg pixel
      }
   }

   for (int x = legendX; x <= legendX + legendWidth; x++) {     //--- Draw top border
      blendPixelSet(mainCanvas, x, legendY, argbBorder);        //--- Blend border pixel
   }
   for (int y = legendY; y <= legendY + legendHeightThis; y++) { //--- Draw right border
      blendPixelSet(mainCanvas, legendX + legendWidth, y, argbBorder); //--- Blend border pixel
   }
   for (int x = legendX; x <= legendX + legendWidth; x++) {     //--- Draw bottom border
      blendPixelSet(mainCanvas, x, legendY + legendHeightThis, argbBorder); //--- Blend border pixel
   }
   for (int y = legendY; y <= legendY + legendHeightThis; y++) { //--- Draw left border
      blendPixelSet(mainCanvas, legendX, y, argbBorder);        //--- Blend border pixel
   }

   mainCanvas.FontSet("Arial", panelFontSize);                  //--- Set legend font

   int itemY = legendY + 10;                                    //--- Set item Y
   int lineSpacing = panelFontSize;                             //--- Set line spacing

   uint argbRedDot = ColorToARGB(dataPointsColor, 255);         //--- Get red dot ARGB
   drawCirclePoint(legendX + 12, itemY, dataPointSize, argbRedDot); //--- Draw data point
   mainCanvas.TextOut(legendX + 22, itemY - 4, "Data Points", argbText, TA_LEFT); //--- Draw data label
   itemY += lineSpacing;                                        //--- Update Y

   uint argbBlueLine = ColorToARGB(regressionLineColor, 255);   //--- Get blue line ARGB
   for (int i = 0; i < 15; i++) {                               //--- Loop to draw line
      blendPixelSet(mainCanvas, legendX + 7 + i, itemY, argbBlueLine); //--- Blend line pixel
      blendPixelSet(mainCanvas, legendX + 7 + i, itemY + 1, argbBlueLine); //--- Blend below pixel
   }
   mainCanvas.TextOut(legendX + 27, itemY - 4, "Regression Line", argbText, TA_LEFT); //--- Draw line label
}

Мы реализуем функцию "drawLegend" для добавления полупрозрачной наложенной панели под статистикой для визуальных клавиш, размещая её относительно "statsPanelX" и вычисляя Y согласно высоте поля статистики, с соответствующей шириной и высотой легенды поля ввода. Затем мы осветляем цвет фона темы с помощью "LightenColor", устанавливаем альфа-значение на 153, преобразуем в ARGB и заполняем область, используя вложенные циклы с параметром "blendPixelSet" для интеграции; нарисуем верхнюю, правую, нижнюю и левую границы аналогичным образом с использованием темы ARGB, как и на панели статистики.

Мы устанавливаем шрифт "Arial" в значение "panelFontSize", инициализируем элемент Y отступами и межстрочным интервалом, заданными размером шрифта, затем рисуем красный значок точки данных с помощью функции "drawCirclePoint" в скорректированном положении, после чего добавляем метку "Data Points" с выравниванием по левому краю с помощью TextOut, обновляя значение Y. Для представления линии мы создаем короткий синий сегмент, смешивая 15 пикселей по горизонтали с цветом ARGB из регрессионного цвета, включая нижний ряд для толщины, и аналогичным образом добавляем метку "Regression Line", обеспечивая четкие визуальные ориентиры. При вызове этих функций получаем следующий результат.

ADDED STATS AND LEGEND PANEL

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

//+------------------------------------------------------------------+
//| Draw Resize Indicator                                            |
//+------------------------------------------------------------------+
void drawResizeIndicator() {
   uint argbIndicator = ColorToARGB(themeColor, 255);           //--- Get indicator ARGB

   if (hoverResizeMode == RESIZE_CORNER || activeResizeMode == RESIZE_CORNER) { //--- Check corner
      int cornerX = currentWidthPixels - resizeGripSize;        //--- Compute corner X
      int cornerY = currentHeightPixels - resizeGripSize;       //--- Compute corner Y

      mainCanvas.FillRectangle(cornerX, cornerY, currentWidthPixels - 1, currentHeightPixels - 1, argbIndicator); //--- Fill corner

      for (int i = 0; i < 3; i++) {                             //--- Loop for lines
         int offset = i * 3;                                    //--- Compute offset
         mainCanvas.Line(cornerX + offset, currentHeightPixels - 1, 
                                  currentWidthPixels - 1, cornerY + offset, argbIndicator); //--- Draw diagonal
      }
   }

   if (hoverResizeMode == RESIZE_RIGHT_EDGE || activeResizeMode == RESIZE_RIGHT_EDGE) { //--- Check right
      int indicatorY = currentHeightPixels / 2 - 15;            //--- Compute indicator Y
      mainCanvas.FillRectangle(currentWidthPixels - 3, indicatorY, 
                                        currentWidthPixels - 1, indicatorY + 30, argbIndicator); //--- Fill right
   }

   if (hoverResizeMode == RESIZE_BOTTOM_EDGE || activeResizeMode == RESIZE_BOTTOM_EDGE) { //--- Check bottom
      int indicatorX = currentWidthPixels / 2 - 15;             //--- Compute indicator X
      mainCanvas.FillRectangle(indicatorX, currentHeightPixels - 3, 
                                        indicatorX + 30, currentHeightPixels - 1, argbIndicator); //--- Fill bottom
   }
}

//+------------------------------------------------------------------+
//| Check if Mouse is Over Header                                    |
//+------------------------------------------------------------------+
bool isMouseOverHeaderBar(int mouseX, int mouseY) {
   return (mouseX >= currentPositionX && mouseX <= currentPositionX + currentWidthPixels &&
           mouseY >= currentPositionY && mouseY <= currentPositionY + HEADER_BAR_HEIGHT); //--- Return if over header
}

//+------------------------------------------------------------------+
//| Check if Mouse is in Resize Zone                                 |
//+------------------------------------------------------------------+
bool isMouseInResizeZone(int mouseX, int mouseY, ResizeDirection &resizeMode) {
   if (!enableResizing) return false;                           //--- Return false if disabled

   int relativeX = mouseX - currentPositionX;                   //--- Compute relative X
   int relativeY = mouseY - currentPositionY;                   //--- Compute relative Y

   bool nearRightEdge = (relativeX >= currentWidthPixels - resizeGripSize && 
                         relativeX <= currentWidthPixels &&
                         relativeY >= HEADER_BAR_HEIGHT && 
                         relativeY <= currentHeightPixels);     //--- Check right edge

   bool nearBottomEdge = (relativeY >= currentHeightPixels - resizeGripSize && 
                          relativeY <= currentHeightPixels &&
                          relativeX >= 0 && 
                          relativeX <= currentWidthPixels);     //--- Check bottom edge

   bool nearCorner = (relativeX >= currentWidthPixels - resizeGripSize && 
                      relativeX <= currentWidthPixels &&
                      relativeY >= currentHeightPixels - resizeGripSize && 
                      relativeY <= currentHeightPixels);      //--- Check corner

   if (nearCorner) {                                            //--- Set corner
      resizeMode = RESIZE_CORNER;                               //--- Set mode
      return true;                                              //--- Return true
   } else if (nearRightEdge) {                                  //--- Set right
      resizeMode = RESIZE_RIGHT_EDGE;                           //--- Set mode
      return true;                                              //--- Return true
   } else if (nearBottomEdge) {                                 //--- Set bottom
      resizeMode = RESIZE_BOTTOM_EDGE;                          //--- Set mode
      return true;                                              //--- Return true
   }

   resizeMode = NO_RESIZE;                                      //--- Set no resize
   return false;                                                //--- Return false
}

//+------------------------------------------------------------------+
//| Handle Canvas Resizing                                           |
//+------------------------------------------------------------------+
void handleCanvasResize(int mouseX, int mouseY) {
   int deltaX = mouseX - resizeStartX;                          //--- Compute delta X
   int deltaY = mouseY - resizeStartY;                          //--- Compute delta Y

   int newWidth = currentWidthPixels;                           //--- Init new width
   int newHeight = currentHeightPixels;                         //--- Init new height

   if (activeResizeMode == RESIZE_RIGHT_EDGE || activeResizeMode == RESIZE_CORNER) { //--- Check right or corner
      newWidth = MathMax(MIN_CANVAS_WIDTH, resizeInitialWidth + deltaX); //--- Compute new width
   }

   if (activeResizeMode == RESIZE_BOTTOM_EDGE || activeResizeMode == RESIZE_CORNER) { //--- Check bottom or corner
      newHeight = MathMax(MIN_CANVAS_HEIGHT, resizeInitialHeight + deltaY); //--- Compute new height
   }

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

   newWidth = MathMin(newWidth, chartWidth - currentPositionX - 10); //--- Clamp width
   newHeight = MathMin(newHeight, chartHeight - currentPositionY - 10); //--- Clamp height

   if (newWidth != currentWidthPixels || newHeight != currentHeightPixels) { //--- Check changed
      currentWidthPixels = newWidth;                            //--- Update width
      currentHeightPixels = newHeight;                          //--- Update height

      mainCanvas.Resize(currentWidthPixels, currentHeightPixels); //--- Resize canvas
      ObjectSetInteger(0, canvasObjectName, OBJPROP_XSIZE, currentWidthPixels); //--- Set X size
      ObjectSetInteger(0, canvasObjectName, OBJPROP_YSIZE, currentHeightPixels); //--- Set Y size

      renderVisualization();                                     //--- Render again
      ChartRedraw();                                             //--- Redraw chart
   }
}

//+------------------------------------------------------------------+
//| Handle Canvas Dragging                                           |
//+------------------------------------------------------------------+
void handleCanvasDrag(int mouseX, int mouseY) {
   int deltaX = mouseX - dragStartX;                            //--- Compute delta X
   int deltaY = mouseY - dragStartY;                            //--- Compute delta Y

   int newX = canvasStartX + deltaX;                            //--- Compute new X
   int newY = canvasStartY + deltaY;                            //--- Compute new Y

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

   newX = MathMax(0, MathMin(chartWidth - currentWidthPixels, newX)); //--- Clamp X
   newY = MathMax(0, MathMin(chartHeight - currentHeightPixels, newY)); //--- Clamp Y

   currentPositionX = newX;                                     //--- Update X
   currentPositionY = newY;                                     //--- Update Y

   ObjectSetInteger(0, canvasObjectName, OBJPROP_XDISTANCE, currentPositionX); //--- Set X distance
   ObjectSetInteger(0, canvasObjectName, OBJPROP_YDISTANCE, currentPositionY); //--- Set Y distance

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

//+------------------------------------------------------------------+
//| Interpolate Between Two Colors                                   |
//+------------------------------------------------------------------+
color InterpolateColors(color startColor, color endColor, double factor) {
   uchar r1 = (uchar)((startColor >> 16) & 0xFF);              //--- Extract start red
   uchar g1 = (uchar)((startColor >> 8) & 0xFF);               //--- Extract start green
   uchar b1 = (uchar)(startColor & 0xFF);                      //--- Extract start blue

   uchar r2 = (uchar)((endColor >> 16) & 0xFF);                //--- Extract end red
   uchar g2 = (uchar)((endColor >> 8) & 0xFF);                 //--- Extract end green
   uchar b2 = (uchar)(endColor & 0xFF);                        //--- Extract end blue

   uchar r = (uchar)(r1 + factor * (r2 - r1));                 //--- Interpolate red
   uchar g = (uchar)(g1 + factor * (g2 - g1));                 //--- Interpolate green
   uchar b = (uchar)(b1 + factor * (b2 - b1));                 //--- Interpolate blue

   return (r << 16) | (g << 8) | b;                            //--- Return interpolated color
}

//+------------------------------------------------------------------+
//| Blend pixel with proper alpha blending                           |
//+------------------------------------------------------------------+
void blendPixelSet(CCanvas &canvas, int x, int y, uint src) {
   if (x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return; //--- Return if out of bounds
   
   uint dst = canvas.PixelGet(x, y);                            //--- Get destination pixel
   
   double sa = ((src >> 24) & 0xFF) / 255.0;                    //--- Compute source alpha
   double sr = ((src >> 16) & 0xFF) / 255.0;                    //--- Compute source red
   double sg = ((src >> 8) & 0xFF) / 255.0;                     //--- Compute source green
   double sb = (src & 0xFF) / 255.0;                            //--- Compute source blue
   
   double da = ((dst >> 24) & 0xFF) / 255.0;                    //--- Compute dest alpha
   double dr = ((dst >> 16) & 0xFF) / 255.0;                    //--- Compute dest red
   double dg = ((dst >> 8) & 0xFF) / 255.0;                     //--- Compute dest green
   double db = (dst & 0xFF) / 255.0;                            //--- Compute dest blue
   
   double out_a = sa + da * (1 - sa);                           //--- Compute out alpha
   if (out_a == 0) {                                            //--- Check transparent
      canvas.PixelSet(x, y, 0);                                 //--- Set transparent
      return;                                                   //--- Return
   }
   
   double out_r = (sr * sa + dr * da * (1 - sa)) / out_a;       //--- Compute out red
   double out_g = (sg * sa + dg * da * (1 - sa)) / out_a;       //--- Compute out green
   double out_b = (sb * sa + db * da * (1 - sa)) / out_a;       //--- Compute out blue
   
   uchar oa = (uchar)(out_a * 255 + 0.5);                       //--- Compute final alpha
   uchar or_ = (uchar)(out_r * 255 + 0.5);                      //--- Compute final red
   uchar og = (uchar)(out_g * 255 + 0.5);                       //--- Compute final green
   uchar ob = (uchar)(out_b * 255 + 0.5);                       //--- Compute final blue
   
   uint out_col = ((uint)oa << 24) | ((uint)or_ << 16) | ((uint)og << 8) | (uint)ob; //--- Compose color
   canvas.PixelSet(x, y, out_col);                              //--- Set blended pixel
}

Сначала мы реализуем функцию "drawResizeIndicator", чтобы визуально сигнализировать об изменении размера элементов на объекте Canvas, преобразуя цвет темы в ARGB. Затем, для углового режима (при наведении курсора или в активном состоянии), мы заполняем небольшой квадрат в правом нижнем углу с помощью FillRectangle и рисуем три диагональные линии, смещенные на 3 пикселя каждая, используя параметр "Line" для создания эффекта захвата. Для правого края мы заполняем вертикальный прямоугольник, центрированный на краю, с помощью функции "FillRectangle". Аналогично, для нижней части — горизонтальный прямоугольник, обеспечивая интуитивно понятную обратную связь без лишнего беспорядка. Далее, функция "isMouseOverHeaderBar" проверяет, находится ли курсор мыши в пределах заголовка, и возвращает значение true, если перемещение курсора возможно.

Для определения областей изменения размера функция "isMouseInResizeZone" проверяет, включено ли изменение размера, вычисляет относительные координаты и определяет область вблизи правого, нижнего или углового положения на основе параметра "resizeGripSize", обновляя режим, например, "RESIZE_CORNER", и возвращая true, если совпадение найдено, в противном случае - "NO_RESIZE" и возвращая false. В функции "handleCanvasResize" мы вычисляем дельты от начала, корректируем новую ширину/высоту для каждого активного режима (справа/снизу/в углу) с помощью MathMax для минимумов, ограничиваем размер графика за вычетом полей с помощью ChartGetInteger, и, если они изменились, обновляем глобальные переменные, изменяем размер объекта Canvas с помощью Resize, устанавливаем размеры объектов с помощью ObjectSetInteger, перерисовываем визуализацию и перерисовываем график.

Для перетаскивания "handleCanvasDrag" высчитывает дельты и новые положения. Эта функция ограничивает значения в пределах границ графика, заданных функцией "ChartGetInteger", чтобы предотвратить переполнение за границы. Затем мы обновляем глобальные переменные и устанавливаем расстояния до объектов с помощью функции "ObjectSetInteger", после чего выполняем перерисовку графика. Мы определяем параметр "InterpolateColors" для смешивания двух цветов. Он извлекает компоненты RGB, выполняет линейную интерполяцию каждого канала и объединяет их для получения градиентов. Наконец, "blendPixelSet" позволяет выполнять альфа-смешивание для наложений, проверку границ и извлечение исходных/целевых компонентов. Вычисляет выходной альфа-канал и предварительно умноженный RGB, ограничивает значение беззнаковыми символами, компонует цвет и устанавливает его с помощью метода PixelSet. Это позволяет плавно комбинировать изображения, например, в панелях. Для обработки индикаторов изменения размера мы сначала вызываем функцию в основной процедуре рендеринга.

//+------------------------------------------------------------------+
//| Render Regression Visualization                                  |
//+------------------------------------------------------------------+
void renderVisualization() {
   mainCanvas.Erase(0);                                         //--- Erase canvas

   if (enableBackgroundFill) {                                  //--- Check background fill
      drawGradientBackground();                                 //--- Draw gradient background
   }

   drawCanvasBorder();                                          //--- Draw border
   drawHeaderBar();                                             //--- Draw header bar
   drawRegressionPlot();                                        //--- Draw plot

   if (showStatistics) {                                        //--- Check show statistics
      drawStatisticsPanel();                                    //--- Draw stats panel
      drawLegend();                                             //--- Draw legend
   }

   if (isHoveringResizeZone && enableResizing) {                //--- Check resize hover
      drawResizeIndicator();                                    //--- Draw resize indicator
   }

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

После запуска программы мы получаем следующий результат.

RESIZE INDICATORS

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

//+------------------------------------------------------------------+
//| Chart Event Handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if (id == CHARTEVENT_MOUSE_MOVE) {                           //--- Check mouse move
      int mouseX = (int)lparam;                                 //--- Set mouse X
      int mouseY = (int)dparam;                                 //--- Set mouse Y
      int mouseState = (int)sparam;                             //--- Set mouse state

      bool previousHoverState = isHoveringCanvas;               //--- Store prev canvas hover
      bool previousHeaderHoverState = isHoveringHeader;         //--- Store prev header hover
      bool previousResizeHoverState = isHoveringResizeZone;     //--- Store prev resize hover

      isHoveringCanvas = (mouseX >= currentPositionX && mouseX <= currentPositionX + currentWidthPixels &&
                         mouseY >= currentPositionY && mouseY <= currentPositionY + currentHeightPixels); //--- Update canvas hover

      isHoveringHeader = isMouseOverHeaderBar(mouseX, mouseY);  //--- Update header hover

      isHoveringResizeZone = isMouseInResizeZone(mouseX, mouseY, hoverResizeMode); //--- Update resize hover

      bool needRedraw = (previousHoverState != isHoveringCanvas || 
                        previousHeaderHoverState != isHoveringHeader ||
                        previousResizeHoverState != isHoveringResizeZone); //--- Check if redraw needed

      if (mouseState == 1 && previousMouseButtonState == 0) {   //--- Check button press
         if (enableDragging && isHoveringHeader && !isHoveringResizeZone) { //--- Check drag start
            isDraggingCanvas = true;                            //--- Set dragging
            dragStartX = mouseX;                               //--- Set start X
            dragStartY = mouseY;                               //--- Set start Y
            canvasStartX = currentPositionX;                   //--- Set canvas X
            canvasStartY = currentPositionY;                   //--- Set canvas Y
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);     //--- Disable scroll
            needRedraw = true;                                 //--- Set redraw
         } else if (isHoveringResizeZone) {                    //--- Check resize start
            isResizingCanvas = true;                           //--- Set resizing
            activeResizeMode = hoverResizeMode;                //--- Set active mode
            resizeStartX = mouseX;                             //--- Set start X
            resizeStartY = mouseY;                             //--- Set start Y
            resizeInitialWidth = currentWidthPixels;           //--- Set initial width
            resizeInitialHeight = currentHeightPixels;         //--- Set initial height
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);     //--- Disable scroll
            needRedraw = true;                                 //--- Set redraw
         }
      } 
      else if (mouseState == 1 && previousMouseButtonState == 1) { //--- Check drag
         if (isDraggingCanvas) {                               //--- Handle drag
            handleCanvasDrag(mouseX, mouseY);                  //--- Handle drag
         } else if (isResizingCanvas) {                        //--- Handle resize
            handleCanvasResize(mouseX, mouseY);                //--- Handle resize
         }
      } 
      else if (mouseState == 0 && previousMouseButtonState == 1) { //--- Check button release
         if (isDraggingCanvas || isResizingCanvas) {           //--- Check active
            isDraggingCanvas = false;                          //--- Reset dragging
            isResizingCanvas = false;                          //--- Reset resizing
            activeResizeMode = NO_RESIZE;                      //--- Reset mode
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);      //--- Enable scroll
            needRedraw = true;                                 //--- Set redraw
         }
      }

      if (needRedraw) {                                         //--- Check redraw
         renderVisualization();                                 //--- Render
         ChartRedraw();                                         //--- Redraw chart
      }

      lastMouseX = mouseX;                                      //--- Update last X
      lastMouseY = mouseY;                                      //--- Update last Y
      previousMouseButtonState = mouseState;                    //--- Update prev state
   }
}

Для управления интерактивными функциями, такими как перетаскивание и изменение размера, мы используем обработчик событий OnChartEvent, сначала проверяя, является ли событие CHARTEVENT_MOUSE_MOVE, а затем извлекая координаты мыши и состояние из параметров. Далее мы сохраняем предыдущие состояния наведения курсора и обновляем флаги для наведения курсора на объект Canvas (полные границы), заголовка с помощью параметра "isMouseOverHeaderBar" и зоны изменения размера с помощью параметра "isMouseInResizeZone", который устанавливает "hoverResizeMode", определяя, требуется ли перерисовка после изменений.

При нажатии кнопки мыши (состояние 1, prev 0), если перетаскивание включено и наведен курсор на заголовок без изменения размера, устанавливаем "isDraggingCanvas", начинается захват, отключаем прокрутку с помощью ChartSetInteger и помечаем флаг перерисовки; если изменяется размер зоны, устанавливаем "isResizingCanvas", активный режим, initials и отключаем прокрутку. В удерживаемом состоянии (состояние 1, prev 1) вызывается функция "handleCanvasDrag", если происходит перетаскивание, или "handleCanvasResize", если изменяется размер. После отпускания (состояние 0, prev 1) сбрасывам флаги и режим, включаем прокрутку, перерисовку флага. При необходимости перерисовки вызываем "renderVisualization" и ChartRedraw. Наконец, для обеспечения непрерывности обновляем последние положения курсора мыши и предыдущее состояние. Также нам понадобится удалить объект Canvas после того, как он более не нужен.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   mainCanvas.Destroy();                                        //--- Destroy canvas
   ChartRedraw();                                               //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   static datetime lastBarTimestamp = 0;                        //--- Store last bar time
   datetime currentBarTimestamp = iTime(_Symbol, chartTimeframe, 0); //--- Get current bar time

   if (currentBarTimestamp > lastBarTimestamp) {                //--- Check new bar
      if (loadSymbolClosePrices()) {                            //--- Reload prices
         if (computeLinearRegression()) {                       //--- Recalculate regression
            renderVisualization();                              //--- Update visualization
            ChartRedraw();                                      //--- Redraw chart
         }
      }
      lastBarTimestamp = currentBarTimestamp;                   //--- Update last time
   }
}

В обработчике OnDeinit мы выполняем очистку, уничтожая основной объект Canvas с помощью Destroy для освобождения ресурсов. Затем перерисовываем график с помощью "ChartRedraw", чтобы удалить все остатки визуальных элементов. В обработчике OnTick мы используем статическую переменную "lastBarTimestamp" для отслеживания времени предыдущего бара, сравниваем его со временем текущего бара из iTime по данному инструменту и таймфрейму, и если сформировался новый бар, перезагружаем цены с помощью функции "loadSymbolClosePrices", пересчитываем регрессию с помощью функции "computeLinearRegression", перерисовываем визуализацию и график. Затем обновляем временную метку для следующего тика. Это знаменует собой полное достижение наших целей. Теперь остаётся проверить работоспособность системы, что и рассматривается в следующем разделе.


Тестирование на истории

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

BACKTEST GIF


Заключение

В заключение отметим, что нами создан графический инструмент на основе Canvas в MQL5 для статистического корреляционного и линейного регрессионного анализа между двумя символами с возможностью перетаскивания и изменения размера. Мы включили ALGLIB для регрессионных расчетов, динамические метки тиков, точки данных и панель статистики, отображающую наклон, пересечение, корреляцию и R-квадрат. Эта интерактивная визуализация помогает лучше понять суть парной торговли, поддерживая настраиваемые темы, границы и обновление новых баров в режиме реального времени В следующей части мы добавим на график режим темы киберпанка и живую анимацию, чтобы сделать его современным и интуитивно понятным. Следите за обновлениями!

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

Прикрепленные файлы |
От начального до среднего уровня: Объекты (II) От начального до среднего уровня: Объекты (II)
В сегодняшней статье мы рассмотрим, как простым способом управлять некоторыми свойствами объектов с помощью кода. Мы также рассмотрим, как с помощью специального приложения можно разместить более одного объекта на одном графике. Кроме того, мы начнём разбираться в важности присвоения краткого названия любому индикатору, который мы собираемся внедрить.
Нейросети в трейдинге: Унифицированное смешивание признаков для торговых решений (Окончание) Нейросети в трейдинге: Унифицированное смешивание признаков для торговых решений (Окончание)
В статье представлена завершающая часть адаптации фреймворка UniMixer средствами MQL5, включая построение SiameseNorm и объекта верхнего уровня CNeuronUniMixerBlock. Описана полная цепочка обработки рыночных данных от токенизации и контекстного выделения до сценарного моделирования и смешивания признаков. Приведены результаты тестирования на исторических данных EURUSD, демонстрирующие умеренную прибыль.
Разработка инструментария для анализа Price Action (Часть 28): Инструмент для торговли пробоя диапазона открытия Разработка инструментария для анализа Price Action (Часть 28): Инструмент для торговли пробоя диапазона открытия
В начале каждой торговой сессии направление рынка часто становится понятным только после того, как цена выходит за пределы диапазона открытия. В этой статье мы разберем, как создать советник на MQL5, который автоматически обнаруживает и анализирует пробои диапазона открытия, предоставляя своевременные сигналы на основе данных для более уверенных внутридневных входов.
Торговые инструменты на MQL5 (Часть 19): Создание интерактивной палитры инструментов графической разметки Торговые инструменты на MQL5 (Часть 19): Создание интерактивной палитры инструментов графической разметки
В этой статье мы создадим интерактивную палитру инструментов в MQL5 для рисования графиков с возможностью перетаскивания, изменения размера панелей и переключения тем. Мы добавим кнопки для таких инструментов, как перекрестие, линии трендов, прямые, прямоугольники, числа Фибоначчи, текст и стрелки, обрабатывающих события мыши для активации и получения указаний. Эта система улучшает анализ торговли благодаря настраиваемому пользовательскому интерфейсу, поддерживающему взаимодействие с графиками в режиме реального времени