English
preview
Торговые инструменты MQL5 (Часть 21): Добавление темы в стиле киберпанк в графики регрессии

Торговые инструменты MQL5 (Часть 21): Добавление темы в стиле киберпанк в графики регрессии

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

Введение

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

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

  1. Создание темы в стиле киберпанк для улучшенной визуализации регрессии
  2. Реализация средствами MQL5
  3. Тестирование на истории
  4. Заключение

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


Создание темы в стиле киберпанк для улучшенной визуализации регрессии

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

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

CYBERPUNK THEMING GIF


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

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

Расширение входных данных и глобальных переменных для управления темами

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

//+------------------------------------------------------------------+
//|  Canvas Graphing PART 2 - Statistical Regression (Cyberpunk).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
#property description "Dual Theme Regression: Standard & Cyberpunk Modes"

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "=== CYBERPUNK MODE SETTINGS ==="
input double             glowIntensity = 0.8;           // Glow Intensity (0-1)

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
bool isCyberpunkThemeEnabled = false;                   // Initialize cyberpunk theme flag
int themeToggleButtonX = 0;                             // Initialize theme button X
int themeToggleButtonY = 0;                             // Initialize theme button Y
const int THEME_TOGGLE_BUTTON_SIZE = 25;                // Set theme toggle button size

const color CYBER_PRIMARY_COLOR = C'0,191,255';         // Set cyber primary color
const color CYBER_SECONDARY_COLOR = C'255,0,255';       // Set cyber secondary color
const color CYBER_DATA_POINTS_COLOR = C'255,50,150';    // Set cyber data points color
const color CYBER_REGRESSION_COLOR = C'0,150,255';      // Set cyber regression color

int currentAnimationFrame = 0;                          // Initialize animation frame

Начнем с добавления новой группы ввода "=== CYBERPUNK MODE SETTINGS ===" со значением "glowIntensity" типа double, изменяющимся от 0 до 1, по умолчанию равным 0,8, для управления интенсивностью эффектов свечения в киберпанк-теме. Далее мы объявляем глобальные переменные для управления темой: "isCyberpunkThemeEnabled" инициализируется значением false в качестве флага для переключения режимов; "themeToggleButtonX" и "themeToggleButtonY" устанавливаются в 0 для позиционирования кнопки; константа "THEME_TOGGLE_BUTTON_SIZE" имеет размер 25 пикселей. Мы определяем специфические для киберпанка цвета, используя обозначение C'...': "CYBER_PRIMARY_COLOR" в ярко-голубом цвете C'0,191,255', "CYBER_SECONDARY_COLOR" в пурпурном цвете C'255,0,255', "CYBER_DATA_POINTS_COLOR" в розовом цвете C'255,50,150' и "CYBER_REGRESSION_COLOR" в синем цвете C'0,150,255', создавая яркие тематические палитры.

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

Реализация утилиты смешивания цветов

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

//+------------------------------------------------------------------+
//| Blend colors                                                     |
//+------------------------------------------------------------------+
color BlendColors(color color1, color color2, double ratio)
  {
   //--- Extract color1 red
   uchar r1 = (uchar)((color1 >> 16) & 0xFF);
   //--- Extract color1 green
   uchar g1 = (uchar)((color1 >> 8) & 0xFF);
   //--- Extract color1 blue
   uchar b1 = (uchar)(color1 & 0xFF);
   
   //--- Extract color2 red
   uchar r2 = (uchar)((color2 >> 16) & 0xFF);
   //--- Extract color2 green
   uchar g2 = (uchar)((color2 >> 8) & 0xFF);
   //--- Extract color2 blue
   uchar b2 = (uchar)(color2 & 0xFF);
   
   //--- Blend red
   uchar r = (uchar)(r1 * (1.0 - ratio) + r2 * ratio);
   //--- Blend green
   uchar g = (uchar)(g1 * (1.0 - ratio) + g2 * ratio);
   //--- Blend blue
   uchar b = (uchar)(b1 * (1.0 - ratio) + b2 * ratio);
   
   //--- Return blended color
   return (r << 16) | (g << 8) | b;
  }

Здесь мы реализуем функцию "BlendColors" для создания плавных цветовых переходов для получения киберпанк-эффектов, извлекая компоненты RGB из двух входных цветов с помощью побитовых сдвигов, затем смешивая каждый канал с помощью (1 - коэффициент) для первого и коэффициента для второго и повторно объединяя в новое значение цвета для динамических градиентов или анимации. Представьте это себе как волшебный миксер для смешивания цветов. Вы даете ему два цвета и говорите: "Смешай их в таком соотношении" (соотношение примерно 0,5 на половину). Получается новый цвет. В киберрежиме мы будем использовать его для создания плавных светящихся эффектов, например, смешивания синего и розового цветов для получения пурпурных огней. Без него цвета будут казаться малоконтрастными; с ним же всё будет выглядеть более гармонично и профессионально. Далее мы добавим кнопку переключения тем в заголовок, чтобы она позволяла нам легко переключаться между темами.

Добавление кнопки переключения темы

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

//--- Adjust line drawing for centering in drawRegressionPlot
//--- Draw line
mainCanvas.LineAA(lineStartScreenX, lineStartScreenY + w - regressionLineWidth/2,
  lineEndScreenX, lineEndScreenY + w - regressionLineWidth/2, argbLine);

//+------------------------------------------------------------------+
//| Draw theme toggle button                                         |
//+------------------------------------------------------------------+
void drawThemeToggleButton()
  {
   //--- Compute button X
   themeToggleButtonX = currentWidthPixels - THEME_TOGGLE_BUTTON_SIZE - 8;
   //--- Compute button Y
   themeToggleButtonY = (HEADER_BAR_HEIGHT - THEME_TOGGLE_BUTTON_SIZE) / 2;
   
   //--- Declare button color
   color buttonColor;
   //--- Check cyber mode
   if (isCyberpunkThemeEnabled)
     {
      //--- Set cyber color
      buttonColor = isHoveringThemeButton ? LightenColor(CYBER_PRIMARY_COLOR, 0.3) : CYBER_PRIMARY_COLOR;
     }
   else
     {
      //--- Set standard color
      buttonColor = isHoveringThemeButton ? LightenColor(themeColor, 0.3) : themeColor;
     }
   
   //--- Get button ARGB
   uint argbButton = ColorToARGB(buttonColor, 200);
   //--- Get border ARGB
   uint argbBorder = ColorToARGB(isCyberpunkThemeEnabled ? CYBER_SECONDARY_COLOR : themeColor, 255);
   
   //--- Fill button
   mainCanvas.FillRectangle(themeToggleButtonX, themeToggleButtonY,
     themeToggleButtonX + THEME_TOGGLE_BUTTON_SIZE,
     themeToggleButtonY + THEME_TOGGLE_BUTTON_SIZE, argbButton);
   
   //--- Draw border
   mainCanvas.Rectangle(themeToggleButtonX, themeToggleButtonY,
     themeToggleButtonX + THEME_TOGGLE_BUTTON_SIZE,
     themeToggleButtonY + THEME_TOGGLE_BUTTON_SIZE, argbBorder);
   
   //--- Set icon font
   mainCanvas.FontSet("Arial Black", 16);
   //--- Get text ARGB
   uint argbText = ColorToARGB(clrWhite, 255);
   //--- Set icon text
   string iconText = isCyberpunkThemeEnabled ? "◆" : "●";
   //--- Draw icon
   mainCanvas.TextOut(themeToggleButtonX + THEME_TOGGLE_BUTTON_SIZE/2, themeToggleButtonY + 3,
     iconText, argbText, TA_CENTER);
  }

Сначала мы изменим функцию 'drawRegressionPlot' таким образом, чтобы линия регрессии была центрирована по вертикали. Это делается путем регулировки положения по оси Y в цикле по ширине. Вычтем половину ширины линии 'regression LineWidth/2' из начальной и конечной координат по оси Y на экране. Это обеспечивает сбалансированную толщину по всей рассчитанной траектории при рисовании с помощью функции 'LineAA'. Раньше толстая линия могла выглядеть смещенной от центра, как кривобокая дорога. Вычитание половины ширины идеально центрирует её. Это небольшое исправление, но оно придает линии аккуратный и профессиональный вид, как будто вы поправляете картину на стене.

Далее нам предстоит реализовать функцию 'drawThemeToggleButton'. Эта функция отображает кнопку переключения в заголовке для переключения режимов. Она вычисляет положение кнопки переключения как нижнее правое с отступом и выбирает цвет в зависимости от темы и состояния наведения курсора. Для придания динамичности используется 'LightenColor.' Функция заполняет прямоугольник полупрозрачным цветом ARGB, рисует границу цветом, соответствующим теме, и устанавливает жирный шрифт "Arial Black". Затем используя метод TextOut можно центрировать квадратную иконку для киберпанка или круглую иконку для стандартного стиля белым цветом ARGB. Как и раньше, можно использовать иконки из символов Wingdings. Однако мы выбрали его из-за простоты и технологичности оформления. Далее мы создаем визуализацию темы киберпанк. Давайте начнем с наложения основного слоя Canvas.

Рендеринг фона и границ темы киберпанк

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

//+------------------------------------------------------------------+
//| Draw cyberpunk background                                        |
//+------------------------------------------------------------------+
void drawCyberpunkBackground()
  {
   //--- Set top color
   color topColor = C'10,10,25';
   //--- Set bottom color
   color bottomColor = C'20,5,30';
   
   //--- Compute alpha
   uchar alphaChannel = (uchar)(255 * backgroundOpacityLevel);
   
   //--- Loop over rows
   for (int y = HEADER_BAR_HEIGHT; y < currentHeightPixels; y++)
     {
      //--- Compute factor
      double factor = (double)(y - HEADER_BAR_HEIGHT) / (currentHeightPixels - HEADER_BAR_HEIGHT);
      //--- Blend row color
      color rowColor = BlendColors(topColor, bottomColor, factor);
      //--- Get ARGB
      uint argbColor = ColorToARGB(rowColor, alphaChannel);

      //--- Loop over columns
      for (int x = 0; x < currentWidthPixels; x++)
        {
         //--- Check for star
         if (MathRand() % 800 == 0)
           {
            //--- Compute brightness
            uchar brightness = (uchar)(100 + MathRand() % 155);
            //--- Get star color
            uint starColor = ColorToARGB(C'255,255,255', brightness);
            //--- Blend star
            blendPixelSet(mainCanvas, x, y, starColor);
           }
         else
           {
            //--- Blend normal
            blendPixelSet(mainCanvas, x, y, argbColor);
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Draw holographic border                                          |
//+------------------------------------------------------------------+
void drawHolographicBorder()
  {
   //--- Compute phase
   double phase = (currentAnimationFrame % 360) * M_PI / 180.0;
   //--- Compute glow factor
   double glowFactor = (MathSin(phase) + 1.0) * 0.5;
   
   //--- Blend border color
   color borderColor = BlendColors(CYBER_PRIMARY_COLOR, CYBER_SECONDARY_COLOR, glowFactor);
   
   //--- Loop for glow layers
   for (int i = 0; i < 3; i++)
     {
      //--- Compute alpha
      uchar alpha = (uchar)(80 * glowIntensity * (3 - i) / 3.0);
      //--- Get glow color
      uint glowColor = ColorToARGB(borderColor, alpha);
      
      //--- Draw top glow
      for (int x = i; x < currentWidthPixels - i; x++)
        {
         //--- Blend pixel
         blendPixelSet(mainCanvas, x, i, glowColor);
        }
      //--- Draw bottom glow
      for (int x = i; x < currentWidthPixels - i; x++)
        {
         //--- Blend pixel
         blendPixelSet(mainCanvas, x, currentHeightPixels - 1 - i, glowColor);
        }
      //--- Draw left glow
      for (int y = i; y < currentHeightPixels - i; y++)
        {
         //--- Blend pixel
         blendPixelSet(mainCanvas, i, y, glowColor);
        }
      //--- Draw right glow
      for (int y = i; y < currentHeightPixels - i; y++)
        {
         //--- Blend pixel
         blendPixelSet(mainCanvas, currentWidthPixels - 1 - i, y, glowColor);
        }
     }
   
   //--- Get border ARGB
   uint argbBorder = ColorToARGB(borderColor, 255);
   //--- Draw outer border
   mainCanvas.Rectangle(0, 0, currentWidthPixels - 1, currentHeightPixels - 1, argbBorder);
   //--- Draw inner border
   mainCanvas.Rectangle(1, 1, currentWidthPixels - 2, currentHeightPixels - 2, argbBorder);
   
   //--- Draw corner accents
   drawCornerAccents(borderColor);
  }

//+------------------------------------------------------------------+
//| Draw corner accents                                              |
//+------------------------------------------------------------------+
void drawCornerAccents(color accentColor)
  {
   //--- Get accent ARGB
   uint argbAccent = ColorToARGB(accentColor, 255);
   //--- Set accent size
   int accentSize = 15;
   
   //--- Loop for top-left
   for (int i = 0; i < 3; i++)
     {
      //--- Draw horizontal
      mainCanvas.Line(5, 5 + i, accentSize, 5 + i, argbAccent);
      //--- Draw vertical
      mainCanvas.Line(5 + i, 5, 5 + i, accentSize, argbAccent);
     }
   
   //--- Loop for top-right
   for (int i = 0; i < 3; i++)
     {
      //--- Draw horizontal
      mainCanvas.Line(currentWidthPixels - accentSize - 1, 5 + i, currentWidthPixels - 6, 5 + i, argbAccent);
      //--- Draw vertical
      mainCanvas.Line(currentWidthPixels - 6 - i, 5, currentWidthPixels - 6 - i, accentSize, argbAccent);
     }
   
   //--- Loop for bottom-left
   for (int i = 0; i < 3; i++)
     {
      //--- Draw horizontal
      mainCanvas.Line(5, currentHeightPixels - 6 - i, accentSize, currentHeightPixels - 6 - i, argbAccent);
      //--- Draw vertical
      mainCanvas.Line(5 + i, currentHeightPixels - accentSize - 1, 5 + i, currentHeightPixels - 6, argbAccent);
     }
   
   //--- Loop for bottom-right
   for (int i = 0; i < 3; i++)
     {
      //--- Draw horizontal
      mainCanvas.Line(currentWidthPixels - accentSize - 1, currentHeightPixels - 6 - i, currentWidthPixels - 6, currentHeightPixels - 6 - i, argbAccent);
      //--- Draw vertical
      mainCanvas.Line(currentWidthPixels - 6 - i, currentHeightPixels - accentSize - 1, currentWidthPixels - 6 - i, currentHeightPixels - 6, argbAccent);
     }
  }

Для фрейма в стиле киберпанк мы реализуем функцию "drawCyberpunkBackground", чтобы создать темный звездный градиент для темы киберпанк, установив вверху темно-сине-черный цвет C'10,10,25', а внизу - пурпурно-черный C'20,5,30', вычисляя альфа по непрозрачности, затем циклом перебирая ряды, чтобы смешать цвета с помощью "BlendColors", и случайным образом добавляем белые звезды с помощью MathRand % 800 == 0 с различной яркостью, смешивая все с помощью "blendPixelSet" для создания космического эффекта.

Затем мы создаем "drawHolographicBorder" для анимированных светящихся краев, вычисляем фазу на основе синуса из "currentAnimationFrame" для пульсации, смешиваем основные и дополнительные киберцвета для динамической границы, затем накладываем слои свечения с уменьшением интенсивности альфа-сигнала в циклах для верхнего / нижнего / левого / правого края, рисуем основные границы с помощью Rectangle и вызываем "drawCornerAccents" для получения улучшений. В функции "drawCornerAccents" мы преобразуем акцентный цвет в ARGB, устанавливаем размер равным 15 и трижды повторяем цикл, чтобы нарисовать короткие горизонтальные и вертикальные линии в каждом углу (верхний левый, верхний правый, нижний левый, нижний правый) с помощью Line, формируя акценты в виде скобок для технологичной рамки. Теперь нам нужно будет нарисовать футуристический заголовок.

Рисование футуристического заголовка

В довершение всего мы создаем градиентный заголовок со светящимся текстом и анимационными эффектами.

//+------------------------------------------------------------------+
//| Draw futuristic header                                           |
//+------------------------------------------------------------------+
void drawFuturisticHeader()
  {
   //--- Compute phase
   double phase = (currentAnimationFrame % 360) * M_PI / 180.0;
   //--- Compute anim factor
   double animFactor = (MathSin(phase) + 1.0) * 0.5;
   
   //--- Loop over header rows
   for (int y = 0; y < HEADER_BAR_HEIGHT; y++)
     {
      //--- Compute grad factor
      double gradFactor = (double)y / HEADER_BAR_HEIGHT;
      
      //--- Declare header color
      color headerColor;
      //--- Check dragging
      if (isDraggingCanvas)
        {
         //--- Blend dragged
         headerColor = BlendColors(DarkenColor(CYBER_PRIMARY_COLOR, 0.7), DarkenColor(CYBER_SECONDARY_COLOR, 0.7), gradFactor);
        }
      //--- Check hovering
      else if (isHoveringHeader)
        {
         //--- Blend hovered
         headerColor = BlendColors(DarkenColor(CYBER_PRIMARY_COLOR, 0.5), DarkenColor(CYBER_SECONDARY_COLOR, 0.5), gradFactor);
        }
      else
        {
         //--- Blend normal
         headerColor = BlendColors(DarkenColor(CYBER_PRIMARY_COLOR, 0.8), DarkenColor(CYBER_SECONDARY_COLOR, 0.8), gradFactor);
        }
      
      //--- Get header ARGB
      uint argbHeader = ColorToARGB(headerColor, 255);
      
      //--- Loop over columns
      for (int x = 0; x < currentWidthPixels; x++)
        {
         //--- Set pixel
         mainCanvas.PixelSet(x, y, argbHeader);
        }
     }
   
   //--- Blend border
   color borderColor = BlendColors(CYBER_PRIMARY_COLOR, CYBER_SECONDARY_COLOR, animFactor);
   //--- Get border ARGB
   uint argbBorder = ColorToARGB(borderColor, 255);
   //--- Draw outer
   mainCanvas.Rectangle(0, 0, currentWidthPixels - 1, HEADER_BAR_HEIGHT, argbBorder);
   //--- Draw inner
   mainCanvas.Rectangle(1, 1, currentWidthPixels - 2, HEADER_BAR_HEIGHT - 1, argbBorder);
   
   //--- Set title font
   mainCanvas.FontSet("Arial Black", titleFontSize);
   
   //--- Format title
   string titleText = StringFormat("◆ %s vs %s - Linear Regression ◆", secondarySymbol, primarySymbol);
   
   //--- Get glow ARGB
   uint glowColor = ColorToARGB(CYBER_PRIMARY_COLOR, (uchar)(120 * glowIntensity));
   //--- Loop for glow Y
   for (int offsetY = -1; offsetY <= 1; offsetY++)
     {
      //--- Loop for glow X
      for (int offsetX = -1; offsetX <= 1; offsetX++)
        {
         //--- Skip center
         if (offsetX == 0 && offsetY == 0) continue;
         //--- Draw glow text
         mainCanvas.TextOut(currentWidthPixels / 2 + offsetX, (HEADER_BAR_HEIGHT - titleFontSize) / 2 + offsetY, 
           titleText, glowColor, TA_CENTER);
        }
     }
   
   //--- Set text color
   uint textColor = ColorToARGB(C'255,105,180', 255);
   //--- Draw title
   mainCanvas.TextOut(currentWidthPixels / 2, (HEADER_BAR_HEIGHT - titleFontSize) / 2, 
     titleText, textColor, TA_CENTER);
  }

Здесь мы реализуем функцию "drawFuturisticHeader" для отрисовки анимированного градиентного заголовка в стиле киберпанк, начиная с вычисления фазы от "currentAnimationFrame" с использованием синуса для пульсирующих эффектов и коэффициента анимации, нормализованного в диапазоне от 0 до 1. Далее мы перебираем в цикле строки заголовка, чтобы вычислить коэффициент вертикального градиента, смешиваем затемненные основные и второстепенные цвета кибер-темы на основе состояния (перетаскивание, наведение курсора или значение по умолчанию) с помощью функций "BlendColors" и "DarkenColor", преобразуем в ARGB и устанавливаем значение для каждого пикселя построчно с помощью PixelSet для динамического затенения.

Для добавления границ мы смешиваем основные и второстепенные цвета с коэффициентом анимации, рисуем внешние и внутренние прямоугольники, используя Rectangle в смешанном ARGB. Мы используем жирный шрифт "Arial Black" с помощью FontSet, форматируем заголовок с помощью символов и названий пар, используя StringFormat, затем создаем эффект свечения, рисуя смещенный текст в сетке 3x3 (пропуская центр) с низкой прозрачностью в цвете CYBER PRIMARY COLOR в формате ARGB, и накладываем основной заголовок ярко-розовым цветом ARGB, центрированным с помощью TextOut метода. Далее мы нарисуем график регрессии в стиле киберпанка, но сначала нам понадобятся некоторые вспомогательные функции для рендеринга сетки и точек данных.

Рисование киберпанковской сетки и неоновых точек

Для наложения графиков добавляем анимированные сетки и светящиеся маркеры данных.

//+------------------------------------------------------------------+
//| Draw cyberpunk grid                                              |
//+------------------------------------------------------------------+
void drawCyberpunkGrid(int startX, int startY, int width, int height)
  {
   //--- Set grid spacing
   int gridSpacing = 40;
   //--- Compute phase
   double phase = (currentAnimationFrame % 120) * M_PI / 60.0;
   //--- Compute alpha
   uchar gridAlpha = (uchar)(30 + 15 * MathSin(phase));
   
   //--- Set grid color
   color gridColor = CYBER_PRIMARY_COLOR;
   //--- Get grid ARGB
   uint argbGrid = ColorToARGB(gridColor, gridAlpha);
   
   //--- Loop vertical lines
   for (int x = startX; x < startX + width; x += gridSpacing)
     {
      //--- Loop over height
      for (int y = startY; y < startY + height; y++)
        {
         //--- Blend grid pixel
         blendPixelSet(mainCanvas, x, y, argbGrid);
        }
     }
   
   //--- Loop horizontal lines
   for (int y = startY; y < startY + height; y += gridSpacing)
     {
      //--- Loop over width
      for (int x = startX; x < startX + width; x++)
        {
         //--- Blend grid pixel
         blendPixelSet(mainCanvas, x, y, argbGrid);
        }
     }
  }

//+------------------------------------------------------------------+
//| Draw neon point                                                  |
//+------------------------------------------------------------------+
void drawNeonPoint(int centerX, int centerY, int radius, color pointColor)
  {
   //--- Loop for glow
   for (int glowRadius = radius + 4; glowRadius > radius; glowRadius--)
     {
      //--- Compute alpha
      uchar glowAlpha = (uchar)(80 * glowIntensity * (radius + 4 - glowRadius) / 4.0);
      //--- Get glow color
      uint glowColor = ColorToARGB(pointColor, glowAlpha);
      //--- Draw glow circle
      drawCirclePoint(centerX, centerY, glowRadius, glowColor);
     }
   
   //--- Get point ARGB
   uint argbPoint = ColorToARGB(pointColor, 255);
   //--- Draw point circle
   drawCirclePoint(centerX, centerY, radius, argbPoint);
   
   //--- Get center ARGB
   uint argbCenter = ColorToARGB(C'255,255,255', 200);
   //--- Draw center circle
   drawCirclePoint(centerX, centerY, radius - 1, argbCenter);
  }

Мы реализуем функцию "drawCyberpunkGrid" для создания анимированной сетки, накладываемой на область построения в режиме киберпанка, устанавливая интервал в 40 пикселей, вычисляя фазу из "currentAnimationFrame" для синусоидальной пульсации и варьируя альфа-канал от 30 до 45 для получения едва заметного свечения. Далее мы используем основной цвет кибер-темы, преобразуем его в ARGB с динамическим альфа-каналом, затем циклически рисуем вертикальные линии с заданным интервалом, смешивая пиксели по всей высоте с помощью "blendPixelSet", а также горизонтальные линии аналогично по ширине, добавляя футуристический фон в стиле "Матрицы", не перегружая при этом данные. Эта сетка поможет усилить тематическое погружение за счет смешивания с низкой степенью прозрачности, гарантируя, что она легко интегрируется с другими элементами. Впрочем, при желании можно оставить всё как есть.

Для добавления неоновых эффектов мы создаём функцию "drawNeonPoint" для точек данных, выполняя цикл от радиуса +4 до радиуса +1, чтобы нарисовать концентрические светящиеся круги с уменьшающейся альфа-интенсивностью, масштабируемой с помощью "glowIntensity", используя функцию "drawCirclePoint" (предполагая, что это базовый круг) в цвете точек ARGB. Затем мы рисуем центральную точку с максимальным радиусом и прозрачностью, за которой следует меньший внутренний круг полупрозрачным белым цветом C'255,255,255' с альфа-каналом 200 для выделения, создавая пульсирующую, научно-фантастическую визуализацию, которая выделяется на фоне темных тонов. Опять же, выбор цвета можно изменить. Мы просто использовали произвольные значения для целей демонстрации. Теперь мы можем использовать эти вспомогательные функции для создания графика в стиле киберпанк.

Создание графика регрессии в стиле киберпанк

Для наглядного представления основного анализа мы адаптируем график, используя сетки, оси, деления, линии и точки в стиле киберпанка.

//+------------------------------------------------------------------+
//| Draw regression plot cyberpunk                                   |
//+------------------------------------------------------------------+
void drawRegressionPlotCyberpunk()
  {
   //--- Return if no data
   if (!dataLoadedSuccessfully) return;

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

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

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

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

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

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

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

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

   //--- Draw grid
   drawCyberpunkGrid(drawAreaLeft, drawAreaTop, plotWidth, plotHeight);

   //--- Set axis color
   color axisColor = CYBER_PRIMARY_COLOR;
   //--- Get axis ARGB
   uint argbAxis = ColorToARGB(axisColor, 255);
   
   //--- Get glow ARGB
   uint glowColor = ColorToARGB(axisColor, (uchar)(60 * glowIntensity));
   //--- Loop for glow
   for (int g = 1; g <= 2; g++)
     {
      //--- Draw Y glow
      for (int y = plotAreaTop; y <= plotAreaBottom; y++)
        {
         //--- Blend left glow
         blendPixelSet(mainCanvas, plotAreaLeft - g - 1, y, glowColor);
         //--- Blend right glow
         blendPixelSet(mainCanvas, plotAreaLeft + g + 1, y, glowColor);
        }
      //--- Draw X glow
      for (int x = plotAreaLeft; x <= plotAreaRight; x++)
        {
         //--- Blend bottom glow
         blendPixelSet(mainCanvas, x, plotAreaBottom + g + 1, glowColor);
         //--- Blend top glow
         blendPixelSet(mainCanvas, x, plotAreaBottom - g - 1, glowColor);
        }
     }
   
   //--- Loop for thick Y-axis
   for (int thick = 0; thick < 2; thick++)
     {
      //--- Draw Y-axis line
      mainCanvas.Line(plotAreaLeft - thick, plotAreaTop, plotAreaLeft - thick, plotAreaBottom, argbAxis);
     }
   
   //--- Loop for thick X-axis
   for (int thick = 0; thick < 2; thick++)
     {
      //--- Draw X-axis line
      mainCanvas.Line(plotAreaLeft, plotAreaBottom + thick, plotAreaRight, plotAreaBottom + thick, argbAxis);
     }

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

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

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

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

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

   //--- Compute pulse phase
   double pulsePhase = (currentAnimationFrame % 60) * M_PI / 30.0;
   //--- Compute pulse factor
   double pulseFactor = (MathSin(pulsePhase) + 1.0) * 0.3 + 0.7;
   //--- Compute point size
   int effectivePointSize = (int)(dataPointSize * pulseFactor);
   
   //--- Loop over points
   for (int i = 0; i < dataPoints; i++)
     {
      //--- Compute screen X
      int screenX = drawAreaLeft + (int)((primaryClosePrices[i] - minX) / rangeX * plotWidth);
      //--- Compute screen Y
      int screenY = drawAreaBottom - (int)((secondaryClosePrices[i] - minY) / rangeY * plotHeight);

      //--- Draw neon point
      drawNeonPoint(screenX, screenY, effectivePointSize, CYBER_DATA_POINTS_COLOR);
     }

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

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

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

Здесь мы реализуем функцию "drawRegressionPlotCyberpunk" для визуализации регрессии в режиме киберпанка, сначала возвращая значение, если данные отсутствуют, затем устанавливая области построения графика и отрисовки с отступами, вычисляя ширину/высоту и завершая работу, если значения недопустимы. Далее определяем минимум/максимум для оси X (основной) и оси Y (вторичный), перебирая цены, устанавливаем нулевые диапазоны равными 1, вызываем функцию "drawCyberpunkGrid" для фоновой сетки, устанавливаем ось в значение CYBER PRIMARY COLOR в формате ARGB, добавляем слои свечения с помощью функции "blendPixelSet" вокруг осей Y/X для создания глубины и рисуем утолщенные линии с помощью функции Line в циклах. Для создания меток мы используем шрифт "Consolas" с цветом CYBER PRIMARY COLOR в формате ARGB, вычисляем тики по оси Y, создаем цикл для рисования коротких линий и выравнивания отформатированных меток по правому краю с помощью функции "formatTickLabel"; аналогично для оси X, где нижние метки центрированы.

Для построения линии мы вычисляем начальную/конечную точку по оси Y, используя параметры регрессии, сопоставляем их с позициями на экране, используем киберрегрессию ARGB и рисуем сглаженные сегменты с помощью LineAA, центрированные с помощью корректировки в половину ширины в цикле. Для усиления эффекта свечения мы вычисляем фазу пульсации из "currentAnimationFrame" с синусом, коэффициент масштабирования 0,7-1,0, зацикливаем данные для позиционирования экранов и выполняем рендеринг с помощью "drawNeonPoint" в цвете кибер-точек для создания анимированного свечения. Наконец, мы устанавливаем жирный шрифт "Arial Bold" с cyber secondary ARGB для меток, добавляем жестко заданные префиксы стрелок для эффектности. Опять же, мы использовали это для простоты, отцентрировали по оси X внизу, повернули по оси Y на 900 с помощью FontAngleSet для вертикали и сбросили значение угла наклона, завершая тематический график. При компиляции это даст нам что-то вроде следующего результата.

CYBERPUNK REGRESSION PLOT

Завершив этот процесс мы можем добавить панели и легенды для этой киберпанк-темы.

//+------------------------------------------------------------------+
//| Draw glass morphism stats panel                                  |
//+------------------------------------------------------------------+
void drawGlassMorphismStatsPanel()
  {
   //--- Set panel X
   int panelX = statsPanelX;
   //--- Set panel Y
   int panelY = HEADER_BAR_HEIGHT + statsPanelY;
   //--- Set panel width
   int panelWidth = statsPanelWidth;
   //--- Set panel height
   int panelHeight = statsPanelHeight;

   //--- Compute bg color
   color panelBgColor = DarkenColor(CYBER_PRIMARY_COLOR, 0.85);
   //--- Set alpha
   uchar bgAlpha = 120;
   //--- Get panel bg ARGB
   uint argbPanelBg = ColorToARGB(panelBgColor, bgAlpha);
   
   //--- Loop over rows
   for (int y = panelY; y <= panelY + panelHeight; y++)
     {
      //--- Loop over columns
      for (int x = panelX; x <= panelX + panelWidth; x++)
        {
         //--- Blend bg pixel
         blendPixelSet(mainCanvas, x, y, argbPanelBg);
        }
     }
   
   //--- Blend border
   color borderColor = BlendColors(CYBER_PRIMARY_COLOR, CYBER_SECONDARY_COLOR, 0.5);
   //--- Get border ARGB
   uint argbBorder = ColorToARGB(borderColor, 255);
   
   //--- Get glow ARGB
   uint glowColor = ColorToARGB(borderColor, (uchar)(60 * glowIntensity));
   //--- Loop for glow
   for (int g = 1; g <= 2; g++)
     {
      //--- Draw top glow
      for (int x = panelX - g; x <= panelX + panelWidth + g; x++)
        {
         //--- Blend pixel
         blendPixelSet(mainCanvas, x, panelY - g, glowColor);
        }
      //--- Draw bottom glow
      for (int x = panelX - g; x <= panelX + panelWidth + g; x++)
        {
         //--- Blend pixel
         blendPixelSet(mainCanvas, x, panelY + panelHeight + g, glowColor);
        }
      //--- Draw left glow
      for (int y = panelY - g; y <= panelY + panelHeight + g; y++)
        {
         //--- Blend pixel
         blendPixelSet(mainCanvas, panelX - g, y, glowColor);
        }
      //--- Draw right glow
      for (int y = panelY - g; y <= panelY + panelHeight + g; y++)
        {
         //--- Blend pixel
         blendPixelSet(mainCanvas, panelX + panelWidth + g, y, glowColor);
        }
     }
   
   //--- Draw top border
   for (int x = panelX; x <= panelX + panelWidth; x++)
     {
      //--- Blend border pixel
      blendPixelSet(mainCanvas, x, panelY, argbBorder);
      //--- Blend bottom border
      blendPixelSet(mainCanvas, x, panelY + panelHeight, argbBorder);
     }
   //--- Draw left and right borders
   for (int y = panelY; y <= panelY + panelHeight; y++)
     {
      //--- Blend left
      blendPixelSet(mainCanvas, panelX, y, argbBorder);
      //--- Blend right
      blendPixelSet(mainCanvas, panelX + panelWidth, y, argbBorder);
     }

   //--- Set stats font
   mainCanvas.FontSet("Consolas", panelFontSize);
   //--- Get text ARGB
   uint argbText = ColorToARGB(CYBER_PRIMARY_COLOR, 255);

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

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

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

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

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

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

   //--- Compute bg color
   color legendBgColor = DarkenColor(CYBER_SECONDARY_COLOR, 0.85);
   //--- Set alpha
   uchar bgAlpha = 120;
   //--- Get legend bg ARGB
   uint argbLegendBg = ColorToARGB(legendBgColor, bgAlpha);
   
   //--- Loop over rows
   for (int y = legendY; y <= legendY + legendHeightThis; y++)
     {
      //--- Loop over columns
      for (int x = legendX; x <= legendX + legendWidth; x++)
        {
         //--- Blend bg pixel
         blendPixelSet(mainCanvas, x, y, argbLegendBg);
        }
     }
   
   //--- Blend border
   color borderColor = BlendColors(CYBER_SECONDARY_COLOR, CYBER_PRIMARY_COLOR, 0.5);
   //--- Get border ARGB
   uint argbBorder = ColorToARGB(borderColor, 255);
   
   //--- Get glow ARGB
   uint glowColor = ColorToARGB(borderColor, (uchar)(60 * glowIntensity));
   //--- Loop for glow
   for (int g = 1; g <= 2; g++)
     {
      //--- Draw top and bottom glow
      for (int x = legendX - g; x <= legendX + legendWidth + g; x++)
        {
         //--- Blend top
         blendPixelSet(mainCanvas, x, legendY - g, glowColor);
         //--- Blend bottom
         blendPixelSet(mainCanvas, x, legendY + legendHeightThis + g, glowColor);
        }
      //--- Draw left and right glow
      for (int y = legendY - g; y <= legendY + legendHeightThis + g; y++)
        {
         //--- Blend left
         blendPixelSet(mainCanvas, legendX - g, y, glowColor);
         //--- Blend right
         blendPixelSet(mainCanvas, legendX + legendWidth + g, y, glowColor);
        }
     }
   
   //--- Draw top and bottom borders
   for (int x = legendX; x <= legendX + legendWidth; x++)
     {
      //--- Blend top
      blendPixelSet(mainCanvas, x, legendY, argbBorder);
      //--- Blend bottom
      blendPixelSet(mainCanvas, x, legendY + legendHeightThis, argbBorder);
     }
   //--- Draw left and right borders
   for (int y = legendY; y <= legendY + legendHeightThis; y++)
     {
      //--- Blend left
      blendPixelSet(mainCanvas, legendX, y, argbBorder);
      //--- Blend right
      blendPixelSet(mainCanvas, legendX + legendWidth, y, argbBorder);
     }

   //--- Set legend font
   mainCanvas.FontSet("Consolas", panelFontSize);
   //--- Get text ARGB
   uint argbText = ColorToARGB(CYBER_SECONDARY_COLOR, 255);

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

   //--- Draw neon point
   drawNeonPoint(legendX + 12, itemY, dataPointSize, CYBER_DATA_POINTS_COLOR);
   //--- Draw data label
   mainCanvas.TextOut(legendX + 22, itemY - 4, "Data Points", argbText, TA_LEFT);
   //--- Update Y
   itemY += lineSpacing;

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

Сначала мы реализуем функцию "drawGlassMorphismStatsPanel" для создания полупрозрачного светящегося наложения статистики в стиле темы киберпанк, позиционируя его на основе входных параметров со смещением от заголовка, затемняя основной цвет киберпанка с помощью функции "DarkenColor" для фона, устанавливая низкое значение альфа-канала 120 и смешивая область пиксель за пикселем с помощью функции "blendPixelSet" для эффекта матового стекла. Далее, смешиваем цвет границы из основных и дополнительных цветов киберпространства с помощью функции «BlendColors», подготавливаем ARGB, добавляем внешние слои свечения, расширяя циклы вокруг краев панели с уменьшающейся интенсивностью, затем рисуем верхнюю/нижнюю/левую/правую границы, смешивая линии. Мы устанавливаем шрифт "Consolas" с помощью FontSet, используем цвет CYBER PRIMARY COLOR в формате ARGB для текста, инициализируем Y отступами и межстрочным интервалом, заданными размером шрифта, форматируем и отображаем уравнение, корреляцию, коэффициент детерминации R-квадрат и количество точек с помощью StringFormat и "TextOut" с выравниванием по левому краю, обновляя значение Y для каждой строки, размещая строки друг под другом.

Аналогично, для "drawGlassMorphismLegend" мы размещаем ниже панель статистики соответствующей ширины, затемняем дополнительный цвет киберпространства для фона, смешиваем заливку, применяем идентичное смешивание границ и расширяем свечение. Мы рисуем верхнюю/нижнюю/левую/правую границы, устанавливаем шрифт Consolas с текстом дополнительного цвета ARGB темы киберпанк, рисуем неоновую точечную иконку в скорректированном положении с помощью функции "drawNeonPoint" цветом точек темы киберпанк, добавляем метку "Data Points", обновляем значение по оси Y; затем смешиваем короткий горизонтальный отрезок линии (ширина 15 пикселей, высота 2 пикселя) цветом регрессии темы киберпанк​и добавляем метку "Regression Line" для обеспечения видимости. Для неонового индикатора изменения размера мы используем следующий подход.

Отрисовка неонового индикатора изменения размера

Для удобства изменения размера мы отображаем выделенные элементы управления с помощью свечения.

//+------------------------------------------------------------------+
//| Draw neon resize indicator                                       |
//+------------------------------------------------------------------+
void drawNeonResizeIndicator()
  {
   //--- Blend indicator color
   color indicatorColor = BlendColors(CYBER_PRIMARY_COLOR, CYBER_SECONDARY_COLOR, 0.5);
   //--- Get indicator ARGB
   uint argbIndicator = ColorToARGB(indicatorColor, 200);
   
   //--- Get glow ARGB
   uint glowColor = ColorToARGB(indicatorColor, (uchar)(100 * glowIntensity));
   
   //--- Check corner
   if (hoverResizeMode == RESIZE_CORNER || activeResizeMode == RESIZE_CORNER)
     {
      //--- Compute corner X
      int cornerX = currentWidthPixels - resizeGripSize;
      //--- Compute corner Y
      int cornerY = currentHeightPixels - resizeGripSize;
      
      //--- Loop for glow
      for (int g = 1; g <= 3; g++)
        {
         //--- Loop Y glow
         for (int y = cornerY - g; y < currentHeightPixels + g; y++)
           {
            //--- Loop X glow
            for (int x = cornerX - g; x < currentWidthPixels + g; x++)
              {
               //--- Blend glow
               blendPixelSet(mainCanvas, x, y, glowColor);
              }
           }
        }
      
      //--- Fill corner
      mainCanvas.FillRectangle(cornerX, cornerY, currentWidthPixels - 1, currentHeightPixels - 1, argbIndicator);
      
      //--- Set line color
      uint lineColor = ColorToARGB(C'255,255,255', 255);
      //--- Loop for lines
      for (int i = 0; i < 4; i++)
        {
         //--- Compute offset
         int offset = i * 3;
         //--- Draw diagonal
         mainCanvas.Line(cornerX + offset, currentHeightPixels - 1, 
                                  currentWidthPixels - 1, cornerY + offset, lineColor);
        }
     }
   
   //--- Check right
   if (hoverResizeMode == RESIZE_RIGHT_EDGE || activeResizeMode == RESIZE_RIGHT_EDGE)
     {
      //--- Compute indicator Y
      int indicatorY = currentHeightPixels / 2 - 15;
      //--- Loop for glow
      for (int g = 1; g <= 3; g++)
        {
         //--- Fill glow
         mainCanvas.FillRectangle(currentWidthPixels - 3 - g, indicatorY - g, 
                                          currentWidthPixels - 1 + g, indicatorY + 30 + g, glowColor);
        }
      //--- Fill indicator
      mainCanvas.FillRectangle(currentWidthPixels - 3, indicatorY, 
                                       currentWidthPixels - 1, indicatorY + 30, argbIndicator);
     }
   
   //--- Check bottom
   if (hoverResizeMode == RESIZE_BOTTOM_EDGE || activeResizeMode == RESIZE_BOTTOM_EDGE)
     {
      //--- Compute indicator X
      int indicatorX = currentWidthPixels / 2 - 15;
      //--- Loop for glow
      for (int g = 1; g <= 3; g++)
        {
         //--- Fill glow
         mainCanvas.FillRectangle(indicatorX - g, currentHeightPixels - 3 - g, 
           indicatorX + 30 + g, currentHeightPixels - 1 + g, glowColor);
        }
      //--- Fill indicator
      mainCanvas.FillRectangle(indicatorX, currentHeightPixels - 3, 
        indicatorX + 30, currentHeightPixels - 1, argbIndicator);
     }
  }

Мы реализуем функцию "drawNeonResizeIndicator" для визуального отображения изменений размера в режиме киберпанка, начиная со смешивания основных и дополнительных цветов киберпанка с помощью функции "BlendColors" для индикатора, преобразования в ARGB с альфа-каналом 200 и подготавливаем вариант цвета свечения с alpha, зависящим от glowIntensity. Для изменения размера угла (при наведении курсора или активации) мы вычисляем положение нижнего правого угла на основе размера элемента управления, добавляем многослойное свечение путем выполнения цикла, заполняя расширенные области вокруг угла с помощью функции "blendPixelSet", затем заполняем квадрат элемента управления с помощью функции FillRectangle и рисуем четыре смещенные диагональные линии в полном белом цвете ARGB с помощью "Line" для создания эффекта полос.

Далее, для правого края, мы центрируем вертикальный индикатор Y, применяем свечение, заполняя слегка расширенные и смещенные прямоугольники циклами, а затем основной заливкой. Аналогично, для нижнего края, по центру разместим горизонтальный индикатор X, добавим заливку свечением и завершим оформление основным прямоугольником-индикатором. Этот неоновый стиль с эффектом свечения повысит интерактивность, позволяя выделяться изменяемым по размеру областям в темном киберпанк-стиле за счет многослойного смешивания для создания глубины. Теперь мы можем последовательно вызывать и создавать графики, используя следующую логику.

//+------------------------------------------------------------------+
//| Render visualization                                             |
//+------------------------------------------------------------------+
void renderVisualization()
  {
   //--- Check cyberpunk mode
   if (isCyberpunkThemeEnabled)
     {
      //--- Render cyberpunk
      renderCyberpunkTheme();
     }
   else
     {
      //--- Render standard
      renderStandardTheme();
     }
  }

//+------------------------------------------------------------------+
//| Render standard theme                                            |
//+------------------------------------------------------------------+
void renderStandardTheme()
  {
   //--- Erase canvas
   mainCanvas.Erase(0);

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

   //--- Draw border
   drawCanvasBorder();
   //--- Draw header
   drawHeaderBar();
   //--- Draw toggle button
   drawThemeToggleButton();
   //--- Draw plot
   drawRegressionPlot();

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

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

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

//+------------------------------------------------------------------+
//| Render cyberpunk theme                                           |
//+------------------------------------------------------------------+
void renderCyberpunkTheme()
  {
   //--- Erase canvas
   mainCanvas.Erase(0);

   //--- Draw cyber background
   drawCyberpunkBackground();
   //--- Draw holographic border
   drawHolographicBorder();
   //--- Draw futuristic header
   drawFuturisticHeader();
   //--- Draw toggle button
   drawThemeToggleButton();
   //--- Draw cyber plot
   drawRegressionPlotCyberpunk();

   //--- Check show stats
   if (showStatistics)
     {
      //--- Draw glass stats
      drawGlassMorphismStatsPanel();
      //--- Draw glass legend
      drawGlassMorphismLegend();
     }

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

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

Здесь мы реализуем функцию "renderVisualization" для условного отображения графика на основе выбранного режима темы, проверяя значение "isCyberpunkThemeEnabled", чтобы вызвать "renderCyberpunkTheme" для футуристических эффектов или "renderStandardTheme" для классического отображения, позволяя плавно переключаться между режимами без избыточного кода. В "renderStandardTheme" мы очищаем данные с объекта Canvas с помощью функции Erase, при необходимости рисуем градиентный фон (если эта функция включена), добавляем границы, поле заголовка, кнопку переключения темы, основной график регрессии, и, если "showStatistics" имеет значение true, включаем панель статистики и легенду; при наведении курсора изменяем размер и включаем, добавляем индикатор, а затем обновляем данные с помощью Update метода.

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

Настройка таймера для анимации

//+------------------------------------------------------------------+
//| Initialize expert                                                |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Render visualization
   renderVisualization();

   //--- Enable mouse events
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
   //--- Redraw chart
   ChartRedraw();
   
   //--- Set timer for animations
   EventSetMillisecondTimer(50);

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

//+------------------------------------------------------------------+
//| Deinitialize expert                                              |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Kill timer
   EventKillTimer();
   //--- Destroy canvas
   mainCanvas.Destroy();
   //--- Redraw chart
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Handle timer                                                     |
//+------------------------------------------------------------------+
void OnTimer()
  {
   //--- Check cyberpunk enabled
   if (isCyberpunkThemeEnabled)
     {
      //--- Increment frame
      currentAnimationFrame++;
      //--- Reset frame
      if (currentAnimationFrame > 360) currentAnimationFrame = 0;
      //--- Render
      renderVisualization();
      //--- Redraw chart
      ChartRedraw();
     }
  }

Начинаем с обработчика OnInit, расширяя предыдущую логику, чтобы включить поддержку анимации, вызываем функцию "renderVisualization" для первоначального отображения, включаем события перемещения мыши, перерисовываем график и устанавливаем 50-миллисекундный таймер с помощью EventSetMillisecondTimer для периодического обновления, после чего возвращаем INIT_SUCCEEDED. Далее, в OnDeinit мы останавливаем таймер с помощью EventKillTimer, чтобы прекратить анимацию, удалить объект Canvas и перерисовываем график для очистки.

Для обработки анимации мы используем OnTimer, проверяем, включен ли режим киберпанка, увеличиваем значение параметра "currentAnimationFrame" и сбрасывая его до 360 для зацикливания. Затем перерисовываем визуализацию с помощью "renderVisualization" и перерисовываем график для применения изменений на основе фреймов, таких как пульсации или свечение. Вот и всё. Наконец, напомним, что мы добавили дополнительную кнопку в заголовок, которую нужно распознавать при наведении курсора. Мы расширим её логику, включив ее в обработчик событий графика.

Обработка переключения темы в событиях

Для обработки кликов мы расширяем обработчик событий, предназначенный для обнаружения нажатий кнопок.

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

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

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

      //--- Update header hover
      isHoveringHeader = isMouseOverHeaderBar(mouseX, mouseY);
      //--- Update theme hover
      isHoveringThemeButton = isMouseOverThemeButton(mouseX, mouseY);
      //--- Update resize hover
      isHoveringResizeZone = isMouseInResizeZone(mouseX, mouseY, hoverResizeMode);

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

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

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

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

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

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

CYBERPUNK PLOT RENDER

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


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

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

BACKTEST GIF

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


Заключение

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

  • Переключать темы на графиках для лучшей видимости при различном освещении
  • Использовать неоновые точки для быстрого определения отклонений в корреляции
  • Использовать пульсирующие линии в качестве визуальных индикаторов силы наклона при совершении парных сделок

В следующих частях мы рассмотрим построение графиков статистических моделей распределения и их графическое представление в виде столбчатых диаграмм. Следите за обновлениями!

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

Статистический арбитраж с использованием коинтегрированных акций (Часть 4): Обновление параметров модели в реальном времени Статистический арбитраж с использованием коинтегрированных акций (Часть 4): Обновление параметров модели в реальном времени
В данной статье описывается простой, но комплексный алгоритм статистического арбитража для торговли корзиной коинтегрированных акций. В него входит полнофункциональный скрипт на языке Python для загрузки и хранения данных; тесты на корреляцию, коинтеграцию и стационарность, а также пример реализации сервиса Metatrader 5 для обновления базы данных и соответствующий советник. Здесь приведены некоторые проектные решения для справки и в целях содействия воспроизведению эксперимента.
Разработка инструментария для анализа Price Action (Часть 30): Советник CCI Zero Line Разработка инструментария для анализа Price Action (Часть 30): Советник CCI Zero Line
Будущее – за автоматизацией анализа движения цен. В этой статье мы используем индикатор Dual CCI (Commodity Channel Index – индекс товарного канала), стратегию пересечения нулевой линии (Zero Line Crossover), EMA и анализ движения цены, чтобы разработать инструмент, который генерирует торговые сигналы и задает уровни стоп-лосса и тейк-профита с помощью ATR. Прочитайте эту статью, чтобы узнать наш подход к разработке советника CCI Zero Line.
От начального до среднего уровня: Указатель на функцию От начального до среднего уровня: Указатель на функцию
Вы, вероятно, уже слышали о указателях, когда речь заходит о программировании. А вы знали, что мы можем использовать данные такого типа здесь, в MQL5? Это, конечно, должно быть сделано так, чтобы мы не теряли контроль и не вызывали странного поведения программы во время её выполнения. Тем не менее, поскольку это ресурс очень специфического назначения и ориентированный на определенные виды деятельности, редко можно услышать, чтобы кто-то обсуждал, что такое указатель и как его использовать в MQL5.
Создание самооптимизирующихся советников на MQL5 (Часть 14): Преобразования данных как параметры настройки регулятора с обратной связью Создание самооптимизирующихся советников на MQL5 (Часть 14): Преобразования данных как параметры настройки регулятора с обратной связью
Предварительная обработка — это мощный, но часто упускаемый из виду параметр настройки. Он находится в тени своих более крупных собратьев: оптимизаторов и блестящих архитектур моделей. Даже незначительное улучшение показателей в данном случае может иметь непропорционально значительный и кумулятивный эффект на прибыльность и риски. Слишком часто эта в значительной степени неизученная наука сводится к простой рутине, рассматриваемой лишь как средство для достижения цели, тогда как на самом деле именно здесь сигнал может быть непосредственно усилен или с такой же легкостью уничтожен.