English
preview
Торговые инструменты на MQL5 (Часть 11):  Панель корреляционной матрицы (Пирсон, Спирман, Кенделл) с тепловой картой и стандартным режимом

Торговые инструменты на MQL5 (Часть 11): Панель корреляционной матрицы (Пирсон, Спирман, Кенделл) с тепловой картой и стандартным режимом

MetaTrader 5Трейдинг |
33 1
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В своей предыдущей статье (Часть 10) мы разработали систему отслеживания стратегий на MetaQuotes Language 5 (MQL5) с визуальными уровнями и показателями успеха, которая обнаруживала сигналы пересечения скользящих средних, отслеживала сделки с несколькими уровнями тейк-профита и стоп-лоссов, а также визуализировала результаты на графике. В части 11 мы разработаем панель мониторинга корреляционной матрицы с использованием методов Пирсона, Спирмена и Кенделла, включающую тепловую карту и стандартные режимы. Эта панель вычисляет взаимосвязи активов, используя выбранный метод, за настраиваемый таймфрейм и в заданных барах. Она поддерживает стандартный режим с цветовыми порогами и звездочками значимости p-значений, а также режим тепловой карты с градиентными визуальными элементами. Дополнительно она включает в себя интерактивный пользовательский интерфейс с селекторами таймфреймов, переключателями режимов и динамической легендой. В статье рассмотрим следующие темы:

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

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


Понимание структуры панели корреляционной матрицы

Структура панели корреляционной матрицы анализирует взаимосвязи между финансовыми активами путем вычисления коэффициентов корреляции, помогая нам выявлять взаимозависимости, влияющие на диверсификацию портфеля, хеджирование или стратегии с использованием нескольких активов. Она обрабатывает изменения цен по выбранным пользователем символам за определенный период и таймфрейм, применяя один из трех статистических методов — коэффициент Пирсона для линейных зависимостей, коэффициент Спирмена для основанных на рангах монотонных ассоциаций или коэффициент Кенделла для согласованности рангов — для количественной оценки того, как активы движутся вместе или в противоположных направлениях. Значимость оценивается с помощью p-значений, указывающих на надежность, а визуальные подсказки, такие как цветовые пороги или градиенты, выделяют сильные положительные, сильные отрицательные, слабые или нейтральные корреляции, что позволяет быстро распознавать паттерны без ручных вычислений.

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

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

CORRELATION MATRIX DASHBOARD FRAMEWORK


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

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

//+------------------------------------------------------------------+
//|                           Correlation Matrix Dashboard PART1.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"

#include <Math\Stat\Math.mqh>

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input string SymbolsList                   = "EURUSDm,GBPUSDm,USDJPYm,AUDUSDm,BTCUSDm,NZDUSDm,US500m,XAUUSDm"; // Comma-separated symbols (up to 30)
input ENUM_TIMEFRAMES CorrelationTimeframe = PERIOD_CURRENT;                                                   // Timeframe for correlation calculation
input int CorrelationBars                  = 100;                                                              // Number of bars for correlation calculation (min 20)
input double StrongPositiveThresholdPct    = 70.0;                                                             // Strong positive threshold in % (e.g., 70.0 for >=0.70)
input double StrongNegativeThresholdPct    = -70.0;                                                            // Strong negative threshold in % (e.g., -70.0 for <=-0.70)
input double PValueThreshold1              = 0.01;                                                             // P-value for *** significance
input double PValueThreshold2              = 0.05;                                                             // P-value for ** significance
input double PValueThreshold3              = 0.10;                                                             // P-value for * significance
input color ColorStrongPositiveBg          = clrLimeGreen;                                                     // Background for strong positive
input color ColorStrongNegativeBg          = clrOrangeRed;                                                     // Background for strong negative
input color ColorNeutralBg                 = C'70,70,70';                                                      // Background for neutral/mild/zero/diagonal cells
input color ColorDiagonalBg                = C'40,40,40';                                                      // Background for diagonal cells
input color ColorTextStrong                = clrWhite;                                                         // Text color for strong correlations
input color ColorTextPositive              = clrDeepSkyBlue;                                                   // Text color for mild positive
input color ColorTextNegative              = clrRed;                                                           // Text color for mild negative
input color ColorTextZero                  = clrWhite;                                                         // Text color for zero or diagonal

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum DisplayMode
{
   MODE_STANDARD, // Standard Thresholds
   MODE_HEATMAP   // Heatmap Gradient
};
input DisplayMode DashboardMode = MODE_STANDARD; // Dashboard Display Mode

enum CorrelationMethod
{
   PEARSON,   // Pearson correlation
   SPEARMAN,  // Spearman rank correlation
   KENDALL    // Kendall tau correlation
};
input CorrelationMethod CorrMethod = PEARSON; // Correlation calculation method

//+------------------------------------------------------------------+
//| Defines                                                          |
//+------------------------------------------------------------------+
#define MAIN_PANEL              "PANEL_MAIN"                     // Define main panel rectangle identifier
#define HEADER_PANEL            "PANEL_HEADER"                   // Define header panel rectangle identifier
#define LEGEND_PANEL            "PANEL_LEGEND"                   // Define legend panel rectangle identifier
#define HEADER_PANEL_ICON       "PANEL_HEADER_ICON"              // Define header icon label identifier
#define HEADER_PANEL_TEXT       "PANEL_HEADER_TEXT"              // Define header title label identifier
#define CLOSE_BUTTON            "BUTTON_CLOSE"                   // Define close button identifier
#define TOGGLE_BUTTON           "BUTTON_TOGGLE"                  // Define toggle (minimize/maximize) button identifier
#define HEATMAP_BUTTON          "BUTTON_HEATMAP"                 // Define heatmap toggle button identifier
#define PVAL_BUTTON             "BUTTON_PVAL"                    // Define P-value toggle button identifier
#define SORT_BUTTON             "BUTTON_SORT"                    // Define sort button identifier
#define THEME_BUTTON            "BUTTON_THEME"                   // Define theme toggle button identifier
#define TF_CELL_RECT            "TF_CELL_RECT_"                  // Define timeframe cell rectangle prefix
#define TF_CELL_TEXT            "TF_CELL_TEXT_"                  // Define timeframe cell text prefix
#define SYMBOL_ROW_RECTANGLE    "SYMBOL_ROW_"                    // Define row symbol rectangle prefix
#define SYMBOL_ROW_TEXT         "SYMBOL_ROW_TEXT_"               // Define row symbol text label prefix
#define SYMBOL_COL_RECTANGLE    "SYMBOL_COL_"                    // Define column symbol rectangle prefix
#define SYMBOL_COL_TEXT         "SYMBOL_COL_TEXT_"               // Define column symbol text label prefix
#define CELL_RECTANGLE          "CELL_"                          // Define correlation cell rectangle prefix
#define CELL_TEXT               "CELL_TEXT_"                     // Define correlation cell text label prefix
#define LEGEND_CELL_RECTANGLE   "LEGEND_CELL_"                   // Define legend cell rectangle prefix
#define LEGEND_CELL_TEXT        "LEGEND_CELL_TEXT_"              // Define legend cell text prefix
#define WIDTH_SYMBOL            80                               // Define width of symbol rectangles
#define WIDTH_CELL              80                               // Define width of correlation cells
#define WIDTH_TF_CELL           45                               // Define width of TF cells
#define WIDTH_LEGEND_CELL       45                               // Define width of legend cells
#define HEIGHT_RECTANGLE        30                               // Define height of all rectangles
#define HEIGHT_HEADER           27                               // Define height of header
#define HEIGHT_TF_CELL          25                               // Define height of TF cells
#define HEIGHT_LEGEND           30                               // Define height of legend cells
#define HEIGHT_LEGEND_PANEL     34                               // Define height of legend panel (with padding)
#define LEGEND_SPACING          5                                // Define spacing between legend cells
#define NUM_LEGEND_ITEMS        15                               // Define increased to cover max possible (e.g., 11 in heatmap)
#define GAP_HEIGHT              8                                // Define gap between sections
#define GAP_MAIN_LEGEND         2                                // Define vertical gap between main panel and legend
#define COLOR_WHITE             clrWhite                         // Define white color for text and backgrounds
#define COLOR_BLACK             clrBlack                         // Define black color for borders and text
#define COLOR_LIGHT_GRAY        C'230,230,230'                   // Define light gray for neutral cells
#define COLOR_DARK_GRAY         C'105,105,105'                   // Define dark gray for headers
#define MAX_SYMBOLS             30                               // Define maximum symbols supported
#define NUM_TF                  8                                // Define number of timeframes

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
int panel_x = 20, panel_y = 40;                                  //--- Initialize panel position coordinates
string symbols_array[MAX_SYMBOLS];                               //--- Declare array to store symbols
int num_symbols = 0;                                             //--- Initialize number of valid symbols
double correlation_matrix[MAX_SYMBOLS][MAX_SYMBOLS];             //--- Declare matrix to store correlations
double pvalue_matrix[MAX_SYMBOLS][MAX_SYMBOLS];                  //--- Declare matrix to store p-values
DisplayMode global_display_mode;                                 //--- Declare runtime display mode
ENUM_TIMEFRAMES global_correlation_tf;                           //--- Declare runtime timeframe
int current_tf_index = -1;                                       //--- Initialize index of current TF
int num_tf_visible;                                              //--- Declare dynamic number of visible TF cells
int num_legend_visible;                                          //--- Declare dynamic number of visible legend items
double visible_corr_vals[];                                      //--- Declare array of visible correlation values for legend

ENUM_TIMEFRAMES tf_list[NUM_TF] = {PERIOD_M1, PERIOD_M5, PERIOD_M15, PERIOD_M30, PERIOD_H1, PERIOD_H4, PERIOD_D1, PERIOD_W1}; //--- Initialize timeframe list
string tf_strings[NUM_TF] = {"M1", "M5", "M15", "M30", "H1", "H4", "D1", "W1"};                                            //--- Initialize timeframe strings

// Enhanced gradient colors for heatmap
color heatmap_colors[] = {clrRed, clrOrangeRed, clrOrange, clrYellow, clrLightGray, clrLime, clrLimeGreen, clrGreen}; //--- Initialize heatmap color gradient array

Мы начинаем реализацию с включения библиотеки Math с помощью команды "#include <Math\Stat\Math.mqh>", которая предоставляет статистические функции, необходимые для вычислений корреляции. Далее определяем входные параметры, позволяющие настраивать панель. Сюда входят: "SymbolsList" в виде строки, разделенной запятыми, содержащей до тридцати символов; "CorrelationTimeframe" с использованием перечисления ENUM_TIMEFRAMES для периода расчета; "CorrelationBars" в виде целого числа, указывающего количество баров (минимум двадцать); пороговые значения, такие как "StrongPositiveThresholdPct" и "StrongNegativeThresholdPct" в процентах для классификации корреляций; пороговые значения p-значения, такие как "PValueThreshold1" для уровней значимости; и цветовые входные параметры, такие как "ColorStrongPositiveBg", установленный на лаймовый цвет для ярко выраженного положительного фона, а также другие цвета для отрицательных, нейтральных, диагональных и текстовых значений.

Затем создаём перечисления для параметров конфигурации. Перечисление "DisplayMode" предлагает значения "MODE_STANDARD" для отображения на основе пороговых значений и "MODE_HEATMAP" для градиентной визуализации, при этом значение "DashboardMode" как вводный параметр по умолчанию равно "standard". Аналогично, перечисление "CorrelationMethod" предоставляет значения "PEARSON" для линейных корреляций, "SPEARMAN" для ранговых корреляций и "KENDALL" для тау-коэффициента, при этом в качестве вводного параметра для "CorrMethod" по умолчанию равно значение Pearson. После этого мы используем defines для установки констант для элементов пользовательского интерфейса и структуры, используя директиву #define. К ним относятся идентификаторы, такие как "MAIN_PANEL" для главного прямоугольника, "HEADER_PANEL" для заголовка и префиксы, такие как "TF_CELL_RECT" для ячеек таймфреймов, а также размеры, такие как "WIDTH_SYMBOL" значением восемьдесят пикселей, "HEIGHT_RECTANGLE" значением тридцать, промежутки, такие как "GAP_HEIGHT" значением восемь, а директивы цвета, такие как "COLOR_WHITE" установлены на белый.

Мы объявляем глобальные переменные для управления состоянием и данными. К ним относятся "panel_x" и "panel_y", инициализированные как двадцать и сорок для позиции, "symbols_array" как строковый массив с максимальным размером символов, "num_symbols", начинающийся с нуля, двумерные двойные массивы "correlation_matrix" и "pvalue_matrix" для хранения значений, "global_display_mode" из перечисления отображений, "global_correlation_tf" из таймфреймов, "current_tf_index" с отрицательным значением и целые числа, такие как "num_tf_visible" и "num_legend_visible", плюс двойной массив "visible_corr_vals" для значений условных обозначений. Наконец, мы инициализируем массивы для таймфреймов и цветов: "tf_list" как массив "ENUM_TIMEFRAMES" со значениями от одной минуты до недели, "tf_strings" как соответствующие строковые метки и "heatmap_colors" как массив цветов для градиентов от красного до зеленого. Следующее, что мы сделаем, - это определим некоторые вспомогательные функции для проведения статистического анализа.

//+------------------------------------------------------------------+
//| Approximate Normal CDF                                           |
//+------------------------------------------------------------------+
bool NormalCDF(double mean, double stddev, double x, double &cdf) {
   if (stddev <= 0.0) return false;                             //--- Check for invalid standard deviation
   double z = (x - mean) / stddev;                              //--- Compute z-score
   if (z < -10) {                                               //--- Handle extreme low z-value
      cdf = 0.0;                                                //--- Set CDF to 0
      return true;                                              //--- Return success
   }
   if (z > 10) {                                                //--- Handle extreme high z-value
      cdf = 1.0;                                                //--- Set CDF to 1
      return true;                                              //--- Return success
   }
   double t = 1 / (1 + 0.2316419 * MathAbs(z));                 //--- Compute t for approximation
   double d = 0.3989423 * MathExp(-z * z / 2);                  //--- Compute density d
   cdf = d * t * (0.3193815 + t * (-0.3565638 + t * (1.7814779 + t * (-1.821256 + t * 1.3302744)))); //--- Calculate CDF approximation
   if (z > 0) cdf = 1 - cdf;                                    //--- Adjust for positive z
   return true;                                                 //--- Return success
}

//+------------------------------------------------------------------+
//| Approximate Student t CDF                                        |
//+------------------------------------------------------------------+
bool StudentCDF(int df, double x, double &cdf) {
   if (df <= 0) return false;                                   //--- Check for invalid degrees of freedom
   double a = df / 2.0;                                         //--- Compute alpha parameter
   double b = 0.5;                                              //--- Set beta parameter
   double xt = df / (df + x * x);                               //--- Compute xt for incomplete beta
   double ib = MathBetaIncomplete(xt, a, b);                    //--- Compute incomplete beta
   double beta = MathExp(MathGammaLog(a) + MathGammaLog(b) - MathGammaLog(a + b)); //--- Compute beta function
   double regularized = ib / beta;                              //--- Compute regularized incomplete beta
   if (x >= 0) {                                                //--- Handle non-negative x
      cdf = 1 - 0.5 * regularized;                              //--- Set CDF for positive side
   } else {                                                     //--- Handle negative x
      cdf = 0.5 * regularized;                                  //--- Set CDF for negative side
   }
   return true;                                                 //--- Return success
}

//+------------------------------------------------------------------+
//| Calculate p-value based on method and correlation                |
//+------------------------------------------------------------------+
double calculate_pvalue(double corr, CorrelationMethod method, int n) {
   if (n < 3) return 1.0;                                       //--- Return invalid if insufficient samples

   double cdf = 0.0;                                            //--- Initialize CDF variable
   if (method == KENDALL) {                                     //--- Handle Kendall method
      double sigma = MathSqrt((4.0 * n + 10.0) / (9.0 * n * (n - 1.0))); //--- Compute sigma
      if (sigma == 0) return 1.0;                               //--- Return invalid if sigma zero
      double z = corr / sigma;                                  //--- Compute z-score
      if (!NormalCDF(0, 1, MathAbs(z), cdf)) return 1.0;        //--- Compute Normal CDF or return invalid
      return 2 * (1 - cdf);                                     //--- Return two-tailed p-value
   } else {                                                     //--- Handle PEARSON or SPEARMAN
      double r2 = corr * corr;                                  //--- Compute squared correlation
      if (r2 >= 1.0) return 0.0;                                //--- Return zero for perfect correlation
      double denom = 1.0 - r2;                                  //--- Compute denominator
      if (denom <= 0.0) return 0.0;                             //--- Avoid division by zero
      double t = corr * MathSqrt((n - 2.0) / denom);            //--- Compute t-statistic
      if (!StudentCDF(n - 2, MathAbs(t), cdf)) return 1.0;      //--- Compute Student CDF or return invalid
      return 2 * (1 - cdf);                                     //--- Return two-tailed p-value
   }
}

Сначала мы определяем функцию "NormalCDF" для аппроксимации кумулятивной функции распределения (CDF) для обеспечения нормального распределения, которая используется при расчете p-значения. Она принимает параметры для среднего значения, стандартного отклонения, значения x и ссылки для хранения результата CDF. Сначала мы проверяем, является ли стандартное отклонение недопустимым, и возвращаем значение false, если это так. Затем мы вычисляем z-показатель, стандартизируя x. Для экстремальных значений z ниже отрицательных десяти или выше десяти мы устанавливаем значение CDF равным нулю или единице соответственно и возвращаем значение true. В противном случае мы вычисляем приближение, используя полиномиальное разложение, основанное на t и плотности d, корректируя CDF на положительное значение z перед возвратом значения true. Обычный график CDF выглядел бы следующим образом.

NORMAL CULUMATIVE DISTRIBUTION FUNCTION (CDF) GRAPH

Далее мы реализуем функцию "StudentCDF" для аппроксимации кумулятивной функции распределения (CDF) для t-распределения Студента, необходимой для p-значений в методах Пирсона и Спирмена, которые мы определим позже. Она принимает степени свободы df, значение x и ссылку на CDF. Мы проверяем df и возвращаем значение false, если недействительно. Затем вычисляем параметры a и b, за которыми следует xt для неполной бета-функции. Используя "MathBetaIncomplete" для получения ib и вычисляя полную бета с помощью гамма-логарифмов, мы получаем упорядоченную неполную бета. В зависимости от того, является ли x неотрицательным или отрицательным, мы соответствующим образом корректируем CDF и возвращаем значение true. График CDF учащихся будет выглядеть следующим образом.

STUDENT CUMULATIVE DISTRIBUTION FUNCTION (CDF) GRAPH

Затем мы создаем функцию "calculate_pvalue" для вычисления p-значения на основе коэффициента корреляции, метода и размера выборки n. Если n меньше трех, мы возвращаем единицу, чтобы указать на недействительность. Инициализируем переменную кумулятивной функции распределения и обрабатываем коэффициент Кенделла отдельно, вычисляя сигму, проверяя наличие нуля, выводя z и используя "NormalCDF" для получения двустороннего p-значения. В отношении коэффициентов Пирсона и Спирмана мы вычисляем квадрат корреляции r2, обрабатываем идеальные корреляции, возвращая ноль, вычисляем знаменатель и t-статистику, затем используем "StudentCDF" с скорректированным df для получения двустороннего p-значения. Следующим шагом нам нужно создать функцию для преобразования списка символов в массивы, которыми мы сможем управлять. Для достижения этого результата мы использовали следующую логику.

//+------------------------------------------------------------------+
//| Parse symbols list into array                                    |
//+------------------------------------------------------------------+
void parse_symbols() {
   string temp = SymbolsList;                                   //--- Copy input symbols list
   num_symbols = 0;                                             //--- Reset symbol count
   while (StringFind(temp, ",") >= 0 && num_symbols < MAX_SYMBOLS) { //--- Loop through comma-separated symbols
      int pos = StringFind(temp, ",");                          //--- Find comma position
      string sym = StringSubstr(temp, 0, pos);                  //--- Extract symbol
      if (SymbolSelect(sym, true)) {                            //--- Select symbol if available
         symbols_array[num_symbols] = sym;                      //--- Store valid symbol
         num_symbols++;                                         //--- Increment count
      } else {                                                  //--- Handle unavailable symbol
         Print("Warning: Symbol ", sym, " not available.");     //--- Print warning
      }
      temp = StringSubstr(temp, pos + 1);                       //--- Update remaining string
   }
   if (StringLen(temp) > 0 && num_symbols < MAX_SYMBOLS) {      //--- Handle last symbol
      if (SymbolSelect(temp, true)) {                           //--- Select last symbol if available
         symbols_array[num_symbols] = temp;                     //--- Store last valid symbol
         num_symbols++;                                         //--- Increment count
      } else {                                                  //--- Handle unavailable last symbol
         Print("Warning: Symbol ", temp, " not available.");    //--- Print warning
      }
   }
   if (num_symbols < 2) {                                       //--- Check minimum symbols
      Print("Error: At least 2 valid symbols required. Found: ", num_symbols); //--- Print error
      ExpertRemove();                                           //--- Remove expert
   }
}

Здесь мы реализуем функцию "parse_symbols", чтобы преобразовать предоставленный пользователем список символов в полезный массив для панели. Она начинается с копирования входного параметра "SymbolsList" во временную строковую переменную и обнуления счетчика "num_symbols". Затем мы входим в цикл, который продолжается до тех пор, пока во временной строке обнаруживается запятая и количество символов не превышает предел "MAX_SYMBOLS". Внутри цикла мы определяем позицию следующей запятой с помощью StringFind, извлекаем подстроку символа от начала до этой позиции с помощью StringSubstr и пытаемся выбрать символ в обзоре рынка с помощью SymbolSelect с параметром true, чтобы добавить его при необходимости. В случае соблюдения условия мы сохраняем символ в массиве "symbols_array" по текущему индексу и увеличиваем "num_symbols"; в противном случае выводим предупреждение о недоступных символах. Мы обновляем временную строку до остатка после запятой, используя еще одну функцию «StringSubstr».

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

PARSED SYMBOLS' ARRAY

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

//+------------------------------------------------------------------+
//| Rank data for Spearman correlation                               |
//+------------------------------------------------------------------+
void rank_data(const double &data[], double &ranks[]) {
   int size = ArraySize(data);                                  //--- Get data size
   int indices[];                                               //--- Declare indices array
   ArrayResize(indices, size);                                  //--- Resize indices
   for (int i = 0; i < size; i++) indices[i] = i;               //--- Initialize indices

   // Sort indices based on data values
   for (int i = 0; i < size - 1; i++) {                         //--- Loop outer for sorting
      for (int j = i + 1; j < size; j++) {                      //--- Loop inner for comparison
         if (data[indices[i]] > data[indices[j]]) {             //--- Check if swap needed
            int temp = indices[i];                              //--- Store temporary
            indices[i] = indices[j];                            //--- Swap indices
            indices[j] = temp;                                  //--- Complete swap
         }
      }
   }

   // Assign ranks, handling ties
   for (int i = 0; i < size; ) {                                //--- Loop through sorted indices
      int start = i;                                            //--- Set start of tie group
      double value = data[indices[i]];                          //--- Get current value
      while (i < size && data[indices[i]] == value) i++;        //--- Skip ties
      double rank = (start + i - 1) / 2.0 + 1.0;                //--- Compute average rank
      for (int k = start; k < i; k++) {                         //--- Assign rank to group
         ranks[indices[k]] = rank;                              //--- Set rank
      }
   }
}

//+------------------------------------------------------------------+
//| Calculate Pearson correlation                                    |
//+------------------------------------------------------------------+
double pearson_correlation(const double &deltas1[], const double &deltas2[], int size) {
   double mean1 = 0, mean2 = 0;                                 //--- Initialize means
   for (int i = 0; i < size; i++) {                             //--- Loop to compute sums
      mean1 += deltas1[i];                                      //--- Accumulate first deltas
      mean2 += deltas2[i];                                      //--- Accumulate second deltas
   }
   mean1 /= size;                                               //--- Compute first mean
   mean2 /= size;                                               //--- Compute second mean

   double var1 = 0, var2 = 0, cov = 0;                          //--- Initialize variances and covariance
   for (int i = 0; i < size; i++) {                             //--- Loop to compute deviations
      double dev1 = deltas1[i] - mean1;                         //--- Compute first deviation
      double dev2 = deltas2[i] - mean2;                         //--- Compute second deviation
      var1 += dev1 * dev1;                                      //--- Accumulate first variance
      var2 += dev2 * dev2;                                      //--- Accumulate second variance
      cov += dev1 * dev2;                                       //--- Accumulate covariance
   }
   if (var1 == 0 || var2 == 0) return 0.0;                      //--- Return zero if no variance
   return cov / MathSqrt(var1 * var2);                          //--- Return correlation
}

//+------------------------------------------------------------------+
//| Calculate Spearman correlation                                   |
//+------------------------------------------------------------------+
double spearman_correlation(const double &deltas1[], const double &deltas2[], int size) {
   double ranks1[], ranks2[];                                   //--- Declare rank arrays
   ArrayResize(ranks1, size);                                   //--- Resize first ranks
   ArrayResize(ranks2, size);                                   //--- Resize second ranks
   rank_data(deltas1, ranks1);                                  //--- Rank first deltas
   rank_data(deltas2, ranks2);                                  //--- Rank second deltas
   return pearson_correlation(ranks1, ranks2, size);            //--- Return Pearson on ranks
}

//+------------------------------------------------------------------+
//| Calculate Kendall correlation                                    |
//+------------------------------------------------------------------+
double kendall_correlation(const double &deltas1[], const double &deltas2[], int size) {
   int concordant = 0, discordant = 0;                          //--- Initialize pair counts
   for (int i = 0; i < size - 1; i++) {                         //--- Loop outer pairs
      for (int j = i + 1; j < size; j++) {                      //--- Loop inner pairs
         double sign1 = deltas1[i] - deltas1[j];                //--- Compute first sign
         double sign2 = deltas2[i] - deltas2[j];                //--- Compute second sign
         if (sign1 * sign2 > 0) concordant++;                   //--- Increment concordant
         else if (sign1 * sign2 < 0) discordant++;              //--- Increment discordant
         // Ties are ignored in basic Kendall tau
      }
   }
   int total_pairs = size * (size - 1) / 2;                     //--- Compute total pairs
   if (total_pairs == 0) return 0.0;                            //--- Return zero if no pairs
   return (concordant - discordant) / (double)total_pairs;      //--- Return tau
}

Во-первых, мы реализуем функцию "rank_data" для присвоения рангов массиву значений данных, что крайне важно для метода корреляции Спирмена для обработки непараметрического ранга. Он принимает постоянную ссылку на массив данных и ссылку на массив рангов. Сначала мы определяем размер с помощью функции ArraySize и объявляем массив индексов, изменяя его размер до соответствия и инициализируя его последовательными значениями от нуля до размера минус один. Затем мы выполняем пузырьковую сортировку индексов на основе соответствующих значений данных, меняя индексы местами, если значение данных в текущем индексе больше, чем в следующем. После сортировки мы проходим циклом по отсортированным индексам, чтобы присвоить ранги, обрабатывая совпадения путем определения групп с одинаковым значением, вычисления среднего ранга для группы по формуле (начало + конец - 1) / 2,0 + 1,0 и применения этого ранга к исходным позициям посредством индексов.

Далее мы определяем функцию "pearson_correlation" для вычисления коэффициента корреляции Пирсона между двумя массивами ценовых дельт заданного размера. Она инициализирует среднее значение для обоих массивов равным нулю, затем в цикле суммирует значения дельт и вычисляет среднее значение путем деления на размер. Затем обнуляем дисперсии и ковариацию и снова выполняем цикл для вычисления отклонений от средних значений, накапливая квадраты отклонений для дисперсий и произведение для ковариации. Если хотя бы одна из дисперсий равна нулю, мы возвращаем 0,0, чтобы избежать деления на ноль; в противном случае мы возвращаем ковариацию, деленную на квадратный корень из произведения дисперсий, используя функцию MathSqrt. Затем мы создаём функцию "spearman_correlation" для вычисления корреляции рангов Спирмена. Она объявляет и изменяет размер двух массивов рангов до размера входных данных, вызывает функцию "rank_data" для каждого массива дельт, чтобы заполнить ранги, и возвращает результат функции "pearson_correlation", примененной к этим массивам рангов, вместо исходных дельт.

Наконец, мы реализуем функцию "kendall_correlation" для коэффициента тау Кенделла. Счетчики для согласованных и несогласованных пар инициализируются нулем. Мы используем вложенные циклы по всему объему данных для сравнения каждой пары элементов и вычисления знаков как разностей в каждом массиве. Если произведение знаков положительное, то увеличивается количество согласованных пар; если отрицательное, то несогласованных — связи игнорируются. Общее количество пар рассчитывается как размер, умноженный на (размер - 1) / 2. Если пар нет, мы возвращаем 0,0, или разницу между согласованными и несогласованными значениями, деленную на общее количество пар. При обычном бивариантном сравнении наполнения они выглядят следующим образом.

PEARSON, SPEARMAN & KENDALL BIVARIATE COMPARISON

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

//+------------------------------------------------------------------+
//| Calculate correlation based on method                            |
//+------------------------------------------------------------------+
double calculate_correlation(string sym1, string sym2) {
   if (sym1 == sym2) return 1.0;                                //--- Return self-correlation

   double prices1[], prices2[];                                 //--- Declare price arrays
   ArrayResize(prices1, CorrelationBars);                       //--- Resize first prices
   ArrayResize(prices2, CorrelationBars);                       //--- Resize second prices
   if (CopyClose(sym1, global_correlation_tf, 0, CorrelationBars, prices1) < CorrelationBars || //--- Copy first closes or check failure
       CopyClose(sym2, global_correlation_tf, 0, CorrelationBars, prices2) < CorrelationBars) {
      return 0.0;                                               //--- Return insufficient data
   }

   // Compute price changes (deltas)
   double deltas1[], deltas2[];                                 //--- Declare delta arrays
   ArrayResize(deltas1, CorrelationBars - 1);                   //--- Resize first deltas
   ArrayResize(deltas2, CorrelationBars - 1);                   //--- Resize second deltas
   for (int i = 0; i < CorrelationBars - 1; i++) {              //--- Loop to compute deltas
      deltas1[i] = prices1[i + 1] - prices1[i];                 //--- Set first delta
      deltas2[i] = prices2[i + 1] - prices2[i];                 //--- Set second delta
   }

   int size = CorrelationBars - 1;                              //--- Set effective size
   switch (CorrMethod) {                                        //--- Switch on method
      case PEARSON:                                             //--- Handle Pearson
         return pearson_correlation(deltas1, deltas2, size);    //--- Return Pearson result
      case SPEARMAN:                                            //--- Handle Spearman
         return spearman_correlation(deltas1, deltas2, size);   //--- Return Spearman result
      case KENDALL:                                             //--- Handle Kendall
         return kendall_correlation(deltas1, deltas2, size);    //--- Return Kendall result
      default:                                                  //--- Handle default
         return 0.0;                                            //--- Return zero
   }
}

//+------------------------------------------------------------------+
//| Update correlation matrix values                                 |
//+------------------------------------------------------------------+
void update_correlations() {
   int n = CorrelationBars - 1;                                 //--- Set sample size
   if (n < 2) {                                                 //--- Check minimum bars
      Print("Error: Insufficient bars for correlation (need at least 3)."); //--- Print error
      return;                                                   //--- Exit function
   }
   for (int i = 0; i < num_symbols; i++) {                      //--- Loop rows
      for (int j = 0; j < num_symbols; j++) {                   //--- Loop columns
         double corr = calculate_correlation(symbols_array[i], symbols_array[j]); //--- Compute correlation
         correlation_matrix[i][j] = corr;                       //--- Store correlation
         if (i == j) {                                          //--- Handle diagonal
            pvalue_matrix[i][j] = 0.0;                          //--- Set p-value to zero
         } else if (corr == 0.0 && n < 3) {                     //--- Handle insufficient data
            pvalue_matrix[i][j] = 1.0;                          //--- Set p-value to one
         } else {                                               //--- Handle normal case
            pvalue_matrix[i][j] = calculate_pvalue(corr, CorrMethod, n); //--- Compute and store p-value
         }
      }
   }
}

Мы создаём функцию "calculate_correlation" для вычисления коэффициента корреляции между двумя символами, возвращающую значение типа double. Она принимает строковые параметры для sym1 и sym2. Если они идентичны, функция немедленно возвращает 1,0, что означает идеальную самокорреляцию. Мы объявляем и изменяем размер двух массивов типа double "prices1" и "prices2" до размера "CorrelationBars". Мы используем CopyClose для получения цен закрытия на каждый символ из таймфрейма "global_correlation_tf", начиная с нуля текущего бара. Если ни одна из копий не может получить полное количество, возвращаем 0,0, указывая на недостаточность данных.

Чтобы сосредоточиться на изменениях цен, мы объявляем и изменяем размер дельта-массивов "deltas1" и "deltas2" на единицу меньше, чем "CorrelationBars". Мы используем цикл для вычисления каждой дельты как разницы между последовательными ценами. Действующий размер устанавливается равным "CorrelationBars" минус один. Оператор switch на перечислении "CorrMethod" вызывает соответствующую функцию корреляции. Для "PEARSON" мы вызываем "pearson_correlation" с помощью delta и size. Для "SPEARMAN" вызываем "spearman_correlation". Для "KENDALL" вызываем "kendall_correlation". Значение по умолчанию возвращает 0.0.

Далее мы реализуем функцию "update_correlations", чтобы заполнить корреляционные матрицы и матрицы p-значений новыми вычислениями. Она вычисляет размер выборки n как "CorrelationBars" минус единица и проверяет, меньше ли она двух, выводя ошибку, если это так, поскольку для значимых корреляций требуется как минимум три бара, а затем завершает работу досрочно. Мы используем вложенные циклы по "num_symbols" для рядов i и столбцов j, вызывая "calculate_correlation" с соответствующими символами из "symbols_array", чтобы получить corr и сохранить его в "correlation_matrix" в [i][j]. Для p-значений, если i равно j для диагонали, мы устанавливаем значение "pvalue_matrix" в [i][j] равным 0.0; если значение corr равно 0.0, а n меньше трех, устанавливаем значение 1.0; в противном случае вычисляем его, используя "calculate_pvalue" с помощью corr, "CorrMethod" и n, сохраняя результат. Далее нам понадобятся вспомогательные программы для создания тепловой карты.

//+------------------------------------------------------------------+
//| Get significance stars based on p-value                          |
//+------------------------------------------------------------------+
string get_significance_stars(double pval) {
   if (pval < PValueThreshold1) return "***";                   //--- Return three stars
   if (pval < PValueThreshold2) return "**";                    //--- Return two stars
   if (pval < PValueThreshold3) return "*";                     //--- Return one star
   return "";                                                   //--- Return empty
}

//+------------------------------------------------------------------+
//| Interpolate between multiple colors based on value (-1 to 1)     |
//+------------------------------------------------------------------+
color interpolate_heatmap_color(double value) {
   if (value == 0.0) return ColorNeutralBg;                     //--- Return neutral for zero
   double abs_val = MathAbs(value);                             //--- Compute absolute value
   int num_stops = ArraySize(heatmap_colors) / 2;               //--- Compute stops per side
   double step = 1.0 / (num_stops - 1);                         //--- Compute step size

   if (value > 0.0) {                                           //--- Handle positive
      int idx = (int)MathFloor(abs_val / step);                 //--- Compute index
      if (idx >= num_stops - 1) idx = num_stops - 2;            //--- Clamp index
      double factor = (abs_val - idx * step) / step;            //--- Compute factor
      return interpolate_color(heatmap_colors[idx + num_stops], heatmap_colors[idx + num_stops + 1], factor); //--- Interpolate positive
   } else {                                                     //--- Handle negative
      int idx = (int)MathFloor(abs_val / step);                 //--- Compute index
      if (idx >= num_stops - 1) idx = num_stops - 2;            //--- Clamp index
      double factor = (abs_val - idx * step) / step;            //--- Compute factor
      return interpolate_color(heatmap_colors[idx], heatmap_colors[idx + 1], factor); //--- Interpolate negative
   }
}

//+------------------------------------------------------------------+
//| Interpolate between two colors based on factor (0 to 1)          |
//+------------------------------------------------------------------+
color interpolate_color(color c1, color c2, double factor) {
   uchar r1 = (uchar)(c1 & 0xFF), g1 = (uchar)((c1 >> 8) & 0xFF), b1 = (uchar)((c1 >> 16) & 0xFF); //--- Extract RGB from first color
   uchar r2 = (uchar)(c2 & 0xFF), g2 = (uchar)((c2 >> 8) & 0xFF), b2 = (uchar)((c2 >> 16) & 0xFF); //--- Extract RGB from second color
   uchar r = (uchar)MathMax(0, MathMin(255, r1 + factor * (r2 - r1) + 0.5)); //--- Interpolate red
   uchar g = (uchar)MathMax(0, MathMin(255, g1 + factor * (g2 - g1) + 0.5)); //--- Interpolate green
   uchar b = (uchar)MathMax(0, MathMin(255, b1 + factor * (b2 - b1) + 0.5)); //--- Interpolate blue
   return (color)((b << 16) | (g << 8) | r);                    //--- Return interpolated color
}

Определяем функцию "get_significance_stars" для определения количества символов звездочки, представляющих статистическую значимость, на основе заданного p-значения, возвращая строку. Она принимает параметр типа double "pval" и использует условные проверки: если "pval" меньше, чем "PValueThreshold1", мы возвращаем "***" для наивысшего значения; если меньше, чем "PValueThreshold2", возвращаем "**"; если меньше, чем "PValueThreshold3", возвращаем "*". В противном случае вернем пустую строку, не имеющую значения.

Далее мы реализуем функцию "interpolate_heatmap_color", чтобы сгенерировать цвет для режима тепловой карты путем интерполяции в массиве градиентов на основе значения корреляции между отрицательным значением и единицей, возвращая тип цвета. Она обрабатывает ноль, возвращая непосредственно "ColorNeutralBg". Мы вычисляем абсолютное значение "abs_val", определяем количество стопов для сторон, деля размер массива "heatmap_colors" на два, и вычисляем размер шага как единицу, деленную на стопы минус единицу. Для положительных значений мы находим индекс с помощью MathFloor типа "abs_val" с шагом, фиксируем его, если он находится на последнем интервале или за его пределами, вычисляем коэффициент как остаток с шагом и вызываем "interpolate_color" с цветами из положительной половины массива плюс коэффициент. Для отрицательных значений поступаем аналогично, но используем отрицательную половину массива.

Затем создаем функцию "interpolate_color" для линейного смешивания двух цветов на основе коэффициента от нуля до единицы, возвращая результирующий цвет. Она извлекает красный, зеленый и синий компоненты в виде символов без знака из "c1" и "c2", используя побитовые операции: маскируя с помощью 0xFF для красного, сдвигая вправо на восемь и маскируя для зеленого, и сдвигая на шестнадцать для синего. Мы интерполируем каждый канал, начиная со значения первого цвета, добавляя коэффициент, умноженный на разницу, ко второму, добавляя 0,5 для округления и фиксируя значение от нуля до 255 с помощью функций  MathMax и MathMin. Наконец, объединяем интерполированные компоненты в единое цветовое значение, используя битовые сдвиги: синий смещается влево на шестнадцать, зеленый - на восемь, а также красный, затем побитово ИЛИ вместе. Теперь мы можем приступить к созданию панели, поскольку у нас есть большинство вспомогательных функций, необходимых для выполнения вычислений.

//+------------------------------------------------------------------+
//| Create rectangle for UI                                          |
//+------------------------------------------------------------------+
bool create_rectangle(string object_name, int x_distance, int y_distance, int x_size, int y_size, 
                      color background_color, color border_color = clrNONE) {
   if (!ObjectCreate(0, object_name, OBJ_RECTANGLE_LABEL, 0, 0, 0)) {        //--- Create rectangle or check failure
      Print(__FUNCTION__, ": failed to create Rectangle: ", GetLastError()); //--- Print error
      return false;                                                          //--- Return failure
   }
   ObjectSetInteger(0, object_name, OBJPROP_XDISTANCE, x_distance);          //--- Set x distance
   ObjectSetInteger(0, object_name, OBJPROP_YDISTANCE, y_distance);          //--- Set y distance
   ObjectSetInteger(0, object_name, OBJPROP_XSIZE, x_size);                  //--- Set x size
   ObjectSetInteger(0, object_name, OBJPROP_YSIZE, y_size);                  //--- Set y size
   ObjectSetInteger(0, object_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);      //--- Set corner
   ObjectSetInteger(0, object_name, OBJPROP_BGCOLOR, background_color);      //--- Set background
   ObjectSetInteger(0, object_name, OBJPROP_COLOR, border_color);            //--- Set border color
   ObjectSetInteger(0, object_name, OBJPROP_BORDER_TYPE, BORDER_FLAT);       //--- Set border type
   ObjectSetInteger(0, object_name, OBJPROP_BACK, false);                    //--- Set back property
   return true;                                                              //--- Return success
}

//+------------------------------------------------------------------+
//| Create text label for UI                                         |
//+------------------------------------------------------------------+
bool create_label(string object_name, string text, int x_distance, int y_distance, int font_size = 10, 
                  color text_color = COLOR_WHITE, string font = "Arial Rounded MT Bold") {
   if (!ObjectCreate(0, object_name, OBJ_LABEL, 0, 0, 0)) {                 //--- Create label or check failure
      Print(__FUNCTION__, ": failed to create Label: ", GetLastError());    //--- Print error
      return false;                                                         //--- Return failure
   }
   ObjectSetInteger(0, object_name, OBJPROP_XDISTANCE, x_distance);         //--- Set x distance
   ObjectSetInteger(0, object_name, OBJPROP_YDISTANCE, y_distance);         //--- Set y distance
   ObjectSetInteger(0, object_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);     //--- Set corner
   ObjectSetString(0, object_name, OBJPROP_TEXT, text);                     //--- Set text
   ObjectSetString(0, object_name, OBJPROP_FONT, font);                     //--- Set font
   ObjectSetInteger(0, object_name, OBJPROP_FONTSIZE, font_size);           //--- Set font size
   ObjectSetInteger(0, object_name, OBJPROP_COLOR, text_color);             //--- Set text color
   ObjectSetInteger(0, object_name, OBJPROP_ANCHOR, ANCHOR_CENTER);         //--- Set anchor
   return true;                                                             //--- Return success
}


//+------------------------------------------------------------------+
//| Create full dashboard UI                                         |
//+------------------------------------------------------------------+
void create_full_dashboard() { 
   color main_bg = C'30,30,30';                                 //--- Set main background
   color header_bg = C'60,60,60';                               //--- Set header background
   color text_color = COLOR_WHITE;                              //--- Set text color
   color neutral_bg = ColorNeutralBg;                           //--- Set neutral background
   color button_text = clrGold;                                 //--- Set button text color
   color theme_icon_color = clrWhite;                           //--- Set theme icon color
   color close_text = clrWhite;                                 //--- Set close text color
   color header_icon_color = clrAqua;                           //--- Set header icon color
   int panel_width = WIDTH_SYMBOL + num_symbols * (WIDTH_CELL - 1) + 4; //--- Compute panel width
   int panel_height = HEIGHT_HEADER + HEIGHT_TF_CELL + GAP_HEIGHT + HEIGHT_RECTANGLE * (num_symbols + 1) - num_symbols + 2; //--- Compute panel height
   create_rectangle(MAIN_PANEL, panel_x, panel_y, panel_width, panel_height, main_bg); //--- Create main panel
   create_rectangle(HEADER_PANEL, panel_x, panel_y, panel_width, HEIGHT_HEADER, header_bg); //--- Create header panel
   create_label(HEADER_PANEL_ICON, CharToString(181), panel_x + 12, panel_y + 14, 18, header_icon_color, "Wingdings"); //--- Create header icon
   create_label(HEADER_PANEL_TEXT, "Correlation Matrix", panel_x + 90, panel_y + 12, 13, text_color); //--- Create header text
   create_label(CLOSE_BUTTON, CharToString('r'), panel_x + (panel_width - 17), panel_y + 14, 18, close_text, "Webdings"); //--- Create close button
   create_label(TOGGLE_BUTTON, CharToString('r'), panel_x + (panel_width - 47), panel_y + 14, 18, button_text, "Wingdings"); //--- Create toggle button
   string heatmap_icon = CharToString(global_display_mode == MODE_STANDARD ? (uchar)82 : (uchar)110); //--- Set heatmap icon
   create_label(HEATMAP_BUTTON, heatmap_icon, panel_x + (panel_width - 77), panel_y + 14, 18, button_text, "Wingdings"); //--- Create heatmap button
   create_label(PVAL_BUTTON, CharToString('X'), panel_x + (panel_width - 107), panel_y + 14, 18, button_text, "Wingdings"); //--- Create PVAL button
   string sort_icon = CharToString('N');                        //--- Set sort icon
   create_label(SORT_BUTTON, sort_icon, panel_x + (panel_width - 137), panel_y + 14, 18, button_text, "Wingdings 3"); //--- Create sort button
   create_label(THEME_BUTTON, CharToString('['), panel_x + (panel_width - 167), panel_y + 14, 18, theme_icon_color, "Wingdings"); //--- Create theme button

   // Timeframe cells row
   int tf_y = panel_y + HEIGHT_HEADER;                          //--- Compute TF y position
   int tf_x_start = panel_x + 2;                                //--- Set TF start x
   for (int i = 0; i < num_tf_visible; i++) {                   //--- Loop visible TFs
      int x_offset = tf_x_start + i * WIDTH_TF_CELL;            //--- Compute offset
      string rect_name = TF_CELL_RECT + IntegerToString(i);     //--- Get rectangle name
      string text_name = TF_CELL_TEXT + IntegerToString(i);     //--- Get text name
      color bg = (i == current_tf_index) ? ColorStrongPositiveBg : header_bg; //--- Set background
      create_rectangle(rect_name, x_offset, tf_y, WIDTH_TF_CELL, HEIGHT_TF_CELL, bg); //--- Create TF rectangle
      create_label(text_name, tf_strings[i], x_offset + (WIDTH_TF_CELL / 2), tf_y + (HEIGHT_TF_CELL / 2), 10, text_color, "Arial Bold"); //--- Create TF text
   }

   // Create row symbols (left column), pushed down
   int matrix_y = tf_y + HEIGHT_TF_CELL + GAP_HEIGHT;           //--- Compute matrix y
   create_rectangle("SYMBOL_ROW_HEADER", panel_x + 2, matrix_y, WIDTH_SYMBOL, HEIGHT_RECTANGLE, header_bg); //--- Create row header rectangle
   create_label("SYMBOL_ROW_HEADER_TEXT", "Symbols", panel_x + (WIDTH_SYMBOL / 2 + 2), matrix_y + (HEIGHT_RECTANGLE / 2), 10, text_color, "Arial Bold"); //--- Create row header text
   for (int i = 0; i < num_symbols; i++) {                      //--- Loop row symbols
      int y_offset = matrix_y + HEIGHT_RECTANGLE * (i + 1) - (1 + i); //--- Compute y offset
      create_rectangle(SYMBOL_ROW_RECTANGLE + IntegerToString(i), panel_x + 2, y_offset, WIDTH_SYMBOL, HEIGHT_RECTANGLE, header_bg); //--- Create row rectangle
      create_label(SYMBOL_ROW_TEXT + IntegerToString(i), symbols_array[i], panel_x + (WIDTH_SYMBOL / 2 + 2), y_offset + (HEIGHT_RECTANGLE / 2 - 1), 10, text_color, "Arial Bold"); //--- Create row text
   }

   // Create column symbols (top row), pushed down
   for (int j = 0; j < num_symbols; j++) {                      //--- Loop column symbols
      int x_offset = panel_x + WIDTH_SYMBOL + j * WIDTH_CELL - j + 1; //--- Compute x offset
      create_rectangle(SYMBOL_COL_RECTANGLE + IntegerToString(j), x_offset, matrix_y, WIDTH_CELL, HEIGHT_RECTANGLE, header_bg); //--- Create column rectangle
      create_label(SYMBOL_COL_TEXT + IntegerToString(j), symbols_array[j], x_offset + (WIDTH_CELL / 2), matrix_y + (HEIGHT_RECTANGLE / 2), 10, text_color, "Arial Bold"); //--- Create column text
   }

   // Create correlation cells, pushed down
   for (int i = 0; i < num_symbols; i++) {                      //--- Loop rows for cells
      int y_offset = matrix_y + HEIGHT_RECTANGLE * (i + 1) - (1 + i); //--- Compute y offset
      for (int j = 0; j < num_symbols; j++) {                   //--- Loop columns for cells
         string cell_name = CELL_RECTANGLE + IntegerToString(i) + "_" + IntegerToString(j); //--- Get cell name
         string text_name = CELL_TEXT + IntegerToString(i) + "_" + IntegerToString(j); //--- Get text name
         int x_offset = panel_x + WIDTH_SYMBOL + j * WIDTH_CELL - j + 1; //--- Compute x offset
         create_rectangle(cell_name, x_offset, y_offset, WIDTH_CELL, HEIGHT_RECTANGLE, neutral_bg); //--- Create cell rectangle
         create_label(text_name, "0.00", x_offset + (WIDTH_CELL / 2), y_offset + (HEIGHT_RECTANGLE / 2 - 1), 10, text_color, "Arial"); //--- Create cell text
      }
   }

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

Здесь мы определяем функцию "create_rectangle" для генерации прямоугольного графического объекта для пользовательского интерфейса, возвращающую логическое значение в случае успеха. Функция принимает в качестве параметров имя объекта в виде строки, целочисленные значения расстояний по осям x и y, размеры, цвет фона, а также опционный цвет рамки, который по умолчанию отсутствует. Мы пытаемся создать объект с помощью функции ObjectCreate с нулевым подокном, именем типа OBJ_RECTANGLE_LABEL и координатами по умолчанию, выводя сообщение об ошибке с именем функции и последним кодом ошибки в случае неудачи и возвращая значение false. В случае успеха мы настраиваем свойства с помощью ObjectSetInteger: расстояния по осям x и y, размеры, угол до левого верхнего угла, цвет фона, цвет рамки, тип границы (плоский) и обратно на false, затем возвращаем значение true.

Далее мы реализуем функцию "create_label", которая создает объект текстовой метки для пользовательского интерфейса и возвращает логическое значение "success". Она принимает имя объекта и текст в виде строк, расстояния по осям x и y, опциональный размер шрифта (по умолчанию — десять), цвет текста (по умолчанию — белый) и шрифт (по умолчанию — Arial Rounded MT Bold). Мы используем "ObjectCreate" с типом OBJ_LABEL  и выводим сообщение об ошибке в случае сбоя. В случае успеха мы устанавливаем расстояния по осям x и y, угол в левом верхнем углу, текст, шрифт, размер шрифта, цвет текста и привязку к центру, используя соответствующие вызовы функций "ObjectSetInteger" и ObjectSetString, возвращая значение true.

Затем создаём функцию "create_full_dashboard", которая, используя указанные выше вспомогательные функции, формирует всю структуру пользовательского интерфейса. Она задает локальные цвета, такие как основной фон — темно-серый, фон заголовка — средне-серый, цвет текста — белый, нейтральный фон поля ввода, текст кнопок — золотой, тема и значки закрытия — белый, а значок заголовка — аква (aqua). Рассчитываем ширину панели на основе ширины символа плюс ячеек, скорректированную с учетом количества символов, и высоту, включающую заголовок, строку таймфрейма, пробел и строки матрицы.

Вызываем "create_rectangle" для главной панели и панели заголовков, затем "create_label" для таких элементов, как значок заголовка с символом 181 из Wingdings, заголовок в качестве корреляционной матрицы, кнопка закрытия с буквой "r" из Webdings, кнопка переключения аналогично, кнопка тепловой карты с динамическим значком на основе режима отображения с использованием приведений CharToString и uchar, кнопка p-значений с 'X' из Wingdings, кнопка сортировки с 'N' из Wingdings 3 и кнопка темы с '[' из Wingdings. Вы можете использовать любой из приведенных ниже символов шрифта по вашему выбору.

SYMBOLS FONT

Для строки таймфрейма рассчитываем позицию y под заголовком и начало оси x, затем перебираем видимые таймфреймы, чтобы создать прямоугольники и метки: рассчитываем смещения, формируем названия с префиксом ячейки таймфрейма и строкой индекса, устанавливаем фон в зависимости от текущего индекса на ярко-положительный цвет или цвет заголовка, а также добавляем метки со строками таймфреймов, набранными шрифтом Arial Bold. Устанавливаем положение матрицы по оси Y ниже строки таймфрейма плюс промежуток, затем создаем прямоугольник заголовка строки и обозначаем его как Symbols («Символы»). Для символов строк слева мы используем цикл для вычисления смещений по оси Y, создаем прямоугольники с префиксом и индексом строки, а также метки с именами символов из массива, выделенные шрифтом Arial Bold. Аналогично, для символов столбцов вверху мы используем цикл для вычисления смещений по оси x, создания прямоугольников с префиксами столбцов и меток с названиями символов.

Для ячеек корреляции мы вкладываем циклы по строкам и столбцам, вычисляя смещения, формируя имена ячеек с префиксом в виде прямоугольника и индексами, разделенными подчеркиванием, и аналогично текстовые имена, затем вызываем функцию "create_rectangle" с нейтральным фоном и функцию "create_label", инициализированную значением 0.00 шрифтом Arial. Наконец, перерисовываем график с помощью функции ChartRedraw в нулевом подокне. Теперь мы можем вызвать эту функцию в обработчике "OnInit", чтобы она выполнила основную работу.

//+------------------------------------------------------------------+
//| Initialize expert                                                |
//+------------------------------------------------------------------+
int OnInit() {
   global_display_mode = DashboardMode;                         //--- Set display mode
   global_correlation_tf = (CorrelationTimeframe == PERIOD_CURRENT ? (ENUM_TIMEFRAMES)_Period : CorrelationTimeframe); //--- Set timeframe
   for (int i = 0; i < NUM_TF; i++) {                           //--- Loop to find index
      if (tf_list[i] == global_correlation_tf) {                //--- Check match
         current_tf_index = i;                                  //--- Set index
         break;                                                 //--- Exit loop
      }
   }
   if (current_tf_index == -1) current_tf_index = 3;            //--- Default to H1
   global_correlation_tf = tf_list[current_tf_index];           //--- Update timeframe
   parse_symbols();                                             //--- Parse symbols
   int panel_width = WIDTH_SYMBOL + num_symbols * (WIDTH_CELL - 1) + 4; //--- Compute width
   num_tf_visible = MathMin(NUM_TF, (panel_width - 2) / WIDTH_TF_CELL); //--- Set visible TFs
   if (current_tf_index >= num_tf_visible) current_tf_index = num_tf_visible - 1; //--- Clamp index
   global_correlation_tf = tf_list[current_tf_index];           //--- Update timeframe
   ArrayInitialize(pvalue_matrix, 1.0);                         //--- Initialize p-values
   create_full_dashboard();                                     //--- Create dashboard
   return(INIT_SUCCEEDED);                                      //--- Return success
}

В обработчике OnInit мы инициализируем параметры выполнения программы и пользовательский интерфейс. Устанавливаем параметр "global_display_mode" равным вводному значению "DashboardMode" и определяем "global_correlation_tf", проверяя, является ли "CorrelationTimeframe" значением PERIOD_CURRENT. В этом случае мы преобразуем текущий период графика _Period в ENUM_TIMEFRAMES; в противном случае мы используем входные параметры напрямую. Затем мы проходим циклом по массиву "tf_list", чтобы найти индекс, соответствующий "global_correlation_tf", и присваиваем его значению "current_tf_index", завершая работу досрочно при совпадении. Если совпадение не найдено — в этом случае значение параметра "current_tf_index" будет равно минус единице — мы по умолчанию устанавливаем его равным трем, что соответствует часовому таймфрейму. Для обеспечения согласованности мы обновляем значение параметра "global_correlation_tf" до значения, соответствующего этому индексу в "tf_list".

Далее мы вызываем функцию "parse_symbols" для обработки списка символов. Вычисляем "panel_width" на основе "WIDTH_SYMBOL" плюс поправка на количество символов, умноженная на "WIDTH_CELL". Значение "num_tf_visible" устанавливается равным минимальному значению "NUM_TF" и доступного пространства в панели, деленного на "WIDTH_TF_CELL". Если значение "current_tf_index" превышает это видимое значение, мы ограничиваем его последним видимым индексом минус один и соответствующим образом вновь обновляем значение "global_correlation_tf". Перед расчетами мы инициализируем массив "pvalue_matrix" значением 1.0, используя параметр ArrayInitialize в качестве состояния по умолчанию. Наконец, мы вызываем функцию "create_full_dashboard" для построения пользовательского интерфейса и возвращаем INIT_SUCCEEDED, что указывает на успешную инициализацию. После компиляции получаем следующий результат.

INITIAL DASHBOARD

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

//+------------------------------------------------------------------+
//| Recreate legend objects based on current mode                    |
//+------------------------------------------------------------------+
void recreate_legend() {
   // Delete existing legend objects
   for (int i = 0; i < NUM_LEGEND_ITEMS; i++) {                 //--- Loop legend items
      ObjectDelete(0, LEGEND_CELL_RECTANGLE + IntegerToString(i)); //--- Delete rectangle
      ObjectDelete(0, LEGEND_CELL_TEXT + IntegerToString(i));   //--- Delete text
   }

   // Define full correlation values based on mode (more points for finer legend)
   double full_corr_vals[];                                     //--- Declare full values array
   if (global_display_mode == MODE_HEATMAP) {                   //--- Handle heatmap mode
      double heatmap_vals[] = {-1.0, -0.8, -0.6, -0.4, -0.2, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0}; //--- Set heatmap values
      ArrayCopy(full_corr_vals, heatmap_vals);                  //--- Copy to full
   } else {                                                     //--- Handle standard mode
      double standard_vals[] = {StrongNegativeThresholdPct / 100.0, -0.75, -0.5, -0.25, 0.0, 0.25, 0.5, 0.75, StrongPositiveThresholdPct / 100.0, 1.0}; //--- Set standard values
      ArrayCopy(full_corr_vals, standard_vals);                 //--- Copy to full
   }

   // Copy to visible and reduce dynamically if needed
   ArrayCopy(visible_corr_vals, full_corr_vals);                //--- Copy to visible
   int panel_width = WIDTH_SYMBOL + num_symbols * (WIDTH_CELL - 1) + 4; //--- Compute panel width
   int available_width = panel_width - 4;                       //--- Compute available width
   double item_cost = WIDTH_LEGEND_CELL + LEGEND_SPACING;       //--- Compute item cost
   int max_fit = (int)MathFloor((available_width + LEGEND_SPACING) / item_cost); //--- Compute max fit
   if (max_fit < 1) max_fit = 1;                                //--- Ensure minimum fit

   while (ArraySize(visible_corr_vals) > max_fit) {             //--- Loop to reduce
      int sz = ArraySize(visible_corr_vals);                    //--- Get current size
      int zero_pos = -1;                                        //--- Initialize zero position
      for (int p = 0; p < sz; p++) {                            //--- Loop to find zero
         if (visible_corr_vals[p] == 0.0) {                     //--- Check for zero
            zero_pos = p;                                       //--- Set position
            break;                                              //--- Exit loop
         }
      }
      if (zero_pos >= 0) {                                      //--- Handle found zero
         ArrayRemove(visible_corr_vals, zero_pos, 1);           //--- Remove zero
         continue;                                              //--- Continue reduction
      }

      // Find min abs > 0
      double min_abs = DBL_MAX;                                 //--- Initialize min abs
      for (int p = 0; p < sz; p++) {                            //--- Loop to find min
         double av = MathAbs(visible_corr_vals[p]);             //--- Get absolute
         if (av > 0 && av < min_abs) min_abs = av;              //--- Update min
      }
      if (min_abs == DBL_MAX) break;                            //--- Exit if no more

      // Remove all == ±min_abs
      for (int p = sz - 1; p >= 0; p--) {                       //--- Loop backward to remove
         if (MathAbs(visible_corr_vals[p]) == min_abs) ArrayRemove(visible_corr_vals, p, 1); //--- Remove match
      }
   }

   num_legend_visible = ArraySize(visible_corr_vals);           //--- Set visible count

   // Calculate positions
   int panel_height = HEIGHT_HEADER + HEIGHT_TF_CELL + GAP_HEIGHT + HEIGHT_RECTANGLE * (num_symbols + 1) - num_symbols + 2; //--- Compute panel height
   int legend_y = panel_y + panel_height + GAP_MAIN_LEGEND;     //--- Compute legend y
   int total_legend_width = num_legend_visible * WIDTH_LEGEND_CELL + (num_legend_visible - 1) * LEGEND_SPACING; //--- Compute total width
   int x_start = panel_x + (panel_width - total_legend_width) / 2; //--- Compute start x

   color neutral_bg = ColorNeutralBg;                           //--- Set neutral background
   color text_color = COLOR_WHITE;                              //--- Set text color

   // Create new legend objects
   for (int i = 0; i < num_legend_visible; i++) {               //--- Loop to create
      int x_offset = x_start + i * (WIDTH_LEGEND_CELL + LEGEND_SPACING); //--- Compute offset
      string rect_name = LEGEND_CELL_RECTANGLE + IntegerToString(i); //--- Get rectangle name
      string text_name = LEGEND_CELL_TEXT + IntegerToString(i); //--- Get text name
      create_rectangle(rect_name, x_offset, legend_y + 2, WIDTH_LEGEND_CELL, HEIGHT_LEGEND, neutral_bg); //--- Create rectangle
      create_label(text_name, "0%", x_offset + WIDTH_LEGEND_CELL / 2, legend_y + 2 + HEIGHT_LEGEND / 2 - 1, 10, text_color, "Arial"); //--- Create label
   }
}

//+------------------------------------------------------------------+
//| Create full dashboard UI                                         |
//+------------------------------------------------------------------+
void create_full_dashboard() { 

   //--- Other dashboard creation logic

   // Create separate legend panel
   int legend_y = panel_y + panel_height + GAP_MAIN_LEGEND;     //--- Compute legend y
   create_rectangle(LEGEND_PANEL, panel_x, legend_y, panel_width, HEIGHT_LEGEND_PANEL, main_bg); //--- Create legend panel
   recreate_legend();                                           //--- Recreate legend
   ChartRedraw(0);                                              //--- Redraw chart
}

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

Мы объявляем полный массив значений корреляции и заполняем его условно: для режима тепловой карты, используя локальный массив со значениями от минус 1,0 до 1,0, с шагом 0,2, скопированными в него; для стандартного режима, используя пороговые значения, преобразованные в десятичные дроби и фиксированные точки, такие как минус 0,75. Чтобы подогнать под размер пространства, мы копируем данные в видимый массив, вычисляем ширину панели, доступную ширину, стоимость элемента с учетом пробелов и максимальное размещение с разделением пола, гарантируя наличие как минимум одного элемента. В цикле, если массив превышает заданный размер, мы удаляем ноль, если он присутствует, или находим и удаляем все экземпляры наименьшего ненулевого абсолютного значения, сканируя массив и используя функцию ArrayRemove в обратном порядке.

Мы установили видимое количество равным обновленному размеру массива. Для позиционирования мы рассчитываем высоту панели на основе констант и строк, легенду по оси y под панелью с отступом, общую ширину легенды с учетом ячеек и интервалов, а также начальную координату x для центрирования. Мы задаем нейтральный цвет фона и текста, затем в цикле создаем прямоугольники и метки: вычисляем смещения, формируем имена, используем функцию "create_rectangle" с нейтральным фоном и функцию "create_label", инициализированную значением 0% шрифтом Arial. В функции "create_full_dashboard", после выполнения остальной логики интерфейса, мы вычисляем значение легенды по оси Y и создаем прямоугольник панели легенды с основным фоном, вызываем функцию "recreate_legend" для его заполнения и перерисовываем график. После компиляции получаем следующий результат.

PANEL WITH LEGEND

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

//+------------------------------------------------------------------+
//| Update TF highlights                                             |
//+------------------------------------------------------------------+
void update_tf_highlights() {
   color inactive_bg = C'60,60,60';                             //--- Set inactive background
   for (int i = 0; i < num_tf_visible; i++) {                   //--- Loop visible TFs
      string rect_name = TF_CELL_RECT + IntegerToString(i);     //--- Get rectangle name
      color bg = (i == current_tf_index) ? ColorStrongPositiveBg : inactive_bg; //--- Set background
      ObjectSetInteger(0, rect_name, OBJPROP_BGCOLOR, bg);      //--- Update background
   }
   ChartRedraw(0);                                              //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Update legend colors and texts based on mode                     |
//+------------------------------------------------------------------+
void update_legend() {
   color default_txt = ColorTextStrong;                         //--- Set default text color
   for (int i = 0; i < num_legend_visible; i++) {               //--- Loop visible legends
      string rect_name = LEGEND_CELL_RECTANGLE + IntegerToString(i); //--- Get rectangle name
      string text_name = LEGEND_CELL_TEXT + IntegerToString(i); //--- Get text name
      double corr = visible_corr_vals[i];                       //--- Get correlation value
      int decimals = (MathAbs(corr) == 0.5 || corr == 0.0 || MathAbs(corr) == 1.0) ? 0 : 1; //--- Set decimals
      string txt_str = DoubleToString(corr * 100, decimals) + "%"; //--- Format text
      color bg_color = ColorNeutralBg;                          //--- Initialize background
      color txt_color = default_txt;                            //--- Initialize text color

      if (corr == 1.0) {                                        //--- Handle perfect positive
         bg_color = ColorDiagonalBg;                            //--- Set diagonal background
         txt_color = default_txt;                               //--- Set text color
      } else if (corr == 0.0) {                                 //--- Handle zero
         bg_color = ColorNeutralBg;                             //--- Set neutral background
         txt_color = default_txt;                               //--- Set text color
      } else {                                                  //--- Handle other values
         if (global_display_mode == MODE_STANDARD) {            //--- Handle standard mode
            double strong_pos = StrongPositiveThresholdPct / 100.0; //--- Set positive threshold
            double strong_neg = StrongNegativeThresholdPct / 100.0; //--- Set negative threshold
            if (corr >= strong_pos) {                           //--- Check strong positive
               bg_color = ColorStrongPositiveBg;                //--- Set positive background
               txt_color = default_txt;                         //--- Set text color
            } else if (corr <= strong_neg) {                    //--- Check strong negative
               bg_color = ColorStrongNegativeBg;                //--- Set negative background
               txt_color = default_txt;                         //--- Set text color
            } else {                                            //--- Handle mild
               bg_color = ColorNeutralBg;                       //--- Set neutral background
               txt_color = (corr > 0.0) ? ColorTextPositive : ColorTextNegative; //--- Set mild text color
            }
         } else {                                               //--- Handle heatmap mode
            txt_color = default_txt;                            //--- Set text color
            bg_color = interpolate_heatmap_color(corr);         //--- Interpolate background
         }
      }

      ObjectSetInteger(0, rect_name, OBJPROP_BGCOLOR, bg_color); //--- Update background
      ObjectSetString(0, text_name, OBJPROP_TEXT, txt_str);     //--- Update text
      ObjectSetInteger(0, text_name, OBJPROP_COLOR, txt_color); //--- Update text color
   }
   ChartRedraw(0);                                              //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Update dashboard cells with correlation values and colors        |
//+------------------------------------------------------------------+
void update_dashboard() {
   update_correlations();                                       //--- Update correlations
   double strong_pos = StrongPositiveThresholdPct / 100.0;      //--- Set positive threshold
   double strong_neg = StrongNegativeThresholdPct / 100.0;      //--- Set negative threshold
   color text_base = ColorTextStrong;                           //--- Set base text color
   for (int i = 0; i < num_symbols; i++) {                      //--- Loop rows
      for (int j = 0; j < num_symbols; j++) {                   //--- Loop columns
         double corr = correlation_matrix[i][j];                //--- Get correlation
         double pval = pvalue_matrix[i][j];                     //--- Get p-value
         string text = DoubleToString(corr * 100, 1) + "%" + get_significance_stars(pval); //--- Format text
         color bg_color = ColorNeutralBg;                       //--- Initialize background
         color txt_color = ColorTextZero;                       //--- Initialize text color

         if (i == j) {                                          //--- Handle diagonal
            bg_color = ColorDiagonalBg;                         //--- Set diagonal background
            txt_color = text_base;                              //--- Set text color
         } else {                                               //--- Handle off-diagonal
            if (global_display_mode == MODE_STANDARD) {         //--- Handle standard mode
               if (corr >= strong_pos) {                        //--- Check strong positive
                  bg_color = ColorStrongPositiveBg;             //--- Set positive background
                  txt_color = text_base;                        //--- Set text color
               } else if (corr <= strong_neg) {                 //--- Check strong negative
                  bg_color = ColorStrongNegativeBg;             //--- Set negative background
                  txt_color = text_base;                        //--- Set text color
               } else {                                         //--- Handle mild
                  bg_color = ColorNeutralBg;                    //--- Set neutral background
                  if (corr > 0.0) {                             //--- Check positive mild
                     txt_color = ColorTextPositive;             //--- Set positive text
                  } else if (corr < 0.0) {                      //--- Check negative mild
                     txt_color = ColorTextNegative;             //--- Set negative text
                  } else {                                      //--- Handle zero
                     txt_color = text_base;                     //--- Set base text
                  }
               }
            } else {                                             //--- Handle heatmap mode
               txt_color = text_base;                            //--- Set text color
               bg_color = interpolate_heatmap_color(corr);       //--- Set interpolated background
            }
         }
         string cell_name = CELL_RECTANGLE + IntegerToString(i) + "_" + IntegerToString(j); //--- Get cell name
         string text_name = CELL_TEXT + IntegerToString(i) + "_" + IntegerToString(j); //--- Get text name
         ObjectSetInteger(0, cell_name, OBJPROP_BGCOLOR, bg_color); //--- Update background
         ObjectSetString(0, text_name, OBJPROP_TEXT, text);      //--- Update text
         ObjectSetInteger(0, text_name, OBJPROP_COLOR, txt_color); //--- Update text color
      }
   }
   update_legend();                                              //--- Update legend
   ChartRedraw(0);                                               //--- Redraw chart
}

Здесь мы реализуем функцию "update_tf_highlights", чтобы визуально выделить выбранную ячейку таймфрейма на панели. Мы устанавливаем неактивный цвет фона на средне-серый (medium gray) и в цикле перебираем количество видимых таймфреймов. Для каждого из них мы формируем имя прямоугольника, используя префикс "TF_CELL_RECT" и строку индекса, затем условно устанавливаем цвет фона на "ColorStrongPositiveBg", если он соответствует "current_tf_index", или на неактивный цвет в противном случае. Мы обновляем свойство фона объекта с помощью ObjectSetInteger, используя "OBJPROP_BGCOLOR", и перерисовываем график.

Далее определяем функцию "update_legend", чтобы обновить цвета и тексты легенды в соответствии с режимом. Мы устанавливаем цвет текста по умолчанию с помощью "ColorTextStrong" и в цикле перебираем видимые элементы условных обозначений. Для каждого из них мы извлекаем названия для rectangle и text, используя префиксы и индекс, получаем значение корреляции из "visible_corr_vals", определяем десятичные дроби на основе проверки абсолютных значений для четкого форматирования и форматируем текстовую строку в виде процентов с помощью функции DoubleToString

Мы инициализируем цвета фона и текста, затем обрабатываем особые случаи: для 1.0 используем диагональный фон; для 0.0 — нейтральный фон - оба с текстом по умолчанию. Для других, в стандартном режиме, вычисляем сильные пороговые значения в процентах, устанавливая фон и цвета текста для сильных положительных, сильных отрицательных или слабых значений на основе сравнений, используя положительный или отрицательный слабый текст для ненулевых значений. В режиме тепловой карты устанавливаем текст по умолчанию и интерполируем фон с помощью "interpolate_heatmap_color". Мы обновляем "OBJPROP_BGCOLOR" прямоугольника, "OBJPROP_TEXT" текста и "OBJPROP_COLOR" текста, используя методы-установщики объектов, затем перерисовываем график.

Затем создаем функцию "update_dashboard", чтобы обновить все ячейки текущими корреляциями и визуальными элементами. Сначала вызываем "update_correlations", вычисляем сильные пороговые значения из процентных значений и устанавливаем базовый цвет текста с помощью "ColorTextStrong".

Во вложенных циклах над символами для строк и столбцов мы извлекаем корреляцию и p-значение из матриц, форматируем текст ячейки в процентах плюс звездочки значимости из "get_significance_stars" и инициализируем цвета. Для диагональных ячеек, где индексы совпадают, мы используем диагональный фон и базовый текст. Для недиагонального изображения в стандартном режиме устанавливаем фон и текст для сильно положительного, сильно отрицательного или умеренного значения с условным положительным / отрицательным / нулевым текстом; в режиме тепловой карты - базовый текст и интерполированный фон из "interpolate_heatmap_color". Мы формируем имена ячеек и текста с помощью префиксов и индексов, обновляем фон с помощью OBJPROP_BGCOLOR, содержимое текста с помощью "OBJPROP_TEXT", а цвет текста с помощью "OBJPROP_COLOR" через методы-установщики.

Наконец, вызываем "update_legend" и перерисовываем график. При инициализации мы вызываем эти функции как события тиков, чтобы применить все эффекты, как показано ниже.

//+------------------------------------------------------------------+
//| Initialize expert                                                |
//+------------------------------------------------------------------+
int OnInit() {
   global_display_mode = DashboardMode;                         //--- Set display mode
   global_correlation_tf = (CorrelationTimeframe == PERIOD_CURRENT ? (ENUM_TIMEFRAMES)_Period : CorrelationTimeframe); //--- Set timeframe
   for (int i = 0; i < NUM_TF; i++) {                           //--- Loop to find index
      if (tf_list[i] == global_correlation_tf) {                //--- Check match
         current_tf_index = i;                                  //--- Set index
         break;                                                 //--- Exit loop
      }
   }
   if (current_tf_index == -1) current_tf_index = 3;            //--- Default to H1
   global_correlation_tf = tf_list[current_tf_index];           //--- Update timeframe
   parse_symbols();                                             //--- Parse symbols
   int panel_width = WIDTH_SYMBOL + num_symbols * (WIDTH_CELL - 1) + 4; //--- Compute width
   num_tf_visible = MathMin(NUM_TF, (panel_width - 2) / WIDTH_TF_CELL); //--- Set visible TFs
   if (current_tf_index >= num_tf_visible) current_tf_index = num_tf_visible - 1; //--- Clamp index
   global_correlation_tf = tf_list[current_tf_index];           //--- Update timeframe
   ArrayInitialize(pvalue_matrix, 1.0);                         //--- Initialize p-values
   create_full_dashboard();                                     //--- Create dashboard
   update_tf_highlights();                                      //--- Update highlights
   update_dashboard();                                          //--- Update initial
   return(INIT_SUCCEEDED);                                      //--- Return success
}

//+------------------------------------------------------------------+
//| Handle tick event                                                |
//+------------------------------------------------------------------+
void OnTick() {
   update_dashboard();                                          //--- Update on tick
}

В обработчике OnInit вызываем "update_tf_highlights" для визуальных элементов таймфрейма и "update_dashboard" для первоначального заполнения данных, возвращая INIT_SUCCEEDED. В обработчике OnTick мы просто вызываем "update_dashboard", чтобы обновить корреляции и визуализацию при каждом новом тике. Если хотите обрабатывать вычисления по каждому бару, можно добавить управляющую логику, но пока мы оставляем все как есть, поскольку у нас всего несколько наборов символов. После компиляции получаем следующий результат.

POPULATED CORRELATION MATRIX DASHBOARD

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


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

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

CORRELATION MATRIX GIF


Заключение

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Cedric Olivier Kusiele Some
Cedric Olivier Kusiele Some | 18 янв. 2026 в 17:39

Здравствуйте, Аллан Мунене Мутирия,

Спасибо, что поделились блестящей идеей в своей статье. В свою очередь, я подготовил ваш исходный код для использования в производстве

Файл .ex5 удален модератором - на форуме разрешен только исходный код

Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Переосмысливаем классические стратегии (Часть 14): Анализ нескольких стратегий Переосмысливаем классические стратегии (Часть 14): Анализ нескольких стратегий
В этой статье мы продолжаем построение ансамбля торговых стратегий с использованием генетического оптимизатора MT5 для настройки параметров стратегий. Сегодня мы проанализируем данные в Python, чтобы проверить, сможет ли такая модель лучше предсказывать, какая стратегия окажется более успешной и какая сработает точнее, и окажется ли это эффективнее прямого прогнозирования доходности. Сразу скажу, что тестирование приложения с такой статистической моделью показало резкое ухудшение в результатах. Все дело в генетическом оптимизаторе — к сожалению, он отдает предпочтение коррелированным стратегиям. Поэтому мы пересмотрим метод, чтобы сохранить фиксированные веса голосов и сосредоточить оптимизацию на настройках индикаторов.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Реализация частичного закрытия позиций в MQL5 Реализация частичного закрытия позиций в MQL5
В статье разрабатывается класс для управления частичным закрытием позиций в MQL5 с последующей интеграцией в советника Order Blocks. Кроме того, представлены результаты тестирования, сравнивающие стратегию с использованием частичных закрытий и без них, а также анализ того, при каких условиях их использование может обеспечивать и максимизировать прибыль. В заключение делается вывод, что в торговых стратегиях, особенно ориентированных на более широкие ценовые движения, использование частичных закрытий может быть довольно выгодным.