Торговые инструменты на MQL5 (Часть 20): Построение графиков на Canvas с использованием статистической корреляции и регрессионного анализа
Введение
В своей предыдущей статье (Часть 19) мы разработали интерактивную палитру инструментов в MetaQuotes Language 5 (MQL5) для рисования графиков, включающую в себя перетаскиваемые панели, изменение размера, переключение тем и кнопки для различных инструментов анализа. В Части 20 мы создаем графический инструмент на основе Canvas для статистической корреляции и линейной регрессии между двумя переменными с перетаскиваемыми и масштабируемыми элементами, динамическими тиками и статистическим отображением. Эта визуализация предоставляет сведения о парной торговле с помощью линий регрессии, точек данных и таких показателей, как наклон и R-квадрат. В статье рассмотрим следующие темы:
- Изучение статистической корреляции и регрессии на графиках Canvas
- Реализация средствами MQL5
- Тестирование на истории
- Заключение
В итоге у вас будет интерактивный регрессионный график, готовый к проведению анализа рынка. Перейдём к реализации!
Изучение статистической корреляции и регрессии на графиках Canvas
Статистическая корреляция измеряет силу и направление связи между двумя переменными, такими как цены символов, используя такие показатели, как коэффициент Пирсона, варьирующийся от -1 (обратная корреляция) до 1 (прямая корреляция), в то время как линейная регрессия строит прямую линию по точкам данных для прогнозирования тенденций по наклону и свободному члену. На графиках Canvas эти данные визуализируются с помощью точек рассеяния для корреляций и линий регрессии для прогнозов, а коэффициент детерминации R² используется для оценки качества построения, что помогает нам выявлять парные зависимости или расхождения. Это графическое представление на перемещаемых объектах Canvas позволяет интерактивно изучать рыночные взаимосвязи, а панели статистики обеспечивают быстрый доступ к аналитической информации. Наш план состоит в том, чтобы загрузить данные символов, вычислить регрессию с использованием ALGLIB, отобразить графики с динамическими тиками и сглаженными точками/линиями, а также вывести статистические данные, такие как наклон и коэффициент детерминации R², на графических накладках. Вкратце, ниже представлено наглядное представление наших целей.

Реализация средствами 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 }
Это дает следующий результат.

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

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

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

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

Заключение
В заключение отметим, что нами создан графический инструмент на основе Canvas в MQL5 для статистического корреляционного и линейного регрессионного анализа между двумя символами с возможностью перетаскивания и изменения размера. Мы включили ALGLIB для регрессионных расчетов, динамические метки тиков, точки данных и панель статистики, отображающую наклон, пересечение, корреляцию и R-квадрат. Эта интерактивная визуализация помогает лучше понять суть парной торговли, поддерживая настраиваемые темы, границы и обновление новых баров в режиме реального времени В следующей части мы добавим на график режим темы киберпанка и живую анимацию, чтобы сделать его современным и интуитивно понятным. Следите за обновлениями!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21303
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От начального до среднего уровня: Объекты (II)
Нейросети в трейдинге: Унифицированное смешивание признаков для торговых решений (Окончание)
Разработка инструментария для анализа Price Action (Часть 28): Инструмент для торговли пробоя диапазона открытия
Торговые инструменты на MQL5 (Часть 19): Создание интерактивной палитры инструментов графической разметки
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования