English
preview
Торговые инструменты MQL5 (Часть 22): Построение гистограммы и функции вероятностной массы (PMF) биномиального распределения

Торговые инструменты MQL5 (Часть 22): Построение гистограммы и функции вероятностной массы (PMF) биномиального распределения

MetaTrader 5Трейдинг |
100 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

У вас есть торговая система, которая приносит череду прибыльных и убыточных сделок, но вы не можете ответить на простые, но крайне важные вопросы: "Какова вероятность того, что из 30 сделок выигрышными будут 20, если мой исторический винрейт (доля выигрышных сделок) составляет 75%?" или "Насколько вероятна серия из 5 убыточных сделок?" Без возможности смоделировать биномиальное распределение вам остается только гадать – вы не можете реалистично оценить риск, установить соответствующие размеры позиций или проверить, является ли эффективность вашей стратегии статистически значимой. Недостаток понимания вероятностной природы результатов часто приводит к чрезмерному использованию кредитного плеча, эмоциональным решениям и нереалистичным ожиданиям прибыли. Эта статья написана для разработчиков MetaQuotes Language 5 (MQL5) и алгоритмических трейдеров, стремящихся количественно оценить вероятностное поведение своих торговых стратегий.

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

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

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


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

Биномиальное распределение моделирует число успешных исходов в фиксированном количестве независимых испытаний, каждое из которых имеет одинаковую вероятность успеха. В торговле "испытание" может представлять собой любое бинарное событие: сделка заканчивается прибылью (успехом) или убытком (неудачей), сигнал действителен или недействителен, или рыночное условие выполняется или нет. Если исторический винрейт вашей стратегии равен "p", биномиальное распределение покажет вам, какова вероятность получения "k" выигрышных сделок из "n" сделок чисто случайно. Вот как можно это использовать на рынке:

  • Оценка вероятности достижения профит-таргета: Предположим, ваша система имеет выигрышные сделки в 60% случаев. Если вы планируете открывать 20 сделок в месяц, вы можете рассчитать вероятность того, что у вас будет как минимум 12 выигрышных сделок – это ценная проверка на практике.
  • Установка реалистичных ожиданий просадки: Распределение помогает оценить максимально вероятную длину серии убыточных сделок. Если вероятность пяти последовательных убыточных сделок очень мала, такая серия может указывать на сбой стратегии.
  • Сравнение стратегий: Две системы могут иметь одинаковые винрейты, но та, которая имеет более узкое распределение (то есть меньшую дисперсию), более надежна – это видно на гистограмме и стандартном отклонении.
  • Проверка размера выборки: Доверительные интервалы показывают, насколько точен ваш расчетный винрейт. Большие интервалы означают, что вам нужно больше сделок, чтобы быть уверенным в истинной эффективности системы.

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

BINOMIAL DISTRIBUTION PLOT FRAMEWORK

Торговое значение признаков

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

  • Гистограмма (эмпирические частоты): Показывает, сколько раз каждое число успешных сделок имело место в вашей смоделированной выборке. Высокий бар на определенном уровне "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 для указания успешной инициализации. Вот что мы получаем после компиляции.

INITIALIZATION

Поскольку все инициализации и вычисления завершены, давайте теперь построим график анализа. Сначала отрисуем объект 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 HEADER WITH BORDERS

После того, как заголовок и границы объекта 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. Завершим построение графика описательными заголовками. При вызове функции получим следующий результат.

BAR AND LINE GRAPH

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

Панель статистики и условные обозначения

Отобразим все вычисленные числа и объясним цвета.

//+------------------------------------------------------------------+
//| 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", что делает обозначения на графике понятными. При условном вызове этих функций получим следующий результат.

STATISTICS AND LEGEND PANELS

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

Индикаторы изменения размера и вспомогательные элементы взаимодействия

Мы завершили настройку визуальной обратной связи и управления мышью.

//+------------------------------------------------------------------+
//| 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" мы заполним горизонтальный прямоугольник внизу по центру, выделяя подсказки для взаимодействия с пользователем. После компиляции получаем следующий результат.

RESIZE INDICATORS

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

//+------------------------------------------------------------------+
//| 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).

BACKTEST GIF

Мы протестировали программу на нескольких наборах параметров. В одном из прогонов с 40 испытаниями и вероятностью успеха 75% гистограмма достигла пика точно на ожидаемом значении в 30 успешных сделок, в моделировании ценовой динамики 10 000 случайных сессий 95%-й доверительный интервал корректно охватил 94,8% наблюдений, а перетаскивание/изменение размера в реальном времени работало плавно даже на всех графиках без лагов. Обновление в реальном времени для новых баров обновляло всю визуализацию менее чем за 40 миллисекунд, подтверждая, что инструмент достаточно быстр для использования.


Заключение

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

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

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

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

Прикрепленные файлы |
CRUD-операции в Firebase с использованием MQL CRUD-операции в Firebase с использованием MQL
В этой статье представлено пошаговое руководство по освоению CRUD-операций (Create, Read, Update, Delete — создание, чтение, обновление и удаление) в Firebase с акцентом на Realtime Database и Firestore. Вы узнаете, как использовать методы Firebase SDK для эффективного управления данными в веб- и мобильных приложениях: от добавления новых записей до запросов, изменения и удаления элементов. Также рассмотрены практические примеры кода и лучшие подходы к структурированию и обработке данных в реальном времени, что помогает разработчикам создавать динамические и масштабируемые приложения на гибкой NoSQL-архитектуре Firebase.
От начального до среднего уровня: Объекты (IV) От начального до среднего уровня: Объекты (IV)
Пожалуй, это самая веселая статья на данный момент. Так происходит, потому что здесь мы реализуем модификацию объекта, присутствующего в MetaTrader 5, чтобы создать другой, изначально отсутствующий на платформе. Конечно, то, что мы здесь рассмотрим, может показаться безумием, но это работает и имеет очень интересное цель.
MetaTrader и Google Таблицы через PythonAnywhere: Руководство по безопасному потоку данных MetaTrader и Google Таблицы через PythonAnywhere: Руководство по безопасному потоку данных
В этой статье показан безопасный способ экспорта данных MetaTrader в Google Таблицы. Google Таблицы — очень ценное решение, поскольку оно работает в облаке, а сохраненные там данные доступны в любое время и из любого места. Поэтому трейдеры могут получать доступ к торговым и связанным с торговлей данным, экспортированным в Google Таблицы, и выполнять дальнейший анализ для будущей торговли в любое время, где бы они ни находились.
Переосмысливаем классические стратегии (Часть 21): Разработка комбинированной стратегии на основе полос Боллинджера и RSI Переосмысливаем классические стратегии (Часть 21): Разработка комбинированной стратегии на основе полос Боллинджера и RSI
В этой статье рассматривается разработка комбинированной алгоритмической торговой стратегии для рынка EURUSD. Эта стратегия сочетает в себе полосы Боллинджера и индикатор относительной силы (RSI). Исходные стратегии, основанные на правилах, давали высококачественные сигналы, но страдали от низкой частоты сделок и ограниченной прибыльностью. Мы проанализировали несколько итераций стратегии, выявив недостатки в нашем понимании рынка, повышенный уровень шума и пониженную эффективность работы стратегии. Благодаря надлежащему использованию алгоритмов статистического обучения, переносу цели моделирования на технические индикаторы, правильному масштабированию и сочетанию прогнозов машинного обучения с классическими правилами торговли, конечная стратегия позволила значительно повысить прибыльность и частоту сделок при сохранении приемлемого качества сигнала.