Торговые инструменты MQL5 (Часть 22): Построение гистограммы и функции вероятностной массы (PMF) биномиального распределения
Введение
У вас есть торговая система, которая приносит череду прибыльных и убыточных сделок, но вы не можете ответить на простые, но крайне важные вопросы: "Какова вероятность того, что из 30 сделок выигрышными будут 20, если мой исторический винрейт (доля выигрышных сделок) составляет 75%?" или "Насколько вероятна серия из 5 убыточных сделок?" Без возможности смоделировать биномиальное распределение вам остается только гадать – вы не можете реалистично оценить риск, установить соответствующие размеры позиций или проверить, является ли эффективность вашей стратегии статистически значимой. Недостаток понимания вероятностной природы результатов часто приводит к чрезмерному использованию кредитного плеча, эмоциональным решениям и нереалистичным ожиданиям прибыли. Эта статья написана для разработчиков MetaQuotes Language 5 (MQL5) и алгоритмических трейдеров, стремящихся количественно оценить вероятностное поведение своих торговых стратегий.
Итак, в своей предыдущей статье (Часть 21) мы улучшили инструмент построения графиков регрессии в MQL5, добавляя режим темы киберпанка с неоновым свечением, анимацией и голографическими рамками для иммерсивной визуализации. В Части 22 мы создадим инструмент построения графиков на MQL5 для визуализации биномиального распределения с помощью гистограммы смоделированных выборок и теоретической кривой функции массы вероятности на интерактивном объекте Canvas. Мы добавим расширенные статистические данные, включая среднее значение, стандартное отклонение, коэффициент асимметрии, коэффициент эксцесса, процентили и доверительные интервалы, а также настраиваемые темы, градиенты и метки. Кроме того, мы включим возможность перетаскивания, изменения размера, обновления в реальном времени и настройки параметров для испытаний, вероятности, размера выборки и отображения, что полезно для анализа торговых стратегий. В статье рассмотрим следующие темы:
- Исследование структуры биномиального распределения
- Реализация средствами MQL5
- Тестирование на истории
- Заключение
В итоге у вас будет функциональный графический инструмент MQL5 для визуализации биномиальных распределений. Перейдём к реализации!
Исследование структуры биномиального распределения
Биномиальное распределение моделирует число успешных исходов в фиксированном количестве независимых испытаний, каждое из которых имеет одинаковую вероятность успеха. В торговле "испытание" может представлять собой любое бинарное событие: сделка заканчивается прибылью (успехом) или убытком (неудачей), сигнал действителен или недействителен, или рыночное условие выполняется или нет. Если исторический винрейт вашей стратегии равен "p", биномиальное распределение покажет вам, какова вероятность получения "k" выигрышных сделок из "n" сделок чисто случайно. Вот как можно это использовать на рынке:
- Оценка вероятности достижения профит-таргета: Предположим, ваша система имеет выигрышные сделки в 60% случаев. Если вы планируете открывать 20 сделок в месяц, вы можете рассчитать вероятность того, что у вас будет как минимум 12 выигрышных сделок – это ценная проверка на практике.
- Установка реалистичных ожиданий просадки: Распределение помогает оценить максимально вероятную длину серии убыточных сделок. Если вероятность пяти последовательных убыточных сделок очень мала, такая серия может указывать на сбой стратегии.
- Сравнение стратегий: Две системы могут иметь одинаковые винрейты, но та, которая имеет более узкое распределение (то есть меньшую дисперсию), более надежна – это видно на гистограмме и стандартном отклонении.
- Проверка размера выборки: Доверительные интервалы показывают, насколько точен ваш расчетный винрейт. Большие интервалы означают, что вам нужно больше сделок, чтобы быть уверенным в истинной эффективности системы.
Наш план состоит в том, чтобы смоделировать выборки из биномиального распределения, используя указанные испытания и вероятность успеха, вычислить гистограмму эмпирических частот, наложить кривую теоретической функции массы вероятности для сравнения и отобразить ключевые статистические данные, такие как среднее значение, стандартное отклонение, коэффициент асимметрии, коэффициент эксцесса, процентили и доверительные интервалы. Мы отобразим это на изменяемом по размеру и перетаскиваемом объекте Canvas с настраиваемыми темами, фоновыми градиентами, метками осей и условными обозначениями, а также включим обновления в реальном времени на основе изменений таймфреймов графика для обеспечения интерактивного вероятностного анализа. Вкратце, эта настройка создает инструмент для изучения свойств распределения и их влияния на торговые сценарии. Посмотрим, чего мы добьемся.

Торговое значение признаков
Прежде чем перейти к реализации, давайте проясним, почему каждый компонент этого инструмента важен с точки зрения трейдера, как уже говорилось ранее.
- Гистограмма (эмпирические частоты): Показывает, сколько раз каждое число успешных сделок имело место в вашей смоделированной выборке. Высокий бар на определенном уровне "k" означает, что результат является общим – это "типичное" поведение вашей стратегии. Если гистограмма сильно растянута, результаты вашей системы сильно варьируются.
- Теоретическая кривая PMF: Плавная линия представляет истинные биномиальные вероятности. Наложив ее на гистограмму, вы сразу увидите, соответствует ли ваша смоделированная выборка ожидаемому распределению. Большие расхождения могут указывать на то, что размер вашей выборки слишком мал или что ваше предположение о независимых испытаниях нарушено.
- Среднее значение: Среднее количество успешных сделок. Для системы с винрейтом "p" и количеством сделок "n" среднее значение равно n × p. Если фактическое среднее значение значительно отклоняется, ваш винрейт может отличаться от того, что вы предполагали.
- Стандартное отклонение: Измеряет разброс исходов. Меньшее стандартное отклонение означает, что результаты вашей системы более последовательны, что имеет решающее значение для управления рисками.
- Коэффициент асимметрии: Сообщает, является ли распределение симметричным или отклоняется влево / вправо. Положительная асимметрия (длинный правый хвост) означает, что время от времени возникают особенно удачные серии; отрицательная асимметрия предупреждает о случайных катастрофических убытках.
- Коэффициент эксцесса: Показывает, насколько распределение подвержено экстремальным исходам. Высокий коэффициент эксцесса ("жирные хвосты") говорит о том, что крупные выигрышные или проигрышные серии случаются чаще, чем можно было бы предсказать при нормальном распределении. Это является ключевым фактором при размещении стоп–лосса.
- Доверительные интервалы (95%, 99%): Задают диапазон, в котором с высокой вероятностью находится истинное среднее значение. Узкие интервалы дают вам уверенность в том, что ваши наблюдаемые показатели эффективности надежны; широкие интервалы сигнализируют о том, что вам нужно больше данных.
С помощью этой статистики вы выходите за рамки простого винрейта и начинаете понимать форму своих торговых исходов, что гораздо более ценно для оценки рисков. Теперь начнем реализацию.
Реализация средствами MQL5
Чтобы создать программу на MQL5, откройте MetaEditor, перейдите в Навигатор, найдите папку «Советники» (Experts), щелкните кнопкой мыши на вкладке "Создать" (New) и следуйте инструкциям по созданию файла. Как только это будет сделано, в среде программирования нужно будет объявить некоторые входные параметры и глобальные переменные, которые будем использовать в программе.
Настройка объекта Canvas и основных библиотек
Для начала создания программы включим необходимые библиотеки и определим вспомогательные структуры.
//+------------------------------------------------------------------+ //| Canvas Graphing PART 3 - Statistical Distributions.mq5 | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property strict #include <Canvas\Canvas.mqh> #include <Math\Stat\Binomial.mqh> #include <Math\Stat\Math.mqh> //+------------------------------------------------------------------+ //| Resize direction enumeration | //+------------------------------------------------------------------+ enum ResizeDirection { NO_RESIZE, // No resize action RESIZE_BOTTOM_EDGE, // Resize using bottom edge RESIZE_RIGHT_EDGE, // Resize using right edge RESIZE_CORNER // Resize using bottom-right corner }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "=== DISTRIBUTION SETTINGS ===" input int numTrials = 40; // Number of trials (n) for binomial distribution input double successProbability = 0.75; // Success probability (p) per trial (0.0-1.0) input int sampleSize = 1000000; // Sample size for generating histogram data input int histogramCells = 20; // Number of cells (bins) in the histogram input int histogramGapPixels = 2; // Gap in pixels between adjacent histogram bars input ENUM_TIMEFRAMES chartTimeframe = PERIOD_CURRENT; // Timeframe used for new-bar detection input group "=== CANVAS DISPLAY SETTINGS ===" input int initialCanvasX = 20; // Initial X position of canvas on chart (pixels) input int initialCanvasY = 30; // Initial Y position of canvas on chart (pixels) input int initialCanvasWidth = 600; // Initial width of canvas in pixels input int initialCanvasHeight = 400; // Initial height of canvas in pixels input int plotPadding = 10; // Internal padding around plot area in pixels input group "=== THEME COLOR (SINGLE CONTROL!) ===" input color themeColor = clrDodgerBlue; // Master theme color controlling all UI accents input bool showBorderFrame = true; // Display decorative border frame around canvas input group "=== HISTOGRAM AND CURVE SETTINGS ===" input color histogramColor = clrRed; // Fill color for histogram bars input color theoreticalCurveColor = clrBlue; // Color of theoretical probability mass function curve input int curveLineWidth = 2; // Thickness in pixels of the theoretical curve input group "=== BACKGROUND SETTINGS ===" input bool enableBackgroundFill = true; // Enable gradient background fill inside canvas input color backgroundTopColor = clrWhite; // Top color of the gradient background input double backgroundOpacityLevel = 0.95; // Background opacity level (0.0 fully transparent - 1.0 opaque) input group "=== TEXT AND LABELS ===" input int titleFontSize = 14; // Font size for main window title input color titleTextColor = clrBlack; // Color of the main title text input int labelFontSize = 11; // Font size for general labels and legend input color labelTextColor = clrBlack; // Color of general label text input int axisLabelFontSize = 12; // Font size for axis tick labels input bool showStatistics = true; // Show statistics panel and legend input group "=== STATS & LEGEND PANEL SETTINGS ===" input int statsPanelX = 70; // X position of statistics panel inside canvas input int statsPanelY = 10; // Y offset of statistics panel from header input int statsPanelWidth = 130; // Width of statistics panel in pixels input int statsPanelHeight = 175; // Height of statistics panel in pixels input int panelFontSize = 13; // Font size used in stats and legend panels input int legendHeight = 35; // Height of legend panel in pixels input group "=== INTERACTION SETTINGS ===" input bool enableDragging = true; // Allow dragging canvas by clicking header input bool enableResizing = true; // Allow resizing canvas with mouse grips input int resizeGripSize = 8; // Size of resize grip detection zones in pixels //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ CCanvas mainCanvas; // Main canvas object for all drawing string canvasObjectName = "DistributionCanvas_Main"; // Name of the graphical object on chart int currentPositionX = initialCanvasX; // Current X coordinate of canvas int currentPositionY = initialCanvasY; // Current Y coordinate of canvas int currentWidthPixels = initialCanvasWidth; // Current canvas width in pixels int currentHeightPixels = initialCanvasHeight; // Current canvas height in pixels bool isDraggingCanvas = false; // True while canvas is being dragged bool isResizingCanvas = false; // True while canvas is being resized int dragStartX = 0; // Mouse X when drag started int dragStartY = 0; // Mouse Y when drag started int canvasStartX = 0; // Canvas X when drag started int canvasStartY = 0; // Canvas Y when drag started int resizeStartX = 0; // Mouse X when resize started int resizeStartY = 0; // Mouse Y when resize started int resizeInitialWidth = 0; // Width when resize started int resizeInitialHeight = 0; // Height when resize started ResizeDirection activeResizeMode = NO_RESIZE; // Currently active resize direction ResizeDirection hoverResizeMode = NO_RESIZE; // Hover resize direction bool isHoveringCanvas = false; // Mouse is over canvas area bool isHoveringHeader = false; // Mouse is over header bar bool isHoveringResizeZone = false; // Mouse is over a resize grip int lastMouseX = 0; // Last recorded mouse X int lastMouseY = 0; // Last recorded mouse Y int previousMouseButtonState = 0; // Previous mouse button state const int MIN_CANVAS_WIDTH = 300; // Minimum allowed canvas width const int MIN_CANVAS_HEIGHT = 200; // Minimum allowed canvas height const int HEADER_BAR_HEIGHT = 35; // Fixed height of header bar double sampleData[]; // Generated binomial sample values double histogramIntervals[]; // Center positions of histogram bins double histogramFrequencies[]; // Frequency count per histogram bin double theoreticalXValues[]; // X values for theoretical PMF double theoreticalYValues[]; // Probability values for theoretical PMF double minDataValue = 0.0; // Minimum value in sample data double maxDataValue = 0.0; // Maximum value in sample data double maxFrequency = 0.0; // Highest histogram frequency double maxTheoreticalValue = 0.0; // Highest theoretical probability bool dataLoadedSuccessfully = false; // True after successful data load double sampleMean = 0.0; // Sample mean double sampleStandardDeviation = 0.0; // Sample standard deviation double sampleSkewness = 0.0; // Sample skewness double sampleKurtosis = 0.0; // Sample kurtosis double percentile25 = 0.0; // 25th percentile double percentile50 = 0.0; // 50th percentile (median) double percentile75 = 0.0; // 75th percentile double confidenceInterval95Lower = 0.0; // 95% CI lower bound double confidenceInterval95Upper = 0.0; // 95% CI upper bound double confidenceInterval99Lower = 0.0; // 99% CI lower bound double confidenceInterval99Upper = 0.0; // 99% CI upper bound
Начнём реализацию с включения необходимых библиотек, таких как "#include <Canvas\Canvas.mqh>" для графики на основе объекта Canvas, "#include <Math\Stat\Binomial.mqh>" для работы с функциями биномиального распределения и "#include <Math\Stat\Math.mqh>" для общих математических и статистических утилит. Это заложит основу для визуализации и вычислений. Далее определим перечисление "ResizeDirection" для управления параметрами изменения размера объекта Canvas, в т.ч. "NO_RESIZE" в качестве состояния по умолчанию, "RESIZE_BOTTOM_EDGE" для корректировки по вертикали, "RESIZE_RIGHT_EDGE" для горизонтальных изменений и "RESIZE_CORNER" для диагонального изменения размера. Эти параметры позже позволят поддерживать интерактивные элементы управления пользователя.
Далее объявим ряд входных параметров, сгруппированных для удобства в окне настроек для лучшей категоризации пользовательского интерфейса в настройках программы. Сюда входят параметры распределения, такие как количество испытаний, вероятность успеха, размер выборки, интервалы гистограммы, промежутки между барами и таймфрейм графика; настройки отображения объекта Canvas, такие как начальное положение, размеры и отступы графика; основной цвет темы с видимостью границы; цвета гистограммы и кривых, а также ширина кривой; параметры заливки фона с основным цветом и прозрачностью; размеры, цвета текста и меток, а также переключатель отображения статистики; положение, размеры и шрифты панелей статистики и условных обозначений; а также параметры взаимодействия для перетаскивания, изменения размера и размера маркера, позволяющие настраивать поведение и внешний вид инструмента.
Наконец, инициализируем глобальные переменные для отслеживания состояния программы, начиная с экземпляра класса CCanvas "mainCanvas" для рендеринга, строки для имени объекта Canvas, текущих позиций и размеров, установленных для ввода значений по умолчанию, логические флаги для перетаскивания, изменения размера и наведения курсора, трекеры координат для операций перетаскивания и изменения размера, режимы активного изменения размера и наведения курсора мыши с использованием перечисления, переменные отслеживания мыши, постоянные минимальные размеры объекта Canvas и высоты заголовка, массивы для выборочных данных, интервалов и частот гистограмм, теоретические значения, минимальные/максимальные трекеры, флаг загрузки данных, и статистические показатели, такие как среднее значение, стандартное отклонение, коэффициенты асимметрии, коэффициенты эксцесса, процентили и границы доверительных интервалов, подготавливая структуру для обработки данных и интерактивного отображения. Эти библиотеки позволяют нам мгновенно переходить от теоретических формул к визуальным эффектам в реальном времени. Теперь определим некоторые вспомогательные функции, чтобы сделать код модульным для обеспечения более простого управления. Начнём со вспомогательных функций цвета темы.
//+------------------------------------------------------------------+ //| Lighten the base color | //+------------------------------------------------------------------+ color LightenColor(color baseColor, double factor) { //--- Extract red component from base color uchar r = (uchar)((baseColor >> 16) & 0xFF); //--- Extract green component from base color uchar g = (uchar)((baseColor >> 8) & 0xFF); //--- Extract blue component from base color uchar b = (uchar)(baseColor & 0xFF); //--- Lighten red component r = (uchar)MathMin(255, r + (255 - r) * factor); //--- Lighten green component g = (uchar)MathMin(255, g + (255 - g) * factor); //--- Lighten blue component b = (uchar)MathMin(255, b + (255 - b) * factor); //--- Reassemble ARGB and return lightened color return (r << 16) | (g << 8) | b; } //+------------------------------------------------------------------+ //| Darken the base color | //+------------------------------------------------------------------+ color DarkenColor(color baseColor, double factor) { //--- Extract red component from base color uchar r = (uchar)((baseColor >> 16) & 0xFF); //--- Extract green component from base color uchar g = (uchar)((baseColor >> 8) & 0xFF); //--- Extract blue component from base color uchar b = (uchar)(baseColor & 0xFF); //--- Darken red component r = (uchar)(r * (1.0 - factor)); //--- Darken green component g = (uchar)(g * (1.0 - factor)); //--- Darken blue component b = (uchar)(b * (1.0 - factor)); //--- Reassemble ARGB and return darkened color return (r << 16) | (g << 8) | b; }
Для обработки цветовых вариаций в теме мы создаем функцию "LightenColor", которая использует базовый цвет и коэффициент для получения более светлого оттенка. Она извлекает красный, зеленый и синий компоненты, используя операции битового сдвига и маски, затем корректирует каждый из них, добавляя часть оставшейся интенсивности к 255, умноженной на коэффициент, ограничивая значение 255 с помощью MathMin, и объединяет их в цветовое значение с помощью битовых сдвигов. Аналогичным образом определим функцию "DarkenColor" для генерации более темной версии путем извлечения компонентов RGB таким же образом. Далее умножим каждый на единицу минус коэффициент для уменьшения интенсивности и вернем вновь собранный цвет. Эти вспомогательные функции позволят динамически затенять такие элементы, как заголовки, границы и фоны, на основе основного цвета темы. Теперь можно определить вспомогательные функции для математических вычислений.
Функции расчета статистики
Поскольку нам нужно нечто большее, чем просто красивая картинка, реализуем полный набор статистических помощников.
//+------------------------------------------------------------------+ //| Calculate mean of array | //+------------------------------------------------------------------+ double CalculateMean(const double &data[]) { //--- Get current array size int size = ArraySize(data); //--- Return zero for empty array if (size == 0) return 0.0; //--- Initialize running sum double sum = 0.0; //--- Accumulate every element for (int i = 0; i < size; i++) { sum += data[i]; } //--- Return arithmetic mean return sum / size; } //+------------------------------------------------------------------+ //| Calculate standard deviation of array | //+------------------------------------------------------------------+ double CalculateStandardDeviation(const double &data[], double mean) { //--- Get current array size int size = ArraySize(data); //--- Return zero for insufficient data if (size <= 1) return 0.0; //--- Initialize sum of squared differences double sumSquaredDiff = 0.0; //--- Loop through all values for (int i = 0; i < size; i++) { //--- Compute deviation from mean double diff = data[i] - mean; //--- Accumulate squared deviation sumSquaredDiff += diff * diff; } //--- Return sample standard deviation return MathSqrt(sumSquaredDiff / (size - 1)); } //+------------------------------------------------------------------+ //| Calculate skewness of array | //+------------------------------------------------------------------+ double CalculateSkewness(const double &data[], double mean, double stdDev) { //--- Get current array size int size = ArraySize(data); //--- Return zero for insufficient data or zero std dev if (size < 3 || stdDev == 0.0) return 0.0; //--- Initialize sum of cubed standardized differences double sumCubedDiff = 0.0; //--- Loop through all values for (int i = 0; i < size; i++) { //--- Standardize the deviation double diff = (data[i] - mean) / stdDev; //--- Accumulate cubed term sumCubedDiff += diff * diff * diff; } //--- Cast size to double for formula double n = (double)size; //--- Apply bias-corrected skewness formula and return return (n / ((n - 1) * (n - 2))) * sumCubedDiff; } //+------------------------------------------------------------------+ //| Calculate kurtosis of array | //+------------------------------------------------------------------+ double CalculateKurtosis(const double &data[], double mean, double stdDev) { //--- Get current array size int size = ArraySize(data); //--- Return zero for insufficient data or zero std dev if (size < 4 || stdDev == 0.0) return 0.0; //--- Initialize sum of fourth powers of standardized differences double sumFourthPower = 0.0; //--- Loop through all values for (int i = 0; i < size; i++) { //--- Standardize the deviation double diff = (data[i] - mean) / stdDev; //--- Square the standardized value double squared = diff * diff; //--- Accumulate fourth power sumFourthPower += squared * squared; } //--- Cast size to double double n = (double)size; //--- First part of excess kurtosis formula double kurtosis = (n * (n + 1) / ((n - 1) * (n - 2) * (n - 3))) * sumFourthPower; //--- Subtract bias correction term kurtosis -= (3 * (n - 1) * (n - 1)) / ((n - 2) * (n - 3)); //--- Return kurtosis value return kurtosis; } //+------------------------------------------------------------------+ //| Calculate percentile of array | //+------------------------------------------------------------------+ double CalculatePercentile(double &data[], double percentile) { //--- Get current array size int size = ArraySize(data); //--- Return zero for empty array if (size == 0) return 0.0; //--- Prepare sorted copy double sortedData[]; //--- Resize to match original ArrayResize(sortedData, size); //--- Copy original data ArrayCopy(sortedData, data); //--- Sort in ascending order ArraySort(sortedData); //--- Compute rank for interpolation double rank = (percentile / 100.0) * (size - 1); //--- Lower index for interpolation int lowerIndex = (int)MathFloor(rank); //--- Upper index for interpolation int upperIndex = (int)MathCeil(rank); //--- Exact match case if (lowerIndex == upperIndex) { return sortedData[lowerIndex]; } //--- Compute interpolation fraction double fraction = rank - lowerIndex; //--- Linear interpolation and return return sortedData[lowerIndex] + fraction * (sortedData[upperIndex] - sortedData[lowerIndex]); } //+------------------------------------------------------------------+ //| Calculate confidence interval | //+------------------------------------------------------------------+ void CalculateConfidenceInterval(double mean, double stdDev, int in_sampleSize, double zScore, double &lowerBound, double &upperBound) { //--- Compute margin of error double marginOfError = zScore * (stdDev / MathSqrt(in_sampleSize)); //--- Set lower bound lowerBound = mean - marginOfError; //--- Set upper bound upperBound = mean + marginOfError; } //+------------------------------------------------------------------+ //| Compute advanced statistics | //+------------------------------------------------------------------+ void ComputeAdvancedStatistics() { //--- Calculate sample mean sampleMean = CalculateMean(sampleData); //--- Calculate sample standard deviation sampleStandardDeviation = CalculateStandardDeviation(sampleData, sampleMean); //--- Calculate skewness sampleSkewness = CalculateSkewness(sampleData, sampleMean, sampleStandardDeviation); //--- Calculate kurtosis sampleKurtosis = CalculateKurtosis(sampleData, sampleMean, sampleStandardDeviation); //--- Calculate first quartile percentile25 = CalculatePercentile(sampleData, 25.0); //--- Calculate median percentile50 = CalculatePercentile(sampleData, 50.0); //--- Calculate third quartile percentile75 = CalculatePercentile(sampleData, 75.0); //--- Compute 95% confidence interval CalculateConfidenceInterval(sampleMean, sampleStandardDeviation, sampleSize, 1.96, confidenceInterval95Lower, confidenceInterval95Upper); //--- Compute 99% confidence interval CalculateConfidenceInterval(sampleMean, sampleStandardDeviation, sampleSize, 2.576, confidenceInterval99Lower, confidenceInterval99Upper); }
Начнем с определения функции "calculateMean" для вычисления среднего значения по набору данных. Она извлекает размер массива с помощью ArraySize и проверяет наличие пустого массива, возвращая ноль, если это так. Затем инициализирует переменную суммы, перебирает каждый элемент для накопления значений и, наконец, делит сумму на размер для возврата среднего значения. Далее мы реализуем функцию "calculateStandardDeviation", которая измеряет дисперсию данных относительно заданного среднего значения. После получения размера и обработки случаев с одним или меньшим количеством элементов, возвращая ноль, накапливает сумму квадратов разности от среднего значения в цикле. Далее извлекает квадратный корень, используя MathSqrt от этой суммы, деленной на размер минус единицу, получая стандартное отклонение выборки.
Чтобы оценить асимметрию в распределении данных, создадим функцию "calculateSkewness". Сначала она проверяет входные параметры на наличие как минимум трех элементов и ненулевого стандартного отклонения, в противном случае возвращает ноль. Внутри цикла нормализует каждое отличие от среднего значения путем деления на стандартное отклонение, умножает его на куб и суммирует эти значения. Итоговый коэффициент асимметрии рассчитывается по формуле n / ((n-1)*(n-2)), умноженной на сумму нормализованных разностей в кубе, где n — размер выборки, приведенный к типу double, что позволяет определить, смещаются ли хвосты распределения больше влево (отрицательно) или вправо (положительный), что имеет решающее значение для понимания потенциальных смещений в биномиальных исходах, таких как неравномерные вероятности успешного результата.
Аналогично, для оценки "хвостатости" распределения относительно нормального распределения определим функцию "calculateKurtosis". Для этого требуется как минимум четыре элемента и ненулевое стандартное отклонение; в противном случае возвращается ноль. Цикл вычисляет нормализованные разности, возводит их в квадрат и накапливает четвертые степени. Коэффициент эксцесса затем выводится по формуле (n*(n+1) / ((n-1)(n-2)(n-3))) * сумма степеней четвертой степени минус (3*(n-1)^2 / ((n-2)*(n-3))), что указывает на лептокуртическую (положительные, более тяжелые хвосты) или платикуртическую (отрицательные, более легкие хвосты) формы. Это помогает оценивать риски выбросов в торговых сценариях, моделируемых биномиальным распределением.
Затем добавим функцию "calculatePercentile" для нахождения конкретного процентиля в данных. Она сортирует копию массива с помощью функций ArrayResize, "ArrayCopy" и ArraySort, вычисляет ранг как (процентиль/100) умноженное на (размер-1) и, при необходимости, интерполирует между нижним и верхним индексами с помощью функций MathFloor и MathCeil. Это позволяет использовать квартили для суммирования распределения данных. Для оценки параметров генеральной совокупности введем функцию типа void "calculateConfidenceInterval", которая вычисляет погрешность как z-показатель, умноженный на стандартное отклонение, деленное на квадратный корень из размера выборки, с помощью функции "MathSqrt". Затем устанавливает нижнюю и верхнюю границы, вычитая и добавляя эту погрешность из среднего значения соответственно.
Наконец, мы заключаем эти вычисления в функцию "computeAdvancedStatistics", которая последовательно вызывает вышеуказанные функции для заполнения глобальных переменных: сначала среднее значение и стандартное отклонение, далее коэффициент асимметрии и коэффициент эксцесса, используя их, затем 25-й, 50-й и 75-й процентили и доверительные интервалы для 95% (z=1,96) и 99% (z=2,576) уровней, централизуя статистический анализ для смоделированных биномиальных выборок. Эти значения можно изменить на желаемые, хотя это наиболее стандартные значения. Теперь перейдем к вычислению гистограммы и инициализации объекта Canvas.
Генерация данных и построение гистограммы
Теперь загрузим фактические биномиальные выборки и подготовим гистограмму и теоретическую кривую.
//+------------------------------------------------------------------+ //| Create distribution canvas | //+------------------------------------------------------------------+ bool CreateCanvas() { //--- Attempt to create bitmap label canvas if (!mainCanvas.CreateBitmapLabel(0, 0, canvasObjectName, currentPositionX, currentPositionY, currentWidthPixels, currentHeightPixels, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Creation failed return false; } //--- Canvas created successfully return true; } //+------------------------------------------------------------------+ //| Load distribution data | //+------------------------------------------------------------------+ bool LoadDistributionData() { //--- Seed random generator with current tick count MathSrand(GetTickCount()); //--- Resize sample array to requested size ArrayResize(sampleData, sampleSize); //--- Generate binomial random samples MathRandomBinomial(numTrials, successProbability, sampleSize, sampleData); //--- Compute histogram from samples if (!ComputeHistogram(sampleData, histogramIntervals, histogramFrequencies, maxDataValue, minDataValue, histogramCells)) { //--- Histogram calculation failed Print("ERROR: Failed to calculate histogram"); //--- Return failure return false; } //--- Resize theoretical arrays ArrayResize(theoreticalXValues, numTrials + 1); ArrayResize(theoreticalYValues, numTrials + 1); //--- Fill X values from 0 to n MathSequence(0, numTrials, 1, theoreticalXValues); //--- Compute binomial PMF values MathProbabilityDensityBinomial(theoreticalXValues, numTrials, successProbability, false, theoreticalYValues); //--- Find maximum histogram frequency maxFrequency = histogramFrequencies[ArrayMaximum(histogramFrequencies)]; //--- Find maximum theoretical probability maxTheoreticalValue = theoreticalYValues[ArrayMaximum(theoreticalYValues)]; //--- Compute scaling factor to match histogram height double scaleFactor = maxFrequency / maxTheoreticalValue; //--- Scale theoretical frequencies to visual match for (int i = 0; i < histogramCells; i++) { histogramFrequencies[i] /= scaleFactor; } //--- Compute all advanced statistics ComputeAdvancedStatistics(); //--- Mark data as ready dataLoadedSuccessfully = true; //--- Log success Print("SUCCESS: Loaded binomial distribution data with advanced statistics"); //--- Return success return true; } //+------------------------------------------------------------------+ //| Compute histogram array with forced range | //+------------------------------------------------------------------+ bool ComputeHistogram(const double &data[], double &intervals[], double &frequency[], double &maxv, double &minv, const int cells = 10) { //--- Invalid cell count aborts if (cells <= 1) return false; //--- Get data length int size = ArraySize(data); //--- Empty data aborts if (size < 1) return false; //--- Force histogram range from 0 to n minv = 0; maxv = numTrials; //--- Compute total range double range = maxv - minv; //--- Compute bin width double width = range / cells; //--- Zero width aborts if (width == 0) return false; //--- Resize output arrays ArrayResize(intervals, cells); ArrayResize(frequency, cells); //--- Initialize interval centers and zero frequencies for (int i = 0; i < cells; i++) { intervals[i] = minv + (i + 0.5) * width; frequency[i] = 0; } //--- Bin each sample value for (int i = 0; i < size; i++) { //--- Current value double val = data[i]; //--- Skip out-of-range values if (val < minv || val > maxv) continue; //--- Compute bin index int ind = (int)((val - minv) / width); //--- Clamp index to valid range if (ind >= cells) ind = cells - 1; if (ind < 0) ind = 0; //--- Increment frequency frequency[ind]++; } //--- Histogram computed successfully return true; }
Определим функцию "CreateCanvas" для инициализации графического объекта Canvas. Она вызывает метод CreateBitmapLabel на объекте "mainCanvas". Идентификатор графика равен нулю для основного графика и нулю для подокна. Параметры включают в себя имя объекта Canvas, текущие положения, размеры и COLOR_FORMAT_ARGB_NORMALIZE для поддержки смешивания цветов с альфа-каналом. Это позволяет отображать прозрачные и многослойные изображения. Если создание не удается, возвращается значение false. В противном случае возвращается значение true, гарантируя готовность объекта Canvas к операциям рисования.
Далее реализуем функцию "loadDistributionData" для подготовки данных биномиального распределения. Мы инициализируем генератор случайных чисел с помощью MathSrand, используя функцию GetTickCount. Это обеспечивает разнообразие симуляций в каждом прогоне. Изменим размер массива "sampleData" в соответствии с размером входной выборки с помощью функции "ArrayResize". Затем генерируем биномиальные случайные выборки с помощью MathRandomBinomial на основе количества испытаний, вероятности успеха и размера выборки. Это заполняет массив смоделированными значениями числа успехов. Далее вызовем "computeHistogram", чтобы разделить данные на интервалы и частоты, обновить минимальные и максимальные значения и обработать сбой, выведя сообщение об ошибке и вернув значение false. Для теоретических значений изменим размер массивов "theoreticalXValues" и "theoreticalYValues", чтобы охватить диапазон от нуля до числа испытаний плюс одно.
Заполним X целыми числами с помощью MathSequence и вычислим значения вероятностной массы с помощью MathProbabilityDensityBinomial в некумулятивном режиме. Найдём максимальную частоту и теоретическое значение, используя функцию ArrayMaximum. Затем вычислим коэффициент масштабирования, чтобы согласовать эмпирические частоты с теоретическими вероятностями. Для визуального сравнения используем цикл для деления каждой частоты на этот коэффициент. Наконец, вызовем функцию "computeAdvancedStatistics" для получения описательных показателей. Установим флаг загрузки данных в значение true, выводим сообщение об успешном завершении и возвращаем true, завершая подготовку данных для отображения.
Для поддержки создания гистограмм создадим функцию "computeHistogram", которая обрабатывает выборочные данные, преобразуя их в сгруппированные по интервалам частоты с заданным диапазоном, подходящим для биномиальных исходов. Сначала она проверяет наличие недопустимых значений в ячейках или пустых данных, возвращая значение false в этом случае. Жестко установим минимальное значение равным нулю, а максимальное - количеству испытаний, вычислим диапазон и ширину интервала и возвратим значение false, если ширина равна нулю, чтобы избежать проблем с разделением. После изменения размера интервалов и массивов частот с помощью ArrayResize мы в цикле устанавливаем каждый интервал в центр его диапазона, добавляя половину ширины к минимальному значению плюс индекс, умноженный на ширину, и инициализируем частоты нулем.
Далее для каждой точки данных пропустим значения, выходящие за пределы диапазона, вычислим индекс интервала, вычитая минимум и деля на ширину, ограничим его диапазоном от нуля до количества ячеек минус одна и увеличим соответствующую частоту. Такой подход обеспечивает дискретную, равномерно расположенную гистограмму, адаптированную к биномиальным целым числам от нуля до n, где n — количество испытаний. Это облегчает точную визуализацию эмпирического распределения по сравнению с теоретической кривой, и возвращает значение true в случае успешного выполнения. Мы можем инициализировать его так, чтобы он выполнял вычисления.
//+------------------------------------------------------------------+ //| Initialize expert | //+------------------------------------------------------------------+ int OnInit() { //--- Copy initial X position from user inputs currentPositionX = initialCanvasX; //--- Copy initial Y position from user inputs currentPositionY = initialCanvasY; //--- Copy initial width from user inputs currentWidthPixels = initialCanvasWidth; //--- Copy initial height from user inputs currentHeightPixels = initialCanvasHeight; //--- Attempt to create the canvas object if (!CreateCanvas()) { //--- Log failure if canvas creation fails Print("ERROR: Failed to create distribution canvas"); //--- Return failure code to stop expert return(INIT_FAILED); } //--- Attempt to load binomial distribution data if (!LoadDistributionData()) { //--- Log failure if data loading fails Print("ERROR: Failed to load distribution data"); //--- Return failure code to stop expert return(INIT_FAILED); } //--- Force immediate chart redraw after initialization ChartRedraw(); //--- Return success code so expert starts normally return(INIT_SUCCEEDED); }
В обработчике OnInit присвоим входные значения глобальным переменным для настройки объекта Canvas. Установим "currentPositionX" равным "initialCanvasX", "currentPositionY" равным "initialCanvasY", "currentWidthPixels" равным "initialCanvasWidth" и "currentHeightPixels" равным "initialCanvasHeight", чтобы позиционировать и изменять размер объекта Canvas в соответствии с указаниями пользователя. Затем вызовем функцию "CreateCanvas" для инициализации графической метки. Если она возвращает значение false, выведем сообщение об ошибке и возвратим INIT_FAILED для остановки программы. Далее вызовем функцию "loadDistributionData" для генерации выборок и подготовки данных, обработаем сбой, выводя сообщение об ошибке и вернем "INIT_FAILED". Наконец, обновим график с помощью ChartRedraw и вернём INIT_SUCCEEDED для указания успешной инициализации. Вот что мы получаем после компиляции.

Поскольку все инициализации и вычисления завершены, давайте теперь построим график анализа. Сначала отрисуем объект Canvas.
Функции визуализации – фон, заголовок и основной график
После подготовки данных мы отрисовываем визуальные слои.
//+------------------------------------------------------------------+ //| Draw gradient background | //+------------------------------------------------------------------+ void DrawGradientBackground() { //--- Compute bottom gradient color from theme color bottomColor = LightenColor(themeColor, 0.85); //--- Loop through every row below the header for (int y = HEADER_BAR_HEIGHT; y < currentHeightPixels; y++) { //--- Compute vertical gradient interpolation factor double gradientFactor = (double)(y - HEADER_BAR_HEIGHT) / (currentHeightPixels - HEADER_BAR_HEIGHT); //--- Interpolate color for current row color currentRowColor = InterpolateColors(backgroundTopColor, bottomColor, gradientFactor); //--- Compute alpha channel value uchar alphaChannel = (uchar)(255 * backgroundOpacityLevel); //--- Convert interpolated color to ARGB format uint argbColor = ColorToARGB(currentRowColor, alphaChannel); //--- Fill entire row with current color for (int x = 0; x < currentWidthPixels; x++) { //--- Set pixel at current position mainCanvas.PixelSet(x, y, argbColor); } } } //+------------------------------------------------------------------+ //| Draw canvas border | //+------------------------------------------------------------------+ void DrawCanvasBorder() { //--- Skip drawing if border is disabled in settings if (!showBorderFrame) return; //--- Choose border color based on resize hover state color borderColor = isHoveringResizeZone ? DarkenColor(themeColor, 0.2) : themeColor; //--- Convert border color to ARGB uint argbBorder = ColorToARGB(borderColor, 255); //--- Draw outer border rectangle mainCanvas.Rectangle(0, 0, currentWidthPixels - 1, currentHeightPixels - 1, argbBorder); //--- Draw inner border rectangle mainCanvas.Rectangle(1, 1, currentWidthPixels - 2, currentHeightPixels - 2, argbBorder); } //+------------------------------------------------------------------+ //| Draw header bar | //+------------------------------------------------------------------+ void DrawHeaderBar() { //--- Declare variable for header background color color headerColor; //--- Choose color when canvas is being dragged if (isDraggingCanvas) { headerColor = DarkenColor(themeColor, 0.1); } //--- Choose color when mouse hovers header else if (isHoveringHeader) { headerColor = LightenColor(themeColor, 0.4); } //--- Default header color when idle else { headerColor = LightenColor(themeColor, 0.7); } //--- Convert header color to ARGB uint argbHeader = ColorToARGB(headerColor, 255); //--- Fill header bar area mainCanvas.FillRectangle(0, 0, currentWidthPixels - 1, HEADER_BAR_HEIGHT, argbHeader); //--- Draw header borders only if enabled if (showBorderFrame) { //--- Convert theme color for borders uint argbBorder = ColorToARGB(themeColor, 255); //--- Draw outer header border mainCanvas.Rectangle(0, 0, currentWidthPixels - 1, HEADER_BAR_HEIGHT, argbBorder); //--- Draw inner header border mainCanvas.Rectangle(1, 1, currentWidthPixels - 2, HEADER_BAR_HEIGHT - 1, argbBorder); } //--- Set bold font for title mainCanvas.FontSet("Arial Bold", titleFontSize); //--- Convert title text color to ARGB uint argbText = ColorToARGB(titleTextColor, 255); //--- Format dynamic title with current parameters string titleText = StringFormat("Binomial Distribution (n=%d, p=%.2f)", numTrials, successProbability); //--- Draw centered title text mainCanvas.TextOut(currentWidthPixels / 2, (HEADER_BAR_HEIGHT - titleFontSize) / 2, titleText, argbText, TA_CENTER); } //+------------------------------------------------------------------+ //| Render distribution visualization | //+------------------------------------------------------------------+ void RenderVisualization() { //--- Clear entire canvas before drawing mainCanvas.Erase(0); //--- Draw gradient background if option is enabled if (enableBackgroundFill) { DrawGradientBackground(); } //--- Draw canvas border frame DrawCanvasBorder(); //--- Draw header bar with title DrawHeaderBar(); //--- Push all drawing operations to screen mainCanvas.Update(); }
На этом этапе определим функцию "drawGradientBackground", которая заполняет объект Canvas с вертикальным градиентом, начиная с области под заголовком. Вычислим нижний цвет, осветляя "themeColor" с помощью "LightenColor", затем в цикле переберем строки для вычисления коэффициента, интерполируем цвета с помощью "InterpolateColors". Применим прозрачность с помощью ColorToARGB и установим значения пикселей построчно, используя метод PixelSet для создания едва заметного эффекта затухания. Далее создадим функцию "drawCanvasBorder", которая добавляет рамку, если включена опция "showBorderFrame", выберем затемненный цвет при наведении курсора с изменением размера с помощью "DarkenColor". Преобразуем его в ARGB и рисуем внутренние и внешние прямоугольники с помощью метода Rectangle для создания эффекта рамки.
Для верхней части мы реализуем функцию "drawHeaderBar", выберем цвет заголовка на основе состояния перетаскивания или наведения курсора, используя "DarkenColor" или "LightenColor". Заполним область методом FillRectangle. Добавим границы, если они включены, установим жирный шрифт с помощью FontSet, отформатируем заголовок с помощью StringFormat и отцентрируя его с помощью метода TextOut. Наконец, определим функцию "renderVisualization" для компоновки отображения: сотрём объект Canvas с помощью функции Erase, передав значение 0, рисуем градиент, если "enableBackgroundFill" равно true, добавим рамку и заголовок, а затем обновим данные с помощью метода Update для отображения элементов. При вызове функции в процессе инициализации и компиляции получим следующий результат.

После того, как заголовок и границы объекта Canvas готовы, можно приступить к построению графиков. Сначала определим вспомогательные функции для форматирования меток тиков.
Построение осей и графиков с оптимальным количеством тиков
Добавим бары гистограммы и теоретическую кривую.
//+------------------------------------------------------------------+ //| Calculate optimal ticks with aggressive spacing | //+------------------------------------------------------------------+ int CalculateOptimalTicks(double minValue, double maxValue, int pixelRange, double &tickValues[]) { //--- Compute data range double range = maxValue - minValue; //--- Guard against zero range if (range == 0 || pixelRange <= 0) { ArrayResize(tickValues, 1); tickValues[0] = minValue; return 1; } //--- Target number of ticks based on pixels int targetTickCount = (int)(pixelRange / 50.0); //--- Enforce minimum ticks if (targetTickCount < 3) targetTickCount = 3; //--- Enforce maximum ticks if (targetTickCount > 20) targetTickCount = 20; //--- Rough step size double roughStep = range / (double)(targetTickCount - 1); //--- Determine magnitude for nice numbers double magnitude = MathPow(10.0, MathFloor(MathLog10(roughStep))); //--- Normalize step double normalized = roughStep / magnitude; //--- Choose nice step value double niceNormalized; if (normalized <= 1.0) niceNormalized = 1.0; else if (normalized <= 1.5) niceNormalized = 1.0; else if (normalized <= 2.0) niceNormalized = 2.0; else if (normalized <= 2.5) niceNormalized = 2.0; else if (normalized <= 3.0) niceNormalized = 2.5; else if (normalized <= 4.0) niceNormalized = 4.0; else if (normalized <= 5.0) niceNormalized = 5.0; else if (normalized <= 7.5) niceNormalized = 5.0; else niceNormalized = 10.0; //--- Final nice step double step = niceNormalized * magnitude; //--- Compute first tick below min double tickMin = MathFloor(minValue / step) * step; //--- Compute last tick above max double tickMax = MathCeil(maxValue / step) * step; //--- Number of ticks int numTicks = (int)MathRound((tickMax - tickMin) / step) + 1; //--- Reduce density if too many ticks if (numTicks > 25) { step *= 2.0; tickMin = MathFloor(minValue / step) * step; tickMax = MathCeil(maxValue / step) * step; numTicks = (int)MathRound((tickMax - tickMin) / step) + 1; } //--- Increase density if too few ticks if (numTicks < 3) { step /= 2.0; tickMin = MathFloor(minValue / step) * step; tickMax = MathCeil(maxValue / step) * step; numTicks = (int)MathRound((tickMax - tickMin) / step) + 1; } //--- Resize output array ArrayResize(tickValues, numTicks); //--- Fill tick values for (int i = 0; i < numTicks; i++) { tickValues[i] = tickMin + i * step; } //--- Return actual tick count return numTicks; } //+------------------------------------------------------------------+ //| Format tick label with appropriate precision | //+------------------------------------------------------------------+ string FormatTickLabel(double value, double range) { //--- No decimals for large range if (range > 100) return DoubleToString(value, 0); //--- One decimal else if (range > 10) return DoubleToString(value, 1); //--- Two decimals else if (range > 1) return DoubleToString(value, 2); //--- Three decimals else if (range > 0.1) return DoubleToString(value, 3); //--- Four decimals for tiny values else return DoubleToString(value, 4); }
Сначала определим функцию "calculateOptimalTicks", которая определяет равномерно расположенные закругленные тиковые отметки для осей на основе диапазона данных и пространства в пикселях. Это обеспечит четкую маркировку без переполнения. Вычислим диапазон и обработаем крайние случаи, устанавливая единственный тик, если значение недопустимо. Затем оцениваем целевое количество, деля "pixelRange" на 50, ограничимся значениями от 3 до 20. Вычисляется приблизительный шаг, который нормализуется с помощью MathLog10, "MathFloor" и MathPow для нахождения подходящего приращения на основе величины с помощью условной логики для таких значений, как 1.0, 2.0, 2.5 и т.д., что повышает читаемость графиков. Скорректируем шаг, если результирующее количество тиков превышает 25 или находится ниже 3, удваивая или уменьшая его вдвое. Изменяем размер массива "tickValues" с помощью "ArrayResize", заполняем его в цикле от минимального значения до максимального значения c и возвращаем значение count, активно адаптируя его к области визуализации.
Далее создадим функцию "formatTickLabel" для преобразования значений тиков в строки с динамической точностью в зависимости от диапазона данных, избегая ненужных десятичных знаков для больших масштабов. Она использует условные проверки: без десятичных знаков, если диапазон больше 100, один, если больше 10, два, если больше 1, три, если больше 0,1, или четыре в противном случае. Вернём отформатированную строку через DoubleToString для кратких меток оси. Теперь можно использовать эти функции для рисования оси графика следующим образом.
//+------------------------------------------------------------------+ //| Draw distribution plot | //+------------------------------------------------------------------+ void DrawDistributionPlot() { //--- Skip if data not ready if (!dataLoadedSuccessfully) return; //--- Define plot margins int plotAreaLeft = 60; int plotAreaRight = currentWidthPixels - 40; int plotAreaTop = HEADER_BAR_HEIGHT + 10; int plotAreaBottom = currentHeightPixels - 50; //--- Apply internal padding int drawAreaLeft = plotAreaLeft + plotPadding; int drawAreaRight = plotAreaRight - plotPadding; int drawAreaTop = plotAreaTop + plotPadding; int drawAreaBottom = plotAreaBottom - plotPadding; //--- Compute usable plot dimensions int plotWidth = drawAreaRight - drawAreaLeft; int plotHeight = drawAreaBottom - drawAreaTop; //--- Abort on zero size if (plotWidth <= 0 || plotHeight <= 0) return; //--- Data ranges for scaling double rangeX = maxDataValue - minDataValue; double rangeY = maxTheoreticalValue; //--- Prevent division by zero if (rangeX == 0) rangeX = 1; if (rangeY == 0) rangeY = 1; //--- Axis color uint argbAxisColor = ColorToARGB(clrBlack, 255); //--- Draw thick Y axis for (int thick = 0; thick < 2; thick++) { mainCanvas.Line(plotAreaLeft - thick, plotAreaTop, plotAreaLeft - thick, plotAreaBottom, argbAxisColor); } //--- Draw thick X axis for (int thick = 0; thick < 2; thick++) { mainCanvas.Line(plotAreaLeft, plotAreaBottom + thick, plotAreaRight, plotAreaBottom + thick, argbAxisColor); } //--- Set font for tick labels mainCanvas.FontSet("Arial", axisLabelFontSize); uint argbTickLabel = ColorToARGB(clrBlack, 255); //--- Y-axis ticks double yTickValues[]; int numYTicks = CalculateOptimalTicks(0, rangeY, plotHeight, yTickValues); //--- Draw each Y tick for (int i = 0; i < numYTicks; i++) { double yValue = yTickValues[i]; if (yValue < 0 || yValue > rangeY) continue; //--- Compute screen Y position int yPos = drawAreaBottom - (int)((yValue - 0) / rangeY * plotHeight); //--- Draw tick mark mainCanvas.Line(plotAreaLeft - 5, yPos, plotAreaLeft, yPos, argbAxisColor); //--- Format label string yLabel = FormatTickLabel(yValue, rangeY); //--- Draw right-aligned label mainCanvas.TextOut(plotAreaLeft - 8, yPos - axisLabelFontSize/2, yLabel, argbTickLabel, TA_RIGHT); } //--- X-axis ticks double xTickValues[]; int numXTicks = CalculateOptimalTicks(minDataValue, maxDataValue, plotWidth, xTickValues); //--- Draw each X tick for (int i = 0; i < numXTicks; i++) { double xValue = xTickValues[i]; if (xValue < minDataValue || xValue > maxDataValue) continue; //--- Compute screen X position int xPos = drawAreaLeft + (int)((xValue - minDataValue) / rangeX * plotWidth); //--- Draw tick mark mainCanvas.Line(xPos, plotAreaBottom, xPos, plotAreaBottom + 5, argbAxisColor); //--- Format label string xLabel = FormatTickLabel(xValue, rangeX); //--- Draw centered label below axis mainCanvas.TextOut(xPos, plotAreaBottom + 7, xLabel, argbTickLabel, TA_CENTER); } //--- Histogram bar color uint argbHist = ColorToARGB(histogramColor, 255); //--- Total gap space double totalGaps = (histogramCells - 1) * histogramGapPixels; //--- Width of each bar double barWidth = (plotWidth - totalGaps) / histogramCells; //--- Minimum visible width if (barWidth < 1) barWidth = 1; //--- Draw every histogram bar for (int i = 0; i < histogramCells; i++) { //--- Left edge of bar int barLeft = drawAreaLeft + (int)(i * (barWidth + histogramGapPixels)); //--- Right edge of bar int barRight = barLeft + (int)barWidth - 1; //--- Height proportional to frequency int barHeight = (int)(histogramFrequencies[i] / rangeY * plotHeight); //--- Top of bar int barTop = drawAreaBottom - barHeight; //--- Draw filled bar if valid if (barRight >= barLeft) { mainCanvas.FillRectangle(barLeft, barTop, barRight, drawAreaBottom, argbHist); } } //--- Theoretical curve color uint argbCurve = ColorToARGB(theoreticalCurveColor, 255); //--- Draw continuous curve for (int i = 0; i < ArraySize(theoreticalXValues) - 1; i++) { //--- Screen X1 int x1 = drawAreaLeft + (int)((theoreticalXValues[i] - minDataValue) / rangeX * plotWidth); //--- Screen Y1 int y1 = drawAreaBottom - (int)(theoreticalYValues[i] / rangeY * plotHeight); //--- Screen X2 int x2 = drawAreaLeft + (int)((theoreticalXValues[i+1] - minDataValue) / rangeX * plotWidth); //--- Screen Y2 int y2 = drawAreaBottom - (int)(theoreticalYValues[i+1] / rangeY * plotHeight); //--- Draw anti-aliased line with thickness for (int w = 0; w < curveLineWidth; w++) { mainCanvas.LineAA(x1, y1 + w, x2, y2 + w, argbCurve); } } //--- Axis label font mainCanvas.FontSet("Arial Bold", labelFontSize); uint argbAxisLabel = ColorToARGB(clrBlack, 255); //--- X axis label string xAxisLabel = "Number of Successes (k)"; mainCanvas.TextOut(currentWidthPixels / 2, currentHeightPixels - 20, xAxisLabel, argbAxisLabel, TA_CENTER); //--- Y axis label (vertical) string yAxisLabel = "Probability / Scaled Frequency"; mainCanvas.FontAngleSet(900); mainCanvas.TextOut(12, currentHeightPixels / 2, yAxisLabel, argbAxisLabel, TA_CENTER); mainCanvas.FontAngleSet(0); }
Определим функцию "drawDistributionPlot" для отображения основной визуализации гистограммы и функции массы вероятности в области графика. Во избежание ошибок вернемся досрочно, если значение "dataLoadedSuccessfully" равно false или если вычисленные размеры графика недействительны. Установим фиксированные поля для области построения графика, скорректируем границы графика с помощью "plotPadding" и вычислим ширину и высоту. Далее определим диапазоны X и Y с минимальными мерами безопасности, равными 1, чтобы предотвратить деление на ноль. После преобразования черного цвета в ARGB для осей мы рисуем утолщенные оси Y и X, используя циклы с помощью метода Line для наглядности.
Для добавления тиков и меток установим шрифт с помощью FontSet и подготовим цвет текста ARGB. Для оси Y мы вычислим оптимальные тики с помощью "calculateOptimalTicks" от 0 до "rangeY". Затем в цикле определим позицию и рисуем каждую линию тика с помощью "Line", форматируем метки с помощью "formatTickLabel" и размещаем их с выравниванием по правому краю с помощью TextOut. Аналогично, для оси X сгенерируем тики от "minDataValue" до "maxDataValue", рисуем тики, направленные вниз и разместим метки по центру под осью.
Для гистограммы преобразуем "histogramColor" в ARGB, вычислим ширину баров с учетом промежутков с помощью функции "histogramGapPixels" и циклом позиционируем и заполняем бары с помощью FillRectangle, масштабированной до "rangeY", и высоту графика, обеспечивая валидность прямоугольников. Для наложения теоретической кривой преобразуем "theoreticalCurveColor" в ARGB и циклом переберем точки, пропорционально сопоставляя координаты X и Y, затем рисуем сглаженные сегменты с помощью функции LineAA в цикле ширины на основании "curveLineWidth", для получения плавных линий. Наконец, выделим шрифт жирным с помощью "FontSet", зададим метки осей в виде строк, отрисуем метку оси X по центру внизу с помощью "TextOut", повернем шрифт на 90 градусов с помощью FontAngleSet для вертикального размещения метки оси Y выровненной по левому краю и сбросим угол на 0. Завершим построение графика описательными заголовками. При вызове функции получим следующий результат.

Теперь, когда график готов, можно добавить панель статистики и условных обозначений.
Панель статистики и условные обозначения
Отобразим все вычисленные числа и объясним цвета.
//+------------------------------------------------------------------+ //| Draw statistics panel | //+------------------------------------------------------------------+ void DrawStatisticsPanel() { //--- Panel coordinates int panelX = statsPanelX; int panelY = HEADER_BAR_HEIGHT + statsPanelY; int panelWidth = statsPanelWidth; int panelHeight = statsPanelHeight; //--- Light background color color panelBgColor = LightenColor(themeColor, 0.9); //--- Semi-transparent alpha uchar bgAlpha = 153; uint argbPanelBg = ColorToARGB(panelBgColor, bgAlpha); uint argbBorder = ColorToARGB(themeColor, 255); uint argbText = ColorToARGB(clrBlack, 255); //--- Fill panel background with blending for (int y = panelY; y <= panelY + panelHeight; y++) { for (int x = panelX; x <= panelX + panelWidth; x++) { BlendPixelSet(mainCanvas, x, y, argbPanelBg); } } //--- Draw top border for (int x = panelX; x <= panelX + panelWidth; x++) { BlendPixelSet(mainCanvas, x, panelY, argbBorder); } //--- Draw right border for (int y = panelY; y <= panelY + panelHeight; y++) { BlendPixelSet(mainCanvas, panelX + panelWidth, y, argbBorder); } //--- Draw left border for (int y = panelY; y <= panelY + panelHeight; y++) { BlendPixelSet(mainCanvas, panelX, y, argbBorder); } //--- Set panel font mainCanvas.FontSet("Arial", panelFontSize); //--- Starting text position int textY = panelY + 6; int lineSpacing = panelFontSize + 1; //--- Display trials string trialsText = StringFormat("Trials (n): %d", numTrials); mainCanvas.TextOut(panelX + 8, textY, trialsText, argbText, TA_LEFT); textY += lineSpacing; //--- Display probability string probText = StringFormat("Prob (p): %.2f", successProbability); mainCanvas.TextOut(panelX + 8, textY, probText, argbText, TA_LEFT); textY += lineSpacing; //--- Display sample size string sampleText = StringFormat("Sample: %d", sampleSize); mainCanvas.TextOut(panelX + 8, textY, sampleText, argbText, TA_LEFT); textY += lineSpacing; //--- Display mean string meanText = StringFormat("Mean: %.2f", sampleMean); mainCanvas.TextOut(panelX + 8, textY, meanText, argbText, TA_LEFT); textY += lineSpacing; //--- Display standard deviation string stdDevText = StringFormat("StdDev: %.2f", sampleStandardDeviation); mainCanvas.TextOut(panelX + 8, textY, stdDevText, argbText, TA_LEFT); textY += lineSpacing; //--- Display skewness string skewText = StringFormat("Skewness: %.3f", sampleSkewness); mainCanvas.TextOut(panelX + 8, textY, skewText, argbText, TA_LEFT); textY += lineSpacing; //--- Display kurtosis string kurtText = StringFormat("Kurtosis: %.3f", sampleKurtosis); mainCanvas.TextOut(panelX + 8, textY, kurtText, argbText, TA_LEFT); textY += lineSpacing; //--- Display Q1 string p25Text = StringFormat("Q1 (25%%): %.1f", percentile25); mainCanvas.TextOut(panelX + 8, textY, p25Text, argbText, TA_LEFT); textY += lineSpacing; //--- Display median string p50Text = StringFormat("Median (50%%): %.1f", percentile50); mainCanvas.TextOut(panelX + 8, textY, p50Text, argbText, TA_LEFT); textY += lineSpacing; //--- Display Q3 string p75Text = StringFormat("Q3 (75%%): %.1f", percentile75); mainCanvas.TextOut(panelX + 8, textY, p75Text, argbText, TA_LEFT); textY += lineSpacing; //--- Display 95% CI string ci95Text = StringFormat("95%% CI: [%.2f, %.2f]", confidenceInterval95Lower, confidenceInterval95Upper); mainCanvas.TextOut(panelX + 8, textY, ci95Text, argbText, TA_LEFT); textY += lineSpacing; //--- Display 99% CI string ci99Text = StringFormat("99%% CI: [%.2f, %.2f]", confidenceInterval99Lower, confidenceInterval99Upper); mainCanvas.TextOut(panelX + 8, textY, ci99Text, argbText, TA_LEFT); } //+------------------------------------------------------------------+ //| Draw legend | //+------------------------------------------------------------------+ void DrawLegend() { //--- Legend coordinates int legendX = statsPanelX; int legendY = HEADER_BAR_HEIGHT + statsPanelY + statsPanelHeight; int legendWidth = statsPanelWidth; int legendHeightThis = legendHeight; //--- Light background color legendBgColor = LightenColor(themeColor, 0.9); uchar bgAlpha = 153; uint argbLegendBg = ColorToARGB(legendBgColor, bgAlpha); uint argbBorder = ColorToARGB(themeColor, 255); uint argbText = ColorToARGB(clrBlack, 255); //--- Fill legend background for (int y = legendY; y <= legendY + legendHeightThis; y++) { for (int x = legendX; x <= legendX + legendWidth; x++) { BlendPixelSet(mainCanvas, x, y, argbLegendBg); } } //--- Top border for (int x = legendX; x <= legendX + legendWidth; x++) { BlendPixelSet(mainCanvas, x, legendY, argbBorder); } //--- Right border for (int y = legendY; y <= legendY + legendHeightThis; y++) { BlendPixelSet(mainCanvas, legendX + legendWidth, y, argbBorder); } //--- Bottom border for (int x = legendX; x <= legendX + legendWidth; x++) { BlendPixelSet(mainCanvas, x, legendY + legendHeightThis, argbBorder); } //--- Left border for (int y = legendY; y <= legendY + legendHeightThis; y++) { BlendPixelSet(mainCanvas, legendX, y, argbBorder); } //--- Set legend font mainCanvas.FontSet("Arial", panelFontSize); //--- Legend item start int itemY = legendY + 10; int lineSpacing = panelFontSize; //--- Histogram sample uint argbHist = ColorToARGB(histogramColor, 255); mainCanvas.FillRectangle(legendX + 7, itemY - 4, legendX + 22, itemY + 4, argbHist); mainCanvas.TextOut(legendX + 27, itemY - 4, "Sample Histogram", argbText, TA_LEFT); itemY += lineSpacing; //--- Theoretical curve sample uint argbCurve = ColorToARGB(theoreticalCurveColor, 255); for (int i = 0; i < 15; i++) { BlendPixelSet(mainCanvas, legendX + 7 + i, itemY, argbCurve); BlendPixelSet(mainCanvas, legendX + 7 + i, itemY + 1, argbCurve); } mainCanvas.TextOut(legendX + 27, itemY - 4, "Theoretical PMF", argbText, TA_LEFT); }
На этом этапе определим функцию "drawStatisticsPanel" для отображения вычисленных статистических данных в полупрозрачной панели. Зададим положение и размер панели из входных признаков, осветлим "themeColor" для фона с помощью "LightenColor". Преобразуем цвета в ARGB, включая частичный альфа-канал для фона, и используем цикл для смешивания пикселей с помощью "blendPixelSet" для заливки и границ, создавая область в рамке. После установки шрифта с помощью FontSet инициализируем положение и интервал текста. Затем форматируем и рисуем каждую строку статистики, используя StringFormat и "TextOut" с выравниванием по левому краю, увеличивая значение по оси Y для вертикального наложения. Охватываем такие параметры, как количество испытаний, вероятность, размер выборки, среднее значение, стандартное отклонение, коэффициент асимметрии, коэффициент эксцесса, квартили и доверительные интервалы.
Далее создадим функцию "drawLegend" для аналогичной панели под статистикой, чтобы предоставлять объяснение элементов графика. Расположим её на одной линии с панелью статистических данных, но со смещением по вертикали. Используем тот же осветленный фон и границы, смешанные в цикле с помощью "blendPixelSet". Установим шрифт. Для элементов мы рисуем небольшой закрашенный прямоугольник с помощью FillRectangle в цветовом пространстве "histogramColor" ARGB в качестве выборки. Добавим к нему метку "Sample Histogram" с помощью "TextOut", затем для кривой смешиваем горизонтальные пиксели в "theoreticalCurveColor", чтобы имитировать отрезок линии, и добавляем метку "Theoretical PMF", что делает обозначения на графике понятными. При условном вызове этих функций получим следующий результат.

После того, как панели статистики и условных обозначений отрисованы, можно перейти к добавлению индикаторов изменения размера в правом и нижнем углах, а также в нижнем правом углу.
Индикаторы изменения размера и вспомогательные элементы взаимодействия
Мы завершили настройку визуальной обратной связи и управления мышью.
//+------------------------------------------------------------------+ //| Draw resize indicator | //+------------------------------------------------------------------+ void DrawResizeIndicator() { //--- Indicator color uint argbIndicator = ColorToARGB(themeColor, 255); //--- Corner grip if (hoverResizeMode == RESIZE_CORNER || activeResizeMode == RESIZE_CORNER) { int cornerX = currentWidthPixels - resizeGripSize; int cornerY = currentHeightPixels - resizeGripSize; //--- Fill corner square mainCanvas.FillRectangle(cornerX, cornerY, currentWidthPixels - 1, currentHeightPixels - 1, argbIndicator); //--- Draw diagonal lines for (int i = 0; i < 3; i++) { int offset = i * 3; mainCanvas.Line(cornerX + offset, currentHeightPixels - 1, currentWidthPixels - 1, cornerY + offset, argbIndicator); } } //--- Right edge grip if (hoverResizeMode == RESIZE_RIGHT_EDGE || activeResizeMode == RESIZE_RIGHT_EDGE) { int indicatorY = currentHeightPixels / 2 - 15; mainCanvas.FillRectangle(currentWidthPixels - 3, indicatorY, currentWidthPixels - 1, indicatorY + 30, argbIndicator); } //--- Bottom edge grip if (hoverResizeMode == RESIZE_BOTTOM_EDGE || activeResizeMode == RESIZE_BOTTOM_EDGE) { int indicatorX = currentWidthPixels / 2 - 15; mainCanvas.FillRectangle(indicatorX, currentHeightPixels - 3, indicatorX + 30, currentHeightPixels - 1, argbIndicator); } }
Определим функцию "drawResizeIndicator", чтобы обеспечить визуальную обратную связь при изменении размера объекта Canvas или при наведении курсора на зоны изменения размера, используя ARGB, преобразованный из "themeColor" с помощью ColorToARGB, для цвета индикатора. Если параметр "hoverResizeMode" или "activeResizeMode" равен "RESIZE_CORNER", мы вычислим координаты углов на основе "resizeGripSize", заполним небольшой квадрат методом FillRectangle и в цикле рисуем три смещенные диагональные линии, используя метод "Line", для создания узора, похожего на захват. Для "RESIZE_RIGHT_EDGE" мы разместим вертикальный индикатор посередине справа и заполним тонкий прямоугольник методом "FillRectangle", чтобы выделить край. Аналогично, для "RESIZE_BOTTOM_EDGE" мы заполним горизонтальный прямоугольник внизу по центру, выделяя подсказки для взаимодействия с пользователем. После компиляции получаем следующий результат.

После завершения настройки индикаторов изменения размера все готово. Осталось только добавить интерактивность, добавив распознаватели событий графика. Сначала определим некоторые вспомогательные функции.
//+------------------------------------------------------------------+ //| Check if mouse is over header | //+------------------------------------------------------------------+ bool IsMouseOverHeaderBar(int mouseX, int mouseY) { //--- Return true if coordinates inside header rectangle return (mouseX >= currentPositionX && mouseX <= currentPositionX + currentWidthPixels && mouseY >= currentPositionY && mouseY <= currentPositionY + HEADER_BAR_HEIGHT); } //+------------------------------------------------------------------+ //| Check if mouse is in resize zone | //+------------------------------------------------------------------+ bool IsMouseInResizeZone(int mouseX, int mouseY, ResizeDirection &resizeMode) { //--- Disabled resizing aborts if (!enableResizing) return false; //--- Mouse relative to canvas int relativeX = mouseX - currentPositionX; int relativeY = mouseY - currentPositionY; //--- Right edge detection bool nearRightEdge = (relativeX >= currentWidthPixels - resizeGripSize && relativeX <= currentWidthPixels && relativeY >= HEADER_BAR_HEIGHT && relativeY <= currentHeightPixels); //--- Bottom edge detection bool nearBottomEdge = (relativeY >= currentHeightPixels - resizeGripSize && relativeY <= currentHeightPixels && relativeX >= 0 && relativeX <= currentWidthPixels); //--- Corner detection bool nearCorner = (relativeX >= currentWidthPixels - resizeGripSize && relativeX <= currentWidthPixels && relativeY >= currentHeightPixels - resizeGripSize && relativeY <= currentHeightPixels); //--- Prioritize corner then edges if (nearCorner) { resizeMode = RESIZE_CORNER; return true; } else if (nearRightEdge) { resizeMode = RESIZE_RIGHT_EDGE; return true; } else if (nearBottomEdge) { resizeMode = RESIZE_BOTTOM_EDGE; return true; } //--- No resize zone resizeMode = NO_RESIZE; return false; } //+------------------------------------------------------------------+ //| Handle canvas resizing | //+------------------------------------------------------------------+ void HandleCanvasResize(int mouseX, int mouseY) { //--- Mouse movement since start int deltaX = mouseX - resizeStartX; int deltaY = mouseY - resizeStartY; //--- Start with current dimensions int newWidth = currentWidthPixels; int newHeight = currentHeightPixels; //--- Apply horizontal resize if needed if (activeResizeMode == RESIZE_RIGHT_EDGE || activeResizeMode == RESIZE_CORNER) { newWidth = MathMax(MIN_CANVAS_WIDTH, resizeInitialWidth + deltaX); } //--- Apply vertical resize if needed if (activeResizeMode == RESIZE_BOTTOM_EDGE || activeResizeMode == RESIZE_CORNER) { newHeight = MathMax(MIN_CANVAS_HEIGHT, resizeInitialHeight + deltaY); } //--- Clamp to chart boundaries int chartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int chartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); newWidth = MathMin(newWidth, chartWidth - currentPositionX - 10); newHeight = MathMin(newHeight, chartHeight - currentPositionY - 10); //--- Update only if changed if (newWidth != currentWidthPixels || newHeight != currentHeightPixels) { currentWidthPixels = newWidth; currentHeightPixels = newHeight; //--- Resize canvas object mainCanvas.Resize(currentWidthPixels, currentHeightPixels); ObjectSetInteger(0, canvasObjectName, OBJPROP_XSIZE, currentWidthPixels); ObjectSetInteger(0, canvasObjectName, OBJPROP_YSIZE, currentHeightPixels); //--- Re-render everything RenderVisualization(); ChartRedraw(); } } //+------------------------------------------------------------------+ //| Handle canvas dragging | //+------------------------------------------------------------------+ void HandleCanvasDrag(int mouseX, int mouseY) { //--- Mouse movement since start int deltaX = mouseX - dragStartX; int deltaY = mouseY - dragStartY; //--- New position int newX = canvasStartX + deltaX; int newY = canvasStartY + deltaY; //--- Get chart dimensions for clamping int chartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int chartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Keep canvas fully visible newX = MathMax(0, MathMin(chartWidth - currentWidthPixels, newX)); newY = MathMax(0, MathMin(chartHeight - currentHeightPixels, newY)); //--- Apply new position currentPositionX = newX; currentPositionY = newY; //--- Update object properties ObjectSetInteger(0, canvasObjectName, OBJPROP_XDISTANCE, currentPositionX); ObjectSetInteger(0, canvasObjectName, OBJPROP_YDISTANCE, currentPositionY); //--- Redraw chart ChartRedraw(); } //+------------------------------------------------------------------+ //| Interpolate between two colors | //+------------------------------------------------------------------+ color InterpolateColors(color startColor, color endColor, double factor) { //--- Extract start components uchar r1 = (uchar)((startColor >> 16) & 0xFF); uchar g1 = (uchar)((startColor >> 8) & 0xFF); uchar b1 = (uchar)(startColor & 0xFF); //--- Extract end components uchar r2 = (uchar)((endColor >> 16) & 0xFF); uchar g2 = (uchar)((endColor >> 8) & 0xFF); uchar b2 = (uchar)(endColor & 0xFF); //--- Linear interpolation for each channel uchar r = (uchar)(r1 + factor * (r2 - r1)); uchar g = (uchar)(g1 + factor * (g2 - g1)); uchar b = (uchar)(b1 + factor * (b2 - b1)); //--- Return interpolated color return (r << 16) | (g << 8) | b; } //+------------------------------------------------------------------+ //| Blend pixel with proper alpha blending | //+------------------------------------------------------------------+ void BlendPixelSet(CCanvas &canvas, int x, int y, uint src) { //--- Skip out-of-bounds pixels if (x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return; //--- Get destination pixel uint dst = canvas.PixelGet(x, y); //--- Source alpha, RGB (0-1) double sa = ((src >> 24) & 0xFF) / 255.0; double sr = ((src >> 16) & 0xFF) / 255.0; double sg = ((src >> 8) & 0xFF) / 255.0; double sb = (src & 0xFF) / 255.0; //--- Destination alpha, RGB (0-1) double da = ((dst >> 24) & 0xFF) / 255.0; double dr = ((dst >> 16) & 0xFF) / 255.0; double dg = ((dst >> 8) & 0xFF) / 255.0; double db = (dst & 0xFF) / 255.0; //--- Output alpha double out_a = sa + da * (1 - sa); //--- Fully transparent result if (out_a == 0) { canvas.PixelSet(x, y, 0); return; } //--- Premultiplied output RGB double out_r = (sr * sa + dr * da * (1 - sa)) / out_a; double out_g = (sg * sa + dg * da * (1 - sa)) / out_a; double out_b = (sb * sa + db * da * (1 - sa)) / out_a; //--- Convert back to 0-255 with rounding uchar oa = (uchar)(out_a * 255 + 0.5); uchar or_ = (uchar)(out_r * 255 + 0.5); uchar og = (uchar)(out_g * 255 + 0.5); uchar ob = (uchar)(out_b * 255 + 0.5); //--- Assemble final ARGB uint out_col = ((uint)oa << 24) | ((uint)or_ << 16) | ((uint)og << 8) | (uint)ob; //--- Write blended pixel canvas.PixelSet(x, y, out_col); }
Сначала определим функцию "isMouseOverHeaderBar", которая определяет, находится ли курсор мыши над областью заголовка, возвращает значение true, если координаты попадают в диапазон от "currentPositionX" до width и от "currentPositionY" до "HEADER_BAR_HEIGHT", что позволяет проверять начало перетаскивания. Далее создадим функцию "isMouseInResizeZone", которая определяет области изменения размера, если "enableResizing" равно true, вычисляет относительные положения, проверяет близость к правому краю, нижнему краю или углу на основе "resizeGripSize", устанавливает ссылку "resizeMode" на соответствующее значение перечисления, например "RESIZE_CORNER", и возвращает значение true, если находится в зоне, или false, если "NO_RESIZE".
Для управления изменением размера мы реализуем функцию "handleCanvasResize", вычислим дельты от начальных точек, обновим новые размеры на основе "activeResizeMode" с использованием MathMax для минимальных значений, ограничиваясь размером графика за вычетом полей с помощью ChartGetInteger для CHART_WIDTH_IN_PIXELS и "CHART_HEIGHT_IN_PIXELS" плюс "MathMin". Затем, если размер изменился, присвоим значение глобальным переменным, изменим размер объекта Canvas с помощью Resize, установим свойства объекта с помощью ObjectSetInteger для OBJPROP_XSIZE и "OBJPROP_YSIZE", перерисуем визуализацию с помощью "renderVisualization" и перерисуем график.
Для перетаскивания мы определим функцию "handleCanvasDrag", вычислим дельты, предложим новые позиции, ограничим их в пределах границ графика с помощью "MathMax" и "MathMin". Обновим "currentPositionX" и "currentPositionY", установим расстояния между объектами с помощью "ObjectSetInteger" для "OBJPROP_XDISTANCE" и "OBJPROP_YDISTANCE" и обновим данные с помощью функции ChartRedraw. Добавим функцию "InterpolateColors" для смешивания начального и конечного цветов с коэффициентом, извлечем компоненты RGB с помощью битовых операций, линейно интерполируя каждый из них и повторно объединяя их с помощью сдвигов для плавных градиентов. Наконец, создадим функцию "blendPixelSet" для альфа-смешивания по определенным координатам, проверки границ по ширине и высоте. Извлечем компоненты источника и назначения, вычислим значения смешивания с помощью альфа-формул, обработаем полную прозрачность и настроим результат с помощью PixelSet для включения наложенных полупрозрачных элементов, таких как панели. Теперь при необходимости мы можем вызывать эти функции в обработчике событий графика.
//+------------------------------------------------------------------+ //| Chart event handler | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Only process mouse move events if (id == CHARTEVENT_MOUSE_MOVE) { //--- Extract mouse coordinates and button state int mouseX = (int)lparam; int mouseY = (int)dparam; int mouseState = (int)sparam; //--- Store previous hover states for redraw decision bool previousHoverState = isHoveringCanvas; bool previousHeaderHoverState = isHoveringHeader; bool previousResizeHoverState = isHoveringResizeZone; //--- Update canvas hover flag isHoveringCanvas = (mouseX >= currentPositionX && mouseX <= currentPositionX + currentWidthPixels && mouseY >= currentPositionY && mouseY <= currentPositionY + currentHeightPixels); //--- Update header hover isHoveringHeader = IsMouseOverHeaderBar(mouseX, mouseY); //--- Update resize hover and mode isHoveringResizeZone = IsMouseInResizeZone(mouseX, mouseY, hoverResizeMode); //--- Redraw needed if any hover state changed bool needRedraw = (previousHoverState != isHoveringCanvas || previousHeaderHoverState != isHoveringHeader || previousResizeHoverState != isHoveringResizeZone); //--- Mouse button just pressed if (mouseState == 1 && previousMouseButtonState == 0) { //--- Start dragging if conditions met if (enableDragging && isHoveringHeader && !isHoveringResizeZone) { isDraggingCanvas = true; dragStartX = mouseX; dragStartY = mouseY; canvasStartX = currentPositionX; canvasStartY = currentPositionY; ChartSetInteger(0, CHART_MOUSE_SCROLL, false); needRedraw = true; } //--- Start resizing if over grip else if (isHoveringResizeZone) { isResizingCanvas = true; activeResizeMode = hoverResizeMode; resizeStartX = mouseX; resizeStartY = mouseY; resizeInitialWidth = currentWidthPixels; resizeInitialHeight = currentHeightPixels; ChartSetInteger(0, CHART_MOUSE_SCROLL, false); needRedraw = true; } } //--- Mouse button still held else if (mouseState == 1 && previousMouseButtonState == 1) { //--- Continue drag if (isDraggingCanvas) { HandleCanvasDrag(mouseX, mouseY); } //--- Continue resize else if (isResizingCanvas) { HandleCanvasResize(mouseX, mouseY); } } //--- Mouse button just released else if (mouseState == 0 && previousMouseButtonState == 1) { //--- End any active interaction if (isDraggingCanvas || isResizingCanvas) { isDraggingCanvas = false; isResizingCanvas = false; activeResizeMode = NO_RESIZE; ChartSetInteger(0, CHART_MOUSE_SCROLL, true); needRedraw = true; } } //--- Redraw if state changed if (needRedraw) { RenderVisualization(); ChartRedraw(); } //--- Update last mouse position and button state lastMouseX = mouseX; lastMouseY = mouseY; previousMouseButtonState = mouseState; } }
Определим обработчик OnChartEvent для обработки взаимодействий с графиком, в частности, для обработки перемещений мыши, когда идентификатор совпадает с CHARTEVENT_MOUSE_MOVE. Преобразуем параметры для извлечения координат и состояния мыши, сохраним предыдущие флаги наведения курсора, обновим "isHoveringCanvas", проверяя, находится ли позиция в пределах границ объекта Canvas, установим "isHoveringHeader" с помощью "isMouseOverHeaderBar" и определим "isHoveringResizeZone" с помощью "isMouseInResizeZone", передадим контрольную отметку режима. Флаг перерисовки устанавливается, если изменяется какое-либо состояние наведения мыши.
Для нажатий кнопки мыши (состояние 1 из 0), если "enableDragging" равно true и наведение происходит на заголовок без зоны изменения размера, активируем перетаскивание, установив "isDraggingCanvas", запишем начальные точки, отключим прокрутку графика с помощью ChartSetInteger для "CHART_MOUSE_SCROLL" и установим флаг перерисовки. Если объект находится в зоне изменения размера, включим "isResizingCanvas", назначим "activeResizeMode", получим начальные значения, отключим прокрутку и установим режим перерисовки. Пока кнопка удерживается (состояние 1 сохраняется), вызовем "handleCanvasDrag" при перетаскивании или "handleCanvasResize" при изменении размера для динамической корректировки положения или размеров. После отпускания (состояние 0 из 1) если какой-либо из параметров активен, сбрасываем флаги и режим в "NO_RESIZE", повторно включаем прокрутку и перерисовку флага. Если требуется перерисовка, вызовем функции "renderVisualization" и "ChartRedraw" для обновления данных. Наконец, для обеспечения непрерывности обновим последние положения курсора мыши и состояние кнопок. Это позволит обработать события графика, но сначала необходимо разрешить перемещение мыши по графику во время инициализации. Вот полный фрагмент кода инициализации, с выделенной для наглядности логикой работы мыши.
Инициализация советника
//+------------------------------------------------------------------+ //| Initialize the expert | //+------------------------------------------------------------------+ int OnInit() { //--- Copy initial X position from inputs currentPositionX = initialCanvasX; //--- Copy initial Y position from inputs currentPositionY = initialCanvasY; //--- Copy initial width from inputs currentWidthPixels = initialCanvasWidth; //--- Copy initial height from inputs currentHeightPixels = initialCanvasHeight; //--- Create canvas object if (!CreateCanvas()) { //--- Log creation failure Print("ERROR: Failed to create distribution canvas"); //--- Fail initialization return(INIT_FAILED); } //--- Load initial distribution data if (!LoadDistributionData()) { //--- Log data load failure Print("ERROR: Failed to load distribution data"); //--- Fail initialization return(INIT_FAILED); } //--- Render first visualization RenderVisualization(); //--- Activate mouse move events ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); //--- Force chart redraw ChartRedraw(); //--- Successful initialization return(INIT_SUCCEEDED); }
Нам также потребуется удалять объекты с нашего графика, когда программа не нужна, и запускать программу для каждого бара для имитационного анализа.
Очистка и обновления в реальном времени
//+------------------------------------------------------------------+ //| Deinitialize the expert | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Destroy canvas and free resources mainCanvas.Destroy(); //--- Force chart redraw to clean up ChartRedraw(); } //+------------------------------------------------------------------+ //| Process new tick | //+------------------------------------------------------------------+ void OnTick() { //--- Remember last processed bar time static datetime lastBarTimestamp = 0; //--- Get time of newest bar on chosen timeframe datetime currentBarTimestamp = iTime(_Symbol, chartTimeframe, 0); //--- New bar detected if (currentBarTimestamp > lastBarTimestamp) { //--- Reload fresh data if (LoadDistributionData()) { //--- Update visualization RenderVisualization(); //--- Redraw chart ChartRedraw(); } //--- Store new bar time lastBarTimestamp = currentBarTimestamp; } }
В обработчике OnDeinit вызовем метод Destroy на "mainCanvas", чтобы освободить графическую метку и связанную с ней память, а затем обновим график, чтобы удалить все визуальные остатки. Далее, в обработчике OnTick используем статическую переменную "lastBarTimestamp" для отслеживания времени открытия предыдущего бара и получаем время открытия текущего бара с помощью iTime для символа _Symbol и таймфрейма "chartTimeframe" и нулевой сдвиг. Если сформировался новый бар (текущая временая метка больше предыдущей), мы перезагрузим данные с помощью "loadDistributionData", повторно отобразим с помощью "renderVisualization" в случае успеха. Перерисуем график и обновим временную метку , чтобы дождаться следующего бара, обеспечивая динамические обновления, привязанные к изменениям рынка. На этом построение графика биномиального распределения завершено. Теперь остаётся проверить работоспособность системы, что и рассматривается в следующем разделе.
Тестирование на истории
Мы провели тестирование, а ниже показан итоговый результат в формате Graphics Interchange Format (GIF).

Мы протестировали программу на нескольких наборах параметров. В одном из прогонов с 40 испытаниями и вероятностью успеха 75% гистограмма достигла пика точно на ожидаемом значении в 30 успешных сделок, в моделировании ценовой динамики 10 000 случайных сессий 95%-й доверительный интервал корректно охватил 94,8% наблюдений, а перетаскивание/изменение размера в реальном времени работало плавно даже на всех графиках без лагов. Обновление в реальном времени для новых баров обновляло всю визуализацию менее чем за 40 миллисекунд, подтверждая, что инструмент достаточно быстр для использования.
Заключение
В заключение отметим, что мы создали графический инструмент на MQL5 для визуализации биномиального распределения с помощью гистограммы моделируемых выборок и кривой функции массы вероятности на интерактивном объекте Canvas, добавили расширенную статистику, включая среднее значение, стандартное отклонение, коэффициент асимметрии, коэффициент эксцесса, процентили и доверительные интервалы, а также настраиваемые темы, градиенты и метки. Включили возможность перетаскивания, изменения размера, обновления в реальном времени и настройки параметров для испытаний, вероятности, размера выборки и отображения для поддержки анализа торговли. После ознакомления со статьей вы сможете:
- Мгновенно визуализировать вероятность любого количества выигрышных сделок для вашей стратегии и соответствующим образом корректировать размер позиции
- Считывать коэффициент асимметрии и доверительные интервалы непосредственно на графике, чтобы выявлять скрытые риски до масштабирования.
- Перетаскивать инструмент на любой график и изменять его размер, чтобы сравнить несколько сценариев друг с другом во время торговой сессии.
В последующих частях мы улучшим этот инструмент, добавив дополнительные функции распределения, но сначала рассмотрим, как можно преобразовать двумерную гистограмму в трехмерную с помощью библиотеки MQL5 DirectX. Следите за обновлениями!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21315
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
CRUD-операции в Firebase с использованием MQL
От начального до среднего уровня: Объекты (IV)
MetaTrader и Google Таблицы через PythonAnywhere: Руководство по безопасному потоку данных
Переосмысливаем классические стратегии (Часть 21): Разработка комбинированной стратегии на основе полос Боллинджера и RSI
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования