English
preview
Торговые инструменты MQL5 (Часть 26): Интеграция частотного разбиения, энтропии и критерия хи-квадрат в визуальный анализатор

Торговые инструменты MQL5 (Часть 26): Интеграция частотного разбиения, энтропии и критерия хи-квадрат в визуальный анализатор

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

Введение

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

В своей предыдущей статье (Часть 25) мы расширили инструмент построения графиков для поддержки семнадцати статистических распределений с циклическим перебором распределений с помощью значка переключения в заголовке. В Части 26 мы создадим инструмент частотного анализа, который группирует цены закрытия в гистограмму, вычисляет энтропию Шеннона и выполняет критерий согласия хи-квадрат. Инструмент также включает панель лога с автоматической прокруткой, режимы для каждого бара/каждого тика и рендеринг с суперсэмплингом. Мы рассмотрим следующие темы:

  1. Исследование разбиения по частотным интервалам, энтропии и структуры хи-квадрат
  2. Реализация средствами MQL5
  3. Тестирование на истории
  4. Заключение

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


Исследование разбиения по частотным интервалам, энтропии и структуры хи-квадрат

Разбиение по частотным интервалам (Frequency binning) делит диапазон цен на интервалы одинаковой ширины и подсчитывает, сколько закрытий приходится на каждый интервал (бин). Плотные интервалы указывают на зоны принятия цены, в то время как разреженные интервалы указывают на импульсивные переходы. Энтропия Шеннона количественно определяет, насколько равномерно распределены количества по интервалам: идеально равномерное распределение дает максимальную энтропию, в то время как гистограмма, в которой преобладают один или два интервала, дает низкую энтропию. Это свидетельствует о сильной кластеризации и потенциально пригодной для торговли рыночной структуре. Критерий хи-квадрат формализует это, сравнивая наблюдаемые количества попаданий в интервалы с ожидаемыми при равномерном распределении, давая статистику по критерию, которая увеличивается по мере усиления кластеризации, со степенями свободы, равными количеству интервалов минус единица.

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

Мы загрузим последние цены закрытия и поместим их в заданные пользователем интервалы. Далее вычислим относительные частоты, энтропию (−∑p·log p) и статистику хи-квадрат. Результаты отображаются на объекте Canvas с панелью статистики и панелью лога с автоматической прокруткой (для каждого бара или тика). В двух словах, вот какой результат мы намерены получить.

FREQUENCY ANALYSIS FRAMEWORK GIF


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

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

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

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

//+------------------------------------------------------------------+
//|                  Canvas Graphing PART 5 - Frequency Analysis.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

enum ENUM_LOG_TYPE
  {
   LOG_INFO,          // Info
   LOG_FREQUENCY,     // Frequency
   LOG_STATISTICAL,   // Statistical
   LOG_VECTOR_MATRIX, // Vector Matrix
   LOG_WARNING,       // Warning
   LOG_SUCCESS        // Success
  };

enum ENUM_COMPUTE_MODE
  {
   PER_BAR,  // Per Bar
   PER_TICK  // Per Tick
  };

input group "=== FREQUENCY ANALYSIS SETTINGS ==="
input int               frequencyBins             = 10;      // Frequency Analysis Bins
input int               analysisWindowSize         = 100;     // Analysis Window Size (bars)
input bool              enableChiSquareTest        = true;    // Enable Chi-Square Test
input bool              enableEntropyCalculation   = true;    // Enable Entropy Calculation
input bool              enableCorrelationAnalysis  = true;    // Enable Correlation Analysis
input int               logUpdateIntervalTicks     = 5;       // Log Update Interval (ticks)
input ENUM_COMPUTE_MODE computeMode                = PER_BAR; // Compute Mode (per bar or per tick)

input group "=== LOG PANEL COLORS ==="
input color             logBackgroundColor       = clrBlack;       // Log Background Color
input double            logBackgroundOpacity     = 0.91;           // Log Background Opacity (0-1)
input color             logFrequencyTextColor    = clrLime;        // Frequency Log Color
input color             logStatisticalTextColor  = clrCyan;        // Statistical Log Color
input color             logVectorMatrixTextColor = clrYellow;      // Vector/Matrix Log Color
input color             logWarningTextColor      = clrOrange;      // Warning Log Color
input color             logSuccessTextColor      = clrSpringGreen; // Success Log Color
input color             logInfoTextColor         = clrWhite;       // Info Log Color

input group "=== LOG SCROLL SETTINGS ==="
input bool              showLogScrollButtons           = false; // Show Log Up/Down Buttons
input int               logTriangleCornerRadius        = 1;     // Log Triangle Round Radius
input double            logTriangleBaseWidthPercent    = 65.0;  // Log Triangle Base Width Percent (of button size)
input double            logTriangleHeightPercent       = 70.0;  // Log Triangle Height Percent (of base width)


//+------------------------------------------------------------------+
//| Structures                                                       |
//+------------------------------------------------------------------+
struct FrequencyBinStruct
  {
   double rangeMinimum; // Minimum value of this bin's price range
   double rangeMaximum; // Maximum value of this bin's price range
   int    count;        // Number of data points that fall in this bin
   double frequency;    // Relative frequency (count / total samples)
  };

struct LogEntryStruct
  {
   string        message;   // Text content of the log entry
   ENUM_LOG_TYPE type;      // Log type controlling the display color
   datetime      timestamp; // Time the entry was recorded
  };

//+------------------------------------------------------------------+
//| Global Variables - Frequency Analysis                            |
//+------------------------------------------------------------------+
FrequencyBinStruct frequencyBinsTable[]; // Array of frequency bin descriptors
double             priceDataArray[];     // Array of close prices used for analysis
int                tickUpdateCounter = 0; // Counter of ticks since the last analysis update

double currentMeanValue          = 0.0; // Sample mean of the price data
double currentStandardDeviation  = 0.0; // Sample standard deviation of the price data
double currentSkewnessValue      = 0.0; // Sample skewness of the price data
double currentModeValue          = 0.0; // Modal bin midpoint value
int    modeFrequencyCount        = 0;   // Count of data points in the modal bin
double chiSquareTestStatistic    = 0.0; // Computed chi-square uniformity test statistic
double shannonEntropyValue       = 0.0; // Computed Shannon entropy of the frequency distribution
double correlationCoefficient    = 0.0; // Computed lag-1 autocorrelation coefficient

double dataMinimum             = 0.0;   // Minimum price value in the current window
double dataMaximum             = 0.0;   // Maximum price value in the current window
double maximumFrequency        = 0.0;   // Highest relative frequency across all bins
bool   dataLoadedSuccessfully  = false; // Flag indicating data was loaded without error

double sampleMeanValue                    = 0.0; // Advanced: sample mean
double sampleStandardDeviation            = 0.0; // Advanced: sample standard deviation
double sampleSkewnessValue                = 0.0; // Advanced: sample skewness
double sampleKurtosisValue                = 0.0; // Advanced: sample excess kurtosis
double percentile25Value                  = 0.0; // 25th percentile (Q1)
double percentile50Value                  = 0.0; // 50th percentile (median)
double percentile75Value                  = 0.0; // 75th percentile (Q3)
double confidenceInterval95LowerBound     = 0.0; // Lower bound of 95% confidence interval
double confidenceInterval95UpperBound     = 0.0; // Upper bound of 95% confidence interval
double confidenceInterval99LowerBound     = 0.0; // Lower bound of 99% confidence interval
double confidenceInterval99UpperBound     = 0.0; // Upper bound of 99% confidence interval

LogEntryStruct logEntriesArray[];    // Array storing all log entries
int  maximumLogEntries    = 100;     // Maximum number of retained log entries
int  logScrollPosition    = 0;       // Current scroll offset of the log panel
int  maximumLogScroll     = 0;       // Maximum scroll offset of the log panel
bool isHoveringOverLogPanel    = false; // Flag for mouse hovering over the log panel
bool isDraggingLogScrollbar    = false; // Flag for active scrollbar drag

//+------------------------------------------------------------------+
//| Log Scroll Constants and Globals                                 |
//+------------------------------------------------------------------+
const int supersamplingFactor       = 4;  // Supersampling multiplier for high-res log rendering
const int logScrollSmoothFactor     = 10; // Pixel multiplier applied to each scroll step
const int logScrollbarFullWidth     = 16; // Full scrollbar track width in pixels
const int logScrollbarThinWidth     = 2;  // Thin (collapsed) scrollbar width in pixels
const int logTrackWidth             = 16; // Width of the scrollbar track area
const int logScrollbarMargin        = 5;  // Inner margin around the scrollbar slider
const int logButtonSize             = 16; // Pixel size of up/down scroll buttons

color logTrackColor                    = C'45,45,45';   // Scrollbar track background color
color logButtonBackgroundColor         = C'60,60,60';   // Scroll button background color
color logButtonBackgroundHoverColor    = C'70,70,70';   // Scroll button hover background color
color logArrowColor                    = C'150,150,150'; // Arrow glyph normal color
color logArrowDisabledColor            = C'80,80,80';   // Arrow glyph color when scroll is at limit
color logArrowHoverColor               = C'100,100,100'; // Arrow glyph hover color
color logSliderBackgroundColor         = C'80,80,80';   // Scrollbar slider normal color
color logSliderBackgroundHoverColor    = C'100,100,100'; // Scrollbar slider hover color

int  logScrollCurrentPosition          = 0;     // Current scroll position in high-res pixels
int  logScrollMaximumPosition          = 0;     // Maximum scroll position in high-res pixels
int  logSliderHeight                   = 0;     // Computed height of the scrollbar slider in display pixels
bool isMovingLogSlider                 = false; // Flag for active slider drag
int  logMouseDownPositionYOnSlider     = 0;     // Local Y of mouse at slider drag start
int  logMouseDownDeltaYOnSlider        = 0;     // Slider top Y at drag start
int  logTotalContentHeight             = 0;     // Total scrollable content height in pixels
int  logVisibleContentHeight           = 0;     // Visible content height in pixels
bool isLogScrollbarVisible             = false; // Flag indicating the scrollbar is currently shown
bool isMouseInLogContentBody           = false; // Flag for mouse inside the log text area
bool previousMouseInLogContentBody     = false; // Previous frame's log body hover state
bool isLogScrollUpButtonHovered        = false; // Flag for mouse hovering over the up scroll button
bool isLogScrollDownButtonHovered      = false; // Flag for mouse hovering over the down scroll button
bool isLogScrollSliderHovered          = false; // Flag for mouse hovering over the slider thumb
bool isLogScrollAreaHovered            = false; // Flag for mouse hovering over the scrollbar track area

Сначала определим "ENUM_LOG_TYPE" для классификации сообщений лога ("LOG_INFO", "LOG_FREQUENCY", "LOG_STATISTICAL", "LOG_VECTOR_MATRIX", "LOG_WARNING", "LOG_SUCCESS"). Это позволяет использовать цветовую кодировку записи в лог. Далее создадим перечисление "ENUM_COMPUTE_MODE" с параметрами "PER_BAR" для обновлений на основе баров и "PER_TICK" для уровней тиков, управляя частотой анализа. Мы добавляем группы входных параметров для настроек частоты, такие как "frequencyBins" для деления гистограммы, "analysisWindowSize" для просмотра данных, переключатели для критерия хи-квадрат, энтропии и корреляции, "logUpdateIntervalTicks" для регулирования частоты тиков и "computeMode", выбирая перечисление со значением по умолчанию "PER_BAR".

Для цветовой гаммы лога мы предоставим входные данные для настройки фона и текста для каждого типа, например, "logFrequencyTextColor" на салатовый, что обеспечивает визуальное различие. Настройки прокрутки включают в себя переключатели, такие как "showLogScrollButtons", и параметры для треугольных форм кнопок, например, "logTriangleCornerRadius". Определим структуру "FrequencyBinStruct" для хранения диапазонов интервалов, количества и частоты для данных гистограммы.

В структуре "LogEntryStruct" хранится сообщение, тип из перечисления и временная метка для каждой записи лога. Глобальные переменные содержат массив "frequencyBinsTable" структуры интервалов, "priceDataArray" для необработанных цен, "tickUpdateCounter", начинающийся с 0, для тайминга. Статистические глобальные переменные, такие как "currentMeanValue" со значением 0,0 для среднего значения, "currentStandardDeviation" для стандартного отклонения, "currentSkewnessValue", "currentModeValue" со значениями "modeFrequencyCount", "chiSquareTestStatistic", "shannonEntropyValue", "correlationCoefficient".

Расширенные статистические параметры, такие как "sampleMeanValue", "percentile25Value" и доверительные границы, также установлены на 0,0. Система записи в лог использует "logEntriesArray" структуры записи, "maximumLogEntries" со значением 100, "logScrollPosition" со значением 0, "maximumLogScroll" со значением 0, а флаги наведения курсора мыши и перетаскивания — на false. Константы прокрутки определяют параметры, такие как "supersamplingFactor" со значением 4 для сглаживания, ширину, цвета для дорожек, кнопок, стрелок, ползунка, а также состояния при наведении курсора и отключения. Глобальные переменные отслеживают положение, высоту и наведение курсора на интерактивную полосу прокрутки. После определения глобальных переменных мы переходим к определению функции управления записями лога, которая передает данные в прокручиваемую панель лога.

Управление очередью записей лога

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

//+------------------------------------------------------------------+
//| Add log entry                                                    |
//+------------------------------------------------------------------+
bool pendingAutoScrollToBottom = false; // Flag to scroll to the bottom on the next render

void AddLogEntry(string entryMessage, ENUM_LOG_TYPE entryType)
  {
   int currentSize = ArraySize(logEntriesArray);

   //--- If the log is full, evict the oldest entry by shifting the array
   if (currentSize >= maximumLogEntries)
     {
      for (int i = 0; i < currentSize - 1; i++)
         logEntriesArray[i] = logEntriesArray[i + 1];
      currentSize = maximumLogEntries - 1;
     }

   //--- Append the new entry at the end of the array
   ArrayResize(logEntriesArray, currentSize + 1);
   logEntriesArray[currentSize].message   = entryMessage;
   logEntriesArray[currentSize].type      = entryType;
   logEntriesArray[currentSize].timestamp = TimeCurrent();

   //--- Request an auto-scroll to the bottom on the next render if enabled
   if (enableLogAutoScroll)
      pendingAutoScrollToBottom = true;
  }

Определим функцию "AddLogEntry" для добавления нового сообщения лога в массив "logEntriesArray", поддерживая ограничение размера для повышения эффективности. Сначала проверим текущий размер массива с помощью ArraySize, и если он соответствует или превышает значение "maximumLogEntries", сдвинем элементы влево на один, чтобы удалить самый старый, уменьшая размер для освобождения места. Далее изменим размер массива на единицу с помощью функции ArrayResize, установим сообщение новой записи, тип из "ENUM_LOG_TYPE" и временную метку с помощью TimeCurrent.  Если "enableLogAutoScroll" имеет значение true, отметим "pendingAutoScrollToBottom", чтобы запустить прокрутку при рендеринге, гарантируя, что последние записи лога будут видны без немедленной перерисовки. Это поможет управлять очередью FIFO для записей, предотвращая неограниченный рост, при этом поддерживая функцию автоматической прокрутки для удобства пользователя. После того, как очередь записей готова, определим основную функцию, которая группирует по интервалам данные о ценах и вычисляет относительные частоты.

Выполнение частотного биннинга на ценовых данных

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

//+------------------------------------------------------------------+
//| Perform frequency analysis                                       |
//+------------------------------------------------------------------+
void PerformFrequencyAnalysis()
  {
   //--- Collect close prices for the configured analysis window
   int dataPointsCount = MathMin(analysisWindowSize, Bars(_Symbol, PERIOD_CURRENT));
   ArrayResize(priceDataArray, dataPointsCount);
   for (int i = 0; i < dataPointsCount; i++)
      priceDataArray[i] = iClose(_Symbol, PERIOD_CURRENT, i);

   //--- Require at least two data points
   if (dataPointsCount < 2) return;

   //--- Find the price range across the window
   dataMinimum = priceDataArray[0];
   dataMaximum = priceDataArray[0];
   for (int i = 1; i < dataPointsCount; i++)
     {
      if (priceDataArray[i] < dataMinimum) dataMinimum = priceDataArray[i];
      if (priceDataArray[i] > dataMaximum) dataMaximum = priceDataArray[i];
     }

   //--- Guard against a zero-range window (all closes identical)
   double dataRange = dataMaximum - dataMinimum;
   if (dataRange == 0) dataRange = _Point;

   //--- Build and initialize all frequency bins
   ArrayResize(frequencyBinsTable, frequencyBins);
   double binWidth = dataRange / frequencyBins;
   for (int i = 0; i < frequencyBins; i++)
     {
      frequencyBinsTable[i].rangeMinimum = dataMinimum + i * binWidth;
      frequencyBinsTable[i].rangeMaximum = dataMinimum + (i + 1) * binWidth;
      frequencyBinsTable[i].count        = 0;
      frequencyBinsTable[i].frequency    = 0.0;
     }

   //--- Assign each data point to its bin using a linear scan
   for (int i = 0; i < dataPointsCount; i++)
      for (int j = 0; j < frequencyBins; j++)
         if (priceDataArray[i] >= frequencyBinsTable[j].rangeMinimum &&
             priceDataArray[i] <  frequencyBinsTable[j].rangeMaximum)
           {
            frequencyBinsTable[j].count++;
            break;
           }

   //--- Include the maximum value in the last bin to handle the closed right boundary
   if (priceDataArray[0] == dataMaximum)
      frequencyBinsTable[frequencyBins - 1].count++;

   //--- Compute relative frequency for each bin
   for (int i = 0; i < frequencyBins; i++)
      frequencyBinsTable[i].frequency = (double)frequencyBinsTable[i].count / dataPointsCount;

   //--- Find the maximum frequency for Y-axis scaling
   maximumFrequency = 0.0;
   for (int i = 0; i < frequencyBins; i++)
      if (frequencyBinsTable[i].frequency > maximumFrequency)
         maximumFrequency = frequencyBinsTable[i].frequency;

   //--- Identify the modal bin
   modeFrequencyCount = 0;
   int modeBinIndex   = 0;
   for (int i = 0; i < frequencyBins; i++)
      if (frequencyBinsTable[i].count > modeFrequencyCount)
        {
         modeFrequencyCount = frequencyBinsTable[i].count;
         modeBinIndex       = i;
        }
   //--- Set the mode value to the modal bin's midpoint
   currentModeValue = (frequencyBinsTable[modeBinIndex].rangeMinimum +
                       frequencyBinsTable[modeBinIndex].rangeMaximum) / 2.0;

   dataLoadedSuccessfully = true;
  }

В этой части мы определим функцию "PerformFrequencyAnalysis" для выполнения основной операции разбиения на интервалы и вычисления частоты на ценовых данных. Сначала определим количество точек данных как минимум от "analysisWindowSize" и доступных баров с помощью Bars, изменим размер "priceDataArray" с помощью ArrayResize и заполним его, циклически извлекая цены закрытия с помощью iClose из текущего символа и таймфрейма, начиная с последних баров. Если имеется менее двух пунктов, мы досрочно выходим из функции. Диапазон данных определяется путем инициализации значений "dataMinimum" и "dataMaximum" первым значением, затем циклом обновляются минимальные и максимальные значения, при этом нулевой диапазон корректируется до значения "_Point" для минимизации разброса. Для настройки интервалов изменим размер "frequencyBinsTable" до значения "frequencyBins", вычислим "binWidth" как диапазон, деленный на количество интервалов, и в цикле присвоим значения "rangeMinimum" и "rangeMaximum" для каждого интервала в качестве последовательных интервалов, обнуляя счетчик и частоту.

Подсчет частот осуществляется с помощью вложенных циклов: для каждой цены проверяется соответствие диапазонам интервалов, и счетчик соответствующего интервала увеличивается, прерываясь при совпадении. В крайних случаях, когда цены равны максимуму, мы увеличиваем последний интервал. Далее вычислим относительные частоты, деля каждое значение на общее количество пунктов. Найдём значение "maximumFrequency" с помощью цикла, чтобы получить самую высокую частоту для масштабирования графика. Для режима сбросим "modeFrequencyCount" и индекс, выполняем цикл для поиска интервала с максимальным значением и установим "currentModeValue" в качестве его середины. Наконец, устанавливаем флаг "dataLoadedSuccessfully" в значение true, завершим анализ и подготовим данные к визуализации и статистическому анализу. После заполнения интервалов вычислим статистические показатели, сопровождающие гистограмму.

Расчет основных статистических показателей, критерия хи-квадрат, энтропии Шеннона и автокорреляции

Статистический уровень охватывает четыре специализированные функции: основные статистические показатели вычисляют среднее значение, стандартное отклонение и асимметрию непосредственно из массива цен; критерий хи-квадрат сравнивает наблюдаемые количества попаданий в интервалы с ожидаемым значением для количественной оценки кластеризации; энтропия Шеннона суммирует отрицательные значения частота-лог-частота по интервалам для измерения предсказуемости; и автокорреляция с лагом 1 вычисляет отношение ковариации последовательных цен к дисперсии для выявления последовательной зависимости.

//+------------------------------------------------------------------+
//| Calculate basic statistics                                       |
//+------------------------------------------------------------------+
void CalculateBasicStatistics()
  {
   int sampleSize = ArraySize(priceDataArray);
   if (sampleSize < 2) return;

   //--- Compute the arithmetic mean
   double sumTotal = 0.0;
   for (int i = 0; i < sampleSize; i++)
      sumTotal += priceDataArray[i];
   currentMeanValue = sumTotal / sampleSize;

   //--- Compute the sample standard deviation
   double sumSquaredDifferences = 0.0;
   for (int i = 0; i < sampleSize; i++)
     {
      double difference = priceDataArray[i] - currentMeanValue;
      sumSquaredDifferences += difference * difference;
     }
   currentStandardDeviation = MathSqrt(sumSquaredDifferences / (sampleSize - 1));

   //--- Compute the skewness only when standard deviation is nonzero
   if (currentStandardDeviation > 0)
     {
      double sumCubedDifferences = 0.0;
      for (int i = 0; i < sampleSize; i++)
        {
         double normalizedValue = (priceDataArray[i] - currentMeanValue) / currentStandardDeviation;
         sumCubedDifferences += normalizedValue * normalizedValue * normalizedValue;
        }
      currentSkewnessValue = (sampleSize / ((sampleSize - 1.0) * (sampleSize - 2.0))) * sumCubedDifferences;
     }
  }

//+------------------------------------------------------------------+
//| Calculate chi-square test                                        |
//+------------------------------------------------------------------+
void CalculateChiSquareTest()
  {
   if (!enableChiSquareTest) return;

   int sampleSize = ArraySize(priceDataArray);
   if (sampleSize < frequencyBins) return;

   //--- Under uniform distribution, each bin expects the same count
   double expectedFrequency    = (double)sampleSize / frequencyBins;
   chiSquareTestStatistic = 0.0;

   //--- Accumulate chi-square contributions from each bin
   for (int i = 0; i < frequencyBins; i++)
     {
      double difference = frequencyBinsTable[i].count - expectedFrequency;
      chiSquareTestStatistic += (difference * difference) / expectedFrequency;
     }
  }

//+------------------------------------------------------------------+
//| Calculate Shannon entropy                                        |
//+------------------------------------------------------------------+
void CalculateShannonEntropy()
  {
   if (!enableEntropyCalculation) return;

   shannonEntropyValue = 0.0;

   //--- Sum -p * log(p) over all non-empty bins
   for (int i = 0; i < frequencyBins; i++)
      if (frequencyBinsTable[i].frequency > 0)
         shannonEntropyValue -= frequencyBinsTable[i].frequency *
                                MathLog(frequencyBinsTable[i].frequency);
  }

//+------------------------------------------------------------------+
//| Calculate auto-correlation                                       |
//+------------------------------------------------------------------+
void CalculateAutoCorrelation()
  {
   if (!enableCorrelationAnalysis) return;

   int sampleSize = ArraySize(priceDataArray);
   if (sampleSize < 3) return;

   //--- Compute variance and lag-1 covariance using the sample mean
   double meanValue    = currentMeanValue;
   double varianceSum  = 0.0;
   double covarianceSum = 0.0;

   for (int i = 0; i < sampleSize; i++)
      varianceSum += (priceDataArray[i] - meanValue) * (priceDataArray[i] - meanValue);

   for (int i = 0; i < sampleSize - 1; i++)
      covarianceSum += (priceDataArray[i] - meanValue) * (priceDataArray[i + 1] - meanValue);

   //--- Divide covariance by variance to obtain the lag-1 autocorrelation
   if (varianceSum > 0)
      correlationCoefficient = covarianceSum / varianceSum;
  }

В этой части определим функцию "CalculateBasicStatistics" для получения основных метрик из "priceDataArray". Сначала проверим, равен ли размер не менее 2, и в противном случае выходим из программы досрочно, затем вычислим среднее значение, суммируя значения и деля на количество. После этого вычислим стандартное отклонение, используя сумму квадратов разностей от среднего значения, деленную на n-1 с помощью MathSqrt, и коэффициент асимметрии как скорректированную сумму кубов нормализованных разностей, что обеспечивает базовые статистические данные без использования сложных инструментов. Далее реализуем функцию "CalculateChiSquareTest" для выполнения критерий согласия хи-квадрат, досрочно выйдем из программы, если "enableChiSquareTest" равно false или размер выборки мал. Установим ожидаемую частоту в качестве размера выборки по интервалам, сбросим "chiSquareTestStatistic" и в цикле суммируем (наблюдаемое - ожидаемое)^2 / ожидаемое для каждого интервала, количественно оценивая отклонение от равномерного распределения.

Для измерения информации создадим функцию "CalculateShannonEntropy", пропускаем, если "enableEntropyCalculation" отключено, сбросим "shannonEntropyValue" и накапливаем отрицательную частоту * log(frequency) для получения положительных частот с помощью "MathLog". Выражаем энтропию в натах для оценки предсказуемости данных. Наконец, определим функцию "CalculateAutoCorrelation" для зависимости с задержкой 1, завершим работу программы, если "enableCorrelationAnalysis" имеет значение false или выборка мала. Вычислим сумму дисперсии как квадрат отклонения от среднего значения, ковариацию как произведение последовательных отклонений и установим "correlationCoefficient" как соотношение ковариации и дисперсии, если оно положительное, обнаруживая последовательную корреляцию в ценах. Вычислив статистические показатели, теперь выведем гистограмму частоты на основной объект Canvas.

Построение графика частотной гистограммы

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

//+------------------------------------------------------------------+
//| Draw frequency histogram plot                                    |
//+------------------------------------------------------------------+
void DrawFrequencyHistogramPlot()
  {
   if (!dataLoadedSuccessfully) return;

   //--- Define the outer and inner (padded) plot area boundaries
   int plotAreaLeftEdge   = 60;
   int plotAreaRightEdge  = currentCanvasWidth  - 40;
   int plotAreaTopEdge    = headerBarHeight + 10;
   int plotAreaBottomEdge = currentCanvasHeight  - 50;

   int drawAreaLeftEdge   = plotAreaLeftEdge   + plotAreaPaddingPixels;
   int drawAreaRightEdge  = plotAreaRightEdge  - plotAreaPaddingPixels;
   int drawAreaTopEdge    = plotAreaTopEdge    + plotAreaPaddingPixels;
   int drawAreaBottomEdge = plotAreaBottomEdge - plotAreaPaddingPixels;

   int plotWidth  = drawAreaRightEdge - drawAreaLeftEdge;
   int plotHeight = drawAreaBottomEdge - drawAreaTopEdge;

   if (plotWidth <= 0 || plotHeight <= 0) return;

   //--- Compute axis ranges, guarding against degenerate zero values
   double xAxisRange = dataMaximum - dataMinimum;
   double yAxisRange = maximumFrequency;
   if (xAxisRange == 0) xAxisRange = 1;
   if (yAxisRange == 0) yAxisRange = 1;

   //--- Draw X and Y axes with a two-pixel stroke for visibility
   uint argbAxisColor = ColorToARGB(clrBlack, 255);
   for (int thickness = 0; thickness < 2; thickness++)
      mainDistributionCanvas.Line(plotAreaLeftEdge - thickness, plotAreaTopEdge,
                                  plotAreaLeftEdge - thickness, plotAreaBottomEdge, argbAxisColor);
   for (int thickness = 0; thickness < 2; thickness++)
      mainDistributionCanvas.Line(plotAreaLeftEdge, plotAreaBottomEdge + thickness,
                                  plotAreaRightEdge, plotAreaBottomEdge + thickness, argbAxisColor);

   //--- Draw Y-axis tick marks and labels
   mainDistributionCanvas.FontSet("Arial", axisLabelFontSize);
   uint argbTickLabelColor = ColorToARGB(clrBlack, 255);

   double yAxisTickValues[];
   int    yAxisTickCount = CalculateOptimalAxisTicks(0, yAxisRange, plotHeight, yAxisTickValues);
   for (int i = 0; i < yAxisTickCount; i++)
     {
      double yTickValue = yAxisTickValues[i];
      if (yTickValue < 0 || yTickValue > yAxisRange) continue;

      int yPosition = drawAreaBottomEdge - (int)((yTickValue / yAxisRange) * plotHeight);
      mainDistributionCanvas.Line(plotAreaLeftEdge - 5, yPosition,
                                  plotAreaLeftEdge,     yPosition, argbAxisColor);
      mainDistributionCanvas.TextOut(plotAreaLeftEdge - 8,
                                     yPosition - axisLabelFontSize / 2,
                                     FormatAxisTickLabel(yTickValue, yAxisRange),
                                     argbTickLabelColor, TA_RIGHT);
     }

   //--- Draw X-axis tick marks and labels
   double xAxisTickValues[];
   int    xAxisTickCount = CalculateOptimalAxisTicks(dataMinimum, dataMaximum, plotWidth, xAxisTickValues);
   for (int i = 0; i < xAxisTickCount; i++)
     {
      double xTickValue = xAxisTickValues[i];
      if (xTickValue < dataMinimum || xTickValue > dataMaximum) continue;

      int xPosition = drawAreaLeftEdge + (int)((xTickValue - dataMinimum) / xAxisRange * plotWidth);
      mainDistributionCanvas.Line(xPosition, plotAreaBottomEdge,
                                  xPosition, plotAreaBottomEdge + 5, argbAxisColor);
      mainDistributionCanvas.TextOut(xPosition, plotAreaBottomEdge + 7,
                                     FormatAxisTickLabel(xTickValue, xAxisRange),
                                     argbTickLabelColor, TA_CENTER);
     }

   //--- Draw histogram bars centered on each bin midpoint
   uint   argbHistogramColor = ColorToARGB(histogramBarColor, 255);
   double totalBarGaps       = (frequencyBins - 1) * histogramBarGapPixels;
   double barWidth           = (plotWidth - totalBarGaps) / frequencyBins;
   if (barWidth < 1) barWidth = 1; // Enforce a minimum bar width of one pixel

   for (int i = 0; i < frequencyBins; i++)
     {
      double binMidpoint     = (frequencyBinsTable[i].rangeMinimum + frequencyBinsTable[i].rangeMaximum) / 2.0;
      int    barLeftPosition = drawAreaLeftEdge +
                               (int)((binMidpoint - dataMinimum) / xAxisRange * plotWidth - barWidth / 2);
      int    barRightPosition = barLeftPosition + (int)barWidth - 1;
      int    barHeight        = (int)(frequencyBinsTable[i].frequency / yAxisRange * plotHeight);
      int    barTopPosition   = drawAreaBottomEdge - barHeight;

      if (barRightPosition >= barLeftPosition)
         mainDistributionCanvas.FillRectangle(barLeftPosition, barTopPosition,
                                              barRightPosition, drawAreaBottomEdge,
                                              argbHistogramColor);
     }

   //--- Draw X and Y axis labels
   mainDistributionCanvas.FontSet("Arial Bold", labelFontSize);
   uint argbAxisLabelColor = ColorToARGB(clrBlack, 255);

   mainDistributionCanvas.TextOut(currentCanvasWidth / 2, currentCanvasHeight - 20,
                                  "Price Bins", argbAxisLabelColor, TA_CENTER);

   //--- Rotate 90° to draw the Y axis label vertically
   mainDistributionCanvas.FontAngleSet(900);
   mainDistributionCanvas.TextOut(12, currentCanvasHeight / 2,
                                  "Relative Frequency", argbAxisLabelColor, TA_CENTER);
   mainDistributionCanvas.FontAngleSet(0);
  }

Определим функцию "DrawFrequencyHistogramPlot" для визуализации интервалов частоты в виде гистограммы на основном объекте Canvas. Чтобы избежать ошибок досрочно завершаем работу функции, если "dataLoadedSuccessfully" имеет значение false. Установим границы области построения с фиксированными полями, корректируем границы отрисовки с помощью "plotAreaPaddingPixels", вычислим ширину и высоту и завершаем работу, если они недействительны. После определения диапазона X из "dataMaximum" - "dataMinimum" и Y из "maximumFrequency" с минимальными защитами от нулевых значений, преобразуем черный цвет в ARGB для осей и рисуем утолщенные линии Y/X с помощью циклов, используя метод "Line". Для установки меток на оси установим шрифт с помощью "FontSet" и подготовим цвет ARGB тиков. Для оси Y мы вычислим оптимальные тики с помощью "CalculateOptimalAxisTicks" от 0 до диапазона Y. Выполним цикл до позиции, рисуем короткие линии с помощью функции "Line", форматируем метки с помощью функции "FormatAxisTickLabel" и размещаем их с выравниванием по правому краю с помощью метода TextOut. Аналогично, для оси X от "dataMinimum" до "dataMaximum" рисуем тики, направленные вниз и центрированные метки.

Для гистограммы преобразуем "histogramBarColor" в ARGB, вычислим ширину столбцов с учетом "histogramBarGapPixels" и перебираем интервалы: используем середину для центрирования, позиционируем столбцы, масштабируем высоту в соответствии с диапазоном Y и заполняем с помощью FillRectangle, если это допустимо. Наконец, сделаем шрифт жирным, установим метки осей как "Price Bins" для оси X, центрированные внизу, и "Relative Frequency" для оси Y, повернутые на 90 градусов с помощью FontAngleSet и центрированные по левому краю. Сбросим угол до 0, завершим построение графика для анализа частоты. После построения гистограммы создадим панель логов, которая отображает записи с временными метками с помощью цветовой кодировки, фоном с суперсэмплированием и интерактивной полосой прокрутки, которая разворачивается при наведении курсора и сворачивается в режиме ожидания.

Рендеринг панели лога с полосой прокрутки с суперсэмплингом

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

//+------------------------------------------------------------------+
//| Get log entry text color                                         |
//+------------------------------------------------------------------+
color GetLogEntryTextColor(ENUM_LOG_TYPE entryType)
  {
   //--- Return the configured display color for each log category
   switch (entryType)
     {
      case LOG_FREQUENCY:    return logFrequencyTextColor;
      case LOG_STATISTICAL:  return logStatisticalTextColor;
      case LOG_VECTOR_MATRIX: return logVectorMatrixTextColor;
      case LOG_WARNING:      return logWarningTextColor;
      case LOG_SUCCESS:      return logSuccessTextColor;
      default:               return logInfoTextColor;
     }
  }

//+------------------------------------------------------------------+
//| Render log panel visualization                                   |
//+------------------------------------------------------------------+
void RenderLogPanelVisualization()
  {
   //--- Erase the high-resolution canvas before drawing
   logPanelHighResolutionCanvas.Erase(0);

   int highResolutionWidth  = currentCanvasWidth   * supersamplingFactor;
   int highResolutionHeight = currentLogPanelHeight * supersamplingFactor;

   //--- Fill the log background with the configured semi-transparent color
   uint argbBackgroundColor = ColorToARGB(logBackgroundColor,
                                          (uchar)(255 * logBackgroundOpacity));
   logPanelHighResolutionCanvas.FillRectangle(0, 0,
      highResolutionWidth - 1, highResolutionHeight - 1, argbBackgroundColor);

   //--- Draw the log panel border if enabled
   if (showBorderFrame)
     {
      uint argbBorderColor = ColorToARGB(masterThemeColor, 255);
      logPanelHighResolutionCanvas.Rectangle(0, 0,
         highResolutionWidth - 1, highResolutionHeight - 1, argbBorderColor);
      logPanelHighResolutionCanvas.Rectangle(
         supersamplingFactor, supersamplingFactor,
         highResolutionWidth  - supersamplingFactor - 1,
         highResolutionHeight - supersamplingFactor - 1, argbBorderColor);
     }

   //--- Draw the panel title
   logPanelHighResolutionCanvas.FontSet("Arial Bold", titleFontSize * supersamplingFactor);
   uint argbHeaderTextColor = ColorToARGB(clrWhite, 255);
   logPanelHighResolutionCanvas.TextOut(highResolutionWidth / 2,
      5 * supersamplingFactor,
      "FREQUENCY ANALYSIS LOG", argbHeaderTextColor, TA_CENTER);

   //--- Switch to the monospaced log font
   logPanelHighResolutionCanvas.FontSet("Courier New", logFontSize * supersamplingFactor);

   //--- Compute layout metrics for the scrollable log area
   int logLineHeightHighRes     = logPanelHighResolutionCanvas.TextHeight("A") + 4 * supersamplingFactor;
   int logStartPositionYHighRes = 35 * supersamplingFactor;
   int logVisibleHeightHighRes  = highResolutionHeight - logStartPositionYHighRes - 5 * supersamplingFactor;
   int logTotalHeightHighRes    = ArraySize(logEntriesArray) * logLineHeightHighRes;

   //--- Compute the maximum scroll position and honor a pending scroll-to-bottom request
   logScrollMaximumPosition = MathMax(0, logTotalHeightHighRes - logVisibleHeightHighRes);
   if (pendingAutoScrollToBottom)
     {
      logScrollCurrentPosition  = logScrollMaximumPosition;
      pendingAutoScrollToBottom = false;
     }
   logScrollCurrentPosition = MathMin(logScrollCurrentPosition, logScrollMaximumPosition);

   isLogScrollbarVisible = logScrollMaximumPosition > 0;

   //--- Determine which log entries are currently visible
   int scrollOffsetHighRes = logScrollCurrentPosition;
   int firstEntryIndex     = scrollOffsetHighRes / logLineHeightHighRes;
   int lastEntryIndex      = (scrollOffsetHighRes + logVisibleHeightHighRes) / logLineHeightHighRes + 1;
   lastEntryIndex  = MathMin(lastEntryIndex,  ArraySize(logEntriesArray) - 1);
   firstEntryIndex = MathMax(firstEntryIndex, 0);

   //--- Render each visible log entry
   for (int i = firstEntryIndex; i <= lastEntryIndex; i++)
     {
      color entryTextColor     = GetLogEntryTextColor(logEntriesArray[i].type);
      uint  argbEntryTextColor = ColorToARGB(entryTextColor, 255);
      string timestampString   = TimeToString(logEntriesArray[i].timestamp, TIME_SECONDS);
      string fullEntryMessage  = StringFormat("[%s] %s", timestampString, logEntriesArray[i].message);

      int yPositionHighRes = logStartPositionYHighRes + i * logLineHeightHighRes - scrollOffsetHighRes;

      //--- Skip entries that are fully outside the visible area
      if (yPositionHighRes + logLineHeightHighRes < logStartPositionYHighRes ||
          yPositionHighRes > logStartPositionYHighRes + logVisibleHeightHighRes) continue;

      logPanelHighResolutionCanvas.TextOut(5 * supersamplingFactor,
                                           yPositionHighRes,
                                           fullEntryMessage, argbEntryTextColor, TA_LEFT);
     }

   //--- Render the scrollbar only when content overflows
   if (isLogScrollbarVisible)
     {
      int logScrollbarPositionXHighRes = highResolutionWidth - (logTrackWidth * supersamplingFactor);
      int logScrollbarPositionYHighRes = 0;
      int logScrollbarHeightHighRes    = highResolutionHeight;

      //--- Fill the scrollbar track background
      uint argbTrackColor = ColorToARGB(logTrackColor, 255);
      logPanelHighResolutionCanvas.FillRectangle(
         logScrollbarPositionXHighRes, logScrollbarPositionYHighRes,
         logScrollbarPositionXHighRes + (logTrackWidth * supersamplingFactor) - 1,
         logScrollbarPositionYHighRes + logScrollbarHeightHighRes - 1, argbTrackColor);

      int logScrollAreaHeightHighRes = logScrollbarHeightHighRes - 2 * (logButtonSize * supersamplingFactor);

      //--- Compute the display-space slider height and position
      int logVisibleHeightDisplay  = currentLogPanelHeight - 25 - 5;
      int logTotalHeightDisplay    = ArraySize(logEntriesArray) * (logFontSize + 4);
      int logScrollAreaHeightDisplay = currentLogPanelHeight - 2 * logButtonSize;
      logSliderHeight = CalculateLogSliderHeight(logVisibleHeightDisplay, logTotalHeightDisplay,
                                                  logScrollAreaHeightDisplay, 20);

      int logSliderPositionYHighRes = logScrollbarPositionYHighRes +
                                      (logButtonSize * supersamplingFactor) +
                                      (int)(((double)logScrollCurrentPosition / logScrollMaximumPosition) *
                                            (logScrollAreaHeightHighRes - (logSliderHeight * supersamplingFactor)));

      if (isLogScrollAreaHovered)
        {
         //--- Draw full-width buttons and slider when the scrollbar is hovered
         color upButtonBg   = showLogScrollButtons
                              ? (isLogScrollUpButtonHovered
                                 ? logButtonBackgroundHoverColor : logButtonBackgroundColor)
                              : logTrackColor;
         logPanelHighResolutionCanvas.FillRectangle(
            logScrollbarPositionXHighRes, logScrollbarPositionYHighRes,
            logScrollbarPositionXHighRes + (logTrackWidth * supersamplingFactor) - 1,
            logScrollbarPositionYHighRes + (logButtonSize * supersamplingFactor) - 1,
            ColorToARGB(upButtonBg, 255));

         //--- Draw up arrow glyph
         color upArrowColor = (logScrollCurrentPosition == 0)
                              ? logArrowDisabledColor
                              : (isLogScrollUpButtonHovered ? logArrowHoverColor : logArrowColor);
         int   arrowPositionX  = logScrollbarPositionXHighRes + (logTrackWidth * supersamplingFactor) / 2;
         double baseWidth      = logButtonSize * (logTriangleBaseWidthPercent / 100.0) * supersamplingFactor;
         int   triangleHeight  = (int)(baseWidth * (logTriangleHeightPercent / 100.0));
         int   arrowPositionY  = logScrollbarPositionYHighRes +
                                 ((logButtonSize * supersamplingFactor) - triangleHeight) / 2;
         DrawRoundedTriangleArrow(logPanelHighResolutionCanvas,
                                  arrowPositionX, arrowPositionY,
                                  (int)baseWidth, triangleHeight, true,
                                  ColorToARGB(upArrowColor, 255));

         //--- Draw down button background
         int   downPositionYHighRes = logScrollbarPositionYHighRes + logScrollbarHeightHighRes -
                                      (logButtonSize * supersamplingFactor);
         color downButtonBg = showLogScrollButtons
                              ? (isLogScrollDownButtonHovered
                                 ? logButtonBackgroundHoverColor : logButtonBackgroundColor)
                              : logTrackColor;
         logPanelHighResolutionCanvas.FillRectangle(
            logScrollbarPositionXHighRes, downPositionYHighRes,
            logScrollbarPositionXHighRes + (logTrackWidth * supersamplingFactor) - 1,
            downPositionYHighRes + (logButtonSize * supersamplingFactor) - 1,
            ColorToARGB(downButtonBg, 255));

         //--- Draw down arrow glyph
         color downArrowColor = (logScrollCurrentPosition >= logScrollMaximumPosition)
                                ? logArrowDisabledColor
                                : (isLogScrollDownButtonHovered ? logArrowHoverColor : logArrowColor);
         int downArrowPositionX = logScrollbarPositionXHighRes + (logTrackWidth * supersamplingFactor) / 2;
         int downArrowPositionY = downPositionYHighRes +
                                  ((logButtonSize * supersamplingFactor) - triangleHeight) / 2;
         DrawRoundedTriangleArrow(logPanelHighResolutionCanvas,
                                  downArrowPositionX, downArrowPositionY,
                                  (int)baseWidth, triangleHeight, false,
                                  ColorToARGB(downArrowColor, 255));

         //--- Draw the full-width pill-shaped slider thumb
         int   sliderPositionXHighRes = logScrollbarPositionXHighRes + (logScrollbarMargin * supersamplingFactor);
         int   sliderWidthHighRes     = (logTrackWidth * supersamplingFactor) - 2 * (logScrollbarMargin * supersamplingFactor);
         int   capRadius              = sliderWidthHighRes / 2;
         color sliderBgColor = (isLogScrollSliderHovered || isMovingLogSlider)
                               ? logSliderBackgroundHoverColor : logSliderBackgroundColor;
         uint  argbSliderColor = ColorToARGB(sliderBgColor, 255);

         //--- Draw top cap, middle fill, and bottom cap of the slider
         logPanelHighResolutionCanvas.Arc(sliderPositionXHighRes + capRadius,
            logSliderPositionYHighRes + capRadius,
            capRadius, capRadius,
            ConvertDegreesToRadians(180), ConvertDegreesToRadians(90), argbSliderColor);
         logPanelHighResolutionCanvas.FillCircle(sliderPositionXHighRes + capRadius,
            logSliderPositionYHighRes + capRadius, capRadius, argbSliderColor);
         logPanelHighResolutionCanvas.Arc(sliderPositionXHighRes + capRadius,
            logSliderPositionYHighRes + (logSliderHeight * supersamplingFactor) - capRadius,
            capRadius, capRadius,
            ConvertDegreesToRadians(90), ConvertDegreesToRadians(90), argbSliderColor);
         logPanelHighResolutionCanvas.FillCircle(sliderPositionXHighRes + capRadius,
            logSliderPositionYHighRes + (logSliderHeight * supersamplingFactor) - capRadius,
            capRadius, argbSliderColor);
         logPanelHighResolutionCanvas.FillRectangle(
            sliderPositionXHighRes,
            logSliderPositionYHighRes + capRadius,
            sliderPositionXHighRes + sliderWidthHighRes,
            logSliderPositionYHighRes + (logSliderHeight * supersamplingFactor) - capRadius,
            argbSliderColor);
        }
      else
        {
         //--- Draw the thin (collapsed) slider when the scrollbar is not hovered
         int  thinWidthHighRes     = logScrollbarThinWidth * supersamplingFactor;
         int  thinPositionXHighRes = logScrollbarPositionXHighRes +
                                     ((logTrackWidth * supersamplingFactor) - thinWidthHighRes) / 2;
         int  capRadius            = thinWidthHighRes / 2;
         uint argbSliderColor      = ColorToARGB(logSliderBackgroundColor, 255);

         logPanelHighResolutionCanvas.Arc(thinPositionXHighRes + capRadius,
            logSliderPositionYHighRes + capRadius,
            capRadius, capRadius,
            ConvertDegreesToRadians(180), ConvertDegreesToRadians(90), argbSliderColor);
         logPanelHighResolutionCanvas.FillCircle(thinPositionXHighRes + capRadius,
            logSliderPositionYHighRes + capRadius, capRadius, argbSliderColor);
         logPanelHighResolutionCanvas.Arc(thinPositionXHighRes + capRadius,
            logSliderPositionYHighRes + (logSliderHeight * supersamplingFactor) - capRadius,
            capRadius, capRadius,
            ConvertDegreesToRadians(90), ConvertDegreesToRadians(90), argbSliderColor);
         logPanelHighResolutionCanvas.FillCircle(thinPositionXHighRes + capRadius,
            logSliderPositionYHighRes + (logSliderHeight * supersamplingFactor) - capRadius,
            capRadius, argbSliderColor);
         logPanelHighResolutionCanvas.FillRectangle(
            thinPositionXHighRes,
            logSliderPositionYHighRes + capRadius,
            thinPositionXHighRes + thinWidthHighRes,
            logSliderPositionYHighRes + (logSliderHeight * supersamplingFactor) - capRadius,
            argbSliderColor);
        }
     }

   //--- Downsample the high-res canvas to the display canvas and flush it
   DownsampleCanvasImage(logPanelCanvas, logPanelHighResolutionCanvas);
   logPanelCanvas.Update();
  }

Сначала определим вспомогательную функцию "GetLogEntryTextColor" для получения соответствующего цвета для записи лога в зависимости от ее типа из "ENUM_LOG_TYPE". Используем оператор switch для сопоставления каждого типа с соответствующим входным цветом, например, "logFrequencyTextColor" для логов частоты или "logSuccessTextColor" для успешного выполнения, по умолчанию используя "logInfoTextColor" для остальных, обеспечивая визуальное различение записей по цветам.

Функция "RenderLogPanelVisualization" отвечает за отрисовку панели лога. Функция очищает высокодетализированный объект Canvas, отрисовывает фон и элементы, обновляет состояние полосы прокрутки, понижает дискретизацию изображения и сбрасывает результат. Мы установим жирный шрифт для заголовка "ЛОГ ЧАСТОТНОГО АНАЛИЗА" по центру с помощью "TextOut", а затем переключаемся на моноширинный шрифт для логов. Рассчитаем высоту строки как высоту текста плюс отступы, видимую область под заголовком, общее содержимое как количество элементов, умноженное на высоту, и максимальную высоту прокрутки как избыточную высоту. Если установлен флаг "pendingAutoScrollToBottom" (при включении автоматической прокрутки для новых записей), перейдём к концу списка и сбросим флаг. Ограничим текущую позицию допустимым диапазоном.

Для видимой полосы прокрутки (если содержимое выходит за границы) рисуем заливку дорожки, а при наведении курсора добавляем кнопки вверх/вниз в виде прямоугольников с фоном (варианты при наведении), стрелки с помощью функции "DrawRoundedTriangleArrow" с заданными цветами (отключается, если достигнуты пределы, корректируется при наведении) и закругленный ползунок с дугами с помощью функций Arc и FillCircle плюс заливка посередине, используя цвета для обычного состояния/состояния при наведении/перетаскивании. Если курсор не наведен, рисуем более тонкий ползунок аналогичным образом. Наконец, понизим дискретизацию изображения высокого разрешения до основного объекта Canvas лога с помощью функции "DownsampleCanvasImage" для сглаживания итогового изображения и вызовем функцию "Update" для отображения, обеспечивая плавное интерактивное отображение лога. После определения всех функций рендеринга и анализа, теперь соберем все воедино в обработчике событий инициализации.

//+------------------------------------------------------------------+
//| Initialize expert advisor                                        |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Restore canvas geometry to the configured initial values
   currentCanvasPositionX = initialCanvasPositionX;
   currentCanvasPositionY = initialCanvasPositionY;
   currentCanvasWidth     = initialCanvasWidth;
   currentCanvasHeight    = initialCanvasHeight;
   currentLogPanelHeight  = logPanelHeight;

   //--- Create the main distribution histogram canvas
   if (!mainDistributionCanvas.CreateBitmapLabel(0, 0, canvasObjectName,
       currentCanvasPositionX, currentCanvasPositionY,
       currentCanvasWidth, currentCanvasHeight,
       COLOR_FORMAT_ARGB_NORMALIZE))
     {
      Print("ERROR: Failed to create distribution canvas");
      return INIT_FAILED;
     }

   //--- Create the log panel canvas positioned below the main canvas
   int logPanelPositionY = currentCanvasPositionY + currentCanvasHeight + panelGap;
   if (!logPanelCanvas.CreateBitmapLabel(0, 0, logCanvasObjectName,
       currentCanvasPositionX, logPanelPositionY,
       currentCanvasWidth, currentLogPanelHeight,
       COLOR_FORMAT_ARGB_NORMALIZE))
     {
      Print("ERROR: Failed to create log panel canvas");
      return INIT_FAILED;
     }

   //--- Create the high-resolution canvas used for supersampled log rendering
   if (!logPanelHighResolutionCanvas.Create(logCanvasHighResolutionName,
       currentCanvasWidth   * supersamplingFactor,
       currentLogPanelHeight * supersamplingFactor,
       COLOR_FORMAT_ARGB_NORMALIZE))
     {
      Print("ERROR: Failed to create log panel high-res canvas");
      return INIT_FAILED;
     }

   //--- Initialize the log system and record startup messages
   ArrayResize(logEntriesArray, 0);
   AddLogEntry("=== FREQUENCY ANALYSIS SYSTEM INITIALIZED ===", LOG_SUCCESS);
   AddLogEntry(StringFormat("Window Size: %d bars | Bins: %d",
               analysisWindowSize, frequencyBins), LOG_INFO);

   //--- Run the initial analysis pipeline
   PerformFrequencyAnalysis();
   CalculateBasicStatistics();
   CalculateChiSquareTest();
   CalculateShannonEntropy();
   CalculateAutoCorrelation();
   CalculateAdvancedStatistics();

   AddLogEntry("Initial frequency analysis completed", LOG_SUCCESS);

   //--- Render both panels and enable mouse interaction
   RenderDistributionVisualization();
   RenderLogPanelVisualization();
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE,  true);
   ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);
   ChartRedraw();

   return INIT_SUCCEEDED;
  }

В обработчике OnInit присваиваем значения входных данных глобальным переменным, таким как "currentCanvasPositionX" из "initialCanvasPositionX", для размещения и размера объекта Canvas, включая "currentLogPanelHeight" из "logPanelHeight", точно так же, как и в предыдущей версии. Создадим основной Canvas для гистограммы распределения с помощью CreateBitmapLabel, используя нормализованное ARGB, проверяем наличие ошибки для вывода на экран и вернем значение INIT_FAILED. Аналогично, вычислим панель лога Y как Y объекта Canvas плюс высота плюс "panelGap", создадим объект Canvas лога и версию с высоким разрешением, масштабированную с помощью "supersamplingFactor" для сглаживания, обработаем ошибки. Для инициализации логов изменим размер "logEntriesArray" на ноль, добавим запись об успешном завершении инициализации системы и еще одну информационную запись, форматирующую размер окна и интервалы с помощью функции StringFormat

Мы выполним первоначальный частотный анализ с помощью функции "PerformFrequencyAnalysis", вычислим основные показатели с помощью "CalculateBasicStatistics", критерий хи-квадрат (если включен) с помощью "CalculateChiSquareTest", энтропию с помощью "CalculateShannonEntropy", корреляцию с помощью "CalculateAutoCorrelation" и расширенную статистику с помощью "CalculateAdvancedStatistics". Далее регистрируем завершение как успешное. Отобразим распределение с помощью "RenderDistributionVisualization", а панель логов — с помощью "RenderLogPanelVisualization". Наконец, включим события перемещения мыши и прокрутки колесика мыши с помощью ChartSetInteger, перерисуем график с помощью "ChartRedraw" и вернем INIT_SUCCEEDED для подтверждения готовности. После завершения инициализации определим обработчик тиков, который управляет обновлениями в реальном времени.

Обеспечение анализа в реальном времени в обработчике тиковых событий

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

//+------------------------------------------------------------------+
//| Handle tick events                                               |
//+------------------------------------------------------------------+
void OnTick()
  {
   static datetime lastBarOpenTime = 0;
   datetime currentBarOpenTime = iTime(_Symbol, PERIOD_CURRENT, 0);

   tickUpdateCounter++;

   //--- Decide whether to run the analysis based on the configured compute mode
   bool performAnalysis = false;
   if (computeMode == PER_BAR)
     {
      //--- Trigger once per new bar
      if (currentBarOpenTime > lastBarOpenTime)
        {
         performAnalysis  = true;
         lastBarOpenTime  = currentBarOpenTime;
        }
     }
   else if (computeMode == PER_TICK)
     {
      //--- Trigger every N ticks according to the configured interval
      if (tickUpdateCounter >= logUpdateIntervalTicks)
        {
         performAnalysis      = true;
         tickUpdateCounter    = 0;
        }
     }

   if (performAnalysis)
     {
      AddLogEntry("--- ANALYSIS UPDATE ---", LOG_INFO);

      //--- Run the full analysis pipeline
      PerformFrequencyAnalysis();
      CalculateBasicStatistics();
      CalculateChiSquareTest();
      CalculateShannonEntropy();
      CalculateAutoCorrelation();
      CalculateAdvancedStatistics();

      //--- Log frequency results
      AddLogEntry(StringFormat("FREQ: Mode=%.5f (count=%d, %.2f%%)",
                  currentModeValue, modeFrequencyCount,
                  (double)modeFrequencyCount / ArraySize(priceDataArray) * 100),
                  LOG_FREQUENCY);

      //--- Log statistical summary
      AddLogEntry(StringFormat("STAT: Mean=%.5f | StdDev=%.5f | Skew=%.3f",
                  currentMeanValue, currentStandardDeviation, currentSkewnessValue),
                  LOG_STATISTICAL);

      //--- Log chi-square result if enabled
      if (enableChiSquareTest)
         AddLogEntry(StringFormat("CHI²: χ²=%.4f (df=%d)",
                     chiSquareTestStatistic, frequencyBins - 1),
                     LOG_STATISTICAL);

      //--- Log entropy result if enabled
      if (enableEntropyCalculation)
         AddLogEntry(StringFormat("ENTROPY: H=%.4f bits", shannonEntropyValue),
                     LOG_VECTOR_MATRIX);

      //--- Log autocorrelation result if enabled
      if (enableCorrelationAnalysis)
         AddLogEntry(StringFormat("CORR: ρ(lag-1)=%.4f", correlationCoefficient),
                     LOG_VECTOR_MATRIX);

      //--- Refresh both panels
      RenderDistributionVisualization();
      RenderLogPanelVisualization();
      ChartRedraw();
     }
  }

В обработчике событий OnTick увеличи "tickUpdateCounter" для отслеживания. Далее установим флаг "performAnalysis" на основе "computeMode": если "PER_BAR", проверяем наличие нового бара, сравним временные метки, и обновляем, если значение true. Если "PER_TICK", анализируем, когда счетчик достигнет "logUpdateIntervalTicks", и сбрасываем его. Если установлен флаг, добавим запись в лог информации об обновлении, выполним полный набор аналитических операций: "PerformFrequencyAnalysis" для разбиения на интервалы, "CalculateBasicStatistics" для расчета среднего значения/стандартного отклонения/коэффициента асимметрии, "CalculateChiSquareTest", если включено, "CalculateShannonEntropy", если включено, "CalculateAutoCorrelation", если активно и "CalculateAdvancedStatistics" для расчета процентилей/доверительных интервалов. Далее регистрируем ключевые результаты, такие как режим (с помощью StringFormat для частоты в процентах), среднее значение/стандартное отклонение/коэффициент асимметрии как статистические данные, условный критерий хи-квадрат с степенями свободы (DF), энтропия как вектор-матрица и корреляция с задержкой 1.

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

//+------------------------------------------------------------------+
//| Deinitialize expert advisor                                      |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Release all canvas bitmap objects from the chart
   mainDistributionCanvas.Destroy();
   logPanelCanvas.Destroy();
   logPanelHighResolutionCanvas.Destroy();
   ChartRedraw();
  }

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

FREQUENCY ANALYSIS ON BINS

Поскольку гистограмма, панель статистики и прокручиваемая панель логов отображаются корректно, реализация завершена. Осталось провести тестирование системы. Это рассматривается в следующем разделе.


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

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

BACKTEST GIF

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


Заключение

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

  • Читать частотную гистограмму для определения интервала с наибольшей плотностью в качестве области текущего значения, рассматривая возвраты цен в этот интервал от отклонения, что может служить сигналом для сделки на возврат к среднему
  • Отслеживать энтропию Шеннона в течение торговых сессий, чтобы обнаруживать перепады, сигнализирующие о формирующейся направленной структуре, различая диапазонные условия, подходящие для торговли на возврат к среднему, и условия пробоя, за которыми стоит следовать
  • Использовать статистику хи-квадрат в качестве фильтра перед открытием сделки, входя в сделки по частотным сетапам только тогда, когда значение достаточно велико, чтобы подтвердить, что кластеризация является статистически значимой, а не случайной

На этом мы завершаем статью.

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

Прикрепленные файлы |
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Моделирование рынка: Первые шаги на SQL в MQL5 (II) Моделирование рынка: Первые шаги на SQL в MQL5 (II)
Хотя многие считают, что мы можем без проблем встраивать SQL-код в другой код, обычно это не так. Причина заключается в том, что SQL-код включается в исполняемый файл в виде строки. И тот факт, что SQL-код внедряется в виде строки, хотя и не вызывает проблем в небольших фрагментах, в итоге это может создать нам немало головной боли.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Алгоритм оптимизации на основе коронавируса — Corona Virus Optimization (CVO) Алгоритм оптимизации на основе коронавируса — Corona Virus Optimization (CVO)
Описываем и реализуем CVO: заражение как генерация кандидатов, покоординатное нормальное возмущение, динамическая популяция. Алгоритм интегрирован в C_AO и проверен на стандартном бенчмарке. Разбор выявляет масштабную причину стагнации и даёт прикладное решение — переход к относительному шагу по ширине диапазона; код готов к использованию.