English
preview
Торговые инструменты MQL5 (Часть 23): Трёхмерные графики с управляемой камерой и поддержкой DirectX для анализа распределений

Торговые инструменты MQL5 (Часть 23): Трёхмерные графики с управляемой камерой и поддержкой DirectX для анализа распределений

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

Введение

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

В предыдущей статье (Часть 22) мы создали инструмент построения графиков на MQL5 для визуализации биномиального распределения с помощью гистограммы смоделированных выборок и теоретической кривой функции массы вероятности на интерактивном объекте Canvas. В Части 23 мы интегрируем Direct3D в инструмент просмотра биномиального распределения в MQL5, позволяющий переключать режимы 2D/3D и управлять камерой для поворота, масштабирования и автоподбора положения камеры. В статье показано, как визуализировать 3D-столбцы гистограммы с помощью опорной плоскости и осей, проецировать кривую PMF и сохранять 2D-статистику, легенду и оформление. В ней также описывается архитектура на основе классов, взаимодействие с мышью, обновления в реальном времени и настройка параметров для улучшения контроля частот и формы PMF. В статье рассмотрим следующие темы:

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

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


Знакомство с архитектурой платформы 3D-визуализации DirectX

Платформа визуализации DirectX 3D на MQL5 использует графику с аппаратным ускорением для отображения сложных 3D-сцен на графиках, интегрируясь с системой canvas для плавного переключения режимов 2D/3D и интерактивного управления. Она использует DirectX для эффективного рендеринга 3D-объектов, таких как параллелепипеды для столбцов гистограммы, плоскости для опорных плоскостей и линии для осей, одновременно управляя положением камеры, освещением и проекциями для создания глубины и перспективы при отображении данных. Эта архитектура поддерживает динамические взаимодействия с пользователем, такие как поворот, масштабирование и автоподбор положения камеры, что делает ее идеальной для изучения многомерных данных, таких как распределения, в торговых контекстах, где визуальная глубина подчеркивает паттерны, невидимые в 2D.

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

DIRECTX 3D ARCHITECTURE GIF


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

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

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

//+------------------------------------------------------------------+
//|    Canvas Graphing PART 3.1 - Statistical Distributions (3D).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 <Canvas\Canvas3D.mqh>
#include <Canvas\DX\DXBox.mqh>
#include <Math\Stat\Binomial.mqh>
#include <Math\Stat\Math.mqh>

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ResizeDirection {
   NO_RESIZE,                                           // No resize
   RESIZE_BOTTOM_EDGE,                                  // Resize bottom edge
   RESIZE_RIGHT_EDGE,                                   // Resize right edge
   RESIZE_CORNER                                        // Resize corner
};

enum ViewModeType {
   VIEW_2D_MODE,                                        // 2D mode
   VIEW_3D_MODE                                         // 3D mode
};

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
sinput group "=== VIEW MODE SETTINGS ==="
input ViewModeType     viewMode = VIEW_2D_MODE;         // View Mode (2D or 3D)

sinput group "=== 3D VIEW SETTINGS ==="
input bool               autoFitCamera = true;          // Auto-Fit Camera on Load
input double             initialCameraDistance = 60.0;  // Initial Camera Distance (3D)
input double             initialCameraAngleX = 0.6;     // Initial Camera Angle X (3D)
input double             initialCameraAngleY = 0.8;     // Initial Camera Angle Y (3D)

sinput group "=== 3D GROUND AND AXES SETTINGS ==="
input color              groundPlaneColor = clrLightGray; // Ground Plane Color
input double             groundPlaneOpacity = 0.6;      // Ground Plane Opacity (0-1)
input float              groundPlaneWidth = 50.0;       // Ground Plane Width (X direction)
input float              groundPlaneDepth = 20.0;       // Ground Plane Depth (Z direction)
input float              groundPlaneThickness = 0.5;    // Ground Plane Thickness (Y direction)
input bool               show3DAxes = true;             // Show 3D Axes
input color              axisXColor = clrRed;           // X Axis Color
input color              axisYColor = clrGreen;         // Y Axis Color
input color              axisZColor = clrBlue;          // Z Axis Color
input float              axisLength = 20.0;             // Axis Length
input float              axisThickness = 0.1f;          // Axis Thickness

//+------------------------------------------------------------------+
//| Constants                                                        |
//+------------------------------------------------------------------+
const int MIN_CANVAS_WIDTH = 300;
const int MIN_CANVAS_HEIGHT = 200;
const int HEADER_BAR_HEIGHT = 35;
const int SWITCH_ICON_SIZE = 24;
const int SWITCH_ICON_MARGIN = 6;

Начнем реализацию с включения дополнительных необходимых библиотек: "#include <Canvas\Canvas3D.mqh>" для включения возможностей 3D-рендеринга и "#include <Canvas\DX\DXBox.mqh>" для примитивов DirectX box, используемых в 3D-моделях. Далее добавим перечисление для просмотров пользователем. Перечисление "ViewModeType" имеет "VIEW_2D_MODE" и "VIEW_3D_MODE" для переключения типов отображения. Затем настроим дополнительные входные параметры, сгруппированные для удобства организации, начиная с выбора режима просмотра с помощью "viewMode", по умолчанию установленного на "VIEW_2D_MODE". Для настроек, специфичных для 3D, мы предоставляем такие входные параметры, как "autoFitCamera" для автоматического позиционирования камеры, "initialCameraDistance", "initialCameraAngleX" и "initialCameraAngleY" для настройки начальных ракурсов камеры.

Кроме того, добавим входные параметры для 3D-поверхности и осей: "groundPlaneColor", "groundPlaneOpacity", размеры, такие как "groundPlaneWidth", "groundPlaneDepth" и "groundPlaneThickness", переключатель "show3DAxes", цвета осей, такие как "axisXColor", а также длины/толщины с помощью "axisLength" и "axisThickness". Это позволяет настраивать 3D-среду. Наконец, объявим дополнительные константы, связанные с иконками, такие как "SWITCH_ICON_SIZE" и "SWITCH_ICON_MARGIN" для кнопки переключения режимов. Для большей ясности мы выделили конкретные изменения. Далее мы преобразуем все наши глобальные переменные и функции в класс, чтобы упростить управление и сделать код модульным. Начнем с глобальных переменных, которые преобразуем в переменные-члены класса.

Определяем класс визуализатора и его переменные-члены

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

//+------------------------------------------------------------------+
//| Distribution visualization window class                          |
//+------------------------------------------------------------------+
class DistributionVisualizer
  {
protected:
   CCanvas3D         m_mainCanvas;           // Main 3D-capable canvas
   string            m_canvasObjectName;     // Chart object name for the canvas bitmap

   int               m_currentPositionX;     // Current X position of the canvas on chart
   int               m_currentPositionY;     // Current Y position of the canvas on chart
   int               m_currentWidth;         // Current canvas width in pixels
   int               m_currentHeight;        // Current canvas height in pixels

   bool              m_isDragging;           // True while user is dragging the canvas
   bool              m_isResizing;           // True while user is resizing the canvas
   int               m_dragStartX;           // Mouse X when drag began
   int               m_dragStartY;           // Mouse Y when drag began
   int               m_canvasStartX;         // Canvas X when drag began
   int               m_canvasStartY;         // Canvas Y when drag began
   int               m_resizeStartX;         // Mouse X when resize began
   int               m_resizeStartY;         // Mouse Y when resize began
   int               m_resizeInitialWidth;   // Canvas width at resize start
   int               m_resizeInitialHeight;  // Canvas height at resize start
   ResizeDirection   m_activeResizeMode;     // Currently active resize direction
   ResizeDirection   m_hoverResizeMode;      // Resize direction under cursor hover

   bool              m_isHoveringCanvas;      // True when mouse is over the canvas
   bool              m_isHoveringHeader;      // True when mouse is over the header bar
   bool              m_isHoveringResizeZone;  // True when mouse is in a resize grip zone
   bool              m_isHoveringSwitchIcon;  // True when mouse is over the mode switch icon
   int               m_lastMouseX;            // Last recorded mouse X coordinate
   int               m_lastMouseY;            // Last recorded mouse Y coordinate
   int               m_previousMouseButtonState; // Mouse button state on previous event

   ViewModeType      m_currentViewMode;      // Active view mode (2D or 3D)
   bool              m_are3DObjectsCreated;  // True once 3D scene objects have been built

   CDXBox            m_histogramBars[];      // Array of 3D boxes representing histogram bars
   CDXBox            m_groundPlane;          // 3D box used as the ground reference plane
   CDXBox            m_axisX;                // 3D box representing the X axis
   CDXBox            m_axisY;                // 3D box representing the Y axis
   CDXBox            m_axisZ;                // 3D box representing the Z axis

   double            m_cameraDistance;       // Distance from camera to scene origin
   double            m_cameraAngleX;         // Camera elevation angle (radians)
   double            m_cameraAngleY;         // Camera azimuth angle (radians)
   int               m_mouse3DStartX;        // Mouse X when 3D rotation drag began
   int               m_mouse3DStartY;        // Mouse Y when 3D rotation drag began
   bool              m_isRotating3D;         // True while user is rotating the 3D scene

   double            m_sampleData[];                   // Raw binomial sample values
   double            m_histogramIntervals[];           // Histogram bin center positions
   double            m_histogramFrequencies[];         // Scaled frequency per histogram bin
   double            m_theoreticalXValues[];           // X values for the theoretical PMF curve
   double            m_theoreticalYValues[];           // Y values for the theoretical PMF curve
   double            m_minDataValue;                   // Minimum value in data range
   double            m_maxDataValue;                   // Maximum value in data range
   double            m_maxFrequency;                   // Peak raw frequency across all bins
   double            m_maxTheoreticalValue;            // Peak theoretical PMF value
   bool              m_isDataLoaded;                   // True once distribution data is ready

   double            m_sampleMean;                     // Computed sample mean
   double            m_sampleStandardDeviation;        // Computed sample standard deviation
   double            m_sampleSkewness;                 // Computed sample skewness
   double            m_sampleKurtosis;                 // Computed sample excess kurtosis
   double            m_percentile25;                   // 25th percentile (Q1)
   double            m_percentile50;                   // 50th percentile (median)
   double            m_percentile75;                   // 75th percentile (Q3)
   double            m_confidenceInterval95Lower;      // Lower bound of 95% confidence interval
   double            m_confidenceInterval95Upper;      // Upper bound of 95% confidence interval
   double            m_confidenceInterval99Lower;      // Lower bound of 99% confidence interval
   double            m_confidenceInterval99Upper;      // Upper bound of 99% confidence interval
};

На этом этапе определим класс "DistributionVisualizer", который инкапсулирует всю логику графического инструмента и служит в качестве менеджера окон как для 2D, так и для 3D визуализации биномиального распределения. В защищенном разделе объявим члены, начиная с объекта CCanvas3D "m_mainCanvas" для 3D-рендеринга, строки "m_canvasObjectName" для идентификатора объекта, целых чисел для текущей позиции и размеров, таких как "m_currentPositionX" и "m_currentWidth", логических значений и целых чисел для состояний взаимодействия, таких как "m_isDragging", "m_dragStartX" и "m_activeResizeMode", используя перечисление "ResizeDirection". Включим связанные с отображением переменные, такие как "m_currentViewMode" из "ViewModeType" и "m_are3DObjectsCreated", массивы "CDXBox" для "m_histogramBars", а также отдельные поля для "m_groundPlane", "m_axisX", "m_axisY", "m_axisZ" для моделирования 3D-элементов.

Для управления камерой мы используем значения типа double, такие как "m_cameraDistance", "m_cameraAngleX", "m_cameraAngleY", а также трекеры взаимодействия "m_mouse3DStartX" и "m_isRotating3D". Хранилище данных включает массивы для выборок, гистограмм и теоретических значений, таких как "m_sampleData" и "m_histogramFrequencies", трекеры min/max, флаг загрузки "m_isDataLoaded" и статистические глобальные переменные, такие как "m_sampleMean" и "m_confidenceInterval95Lower". Мы не будем ссылаться на каждую переменную-член, поскольку большинство из них идентичны предыдущим версиям. Для большей ясности мы добавили подробные комментарии. После этого создадим публичный конструктор для инициализации наших переменных следующим образом.

Инициализация и уничтожение экземпляра класса

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

public:
   //+------------------------------------------------------------------+
   //| Initialize all member variables to safe defaults                 |
   //+------------------------------------------------------------------+
   DistributionVisualizer(void)
     {
      //--- Set canvas object name
      m_canvasObjectName = "DistCanvas";
      //--- Set initial canvas X position from input
      m_currentPositionX = initialCanvasX;
      //--- Set initial canvas Y position from input
      m_currentPositionY = initialCanvasY;
      //--- Set initial canvas width from input
      m_currentWidth = initialCanvasWidth;
      //--- Set initial canvas height from input
      m_currentHeight = initialCanvasHeight;

      //--- Reset drag state
      m_isDragging = false;
      //--- Reset resize state
      m_isResizing = false;
      //--- Reset drag origin X
      m_dragStartX = 0;
      //--- Reset drag origin Y
      m_dragStartY = 0;
      //--- Reset canvas position snapshot X
      m_canvasStartX = 0;
      //--- Reset canvas position snapshot Y
      m_canvasStartY = 0;
      //--- Reset resize origin X
      m_resizeStartX = 0;
      //--- Reset resize origin Y
      m_resizeStartY = 0;
      //--- Reset width snapshot for resize
      m_resizeInitialWidth = 0;
      //--- Reset height snapshot for resize
      m_resizeInitialHeight = 0;
      //--- Reset active resize direction
      m_activeResizeMode = NO_RESIZE;
      //--- Reset hover resize direction
      m_hoverResizeMode = NO_RESIZE;

      //--- Reset canvas hover flag
      m_isHoveringCanvas = false;
      //--- Reset header hover flag
      m_isHoveringHeader = false;
      //--- Reset resize zone hover flag
      m_isHoveringResizeZone = false;
      //--- Reset switch icon hover flag
      m_isHoveringSwitchIcon = false;
      //--- Reset last mouse X
      m_lastMouseX = 0;
      //--- Reset last mouse Y
      m_lastMouseY = 0;
      //--- Reset previous mouse button state
      m_previousMouseButtonState = 0;

      //--- Set view mode from input
      m_currentViewMode = viewMode;
      //--- Mark 3D objects as not yet created
      m_are3DObjectsCreated = false;

      //--- Set camera distance from input
      m_cameraDistance = initialCameraDistance;
      //--- Set camera elevation angle from input
      m_cameraAngleX = initialCameraAngleX;
      //--- Set camera azimuth angle from input
      m_cameraAngleY = initialCameraAngleY;
      //--- Reset 3D rotation drag origin X
      m_mouse3DStartX = -1;
      //--- Reset 3D rotation drag origin Y
      m_mouse3DStartY = -1;
      //--- Mark 3D rotation as inactive
      m_isRotating3D = false;

      //--- Reset data range minimum
      m_minDataValue = 0.0;
      //--- Reset data range maximum
      m_maxDataValue = 0.0;
      //--- Reset peak histogram frequency
      m_maxFrequency = 0.0;
      //--- Reset peak theoretical PMF value
      m_maxTheoreticalValue = 0.0;
      //--- Mark data as not yet loaded
      m_isDataLoaded = false;

      //--- Reset sample mean
      m_sampleMean = 0.0;
      //--- Reset standard deviation
      m_sampleStandardDeviation = 0.0;
      //--- Reset skewness
      m_sampleSkewness = 0.0;
      //--- Reset kurtosis
      m_sampleKurtosis = 0.0;
      //--- Reset 25th percentile
      m_percentile25 = 0.0;
      //--- Reset median
      m_percentile50 = 0.0;
      //--- Reset 75th percentile
      m_percentile75 = 0.0;
      //--- Reset 95% CI lower bound
      m_confidenceInterval95Lower = 0.0;
      //--- Reset 95% CI upper bound
      m_confidenceInterval95Upper = 0.0;
      //--- Reset 99% CI lower bound
      m_confidenceInterval99Lower = 0.0;
      //--- Reset 99% CI upper bound
      m_confidenceInterval99Upper = 0.0;
}

Определим конструктор для класса "DistributionVisualizer", чтобы инициализировать переменные-члены значениями по умолчанию или значениями на основе входных параметров, при создании экземпляра. Установим имя объекта Canvas как "DistCanvas" и назначим начальные позиции и размеры на основе пользовательских входных параметров. Затем сбросим флаги взаимодействия в значение false, трекеры координат — в ноль, а режимы изменения размера — в "NO_RESIZE". Инициализируем состояния при наведении курсора значением false, трекеры мыши — значением zero, режим просмотра — значением "viewMode", а флаг 3D-объекта — значением false. Для камеры зададим начальное расстояние и углы, установим значение 3D-мыши равным -1, а флаг вращения — значением false. Наконец, установим значения по умолчанию для минимального/максимального значения данных и флага загрузки на соответствующие начальные значения, а также обнулим статистические метрики, подготавливая класс к операциям. Теперь нам понадобится уничтожать элементы при деинициализации, поэтому мы можем это сделать в деструкторе класса. Обычно он использует имя класса так же, как и конструктор, за исключением того, что имеет префикс тильды.

//+------------------------------------------------------------------+
//| Release all 3D scene objects on destruction                      |
//+------------------------------------------------------------------+
~DistributionVisualizer(void)
  {
   //--- Get total number of histogram bar boxes
   int count = ArraySize(m_histogramBars);
   //--- Loop over every bar and release its DirectX resources
   for(int i = 0; i < count; i++)
     {
      m_histogramBars[i].Shutdown();
     }
   //--- Release ground plane DirectX resources
   m_groundPlane.Shutdown();
   //--- Release X axis DirectX resources
   m_axisX.Shutdown();
   //--- Release Y axis DirectX resources
   m_axisY.Shutdown();
   //--- Release Z axis DirectX resources
   m_axisZ.Shutdown();
  }

Здесь мы определим деструктор для класса "DistributionVisualizer", чтобы обеспечить надлежащую очистку ресурсов при уничтожении объекта. Получим количество столбцов гистограммы с помощью ArraySize в переменной "m_histogramBars". Затем проходим циклом по каждому из них, чтобы вызвать метод "Shutdown", освобождая соответствующие ресурсы DirectX для 3D-баров. Далее вызовем метод "Shutdown" для объектов "m_groundPlane", "m_axisX", "m_axisY" и "m_axisZ", чтобы освободить память для объектов опорная плоскость и ось, что предотвратит утечки памяти и обеспечит корректное завершение работы. Важно понимать, что деструктор вызывается автоматически; его определение необязательно, но рекомендуется для надлежащей очистки ресурсов.

Затем определим функции-члены класса. Можно объявить их внутри класса, а затем определить вне класса, используя оператор области видимости (::), но в нашем случае мы объявим их публично внутри класса как функции, чтобы уменьшить сложность. Начнём с 3D-визуализации.

Построение столбцов трёхмерной гистограммы и отображение данных и камеры

Именно здесь формируется основная трёхмерная сцена. Определим функции для создания и раскрашивания каждого столбца гистограммы в виде примитива DirectX box, вычислим автоматически подгоняемое расстояние камеры, которое помещает всю сцену в поле зрения на основе фактической высоты столбцов. Создадим опорную плоскость, которая визуально закрепляет столбцы, создадим линии осей X, Y и Z для пространственной ориентации, обновим мировую позицию камеры и направление освещения на каждом фрейме на основе текущих значений угла и расстояния, а также динамически изменим положение и масштаб каждого столбца при изменении базовых данных распределения.

//+------------------------------------------------------------------+
//| Allocate and configure one DXBox per histogram bin               |
//+------------------------------------------------------------------+
bool create3DHistogramBars()
  {
   //--- Allocate the bar array to match the required bin count
   ArrayResize(m_histogramBars, histogramCells);

   //--- Decompose histogram colour into RGB byte components
   uchar r = (uchar)((histogramColor)       & 0xFF);
   uchar g = (uchar)((histogramColor >> 8)  & 0xFF);
   uchar b = (uchar)((histogramColor >> 16) & 0xFF);

   //--- Create and configure each bar box
   for(int i = 0; i < histogramCells; i++)
     {
      //--- Create unit box; actual transform applied later in update step
      if(!m_histogramBars[i].Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                                    DXVector3(-0.5f, 0.0f, -0.5f),
                                    DXVector3(0.5f, 1.0f, 0.5f)))
        {
         Print("ERROR: Failed to create 3D box for bar ", i);
         return false;
        }

      //--- Set the bar's base diffuse colour from the histogram colour input
      m_histogramBars[i].DiffuseColorSet(DXColor(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f));
      //--- Add a subtle specular highlight for depth perception
      m_histogramBars[i].SpecularColorSet(DXColor(0.2f, 0.2f, 0.2f, 0.3f));
      //--- Set specular shininess exponent
      m_histogramBars[i].SpecularPowerSet(32.0f);
      //--- Disable self-emission (bars lit only by scene lights)
      m_histogramBars[i].EmissionColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));

      //--- Register the bar with the 3D scene
      m_mainCanvas.ObjectAdd(GetPointer(m_histogramBars[i]));
     }

   return true;
  }

//+------------------------------------------------------------------+
//| Compute an optimal camera distance and angles for the scene      |
//+------------------------------------------------------------------+
void autoFitCameraPosition()
  {
   //--- Abort if not in 3D mode or data has not been loaded
   if(m_currentViewMode != VIEW_3D_MODE || !m_isDataLoaded) return;

   //--- Fixed scene width used for distance estimation
   float totalWidth  = 30.0f;
   //--- Track the tallest bar in the scene
   float maxBarHeight = 0.0f;

   //--- Use the peak theoretical value as the Y scale reference
   double rangeY = m_maxTheoreticalValue;
   //--- Guard against division by zero
   if(rangeY == 0) rangeY = 1;

   //--- Find the maximum normalised bar height across all bins
   for(int i = 0; i < histogramCells; i++)
     {
      float normalizedHeight = (float)(m_histogramFrequencies[i] / rangeY);
      float barHeight = normalizedHeight * 15.0f;
      if(barHeight > maxBarHeight) maxBarHeight = barHeight;
     }

   //--- Determine bounding extents for the entire scene
   float sceneWidth  = totalWidth;
   float sceneHeight = MathMax(maxBarHeight, 15.0f);
   float sceneDepth  = 10.0f;

   //--- Compute the scene bounding diagonal for FOV-based fitting
   float diagonal = MathSqrt(sceneWidth  * sceneWidth  +
                             sceneHeight * sceneHeight +
                             sceneDepth  * sceneDepth);

   //--- Match the projection FOV used in initialize3DContext
   float fov = (float)(DX_PI / 6.0);
   //--- Derive camera distance so the scene fills the view with a 1.5x margin
   m_cameraDistance = (diagonal / 2.0f) / MathTan(fov / 2.0f) * 1.5;

   //--- Set a comfortable default elevation angle
   m_cameraAngleX = 0.5;
   //--- Set a comfortable default azimuth angle
   m_cameraAngleY = 0.7;

   //--- Enforce minimum distance to avoid clipping near-plane
   if(m_cameraDistance <  35.0) m_cameraDistance =  35.0;
   //--- Enforce maximum distance to keep bars visible
   if(m_cameraDistance > 100.0) m_cameraDistance = 100.0;

   Print("Auto-fit camera: Distance = ", m_cameraDistance,
         ", AngleX = ", m_cameraAngleX, ", AngleY = ", m_cameraAngleY);
  }

//+------------------------------------------------------------------+
//| Create the flat ground reference plane in 3D space               |
//+------------------------------------------------------------------+
bool createGroundPlane()
  {
   //--- Build a thin box spanning the configured width and depth
   if(!m_groundPlane.Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                            DXVector3(-groundPlaneWidth / 2.0f, -groundPlaneThickness, -groundPlaneDepth / 2.0f),
                            DXVector3( groundPlaneWidth / 2.0f,  0.0f,                  groundPlaneDepth / 2.0f)))
     {
      Print("ERROR: Failed to create ground plane");
      return false;
     }

   //--- Decompose ground colour into RGB byte components (BGR layout)
   uchar r = (uchar)((groundPlaneColor >> 16) & 0xFF);
   uchar g = (uchar)((groundPlaneColor >> 8)  & 0xFF);
   uchar b = (uchar)( groundPlaneColor         & 0xFF);
   //--- Apply the configured ground colour and opacity
   m_groundPlane.DiffuseColorSet(DXColor(r / 255.0f, g / 255.0f, b / 255.0f,
                                         (float)groundPlaneOpacity));
   //--- Remove specular highlight for a flat matte look
   m_groundPlane.SpecularColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));

   //--- Register the ground plane with the 3D scene
   m_mainCanvas.ObjectAdd(GetPointer(m_groundPlane));

   return true;
  }

//+------------------------------------------------------------------+
//| Create colour-coded X, Y, and Z coordinate axis boxes            |
//+------------------------------------------------------------------+
bool create3DAxes()
  {
   //--- Create X axis as a thin horizontal box along positive X
   if(!m_axisX.Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                      DXVector3(0.0f, 0.0f, 0.0f),
                      DXVector3(axisLength, axisThickness, axisThickness)))
     {
      Print("ERROR: Failed to create X axis");
      return false;
     }
   //--- Extract X axis colour components
   uchar rx = (uchar)((axisXColor >> 16) & 0xFF);
   uchar gx = (uchar)((axisXColor >> 8)  & 0xFF);
   uchar bx = (uchar)( axisXColor         & 0xFF);
   //--- Apply X axis diffuse colour
   m_axisX.DiffuseColorSet(DXColor(rx / 255.0f, gx / 255.0f, bx / 255.0f, 1.0f));
   //--- Remove specular for clean axis appearance
   m_axisX.SpecularColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));
   //--- Register X axis with the scene
   m_mainCanvas.ObjectAdd(GetPointer(m_axisX));

   //--- Create Y axis as a thin vertical box along positive Y
   if(!m_axisY.Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                      DXVector3(0.0f, 0.0f, 0.0f),
                      DXVector3(axisThickness, axisLength, axisThickness)))
     {
      Print("ERROR: Failed to create Y axis");
      return false;
     }
   //--- Extract Y axis colour components
   uchar ry = (uchar)((axisYColor >> 16) & 0xFF);
   uchar gy = (uchar)((axisYColor >> 8)  & 0xFF);
   uchar by = (uchar)( axisYColor         & 0xFF);
   //--- Apply Y axis diffuse colour
   m_axisY.DiffuseColorSet(DXColor(ry / 255.0f, gy / 255.0f, by / 255.0f, 1.0f));
   //--- Remove specular for clean axis appearance
   m_axisY.SpecularColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));
   //--- Register Y axis with the scene
   m_mainCanvas.ObjectAdd(GetPointer(m_axisY));

   //--- Create Z axis as a thin depth box along positive Z
   if(!m_axisZ.Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                      DXVector3(0.0f, 0.0f, 0.0f),
                      DXVector3(axisThickness, axisThickness, axisLength)))
     {
      Print("ERROR: Failed to create Z axis");
      return false;
     }
   //--- Extract Z axis colour components
   uchar rz = (uchar)((axisZColor >> 16) & 0xFF);
   uchar gz = (uchar)((axisZColor >> 8)  & 0xFF);
   uchar bz = (uchar)( axisZColor         & 0xFF);
   //--- Apply Z axis diffuse colour
   m_axisZ.DiffuseColorSet(DXColor(rz / 255.0f, gz / 255.0f, bz / 255.0f, 1.0f));
   //--- Remove specular for clean axis appearance
   m_axisZ.SpecularColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));
   //--- Register Z axis with the scene
   m_mainCanvas.ObjectAdd(GetPointer(m_axisZ));

   return true;
  }

//+------------------------------------------------------------------+
//| Recompute and apply the view matrix from spherical coordinates   |
//+------------------------------------------------------------------+
void updateCameraPosition()
  {
   //--- Only apply in 3D mode
   if(m_currentViewMode != VIEW_3D_MODE) return;

   //--- Start with a camera positioned along the negative Z axis
   DXVector4 camera = DXVector4(0.0f, 0.0f, (float)(-m_cameraDistance), 1.0f);

   //--- Rotate camera around the X axis by the elevation angle
   DXMatrix rotationX;
   DXMatrixRotationX(rotationX, (float)m_cameraAngleX);
   DXVec4Transform(camera, camera, rotationX);

   //--- Rotate the result around the Y axis by the azimuth angle
   DXMatrix rotationY;
   DXMatrixRotationY(rotationY, (float)m_cameraAngleY);
   DXVec4Transform(camera, camera, rotationY);

   //--- Apply the final camera world position
   m_mainCanvas.ViewPositionSet(DXVector3(camera));

   //--- Place the key light slightly above the camera position
   DXVector3 cameraPos = DXVector3(camera.x, camera.y, camera.z);
   DXVector3 lightPos  = DXVector3(cameraPos.x, cameraPos.y + 10.0f, cameraPos.z);
   //--- Scene origin is always the light target
   DXVector3 target    = DXVector3(0.0f, 0.0f, 0.0f);

   //--- Compute the raw light direction vector
   DXVector3 lightDir;
   lightDir.x = target.x - lightPos.x;
   lightDir.y = target.y - lightPos.y;
   lightDir.z = target.z - lightPos.z;

   //--- Compute the vector length for normalisation
   float length = MathSqrt(lightDir.x * lightDir.x +
                           lightDir.y * lightDir.y +
                           lightDir.z * lightDir.z);
   //--- Normalise the direction vector if it has non-zero length
   if(length > 0.0f)
     {
      lightDir.x /= length;
      lightDir.y /= length;
      lightDir.z /= length;
     }

   //--- Apply the normalised light direction to the scene
   m_mainCanvas.LightDirectionSet(lightDir);
  }

//+------------------------------------------------------------------+
//| Reposition and scale every 3D histogram bar to match data        |
//+------------------------------------------------------------------+
void update3DHistogramBars()
  {
   //--- Abort if data has not been loaded
   if(!m_isDataLoaded) return;

   //--- Compute the X data range for spatial mapping
   double rangeX = m_maxDataValue - m_minDataValue;
   //--- Use the peak PMF value as the height scale reference
   double rangeY = m_maxTheoreticalValue;
   //--- Guard against division by zero for X
   if(rangeX == 0) rangeX = 1;
   //--- Guard against division by zero for Y
   if(rangeY == 0) rangeY = 1;

   //--- Total scene width used to space the bars evenly
   float totalWidth = 30.0f;
   //--- Compute even spacing for each bar slot
   float barSpacing = totalWidth / (float)histogramCells;
   //--- Make each bar 80% of its slot to leave a small gap
   float barWidth   = barSpacing * 0.8f;
   //--- Shift origin so bars are centred on the scene
   float offsetX    = -totalWidth / 2.0f;

   //--- Update scale and position of every bar
   for(int i = 0; i < histogramCells; i++)
     {
      //--- Normalise this bin's frequency against the peak PMF
      float normalizedHeight = (float)(m_histogramFrequencies[i] / rangeY);
      //--- Map to scene height units (max 15 units tall)
      float barHeight = normalizedHeight * 15.0f;
      //--- Enforce a minimum visible height so bars are always rendered
      if(barHeight < 0.5f) barHeight = 0.5f;

      //--- Compute the bar's X centre in scene space
      float xPos = offsetX + (float)i * barSpacing + barWidth / 2.0f;

      //--- Build scale, translation and combined transform matrices
      DXMatrix scale, translation, transform;
      DXMatrixScaling(scale, barWidth, barHeight, barWidth);
      DXMatrixTranslation(translation, xPos, 0.0f, 0.0f);
      DXMatrixMultiply(transform, scale, translation);

      //--- Apply the combined world transform to this bar
      m_histogramBars[i].TransformMatrixSet(transform);
     }
  }

Сначала определим функцию "create3DHistogramBars" для инициализации 3D-столбцов для гистограммы. Изменим размер массива "m_histogramBars" в соответствии с "histogramCells" с помощью ArrayResize, извлечем компоненты RGB из "histogramColor" с помощью битовых операций. Затем в цикле будем перебирать ячейки для создания каждого "CDXBox" с помощью метода "Create", передавая диспетчер DX и входную сцену, а также векторные размеры для юнит-бокса. Если создание не удается, выводим ошибку и возвращаем false; в противном случае устанавливаем диффузный цвет с помощью "DiffuseColorSet", используя нормализованный RGB, применим блики с помощью "SpecularColorSet" и мощность с помощью "SpecularPowerSet", обнулим излучение с помощью "EmissionColorSet". Добавим прямоугольник на объект Canvas с помощью "ObjectAdd" с указателем, возвращая значение true в случае успеха.

Для автоматического позиционирования камеры для оптимального просмотра мы реализуем функцию "autoFitCameraPosition", досрочно завершая работу, если не находимся в режиме "VIEW_3D_MODE" или если данные не загружены. Зададим общую ширину, найдём максимальную нормализованную высоту столбцов, масштабированную до 15,0f, путем перебора частот, деленных на диапазон Y, вычислим размеры сцены по высоте с помощью MathMax, вычислим диагональ с помощью MathSqrt и рассчитаем "m_cameraDistance" на основе поля зрения с помощью MathTan, применяя множитель 1,5. Мы назначим фиксированные углы для "m_cameraAngleX" и "m_cameraAngleY", ограничим расстояние в диапазоне от 35,0 до 100,0 и выведем эти настройки для отладки.

Далее создадим функцию "createGroundPlane" для добавления поверхности основания в 3D. Вызовем метод "Create" для объекта "m_groundPlane" с векторами, центрированными в начале координат и скорректированными в соответствии с входными размерами и толщиной. Обработаем сбой выводом ошибки и возвратом значения false. Извлечем RGB из "groundPlaneColor", установим значение diffuse с помощью "DiffuseColorSet", включающего "groundPlaneOpacity" для прозрачности, установим зеркальное отражение равным нулю и добавим на объект Canvas с помощью "ObjectAdd", вернем значение true. Для ориентации определим функцию "create3DAxes" для построения осей X, Y и Z, если значение "show3DAxes" равно true. Для каждой оси вызываем "Create" с соответствующими размерами векторов вдоль их направлений, извлечем RGB из соответствующих цветов, таких как "axisXColor", установим значение diffuse полностью непрозрачным и зеркальным с помощью "DiffuseColorSet" и "SpecularColorSet". Добавим на объект Canvas с помощью "ObjectAdd" и вернем true, если все выполнено успешно, или false в случае наличия ошибок.

Затем реализуем функцию "updateCameraPosition" для настройки 3D-изображения. Возвратимся, если оно не в 3D-режиме. Сформируем вектор камеры с отрицательным значением "m_cameraDistance" на Z, создадим матрицы поворота с помощью "DXMatrixRotationX" и "DXMatrixRotationY" на основе углов, последовательно преобразуем вектор, используя "DXVec4Transform", и установим положение обзора с помощью "ViewPositionSet". Для имитации направленного освещения вычислим положение источника света над камерой, вычислим и нормализуем направление на цель в начале координат с помощью "MathSqrt" и применим его с помощью "LightDirectionSet".

Наконец определим функцию "update3DHistogramBars" для динамического позиционирования и масштабирования столбцов, которая завершает работу при отсутствии данных. После вычисления диапазонов с помощью мер предосторожности мы установим общую ширину равной 30,0f, вычислим расстояние и ширину столбцов, смещение для центрирования, затем выполним цикл для нормализации высот, масштабированных до 15,0f с минимальным значением 0,5f, вычислим позиции X, построим масштабную матрицу с помощью "DXMatrixScaling" и переведем с помощью "DXMatrixTranslation", умножим их с помощью "DXMatrixMultiply" и применим преобразование к каждому столбцу с помощью "TransformMatrixSet" для размещения в 3D. Далее реализуем отрисовку заголовка и рамки для 3D-режима.

Рисование заголовка, значка переключателя и границы в трехмерном режиме

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

//+------------------------------------------------------------------+
//| Draw the circular 2D/3D toggle icon in the header bar            |
//+------------------------------------------------------------------+
void drawSwitchIcon()
  {
   //--- Compute the icon's top-left corner from the canvas right margin
   int iconX = m_currentWidth  - SWITCH_ICON_SIZE - SWITCH_ICON_MARGIN;
   //--- Vertically centre the icon within the header bar
   int iconY = (HEADER_BAR_HEIGHT - SWITCH_ICON_SIZE) / 2;

   //--- Compute icon background colour from hover state
   color iconBgColor = m_isHoveringSwitchIcon
                       ? DarkenColor(themeColor, 0.1)     // Slightly darker on hover
                       : LightenColor(themeColor, 0.5);   // Default lighter shade

   uint argbIconBg = ColorToARGB(iconBgColor, 255);

   //--- Fill the circular icon background
   m_mainCanvas.FillCircle(iconX + SWITCH_ICON_SIZE / 2, iconY + SWITCH_ICON_SIZE / 2,
                           SWITCH_ICON_SIZE / 2, argbIconBg);

   //--- Draw the circular icon border using the theme colour
   uint argbBorder = ColorToARGB(themeColor, 255);
   m_mainCanvas.Circle(iconX + SWITCH_ICON_SIZE / 2, iconY + SWITCH_ICON_SIZE / 2,
                       SWITCH_ICON_SIZE / 2, argbBorder);

   //--- Set a small bold font for the mode label
   m_mainCanvas.FontSet("Arial Bold", 10);
   uint argbLabel = ColorToARGB(clrWhite, 255);

   //--- Display "2D" or "3D" depending on the active mode
   string modeLabel = (m_currentViewMode == VIEW_2D_MODE) ? "2D" : "3D";
   m_mainCanvas.TextOut(iconX + SWITCH_ICON_SIZE / 2, iconY + (SWITCH_ICON_SIZE - 10) / 2,
                        modeLabel, argbLabel, TA_CENTER);
  }

//+------------------------------------------------------------------+
//| Draw the header bar overlaid on the 3D rendered scene            |
//+------------------------------------------------------------------+
void drawHeaderBarOn3D()
  {
   //--- Compute the header fill colour from the current interaction state
   color headerColor;
   if(m_isDragging)
      headerColor = DarkenColor(themeColor, 0.1);       // Slightly darker while dragging
   else if(m_isHoveringHeader)
      headerColor = LightenColor(themeColor, 0.4);      // Medium light on hover
   else
      headerColor = LightenColor(themeColor, 0.7);      // Very light at rest

   uint argbHeader = ColorToARGB(headerColor, 255);

   //--- Paint over the top portion of the 3D render with the header colour
   m_mainCanvas.FillRectangle(0, 0, m_currentWidth - 1, HEADER_BAR_HEIGHT, argbHeader);

   //--- Overlay a border frame on the header if enabled
   if(showBorderFrame)
     {
      uint argbBorder = ColorToARGB(themeColor, 255);
      m_mainCanvas.Rectangle(0, 0, m_currentWidth - 1, HEADER_BAR_HEIGHT, argbBorder);
      m_mainCanvas.Rectangle(1, 1, m_currentWidth - 2, HEADER_BAR_HEIGHT - 1, argbBorder);
     }

   //--- Set the bold title font
   m_mainCanvas.FontSet("Arial Bold", titleFontSize);
   uint argbText = ColorToARGB(titleTextColor, 255);

   //--- Format the title string with current parameters and 3D label
   string titleText = StringFormat("Binomial Distribution (n=%d, p=%.2f) - 3D View",
                                   numTrials, successProbability);
   //--- Draw the title centred horizontally within the header
   m_mainCanvas.TextOut(m_currentWidth / 2, (HEADER_BAR_HEIGHT - titleFontSize) / 2,
                        titleText, argbText, TA_CENTER);

   //--- Draw the interactive 2D/3D mode switch icon
   drawSwitchIcon();
  }
//+------------------------------------------------------------------+
//| Draw the outer and inner border rectangles for 3D overlay        |
//+------------------------------------------------------------------+
void draw3DBorder()
  {
   //--- Use a slightly darker border when hovering a resize zone
   color borderColor = m_isHoveringResizeZone ? DarkenColor(themeColor, 0.2) : themeColor;
   uint  argbBorder  = ColorToARGB(borderColor, 255);

   //--- Draw the outermost border rectangle over the 3D render
   m_mainCanvas.Rectangle(0, 0, m_currentWidth - 1, m_currentHeight - 1, argbBorder);
   //--- Draw an inner border rectangle for a double-line effect
   m_mainCanvas.Rectangle(1, 1, m_currentWidth - 2, m_currentHeight - 2, argbBorder);
  }

В этой части определим функцию "drawSwitchIcon" для отображения кнопки-переключателя в заголовке для переключения режимов просмотра. Вычислим положение значка на основе таких констант, как "SWITCH_ICON_SIZE" и "SWITCH_ICON_MARGIN", выберем цвет фона, который затемняется при наведении курсора мыши с помощью "DarkenColor" или наоборот осветляется с помощью "LightenColor". Преобразуем в ARGB с помощью "ColorToARGB" и заполним круг с помощью метода fillCircle.  Добавим окружность с помощью Circle, установим жирный шрифт с помощью "FontSet", подготовим белый текст в формате ARGB, форматируем метку как "2D" или "3D" в зависимости от значения "m_currentViewMode" и центрируем ее с помощью TextOut для обеспечения интерактивной обратной связи.

Далее создадим функцию "drawHeaderBarOn3D" для наложения заголовка в 3D-режиме, аналогичном 2D, но с измененным заголовком. Определим цвет заголовка на основе состояния перетаскивания или наведения курсора с помощью параметров "DarkenColor" или "LightenColor", заполним прямоугольник с помощью FillRectangle, добавим границы, если параметр "showBorderFrame" имеет значение true. С помощью Rectangle, установим шрифт, отформатируем заголовок, включая "- 3D View", с помощью StringFormat, центрируем его с помощью "TextOut" и вызовем функцию "drawSwitchIcon", чтобы добавить переключатель. Для создания трехмерной рамки холста мы реализуем функцию "draw3DBorder". Выберем цвет границы, затемняемый при изменении размера и наведении курсора с помощью параметра "DarkenColor", преобразуя его в ARGB, и рисуем внутренние и внешние прямоугольники с помощью "Rectangle" для согласованности с двухмерными границами. Далее построим трехмерную теоретическую кривую, используя трехмерную плоскость для обеспечения согласованности. Для простоты пока будем использовать обычную прямую линию.

Проецирование теоретической кривой на трехмерную сцену

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

//+------------------------------------------------------------------+
//| Project the theoretical PMF curve into 3D screen space           |
//+------------------------------------------------------------------+
void draw3DTheoreticalCurve()
  {
   //--- Abort if data has not been loaded yet
   if(!m_isDataLoaded) return;

   //--- Compute the data ranges for world-space mapping
   double rangeX = m_maxDataValue - m_minDataValue;
   double rangeY = m_maxTheoreticalValue;
   if(rangeX == 0) rangeX = 1;
   if(rangeY == 0) rangeY = 1;

   //--- Match the total scene width used for the histogram bars
   float totalWidth = 30.0f;
   float offsetX    = -totalWidth / 2.0f;

   //--- Retrieve the current view-projection matrices for projection
   DXMatrix projection, view, worldToScreen;
   m_mainCanvas.ViewMatrixGet(view);
   m_mainCanvas.ProjectionMatrixGet(projection);
   //--- Combine view and projection into a single world-to-clip matrix
   DXMatrixMultiply(worldToScreen, view, projection);

   uint curveColor = ColorToARGB(theoreticalCurveColor, 255);

   //--- Transform and draw each consecutive pair of PMF samples
   for(int i = 0; i < ArraySize(m_theoreticalXValues) - 1; i++)
     {
      //--- Map PMF X and Y values into 3D world space
      float x1 = offsetX + (float)((m_theoreticalXValues[i]     - m_minDataValue) / rangeX * totalWidth);
      float y1 = (float)(m_theoreticalYValues[i]     / rangeY * 20.0);
      float x2 = offsetX + (float)((m_theoreticalXValues[i + 1] - m_minDataValue) / rangeX * totalWidth);
      float y2 = (float)(m_theoreticalYValues[i + 1] / rangeY * 20.0);

      //--- Build homogeneous 3D points on the Z = 0 plane
      DXVector4 p1_3d = DXVector4(x1, y1, 0.0f, 1.0f);
      DXVector4 p2_3d = DXVector4(x2, y2, 0.0f, 1.0f);

      //--- Project both points into clip space
      DXVec4Transform(p1_3d, p1_3d, worldToScreen);
      DXVec4Transform(p2_3d, p2_3d, worldToScreen);

      //--- Perform perspective divide only for points in front of the near plane
      if(p1_3d.w > 0.0f && p2_3d.w > 0.0f)
        {
         DXVec4Scale(p1_3d, p1_3d, 1.0f / p1_3d.w);
         DXVec4Scale(p2_3d, p2_3d, 1.0f / p2_3d.w);

         //--- Convert NDC coordinates to pixel coordinates
         int sx1 = (int)((float)m_currentWidth  * (0.5f + 0.5f * p1_3d.x));
         int sy1 = (int)((float)m_currentHeight * (0.5f - 0.5f * p1_3d.y));
         int sx2 = (int)((float)m_currentWidth  * (0.5f + 0.5f * p2_3d.x));
         int sy2 = (int)((float)m_currentHeight * (0.5f - 0.5f * p2_3d.y));

         //--- Clip line endpoints against the header bar boundary
         if(clipLineToHeader(sx1, sy1, sx2, sy2))
           {
            //--- Draw multiple offset passes for the configured line width
            for(int w = 0; w < curveLineWidth; w++)
               m_mainCanvas.LineAA(sx1, sy1 + w, sx2, sy2 + w, curveColor);
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Clip a line segment to exclude the header bar region             |
//+------------------------------------------------------------------+
bool clipLineToHeader(int &x1, int &y1, int &x2, int &y2)
  {
   //--- Reject the segment entirely if both endpoints are inside the header
   if(y1 < HEADER_BAR_HEIGHT && y2 < HEADER_BAR_HEIGHT)
      return false;

   //--- Accept the segment entirely if both endpoints are below the header
   if(y1 >= HEADER_BAR_HEIGHT && y2 >= HEADER_BAR_HEIGHT)
      return true;

   //--- Clip the first endpoint when it falls inside the header
   if(y1 < HEADER_BAR_HEIGHT)
     {
      if(y2 != y1)
        {
         //--- Linearly interpolate X to find the intersection with the header boundary
         x1 = x1 + (x2 - x1) * (HEADER_BAR_HEIGHT - y1) / (y2 - y1);
         y1 = HEADER_BAR_HEIGHT;
        }
     }
   //--- Clip the second endpoint when it falls inside the header
   else if(y2 < HEADER_BAR_HEIGHT)
     {
      if(y2 != y1)
        {
         //--- Linearly interpolate X to find the intersection with the header boundary
         x2 = x1 + (x2 - x1) * (HEADER_BAR_HEIGHT - y1) / (y2 - y1);
         y2 = HEADER_BAR_HEIGHT;
        }
     }

   return true;
  }

В этой части мы определим функцию "draw3DTheoreticalCurve", которая накладывает теоретическую функцию массы вероятности в виде 2D-линии на 3D-сцену, обеспечивая ее корректное отображение в перспективе без необходимости полного моделирования 3D-кривой. Это возможно, но пока мы просто не хотим этого делать, так как хотим сосредоточиться только на 3D-столбцах. Если данные не загружены, функция немедленно завершает работу, вычисляем диапазоны X и Y с соблюдением мер предосторожности, установим общую ширину, соответствующую гистограмме, для выравнивания и вычислим смещение для центрирования.

Чтобы спроецировать 3D-точки в 2D-пространство экрана, мы объявим матрицы, извлечем область просмотра и проекцию с помощью "ViewMatrixGet" и "ProjectionMatrixGet", умножим их на матрицу "мир-экран" с помощью "DXMatrixMultiply" и подготовим цвет кривой с помощью ColorToARGB. Перебирая последовательные теоретические точки, мы масштабируем координаты X и Y, чтобы они соответствовали трехмерному пространству, формируем точки "DXVector4" при z=0, преобразуем их с помощью "DXVec4Transform". Проверим положительное значение w на видимость, нормализуем путем деления с помощью "DXVec4Scale", преобразуем в экранные целые числа на основе размеров объекта Canvas. Далее обрежем сегмент, чтобы избежать заголовка, используя "clipLineToHeader" и нарисуем сглаженные линии с помощью LineAA в цикле ширины из "curveLineWidth" для определения толщины.

Этот метод проекции имеет решающее значение, поскольку связывает 3D-рендеринг с 2D-наложениями: преобразуя мировые координаты с помощью объединенной матрицы, мы имитируем глубину, одновременно рисуя плоские линии на объекте Canvas, позволяя кривой выглядеть так, как будто она парит в 3D-пространстве относительно столбцов. Это улучшает визуальную корреляцию без сложной 3D-сплайновой интерполяции. Чтобы предотвратить рисование поверх заголовка, мы реализуем функцию "clipLineToHeader" как простую утилиту для обрезки линии по границе заголовка. Отклоним, если обе координаты y находятся выше значения "HEADER_BAR_HEIGHT", примем, если обе находятся ниже. В противном случае обрежем проблемную точку, интерполируя x по границе y, обновим координаты справочно и обеспечим корректное отображение 2D-элементов в 3D-виде. Теперь можно приступить к инициализации 3D-модели, чтобы отслеживать прогресс. Теперь определим логику для создания визуализации.

Создание и инициализация трехмерного контекста

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

//+------------------------------------------------------------------+
//| Dispatch rendering to the active 2D or 3D pipeline               |
//+------------------------------------------------------------------+
void renderVisualization()
  {
   //--- Render using the 2D canvas pipeline
   if(m_currentViewMode == VIEW_2D_MODE)
      render2DVisualization();
   else
      //--- Render using the 3D DirectX pipeline
      render3DVisualization();
  }
//+------------------------------------------------------------------+
//| Render the 3D scene and overlay 2D UI elements on top            |
//+------------------------------------------------------------------+
void render3DVisualization()
  {
   //--- Abort if data has not been loaded
   if(!m_isDataLoaded) return;

   //--- Recompute the view and light transforms for this frame
   updateCameraPosition();
   //--- Reposition all 3D histogram bars to match current data
   update3DHistogramBars();

   //--- Use the configured background colour for the clear pass
   color bgColor     = backgroundTopColor;
   uint  bgColorArgb = ColorToARGB(bgColor, 255);
   //--- Clear colour and depth buffers, then render the 3D scene
   m_mainCanvas.Render(DX_CLEAR_COLOR | DX_CLEAR_DEPTH, bgColorArgb);

   //--- Overlay the 2D border frame on top of the 3D render
   if(showBorderFrame)
      draw3DBorder();

   //--- Overlay the header bar on the rendered 3D image
   drawHeaderBarOn3D();
   //--- Overlay the stats panel and legend if enabled
   if(showStatistics)
     {
      drawStatisticsPanelOn3D();
      drawLegendOn3D();
     }
   //--- Project and draw the theoretical PMF curve into screen space
   draw3DTheoreticalCurve();

   //--- Draw the resize grip indicator when hovering
   if(m_isHoveringResizeZone && enableResizing)
      drawResizeIndicatorOn3D();

   //--- Flush the pixel buffer to the chart object
   m_mainCanvas.Update();
  }
//+------------------------------------------------------------------+
//| Create bitmap label, initialise 3D context and scene objects     |
//+------------------------------------------------------------------+
bool createCanvasAndObjects()
  {
   //--- Create the canvas bitmap label on the chart
   if(!m_mainCanvas.CreateBitmapLabel(m_canvasObjectName, 0, 0, m_currentWidth, m_currentHeight,
                                      COLOR_FORMAT_ARGB_NORMALIZE))
     {
      Print("ERROR: Failed to create canvas");
      return false;
     }

   //--- Position the canvas horizontally
   ObjectSetInteger(0, m_canvasObjectName, OBJPROP_XDISTANCE, m_currentPositionX);
   //--- Position the canvas vertically
   ObjectSetInteger(0, m_canvasObjectName, OBJPROP_YDISTANCE, m_currentPositionY);

   //--- Initialise the DirectX 3D rendering context
   if(!initialize3DContext())
     {
      Print("ERROR: Failed to initialize 3D context");
      return false;
     }

   //--- Build all 3D scene objects (bars, ground, axes)
   if(!create3DObjects())
     {
      Print("ERROR: Failed to create 3D objects");
      return false;
     }

   return true;
  }

//+------------------------------------------------------------------+
//| Set up projection, lighting and initial camera for 3D rendering  |
//+------------------------------------------------------------------+
bool initialize3DContext()
  {
   //--- Set perspective projection matrix with 30-degree FOV
   m_mainCanvas.ProjectionMatrixSet((float)(DX_PI / 6.0),
                                    (float)m_currentWidth / (float)m_currentHeight,
                                    0.1f, 1000.0f);

   //--- Point the camera at the scene origin
   m_mainCanvas.ViewTargetSet(DXVector3(0.0f, 0.0f, 0.0f));
   //--- Define world up direction as positive Y
   m_mainCanvas.ViewUpDirectionSet(DXVector3(0.0f, 1.0f, 0.0f));

   //--- Set directional light colour to near-white
   m_mainCanvas.LightColorSet(DXColor(1.0f, 1.0f, 1.0f, 0.9f));
   //--- Set ambient light colour for soft fill
   m_mainCanvas.AmbientColorSet(DXColor(0.6f, 0.6f, 0.6f, 0.5f));

   //--- Auto-fit camera if enabled and data is ready
   if(autoFitCamera && m_isDataLoaded)
      autoFitCameraPosition();

   //--- Recompute and apply the camera transform
   updateCameraPosition();

   Print("SUCCESS: 3D context initialized");
   return true;
  }
//+------------------------------------------------------------------+
//| Build all 3D scene objects: bars, ground plane, and axes         |
//+------------------------------------------------------------------+
bool create3DObjects()
  {
   //--- Create the histogram bar boxes
   if(!create3DHistogramBars())
     {
      Print("ERROR: Failed to create 3D histogram bars");
      return false;
     }

   //--- Create the flat ground reference plane
   if(!createGroundPlane())
     {
      Print("ERROR: Failed to create ground plane");
      return false;
     }

   //--- Create coordinate axes only when the option is enabled
   if(show3DAxes && !create3DAxes())
     {
      Print("ERROR: Failed to create 3D axes");
      return false;
     }

   //--- Flag that all 3D objects are ready
   m_are3DObjectsCreated = true;
   Print("SUCCESS: 3D objects created");
   return true;
  }

//+------------------------------------------------------------------+
//| Prepare the scene for 3D rendering after a mode switch           |
//+------------------------------------------------------------------+
bool setup3DMode()
  {
   //--- If objects already exist, simply refresh the camera
   if(m_are3DObjectsCreated)
     {
      //--- Auto-fit camera when enabled and data is available
      if(autoFitCamera && m_isDataLoaded)
         autoFitCameraPosition();

      //--- Apply updated camera transform
      updateCameraPosition();
      return true;
     }

   //--- Warn if this path is reached unexpectedly
   Print("WARNING: 3D objects not created - this shouldn't happen!");
   //--- Attempt to create objects as a fallback
   return create3DObjects();
  }

//+------------------------------------------------------------------+
//| Generate binomial sample, compute histogram and statistics       |
//+------------------------------------------------------------------+
bool loadDistributionData()
  {
   //--- Seed the random number generator with current tick count
   MathSrand(GetTickCount());

   //--- Allocate the sample data buffer
   ArrayResize(m_sampleData, sampleSize);
   //--- Fill the buffer with random binomial variates
   MathRandomBinomial(numTrials, successProbability, sampleSize, m_sampleData);

   //--- Compute the frequency histogram from the sample
   if(!computeHistogram(m_sampleData, m_histogramIntervals, m_histogramFrequencies,
                        m_maxDataValue, m_minDataValue, histogramCells))
     {
      Print("ERROR: Failed to calculate histogram");
      return false;
     }

   //--- Allocate arrays for the theoretical PMF curve
   ArrayResize(m_theoreticalXValues, numTrials + 1);
   ArrayResize(m_theoreticalYValues, numTrials + 1);
   //--- Fill X values as integer sequence 0 .. numTrials
   MathSequence(0, numTrials, 1, m_theoreticalXValues);
   //--- Compute binomial PMF for each X value
   MathProbabilityDensityBinomial(m_theoreticalXValues, numTrials, successProbability,
                                  false, m_theoreticalYValues);

   //--- Find the tallest histogram bin
   m_maxFrequency = m_histogramFrequencies[ArrayMaximum(m_histogramFrequencies)];
   //--- Find the peak theoretical PMF value
   m_maxTheoreticalValue = m_theoreticalYValues[ArrayMaximum(m_theoreticalYValues)];

   //--- Compute scaling factor to align histogram with PMF curve
   double scaleFactor = m_maxFrequency / m_maxTheoreticalValue;
   //--- Scale every bin frequency so it matches PMF units
   for(int i = 0; i < histogramCells; i++)
      m_histogramFrequencies[i] /= scaleFactor;

   //--- Compute all descriptive statistics
   computeAdvancedStatistics();

   //--- Mark data as successfully loaded
   m_isDataLoaded = true;

   //--- Update 3D bar heights if the scene is already built
   if(m_currentViewMode == VIEW_3D_MODE && m_are3DObjectsCreated)
     {
      //--- Refit camera to new data extents
      if(autoFitCamera)
         autoFitCameraPosition();
      //--- Reposition every bar in 3D space
      update3DHistogramBars();
     }

   Print("SUCCESS: Loaded distribution data");
   return true;
  }

Во-первых, мы определим функцию "renderVisualization" для обработки отрисовки на основе текущего режима, проверим "m_currentViewMode" на соответствие "VIEW_2D_MODE" для вызова "render2DVisualization" или, в противном случае, "render3DVisualization", централизуем логику отрисовки как для 2D, так и для 3D просмотра. Мы не будем много работать с 2D, поскольку логика та же, что и в предыдущей версии. Для 3D-графики мы реализуем функцию "render3DVisualization", которая завершается досрочно без данных, обновим параметры камеры и столбцов гистограммы, установим цвет фона из "backgroundTopColor", преобразованного в ARGB. Очистим сцену с помощью функции Render с помощью флагов "DX_CLEAR_COLOR | DX_CLEAR_DEPTH", отрисует границу, если она включена, с помощью функции "draw3DBorder". Добавим заголовок с помощью функции "drawHeaderBarOn3D" и завершим работу функцией "Update" для отображения 3D-контента. Для настройки объекта Canvas мы создаём функцию "createCanvasAndObjects", вызывая CreateBitmapLabel для "m_mainCanvas" с нормализованным форматом ARGB Установим расстояния между объектами с помощью ObjectSetInteger для "OBJPROP_XDISTANCE" и "OBJPROP_YDISTANCE". Далее вызовем "initialize3DContext" и "create3DObjects", возвращая false в случае любой ошибки с выводом сообщений об ошибке.

Затем определим функцию "initialize3DContext" для настройки 3D-среды: установим проекционную матрицу с помощью "ProjectionMatrixSet", используя FOV в 30 градусов, коэффициент отношения из размеров и ближнюю/дальнюю плоскости. Определим целевую точку обзора и направление вверх с помощью "ViewTargetSet" и "ViewUpDirectionSet" в начале координат. Применим цвета света и окружающего освещения с помощью "LightColorSet" и "AmbientColorSet". Выполняем автоподбор положения камеры, если это включено и данные, загруженные вызовом "autoFitCameraPosition". Обновим положение с помощью "updateCameraPosition"; и выведем сообщение об успешном завершении.

Далее функция "create3DObjects" организует создание 3D-элементов, последовательно вызывая функции "create3DHistogramBars", "createGroundPlane" и условно "create3DAxes", если "show3DAxes" имеет значение true. Установим "m_are3DObjectsCreated" в значение true в случае успеха с сообщением для вывода на экран или возвращая значение false в случае любой ошибки. Для активации режима мы реализуем функцию "setup3DMode", которая проверяет, созданы ли объекты, и если да, производит автоподбор положения камеры и обновляет камеру. В противном случае выводит предупреждение и пытается создать их с помощью функции "create3DObjects". Теперь определим "loadDistributionData" для подготовки биномиальных данных: зададим начальное значение генератора случайных чисел с помощью MathSrand, используя GetTickCount, изменим размер "m_sampleData" и генерируем выборки с помощью MathRandomBinomial, вычислим гистограмму с помощью "computeHistogram", установим теоретические массивы с помощью "MathSequence" и MathProbabilityDensityBinomial, найдем максимальные значения с помощью ArrayMaximum, масштабируем частоты в соответствии с теоретическим максимумом. Вычислим статистику, установим "m_isDataLoaded" в значение true, и если находимся в режиме "VIEW_3D_MODE" с созданными объектами, производим автоподбор положения камеры и обновляем столбцы, выводим сообщение об успешном завершении. Для инициализации вызовем функции следующим образом в обработчике событий инициализации.

Подключение обработчика событий инициализации

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

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
DistributionVisualizer *distributionVisualizer = NULL; // Pointer to the active visualizer instance

//+------------------------------------------------------------------+
//| Initialise the EA, create the canvas and load distribution data  |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Enable mouse movement events on the chart
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE,  true);
   //--- Enable mouse wheel events on the chart
   ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);

   //--- Allocate and construct the visualizer object
   distributionVisualizer = new DistributionVisualizer();
   if(distributionVisualizer == NULL)
     {
      Print("ERROR: Failed to create window object");
      return INIT_FAILED;
     }

   //--- Create the canvas bitmap label and initialise the 3D scene
   if(!distributionVisualizer.createCanvasAndObjects())
     {
      Print("ERROR: Failed to create canvas");
      delete distributionVisualizer;
      distributionVisualizer = NULL;
      return INIT_FAILED;
     }

   //--- Generate the binomial sample and compute all statistics
   if(!distributionVisualizer.loadDistributionData())
     {
      Print("ERROR: Failed to load distribution data");
      delete distributionVisualizer;
      distributionVisualizer = NULL;
      return INIT_FAILED;
     }

   //--- Render the initial frame
   distributionVisualizer.renderVisualization();
   ChartRedraw();

   Print("SUCCESS: Distribution window initialized");
   return INIT_SUCCEEDED;
  }

Сначала объявим глобальный указатель "distributionVisualizer" на класс "DistributionVisualizer", инициализируемый значением NULL, для управления экземпляром инструмента во всей программе. В обработчике OnInit включим события мыши, установив CHART_EVENT_MOUSE_MOVE и "CHART_EVENT_MOUSE_WHEEL" в значение true с помощью ChartSetInteger для поддержки взаимодействия. Создадим экземпляр визуализатора с помощью new, проверим наличие значения NULL и обработаем сбой, выводя сообщение об ошибке и вернём "INIT_FAILED". Затем вызовем "createCanvasAndObjects" для настройки объекта Canvas и 3D-элементов, с обработкой ошибок для удаления экземпляра и возврата ошибки. Затем загрузим данные с помощью функции "loadDistributionData", снова выполняя очистку при возникновении ошибки. Наконец отобразим визуализацию, перерисуем график с помощью ChartRedraw, выводим сообщение об успешном завершении и вернём INIT_SUCCEEDED для подтверждения настройки. После компиляции получаем следующий результат.

3D BARS  INITIALIZED

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

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

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

//+------------------------------------------------------------------+
//| Draw the statistics panel overlay on the 3D render               |
//+------------------------------------------------------------------+
void drawStatisticsPanelOn3D()
  {
   //--- Reuse the same 2D statistics panel for the 3D overlay
   drawStatisticsPanel();
  }
//+------------------------------------------------------------------+
//| Draw the legend panel overlay on the 3D render                   |
//+------------------------------------------------------------------+
void drawLegendOn3D()
  {
   //--- Compute legend panel absolute position (same layout as 2D)
   int legendX          = statsPanelX;
   int legendY          = HEADER_BAR_HEIGHT + statsPanelY + statsPanelHeight;
   int legendWidth      = statsPanelWidth;
   int legendHeightThis = legendHeight;

   //--- Compute legend background colour as a very light theme tint
   color legendBgColor = LightenColor(themeColor, 0.9);
   uchar bgAlpha       = 153;
   uint  argbLegendBg  = ColorToARGB(legendBgColor, bgAlpha);
   uint  argbBorder    = ColorToARGB(themeColor, 255);
   uint  argbText      = ColorToARGB(clrBlack, 255);

   //--- Flood-fill the legend background with alpha blending
   for(int y = legendY; y <= legendY + legendHeightThis; y++)
      for(int x = legendX; x <= legendX + legendWidth; x++)
         blendPixelSet(m_mainCanvas, x, y, argbLegendBg);

   //--- Draw all four border lines of the legend panel
   for(int x = legendX; x <= legendX + legendWidth; x++)
      blendPixelSet(m_mainCanvas, x, legendY, argbBorder); // Top border
   for(int y = legendY; y <= legendY + legendHeightThis; y++)
      blendPixelSet(m_mainCanvas, legendX + legendWidth, y, argbBorder); // Right border
   for(int x = legendX; x <= legendX + legendWidth; x++)
      blendPixelSet(m_mainCanvas, x, legendY + legendHeightThis, argbBorder); // Bottom border
   for(int y = legendY; y <= legendY + legendHeightThis; y++)
      blendPixelSet(m_mainCanvas, legendX, y, argbBorder); // Left border

   //--- Set the legend text font
   m_mainCanvas.FontSet("Arial", panelFontSize);

   //--- Initialise vertical text cursor inside the legend
   int itemY      = legendY + 10;
   int lineSpacing = panelFontSize;

   //--- Draw 3D histogram colour swatch and its label
   uint argbHist = ColorToARGB(histogramColor, 255);
   m_mainCanvas.FillRectangle(legendX + 7, itemY - 4, legendX + 22, itemY + 4, argbHist);
   m_mainCanvas.TextOut(legendX + 27, itemY - 4, "3D Histogram", argbText, TA_LEFT);
   itemY += lineSpacing;

   //--- Draw a short horizontal line as the curve colour swatch
   uint argbCurve = ColorToARGB(theoreticalCurveColor, 255);
   for(int i = 0; i < 15; i++)
     {
      blendPixelSet(m_mainCanvas, legendX + 7 + i, itemY,     argbCurve); // Upper swatch pixel row
      blendPixelSet(m_mainCanvas, legendX + 7 + i, itemY + 1, argbCurve); // Lower swatch pixel row
     }
   //--- Draw the PMF curve label beside the swatch
   m_mainCanvas.TextOut(legendX + 27, itemY - 4, "Theoretical PMF", argbText, TA_LEFT);
  }
//+------------------------------------------------------------------+
//| Draw resize grip indicators overlaid on the 3D render            |
//+------------------------------------------------------------------+
void drawResizeIndicatorOn3D()
  {
   //--- Reuse the same 2D resize indicator for the 3D overlay
   drawResizeIndicator();
  }

//--- Call these in the 3D visual function

if(showStatistics)
  {
   drawStatisticsPanelOn3D();
   drawLegendOn3D();
  }
//--- Project and draw the theoretical PMF curve into screen space
draw3DTheoreticalCurve();

//--- Draw the resize grip indicator when hovering
if(m_isHoveringResizeZone && enableResizing)
   drawResizeIndicatorOn3D();

В этой части определим функцию "drawStatisticsPanelOn3D" для отображения статистики в 3D-режиме путем простого вызова функции "drawStatisticsPanel". Повторно используем 2D-логику для обеспечения согласованности наложения. Для легенды в 3D реализуем "drawLegendOn3D" аналогично 2D, но с меткой "3D-гистограмма": установим позиции из входных параметров, осветлим "themeColor" для фона с помощью "LightenColor", подготовим цвета ARGB, включая альфа-канал, с помощью ColorToARGB. В цикле смешаем пиксели для заливки и границ с помощью "blendPixelSet", установим шрифт с помощью FontSet, рисуем прямоугольник образца гистограммы с помощью FillRectangle и добавим метку с помощью "TextOut". Далее смешаем линию образца кривой и добавим ее метку. Устроим визуальные кнопки, адаптированные для 3D. Для индикации областей изменения размера в 3D создадим "drawResizeIndicatorOn3D", который вызывает "drawResizeIndicator" для обеспечения той же обратной связи, что и в 2D.

В процессе 3D-рендеринга, если "showStatistics" имеет значение true, вызовем функции "drawStatisticsPanelOn3D" и "drawLegendOn3D" для наложения информационных панелей. Рисуем кривую с помощью функции "draw3DTheoreticalCurve" для вероятностного наложения и если происходит наведение курсора на зону изменения размера с включенной функцией изменения размера, добавляем индикатор с помощью функции "drawResizeIndicatorOn3D". Интегрируем 2D-элементы после 3D-рендеринга для гибридного отображения. При компилировании получаем следующий результат.

STATISTICS AND LEGEND PANEL

После завершения работы с панелью необходимо обработать взаимодействие с графиком.

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

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

//+------------------------------------------------------------------+
//| Process mouse move and button events for interaction             |
//+------------------------------------------------------------------+
void handleMouseEvent(int mouseX, int mouseY, int mouseState)
  {
   //--- Snapshot previous hover states to detect changes
   bool previousHoverState       = m_isHoveringCanvas;
   bool previousHeaderHoverState = m_isHoveringHeader;
   bool previousResizeHoverState = m_isHoveringResizeZone;
   bool previousSwitchHoverState = m_isHoveringSwitchIcon;

   //--- Update canvas hover flag based on cursor position
   m_isHoveringCanvas = (mouseX >= m_currentPositionX &&
                         mouseX <= m_currentPositionX + m_currentWidth &&
                         mouseY >= m_currentPositionY &&
                         mouseY <= m_currentPositionY + m_currentHeight);

   //--- Update individual zone hover flags
   m_isHoveringHeader      = isMouseOverHeaderBar(mouseX, mouseY);
   m_isHoveringSwitchIcon  = isMouseOverSwitchIcon(mouseX, mouseY);
   m_isHoveringResizeZone  = isMouseInResizeZone(mouseX, mouseY, m_hoverResizeMode);

   //--- Determine if a redraw is needed due to hover state changes
   bool needRedraw = (previousHoverState       != m_isHoveringCanvas  ||
                      previousHeaderHoverState != m_isHoveringHeader  ||
                      previousResizeHoverState != m_isHoveringResizeZone ||
                      previousSwitchHoverState != m_isHoveringSwitchIcon);

   //--- Handle 3D orbit drag when in 3D mode and not over the header
   if(m_currentViewMode == VIEW_3D_MODE && m_isHoveringCanvas && !m_isHoveringHeader)
     {
      //--- Begin rotation on fresh left-button press
      if(mouseState == 1 && m_previousMouseButtonState == 0)
        {
         m_isRotating3D  = true;
         m_mouse3DStartX = mouseX;
         m_mouse3DStartY = mouseY;
         //--- Prevent chart from consuming mouse scroll during rotation
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
        }
      //--- Continue rotation while button is held and dragging
      else if(mouseState == 1 && m_previousMouseButtonState == 1 && m_isRotating3D)
        {
         //--- Update azimuth angle proportional to horizontal mouse delta
         m_cameraAngleY += (mouseX - m_mouse3DStartX) / 300.0;
         //--- Update elevation angle proportional to vertical mouse delta
         m_cameraAngleX += (mouseY - m_mouse3DStartY) / 300.0;

         //--- Clamp elevation to avoid gimbal lock at poles
         if(m_cameraAngleX < -DX_PI * 0.49) m_cameraAngleX = -DX_PI * 0.49;
         if(m_cameraAngleX >  DX_PI * 0.49) m_cameraAngleX =  DX_PI * 0.49;

         //--- Update rotation anchor for the next delta computation
         m_mouse3DStartX = mouseX;
         m_mouse3DStartY = mouseY;
         needRedraw = true;
        }
      //--- End rotation on button release
      else if(mouseState == 0 && m_previousMouseButtonState == 1)
        {
         m_isRotating3D = false;
         //--- Restore chart scroll on release
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
        }
     }

   //--- Handle button-press interactions
   if(mouseState == 1 && m_previousMouseButtonState == 0)
     {
      //--- Switch view mode when the icon is clicked
      if(m_isHoveringSwitchIcon)
        {
         switchViewMode();
         m_previousMouseButtonState = mouseState;
         return;
        }
      //--- Begin canvas drag when clicking the header (not a resize zone)
      else if(enableDragging && m_isHoveringHeader && !m_isHoveringResizeZone)
        {
         m_isDragging   = true;
         m_dragStartX   = mouseX;
         m_dragStartY   = mouseY;
         m_canvasStartX = m_currentPositionX;
         m_canvasStartY = m_currentPositionY;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
         needRedraw = true;
        }
      //--- Begin canvas resize when clicking a resize grip zone
      else if(m_isHoveringResizeZone)
        {
         m_isResizing          = true;
         m_activeResizeMode    = m_hoverResizeMode;
         m_resizeStartX        = mouseX;
         m_resizeStartY        = mouseY;
         m_resizeInitialWidth  = m_currentWidth;
         m_resizeInitialHeight = m_currentHeight;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
         needRedraw = true;
        }
     }
   //--- Continue drag or resize while button stays pressed
   else if(mouseState == 1 && m_previousMouseButtonState == 1)
     {
      if(m_isDragging)
         handleCanvasDrag(mouseX, mouseY);
      else if(m_isResizing)
         handleCanvasResize(mouseX, mouseY);
     }
   //--- End drag or resize on button release
   else if(mouseState == 0 && m_previousMouseButtonState == 1)
     {
      if(m_isDragging || m_isResizing)
        {
         m_isDragging       = false;
         m_isResizing       = false;
         m_activeResizeMode = NO_RESIZE;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
         needRedraw = true;
        }
     }

   //--- Redraw the visualization if any state changed
   if(needRedraw)
     {
      renderVisualization();
      ChartRedraw();
     }

   //--- Record current mouse position for next event
   m_lastMouseX = mouseX;
   m_lastMouseY = mouseY;
   //--- Record current button state for next event
   m_previousMouseButtonState = mouseState;
  }

//+------------------------------------------------------------------+
//| Handle mouse wheel zoom for the 3D scene                         |
//+------------------------------------------------------------------+
void handleMouseWheel(int mouseX, int mouseY, double delta)
  {
   //--- Determine if the wheel event occurred over the 3D canvas body
   bool isOverCanvas = (mouseX >= m_currentPositionX &&
                        mouseX <= m_currentPositionX + m_currentWidth &&
                        mouseY >= m_currentPositionY + HEADER_BAR_HEIGHT &&
                        mouseY <= m_currentPositionY + m_currentHeight);

   //--- Apply zoom only in 3D mode and when cursor is over the canvas
   if(m_currentViewMode == VIEW_3D_MODE && isOverCanvas)
     {
      //--- Suppress chart scroll so wheel is captured by the visualizer
      ChartSetInteger(0, CHART_MOUSE_SCROLL, false);

      //--- Adjust camera distance by a small fraction of the wheel delta
      m_cameraDistance *= 1.0 - delta * 0.001;

      //--- Clamp distance to prevent clipping through the scene
      if(m_cameraDistance <  20.0) m_cameraDistance =  20.0;
      if(m_cameraDistance > 200.0) m_cameraDistance = 200.0;

      //--- Re-render with updated camera distance
      renderVisualization();
      ChartRedraw();
     }
   else
     {
      //--- Restore chart scroll when wheel is outside the canvas
      ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
     }
  }

//+------------------------------------------------------------------+
//| Return true when the cursor is over the mode switch icon         |
//+------------------------------------------------------------------+
bool isMouseOverSwitchIcon(int mouseX, int mouseY)
  {
   //--- Compute the icon's left edge from the canvas right margin
   int iconX = m_currentPositionX + m_currentWidth - SWITCH_ICON_SIZE - SWITCH_ICON_MARGIN;
   //--- Vertically centre the icon within the header bar
   int iconY = m_currentPositionY + (HEADER_BAR_HEIGHT - SWITCH_ICON_SIZE) / 2;

   //--- Return true if the cursor falls within the icon bounding box
   return (mouseX >= iconX && mouseX <= iconX + SWITCH_ICON_SIZE &&
           mouseY >= iconY && mouseY <= iconY + SWITCH_ICON_SIZE);
  }

//+------------------------------------------------------------------+
//| Toggle between 2D and 3D view modes                              |
//+------------------------------------------------------------------+
void switchViewMode()
  {
   //--- Switch from 2D to 3D
   if(m_currentViewMode == VIEW_2D_MODE)
     {
      m_currentViewMode = VIEW_3D_MODE;
      Print("Switched to 3D mode");

      //--- Set up the 3D scene; revert to 2D on failure
      if(!setup3DMode())
        {
         Print("ERROR: Failed to setup 3D mode, reverting to 2D");
         m_currentViewMode = VIEW_2D_MODE;
        }
      else
        {
         //--- Auto-fit camera to the scene on mode entry
         if(autoFitCamera)
            autoFitCameraPosition();
        }
     }
   else
     {
      //--- Switch from 3D back to 2D
      m_currentViewMode = VIEW_2D_MODE;
      Print("Switched to 2D mode");
     }

   //--- Render the scene in the new mode immediately
   renderVisualization();
   ChartRedraw();
  }

Определим функцию "handleMouseEvent" для управления всеми взаимодействиями мыши в визуализаторе. Сохраним предыдущие состояния при наведении курсора, обновим "m_isHoveringCanvas", проверяя положение мыши относительно границ объекта Canvas, установим "m_isHoveringHeader" с помощью "isMouseOverHeaderBar", "m_isHoveringSwitchIcon" с помощью "isMouseOverSwitchIcon" и "m_isHoveringResizeZone" с помощью "isMouseInResizeZone".

Флаг перерисовки активируется, если изменяется какое-либо состояние наведения мыши. В режиме "VIEW_3D_MODE" над холстом, но не над заголовком, обработаем вращение: при нажатии установим "m_isRotating3D" в значение true, запишем начальные точки, отключим прокрутку с помощью "ChartSetInteger" для CHART_MOUSE_SCROLL. При перетаскивании регулируем "m_cameraAngleY" и "m_cameraAngleX" с помощью дельт, масштабированных на 300,0, ограничиваем угол по оси X в диапазоне от -0,49PI до 0,49PI, чтобы предотвратить перевороты. Обновим начальные точки и перерисуем флаг. При освобождении сбрасываем вращение и включаем прокрутку. Далее проверим нажатия: если над значком переключения, вызываем "switchViewMode" и возвращаемся после обновления состояния. Если перетаскивание возможно над заголовком без изменения размера, активируем перетаскивание с начальными точками и отключаем прокрутку; если над зоной изменения размера, включаем изменение размера, устанавливаем режим, захватываем начальные точки и отключаем прокрутку, отметим перерисовку. Для удерживаемой кнопки вызовем "handleCanvasDrag" или "handleCanvasResize". При отпускании кнопки сбросим флаги/режим и включим прокрутку, отметим перерисовку. При необходимости вызовем "renderVisualization" и ChartRedraw, затем обновим последнее значение курсора мыши и состояние.

Далее реализуем функцию "handleMouseWheel" для масштабирования в 3D. Проверим, находится ли курсор мыши над областью графика под заголовком в режиме "VIEW_3D_MODE", отключим прокрутку, умножим "m_cameraDistance" на 1,0 минус масштабированное значение для плавной регулировки, ограничим значение в диапазоне от 20,0 до 200,0 и выполняем повторную отрисовку. В противном случае включим прокрутку для навигации по графику.

Для обнаружения наведения курсора на кнопку переключения создадим функцию "isMouseOverSwitchIcon", вычисляющую координаты значка на основе текущих размеров и констант и возвращающую значение true, если курсор мыши находится внутри границ квадрата. И вот определим функцию "switchViewMode" для переключения режимов: если 2D, установим значение в "VIEW_3D_MODE", выводим сообщение о переключении, пытаемся выполнить "setup3DMode" и возвращаем/ выводим сообщение об ошибке в случае сбоя. В противном случае производим автоподбор положения камеры, если функция включена. Если используется 3D-формат, переключимся на 2D и выведем на экран. Затем визуализируем новый режим и перерисуем график для немедленного обновления. Теперь можно вызывать эти функции в обработчике событий графика.

//+------------------------------------------------------------------+
//| Route chart mouse and wheel events to the visualizer             |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   //--- Abort if the visualizer has not been initialised
   if(distributionVisualizer == NULL) return;

   //--- Handle mouse move and button events
   if(id == CHARTEVENT_MOUSE_MOVE)
     {
      int mouseX     = (int)lparam;                        // Horizontal cursor position in pixels
      int mouseY     = (int)dparam;                        // Vertical cursor position in pixels
      int mouseState = (int)StringToInteger(sparam);       // Bitmask of pressed mouse buttons

      distributionVisualizer.handleMouseEvent(mouseX, mouseY, mouseState);
     }

   //--- Handle mouse wheel scroll events
   if(id == CHARTEVENT_MOUSE_WHEEL)
     {
      //--- Unpack the cursor X from the low 16 bits of lparam
      int mouseX = (int)(short) lparam;
      //--- Unpack the cursor Y from the high 16 bits of lparam
      int mouseY = (int)(short)(lparam >> 16);

      distributionVisualizer.handleMouseWheel(mouseX, mouseY, dparam);
     }
  }

В обработчике OnChartEvent обработаем взаимодействия с графиком глобально, досрочно выходим, если "distributionVisualizer" равен NULL, чтобы избежать ошибок. Если идентификатор равен CHARTEVENT_MOUSE_MOVE, преобразуем параметры для получения координат и состояния мыши. Далее делегируем вызов функции "handleMouseEvent" в экземпляре визуализатора. Для "CHARTEVENT_MOUSE_WHEEL" извлечем скорректированные положения мыши из lparam bits и вызовем "handleMouseWheel" с дельтой, включая масштабирование с помощью колесика мыши в 3D. Теперь можно обновить обработчик событий деинициализации и тиков, чтобы изменения вступили в силу, используя тот же формат, что и ниже.

//+------------------------------------------------------------------+
//| Release all resources when the EA is removed                     |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Destroy the visualizer object if it still exists
   if(distributionVisualizer != NULL)
     {
      delete distributionVisualizer;
      distributionVisualizer = NULL;
     }

   //--- Redraw the chart to remove the canvas bitmap object
   ChartRedraw();
   Print("Distribution window deinitialized");
  }

//+------------------------------------------------------------------+
//| Reload distribution data on each new bar                         |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- Track the last processed bar open time across calls
   static datetime lastBarTimestamp = 0;
   //--- Read the current bar open time on the configured timeframe
   datetime currentBarTimestamp = iTime(_Symbol, chartTimeframe, 0);

   //--- Reload and redraw only when a new bar has formed
   if(currentBarTimestamp > lastBarTimestamp)
     {
      if(distributionVisualizer != NULL)
        {
         //--- Regenerate the binomial sample and statistics
         if(distributionVisualizer.loadDistributionData())
           {
            //--- Redraw the updated visualization
            distributionVisualizer.renderVisualization();
            ChartRedraw();
           }
        }
      //--- Update the bar timestamp to prevent repeated processing
      lastBarTimestamp = currentBarTimestamp;
     }
  }

В обработчике OnDeinit проверим, не равен ли "distributionVisualizer" значению NULL, удалим экземпляр для освобождения ресурсов, установим указатель в значение NULL, перерисуем график с помощью функции "ChartRedraw" и выведем сообщение о деинициализации. Далее, в обработчике OnTick используем статическую переменную "lastBarTimestamp" для отслеживания времени открытия предыдущего столбца и получим время открытия текущего столбца с помощью iTime с помощью символа, "chartTimeframe", и нулевого сдвига. Если обнаружен новый столбец, проверим существование визуализатора, перезагрузим данные с помощью функции "loadDistributionData", в случае успеха выполним повторную отрисовку с помощью функции "renderVisualization". Перерисуем график и обновим временную метку. Полный цикл ведения лога выглядит следующим образом.

INTERACTION CYCLE

На этом наша реализация 3D-визуализации завершена. Теперь остаётся проверить работоспособность системы, что и рассматривается в следующем разделе.


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

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

BACKTEST GIF

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


Заключение

В этой статье мы интегрировали DirectX 3D в инструмент просмотра биномиального распределения в MQL5, позволяющий переключать режимы 2D/3D и управлять камерой для поворота, масштабирования и автоподбора положения камеры. Мы визуализировали столбцы трёхмерной гистограммы с опорной плоскостью и цветовыми осями, спроецировали теоретическую кривую PMF в перспективное пространство и сохранили двухмерные элементы, такие как панели статистики, легенда и настраиваемые темы. Детали реализации включали архитектуру на основе классов, взаимодействие с мышью, обновление новых столбцов в реальном времени и настраиваемые входные параметры для испытаний, вероятность, размер выборки и параметры отображения. После ознакомления со статьей вы сможете:

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

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

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

Моделирование рынка: Первые шаги на SQL в MQL5 (V) Моделирование рынка: Первые шаги на SQL в MQL5 (V)
В предыдущей статье я показал, как следовало действовать для добавления механизма запросов. Это было нужно для того, чтобы внутри кода MQL5 вы могли полноценно использовать SQL и получать результаты при выполнении команды SQL SELECT FROM. Но осталось рассказать последнюю функцию, которую нам необходимо реализовать. Это функция DatabaseReadBind. И, поскольку для правильного понимания требуется чуть более развернутое объяснение, было решено сделать это не в той предыдущей статье, а в сегодняшней. Итак, поскольку тема будет довольно объемной, перейдём сразу к следующему разделу.
Нативная реализация RSA-шифрования на MQL5 Нативная реализация RSA-шифрования на MQL5
В MQL5 отсутствует встроенная асимметричная криптография, из-за чего безопасный обмен данными по незащищённым каналам вроде HTTP становится затруднительным. В этой статье представлена чистая реализация RSA на MQL5 с использованием схемы дополнения PKCS#1 v1.5, позволяющая безопасно передавать сеансовые ключи для AES и небольшие блоки данных без внешних библиотек. Такой подход обеспечивает уровень безопасности, похожий на HTTPS, поверх обычного HTTP и, более того, закрывает важный пробел в защищённой коммуникации для приложений MQL5.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Осваиваем графики Kagi в MQL5 (Часть I): Создание движка графика Kagi Осваиваем графики Kagi в MQL5 (Часть I): Создание движка графика Kagi
Узнайте, как создать полноценный движок графиков Kagi в MQL5: строить ценовые развороты, формировать динамические отрезки линий и обновлять структуру Kagi в реальном времени. В первой части показано, как отображать графики Kagi непосредственно в MetaTrader 5, давая трейдерам ясное представление о смене тренда и силе рынка и одновременно закладывая основу для автоматизированной торговой логики на базе Kagi во второй части.