Торговые инструменты MQL5 (Часть 26): Интеграция частотного разбиения, энтропии и критерия хи-квадрат в визуальный анализатор
Введение
У вас есть инструмент построения графиков с несколькими распределениями, но он моделирует синтетические выборки, а не фактические рыночные данные, поэтому вы не можете увидеть, как распределяются цены закрытия по ценовым уровням, количественно оценить предсказуемость текущей структуры рынка или проверить, склонны ли цены к кластеризации или распределены равномерно. Эта статья предназначена для разработчиков MetaQuotes Language 5 (MQL5) и алгоритмических трейдеров, стремящихся создать инструмент частотного анализа, работающий непосредственно с данными о ценах в реальном времени, что позволяет получать статистические выводы, пригодные для практического применения.
В своей предыдущей статье (Часть 25) мы расширили инструмент построения графиков для поддержки семнадцати статистических распределений с циклическим перебором распределений с помощью значка переключения в заголовке. В Части 26 мы создадим инструмент частотного анализа, который группирует цены закрытия в гистограмму, вычисляет энтропию Шеннона и выполняет критерий согласия хи-квадрат. Инструмент также включает панель лога с автоматической прокруткой, режимы для каждого бара/каждого тика и рендеринг с суперсэмплингом. Мы рассмотрим следующие темы:
- Исследование разбиения по частотным интервалам, энтропии и структуры хи-квадрат
- Реализация средствами MQL5
- Тестирование на истории
- Заключение
В итоге у вас будет комплексный инструмент частотного анализа на MQL5, готовый к пользовательским настройкам. Перейдём к реализации!
Исследование разбиения по частотным интервалам, энтропии и структуры хи-квадрат
Разбиение по частотным интервалам (Frequency binning) делит диапазон цен на интервалы одинаковой ширины и подсчитывает, сколько закрытий приходится на каждый интервал (бин). Плотные интервалы указывают на зоны принятия цены, в то время как разреженные интервалы указывают на импульсивные переходы. Энтропия Шеннона количественно определяет, насколько равномерно распределены количества по интервалам: идеально равномерное распределение дает максимальную энтропию, в то время как гистограмма, в которой преобладают один или два интервала, дает низкую энтропию. Это свидетельствует о сильной кластеризации и потенциально пригодной для торговли рыночной структуре. Критерий хи-квадрат формализует это, сравнивая наблюдаемые количества попаданий в интервалы с ожидаемыми при равномерном распределении, давая статистику по критерию, которая увеличивается по мере усиления кластеризации, со степенями свободы, равными количеству интервалов минус единица.
На рынке используйте частотную гистограмму, чтобы определить интервал с наибольшей плотностью в качестве текущей зоны стоимости - цена, возвращающаяся к этому интервалу после отклонения, что может служить сигналом для сделки на возврат к среднему. Отслеживайте энтропию во время сессий: падение энтропии после периода диапазона может указывать на формирование направленного сценария пробоя. Используйте статистику хи-квадрат в качестве фильтра — высокие значения подтверждают ценность торговли при неслучайной кластеризации, в то время как значения, близкие к нулю, указывают на случайное блуждание, при котором стратегии, основанные на частоте, теряют преимущество.
Мы загрузим последние цены закрытия и поместим их в заданные пользователем интервалы. Далее вычислим относительные частоты, энтропию (−∑p·log p) и статистику хи-квадрат. Результаты отображаются на объекте Canvas с панелью статистики и панелью лога с автоматической прокруткой (для каждого бара или тика). В двух словах, вот какой результат мы намерены получить.

Реализация средствами 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(); }
После компиляции получаем следующий результат.

Поскольку гистограмма, панель статистики и прокручиваемая панель логов отображаются корректно, реализация завершена. Осталось провести тестирование системы. Это рассматривается в следующем разделе.
Тестирование на истории
Мы провели тестирование, а ниже показан итоговый результат визуализации в формате Graphics Interchange Format (GIF).

В ходе тестирования частотная гистограмма корректно обновлялась на каждом новом баре в режиме отображения по каждому бару и с заданным интервалом тиков в режиме отображения по каждому тику. Значения энтропии Шеннона и хи-квадрат существенно изменялись в зависимости от рыночных условий. Панель логов автоматически прокручивалась до последней записи без нарушения структуры, когда количество записей превышало видимую область.
Заключение
В заключение отметим, что мы разработали инструмент частотного анализа в MQL5, который группирует по интервалам цены закрытия в гистограммы, вычисляет энтропию Шеннона для количественной оценки предсказуемости рынка и применяет критерии хи-квадрат для обнаружения неслучайной кластеризации цен. Реализация охватывает построение интервалов равной ширины, вычисление относительной частоты, базовую и расширенную статистику, панель логов с суперсэмплингом с автоматической прокруткой и закругленной интерактивной полосой прокрутки, а также режимы обновления по барам или по тикам. После прочтения этой статьи вы сможете:
- Читать частотную гистограмму для определения интервала с наибольшей плотностью в качестве области текущего значения, рассматривая возвраты цен в этот интервал от отклонения, что может служить сигналом для сделки на возврат к среднему
- Отслеживать энтропию Шеннона в течение торговых сессий, чтобы обнаруживать перепады, сигнализирующие о формирующейся направленной структуре, различая диапазонные условия, подходящие для торговли на возврат к среднему, и условия пробоя, за которыми стоит следовать
- Использовать статистику хи-квадрат в качестве фильтра перед открытием сделки, входя в сделки по частотным сетапам только тогда, когда значение достаточно велико, чтобы подтвердить, что кластеризация является статистически значимой, а не случайной
На этом мы завершаем статью.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21339
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Моделирование рынка: Первые шаги на SQL в MQL5 (II)
Алгоритм оптимизации на основе коронавируса — Corona Virus Optimization (CVO)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования