English Русский
preview
MQL5-Handelswerkzeuge (Teil 22): Grafische Darstellung des Histogramms und der Wahrscheinlichkeitsfunktion (PMF) von der Binomialverteilung

MQL5-Handelswerkzeuge (Teil 22): Grafische Darstellung des Histogramms und der Wahrscheinlichkeitsfunktion (PMF) von der Binomialverteilung

MetaTrader 5Handel |
19 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Einführung

Sie verfügen über ein Handelssystem, das eine Folge von Gewinnen und Verlusten hervorbringt, können jedoch einfache, aber entscheidende Fragen nicht beantworten: „Wie hoch ist die Wahrscheinlichkeit, dass ich bei 30 Trades 20 Gewinn-Trades erziele, wenn meine historische Gewinnquote bei 75 % liegt?“ oder „Wie wahrscheinlich ist eine Verlustserie von 5 Trades?“ Ohne eine Möglichkeit, die Binomialverteilung zu modellieren, können Sie nur raten – Sie können Risiken nicht realistisch einschätzen, keine angemessenen Positionsgrößen festlegen und auch nicht überprüfen, ob die Performance Ihrer Strategie statistisch aussagekräftig ist. Dieser Mangel an Verständnis für Wahrscheinlichkeiten führt häufig zu einer zu hohen Hebelung, emotionalen Entscheidungen und unrealistischen Gewinnerwartungen. Dieser Artikel richtet sich an Entwickler, die mit MetaQuotes Language 5 (MQL5) arbeiten, sowie an algorithmische Händler, die das probabilistische Verhalten ihrer Handelsstrategien quantifizieren möchten.

In unserem vorherigen Artikel (Teil 21) haben wir das Grafik-Tool der Regression in MQL5 erweitert, indem wir das Farbschema Cyberpunk mit Neon-Effekten, Animationen und holografischen Effekten für eine eindrucksvolle Visualisierung hinzugefügt haben. In Teil 22 erstellen wir ein MQL5-Diagrammtool zur Visualisierung der Binomialverteilung anhand eines Histogramms aus simulierten Stichproben und der theoretischen Wahrscheinlichkeitsfunktion (PMF) auf einer interaktiven Zeichenfläche. Wir bieten erweiterte statistische Funktionen, darunter Mittelwert, Standardabweichung, Schiefe, Kurtosis, Perzentile und Konfidenzintervalle sowie anpassbare Farbschemata, Farbverläufe und Beschriftungen. Darüber hinaus ermöglichen wir das Ziehen, die Größenanpassung, Echtzeit-Aktualisierungen sowie die Anpassung von Parametern für Versuche, Wahrscheinlichkeit, Stichprobengröße und Darstellung und unterstützen so die Handelsanalyse. Wir werden die folgenden Themen behandeln:

  1. Das Konzept der Binomialverteilung verstehen
  2. Implementierung in MQL5
  3. Backtests
  4. Schlussfolgerung

Am Ende verfügen Sie über ein funktionsfähiges MQL5-Diagrammtool zur Visualisierung von Binomialverteilungen, das Sie ganz nach Ihren Wünschen anpassen können – legen wir los!


Das Konzept der Binomialverteilung verstehen

Die Binomialverteilung beschreibt die Anzahl der Erfolge bei einer festen Anzahl unabhängiger Versuche, bei denen jeweils die gleiche Erfolgswahrscheinlichkeit besteht. Im Handel kann ein Versuch jedes binäre Ereignis sein: Ein Trade endet entweder mit Gewinn (Erfolg) oder Verlust (Misserfolg), ein Signal ist gültig oder ungültig, oder eine Marktbedingung ist erfüllt oder nicht erfüllt. Wenn Ihre Strategie eine historische Gewinnquote von p aufweist, gibt die Binomialverteilung an, wie wahrscheinlich es ist, dass Sie rein zufällig k Gewinne bei n Trades erzielen. So können wir es auf dem Markt einsetzen:

  • Schätzung der Wahrscheinlichkeit, ein Gewinnziel zu erreichen: Nehmen wir an, Ihr System gewinnt in 60 % der Fälle. Wenn Sie vorhaben, im Monat 20 Trades zu tätigen, können Sie die Wahrscheinlichkeit berechnen, dass mindestens 12 davon Gewinne abwerfen – ein wertvoller Realitätscheck.
  • Realistische Erwartungen hinsichtlich der Drawdowns festlegen: Anhand der Verteilung können Sie die maximal wahrscheinliche Verlustserie abschätzen. Wenn die Wahrscheinlichkeit von fünf aufeinanderfolgenden Verlusten sehr gering ist, könnte eine Serie dieser Länge auf ein Versagen der Strategie hindeuten.
  • Vergleich von Strategien: Zwei Systeme können zwar ähnliche Gewinnquoten aufweisen, doch das System mit der engeren Verteilung (geringerer Varianz) ist zuverlässiger – dies lässt sich anhand des Histogramms und der Standardabweichung erkennen.
  • Überprüfen der Stichprobengröße: Die Konfidenzintervalle zeigen Ihnen, wie genau Ihre geschätzte Gewinnquote ist. Große Intervalle bedeuten, dass Sie mehr Trades benötigen, um sich von der tatsächlichen Leistungsfähigkeit des Systems überzeugen zu können.

Unser Plan sieht vor, Stichproben aus der Binomialverteilung unter Verwendung einer festgelegten Anzahl von Versuchen und einer festgelegten Erfolgswahrscheinlichkeit zu simulieren, ein Histogramm der empirischen Häufigkeiten zu berechnen, die theoretische Kurve der PMF zum Vergleich darzustellen und wichtige statistische Kennzahlen wie Mittelwert, Standardabweichung, Schiefe, Kurtosis, Perzentile und Konfidenzintervalle darzustellen. Wir werden dies auf einer in der Größe anpassbaren und verschiebbaren Zeichenfläche mit anpassbaren Farbschemata, Hintergrundverläufen, Achsenbeschriftungen und einer Legende darstellen und dabei Echtzeit-Aktualisierungen auf der Grundlage von Änderungen des Chart-Zeitrahmens ermöglichen, um eine interaktive Wahrscheinlichkeitsanalyse zu bieten. Kurz gesagt: Mit dieser Konfiguration steht ein Werkzeug zur Verfügung, mit dem sich Verteilungseigenschaften und deren Auswirkungen in Handelsszenarien untersuchen lassen. Sehen Sie, was wir erreichen werden.

FRAMEWORK FÜR DIAGRAMME ZUR BINOMVERTEILUNG

Bedeutung der Funktionen für den Handel

Bevor wir uns mit der Umsetzung befassen, wollen wir zunächst klären, warum jede Komponente dieses Tools aus Sicht eines Traders von Bedeutung ist, wie bereits zuvor erwähnt.

  • Histogramm (empirische Häufigkeiten): Es zeigt an, wie oft die jeweilige Anzahl an Erfolgen in Ihrer simulierten Stichprobe aufgetreten ist. Ein hoher Balken bei einem bestimmten k-Wert bedeutet, dass dieses Ergebnis häufig auftritt – dies ist das typische Verhalten Ihrer Strategie. Wenn das Histogramm breit verteilt ist, weisen die Ergebnisse Ihres Systems starke Schwankungen auf.
  • Theoretische PMF-Kurve: Die glatte Kurve stellt die tatsächlichen binomialen Wahrscheinlichkeiten dar. Wenn Sie sie über das Histogramm legen, sehen Sie sofort, ob Ihre simulierte Stichprobe der erwarteten Verteilung entspricht. Große Abweichungen könnten darauf hindeuten, dass Ihr Stichprobenumfang zu klein ist oder Ihre Annahme unabhängiger Versuche nicht erfüllt ist.
  • Mittelwert: Er zeigt die durchschnittliche Anzahl der Erfolge. Bei einem System mit einer Gewinnquote p und n Trades beträgt der Mittelwert n × p. Wenn Ihr tatsächlicher Mittelwert erheblich davon abweicht, kann Ihre Gewinnquote von Ihren Annahmen abweichen.
  • Standardabweichung: Sie misst die Streuung der Ergebnisse. Eine geringere Standardabweichung bedeutet, dass die Ergebnisse Ihres Systems konsistenter sind – was für das Risikomanagement von entscheidender Bedeutung ist.
  • Schiefe: Sie gibt an, ob die Verteilung symmetrisch ist oder nach links bzw. rechts tendiert. Eine positive Schiefe (langer rechter Schwanz) bedeutet gelegentliche sehr gute Serien; eine negative Schiefe warnt vor gelegentlichen verheerenden Verlustserien.
  • Kurtosis: Gibt an, wie anfällig die Verteilung für extreme Ergebnisse ist. Eine hohe Kurtosis (Fat Tails) deutet darauf hin, dass lange Gewinn- oder Verlustserien häufiger auftreten, als es eine Normalverteilung erwarten lassen würde – ein entscheidender Faktor bei der Festlegung von Stop-Loss-Limits.
  • Konfidenzintervalle (95 %, 99 %): Sie geben einen Bereich an, in den der tatsächliche Mittelwert der Gewinnquote Ihres Systems voraussichtlich fallen wird. Enge Intervalle geben Ihnen die Gewissheit, dass Ihre beobachtete Leistung zuverlässig ist; weite Intervalle deuten darauf hin, dass Sie mehr Daten benötigen.

Anhand dieser Statistiken blicken Sie über die reine Gewinnquote hinaus und beginnen, die Struktur Ihrer Handelsergebnisse zu verstehen – was für die Risikobewertung weitaus wertvoller ist. Beginnen wir nun mit der Umsetzung.


Implementierung in MQL5

Um das Programm in MQL5 zu erstellen, öffnen wir den MetaEditor, gehen zum Navigator, suchen den Ordner Experts, klicken auf die Registerkarte Neu und folgen den Anweisungen, um die Datei zu erstellen. Sobald sie erstellt ist, müssen wir in der Programmierumgebung einige Eingabeparameter und globale Variablen deklarieren, die wir im gesamten Programm verwenden werden.

Einrichtung der Canvas- und Kernbibliotheken

Um mit der Erstellung des Programms zu beginnen, binden wir zunächst die erforderlichen Bibliotheken ein und definieren unterstützende Strukturen.

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

#include <Canvas\Canvas.mqh>
#include <Math\Stat\Binomial.mqh>
#include <Math\Stat\Math.mqh>

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

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "=== DISTRIBUTION SETTINGS ==="
input int    numTrials = 40;                    // Number of trials (n) for binomial distribution
input double successProbability = 0.75;         // Success probability (p) per trial (0.0-1.0)
input int    sampleSize = 1000000;              // Sample size for generating histogram data
input int    histogramCells = 20;               // Number of cells (bins) in the histogram
input int    histogramGapPixels = 2;            // Gap in pixels between adjacent histogram bars
input ENUM_TIMEFRAMES chartTimeframe = PERIOD_CURRENT; // Timeframe used for new-bar detection

input group "=== CANVAS DISPLAY SETTINGS ==="
input int    initialCanvasX = 20;               // Initial X position of canvas on chart (pixels)
input int    initialCanvasY = 30;               // Initial Y position of canvas on chart (pixels)
input int    initialCanvasWidth = 600;          // Initial width of canvas in pixels
input int    initialCanvasHeight = 400;         // Initial height of canvas in pixels
input int    plotPadding = 10;                  // Internal padding around plot area in pixels

input group "=== THEME COLOR (SINGLE CONTROL!) ==="
input color  themeColor = clrDodgerBlue;        // Master theme color controlling all UI accents
input bool   showBorderFrame = true;            // Display decorative border frame around canvas

input group "=== HISTOGRAM AND CURVE SETTINGS ==="
input color  histogramColor = clrRed;           // Fill color for histogram bars
input color  theoreticalCurveColor = clrBlue;   // Color of theoretical probability mass function curve
input int    curveLineWidth = 2;                // Thickness in pixels of the theoretical curve

input group "=== BACKGROUND SETTINGS ==="
input bool   enableBackgroundFill = true;       // Enable gradient background fill inside canvas
input color  backgroundTopColor = clrWhite;     // Top color of the gradient background
input double backgroundOpacityLevel = 0.95;     // Background opacity level (0.0 fully transparent - 1.0 opaque)

input group "=== TEXT AND LABELS ==="
input int    titleFontSize = 14;                // Font size for main window title
input color  titleTextColor = clrBlack;         // Color of the main title text
input int    labelFontSize = 11;                // Font size for general labels and legend
input color  labelTextColor = clrBlack;         // Color of general label text
input int    axisLabelFontSize = 12;            // Font size for axis tick labels
input bool   showStatistics = true;             // Show statistics panel and legend

input group "=== STATS & LEGEND PANEL SETTINGS ==="
input int    statsPanelX = 70;                  // X position of statistics panel inside canvas
input int    statsPanelY = 10;                  // Y offset of statistics panel from header
input int    statsPanelWidth = 130;             // Width of statistics panel in pixels
input int    statsPanelHeight = 175;            // Height of statistics panel in pixels
input int    panelFontSize = 13;                // Font size used in stats and legend panels
input int    legendHeight = 35;                 // Height of legend panel in pixels

input group "=== INTERACTION SETTINGS ==="
input bool   enableDragging = true;             // Allow dragging canvas by clicking header
input bool   enableResizing = true;             // Allow resizing canvas with mouse grips
input int    resizeGripSize = 8;                // Size of resize grip detection zones in pixels

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
CCanvas mainCanvas;                                     // Main canvas object for all drawing
string canvasObjectName = "DistributionCanvas_Main";    // Name of the graphical object on chart
int currentPositionX = initialCanvasX;                  // Current X coordinate of canvas
int currentPositionY = initialCanvasY;                  // Current Y coordinate of canvas
int currentWidthPixels = initialCanvasWidth;            // Current canvas width in pixels
int currentHeightPixels = initialCanvasHeight;          // Current canvas height in pixels
bool isDraggingCanvas = false;                          // True while canvas is being dragged
bool isResizingCanvas = false;                          // True while canvas is being resized
int dragStartX = 0;                                     // Mouse X when drag started
int dragStartY = 0;                                     // Mouse Y when drag started
int canvasStartX = 0;                                   // Canvas X when drag started
int canvasStartY = 0;                                   // Canvas Y when drag started
int resizeStartX = 0;                                   // Mouse X when resize started
int resizeStartY = 0;                                   // Mouse Y when resize started
int resizeInitialWidth = 0;                             // Width when resize started
int resizeInitialHeight = 0;                            // Height when resize started
ResizeDirection activeResizeMode = NO_RESIZE;           // Currently active resize direction
ResizeDirection hoverResizeMode = NO_RESIZE;            // Hover resize direction
bool isHoveringCanvas = false;                          // Mouse is over canvas area
bool isHoveringHeader = false;                          // Mouse is over header bar
bool isHoveringResizeZone = false;                      // Mouse is over a resize grip
int lastMouseX = 0;                                     // Last recorded mouse X
int lastMouseY = 0;                                     // Last recorded mouse Y
int previousMouseButtonState = 0;                       // Previous mouse button state
const int MIN_CANVAS_WIDTH = 300;                       // Minimum allowed canvas width
const int MIN_CANVAS_HEIGHT = 200;                      // Minimum allowed canvas height
const int HEADER_BAR_HEIGHT = 35;                       // Fixed height of header bar
double sampleData[];                                    // Generated binomial sample values
double histogramIntervals[];                            // Center positions of histogram bins
double histogramFrequencies[];                          // Frequency count per histogram bin
double theoreticalXValues[];                            // X values for theoretical PMF
double theoreticalYValues[];                            // Probability values for theoretical PMF
double minDataValue = 0.0;                              // Minimum value in sample data
double maxDataValue = 0.0;                              // Maximum value in sample data
double maxFrequency = 0.0;                              // Highest histogram frequency
double maxTheoreticalValue = 0.0;                       // Highest theoretical probability
bool dataLoadedSuccessfully = false;                    // True after successful data load
double sampleMean = 0.0;                                // Sample mean
double sampleStandardDeviation = 0.0;                   // Sample standard deviation
double sampleSkewness = 0.0;                            // Sample skewness
double sampleKurtosis = 0.0;                            // Sample kurtosis
double percentile25 = 0.0;                              // 25th percentile
double percentile50 = 0.0;                              // 50th percentile (median)
double percentile75 = 0.0;                              // 75th percentile
double confidenceInterval95Lower = 0.0;                 // 95% CI lower bound
double confidenceInterval95Upper = 0.0;                 // 95% CI upper bound
double confidenceInterval99Lower = 0.0;                 // 99% CI lower bound
double confidenceInterval99Upper = 0.0;                 // 99% CI upper bound

Wir beginnen die Implementierung mit dem Einbinden der erforderlichen Bibliotheken: #include <Canvas\Canvas.mqh> für Canvas-basierte Grafiken, #include <Math\Stat\Binomial.mqh> zur Behandlung von Binomialverteilungsfunktionen und #include <Math\Stat\Math.mqh> für allgemeine mathematische und statistische Hilfsfunktionen, womit wir die Grundlage für Visualisierungen und Berechnungen schaffen. Als Nächstes definieren wir die Enumeration ResizeDirection, um die Optionen zur Größenanpassung der Zeichenfläche zu verwalten, darunter NO_RESIZE als Standardzustand, RESIZE_BOTTOM_EDGE für vertikale Anpassungen, RESIZE_RIGHT_EDGE für horizontale Änderungen sowie RESIZE_CORNER für die diagonale Größenanpassung, was später interaktive Benutzersteuerelemente unterstützen wird.

Anschließend definieren wir eine Reihe von Eingabeparametern, die über input group in Gruppen organisiert sind, um die Benutzeroberfläche in den Programmeinstellungen besser zu strukturieren. Dazu gehören Verteilungsparameter wie die Anzahl der Versuche, die Erfolgswahrscheinlichkeit, die Stichprobengröße, die Histogrammzellen, die Abstände zwischen den Balken und der Zeitrahmen des Charts; Anzeigeeinstellungen für die Zeichenfläche wie Anfangspositionen, Abmessungen und Innenabstand um den Plotbereich; eine Hauptthemenfarbe mit einstellbarer Sichtbarkeit der Rahmen; Farben für Histogramm und Kurven sowie die Kurvenbreite; Optionen für die Hintergrundfüllung mit Farbe und Deckkraft; Text- und Beschriftungsgrößen, Farben sowie die Umschaltfunktion für die Anzeige von Statistiken; Positionen, Größen und Schriftarten der Statistik- und Legendenbereiche; sowie Interaktionseinstellungen für das Ziehen, die Größenänderung und die Griffgröße, mit denen wir das Verhalten und das Erscheinungsbild des Tools anpassen können.

Schließlich initialisieren wir globale Variablen, um den Programmzustand zu verfolgen, beginnend mit der Instanz der Klasse CCanvas namens mainCanvas für die Darstellung, einer Zeichenkette für den Namen des Canvas-Objekts, aktuellen Positionen und Abmessungen, die auf die Standardwerte gesetzt sind, boolesche Flags für die Zustände Ziehen, Größenänderung und Hover, Koordinaten-Tracker für Zieh- und Größenänderungsvorgänge, die aktiven und Hover-Größenänderungsmodi unter Verwendung der Enumeration, Variablen zur Mausverfolgung, konstante Mindestgrößen für die Zeichenfläche und die Kopfzeilenhöhe, Arrays für Stichprobendaten, Histogrammintervalle und -häufigkeiten, theoretische Werte, Min-/Max-Tracker, ein Flag zum Laden von Daten sowie statistische Kennzahlen wie Mittelwert, Standardabweichung, Schiefe, Kurtosis, Perzentile und Grenzen des Konfidenzintervalls, um die Struktur für die Datenverarbeitung und die interaktive Darstellung vorzubereiten. Mit diesen Bibliotheken können wir sofort von theoretischen Formeln zu Live-Visualisierungen übergehen. Nun werden wir einige Hilfsfunktionen definieren, um den Code modular zu gestalten und so die Verwaltung zu vereinfachen. Wir beginnen mit den Hilfsfunktionen für die Themenfarben.

//+------------------------------------------------------------------+
//| Lighten the base color                                           |
//+------------------------------------------------------------------+
color LightenColor(color baseColor, double factor)
  {
   //--- Extract red component from base color
   uchar r = (uchar)((baseColor >> 16) & 0xFF);
   //--- Extract green component from base color
   uchar g = (uchar)((baseColor >> 8) & 0xFF);
   //--- Extract blue component from base color
   uchar b = (uchar)(baseColor & 0xFF);
   
   //--- Lighten red component
   r = (uchar)MathMin(255, r + (255 - r) * factor);
   //--- Lighten green component
   g = (uchar)MathMin(255, g + (255 - g) * factor);
   //--- Lighten blue component
   b = (uchar)MathMin(255, b + (255 - b) * factor);
   
   //--- Reassemble ARGB and return lightened color
   return (r << 16) | (g << 8) | b;
  }

//+------------------------------------------------------------------+
//| Darken the base color                                            |
//+------------------------------------------------------------------+
color DarkenColor(color baseColor, double factor)
  {
   //--- Extract red component from base color
   uchar r = (uchar)((baseColor >> 16) & 0xFF);
   //--- Extract green component from base color
   uchar g = (uchar)((baseColor >> 8) & 0xFF);
   //--- Extract blue component from base color
   uchar b = (uchar)(baseColor & 0xFF);
   
   //--- Darken red component
   r = (uchar)(r * (1.0 - factor));
   //--- Darken green component
   g = (uchar)(g * (1.0 - factor));
   //--- Darken blue component
   b = (uchar)(b * (1.0 - factor));
   
   //--- Reassemble ARGB and return darkened color
   return (r << 16) | (g << 8) | b;
  }

Um Abweichungen vom Farbschema zu berücksichtigen, erstellen wir die Funktion LightenColor, die eine Grundfarbe und einen Faktor als Parameter entgegennimmt, um einen helleren Farbton zu erzeugen. Sie extrahiert die roten, grünen und blauen Komponenten mithilfe von Bitverschiebungsoperationen und Masken, passt diese anschließend jeweils an, indem sie einen Teil der verbleibenden Intensität zu 255 multipliziert mit dem Faktor addiert, den Wert mit MathMin auf 255 begrenzt und sie schließlich mithilfe von Bitverschiebungen wieder zu einem Farbwert zusammenführt. Ebenso definieren wir die Funktion DarkenColor, um eine dunklere Version zu erzeugen, indem wir die RGB-Komponenten auf dieselbe Weise extrahieren, sie anschließend jeweils mit 1 minus dem Faktor multiplizieren, um die Intensität zu verringern, und schließlich die wieder zusammengesetzte Farbe zurückgeben. Diese Hilfsfunktionen ermöglichen eine dynamische Farbgebung für Elemente wie Kopfzeilen, Rahmen und Hintergründe auf Basis der Master-Themenfarbe. Wir können nun Hilfsfunktionen für mathematische Berechnungen definieren.

Funktionen zur Berechnung statistischer Werte

Da wir mehr als nur ein hübsches Bild brauchen, implementieren wir eine ganze Reihe statistischer Hilfsfunktionen.

//+------------------------------------------------------------------+
//| Calculate mean of array                                          |
//+------------------------------------------------------------------+
double CalculateMean(const double &data[])
  {
   //--- Get current array size
   int size = ArraySize(data);
   //--- Return zero for empty array
   if (size == 0) return 0.0;
   
   //--- Initialize running sum
   double sum = 0.0;
   //--- Accumulate every element
   for (int i = 0; i < size; i++)
     {
      sum += data[i];
     }
   //--- Return arithmetic mean
   return sum / size;
  }

//+------------------------------------------------------------------+
//| Calculate standard deviation of array                            |
//+------------------------------------------------------------------+
double CalculateStandardDeviation(const double &data[], double mean)
  {
   //--- Get current array size
   int size = ArraySize(data);
   //--- Return zero for insufficient data
   if (size <= 1) return 0.0;
   
   //--- Initialize sum of squared differences
   double sumSquaredDiff = 0.0;
   //--- Loop through all values
   for (int i = 0; i < size; i++)
     {
      //--- Compute deviation from mean
      double diff = data[i] - mean;
      //--- Accumulate squared deviation
      sumSquaredDiff += diff * diff;
     }
   //--- Return sample standard deviation
   return MathSqrt(sumSquaredDiff / (size - 1));
  }

//+------------------------------------------------------------------+
//| Calculate skewness of array                                      |
//+------------------------------------------------------------------+
double CalculateSkewness(const double &data[], double mean, double stdDev)
  {
   //--- Get current array size
   int size = ArraySize(data);
   //--- Return zero for insufficient data or zero std dev
   if (size < 3 || stdDev == 0.0) return 0.0;
   
   //--- Initialize sum of cubed standardized differences
   double sumCubedDiff = 0.0;
   //--- Loop through all values
   for (int i = 0; i < size; i++)
     {
      //--- Standardize the deviation
      double diff = (data[i] - mean) / stdDev;
      //--- Accumulate cubed term
      sumCubedDiff += diff * diff * diff;
     }
   
   //--- Cast size to double for formula
   double n = (double)size;
   //--- Apply bias-corrected skewness formula and return
   return (n / ((n - 1) * (n - 2))) * sumCubedDiff;
  }

//+------------------------------------------------------------------+
//| Calculate kurtosis of array                                      |
//+------------------------------------------------------------------+
double CalculateKurtosis(const double &data[], double mean, double stdDev)
  {
   //--- Get current array size
   int size = ArraySize(data);
   //--- Return zero for insufficient data or zero std dev
   if (size < 4 || stdDev == 0.0) return 0.0;
   
   //--- Initialize sum of fourth powers of standardized differences
   double sumFourthPower = 0.0;
   //--- Loop through all values
   for (int i = 0; i < size; i++)
     {
      //--- Standardize the deviation
      double diff = (data[i] - mean) / stdDev;
      //--- Square the standardized value
      double squared = diff * diff;
      //--- Accumulate fourth power
      sumFourthPower += squared * squared;
     }
   
   //--- Cast size to double
   double n = (double)size;
   //--- First part of excess kurtosis formula
   double kurtosis = (n * (n + 1) / ((n - 1) * (n - 2) * (n - 3))) * sumFourthPower;
   //--- Subtract bias correction term
   kurtosis -= (3 * (n - 1) * (n - 1)) / ((n - 2) * (n - 3));
   
   //--- Return kurtosis value
   return kurtosis;
  }

//+------------------------------------------------------------------+
//| Calculate percentile of array                                    |
//+------------------------------------------------------------------+
double CalculatePercentile(double &data[], double percentile)
  {
   //--- Get current array size
   int size = ArraySize(data);
   //--- Return zero for empty array
   if (size == 0) return 0.0;
   
   //--- Prepare sorted copy
   double sortedData[];
   //--- Resize to match original
   ArrayResize(sortedData, size);
   //--- Copy original data
   ArrayCopy(sortedData, data);
   //--- Sort in ascending order
   ArraySort(sortedData);
   
   //--- Compute rank for interpolation
   double rank = (percentile / 100.0) * (size - 1);
   //--- Lower index for interpolation
   int lowerIndex = (int)MathFloor(rank);
   //--- Upper index for interpolation
   int upperIndex = (int)MathCeil(rank);
   
   //--- Exact match case
   if (lowerIndex == upperIndex)
     {
      return sortedData[lowerIndex];
     }
   
   //--- Compute interpolation fraction
   double fraction = rank - lowerIndex;
   //--- Linear interpolation and return
   return sortedData[lowerIndex] + fraction * (sortedData[upperIndex] - sortedData[lowerIndex]);
  }

//+------------------------------------------------------------------+
//| Calculate confidence interval                                    |
//+------------------------------------------------------------------+
void CalculateConfidenceInterval(double mean, double stdDev, int in_sampleSize, double zScore, 
                                 double &lowerBound, double &upperBound)
  {
   //--- Compute margin of error
   double marginOfError = zScore * (stdDev / MathSqrt(in_sampleSize));
   //--- Set lower bound
   lowerBound = mean - marginOfError;
   //--- Set upper bound
   upperBound = mean + marginOfError;
  }

//+------------------------------------------------------------------+
//| Compute advanced statistics                                      |
//+------------------------------------------------------------------+
void ComputeAdvancedStatistics()
  {
   //--- Calculate sample mean
   sampleMean = CalculateMean(sampleData);
   //--- Calculate sample standard deviation
   sampleStandardDeviation = CalculateStandardDeviation(sampleData, sampleMean);
   
   //--- Calculate skewness
   sampleSkewness = CalculateSkewness(sampleData, sampleMean, sampleStandardDeviation);
   //--- Calculate kurtosis
   sampleKurtosis = CalculateKurtosis(sampleData, sampleMean, sampleStandardDeviation);
   
   //--- Calculate first quartile
   percentile25 = CalculatePercentile(sampleData, 25.0);
   //--- Calculate median
   percentile50 = CalculatePercentile(sampleData, 50.0);
   //--- Calculate third quartile
   percentile75 = CalculatePercentile(sampleData, 75.0);
   
   //--- Compute 95% confidence interval
   CalculateConfidenceInterval(sampleMean, sampleStandardDeviation, sampleSize, 1.96, 
                               confidenceInterval95Lower, confidenceInterval95Upper);
   //--- Compute 99% confidence interval
   CalculateConfidenceInterval(sampleMean, sampleStandardDeviation, sampleSize, 2.576, 
                               confidenceInterval99Lower, confidenceInterval99Upper);
  }

Zunächst definieren wir die Funktion CalculateMean, um den Mittelwert des Datensatzes zu berechnen. Die Funktion ermittelt die Größe des Arrays mit ArraySize und prüft, ob das Array leer ist; ist dies der Fall, gibt sie den Wert Null zurück. Danach wird eine Summenvariable initialisiert, es wird jede einzelne Element durchlaufen, um die Werte zu addieren, und schließlich wird die Summe durch die Anzahl der Elemente geteilt, um den Mittelwert zurückzugeben. Als Nächstes implementieren wir die Funktion CalculateStandardDeviation, die die Streuung der Daten um den angegebenen Mittelwert misst. Nachdem die Größe ermittelt und Fälle mit einem oder weniger Elementen durch die Rückgabe von Null behandelt wurden, wird in einer Schleife die Summe der quadrierten Abweichungen vom Mittelwert berechnet; anschließend wird mithilfe von MathSqrt die Quadratwurzel aus dieser Summe geteilt durch die Größe minus eins gebildet, wodurch sich die Stichprobenstandardabweichung ergibt.

Um die Asymmetrie in der Datenverteilung zu bewerten, erstellen wir die Funktion calculateSkewness. Zunächst wird die Eingabe auf mindestens drei Elemente und eine Standardabweichung ungleich Null überprüft; andernfalls wird Null zurückgegeben. Innerhalb einer Schleife wird jede Abweichung vom Mittelwert durch Division durch die Standardabweichung normiert, auf die dritte Potenz erhoben und die Summe dieser Werte gebildet. Die endgültige Schiefe wird anhand der Formel n / ((n-1)*(n-2)), multipliziert mit der Summe der normierten Differenzen zum dritten Potenz, wobei n die auf den Typ double umgewandelte Größe des Datensatzes ist. Dies liefert ein Maß dafür, ob die Verteilung stärker nach links (negativ) oder nach rechts (positiv) tendiert, was entscheidend für das Verständnis potenzieller Verzerrungen bei binomialen Ergebnissen wie ungleichmäßigen Erfolgswahrscheinlichkeiten ist.

In ähnlicher Weise definieren wir zur Bewertung der Wölbung der Verteilung im Vergleich zu einer Normalverteilung die Funktion calculateKurtosis. Die Funktion erfordert mindestens vier Elemente und eine Standardabweichung ungleich Null; andernfalls wird Null zurückgegeben. Die Schleife berechnet normierte Differenzen, quadriert diese und summiert die vierten Potenzen. Die Kurtosis wird dann aus der Formel (n*(n+1) / ((n-1)(n-2)(n-3))) * Summe der vierten Potenzen minus (3*(n-1)^2 / ((n-2)*(n-3))), was auf leptokurtische (positive, stärkere Tails) oder platykurtische (negative, schwächere Tails) Verteilungen hinweist. Dies hilft bei der Bewertung von Ausreißerrisiken in Handelsszenarien, die durch die Binomialverteilung modelliert werden.

Anschließend fügen wir die Funktion calculatePercentile hinzu, um ein bestimmtes Perzentil in den Daten zu ermitteln. Es sortiert eine Kopie des Arrays mithilfe von ArrayResize, ArrayCopy und ArraySort, berechnet den Rang als (Perzentil/100) mal (Größe-1) und führt bei Bedarf mit MathFloor und MathCeil eine Interpolation zwischen dem unteren und dem oberen Index durch, wodurch Quartile zur Zusammenfassung der Datenstreuung ermöglicht werden. Zur Schätzung von Populationsparametern führen wir die void-Funktion calculateConfidenceInterval ein, die den Fehlerbereich als Produkt aus dem z-Wert und der Standardabweichung, geteilt durch die Quadratwurzel der Stichprobengröße (unter Verwendung von MathSqrt), berechnet und anschließend die Unter- und Obergrenze festlegt, indem sie diesen Fehlerbereich vom Mittelwert abzieht bzw. zu diesem addiert.

Abschließend fassen wir diese Berechnungen in der Funktion computeAdvancedStatistics zusammen, die die oben genannten Funktionen nacheinander aufruft, um globale Variablen zu füllen: zunächst Mittelwert und Standardabweichung, dann anhand dieser Werte Schiefe und Kurtosis, gefolgt vom 25., 50. und 75. Perzentil sowie die Konfidenzintervalle für 95 % (z = 1,96) und 99 % (z = 2,576), wodurch die statistische Analyse für die simulierten binomialen Stichproben zentralisiert wird. Sie können diese Werte nach Bedarf anpassen, auch wenn es sich hierbei um die gebräuchlichsten Standardwerte handelt. Wir werden nun mit der Berechnung des Histogramms und der Initialisierung der Zeichenfläche fortfahren.

Datengenerierung und Histogrammberechnung

Nun laden wir die eigentlichen Binomialstichproben und erstellen das Histogramm sowie die theoretische Kurve.

//+------------------------------------------------------------------+
//| Create distribution canvas                                       |
//+------------------------------------------------------------------+
bool CreateCanvas()
  {
   //--- Attempt to create bitmap label canvas
   if (!mainCanvas.CreateBitmapLabel(0, 0, canvasObjectName, 
       currentPositionX, currentPositionY, currentWidthPixels, currentHeightPixels, 
       COLOR_FORMAT_ARGB_NORMALIZE))
     {
      //--- Creation failed
      return false;
     }
   //--- Canvas created successfully
   return true;
  }

//+------------------------------------------------------------------+
//| Load distribution data                                           |
//+------------------------------------------------------------------+
bool LoadDistributionData()
  {
   //--- Seed random generator with current tick count
   MathSrand(GetTickCount());

   //--- Resize sample array to requested size
   ArrayResize(sampleData, sampleSize);
   //--- Generate binomial random samples
   MathRandomBinomial(numTrials, successProbability, sampleSize, sampleData);

   //--- Compute histogram from samples
   if (!ComputeHistogram(sampleData, histogramIntervals, histogramFrequencies, maxDataValue, minDataValue, histogramCells))
     {
      //--- Histogram calculation failed
      Print("ERROR: Failed to calculate histogram");
      //--- Return failure
      return false;
     }

   //--- Resize theoretical arrays
   ArrayResize(theoreticalXValues, numTrials + 1);
   ArrayResize(theoreticalYValues, numTrials + 1);
   //--- Fill X values from 0 to n
   MathSequence(0, numTrials, 1, theoreticalXValues);
   //--- Compute binomial PMF values
   MathProbabilityDensityBinomial(theoreticalXValues, numTrials, successProbability, false, theoreticalYValues);

   //--- Find maximum histogram frequency
   maxFrequency = histogramFrequencies[ArrayMaximum(histogramFrequencies)];
   //--- Find maximum theoretical probability
   maxTheoreticalValue = theoreticalYValues[ArrayMaximum(theoreticalYValues)];

   //--- Compute scaling factor to match histogram height
   double scaleFactor = maxFrequency / maxTheoreticalValue;
   //--- Scale theoretical frequencies to visual match
   for (int i = 0; i < histogramCells; i++)
     {
      histogramFrequencies[i] /= scaleFactor;
     }

   //--- Compute all advanced statistics
   ComputeAdvancedStatistics();

   //--- Mark data as ready
   dataLoadedSuccessfully = true;
   //--- Log success
   Print("SUCCESS: Loaded binomial distribution data with advanced statistics");
   //--- Return success
   return true;
  }

//+------------------------------------------------------------------+
//| Compute histogram array with forced range                        |
//+------------------------------------------------------------------+
bool ComputeHistogram(const double &data[], double &intervals[], double &frequency[], double &maxv, double &minv, const int cells = 10)
  {
   //--- Invalid cell count aborts
   if (cells <= 1) return false;
   //--- Get data length
   int size = ArraySize(data);
   //--- Empty data aborts
   if (size < 1) return false;
   
   //--- Force histogram range from 0 to n
   minv = 0;
   maxv = numTrials;
   //--- Compute total range
   double range = maxv - minv;
   //--- Compute bin width
   double width = range / cells;
   //--- Zero width aborts
   if (width == 0) return false;
   
   //--- Resize output arrays
   ArrayResize(intervals, cells);
   ArrayResize(frequency, cells);
   
   //--- Initialize interval centers and zero frequencies
   for (int i = 0; i < cells; i++)
     {
      intervals[i] = minv + (i + 0.5) * width;
      frequency[i] = 0;
     }
   
   //--- Bin each sample value
   for (int i = 0; i < size; i++)
     {
      //--- Current value
      double val = data[i];
      //--- Skip out-of-range values
      if (val < minv || val > maxv) continue;
      //--- Compute bin index
      int ind = (int)((val - minv) / width);
      //--- Clamp index to valid range
      if (ind >= cells) ind = cells - 1;
      if (ind < 0) ind = 0;
      //--- Increment frequency
      frequency[ind]++;
     }
   //--- Histogram computed successfully
   return true;
  }

Wir definieren die Funktion CreateCanvas, um die grafische Zeichenfläche zu initialisieren. Es ruft die Methode CreateBitmapLabel für das Objekt mainCanvas auf. Die Chart-ID lautet Null für das Hauptdiagramm und Null für das Unterfenster. Zu den Parametern gehören der Name des Zeichenflächen-Objekts, die aktuellen Positionen, die Abmessungen sowie COLOR_FORMAT_ARGB_NORMALIZE für die Unterstützung von Farben mit Alpha-Durchsicht. Dies ermöglicht eine transparente und mehrschichtige Darstellung. Wenn die Erstellung fehlschlägt, wird false zurückgegeben. Andernfalls gibt die Funktion true zurück, wodurch sichergestellt wird, dass die Zeichenfläche für Zeichenvorgänge bereit ist.

Als Nächstes implementieren wir die Funktion loadDistributionData, um Daten für die Binomialverteilung vorzubereiten. Wir initialisieren den Zufallszahlengenerator mit MathSrand unter Verwendung der FunktionGetTickCount. Dadurch werden bei jedem Durchlauf abwechslungsreiche Simulationen gewährleistet. Wir passen die Größe des Arrays sampleData mithilfe von ArrayResize an die Größe der Eingabedaten an. Anschließend generieren wir mit MathRandomBinomial binomiale Zufallsstichproben auf der Grundlage der Anzahl der Versuche, der Erfolgswahrscheinlichkeit und der Stichprobengröße. Dadurch wird das Array mit simulierten Erfolgszahlen gefüllt. Danach rufen wir ComputeHistogram auf, um die Daten in Intervalle und Häufigkeiten zu gruppieren, die Minimal- und Maximalwerte zu aktualisieren und Fehler zu behandeln, indem wir eine Fehlermeldung ausgeben und false zurückgeben. Für die theoretischen Werte passen wir die Größe der Arrays theoreticalXValues und theoreticalYValues so an, dass sie den Bereich von Null bis zur Anzahl der Versuche plus eins abdecken.

Wir füllen X mithilfe von MathSequence mit ganzen Zahlen und berechnen die Wahrscheinlichkeitsdichtewerte mit MathProbabilityDensityBinomial im nicht-kumulativen Modus. Mit der Funktion ArrayMaximumermitteln wir die maximale Frequenz und den theoretischen Wert. Danach berechnen wir einen Skalierungsfaktor, um die empirischen Häufigkeiten an die theoretischen Wahrscheinlichkeiten anzupassen. Zum visuellen Vergleich dividieren wir in einer Schleife jede Frequenz durch diesen Faktor. Abschließend rufen wir computeAdvancedStatistics auf, um deskriptive Kennzahlen zu ermitteln. Wir setzen das Flag für den Datenladezustand auf true, geben eine Erfolgsmeldung aus und geben true zurück, womit die Datenaufbereitung für die Darstellung abgeschlossen ist.

Um die Erstellung eines Histogramms zu unterstützen, definieren wir die Funktion ComputeHistogram, die die Stichprobendaten in Häufigkeitsverteilungen mit festgelegten Klassen umwandelt, deren Bereich für binomiale Ergebnisse geeignet ist. Zunächst wird geprüft, ob die Zellanzahl ungültig ist oder leere Daten vorliegen; ist dies der Fall, wird false zurückgegeben. Wir legen den Minimalwert fest auf Null und den Maximalwert auf die Anzahl der Versuche fest, berechnen den Bereich und die Schrittweite und geben false zurück, wenn die Schrittweite Null ist, um Probleme bei der Division zu vermeiden. Nachdem wir die Größe der Arrays intervals und frequencies mit ArrayResize angepasst haben, setzen wir in einer Schleife jedes Intervall auf die Mitte seines Bins, indem wir die Hälfte der Breite zum Minimalwert addieren sowie den Index multipliziert mit der Breite, und setzen frequencies auf Null.

Anschließend lassen wir für jeden Datenpunkt Werte außerhalb des Bereichs außer Acht, berechnen den Bin-Index, indem wir das Minimum subtrahieren und durch die Breite dividieren, begrenzen ihn auf den Bereich zwischen Null und Zellen minus eins und erhöhen die entsprechende Häufigkeit. Dieser Ansatz gewährleistet ein diskretes, gleichmäßig unterteiltes Histogramm, das auf binomiale ganze Zahlen von null bis n zugeschnitten ist, wobei n die Anzahl der Versuche ist. Dies erleichtert die genaue Visualisierung der empirischen Verteilung im Vergleich zur theoretischen Kurve und gibt bei Erfolg den Wert true zurück. Wir können das so initialisieren, dass es die Berechnungen durchführt.

//+------------------------------------------------------------------+
//| Initialize expert                                                |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Copy initial X position from user inputs
   currentPositionX = initialCanvasX;
   //--- Copy initial Y position from user inputs
   currentPositionY = initialCanvasY;
   //--- Copy initial width from user inputs
   currentWidthPixels = initialCanvasWidth;
   //--- Copy initial height from user inputs
   currentHeightPixels = initialCanvasHeight;

   //--- Attempt to create the canvas object
   if (!CreateCanvas())
     {
      //--- Log failure if canvas creation fails
      Print("ERROR: Failed to create distribution canvas");
      //--- Return failure code to stop expert
      return(INIT_FAILED);
     }

   //--- Attempt to load binomial distribution data
   if (!LoadDistributionData())
     {
      //--- Log failure if data loading fails
      Print("ERROR: Failed to load distribution data");
      //--- Return failure code to stop expert
      return(INIT_FAILED);
     }

   //--- Force immediate chart redraw after initialization
   ChartRedraw();

   //--- Return success code so expert starts normally
   return(INIT_SUCCEEDED);
  }

In OnInit weisen wir globalen Variablen Eingabewerte für die Einrichtung der Zeichenfläche zu, wobei wir currentPositionX auf initialCanvasX, currentPositionY auf initialCanvasY, currentWidthPixels auf initialCanvasWidth und currentHeightPixels auf initialCanvasHeight setzen, um die Zeichenfläche entsprechend den Vorgaben des Benutzers zu positionieren und zu skalieren. Anschließend rufen wir die Funktion CreateCanvas auf, um das Bitmap-Label zu initialisieren. Falls diese Funktion den Wert false zurückgibt, geben wir eine Fehlermeldung aus und kehren mit dem Wert INIT_FAILED zurück, um das Programm zu beenden. Als Nächstes rufen wir die Funktion loadDistributionData auf, um Stichproben zu generieren und Daten vorzubereiten. Bei einem Fehler wird eine Fehlermeldung ausgegeben und INIT_FAILED zurückgegeben. Schließlich aktualisieren wir das Chart mit ChartRedraw und geben INIT_SUCCEEDED zurück, um die erfolgreiche Initialisierung anzuzeigen. Nach dem Kompilieren ergibt sich folgendes Bild.

INITIALISIERUNG

Da nun alle Initialisierungen und Berechnungen abgeschlossen sind, wollen wir nun das Analyse-Diagramm darstellen. Zuerst zeichnen wir die Zeichenfläche.

Darstellungsfunktionen – Hintergrund, Header und Hauptdiagramm

Sobald die Daten vorliegen, zeichnen wir die visuellen Ebenen.

//+------------------------------------------------------------------+
//| Draw gradient background                                         |
//+------------------------------------------------------------------+
void DrawGradientBackground()
  {
   //--- Compute bottom gradient color from theme
   color bottomColor = LightenColor(themeColor, 0.85);
   
   //--- Loop through every row below the header
   for (int y = HEADER_BAR_HEIGHT; y < currentHeightPixels; y++)
     {
      //--- Compute vertical gradient interpolation factor
      double gradientFactor = (double)(y - HEADER_BAR_HEIGHT) / (currentHeightPixels - HEADER_BAR_HEIGHT);
      //--- Interpolate color for current row
      color currentRowColor = InterpolateColors(backgroundTopColor, bottomColor, gradientFactor);
      //--- Compute alpha channel value
      uchar alphaChannel = (uchar)(255 * backgroundOpacityLevel);
      //--- Convert interpolated color to ARGB format
      uint argbColor = ColorToARGB(currentRowColor, alphaChannel);

      //--- Fill entire row with current color
      for (int x = 0; x < currentWidthPixels; x++)
        {
         //--- Set pixel at current position
         mainCanvas.PixelSet(x, y, argbColor);
        }
     }
  }

//+------------------------------------------------------------------+
//| Draw canvas border                                               |
//+------------------------------------------------------------------+
void DrawCanvasBorder()
  {
   //--- Skip drawing if border is disabled in settings
   if (!showBorderFrame) return;

   //--- Choose border color based on resize hover state
   color borderColor = isHoveringResizeZone ? DarkenColor(themeColor, 0.2) : themeColor;
   //--- Convert border color to ARGB
   uint argbBorder = ColorToARGB(borderColor, 255);

   //--- Draw outer border rectangle
   mainCanvas.Rectangle(0, 0, currentWidthPixels - 1, currentHeightPixels - 1, argbBorder);
   //--- Draw inner border rectangle
   mainCanvas.Rectangle(1, 1, currentWidthPixels - 2, currentHeightPixels - 2, argbBorder);
  }

//+------------------------------------------------------------------+
//| Draw header bar                                                  |
//+------------------------------------------------------------------+
void DrawHeaderBar()
  {
   //--- Declare variable for header background color
   color headerColor;
   //--- Choose color when canvas is being dragged
   if (isDraggingCanvas)
     {
      headerColor = DarkenColor(themeColor, 0.1);
     }
   //--- Choose color when mouse hovers header
   else if (isHoveringHeader)
     {
      headerColor = LightenColor(themeColor, 0.4);
     }
   //--- Default header color when idle
   else
     {
      headerColor = LightenColor(themeColor, 0.7);
     }
   //--- Convert header color to ARGB
   uint argbHeader = ColorToARGB(headerColor, 255);

   //--- Fill header bar area
   mainCanvas.FillRectangle(0, 0, currentWidthPixels - 1, HEADER_BAR_HEIGHT, argbHeader);

   //--- Draw header borders only if enabled
   if (showBorderFrame)
     {
      //--- Convert theme color for borders
      uint argbBorder = ColorToARGB(themeColor, 255);
      //--- Draw outer header border
      mainCanvas.Rectangle(0, 0, currentWidthPixels - 1, HEADER_BAR_HEIGHT, argbBorder);
      //--- Draw inner header border
      mainCanvas.Rectangle(1, 1, currentWidthPixels - 2, HEADER_BAR_HEIGHT - 1, argbBorder);
     }

   //--- Set bold font for title
   mainCanvas.FontSet("Arial Bold", titleFontSize);
   //--- Convert title text color to ARGB
   uint argbText = ColorToARGB(titleTextColor, 255);

   //--- Format dynamic title with current parameters
   string titleText = StringFormat("Binomial Distribution (n=%d, p=%.2f)", numTrials, successProbability);
   //--- Draw centered title text
   mainCanvas.TextOut(currentWidthPixels / 2, (HEADER_BAR_HEIGHT - titleFontSize) / 2, 
                      titleText, argbText, TA_CENTER);
  }

//+------------------------------------------------------------------+
//| Render distribution visualization                                |
//+------------------------------------------------------------------+
void RenderVisualization()
  {
   //--- Clear entire canvas before drawing
   mainCanvas.Erase(0);

   //--- Draw gradient background if option is enabled
   if (enableBackgroundFill)
     {
      DrawGradientBackground();
     }

   //--- Draw canvas border frame
   DrawCanvasBorder();
   //--- Draw header bar with title
   DrawHeaderBar();

   //--- Push all drawing operations to screen
   mainCanvas.Update();
  }

Hier definieren wir die Funktion drawGradientBackground, um die Zeichenfläche mit einem vertikalen Farbverlauf zu füllen, der unterhalb der Kopfzeile beginnt. Wir berechnen die Hintergrundfarbe, indem wir themeColor mit LightenColor aufhellen, anschließend in einer Schleife über die Zeilen gehen, um einen Faktor zu berechnen, die Farben mit InterpolateColors interpolieren, die Deckkraft über ColorToARGB anwenden und die Pixel Zeile für Zeile mithilfe der PixelSet-Methodesetzen, um einen subtilen Überblendungseffekt zu erzielen. Als Nächstes erstellen wir die Funktion drawCanvasBorder, um einen Rahmen hinzuzufügen, wenn showBorderFrame aktiviert ist. Dabei wählen wir mit DarkenColor eine abgedunkelte Farbe aus, wenn der Mauszeiger bei einer Größenänderung über das Element bewegt wird, wandeln diese in ARGB um und zeichnen mit der Rectangle-Methodeinnere und äußere Rechtecke, um ein umrandetes Erscheinungsbild zu erzielen.

Für den oberen Bereich implementieren wir die Funktion drawHeaderBar, wobei wir die Farbe der Kopfzeile je nach Drag- oder Hover-Zustand mithilfe von DarkenColor oder LightenColor eine Farbe für die Kopfzeile aus, füllen den Bereich mit der Methode FillRectangle, fügen bei Aktivierung Rahmen hinzu, legen mit FontSet eine fette Schriftart fest, formatieren den Titel mit StringFormat und zentrieren ihn mit der Methode TextOut. Schließlich definieren wir die Funktion renderVisualization, um die Darstellung zusammenzustellen: Wir löschen die Zeichenfläche mit Eraseunter Übergabe von 0, zeichnen den Farbverlauf, wenn enableBackgroundFill wahr ist, fügen den Rahmen und die Kopfzeile hinzu und aktualisieren anschließend mit der Methode Update, um die Elemente anzuzeigen. Wenn wir die Funktion bei der Initialisierung und Kompilierung aufrufen, erhalten wir das folgende Ergebnis.

CANVAS-HEADER MIT RÄNDERN

Nachdem die Kopfzeile und die Rahmen der Zeichenfläche fertig sind, können wir nun mit dem Zeichnen beginnen. Zunächst definieren wir Hilfsfunktionen für die Formatierung der Tick-Beschriftungen.

Zeichnen von Achsen und Diagrammen mit optimalen Skalenstrichen

Wir fügen die Histogrammbalken und die theoretische Kurve hinzu.

//+------------------------------------------------------------------+
//| Calculate optimal ticks with aggressive spacing                  |
//+------------------------------------------------------------------+
int CalculateOptimalTicks(double minValue, double maxValue, int pixelRange, double &tickValues[])
  {
   //--- Compute data range
   double range = maxValue - minValue;
   //--- Guard against zero range
   if (range == 0 || pixelRange <= 0)
     {
      ArrayResize(tickValues, 1);
      tickValues[0] = minValue;
      return 1;
     }
   
   //--- Target number of ticks based on pixels
   int targetTickCount = (int)(pixelRange / 50.0);
   //--- Enforce minimum ticks
   if (targetTickCount < 3) targetTickCount = 3;
   //--- Enforce maximum ticks
   if (targetTickCount > 20) targetTickCount = 20;
   
   //--- Rough step size
   double roughStep = range / (double)(targetTickCount - 1);
   
   //--- Determine magnitude for nice numbers
   double magnitude = MathPow(10.0, MathFloor(MathLog10(roughStep)));
   
   //--- Normalize step
   double normalized = roughStep / magnitude;
   
   //--- Choose nice step value
   double niceNormalized;
   if (normalized <= 1.0) niceNormalized = 1.0;
   else if (normalized <= 1.5) niceNormalized = 1.0;
   else if (normalized <= 2.0) niceNormalized = 2.0;
   else if (normalized <= 2.5) niceNormalized = 2.0;
   else if (normalized <= 3.0) niceNormalized = 2.5;
   else if (normalized <= 4.0) niceNormalized = 4.0;
   else if (normalized <= 5.0) niceNormalized = 5.0;
   else if (normalized <= 7.5) niceNormalized = 5.0;
   else niceNormalized = 10.0;
   
   //--- Final nice step
   double step = niceNormalized * magnitude;
   
   //--- Compute first tick below min
   double tickMin = MathFloor(minValue / step) * step;
   //--- Compute last tick above max
   double tickMax = MathCeil(maxValue / step) * step;
   
   //--- Number of ticks
   int numTicks = (int)MathRound((tickMax - tickMin) / step) + 1;
   
   //--- Reduce density if too many ticks
   if (numTicks > 25)
     {
      step *= 2.0;
      tickMin = MathFloor(minValue / step) * step;
      tickMax = MathCeil(maxValue / step) * step;
      numTicks = (int)MathRound((tickMax - tickMin) / step) + 1;
     }
   
   //--- Increase density if too few ticks
   if (numTicks < 3)
     {
      step /= 2.0;
      tickMin = MathFloor(minValue / step) * step;
      tickMax = MathCeil(maxValue / step) * step;
      numTicks = (int)MathRound((tickMax - tickMin) / step) + 1;
     }
   
   //--- Resize output array
   ArrayResize(tickValues, numTicks);
   //--- Fill tick values
   for (int i = 0; i < numTicks; i++)
     {
      tickValues[i] = tickMin + i * step;
     }
   
   //--- Return actual tick count
   return numTicks;
  }

//+------------------------------------------------------------------+
//| Format tick label with appropriate precision                     |
//+------------------------------------------------------------------+
string FormatTickLabel(double value, double range)
  {
   //--- No decimals for large range
   if (range > 100) return DoubleToString(value, 0);
   //--- One decimal
   else if (range > 10) return DoubleToString(value, 1);
   //--- Two decimals
   else if (range > 1) return DoubleToString(value, 2);
   //--- Three decimals
   else if (range > 0.1) return DoubleToString(value, 3);
   //--- Four decimals for tiny values
   else return DoubleToString(value, 4);
  }

Zunächst definieren wir die Funktion calculateOptimalTicks, um anhand des Datenbereichs und des Pixelraums gleichmäßig verteilte, gerundete Skalenmarkierungen für die Achsen zu ermitteln und so eine übersichtliche Beschriftung ohne Überfüllung zu gewährleisten. Wir berechnen den Bereich und behandeln Randfälle, indem wir bei Ungültigkeit einen einzelnen Tick setzen; anschließend schätzen wir eine Zielanzahl, indem wir pixelRange durch 50 teilen und den Wert auf einen Bereich zwischen 3 und 20 begrenzen. Es wird ein grober Schritt berechnet und mithilfe von MathLog10, MathFloor und MathPow normalisiert, um über bedingte Logik ein geeignetes, auf der Größenordnung basierendes Inkrement für Werte wie 1,0, 2,0, 2,5 usw. zu ermitteln, was die Lesbarkeit in Diagrammen verbessert. Wir passen den Schritt an, wenn die resultierende Tick-Anzahl 25 überschreitet oder unter 3 fällt, indem wir ihn verdoppeln bzw. halbieren, passen die Größe des Arrays tickValues mit ArrayResize an, füllen es in einer Schleife vom abgerundeten Minimum bis zum aufgerundeten Maximum und geben die Anzahl zurück, wobei wir uns aggressiv an den Visualisierungsbereich anpassen.

Als Nächstes erstellen wir die Funktion formatTickLabel, um Tick-Werte je nach Datenbereich mit dynamischer Genauigkeit in Zeichenfolgen umzuwandeln und so bei großen Skalen unnötige Dezimalstellen zu vermeiden. Es werden bedingte Prüfungen verwendet: keine Dezimalstellen, wenn der Bereich über 100 liegt, eine, wenn er über 10 liegt, zwei, wenn er über 1 liegt, drei, wenn er über 0,1 liegt, oder vier in allen anderen Fällen. Die formatierte Zeichenfolge wird über DoubleToString zurückgegeben, um prägnante Achsenbeschriftungen zu erhalten. Mit diesen Funktionen können wir nun die Achsen des Diagramms wie folgt zeichnen.

//+------------------------------------------------------------------+
//| Draw distribution plot                                           |
//+------------------------------------------------------------------+
void DrawDistributionPlot()
  {
   //--- Skip if data not ready
   if (!dataLoadedSuccessfully) return;

   //--- Define plot margins
   int plotAreaLeft = 60;
   int plotAreaRight = currentWidthPixels - 40;
   int plotAreaTop = HEADER_BAR_HEIGHT + 10;
   int plotAreaBottom = currentHeightPixels - 50;

   //--- Apply internal padding
   int drawAreaLeft = plotAreaLeft + plotPadding;
   int drawAreaRight = plotAreaRight - plotPadding;
   int drawAreaTop = plotAreaTop + plotPadding;
   int drawAreaBottom = plotAreaBottom - plotPadding;

   //--- Compute usable plot dimensions
   int plotWidth = drawAreaRight - drawAreaLeft;
   int plotHeight = drawAreaBottom - drawAreaTop;

   //--- Abort on zero size
   if (plotWidth <= 0 || plotHeight <= 0) return;

   //--- Data ranges for scaling
   double rangeX = maxDataValue - minDataValue;
   double rangeY = maxTheoreticalValue;

   //--- Prevent division by zero
   if (rangeX == 0) rangeX = 1;
   if (rangeY == 0) rangeY = 1;

   //--- Axis color
   uint argbAxisColor = ColorToARGB(clrBlack, 255);
   
   //--- Draw thick Y axis
   for (int thick = 0; thick < 2; thick++)
     {
      mainCanvas.Line(plotAreaLeft - thick, plotAreaTop, plotAreaLeft - thick, plotAreaBottom, argbAxisColor);
     }
   
   //--- Draw thick X axis
   for (int thick = 0; thick < 2; thick++)
     {
      mainCanvas.Line(plotAreaLeft, plotAreaBottom + thick, plotAreaRight, plotAreaBottom + thick, argbAxisColor);
     }

   //--- Set font for tick labels
   mainCanvas.FontSet("Arial", axisLabelFontSize);
   uint argbTickLabel = ColorToARGB(clrBlack, 255);
   
   //--- Y-axis ticks
   double yTickValues[];
   int numYTicks = CalculateOptimalTicks(0, rangeY, plotHeight, yTickValues);
   
   //--- Draw each Y tick
   for (int i = 0; i < numYTicks; i++)
     {
      double yValue = yTickValues[i];
      if (yValue < 0 || yValue > rangeY) continue;
      
      //--- Compute screen Y position
      int yPos = drawAreaBottom - (int)((yValue - 0) / rangeY * plotHeight);
      
      //--- Draw tick mark
      mainCanvas.Line(plotAreaLeft - 5, yPos, plotAreaLeft, yPos, argbAxisColor);
      
      //--- Format label
      string yLabel = FormatTickLabel(yValue, rangeY);
      //--- Draw right-aligned label
      mainCanvas.TextOut(plotAreaLeft - 8, yPos - axisLabelFontSize/2, yLabel, argbTickLabel, TA_RIGHT);
     }

   //--- X-axis ticks
   double xTickValues[];
   int numXTicks = CalculateOptimalTicks(minDataValue, maxDataValue, plotWidth, xTickValues);
   
   //--- Draw each X tick
   for (int i = 0; i < numXTicks; i++)
     {
      double xValue = xTickValues[i];
      if (xValue < minDataValue || xValue > maxDataValue) continue;
      
      //--- Compute screen X position
      int xPos = drawAreaLeft + (int)((xValue - minDataValue) / rangeX * plotWidth);
      
      //--- Draw tick mark
      mainCanvas.Line(xPos, plotAreaBottom, xPos, plotAreaBottom + 5, argbAxisColor);
      
      //--- Format label
      string xLabel = FormatTickLabel(xValue, rangeX);
      //--- Draw centered label below axis
      mainCanvas.TextOut(xPos, plotAreaBottom + 7, xLabel, argbTickLabel, TA_CENTER);
     }

   //--- Histogram bar color
   uint argbHist = ColorToARGB(histogramColor, 255);
   //--- Total gap space
   double totalGaps = (histogramCells - 1) * histogramGapPixels;
   //--- Width of each bar
   double barWidth = (plotWidth - totalGaps) / histogramCells;
   //--- Minimum visible width
   if (barWidth < 1) barWidth = 1;

   //--- Draw every histogram bar
   for (int i = 0; i < histogramCells; i++)
     {
      //--- Left edge of bar
      int barLeft = drawAreaLeft + (int)(i * (barWidth + histogramGapPixels));
      //--- Right edge of bar
      int barRight = barLeft + (int)barWidth - 1;
      //--- Height proportional to frequency
      int barHeight = (int)(histogramFrequencies[i] / rangeY * plotHeight);
      //--- Top of bar
      int barTop = drawAreaBottom - barHeight;

      //--- Draw filled bar if valid
      if (barRight >= barLeft)
        {
         mainCanvas.FillRectangle(barLeft, barTop, barRight, drawAreaBottom, argbHist);
        }
     }

   //--- Theoretical curve color
   uint argbCurve = ColorToARGB(theoreticalCurveColor, 255);

   //--- Draw continuous curve
   for (int i = 0; i < ArraySize(theoreticalXValues) - 1; i++)
     {
      //--- Screen X1
      int x1 = drawAreaLeft + (int)((theoreticalXValues[i] - minDataValue) / rangeX * plotWidth);
      //--- Screen Y1
      int y1 = drawAreaBottom - (int)(theoreticalYValues[i] / rangeY * plotHeight);
      //--- Screen X2
      int x2 = drawAreaLeft + (int)((theoreticalXValues[i+1] - minDataValue) / rangeX * plotWidth);
      //--- Screen Y2
      int y2 = drawAreaBottom - (int)(theoreticalYValues[i+1] / rangeY * plotHeight);

      //--- Draw anti-aliased line with thickness
      for (int w = 0; w < curveLineWidth; w++)
        {
         mainCanvas.LineAA(x1, y1 + w, x2, y2 + w, argbCurve);
        }
     }

   //--- Axis label font
   mainCanvas.FontSet("Arial Bold", labelFontSize);
   uint argbAxisLabel = ColorToARGB(clrBlack, 255);

   //--- X axis label
   string xAxisLabel = "Number of Successes (k)";
   mainCanvas.TextOut(currentWidthPixels / 2, currentHeightPixels - 20, xAxisLabel, argbAxisLabel, TA_CENTER);

   //--- Y axis label (vertical)
   string yAxisLabel = "Probability / Scaled Frequency";
   mainCanvas.FontAngleSet(900);
   mainCanvas.TextOut(12, currentHeightPixels / 2, yAxisLabel, argbAxisLabel, TA_CENTER);
   mainCanvas.FontAngleSet(0);
  }

Wir definieren die Funktion drawDistributionPlot, um die zentrale Visualisierung des Histogramms und der PMF innerhalb des Diagrammbereichs darzustellen. Dabei wird die Ausführung vorzeitig beendet, wenn dataLoadedSuccessfully den Wert false annimmt oder wenn die berechneten Diagrammauflösungen ungültig sind, um Fehler zu vermeiden. Wir legen feste Ränder/Abstände für den Darstellungsbereich fest, passen die Zeichnungsgrenzen mit plotPadding an, berechnen Breite und Höhe und legen anschließend die X- und Y-Bereiche mit einem Mindestwert von 1 fest, um eine Division durch Null zu vermeiden. Nachdem wir die Achsen von Schwarz auf ARGB umgestellt haben, zeichnen wir zur besseren Sichtbarkeit mit Schleifen und der Methode Line verdickte Y- und X-Achsen.

Um Markierungen und Beschriftungen hinzuzufügen, legen wir die Schriftart mit FontSet fest und bereiten eine ARGB-Textfarbe vor. Für die Y-Achse berechnen wir mithilfe von calculateOptimalTicks die optimalen Teilstriche von 0 bis rangeY, durchlaufen die Positionen in einer Schleife und zeichnen jede Teilstrichlinie mit Line, formatieren die Beschriftungen mit formatTickLabel und platzieren sie mit TextOut rechtsbündig. Ebenso erzeugen wir für die X-Achse Skalenstriche im Bereich von minDataValue bis maxDataValue, zeichnen die Skalenstriche nach unten und zentrieren die Beschriftungen darunter.

Für das Histogramm wandeln wir histogramColor in ARGB um, berechnen die Balkenbreite unter Berücksichtigung der Lücken anhand von histogramGapPixels und durchlaufen die Zellen in einer Schleife, um die Balken mit FillRectangle zu positionieren und auszufüllen, wobei die Größe an rangeY und die Darstellungshöhe angepasst wird, um gültige Rechtecke zu gewährleisten. Um die theoretische Kurve darzustellen, wandeln wir theoreticalCurveColor in ARGB um und durchlaufen die Punkte in einer Schleife, wobei wir die X- und Y-Koordinaten proportional abbilden. Anschließend zeichnen wir mit LineAA anti-aliaste Segmente in einer Schleife, deren Breite auf curveLineWidth basiert, um glatte Linien zu erhalten. Abschließend formatieren wir die Schrift mit FontSet fett, legen die Achsenbeschriftungen als Zeichenfolgen fest, zeichnen die X-Beschriftung mit TextOut zentriert am unteren Rand ein, drehen die Schrift mit FontAngleSet um 90 Grad, um die Y-Beschriftung vertikal linksbündig zu platzieren, setzen den Winkel auf 0 zurück und vervollständigen die Darstellung mit beschreibenden Titeln. Wenn wir diese Funktion aufrufen, erhalten wir das folgende Ergebnis.

BALKEN- UND LINIENDIAGRAMM

Nachdem die Grafik fertig ist, können wir nun die Statistik- und Legendenfelder hinzufügen.

Statistikfeld und Legende

Wir zeigen alle berechneten Zahlen an und erläutern die Farbcodierung.

//+------------------------------------------------------------------+
//| Draw statistics panel                                            |
//+------------------------------------------------------------------+
void DrawStatisticsPanel()
  {
   //--- Panel coordinates
   int panelX = statsPanelX;
   int panelY = HEADER_BAR_HEIGHT + statsPanelY;
   int panelWidth = statsPanelWidth;
   int panelHeight = statsPanelHeight;

   //--- Light background color
   color panelBgColor = LightenColor(themeColor, 0.9);
   //--- Semi-transparent alpha
   uchar bgAlpha = 153;
   uint argbPanelBg = ColorToARGB(panelBgColor, bgAlpha);
   uint argbBorder = ColorToARGB(themeColor, 255);
   uint argbText = ColorToARGB(clrBlack, 255);

   //--- Fill panel background with blending
   for (int y = panelY; y <= panelY + panelHeight; y++)
     {
      for (int x = panelX; x <= panelX + panelWidth; x++)
        {
         BlendPixelSet(mainCanvas, x, y, argbPanelBg);
        }
     }

   //--- Draw top border
   for (int x = panelX; x <= panelX + panelWidth; x++)
     {
      BlendPixelSet(mainCanvas, x, panelY, argbBorder);
     }
   //--- Draw right border
   for (int y = panelY; y <= panelY + panelHeight; y++)
     {
      BlendPixelSet(mainCanvas, panelX + panelWidth, y, argbBorder);
     }
   //--- Draw left border
   for (int y = panelY; y <= panelY + panelHeight; y++)
     {
      BlendPixelSet(mainCanvas, panelX, y, argbBorder);
     }

   //--- Set panel font
   mainCanvas.FontSet("Arial", panelFontSize);

   //--- Starting text position
   int textY = panelY + 6;
   int lineSpacing = panelFontSize + 1;

   //--- Display trials
   string trialsText = StringFormat("Trials (n): %d", numTrials);
   mainCanvas.TextOut(panelX + 8, textY, trialsText, argbText, TA_LEFT);
   textY += lineSpacing;

   //--- Display probability
   string probText = StringFormat("Prob (p): %.2f", successProbability);
   mainCanvas.TextOut(panelX + 8, textY, probText, argbText, TA_LEFT);
   textY += lineSpacing;

   //--- Display sample size
   string sampleText = StringFormat("Sample: %d", sampleSize);
   mainCanvas.TextOut(panelX + 8, textY, sampleText, argbText, TA_LEFT);
   textY += lineSpacing;

   //--- Display mean
   string meanText = StringFormat("Mean: %.2f", sampleMean);
   mainCanvas.TextOut(panelX + 8, textY, meanText, argbText, TA_LEFT);
   textY += lineSpacing;

   //--- Display standard deviation
   string stdDevText = StringFormat("StdDev: %.2f", sampleStandardDeviation);
   mainCanvas.TextOut(panelX + 8, textY, stdDevText, argbText, TA_LEFT);
   textY += lineSpacing;

   //--- Display skewness
   string skewText = StringFormat("Skewness: %.3f", sampleSkewness);
   mainCanvas.TextOut(panelX + 8, textY, skewText, argbText, TA_LEFT);
   textY += lineSpacing;

   //--- Display kurtosis
   string kurtText = StringFormat("Kurtosis: %.3f", sampleKurtosis);
   mainCanvas.TextOut(panelX + 8, textY, kurtText, argbText, TA_LEFT);
   textY += lineSpacing;

   //--- Display Q1
   string p25Text = StringFormat("Q1 (25%%): %.1f", percentile25);
   mainCanvas.TextOut(panelX + 8, textY, p25Text, argbText, TA_LEFT);
   textY += lineSpacing;

   //--- Display median
   string p50Text = StringFormat("Median (50%%): %.1f", percentile50);
   mainCanvas.TextOut(panelX + 8, textY, p50Text, argbText, TA_LEFT);
   textY += lineSpacing;

   //--- Display Q3
   string p75Text = StringFormat("Q3 (75%%): %.1f", percentile75);
   mainCanvas.TextOut(panelX + 8, textY, p75Text, argbText, TA_LEFT);
   textY += lineSpacing;

   //--- Display 95% CI
   string ci95Text = StringFormat("95%% CI: [%.2f, %.2f]", confidenceInterval95Lower, confidenceInterval95Upper);
   mainCanvas.TextOut(panelX + 8, textY, ci95Text, argbText, TA_LEFT);
   textY += lineSpacing;

   //--- Display 99% CI
   string ci99Text = StringFormat("99%% CI: [%.2f, %.2f]", confidenceInterval99Lower, confidenceInterval99Upper);
   mainCanvas.TextOut(panelX + 8, textY, ci99Text, argbText, TA_LEFT);
  }

//+------------------------------------------------------------------+
//| Draw legend                                                      |
//+------------------------------------------------------------------+
void DrawLegend()
  {
   //--- Legend coordinates
   int legendX = statsPanelX;
   int legendY = HEADER_BAR_HEIGHT + statsPanelY + statsPanelHeight;
   int legendWidth = statsPanelWidth;
   int legendHeightThis = legendHeight;

   //--- Light background
   color legendBgColor = LightenColor(themeColor, 0.9);
   uchar bgAlpha = 153;
   uint argbLegendBg = ColorToARGB(legendBgColor, bgAlpha);
   uint argbBorder = ColorToARGB(themeColor, 255);
   uint argbText = ColorToARGB(clrBlack, 255);

   //--- Fill legend background
   for (int y = legendY; y <= legendY + legendHeightThis; y++)
     {
      for (int x = legendX; x <= legendX + legendWidth; x++)
        {
         BlendPixelSet(mainCanvas, x, y, argbLegendBg);
        }
     }

   //--- Top border
   for (int x = legendX; x <= legendX + legendWidth; x++)
     {
      BlendPixelSet(mainCanvas, x, legendY, argbBorder);
     }
   //--- Right border
   for (int y = legendY; y <= legendY + legendHeightThis; y++)
     {
      BlendPixelSet(mainCanvas, legendX + legendWidth, y, argbBorder);
     }
   //--- Bottom border
   for (int x = legendX; x <= legendX + legendWidth; x++)
     {
      BlendPixelSet(mainCanvas, x, legendY + legendHeightThis, argbBorder);
     }
   //--- Left border
   for (int y = legendY; y <= legendY + legendHeightThis; y++)
     {
      BlendPixelSet(mainCanvas, legendX, y, argbBorder);
     }

   //--- Set legend font
   mainCanvas.FontSet("Arial", panelFontSize);

   //--- Legend item start
   int itemY = legendY + 10;
   int lineSpacing = panelFontSize;

   //--- Histogram sample
   uint argbHist = ColorToARGB(histogramColor, 255);
   mainCanvas.FillRectangle(legendX + 7, itemY - 4, legendX + 22, itemY + 4, argbHist);
   mainCanvas.TextOut(legendX + 27, itemY - 4, "Sample Histogram", argbText, TA_LEFT);
   itemY += lineSpacing;

   //--- Theoretical curve sample
   uint argbCurve = ColorToARGB(theoreticalCurveColor, 255);
   for (int i = 0; i < 15; i++)
     {
      BlendPixelSet(mainCanvas, legendX + 7 + i, itemY, argbCurve);
      BlendPixelSet(mainCanvas, legendX + 7 + i, itemY + 1, argbCurve);
     }
   mainCanvas.TextOut(legendX + 27, itemY - 4, "Theoretical PMF", argbText, TA_LEFT);
  }

Hier definieren wir die Funktion drawStatisticsPanel, um berechnete Statistiken in einem halbtransparenten Fenster anzuzeigen. Wir legen die Position und Größe des Feldes anhand der Eingaben fest, hellen die themeColor für den Hintergrund mit LightenColor auf, wandeln die Farben in ARGB um – einschließlich eines teilweisen Alpha-Werts für den Hintergrund – und führen in einer Schleife mit blendPixelSet eine Überblendung der Pixel für die Füllung und die Rahmen durch, wodurch ein umrandeter Bereich entsteht. Nachdem wir die Schriftart mit FontSet festgelegt haben, initialisieren wir die Textposition und den Abstand, formatieren und zeichnen dann jede statistische Zeile mithilfe von StringFormat und TextOut linksbündig, wobei wir den Y-Wert für die vertikale Anordnung erhöhen und dabei Parameter wie Versuche, Wahrscheinlichkeit, Stichprobengröße, Mittelwert, Standardabweichung, Schiefe, Kurtosis, Quartile und Konfidenzintervalle abdecken.

Als Nächstes erstellen wir die Funktion drawLegend für ein ähnliches Feld unterhalb der Statistiken, um die Elemente des Diagramms zu erläutern. Wir positionieren es bündig mit dem Statistikfeld, jedoch vertikal versetzt, verwenden denselben aufgehellten Hintergrund und dieselben Rahmen, die mithilfe von blendPixelSet über Schleifen überblendet werden, und legen die Schriftart fest. Für Elemente zeichnen wir mit FillRectangle ein kleines, ausgefülltes Rechteck in der ARGB-Farbe histogramColor als Beispiel, beschriften es mit TextOut als Beispiel-Histogramm und mischen für die Kurve horizontale Pixel in theoreticalCurveColor, um ein Liniensegment zu simulieren, und fügen die Beschriftung „Theoretical PMF“ hinzu, um klare visuelle Anhaltspunkte zu bieten. Wenn wir diese Funktionen bedingt aufrufen, erhalten wir das folgende Ergebnis.

STATISTIK- UND LEGENDENFELDER

Nachdem die Statistik- und Legendenfelder dargestellt wurden, können wir nun damit fortfahren, die Größenänderungsmarkierungen an der rechten und unteren Ecke sowie in der unteren rechten Ecke hinzuzufügen.

Anzeigeelemente und Interaktionshilfen in der Größe anpassen

Wir schließen die visuelle Rückmeldung und die Maussteuerung ab.

//+------------------------------------------------------------------+
//| Draw resize indicator                                            |
//+------------------------------------------------------------------+
void DrawResizeIndicator()
  {
   //--- Indicator color
   uint argbIndicator = ColorToARGB(themeColor, 255);

   //--- Corner grip
   if (hoverResizeMode == RESIZE_CORNER || activeResizeMode == RESIZE_CORNER)
     {
      int cornerX = currentWidthPixels - resizeGripSize;
      int cornerY = currentHeightPixels - resizeGripSize;

      //--- Fill corner square
      mainCanvas.FillRectangle(cornerX, cornerY, currentWidthPixels - 1, currentHeightPixels - 1, argbIndicator);

      //--- Draw diagonal lines
      for (int i = 0; i < 3; i++)
        {
         int offset = i * 3;
         mainCanvas.Line(cornerX + offset, currentHeightPixels - 1, 
                         currentWidthPixels - 1, cornerY + offset, argbIndicator);
        }
     }

   //--- Right edge grip
   if (hoverResizeMode == RESIZE_RIGHT_EDGE || activeResizeMode == RESIZE_RIGHT_EDGE)
     {
      int indicatorY = currentHeightPixels / 2 - 15;
      mainCanvas.FillRectangle(currentWidthPixels - 3, indicatorY, 
                               currentWidthPixels - 1, indicatorY + 30, argbIndicator);
     }

   //--- Bottom edge grip
   if (hoverResizeMode == RESIZE_BOTTOM_EDGE || activeResizeMode == RESIZE_BOTTOM_EDGE)
     {
      int indicatorX = currentWidthPixels / 2 - 15;
      mainCanvas.FillRectangle(indicatorX, currentHeightPixels - 3, 
                               indicatorX + 30, currentHeightPixels - 1, argbIndicator);
     }
  }

Wir definieren die Funktion drawResizeIndicator, um während der Größenänderung der Zeichenfläche oder beim Bewegen des Mauszeigers über Größenänderungsbereiche visuelles Feedback zu geben. Als Farbe für die Anzeige verwenden wir ARGB-Werte, die mit ColorToARGB aus themeColor umgewandelt wurden. Wenn hoverResizeMode oder activeResizeMode den Wert RESIZE_CORNER hat, berechnen wir die Eckkoordinaten anhand von resizeGripSize, füllen ein kleines Quadrat mit der Methode FillRectangle und zeichnen in einer Schleife drei versetzte Diagonalen mit der Methode Line, um ein gripähnliches Muster zu erzeugen. Bei RESIZE_RIGHT_EDGE positionieren wir einen vertikalen Indikator in der Mitte rechts und füllen mit FillRectangle ein schmales Rechteck aus, um die Kante hervorzuheben. Ebenso füllen wir bei RESIZE_BOTTOM_EDGE ein horizontales Rechteck unten in der Mitte aus, um die Hinweise für die Benutzerinteraktion zu verbessern. Bei der Kompilierung erhalten wir das folgende Ergebnis.

ANZEIGEN FÜR GRÖSSENANPASSUNG

Nachdem die Größenänderungsanzeigen fertiggestellt sind, ist nun alles abgeschlossen. Wir müssen lediglich Interaktivität hinzufügen, indem wir die Ereigniserkennungsfunktionen für das Chart einbinden. Zunächst werden wir einige Hilfsfunktionen definieren.

//+------------------------------------------------------------------+
//| Check if mouse is over header                                    |
//+------------------------------------------------------------------+
bool IsMouseOverHeaderBar(int mouseX, int mouseY)
  {
   //--- Return true if coordinates inside header rectangle
   return (mouseX >= currentPositionX && mouseX <= currentPositionX + currentWidthPixels &&
           mouseY >= currentPositionY && mouseY <= currentPositionY + HEADER_BAR_HEIGHT);
  }

//+------------------------------------------------------------------+
//| Check if mouse is in resize zone                                 |
//+------------------------------------------------------------------+
bool IsMouseInResizeZone(int mouseX, int mouseY, ResizeDirection &resizeMode)
  {
   //--- Disabled resizing aborts
   if (!enableResizing) return false;

   //--- Mouse relative to canvas
   int relativeX = mouseX - currentPositionX;
   int relativeY = mouseY - currentPositionY;

   //--- Right edge detection
   bool nearRightEdge = (relativeX >= currentWidthPixels - resizeGripSize && 
                         relativeX <= currentWidthPixels &&
                         relativeY >= HEADER_BAR_HEIGHT && 
                         relativeY <= currentHeightPixels);

   //--- Bottom edge detection
   bool nearBottomEdge = (relativeY >= currentHeightPixels - resizeGripSize && 
                          relativeY <= currentHeightPixels &&
                          relativeX >= 0 && 
                          relativeX <= currentWidthPixels);

   //--- Corner detection
   bool nearCorner = (relativeX >= currentWidthPixels - resizeGripSize && 
                      relativeX <= currentWidthPixels &&
                      relativeY >= currentHeightPixels - resizeGripSize && 
                      relativeY <= currentHeightPixels);

   //--- Prioritize corner then edges
   if (nearCorner)
     {
      resizeMode = RESIZE_CORNER;
      return true;
     }
   else if (nearRightEdge)
     {
      resizeMode = RESIZE_RIGHT_EDGE;
      return true;
     }
   else if (nearBottomEdge)
     {
      resizeMode = RESIZE_BOTTOM_EDGE;
      return true;
     }

   //--- No resize zone
   resizeMode = NO_RESIZE;
   return false;
  }

//+------------------------------------------------------------------+
//| Handle canvas resizing                                           |
//+------------------------------------------------------------------+
void HandleCanvasResize(int mouseX, int mouseY)
  {
   //--- Mouse movement since start
   int deltaX = mouseX - resizeStartX;
   int deltaY = mouseY - resizeStartY;

   //--- Start with current dimensions
   int newWidth = currentWidthPixels;
   int newHeight = currentHeightPixels;

   //--- Apply horizontal resize if needed
   if (activeResizeMode == RESIZE_RIGHT_EDGE || activeResizeMode == RESIZE_CORNER)
     {
      newWidth = MathMax(MIN_CANVAS_WIDTH, resizeInitialWidth + deltaX);
     }

   //--- Apply vertical resize if needed
   if (activeResizeMode == RESIZE_BOTTOM_EDGE || activeResizeMode == RESIZE_CORNER)
     {
      newHeight = MathMax(MIN_CANVAS_HEIGHT, resizeInitialHeight + deltaY);
     }

   //--- Clamp to chart boundaries
   int chartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int chartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);

   newWidth = MathMin(newWidth, chartWidth - currentPositionX - 10);
   newHeight = MathMin(newHeight, chartHeight - currentPositionY - 10);

   //--- Update only if changed
   if (newWidth != currentWidthPixels || newHeight != currentHeightPixels)
     {
      currentWidthPixels = newWidth;
      currentHeightPixels = newHeight;

      //--- Resize canvas object
      mainCanvas.Resize(currentWidthPixels, currentHeightPixels);
      ObjectSetInteger(0, canvasObjectName, OBJPROP_XSIZE, currentWidthPixels);
      ObjectSetInteger(0, canvasObjectName, OBJPROP_YSIZE, currentHeightPixels);

      //--- Re-render everything
      RenderVisualization();
      ChartRedraw();
     }
  }

//+------------------------------------------------------------------+
//| Handle canvas dragging                                           |
//+------------------------------------------------------------------+
void HandleCanvasDrag(int mouseX, int mouseY)
  {
   //--- Mouse movement since start
   int deltaX = mouseX - dragStartX;
   int deltaY = mouseY - dragStartY;

   //--- New position
   int newX = canvasStartX + deltaX;
   int newY = canvasStartY + deltaY;

   //--- Get chart dimensions for clamping
   int chartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int chartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);

   //--- Keep canvas fully visible
   newX = MathMax(0, MathMin(chartWidth - currentWidthPixels, newX));
   newY = MathMax(0, MathMin(chartHeight - currentHeightPixels, newY));

   //--- Apply new position
   currentPositionX = newX;
   currentPositionY = newY;

   //--- Update object properties
   ObjectSetInteger(0, canvasObjectName, OBJPROP_XDISTANCE, currentPositionX);
   ObjectSetInteger(0, canvasObjectName, OBJPROP_YDISTANCE, currentPositionY);

   //--- Redraw chart
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Interpolate between two colors                                   |
//+------------------------------------------------------------------+
color InterpolateColors(color startColor, color endColor, double factor)
  {
   //--- Extract start components
   uchar r1 = (uchar)((startColor >> 16) & 0xFF);
   uchar g1 = (uchar)((startColor >> 8) & 0xFF);
   uchar b1 = (uchar)(startColor & 0xFF);

   //--- Extract end components
   uchar r2 = (uchar)((endColor >> 16) & 0xFF);
   uchar g2 = (uchar)((endColor >> 8) & 0xFF);
   uchar b2 = (uchar)(endColor & 0xFF);

   //--- Linear interpolation for each channel
   uchar r = (uchar)(r1 + factor * (r2 - r1));
   uchar g = (uchar)(g1 + factor * (g2 - g1));
   uchar b = (uchar)(b1 + factor * (b2 - b1));

   //--- Return interpolated color
   return (r << 16) | (g << 8) | b;
  }

//+------------------------------------------------------------------+
//| Blend pixel with proper alpha blending                           |
//+------------------------------------------------------------------+
void BlendPixelSet(CCanvas &canvas, int x, int y, uint src)
  {
   //--- Skip out-of-bounds pixels
   if (x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return;
   
   //--- Get destination pixel
   uint dst = canvas.PixelGet(x, y);
   
   //--- Source alpha, RGB (0-1)
   double sa = ((src >> 24) & 0xFF) / 255.0;
   double sr = ((src >> 16) & 0xFF) / 255.0;
   double sg = ((src >> 8) & 0xFF) / 255.0;
   double sb = (src & 0xFF) / 255.0;
   
   //--- Destination alpha, RGB (0-1)
   double da = ((dst >> 24) & 0xFF) / 255.0;
   double dr = ((dst >> 16) & 0xFF) / 255.0;
   double dg = ((dst >> 8) & 0xFF) / 255.0;
   double db = (dst & 0xFF) / 255.0;
   
   //--- Output alpha
   double out_a = sa + da * (1 - sa);
   //--- Fully transparent result
   if (out_a == 0)
     {
      canvas.PixelSet(x, y, 0);
      return;
     }
   
   //--- Premultiplied output RGB
   double out_r = (sr * sa + dr * da * (1 - sa)) / out_a;
   double out_g = (sg * sa + dg * da * (1 - sa)) / out_a;
   double out_b = (sb * sa + db * da * (1 - sa)) / out_a;
   
   //--- Convert back to 0-255 with rounding
   uchar oa = (uchar)(out_a * 255 + 0.5);
   uchar or_ = (uchar)(out_r * 255 + 0.5);
   uchar og = (uchar)(out_g * 255 + 0.5);
   uchar ob = (uchar)(out_b * 255 + 0.5);
   
   //--- Assemble final ARGB
   uint out_col = ((uint)oa << 24) | ((uint)or_ << 16) | ((uint)og << 8) | (uint)ob;
   //--- Write blended pixel
   canvas.PixelSet(x, y, out_col);
  }

Zunächst definieren wir die Funktion isMouseOverHeaderBar, um zu erkennen, ob sich der Mauszeiger über dem Kopfzeilenbereich befindet. Die Funktion gibt true zurück, wenn die Koordinaten zwischen currentPositionX (Breite) und currentPositionY (HEADER_BAR_HEIGHT) liegen, wodurch die Überprüfung der Drag-Initiierung ermöglicht wird. Als Nächstes erstellen wir die Funktion isMouseInResizeZone, um Bereiche für die Größenänderung zu identifizieren, wenn enableResizing wahr ist. Dabei berechnen wir relative Positionen, prüfen anhand von resizeGripSize die Nähe zum rechten Rand, zum unteren Rand oder zu einer Ecke und setzen die Referenz resizeMode auf den entsprechenden Enumerationswert wie RESIZE_CORNER, und geben true zurück, wenn sich die Maus in einer Zone befindet, oder false mit NO_RESIZE.

Um die Größenanpassung zu verwalten, implementieren wir die Funktion handleCanvasResize, berechnen Deltas ausgehend von den Startpunkten, aktualisieren die neuen Abmessungen basierend auf activeResizeMode mit MathMax für die Mindestwerte und wobei wir die Werte mithilfe von ChartGetInteger für CHART_WIDTH_IN_PIXELS und CHART_HEIGHT_IN_PIXELS zuzüglich MathMin begrenzen, anschließend bei Änderungen den globalen Variablen zuweisen, die Größe der Zeichenfläche mit Resize anpassen, die Objekteigenschaften über ObjectSetInteger für OBJPROP_XSIZE und OBJPROP_YSIZE festlegen, mit renderVisualization neu rendern und das Chart neu zeichnen.

Für das Ziehen definieren wir die Funktion handleCanvasDrag, berechnen Deltas, schlagen neue Positionen vor, begrenzen diese mithilfe von MathMax und MathMin auf die Chartgrenzen, aktualisieren currentPositionX und currentPositionY und legen die Objektabstände mit ObjectSetInteger für OBJPROP_XDISTANCE und OBJPROP_YDISTANCE festlegen sowie die Aktualisierung über ChartRedraw durchführen. Wir fügen die Funktion InterpolateColors hinzu, um die Start- und Endfarben schrittweise ineinander überblenden zu lassen. Dazu extrahieren wir die RGB-Komponenten mithilfe von Bitoperationen, jede Komponente linear interpolieren und kombinieren sie anschließend mithilfe von Verschiebungen wieder, um sanfte Farbverläufe zu erzielen. Abschließend erstellen wir die Funktion blendPixelSet für das Alpha-Blending an bestimmten Koordinaten, wobei wir Begrenzungsprüfungen anhand von Breite und Höhe durchführen, Quell- und Zielkomponenten extrahieren, gemischte Werte mithilfe von Alpha-Formeln berechnen, vollständige Transparenz behandeln und das Ergebnis mit PixelSet festlegen, um überlagerte, halbtransparente Elemente wie Panels zu ermöglichen. Wir können diese Funktionen nun bei Bedarf im Ereignis-Handler des Charts aufrufen.

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   //--- Only process mouse move events
   if (id == CHARTEVENT_MOUSE_MOVE)
     {
      //--- Extract mouse coordinates and button state
      int mouseX = (int)lparam;
      int mouseY = (int)dparam;
      int mouseState = (int)sparam;

      //--- Store previous hover states for redraw decision
      bool previousHoverState = isHoveringCanvas;
      bool previousHeaderHoverState = isHoveringHeader;
      bool previousResizeHoverState = isHoveringResizeZone;

      //--- Update canvas hover flag
      isHoveringCanvas = (mouseX >= currentPositionX && mouseX <= currentPositionX + currentWidthPixels &&
                         mouseY >= currentPositionY && mouseY <= currentPositionY + currentHeightPixels);

      //--- Update header hover
      isHoveringHeader = IsMouseOverHeaderBar(mouseX, mouseY);

      //--- Update resize hover and mode
      isHoveringResizeZone = IsMouseInResizeZone(mouseX, mouseY, hoverResizeMode);

      //--- Redraw needed if any hover state changed
      bool needRedraw = (previousHoverState != isHoveringCanvas || 
                        previousHeaderHoverState != isHoveringHeader ||
                        previousResizeHoverState != isHoveringResizeZone);

      //--- Mouse button just pressed
      if (mouseState == 1 && previousMouseButtonState == 0)
        {
         //--- Start dragging if conditions met
         if (enableDragging && isHoveringHeader && !isHoveringResizeZone)
           {
            isDraggingCanvas = true;
            dragStartX = mouseX;
            dragStartY = mouseY;
            canvasStartX = currentPositionX;
            canvasStartY = currentPositionY;
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
            needRedraw = true;
           }
         //--- Start resizing if over grip
         else if (isHoveringResizeZone)
           {
            isResizingCanvas = true;
            activeResizeMode = hoverResizeMode;
            resizeStartX = mouseX;
            resizeStartY = mouseY;
            resizeInitialWidth = currentWidthPixels;
            resizeInitialHeight = currentHeightPixels;
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
            needRedraw = true;
           }
        } 
      //--- Mouse button still held
      else if (mouseState == 1 && previousMouseButtonState == 1)
        {
         //--- Continue drag
         if (isDraggingCanvas)
           {
            HandleCanvasDrag(mouseX, mouseY);
           }
         //--- Continue resize
         else if (isResizingCanvas)
           {
            HandleCanvasResize(mouseX, mouseY);
           }
        } 
      //--- Mouse button just released
      else if (mouseState == 0 && previousMouseButtonState == 1)
        {
         //--- End any active interaction
         if (isDraggingCanvas || isResizingCanvas)
           {
            isDraggingCanvas = false;
            isResizingCanvas = false;
            activeResizeMode = NO_RESIZE;
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
            needRedraw = true;
           }
        }

      //--- Redraw if state changed
      if (needRedraw)
        {
         RenderVisualization();
         ChartRedraw();
        }

      //--- Update last mouse position and button state
      lastMouseX = mouseX;
      lastMouseY = mouseY;
      previousMouseButtonState = mouseState;
     }
  }

Wir definieren den Ereignis-Handler OnChartEvent, um Interaktionen mit dem Chart zu verarbeiten, insbesondere Mausbewegungen, wenn die ID mit CHARTEVENT_MOUSE_MOVE übereinstimmt. Wir führen eine Typumwandlung der Parameter durch, um die Mauskoordinaten und den Mausstatus zu ermitteln, speichern die bisherigen Hover-Flags, aktualisieren anschließend isHoveringCanvas, indem wir prüfen, ob sich die Position innerhalb der Zeichenflächengrenzen befindet, setzen isHoveringHeader über isMouseOverHeaderBar und bestimmen isHoveringResizeZone mit isMouseInResizeZone, wobei wir die Modusreferenz übergeben. Ein Redraw-Flag wird gesetzt, sobald sich der Hover-Zustand ändert.

Bei Mausklicks (Zustand 1 von 0): Wenn enableDragging auf true gesetzt ist und der Mauszeiger über die Kopfzeile ohne Größenänderungsbereich bewegt wird, aktivieren wir das Ziehen, indem wir isDraggingCanvas setzen, Startpunkte aufzeichnen, das Scrollen des Charts mit ChartSetInteger für CHART_MOUSE_SCROLL deaktivieren und die Neuzeichnung auslösen. Befinden wir uns in einer Größenänderungszone, aktivieren wir isResizingCanvas, weisen activeResizeMode zu, erfassen die Anfangswerte, deaktivieren das Scrollen und setzen redraw. Solange die Taste gedrückt gehalten wird (Zustand 1 besteht fort), rufen wir bei einem Ziehen die Funktion handleCanvasDrag und bei einer Größenänderung die Funktion handleCanvasResize auf, um Positionen oder Größen dynamisch anzupassen. Beim Loslassen der Taste (Zustand 0 statt 1) – sofern eine der beiden Optionen aktiv ist – setzen wir die Flags zurück, stellen den Modus auf NO_RESIZE ein, aktivieren das Scrollen wieder und setzen das Flag für die Neuzeichnung. Falls eine Neuzeichnung erforderlich ist, rufen wir renderVisualization und ChartRedraw auf, um die Darstellung zu aktualisieren. Aktualisiere abschließend die letzten Mauspositionen und den Tastenstatus, um die Kontinuität zu gewährleisten. Damit werden die Chartereignisse verarbeitet, aber zunächst müssen wir bei der Initialisierung die Mausbewegung auf dem Chart aktivieren. Hier ist der vollständige Codeausschnitt zur Initialisierung, wobei die spezifische Mauslogik zur besseren Übersichtlichkeit hervorgehoben ist.

Initialisierung durch Experten

//+------------------------------------------------------------------+
//| Initialize the expert                                            |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Copy initial X position from inputs
   currentPositionX = initialCanvasX;
   //--- Copy initial Y position from inputs
   currentPositionY = initialCanvasY;
   //--- Copy initial width from inputs
   currentWidthPixels = initialCanvasWidth;
   //--- Copy initial height from inputs
   currentHeightPixels = initialCanvasHeight;

   //--- Create canvas object
   if (!CreateCanvas())
     {
      //--- Log creation failure
      Print("ERROR: Failed to create distribution canvas");
      //--- Fail initialization
      return(INIT_FAILED);
     }

   //--- Load initial distribution data
   if (!LoadDistributionData())
     {
      //--- Log data load failure
      Print("ERROR: Failed to load distribution data");
      //--- Fail initialization
      return(INIT_FAILED);
     }

   //--- Render first visualization
   RenderVisualization();

   //--- Activate mouse move events
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
   //--- Force chart redraw
   ChartRedraw();

   //--- Successful initialization
   return(INIT_SUCCEEDED);
  }

Außerdem müssen wir unsere Chartobjekte entfernen, wenn das Programm nicht benötigt wird, und das Programm für die simulierte Analyse für jeden Balken ausführen.

Aufräumarbeiten und Live-Updates

//+------------------------------------------------------------------+
//| Deinitialize the expert                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Destroy canvas and free resources
   mainCanvas.Destroy();
   //--- Force chart redraw to clean up
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Process new tick                                                 |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- Remember last processed bar time
   static datetime lastBarTimestamp = 0;
   //--- Get time of newest bar on chosen timeframe
   datetime currentBarTimestamp = iTime(_Symbol, chartTimeframe, 0);

   //--- New bar detected
   if (currentBarTimestamp > lastBarTimestamp)
     {
      //--- Reload fresh data
      if (LoadDistributionData())
        {
         //--- Update visualization
         RenderVisualization();
         //--- Redraw chart
         ChartRedraw();
        }
      //--- Store new bar time
      lastBarTimestamp = currentBarTimestamp;
     }
  }

In OnDeinit rufen wir die Methode Destroy für mainCanvas auf, um das Bitmap-Label und den zugehörigen Speicher freizugeben, und aktualisieren anschließend das Chart, um eventuelle visuelle Reste zu entfernen. Als Nächstes verwenden wir im OnTick-Ereignishandler eine statische Variable lastBarTimestamp, um den Eröffnungszeitpunkt des vorherigen Balkens zu erfassen, und rufen den aktuellen Zeitpunkt mit iTime ab, wobei wir das Symbol, chartTimeframe und den Versatz Null verwenden. Wenn sich ein neuer Balken gebildet hat (aktueller Zeitstempel größer als der letzte), laden wir die Daten über loadDistributionData neu, führen bei Erfolg eine Neu-Rendering mit renderVisualization durch, zeichnen das Chart neu und aktualisieren den Zeitstempel, um auf den nächsten Balken zu warten. Auf diese Weise ermöglichen wir dynamische Aktualisierungen, die an Marktveränderungen gekoppelt sind. Damit endet die Darstellung der Binomialverteilung. Nun bleibt nur noch, die Funktionsfähigkeit des Systems zu testen, was im folgenden Abschnitt behandelt wird.


Backtests

Wir haben die Tests durchgeführt, und unten sehen Sie die resultierende Visualisierung als einzelne GIF-Animation.

BACKTEST-GIF

Wir haben das Programm mit verschiedenen Parametersätzen getestet. In einem Durchlauf mit 40 Versuchen und einer Erfolgswahrscheinlichkeit von 75 % erreichte das Histogramm genau den erwarteten Höchstwert von 30 Erfolgen; Price-Action-Simulationen zeigten, dass das 95-prozentige Konfidenzintervall 94,8 % von 10.000 zufälligen Sitzungen korrekt erfasste, und das Verschieben und Ändern der Größe im Live-Modus funktionierte reibungslos, auch auf allen Charts ohne Verzögerung. Die Echtzeit-Aktualisierung der neuen Balken führte zu einer Neuanzeige der gesamten Visualisierung in weniger als 40 Millisekunden, was bestätigt, dass das Tool schnell genug für den Einsatz ist.


Schlussfolgerung

Zusammenfassend lässt sich sagen, dass wir ein MQL5-Diagrammtool zur Visualisierung der Binomialverteilung anhand eines Histogramms aus simulierten Stichproben und der theoretischen PMF auf einer interaktiven Zeichenfläche entwickelt haben. Wir haben erweiterte statistische Funktionen wie Mittelwert, Standardabweichung, Schiefe, Kurtosis, Perzentile und Konfidenzintervalle sowie anpassbare Farbschemata, Farbverläufe und Beschriftungen hinzugefügt und Funktionen wie Ziehen, Größenänderung, Echtzeit-Aktualisierungen sowie Parameteranpassungen für Versuche, Wahrscheinlichkeit, Stichprobengröße und Darstellung implementiert, um die Handelsanalyse zu unterstützen. Nach der Lektüre dieses Artikels werden Sie in der Lage sein:

  • Verschaffen Sie sich sofort einen Überblick über die Wahrscheinlichkeit einer beliebigen Anzahl von gewinnbringenden Trades für Ihre Strategie und passen Sie die Positionsgröße entsprechend an
  • Lesen Sie die Schiefe und die Konfidenzintervalle direkt aus dem Chart ab, um versteckte Risiken an den Randbereichen zu erkennen, bevor Sie das Handelsvolumen erhöhen.
  • Ziehen Sie das Tool auf ein beliebiges Chart und passen Sie dessen Größe an, um während des Live-Handels mehrere Szenarien nebeneinander zu vergleichen.

In den folgenden Teilen werden wir das Diagramm durch Hinzufügen weiterer Verteilungsfunktionen erweitern, doch zunächst wollen wir untersuchen, wie wir das 2D-Balkendiagramm mithilfe der Bibliothek MQL5-DirectX in ein 3D-Balkendiagramm umwandeln können. Bleiben Sie dran!

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/21315

Trading-Disziplin im Code verankern (Teil 3): Durchsetzung symbolbezogener Handelsgrenzen mit einer Whitelist in MQL5 Trading-Disziplin im Code verankern (Teil 3): Durchsetzung symbolbezogener Handelsgrenzen mit einer Whitelist in MQL5
In diesem Artikel wird ein MQL5-Framework beschrieben, das den Handel auf eine genehmigte Auswahl von Symbolen beschränkt. Die Lösung kombiniert eine gemeinsam genutzte Bibliothek, ein Konfigurations-Dashboard und einen Expert Advisor zur Durchsetzung, der jeden Handel anhand einer Whitelist überprüft und blockierte Versuche protokolliert. Sie enthält voll funktionsfähige Code-Beispiele, eine klare Erläuterung der strukturellen Entwurfsentscheidungen sowie Validierungstests, die eine zuverlässige Symbolfilterung, eine kontrollierte Marktpräsenz und eine transparente Überwachung der Regelumsetzung bestätigen.
Verwendung des MQL5-Wirtschaftskalenders zur Nachrichtenfilterung (Teil 2): Verwaltung von Stopps offener Positionen während Nachrichtenereignissen Verwendung des MQL5-Wirtschaftskalenders zur Nachrichtenfilterung (Teil 2): Verwaltung von Stopps offener Positionen während Nachrichtenereignissen
In Teil 2 erweitern wir den Nachrichtenfilter, um bestehende Positionen während Nachrichtenereignissen zu schützen. Anstatt Trades zu schließen, entfernen wir die Werte von Stop-Loss und Take-Profit vorübergehend und speichern sie sicher im Speicher. Wenn das Nachrichtenfenster endet, werden die Stopps deterministisch unter Einhaltung der Mindestabstände des Brokers wiederhergestellt und angepasst, falls der Kurs die ursprünglichen Niveaus bereits überschritten hat. Das Ergebnis ist ein Mechanismus, der die Handelslogik wahrt, ohne die Einstiegslogik zu beeinträchtigen, und dafür sorgt, dass der EA auch bei Volatilität die Kontrolle behält.
Implementierung des Truncated-Newton-Conjugate-Gradient-Algorithmus in MQL5 Implementierung des Truncated-Newton-Conjugate-Gradient-Algorithmus in MQL5
Dieser Artikel implementiert einen Truncated-Newton-Conjugate-Gradient (TNC) Optimierer mit Box-Beschränkungen in MQL5 und beschreibt detailliert seine Kernkomponenten: Skalierung, Projektion auf Grenzen, Liniensuche und Hessematrix-Vektor-Produkte mittels finiter Differenzen. Es stellt eine Zielfunktions-Wrapperklasse bereit, die analytische oder numerische Ableitungen unterstützt, und validiert die Implementierung am Rosenbrock-Benchmark. Ein Beispiel für eine logistische Regression veranschaulicht, wie TNC als weitgehend austauschbare Alternative zu LBFGS eingesetzt werden kann.
Larry Williams’ Marktgeheimnisse (Teil 13): Automatisierung von Hidden-Smash-Day-Umkehrmustern Larry Williams’ Marktgeheimnisse (Teil 13): Automatisierung von Hidden-Smash-Day-Umkehrmustern
Der Artikel beschreibt die Erstellung eines transparenten MQL5-Expert-Advisors für die Hidden-Smash-Day-Reversals von Larry Williams. Signale werden nur bei neuen Bars generiert: Eine Setup-Bar wird validiert und anschließend bestätigt, wenn die nächste Sitzung jenseits des Extremums schließt. Das Risiko wird über ATR oder strukturelle Stopps mit einem festgelegten Risiko-Ertrags-Verhältnis gesteuert; die Positionsgröße kann fest oder vom Kontostand abhängig sein, und Richtungsfilter sowie eine Ein-Positions-Regel gewährleisten reproduzierbare Tests.