MQL5-Handelswerkzeuge (Teil 22): Grafische Darstellung des Histogramms und der Wahrscheinlichkeitsfunktion (PMF) von der Binomialverteilung
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:
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.

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.

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.

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.

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.

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.

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.

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
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Trading-Disziplin im Code verankern (Teil 3): Durchsetzung symbolbezogener Handelsgrenzen mit einer Whitelist in MQL5
Verwendung des MQL5-Wirtschaftskalenders zur Nachrichtenfilterung (Teil 2): Verwaltung von Stopps offener Positionen während Nachrichtenereignissen
Implementierung des Truncated-Newton-Conjugate-Gradient-Algorithmus in MQL5
Larry Williams’ Marktgeheimnisse (Teil 13): Automatisierung von Hidden-Smash-Day-Umkehrmustern
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.