MQL5 Trading Tools (Teil 20): Grafiken auf Zeichenflächen mit statistischer Korrelations- und linearer Regressionsanalyse
Einführung
In unserem vorherigen Artikel (Teil 19) haben wir eine interaktive Werkzeugpalette in MetaQuotes Language 5 (MQL5) zum Zeichnen auf Charts entwickelt, die über verschiebbare Bedienfelder, Größenanpassung, Themenwechsel und Schaltflächen für verschiedene Analysewerkzeuge verfügt. In Teil 20 erstellen wir ein grafisches Tool auf einer Zeichenfläche zur Darstellung statistischer Korrelationen und linearer Regressionen zwischen zwei Variablen, das über verschiebbare und in der Größe anpassbare Elemente, dynamische Teilstriche und eine statistische Anzeige verfügt. Diese Visualisierung liefert Einblicke in den Pair-Trading-Ansatz anhand von Regressionsgeraden, Datenpunkten und Kennzahlen wie der Steigung und R-Quadrat. Wir werden die folgenden Themen behandeln:
- Grundlagen statistischer Korrelation und Regression als Grafiken auf Zeichenflächen
- Implementierung in MQL5
- Backtests
- Schlussfolgerung
Am Ende verfügen Sie über ein interaktives Regressionsdiagramm, das Sie für die Marktanalyse nutzen können – legen wir los!
Grundlagen statistischer Korrelation und Regression als Grafiken auf Zeichenflächen
Die statistische Korrelation misst die Stärke und Richtung des Zusammenhangs zwischen zwei Variablen, beispielsweise Kursen von Symbolen, anhand von Kennzahlen wie dem Pearson-Korrelationskoeffizienten, der zwischen -1 (invers) und 1 (direkt) liegt, während die lineare Regression eine Gerade an die Datenpunkte anpasst, um anhand der Steigung und des Achsenabschnitts Trends vorherzusagen. In den Diagrammen auf Zeichenflächen (canvas) werden diese durch Punkte in einem Streudiagramm und Regressionsgeraden für Vorhersagen visualisiert, ergänzt durch R-Quadrat zur Anzeige der Anpassungsqualität, was uns dabei hilft, Abhängigkeiten oder Divergenzen zwischen zwei Handelsinstrumenten zu erkennen. Diese grafische Darstellung auf verschiebbaren Zeichenflächen ermöglicht eine interaktive Erkundung von Marktzusammenhängen, ergänzt durch Statistikfelder für schnelle Einblicke. Unser Plan sieht vor, Symboldaten zu laden, mithilfe von ALGLIB eine Regression zu berechnen, Plots mit dynamischen Teilstrichen und geglätteten Punkten/Linien zu rendern und statistische Kennzahlen wie die Steigung und R-Quadrat-Wert in Überlagerungen anzuzeigen. Kurz gesagt: Hier ist eine visuelle Darstellung unserer Ziele.

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.
//+------------------------------------------------------------------+ //| Canvas Graphing PART 1 - Statistical Regression.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 <Math\Alglib\alglib.mqh> #include <Canvas\Canvas.mqh> //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ enum ResizeDirection { NO_RESIZE, // No resize RESIZE_BOTTOM_EDGE, // Resize bottom edge RESIZE_RIGHT_EDGE, // Resize right edge RESIZE_CORNER // Resize corner }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ sinput group "=== REGRESSION SETTINGS ===" input int maxHistoryBars = 200; // Maximum History Bars input ENUM_TIMEFRAMES chartTimeframe = PERIOD_CURRENT; // Chart Timeframe input string primarySymbol = "AUDUSDm"; // Primary Symbol (X-axis) input string secondarySymbol = "EURUSDm"; // Secondary Symbol (Y-axis) sinput group "=== CANVAS DISPLAY SETTINGS ===" input int initialCanvasX = 20; // Initial Canvas X Position input int initialCanvasY = 30; // Initial Canvas Y Position input int initialCanvasWidth = 600; // Initial Canvas Width input int initialCanvasHeight = 400; // Initial Canvas Height input int plotPadding = 10; // Plot Area Internal Padding (px) sinput group "=== THEME COLOR (SINGLE CONTROL!) ===" input color themeColor = clrDodgerBlue; // Master Theme Color input bool showBorderFrame = true; // Show Border Frame sinput group "=== REGRESSION LINE SETTINGS ===" input color regressionLineColor = clrBlue; // Regression Line Color input int regressionLineWidth = 2; // Regression Line Width input color dataPointsColor = clrRed; // Data Points Color input int dataPointSize = 3; // Data Point Size sinput group "=== BACKGROUND SETTINGS ===" input bool enableBackgroundFill = true; // Enable Background Fill input color backgroundTopColor = clrWhite; // Background Top Color input double backgroundOpacityLevel = 0.95; // Background Opacity (0-1) sinput group "=== TEXT AND LABELS ===" input int titleFontSize = 14; // Title Font Size input color titleTextColor = clrBlack; // Title Text Color input int labelFontSize = 11; // Label Font Size input color labelTextColor = clrBlack; // Label Text Color input int axisLabelFontSize = 12; // Axis Labels Font Size input bool showStatistics = true; // Show Statistics & Legend sinput group "=== STATS & LEGEND PANEL SETTINGS ===" input int statsPanelX = 70; // Stats Panel X Position input int statsPanelY = 10; // Stats Panel Y Offset (from header) input int statsPanelWidth = 130; // Stats Panel Width input int statsPanelHeight = 65; // Stats Panel Height input int panelFontSize = 13; // Stats & Legend Font Size input int legendHeight = 35; // Legend Panel Height sinput group "=== INTERACTION SETTINGS ===" input bool enableDragging = true; // Enable Canvas Dragging input bool enableResizing = true; // Enable Canvas Resizing input int resizeGripSize = 8; // Resize Grip Size (pixels) //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ CCanvas mainCanvas; //--- Declare main canvas string canvasObjectName = "RegressionCanvas_Main"; //--- Set canvas object name int currentPositionX = initialCanvasX; //--- Initialize current X position int currentPositionY = initialCanvasY; //--- Initialize current Y position int currentWidthPixels = initialCanvasWidth; //--- Initialize current width int currentHeightPixels = initialCanvasHeight; //--- Initialize current height bool isDraggingCanvas = false; //--- Initialize dragging flag bool isResizingCanvas = false; //--- Initialize resizing flag int dragStartX = 0, dragStartY = 0; //--- Initialize drag start coordinates int canvasStartX = 0, canvasStartY = 0; //--- Initialize canvas start coordinates int resizeStartX = 0, resizeStartY = 0; //--- Initialize resize start coordinates int resizeInitialWidth = 0, resizeInitialHeight = 0; //--- Initialize resize initial dimensions ResizeDirection activeResizeMode = NO_RESIZE; //--- Initialize active resize mode ResizeDirection hoverResizeMode = NO_RESIZE; //--- Initialize hover resize mode bool isHoveringCanvas = false; //--- Initialize canvas hover flag bool isHoveringHeader = false; //--- Initialize header hover flag bool isHoveringResizeZone = false; //--- Initialize resize hover flag int lastMouseX = 0, lastMouseY = 0; //--- Initialize last mouse coordinates int previousMouseButtonState = 0; //--- Initialize previous mouse state const int MIN_CANVAS_WIDTH = 300; //--- Set minimum canvas width const int MIN_CANVAS_HEIGHT = 200; //--- Set minimum canvas height const int HEADER_BAR_HEIGHT = 35; //--- Set header bar height double regressionSlope = 0.0; //--- Initialize regression slope double regressionIntercept = 0.0; //--- Initialize regression intercept double correlationCoefficient = 0.0; //--- Initialize correlation coefficient double rSquared = 0.0; //--- Initialize R-squared double primaryClosePrices[]; //--- Declare primary close prices array double secondaryClosePrices[]; //--- Declare secondary close prices array bool dataLoadedSuccessfully = false; //--- Initialize data loaded flag
Wir beginnen die Implementierung in MQL5, indem wir die Bibliothek ALGLIB mit #include <Math\Alglib\alglib.mqh> einbinden, für fortgeschrittene statistische Berechnungen wie lineare Regression, sowie die Canvas-Bibliothek über #include <Canvas\Canvas.mqh>, um die grafische Darstellung im Chart zu übernehmen. Als Nächstes definieren wir die Enumeration ResizeDirection mit den Optionen Keine Größenänderung, unterer Rand, rechter Rand und Ecke, um eine strukturierte Steuerung der interaktiven Größenänderung zu ermöglichen. Unter Eingabegruppen organisieren wir Parameter für Regressionseinstellungen wie maximale Baranzahl, Zeitrahmen und primäre/sekundäre Symbole; die Darstellung auf der Zeichenfläche mit Startposition, Größe und Abstand; eine Master-Themenfarbe und die Umschaltfunktion für Rahmen; Linien- und Punktstile; Hintergrundfüllung mit Farbe und Deckkraft; Textelemente einschließlich Schriftarten, Farben und Sichtbarkeit der Statistiken; Positionen und Größen der Bedienfelder für Statistiken/Legende; sowie Interaktionsoptionen für das Verschieben und die Größenänderung mit Griffgröße.
Zu den globalen Variablen gehören die Hauptzeichenfläche mainCanvas mit dem Namen RegressionCanvas_Main, die Verfolgung der aktuellen Position und der Abmessungen, Flags und Koordinaten für das Verschieben und die Größenänderung, Hover-Zustände und Mausverfolgung, Konstanten für Mindestgrößen und die Höhe der Kopfzeile, Regressionskennzahlen wie Steigung und R-Quadrat, Preis-Arrays für Symbole sowie ein Flag für das Laden der Daten. Als Nächstes werden wir einige Hilfsfunktionen für Farbschemata definieren, die bei der Farbabbildung helfen sollen.
//+------------------------------------------------------------------+ //| Theme Color Helper Functions | //+------------------------------------------------------------------+ color LightenColor(color baseColor, double factor) { uchar r = (uchar)((baseColor >> 16) & 0xFF); //--- Extract red component uchar g = (uchar)((baseColor >> 8) & 0xFF); //--- Extract green component uchar b = (uchar)(baseColor & 0xFF); //--- Extract blue component r = (uchar)MathMin(255, r + (255 - r) * factor); //--- Lighten red g = (uchar)MathMin(255, g + (255 - g) * factor); //--- Lighten green b = (uchar)MathMin(255, b + (255 - b) * factor); //--- Lighten blue return (r << 16) | (g << 8) | b; //--- Return lightened color } color DarkenColor(color baseColor, double factor) { uchar r = (uchar)((baseColor >> 16) & 0xFF); //--- Extract red component uchar g = (uchar)((baseColor >> 8) & 0xFF); //--- Extract green component uchar b = (uchar)(baseColor & 0xFF); //--- Extract blue component r = (uchar)(r * (1.0 - factor)); //--- Darken red g = (uchar)(g * (1.0 - factor)); //--- Darken green b = (uchar)(b * (1.0 - factor)); //--- Darken blue return (r << 16) | (g << 8) | b; //--- Return darkened color }
Hier implementieren wir zwei Hilfsfunktionen, LightenColor und DarkenColor, um die Hauptfarbe des Themes dynamisch anzupassen und so visuelle Effekte wie Farbverläufe und Hover-Effekte im Regressionsdiagramm zu erzielen. Bei LightenColor extrahieren wir mithilfe von Bitverschiebungen die RGB-Komponenten aus der Grundfarbe, hellen diese dann jeweils auf, indem wir einen um einen bestimmten Faktor skalierten Anteil der verbleibenden Intensität auf 255 addieren, wobei wir mit MathMin eine Begrenzung vornehmen, um einen Überlauf zu vermeiden, und kombinieren sie schließlich wieder zu einem Farbwert.
Ebenso extrahiert DarkenColor Komponenten und multipliziert jede davon mit (1 – Faktor), um die Intensität zu verringern und so Farbtöne für Rahmen oder Hintergründe zu erzeugen. Diese Funktionen sind für die Einheitlichkeit des Themes von entscheidender Bedeutung, da sie aus einer einzigen Eingabefarbe Variationen ableiten und so subtile Farbverläufe sowie reaktive UI-Elemente ermöglichen, ohne dass mehrere Farben fest codiert werden müssen. Um fortzufahren, initialisieren wir die Zeichenfläche und laden die Symboldaten, die wir für die Analyse verwenden werden. Wir werden Funktionen verwenden, um unseren Code modular und übersichtlich zu gestalten und ihn so für zukünftige Erweiterungen vorzubereiten. Um dies zu erreichen, haben wir folgenden Ansatz verfolgt.
//+------------------------------------------------------------------+ //| Create Regression Canvas | //+------------------------------------------------------------------+ bool CreateCanvas() { if (!mainCanvas.CreateBitmapLabel(0, 0, canvasObjectName, currentPositionX, currentPositionY, currentWidthPixels, currentHeightPixels, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create bitmap label return false; //--- Return failure } return true; //--- Return success } //+------------------------------------------------------------------+ //| Load Price Data for Regression Analysis | //+------------------------------------------------------------------+ bool loadSymbolClosePrices() { if (!SymbolSelect(primarySymbol, true)) { //--- Select primary symbol Print("ERROR: Primary symbol not found: ", primarySymbol); //--- Print error return false; //--- Return failure } if (!SymbolSelect(secondarySymbol, true)) { //--- Select secondary symbol Print("ERROR: Secondary symbol not found: ", secondarySymbol); //--- Print error return false; //--- Return failure } int copiedPrimary = CopyClose(primarySymbol, chartTimeframe, 1, maxHistoryBars, primaryClosePrices); //--- Copy primary closes if (copiedPrimary <= 0) { //--- Check copy success Print("ERROR: Failed to copy data for ", primarySymbol, ". Error: ", GetLastError()); //--- Print error return false; //--- Return failure } int copiedSecondary = CopyClose(secondarySymbol, chartTimeframe, 1, maxHistoryBars, secondaryClosePrices); //--- Copy secondary closes if (copiedSecondary <= 0) { //--- Check copy success Print("ERROR: Failed to copy data for ", secondarySymbol, ". Error: ", GetLastError()); //--- Print error return false; //--- Return failure } int actualBars = MathMin(copiedPrimary, copiedSecondary); //--- Get min bars ArrayResize(primaryClosePrices, actualBars); //--- Resize primary array ArrayResize(secondaryClosePrices, actualBars); //--- Resize secondary array dataLoadedSuccessfully = true; //--- Set loaded flag Print("SUCCESS: Loaded ", actualBars, " bars for both symbols"); //--- Print success return true; //--- Return success }
Zunächst implementieren wir die Funktion CreateCanvas, um den grafischen Hauptbereich für das Regressionsdiagramm einzurichten. Dabei rufen wir auf mainCanvas die Methode CreateBitmapLabel mit der aktuellen Position, den Abmessungen und COLOR_FORMAT_ARGB_NORMALIZE für Alpha-Unterstützung auf. Bei einem Fehlschlag wird false bzw. bei Erfolg true zurückgegeben. Diese Funktion rufen wir während der Initialisierung auf, um die visuelle Grundlage zu schaffen.
Als Nächstes erstellen wir die Funktion loadSymbolClosePrices, um historische Daten für die Analyse abzurufen. Dabei wählen wir zunächst mit SymbolSelect die Symbole aus und protokollieren Fehler, falls diese nicht gefunden werden. Anschließend kopieren wir die Schlusskurse über CopyClose für Primär- und Sekundärkurse in Arrays, prüfen, ob die Anzahl positiv ist, und behandeln Fehler mit GetLastError. Um Konsistenz zu gewährleisten, nehmen wir die Mindestanzahl an Bars zwischen den Kopien, passen die Größe der Arrays entsprechend an, setzen das Flag dataLoadedSuccessfully, geben bei geladenen Bars eine Erfolgsmeldung aus und geben true zurück, sodass Regressionsberechnungen nur mit gültigen Daten durchgeführt werden können. Wir können dies nun im Initialisierungs-Ereignis-Handler aufrufen, um die Initialisierung abzuschließen.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { currentPositionX = initialCanvasX; //--- Set current X from input currentPositionY = initialCanvasY; //--- Set current Y from input currentWidthPixels = initialCanvasWidth; //--- Set current width from input currentHeightPixels = initialCanvasHeight; //--- Set current height from input if (!CreateCanvas()) { //--- Create canvas or fail Print("ERROR: Failed to create regression canvas"); //--- Print error return(INIT_FAILED); //--- Return failure } if (!loadSymbolClosePrices()) { //--- Load prices or fail Print("ERROR: Failed to load price data for symbols"); //--- Print error return(INIT_FAILED); //--- Return failure } ChartRedraw(); //--- Redraw chart return(INIT_SUCCEEDED); //--- Return success }
In OnInit legen wir die aktuelle Position und die Abmessungen anhand der Anfangseingaben fest, um sicherzustellen, dass die Zeichenfläche an der vom Benutzer festgelegten Stelle und in der vom Benutzer festgelegten Größe beginnt. Als Nächstes rufen wir CreateCanvas auf, um den grafischen Hauptbereich zu initialisieren. Dabei wird ein Fehler protokolliert und bei Misserfolg INIT_FAILED zurückgegeben. Anschließend werden die Kursdaten mit loadSymbolClosePrices geladen, wobei Fehler auf ähnliche Weise behandelt werden, um zu verhindern, dass der Vorgang ohne gültige Eingaben fortgesetzt wird. Abschließend zeichnen wir das Chart neu, um die Darstellung anzuzeigen, und geben INIT_SUCCEEDED zurück, womit die Einrichtung für die interaktive Regressionsanalyse abgeschlossen ist. Wir können nun die Gleichung zur Berechnung der Regressionsgeraden definieren, damit wir sie bei der Visualisierung verwenden können.
//+------------------------------------------------------------------+ //| Calculate Linear Regression Parameters | //+------------------------------------------------------------------+ bool computeLinearRegression() { int dataSize = ArraySize(primaryClosePrices); //--- Get data size if (dataSize <= 0 || ArraySize(secondaryClosePrices) != dataSize) { //--- Check valid size return false; //--- Return failure } double tempPrimary[], tempSecondary[]; //--- Declare temp arrays ArraySetAsSeries(tempPrimary, true); //--- Set primary as series ArraySetAsSeries(tempSecondary, true); //--- Set secondary as series ArrayCopy(tempPrimary, primaryClosePrices); //--- Copy primary ArrayCopy(tempSecondary, secondaryClosePrices); //--- Copy secondary CMatrixDouble regressionMatrix(dataSize, 2); //--- Create regression matrix for (int i = 0; i < dataSize; i++) { //--- Loop over data regressionMatrix.Set(i, 0, tempPrimary[i]); //--- Set X value regressionMatrix.Set(i, 1, tempSecondary[i]); //--- Set Y value } CLinReg linearRegression; //--- Declare linear regression CLinearModel linearModel; //--- Declare linear model CLRReport regressionReport; //--- Declare report int returnCode; //--- Declare return code linearRegression.LRBuild(regressionMatrix, dataSize, 1, returnCode, linearModel, regressionReport); //--- Build regression if (returnCode != 1) { //--- Check success Print("ERROR: Linear regression calculation failed with code: ", returnCode); //--- Print error return false; //--- Return failure } int numberOfVars; //--- Declare vars count double coefficientsArray[]; //--- Declare coefficients linearRegression.LRUnpack(linearModel, coefficientsArray, numberOfVars); //--- Unpack model regressionSlope = coefficientsArray[0]; //--- Set slope regressionIntercept = coefficientsArray[1]; //--- Set intercept computeStatistics(); //--- Compute statistics PrintFormat("Regression Equation: Y = %.6f + %.6f * X", regressionIntercept, regressionSlope); //--- Print equation PrintFormat("Correlation: %.4f | R-Squared: %.4f", correlationCoefficient, rSquared); //--- Print stats return true; //--- Return success } //+------------------------------------------------------------------+ //| Calculate Regression Statistics | //+------------------------------------------------------------------+ void computeStatistics() { int n = ArraySize(primaryClosePrices); //--- Get size if (n <= 0) return; //--- Return if empty double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0; //--- Initialize sums for (int i = 0; i < n; i++) { //--- Loop over data double x = primaryClosePrices[i]; //--- Get X double y = secondaryClosePrices[i]; //--- Get Y sumX += x; //--- Accumulate X sumY += y; //--- Accumulate Y sumXY += x * y; //--- Accumulate XY sumX2 += x * x; //--- Accumulate X2 sumY2 += y * y; //--- Accumulate Y2 } double meanX = sumX / n; //--- Compute mean X double meanY = sumY / n; //--- Compute mean Y double numerator = n * sumXY - sumX * sumY; //--- Compute numerator double denominatorX = MathSqrt(n * sumX2 - sumX * sumX); //--- Compute denominator X double denominatorY = MathSqrt(n * sumY2 - sumY * sumY); //--- Compute denominator Y if (denominatorX != 0 && denominatorY != 0) { //--- Check denominators correlationCoefficient = numerator / (denominatorX * denominatorY); //--- Compute correlation rSquared = correlationCoefficient * correlationCoefficient; //--- Compute R-squared } else { //--- Handle zero denominators correlationCoefficient = 0; //--- Set correlation to 0 rSquared = 0; //--- Set R-squared to 0 } }
Wir implementieren die Funktion computeLinearRegression, um mithilfe der ALGLIB-Bibliothek eine lineare Regressionsanalyse durchzuführen. Dabei rufen wir zunächst die Datengröße aus primaryClosePrices ab und prüfen, ob sie mit secondaryClosePrices übereinstimmt. Ist sie ungültig oder leer, wird false zurückgegeben, um Fehler zu vermeiden. Als Nächstes bereiten wir die temporären Arrays tempPrimary und tempSecondary vor, die mit ArraySetAsSeries als Reihen festgelegt werden, um die richtige Reihenfolge zu gewährleisten, kopieren die Kursdaten und erstellen eine CMatrixDouble-Regressionsmatrix der Größe dataSize × 2, wobei in einer Schleife Spalte 0 mit den Primärkursen (X) und Spalte 1 mit den Sekundärkursen (Y) gefüllt wird.
Wir deklarieren ALGLIB-Objekte, darunter CLinReg für die Regression, CLinearModel für das Modell, CLRReport für die Ergebnisse sowie einen Rückgabewert, und rufen anschließend linearRegression.LRBuild mit der Matrix, der Größe und einer Variablen auf und prüfen, ob returnCode den Wert 1 hat, was einen Erfolg bedeutet; ist dies nicht der Fall, wird eine Fehlermeldung ausgegeben und false zurückgegeben. Bei Erfolg entpacken wir das Modell mit linearRegression.LRUnpack in coefficientsArray aus und weisen regressionSlope (Index 0) die Steigung und regressionIntercept (Index 1) den Achsenabschnitt zu. Anschließend rufen wir computeStatistics auf, um zusätzliche Kennzahlen zu berechnen, geben die Regressionsgleichung und die Statistiken mithilfe von PrintFormat aus und geben true zurück.
Die Funktion computeStatistics berechnet zur Überprüfung manuell die Korrelation und den Wert von R-Quadrat, wobei sie n aus der Array-Größe entnimmt und Summen für X, Y, XY, X², Y² initialisiert, um diese Werte anschließend in einer Schleife aus den Kurs-Arrays zu ermitteln. Wir berechnen die Mittelwerte meanX und meanY als Summen geteilt durch n, dann den Zähler als n·sumXY – sumX·sumY und die Nenner als Quadratwurzeln aus (n·sumX² – sumX²) sowie analog für Y, wobei correlationCoefficient auf den Zähler geteilt durch das Produkt der Nenner gesetzt wird, sofern dieser ungleich Null ist (Pearson-Koeffizient r, der die Stärke der linearen Beziehung von -1 bis 1 misst), andernfalls auf 0; R-Quadrat gibt als Quadratwert die vom Modell erklärte Varianz an. Diese statistische Berechnung ist entscheidend für die Quantifizierung von Paarbeziehungen, wobei eine hohe positive Korrelation auf ähnliche Kursbewegungen hindeutet und somit Strategien wie das Hedging unterstützt, während ein niedriger R-Quadrat-Wert auf eine schlechte Anpassung hinweist. Wir können die Funktion tatsächlich bei der Initialisierung aufrufen, um die Berechnung im Backend wie folgt durchzuführen.
if (!computeLinearRegression()) { //--- Compute regression or fail Print("ERROR: Failed to calculate regression parameters"); //--- Print error return(INIT_FAILED); //--- Return failure }
Daraus ergibt sich folgendes Ergebnis.

Wir können sehen, dass die Regression korrekt berechnet wurde. Nun können wir damit fortfahren, die Daten im Chart darzustellen. Lassen Sie uns nun die Zeichenfläche rendern, auf der wir die Plots darstellen werden.
//+------------------------------------------------------------------+ //| Render Regression Visualization | //+------------------------------------------------------------------+ void renderVisualization() { mainCanvas.Erase(0); //--- Erase canvas if (enableBackgroundFill) { //--- Check background fill drawGradientBackground(); //--- Draw gradient background } drawCanvasBorder(); //--- Draw border drawHeaderBar(); //--- Draw header bar mainCanvas.Update(); //--- Update canvas } //+------------------------------------------------------------------+ //| Draw Gradient Background | //+------------------------------------------------------------------+ void drawGradientBackground() { color bottomColor = LightenColor(themeColor, 0.85); //--- Compute bottom color for (int y = HEADER_BAR_HEIGHT; y < currentHeightPixels; y++) { //--- Loop over rows double gradientFactor = (double)(y - HEADER_BAR_HEIGHT) / (currentHeightPixels - HEADER_BAR_HEIGHT); //--- Compute factor color currentRowColor = InterpolateColors(backgroundTopColor, bottomColor, gradientFactor); //--- Interpolate color uchar alphaChannel = (uchar)(255 * backgroundOpacityLevel); //--- Compute alpha uint argbColor = ColorToARGB(currentRowColor, alphaChannel); //--- Get ARGB for (int x = 0; x < currentWidthPixels; x++) { //--- Loop over columns mainCanvas.PixelSet(x, y, argbColor); //--- Set pixel } } } //+------------------------------------------------------------------+ //| Draw Canvas Border | //+------------------------------------------------------------------+ void drawCanvasBorder() { if (!showBorderFrame) return; //--- Return if no border color borderColor = isHoveringResizeZone ? DarkenColor(themeColor, 0.2) : themeColor; //--- Get border color uint argbBorder = ColorToARGB(borderColor, 255); //--- Get ARGB border mainCanvas.Rectangle(0, 0, currentWidthPixels - 1, currentHeightPixels - 1, argbBorder); //--- Draw outer border mainCanvas.Rectangle(1, 1, currentWidthPixels - 2, currentHeightPixels - 2, argbBorder); //--- Draw inner border } //+------------------------------------------------------------------+ //| Draw Header Bar | //+------------------------------------------------------------------+ void drawHeaderBar() { color headerColor; //--- Declare header color if (isDraggingCanvas) { //--- Check dragging headerColor = DarkenColor(themeColor, 0.1); //--- Set darker color } else if (isHoveringHeader) { //--- Check hovering headerColor = LightenColor(themeColor, 0.4); //--- Set medium light } else { //--- Default headerColor = LightenColor(themeColor, 0.7); //--- Set very light } uint argbHeader = ColorToARGB(headerColor, 255); //--- Get ARGB header mainCanvas.FillRectangle(0, 0, currentWidthPixels - 1, HEADER_BAR_HEIGHT, argbHeader); //--- Fill header if (showBorderFrame) { //--- Check show border uint argbBorder = ColorToARGB(themeColor, 255); //--- Get ARGB border mainCanvas.Rectangle(0, 0, currentWidthPixels - 1, HEADER_BAR_HEIGHT, argbBorder); //--- Draw outer mainCanvas.Rectangle(1, 1, currentWidthPixels - 2, HEADER_BAR_HEIGHT - 1, argbBorder); //--- Draw inner } mainCanvas.FontSet("Arial Bold", titleFontSize); //--- Set title font uint argbText = ColorToARGB(titleTextColor, 255); //--- Get ARGB text string titleText = StringFormat("%s vs %s - Linear Regression", secondarySymbol, primarySymbol); //--- Format title mainCanvas.TextOut(currentWidthPixels / 2, (HEADER_BAR_HEIGHT - titleFontSize) / 2, titleText, argbText, TA_CENTER); //--- Draw title } //--- We call the visualization function in the initialization event //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { currentPositionX = initialCanvasX; //--- Set current X from input currentPositionY = initialCanvasY; //--- Set current Y from input currentWidthPixels = initialCanvasWidth; //--- Set current width from input currentHeightPixels = initialCanvasHeight; //--- Set current height from input if (!CreateCanvas()) { //--- Create canvas or fail Print("ERROR: Failed to create regression canvas"); //--- Print error return(INIT_FAILED); //--- Return failure } if (!loadSymbolClosePrices()) { //--- Load prices or fail Print("ERROR: Failed to load price data for symbols"); //--- Print error return(INIT_FAILED); //--- Return failure } if (!computeLinearRegression()) { //--- Compute regression or fail Print("ERROR: Failed to calculate regression parameters"); //--- Print error return(INIT_FAILED); //--- Return failure } renderVisualization(); //--- Render visualization ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); //--- Enable mouse events ChartRedraw(); //--- Redraw chart return(INIT_SUCCEEDED); //--- Return success }
Hier implementieren wir die Funktion renderVisualization, um das gesamte Diagramm auf der Zeichenfläche zu erstellen. Zunächst löschen wir den Inhalt, indem wir Erase auf 0 setzen, um eine leere Zeichenfläche zu erhalten. Anschließend zeichnen wir bedingt einen Farbverlaufshintergrund, wenn enableBackgroundFill wahr ist, gefolgt von der Umrandung und der Kopfzeile. Abschließend rufen wir Update auf, um den Inhalt anzuzeigen. Sie können jeden beliebigen Rahmenstil oder jede beliebige Farbgebung verwenden; wir haben uns für die Demonstration lediglich eine willkürliche Variante ausgedacht.
Anschließend erzeugt die Funktion drawGradientBackground einen vertikalen Farbverlauf von der Kopfzeile nach unten, wobei die Themenfarbe für den unteren Bereich mithilfe von LightenColor aufgehellt wird, wobei sie in einer Schleife über die Zeilen läuft, um Interpolationsfaktoren zu berechnen, Farben mit InterpolateColors zu mischen, ARGB-Werte mit Deckkraft zu versehen und jedes Pixel zeilenweise mit PixelSet festzulegen, um sanfte Übergänge zu erzielen. Um die Zeichenfläche einzurahmen, prüft drawCanvasBorder den Wert von showBorderFrame und bricht bei false vorzeitig ab; andernfalls wird die Themenfarbe bei einer Größenänderung per Mauszeiger mit DarkenColor abgedunkelt, in ARGB konvertiert und es werden mit Rectangle äußere und innere Rechtecke gezeichnet, um einen Rahmeneffekt zu erzielen.
Für den oberen Bereich wählt drawHeaderBar die Füllfarbe je nach Verschieben (abgedunkelt), beim Darüberfahren mit der Maus (mittlere Aufhellung) oder die Standardfarbe (starke Aufhellung) über DarkenColor oder LightenColor aus, füllt das Leistenrechteck aus, fügt bei Aktivierung Rahmen hinzu, legt die fette Schriftart Arial fest, formatiert den Titel mit Symbolen und zentriert ihn mit TextOut in der Textfarbe ARGB. In OnInit rufen wir nach der Einrichtung und der Datenverarbeitung die Funktion renderVisualization auf, um das anfängliche Chart zu generieren, aktivieren mit ChartSetInteger Mausbewegungsereignisse und zeichnen das Chart neu, damit es sofort angezeigt werden kann. Es gehört zu den bewährten Programmierpraktiken, den Fortschritt bei jedem Meilenstein zu kompilieren und zu testen. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

Nun können wir mit der Visualisierung unseres Plots fortfahren, bei der wir die Linie und die Datenpunkte zeichnen werden.
//+------------------------------------------------------------------+ //| Calculate optimal ticks with AGGRESSIVE spacing (fills space!) | //+------------------------------------------------------------------+ int calculateOptimalTicks(double minValue, double maxValue, int pixelRange, double &tickValues[]) { double range = maxValue - minValue; //--- Compute range if (range == 0 || pixelRange <= 0) { //--- Check invalid ArrayResize(tickValues, 1); //--- Resize to 1 tickValues[0] = minValue; //--- Set single tick return 1; //--- Return 1 } int targetTickCount = (int)(pixelRange / 50.0); //--- Compute target count if (targetTickCount < 3) targetTickCount = 3; //--- Min 3 if (targetTickCount > 20) targetTickCount = 20; //--- Max 20 double roughStep = range / (double)(targetTickCount - 1); //--- Compute rough step double magnitude = MathPow(10.0, MathFloor(MathLog10(roughStep))); //--- Compute magnitude double normalized = roughStep / magnitude; //--- Normalize double niceNormalized; //--- Declare nice normalized if (normalized <= 1.0) niceNormalized = 1.0; //--- Set 1.0 else if (normalized <= 1.5) niceNormalized = 1.0; //--- Set 1.0 else if (normalized <= 2.0) niceNormalized = 2.0; //--- Set 2.0 else if (normalized <= 2.5) niceNormalized = 2.0; //--- Set 2.0 else if (normalized <= 3.0) niceNormalized = 2.5; //--- Set 2.5 else if (normalized <= 4.0) niceNormalized = 4.0; //--- Set 4.0 else if (normalized <= 5.0) niceNormalized = 5.0; //--- Set 5.0 else if (normalized <= 7.5) niceNormalized = 5.0; //--- Set 5.0 else niceNormalized = 10.0; //--- Set 10.0 double step = niceNormalized * magnitude; //--- Compute step double tickMin = MathFloor(minValue / step) * step; //--- Compute tick min double tickMax = MathCeil(maxValue / step) * step; //--- Compute tick max int numTicks = (int)MathRound((tickMax - tickMin) / step) + 1; //--- Compute num ticks if (numTicks > 25) { //--- Check too many step *= 2.0; //--- Double step tickMin = MathFloor(minValue / step) * step; //--- Recalc min tickMax = MathCeil(maxValue / step) * step; //--- Recalc max numTicks = (int)MathRound((tickMax - tickMin) / step) + 1; //--- Recalc num } if (numTicks < 3) { //--- Check too few step /= 2.0; //--- Halve step tickMin = MathFloor(minValue / step) * step; //--- Recalc min tickMax = MathCeil(maxValue / step) * step; //--- Recalc max numTicks = (int)MathRound((tickMax - tickMin) / step) + 1; //--- Recalc num } ArrayResize(tickValues, numTicks); //--- Resize array for (int i = 0; i < numTicks; i++) { //--- Loop to set ticks tickValues[i] = tickMin + i * step; //--- Set tick value } return numTicks; //--- Return count } //+------------------------------------------------------------------+ //| Format tick label with appropriate precision | //+------------------------------------------------------------------+ string formatTickLabel(double value, double range) { if (range > 100) return DoubleToString(value, 0); //--- Format no decimals else if (range > 10) return DoubleToString(value, 1); //--- Format 1 decimal else if (range > 1) return DoubleToString(value, 2); //--- Format 2 decimals else if (range > 0.1) return DoubleToString(value, 3); //--- Format 3 decimals else return DoubleToString(value, 4); //--- Format 4 decimals } //+------------------------------------------------------------------+ //| Draw Regression Plot WITH CUSTOMIZABLE INTERNAL PADDING | //+------------------------------------------------------------------+ void drawRegressionPlot() { if (!dataLoadedSuccessfully) return; //--- Return if no data int plotAreaLeft = 60; //--- Set plot left int plotAreaRight = currentWidthPixels - 40; //--- Set plot right int plotAreaTop = HEADER_BAR_HEIGHT + 10; //--- Set plot top int plotAreaBottom = currentHeightPixels - 50; //--- Set plot bottom int drawAreaLeft = plotAreaLeft + plotPadding; //--- Set draw left int drawAreaRight = plotAreaRight - plotPadding; //--- Set draw right int drawAreaTop = plotAreaTop + plotPadding; //--- Set draw top int drawAreaBottom = plotAreaBottom - plotPadding; //--- Set draw bottom int plotWidth = drawAreaRight - drawAreaLeft; //--- Compute plot width int plotHeight = drawAreaBottom - drawAreaTop; //--- Compute plot height if (plotWidth <= 0 || plotHeight <= 0) return; //--- Return if invalid double minX = primaryClosePrices[0]; //--- Init min X double maxX = primaryClosePrices[0]; //--- Init max X double minY = secondaryClosePrices[0]; //--- Init min Y double maxY = secondaryClosePrices[0]; //--- Init max Y int dataPoints = ArraySize(primaryClosePrices); //--- Get data points for (int i = 1; i < dataPoints; i++) { //--- Loop over points if (primaryClosePrices[i] < minX) minX = primaryClosePrices[i]; //--- Update min X if (primaryClosePrices[i] > maxX) maxX = primaryClosePrices[i]; //--- Update max X if (secondaryClosePrices[i] < minY) minY = secondaryClosePrices[i]; //--- Update min Y if (secondaryClosePrices[i] > maxY) maxY = secondaryClosePrices[i]; //--- Update max Y } double rangeX = maxX - minX; //--- Compute range X double rangeY = maxY - minY; //--- Compute range Y if (rangeX == 0) rangeX = 1; //--- Set min range X if (rangeY == 0) rangeY = 1; //--- Set min range Y uint argbAxisColor = ColorToARGB(clrBlack, 255); //--- Get axis ARGB for (int thick = 0; thick < 2; thick++) { //--- Loop for thick Y-axis mainCanvas.Line(plotAreaLeft - thick, plotAreaTop, plotAreaLeft - thick, plotAreaBottom, argbAxisColor); //--- Draw Y-axis line } for (int thick = 0; thick < 2; thick++) { //--- Loop for thick X-axis mainCanvas.Line(plotAreaLeft, plotAreaBottom + thick, plotAreaRight, plotAreaBottom + thick, argbAxisColor); //--- Draw X-axis line } mainCanvas.FontSet("Arial", axisLabelFontSize); //--- Set tick font uint argbTickLabel = ColorToARGB(clrBlack, 255); //--- Get tick label ARGB double yTickValues[]; //--- Declare Y ticks int numYTicks = calculateOptimalTicks(minY, maxY, plotHeight, yTickValues); //--- Compute Y ticks for (int i = 0; i < numYTicks; i++) { //--- Loop over Y ticks double yValue = yTickValues[i]; //--- Get Y value if (yValue < minY || yValue > maxY) continue; //--- Skip out of range int yPos = drawAreaBottom - (int)((yValue - minY) / rangeY * plotHeight); //--- Compute Y pos mainCanvas.Line(plotAreaLeft - 5, yPos, plotAreaLeft, yPos, argbAxisColor); //--- Draw tick string yLabel = formatTickLabel(yValue, rangeY); //--- Format label mainCanvas.TextOut(plotAreaLeft - 8, yPos - axisLabelFontSize/2, yLabel, argbTickLabel, TA_RIGHT); //--- Draw label } double xTickValues[]; //--- Declare X ticks int numXTicks = calculateOptimalTicks(minX, maxX, plotWidth, xTickValues); //--- Compute X ticks for (int i = 0; i < numXTicks; i++) { //--- Loop over X ticks double xValue = xTickValues[i]; //--- Get X value if (xValue < minX || xValue > maxX) continue; //--- Skip out of range int xPos = drawAreaLeft + (int)((xValue - minX) / rangeX * plotWidth); //--- Compute X pos mainCanvas.Line(xPos, plotAreaBottom, xPos, plotAreaBottom + 5, argbAxisColor); //--- Draw tick string xLabel = formatTickLabel(xValue, rangeX); //--- Format label mainCanvas.TextOut(xPos, plotAreaBottom + 7, xLabel, argbTickLabel, TA_CENTER); //--- Draw label } uint argbPoints = ColorToARGB(dataPointsColor, 255); //--- Get points ARGB for (int i = 0; i < dataPoints; i++) { //--- Loop over points int screenX = drawAreaLeft + (int)((primaryClosePrices[i] - minX) / rangeX * plotWidth); //--- Compute screen X int screenY = drawAreaBottom - (int)((secondaryClosePrices[i] - minY) / rangeY * plotHeight); //--- Compute screen Y drawCirclePoint(screenX, screenY, dataPointSize, argbPoints); //--- Draw point } double lineStartY = regressionIntercept + regressionSlope * minX; //--- Compute start Y double lineEndY = regressionIntercept + regressionSlope * maxX; //--- Compute end Y int lineStartScreenX = drawAreaLeft; //--- Set start screen X int lineStartScreenY = drawAreaBottom - (int)((lineStartY - minY) / rangeY * plotHeight); //--- Compute start screen Y int lineEndScreenX = drawAreaRight; //--- Set end screen X int lineEndScreenY = drawAreaBottom - (int)((lineEndY - minY) / rangeY * plotHeight); //--- Compute end screen Y uint argbLine = ColorToARGB(regressionLineColor, 255); //--- Get line ARGB for (int w = 0; w < regressionLineWidth; w++) { //--- Loop for width mainCanvas.LineAA(lineStartScreenX, lineStartScreenY + w, lineEndScreenX, lineEndScreenY + w, argbLine); //--- Draw line } mainCanvas.FontSet("Arial Bold", labelFontSize); //--- Set axis label font uint argbAxisLabel = ColorToARGB(clrBlack, 255); //--- Get axis label ARGB string xAxisLabel = primarySymbol + " (X-axis)"; //--- Set X label mainCanvas.TextOut(currentWidthPixels / 2, currentHeightPixels - 20, xAxisLabel, argbAxisLabel, TA_CENTER); //--- Draw X label string yAxisLabel = secondarySymbol + " (Y-axis)"; //--- Set Y label mainCanvas.FontAngleSet(900); //--- Set vertical angle mainCanvas.TextOut(12, currentHeightPixels / 2, yAxisLabel, argbAxisLabel, TA_CENTER); //--- Draw Y label mainCanvas.FontAngleSet(0); //--- Reset angle } //+------------------------------------------------------------------+ //| Draw Circle Point with Anti-Aliasing (smooth like CGraphic) | //+------------------------------------------------------------------+ void drawCirclePoint(int centerX, int centerY, int radius, uint argbColor) { uchar srcAlpha = (uchar)((argbColor >> 24) & 0xFF); //--- Extract source alpha uchar srcRed = (uchar)((argbColor >> 16) & 0xFF); //--- Extract source red uchar srcGreen = (uchar)((argbColor >> 8) & 0xFF); //--- Extract source green uchar srcBlue = (uchar)(argbColor & 0xFF); //--- Extract source blue double radiusDouble = (double)radius + 0.5; //--- Adjust radius int extent = radius + 2; //--- Compute extent for (int dy = -extent; dy <= extent; dy++) { //--- Loop over dy for (int dx = -extent; dx <= extent; dx++) { //--- Loop over dx double distance = MathSqrt((double)(dx * dx + dy * dy)); //--- Compute distance if (distance <= radiusDouble) { //--- Check within radius double coverage = 1.0; //--- Set full coverage if (distance > radiusDouble - 1.0) { //--- Check edge coverage = radiusDouble - distance; //--- Compute coverage if (coverage < 0) coverage = 0; //--- Clamp min if (coverage > 1.0) coverage = 1.0; //--- Clamp max } uchar finalAlpha = (uchar)(srcAlpha * coverage); //--- Compute final alpha if (finalAlpha == 0) continue; //--- Skip if transparent uint pixelColor = ((uint)finalAlpha << 24) | ((uint)srcRed << 16) | ((uint)srcGreen << 8) | (uint)srcBlue; //--- Compose color int px = centerX + dx; //--- Compute pixel X int py = centerY + dy; //--- Compute pixel Y if (px >= 0 && px < currentWidthPixels && py >= 0 && py < currentHeightPixels) { //--- Check bounds blendPixelSet(mainCanvas, px, py, pixelColor); //--- Blend pixel } } } } }
Für die Darstellung nutzen wir die Funktion drawRegressionPlot, um die Regressionsanalyse auf der Zeichenfläche zu visualisieren. Dabei wird zunächst vorzeitig beendet, wenn keine Daten geladen wurden, anschließend werden die Grenzen des Darstellungsbereichs mit festen Rändern definiert und plotPadding für den internen Abstand angewendet, die effektiven Zeichnungsabmessungen werden berechnet und der Vorgang bei Ungültigkeit beendet. Als Nächstes ermitteln wir die Min- und Max-Werte für X (Primärpreise) und Y (Sekundärpreise), indem wir die Arrays durchlaufen, die Nullbereiche zur Skalierung auf 1 anpassen, Schwarz für die Achsen in ARGB umwandeln und mit der Funktion Line in Schleifen verdickte Y- und X-Linien mit doppelter Breite zeichnen.
Um die Achsen zu beschriften, legen wir mit FontSet die Schriftart Arial fest, bereiten ARGB für die Teilstriche vor, berechnen die Y-Ticks über calculateOptimalTicks in yTickValues, positionieren jeden einzelnen in einer Schleife, zeichnen kurze Ticks mit Line und fügen mithilfe von formatTickLabel rechtsbündige Beschriftungen basierend auf dem Bereich hinzu; ebenso verfahren wir bei den X-Teilstrichen mit unten zentrierten Beschriftungen. Wir zeichnen Datenpunkte, indem wir Preise in Bildschirmkoordinaten umrechnen, die anhand von Bereichen und Abmessungen skaliert werden, und für jeden Punkt die Funktion drawCirclePoint mit Radius und ARGB aus den jeweiligen Eingaben aufrufen.
Für die Regressionsgerade berechnen wir die Start- und Endwerte von Y anhand des Achsenabschnitts und der Steigung über die Minimal- und Maximalwerte von X, ordnen diese den Bildschirmpositionen zu, bereiten ARGB vor und zeichnen Segmente mit Anti-Aliasing, wobei die Schleife LineAA für die Breite verwendet wird. Abschließend fügen wir eine fettgedruckte Beschriftung für die X-Achse hinzu, die unten zentriert ist, sowie eine um 90 Grad vertikal gedrehte Beschriftung für die Y-Achse, die mit FontAngleSet links zentriert ist, und setzen den Winkel anschließend zurück. In der Funktion drawCirclePoint extrahieren wir die ARGB-Komponenten, passen den Radius für das Anti-Aliasing an, durchlaufen einen erweiterten Bereich in einer Schleife, berechnen Abstände mit MathSqrt, legen die vollständige oder randseitige Abdeckung fest (Ausblenden an den Rändern), berechnen den endgültigen Alpha-Wert und die Pixelfarbe und mischen die betroffenen Pixel mithilfe von blendPixelSet, um glatte Kreise zu erzeugen, die der Qualität von CGraphic nachempfunden sind. Wenn wir diese Funktion in der Basis-Render-Funktion aufrufen, erhalten wir das folgende Ergebnis.

Wir können sehen, dass wir die Grafik zur Regressionsanalyse erfolgreich dargestellt haben. Nun müssen die zusammenfassenden Kennzahlen in Panels in der oberen linken Ecke der Zeichenfläche angezeigt werden, aber Sie können sie natürlich auch an einer beliebigen anderen Stelle darstellen. Wir hätten sie in einer separaten Zeichenfläche unterhalb oder rechts neben der Hauptzeichenfläche darstellen können, aber die Darstellung oberhalb der Hauptzeichenfläche erschien uns moderner und intuitiver, da wir auch die Möglichkeit verschachtelter Zeichenflächen oder einer Überlagerung ausprobieren wollten. Das liegt allerdings ganz bei Ihnen. Um das zu erreichen, haben wir folgende Logik angewendet. Fangen wir mit dem Statistikfenster an.
//+------------------------------------------------------------------+ //| Draw Statistics Panel AS OVERLAY | //+------------------------------------------------------------------+ void drawStatisticsPanel() { int panelX = statsPanelX; //--- Set panel X int panelY = HEADER_BAR_HEIGHT + statsPanelY; //--- Set panel Y int panelWidth = statsPanelWidth; //--- Set panel width int panelHeight = statsPanelHeight; //--- Set panel height color panelBgColor = LightenColor(themeColor, 0.9); //--- Compute bg color uchar bgAlpha = 153; //--- Set alpha uint argbPanelBg = ColorToARGB(panelBgColor, bgAlpha); //--- Get panel bg ARGB uint argbBorder = ColorToARGB(themeColor, 255); //--- Get border ARGB uint argbText = ColorToARGB(clrBlack, 255); //--- Get text ARGB for (int y = panelY; y <= panelY + panelHeight; y++) { //--- Loop over rows for (int x = panelX; x <= panelX + panelWidth; x++) { //--- Loop over columns blendPixelSet(mainCanvas, x, y, argbPanelBg); //--- Blend bg pixel } } for (int x = panelX; x <= panelX + panelWidth; x++) { //--- Draw top border blendPixelSet(mainCanvas, x, panelY, argbBorder); //--- Blend border pixel } for (int y = panelY; y <= panelY + panelHeight; y++) { //--- Draw right border blendPixelSet(mainCanvas, panelX + panelWidth, y, argbBorder); //--- Blend border pixel } for (int y = panelY; y <= panelY + panelHeight; y++) { //--- Draw left border blendPixelSet(mainCanvas, panelX, y, argbBorder); //--- Blend border pixel } mainCanvas.FontSet("Arial", panelFontSize); //--- Set stats font int textY = panelY + 8; //--- Set text Y int lineSpacing = panelFontSize; //--- Set line spacing string equationText = StringFormat("Y = %.3f + %.3f * X", regressionIntercept, regressionSlope); //--- Format equation mainCanvas.TextOut(panelX + 8, textY, equationText, argbText, TA_LEFT); //--- Draw equation textY += lineSpacing; //--- Update Y string correlationText = StringFormat("Correlation: %.4f", correlationCoefficient); //--- Format correlation mainCanvas.TextOut(panelX + 8, textY, correlationText, argbText, TA_LEFT); //--- Draw correlation textY += lineSpacing; //--- Update Y string rSquaredText = StringFormat("R-Squared: %.4f", rSquared); //--- Format R-squared mainCanvas.TextOut(panelX + 8, textY, rSquaredText, argbText, TA_LEFT); //--- Draw R-squared textY += lineSpacing; //--- Update Y string dataPointsText = StringFormat("Points: %d", ArraySize(primaryClosePrices)); //--- Format points mainCanvas.TextOut(panelX + 8, textY, dataPointsText, argbText, TA_LEFT); //--- Draw points }
Wir implementieren die Funktion drawStatisticsPanel, um ein halbtransparentes Fenster mit Regressionskennzahlen auf der Zeichenfläche einzublenden. Die Positionierung erfolgt anhand von Eingabewerten wie statsPanelX und einem Versatz zur Höhe der Kopfzeile, wobei Breite und Höhe fest vorgegeben sind. Anschließend hellen wir die Themenfarbe für den Hintergrund mit LightenColor auf, stellen den Alpha-Wert auf 153 ein, um einen dezenten Effekt zu erzielen, wandeln in einen ARGB-Farbwert um und füllen den Bereich des Fensters Pixel für Pixel mithilfe verschachtelter Schleifen und blendPixelSet, um eine nahtlose Integration in den vorhandenen Inhalt zu gewährleisten.
Um den Rahmen zu gestalten, zeichnen wir obere, rechte, linke und untere Rahmenlinien, indem wir die Randpixel in Schleifen mit dem ARGB-Thema überblenden und so einen einfachen Umriss ohne vollständige Rechtecke erzeugen. Wir legen die Schriftart Arial für panelFontSize fest, initialisieren den Text Y mit einem Abstand und Zeilenabstand, der sich aus der Schriftgröße ergibt, formatieren und zeichnen dann die Gleichung mithilfe von StringFormat und TextOut linksbündig und aktualisieren dabei Y; ebenso verfahren wir bei der Korrelation, R-Quadrat und der Anzahl der Datenpunkte, die sich aus der Array-Größe ergibt. Dieses Feld stellt wichtige Kennzahlen wie Y = y-Achsenabschnitt + Steigung * X in kompakter Form dar und verbessert so die Verständlichkeit, ohne das Hauptdiagramm zu überladen. Für das Legendenfeld haben wir einen ähnlichen Ansatz gewählt.
//+------------------------------------------------------------------+ //| Draw Legend | //+------------------------------------------------------------------+ void drawLegend() { int legendX = statsPanelX; //--- Set legend X int legendY = HEADER_BAR_HEIGHT + statsPanelY + statsPanelHeight; //--- Set legend Y int legendWidth = statsPanelWidth; //--- Set legend width int legendHeightThis = legendHeight; //--- Set legend height color legendBgColor = LightenColor(themeColor, 0.9); //--- Compute bg color uchar bgAlpha = 153; //--- Set alpha uint argbLegendBg = ColorToARGB(legendBgColor, bgAlpha); //--- Get legend bg ARGB uint argbBorder = ColorToARGB(themeColor, 255); //--- Get border ARGB uint argbText = ColorToARGB(clrBlack, 255); //--- Get text ARGB for (int y = legendY; y <= legendY + legendHeightThis; y++) { //--- Loop over rows for (int x = legendX; x <= legendX + legendWidth; x++) { //--- Loop over columns blendPixelSet(mainCanvas, x, y, argbLegendBg); //--- Blend bg pixel } } for (int x = legendX; x <= legendX + legendWidth; x++) { //--- Draw top border blendPixelSet(mainCanvas, x, legendY, argbBorder); //--- Blend border pixel } for (int y = legendY; y <= legendY + legendHeightThis; y++) { //--- Draw right border blendPixelSet(mainCanvas, legendX + legendWidth, y, argbBorder); //--- Blend border pixel } for (int x = legendX; x <= legendX + legendWidth; x++) { //--- Draw bottom border blendPixelSet(mainCanvas, x, legendY + legendHeightThis, argbBorder); //--- Blend border pixel } for (int y = legendY; y <= legendY + legendHeightThis; y++) { //--- Draw left border blendPixelSet(mainCanvas, legendX, y, argbBorder); //--- Blend border pixel } mainCanvas.FontSet("Arial", panelFontSize); //--- Set legend font int itemY = legendY + 10; //--- Set item Y int lineSpacing = panelFontSize; //--- Set line spacing uint argbRedDot = ColorToARGB(dataPointsColor, 255); //--- Get red dot ARGB drawCirclePoint(legendX + 12, itemY, dataPointSize, argbRedDot); //--- Draw data point mainCanvas.TextOut(legendX + 22, itemY - 4, "Data Points", argbText, TA_LEFT); //--- Draw data label itemY += lineSpacing; //--- Update Y uint argbBlueLine = ColorToARGB(regressionLineColor, 255); //--- Get blue line ARGB for (int i = 0; i < 15; i++) { //--- Loop to draw line blendPixelSet(mainCanvas, legendX + 7 + i, itemY, argbBlueLine); //--- Blend line pixel blendPixelSet(mainCanvas, legendX + 7 + i, itemY + 1, argbBlueLine); //--- Blend below pixel } mainCanvas.TextOut(legendX + 27, itemY - 4, "Regression Line", argbText, TA_LEFT); //--- Draw line label }
Wir implementieren die Funktion drawLegend, um unterhalb der Statistiken ein halbtransparentes Überlagerungsfeld für visuelle Schlüssel hinzuzufügen. Dabei positionieren wir es anhand von statsPanelX und berechnen die Y-Koordinate anhand der Höhe der Statistiken mit derselben Breite wie das Statistikfeld und der über den Eingabeparameter festgelegten Legendenhöhe. Als Nächstes hellen wir die Themenfarbe für den Hintergrund mit LightenColor auf, setzen den Alpha-Wert auf 153, konvertieren in ARGB und füllen den Bereich mithilfe verschachtelter Schleifen mit blendPixelSet zur Integration; zeichnen die oberen, rechten, unteren und linken Ränder auf ähnliche Weise mit dem Themen-ARGB, genau wie beim Statistikfeld.
Wir legen die Schriftart Arial mit der Größe panelFontSize fest, initialisieren das Element Y mit Innenabstand und Zeilenabstand entsprechend der Schriftgröße, zeichnen dann mithilfe von drawCirclePoint ein rotes Datenpunktsymbol an der angepassten Position, gefolgt von der Beschriftung Data Points mit linksbündigem Text (TextOut), und aktualisieren Y. Um die Linie darzustellen, erstellen wir ein kurzes blaues Segment, indem wir 15 Pixel horizontal mit ARGB-Werten aus der Regressionsfarbe überblenden, einschließlich einer unteren Linie für die Dicke, und fügen auf ähnliche Weise die Beschriftung Regressionslinie hinzu, um klare visuelle Anhaltspunkte zu bieten. Wenn wir diese Funktionen aufrufen, erhalten wir das folgende Ergebnis.

Nachdem wir das Statistik- und Legendenfeld hinzugefügt haben, wenden wir uns nun den Größenänderungsanzeigen zu, die hervorgehoben werden, wenn wir mit der Maus über den unteren oder rechten Rand sowie über die rechte untere Ecke fahren. In den bisherigen Tools, die wir in dieser Reihe erstellt haben, haben wir Symbole verwendet, doch für dieses Bedienfeld werden wir einen anderen Ansatz verfolgen und die Indikatoren ohne externe Hilfsmittel miteinander verschmelzen. Dazu müssen wir Chart-Ereignisse verarbeiten. Lassen Sie uns doch einfach alle Chart-Ereignisse auf einmal bearbeiten.
//+------------------------------------------------------------------+ //| Draw Resize Indicator | //+------------------------------------------------------------------+ void drawResizeIndicator() { uint argbIndicator = ColorToARGB(themeColor, 255); //--- Get indicator ARGB if (hoverResizeMode == RESIZE_CORNER || activeResizeMode == RESIZE_CORNER) { //--- Check corner int cornerX = currentWidthPixels - resizeGripSize; //--- Compute corner X int cornerY = currentHeightPixels - resizeGripSize; //--- Compute corner Y mainCanvas.FillRectangle(cornerX, cornerY, currentWidthPixels - 1, currentHeightPixels - 1, argbIndicator); //--- Fill corner for (int i = 0; i < 3; i++) { //--- Loop for lines int offset = i * 3; //--- Compute offset mainCanvas.Line(cornerX + offset, currentHeightPixels - 1, currentWidthPixels - 1, cornerY + offset, argbIndicator); //--- Draw diagonal } } if (hoverResizeMode == RESIZE_RIGHT_EDGE || activeResizeMode == RESIZE_RIGHT_EDGE) { //--- Check right int indicatorY = currentHeightPixels / 2 - 15; //--- Compute indicator Y mainCanvas.FillRectangle(currentWidthPixels - 3, indicatorY, currentWidthPixels - 1, indicatorY + 30, argbIndicator); //--- Fill right } if (hoverResizeMode == RESIZE_BOTTOM_EDGE || activeResizeMode == RESIZE_BOTTOM_EDGE) { //--- Check bottom int indicatorX = currentWidthPixels / 2 - 15; //--- Compute indicator X mainCanvas.FillRectangle(indicatorX, currentHeightPixels - 3, indicatorX + 30, currentHeightPixels - 1, argbIndicator); //--- Fill bottom } } //+------------------------------------------------------------------+ //| Check if Mouse is Over Header | //+------------------------------------------------------------------+ bool isMouseOverHeaderBar(int mouseX, int mouseY) { return (mouseX >= currentPositionX && mouseX <= currentPositionX + currentWidthPixels && mouseY >= currentPositionY && mouseY <= currentPositionY + HEADER_BAR_HEIGHT); //--- Return if over header } //+------------------------------------------------------------------+ //| Check if Mouse is in Resize Zone | //+------------------------------------------------------------------+ bool isMouseInResizeZone(int mouseX, int mouseY, ResizeDirection &resizeMode) { if (!enableResizing) return false; //--- Return false if disabled int relativeX = mouseX - currentPositionX; //--- Compute relative X int relativeY = mouseY - currentPositionY; //--- Compute relative Y bool nearRightEdge = (relativeX >= currentWidthPixels - resizeGripSize && relativeX <= currentWidthPixels && relativeY >= HEADER_BAR_HEIGHT && relativeY <= currentHeightPixels); //--- Check right edge bool nearBottomEdge = (relativeY >= currentHeightPixels - resizeGripSize && relativeY <= currentHeightPixels && relativeX >= 0 && relativeX <= currentWidthPixels); //--- Check bottom edge bool nearCorner = (relativeX >= currentWidthPixels - resizeGripSize && relativeX <= currentWidthPixels && relativeY >= currentHeightPixels - resizeGripSize && relativeY <= currentHeightPixels); //--- Check corner if (nearCorner) { //--- Set corner resizeMode = RESIZE_CORNER; //--- Set mode return true; //--- Return true } else if (nearRightEdge) { //--- Set right resizeMode = RESIZE_RIGHT_EDGE; //--- Set mode return true; //--- Return true } else if (nearBottomEdge) { //--- Set bottom resizeMode = RESIZE_BOTTOM_EDGE; //--- Set mode return true; //--- Return true } resizeMode = NO_RESIZE; //--- Set no resize return false; //--- Return false } //+------------------------------------------------------------------+ //| Handle Canvas Resizing | //+------------------------------------------------------------------+ void handleCanvasResize(int mouseX, int mouseY) { int deltaX = mouseX - resizeStartX; //--- Compute delta X int deltaY = mouseY - resizeStartY; //--- Compute delta Y int newWidth = currentWidthPixels; //--- Init new width int newHeight = currentHeightPixels; //--- Init new height if (activeResizeMode == RESIZE_RIGHT_EDGE || activeResizeMode == RESIZE_CORNER) { //--- Check right or corner newWidth = MathMax(MIN_CANVAS_WIDTH, resizeInitialWidth + deltaX); //--- Compute new width } if (activeResizeMode == RESIZE_BOTTOM_EDGE || activeResizeMode == RESIZE_CORNER) { //--- Check bottom or corner newHeight = MathMax(MIN_CANVAS_HEIGHT, resizeInitialHeight + deltaY); //--- Compute new height } int chartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width int chartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height newWidth = MathMin(newWidth, chartWidth - currentPositionX - 10); //--- Clamp width newHeight = MathMin(newHeight, chartHeight - currentPositionY - 10); //--- Clamp height if (newWidth != currentWidthPixels || newHeight != currentHeightPixels) { //--- Check changed currentWidthPixels = newWidth; //--- Update width currentHeightPixels = newHeight; //--- Update height mainCanvas.Resize(currentWidthPixels, currentHeightPixels); //--- Resize canvas ObjectSetInteger(0, canvasObjectName, OBJPROP_XSIZE, currentWidthPixels); //--- Set X size ObjectSetInteger(0, canvasObjectName, OBJPROP_YSIZE, currentHeightPixels); //--- Set Y size renderVisualization(); //--- Render again ChartRedraw(); //--- Redraw chart } } //+------------------------------------------------------------------+ //| Handle Canvas Dragging | //+------------------------------------------------------------------+ void handleCanvasDrag(int mouseX, int mouseY) { int deltaX = mouseX - dragStartX; //--- Compute delta X int deltaY = mouseY - dragStartY; //--- Compute delta Y int newX = canvasStartX + deltaX; //--- Compute new X int newY = canvasStartY + deltaY; //--- Compute new Y int chartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width int chartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height newX = MathMax(0, MathMin(chartWidth - currentWidthPixels, newX)); //--- Clamp X newY = MathMax(0, MathMin(chartHeight - currentHeightPixels, newY)); //--- Clamp Y currentPositionX = newX; //--- Update X currentPositionY = newY; //--- Update Y ObjectSetInteger(0, canvasObjectName, OBJPROP_XDISTANCE, currentPositionX); //--- Set X distance ObjectSetInteger(0, canvasObjectName, OBJPROP_YDISTANCE, currentPositionY); //--- Set Y distance ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Interpolate Between Two Colors | //+------------------------------------------------------------------+ color InterpolateColors(color startColor, color endColor, double factor) { uchar r1 = (uchar)((startColor >> 16) & 0xFF); //--- Extract start red uchar g1 = (uchar)((startColor >> 8) & 0xFF); //--- Extract start green uchar b1 = (uchar)(startColor & 0xFF); //--- Extract start blue uchar r2 = (uchar)((endColor >> 16) & 0xFF); //--- Extract end red uchar g2 = (uchar)((endColor >> 8) & 0xFF); //--- Extract end green uchar b2 = (uchar)(endColor & 0xFF); //--- Extract end blue uchar r = (uchar)(r1 + factor * (r2 - r1)); //--- Interpolate red uchar g = (uchar)(g1 + factor * (g2 - g1)); //--- Interpolate green uchar b = (uchar)(b1 + factor * (b2 - b1)); //--- Interpolate blue return (r << 16) | (g << 8) | b; //--- Return interpolated color } //+------------------------------------------------------------------+ //| Blend pixel with proper alpha blending | //+------------------------------------------------------------------+ void blendPixelSet(CCanvas &canvas, int x, int y, uint src) { if (x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return; //--- Return if out of bounds uint dst = canvas.PixelGet(x, y); //--- Get destination pixel double sa = ((src >> 24) & 0xFF) / 255.0; //--- Compute source alpha double sr = ((src >> 16) & 0xFF) / 255.0; //--- Compute source red double sg = ((src >> 8) & 0xFF) / 255.0; //--- Compute source green double sb = (src & 0xFF) / 255.0; //--- Compute source blue double da = ((dst >> 24) & 0xFF) / 255.0; //--- Compute dest alpha double dr = ((dst >> 16) & 0xFF) / 255.0; //--- Compute dest red double dg = ((dst >> 8) & 0xFF) / 255.0; //--- Compute dest green double db = (dst & 0xFF) / 255.0; //--- Compute dest blue double out_a = sa + da * (1 - sa); //--- Compute out alpha if (out_a == 0) { //--- Check transparent canvas.PixelSet(x, y, 0); //--- Set transparent return; //--- Return } double out_r = (sr * sa + dr * da * (1 - sa)) / out_a; //--- Compute out red double out_g = (sg * sa + dg * da * (1 - sa)) / out_a; //--- Compute out green double out_b = (sb * sa + db * da * (1 - sa)) / out_a; //--- Compute out blue uchar oa = (uchar)(out_a * 255 + 0.5); //--- Compute final alpha uchar or_ = (uchar)(out_r * 255 + 0.5); //--- Compute final red uchar og = (uchar)(out_g * 255 + 0.5); //--- Compute final green uchar ob = (uchar)(out_b * 255 + 0.5); //--- Compute final blue uint out_col = ((uint)oa << 24) | ((uint)or_ << 16) | ((uint)og << 8) | (uint)ob; //--- Compose color canvas.PixelSet(x, y, out_col); //--- Set blended pixel }
Zunächst implementieren wir die Funktion drawResizeIndicator, um Interaktionen zur Größenänderung auf der Zeichenfläche visuell zu kennzeichnen, wobei wir die Themenfarbe in ARGB umwandeln. Anschließend füllen wir im Eckmodus (Hover oder aktiv) mit FillRectangle ein kleines Quadrat unten rechts und zeichnen mit Line drei jeweils um 3 Pixel versetzte diagonale Linien, um einen Griff-Effekt zu erzielen. Für den rechten Rand füllen wir mit FillRectangle ein vertikales Rechteck aus, das am Rand zentriert ist; ebenso für den unteren Rand ein horizontales Rechteck, wodurch ein intuitives Feedback ohne Unübersichtlichkeit entsteht. Anschließend prüft isMouseOverHeaderBar, ob sich der Mauszeiger innerhalb der Grenzen der Kopfzeile befindet, und gibt true zurück, wenn ein Verschieben möglich ist.
Um Bereiche für die Größenänderung zu erkennen, prüft isMouseInResizeZone, ob die Größenänderung aktiviert ist, berechnet relative Koordinaten und wertet die rechte, unten oder in einer Ecke, basierend auf resizeGripSize, aktualisiert den Modus wie RESIZE_CORNER und gibt bei Übereinstimmung true zurück, andernfalls NO_RESIZE und false. In handleCanvasResize berechnen wir die Deltas vom Startpunkt aus, passen die neue Breite/Höhe je nach aktivem Modus (rechts/unten/Ecke) mit MathMax für die Mindestwerte an, begrenzen diese mithilfe von ChartGetInteger auf die Chart-Abmessungen abzüglich der Ränder und aktualisieren bei Änderungen die globalen Variablen, passen die Größe der Zeichenfläche mit Resize an, legen die Objektgrößen über ObjectSetInteger fest, rendern die Visualisierung neu und zeichnen das Chart neu.
Beim Verschieben berechnet handleCanvasDrag Deltas und neue Positionen. Es begrenzt diese Werte mithilfe von ChartGetInteger auf den Chart-Bereich, um einen Überlauf zu verhindern. Anschließend aktualisieren wir die globalen Variablen und legen die Objektabstände mit ObjectSetInteger fest, woraufhin die Grafik neu gezeichnet wird. Wir definieren InterpolateColors, um zwei Farben zu mischen. Es extrahiert die RGB-Werte, interpoliert jeden Kanal linear und kombiniert die Werte anschließend wieder, um Farbverläufe zu erzeugen. Schließlich ermöglicht blendPixelSet das Alpha-Blending für Überlagerungen, führt Begrenzungsprüfungen durch und extrahiert Quell- und Zielkomponenten. Es berechnet den Ausgabe-Alpha-Wert und die vorvervielfachten RGB-Werte, begrenzt diese auf vorzeichenlose Zeichen, setzt die Farbe zusammen und legt sie mit der Methode PixelSet fest. Dies ermöglicht eine nahtlose Einbindung, beispielsweise in Bedienfeldern. Um die Größenänderungsanzeigen zu verarbeiten, rufen wir zunächst die Funktion in der Haupt-Render-Routine auf.
//+------------------------------------------------------------------+ //| Render Regression Visualization | //+------------------------------------------------------------------+ void renderVisualization() { mainCanvas.Erase(0); //--- Erase canvas if (enableBackgroundFill) { //--- Check background fill drawGradientBackground(); //--- Draw gradient background } drawCanvasBorder(); //--- Draw border drawHeaderBar(); //--- Draw header bar drawRegressionPlot(); //--- Draw plot if (showStatistics) { //--- Check show statistics drawStatisticsPanel(); //--- Draw stats panel drawLegend(); //--- Draw legend } if (isHoveringResizeZone && enableResizing) { //--- Check resize hover drawResizeIndicator(); //--- Draw resize indicator } mainCanvas.Update(); //--- Update canvas }
Wenn wir das Programm ausführen, erhalten wir folgendes Ergebnis.

Auf dem Bild ist zu erkennen, dass sich die Größenänderungsmarkierungen nun perfekt einfügen. Wir können nun die eigentlichen Interaktionen, wie z. B. das Ändern der Größe und das Verschieben, im Ereignis-Handler des Charts verarbeiten.
//+------------------------------------------------------------------+ //| Chart Event Handler | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (id == CHARTEVENT_MOUSE_MOVE) { //--- Check mouse move int mouseX = (int)lparam; //--- Set mouse X int mouseY = (int)dparam; //--- Set mouse Y int mouseState = (int)sparam; //--- Set mouse state bool previousHoverState = isHoveringCanvas; //--- Store prev canvas hover bool previousHeaderHoverState = isHoveringHeader; //--- Store prev header hover bool previousResizeHoverState = isHoveringResizeZone; //--- Store prev resize hover isHoveringCanvas = (mouseX >= currentPositionX && mouseX <= currentPositionX + currentWidthPixels && mouseY >= currentPositionY && mouseY <= currentPositionY + currentHeightPixels); //--- Update canvas hover isHoveringHeader = isMouseOverHeaderBar(mouseX, mouseY); //--- Update header hover isHoveringResizeZone = isMouseInResizeZone(mouseX, mouseY, hoverResizeMode); //--- Update resize hover bool needRedraw = (previousHoverState != isHoveringCanvas || previousHeaderHoverState != isHoveringHeader || previousResizeHoverState != isHoveringResizeZone); //--- Check if redraw needed if (mouseState == 1 && previousMouseButtonState == 0) { //--- Check button press if (enableDragging && isHoveringHeader && !isHoveringResizeZone) { //--- Check drag start isDraggingCanvas = true; //--- Set dragging dragStartX = mouseX; //--- Set start X dragStartY = mouseY; //--- Set start Y canvasStartX = currentPositionX; //--- Set canvas X canvasStartY = currentPositionY; //--- Set canvas Y ChartSetInteger(0, CHART_MOUSE_SCROLL, false); //--- Disable scroll needRedraw = true; //--- Set redraw } else if (isHoveringResizeZone) { //--- Check resize start isResizingCanvas = true; //--- Set resizing activeResizeMode = hoverResizeMode; //--- Set active mode resizeStartX = mouseX; //--- Set start X resizeStartY = mouseY; //--- Set start Y resizeInitialWidth = currentWidthPixels; //--- Set initial width resizeInitialHeight = currentHeightPixels; //--- Set initial height ChartSetInteger(0, CHART_MOUSE_SCROLL, false); //--- Disable scroll needRedraw = true; //--- Set redraw } } else if (mouseState == 1 && previousMouseButtonState == 1) { //--- Check drag if (isDraggingCanvas) { //--- Handle drag handleCanvasDrag(mouseX, mouseY); //--- Handle drag } else if (isResizingCanvas) { //--- Handle resize handleCanvasResize(mouseX, mouseY); //--- Handle resize } } else if (mouseState == 0 && previousMouseButtonState == 1) { //--- Check button release if (isDraggingCanvas || isResizingCanvas) { //--- Check active isDraggingCanvas = false; //--- Reset dragging isResizingCanvas = false; //--- Reset resizing activeResizeMode = NO_RESIZE; //--- Reset mode ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll needRedraw = true; //--- Set redraw } } if (needRedraw) { //--- Check redraw renderVisualization(); //--- Render ChartRedraw(); //--- Redraw chart } lastMouseX = mouseX; //--- Update last X lastMouseY = mouseY; //--- Update last Y previousMouseButtonState = mouseState; //--- Update prev state } }
Wir verwenden die Ereignisbehandlung von OnChartEvent, um interaktive Funktionen wie das Verschieben und die Größenänderung zu verwalten. Dabei prüfen wir zunächst, ob es sich bei dem Ereignis um CHARTEVENT_MOUSE_MOVE handelt, und extrahieren anschließend die Mauskoordinaten und den Mausstatus aus den Parametern. Als Nächstes speichern wir frühere Hover-Zustände und aktualisieren die Flags für den Hover-Zustand der Zeichenfläche (gesamter Bereich), die Kopfzeile mit isMouseOverHeaderBar und den Größenänderungsbereich über isMouseInResizeZone, wodurch hoverResizeMode gesetzt wird, um zu bestimmen, ob aufgrund von Änderungen ein Neuzeichnen erforderlich ist.
Beim Herunterdrücken der Maustaste (Zustand 1, zuvor 0): Wenn das Verschieben aktiviert ist und der Mauszeiger über der Kopfzeile schwebt, ohne dass die Größe geändert wird, setzen wir isDraggingCanvas, die Erfassung beginnt, deaktivieren das Scrollen mit ChartSetInteger und setzen das Flag für die Neuzeichnung; befindet sich der Mauszeiger im Bereich zur Größenänderung, setzen wir isResizingCanvas, wechseln in den aktiven Modus, initialisieren die Werte und deaktivieren das Scrollen. Solange die Taste gedrückt gehalten wird (Zustand 1, vorher 1), rufen wir bei einem Verschieben die Funktion handleCanvasDrag auf und bei einer Größenänderung die Funktion handleCanvasResize. Beim Loslassen (Zustand 0, vorher 1) werden die Flags und der Modus zurückgesetzt, das Scrollen aktiviert und das Flag Neuzeichnen gesetzt. Falls eine Neuzeichnung erforderlich ist, rufen wir renderVisualization und ChartRedraw auf. Aktualisiere abschließend die letzten Mauspositionen und den vorherigen Zustand, um die Kontinuität zu gewährleisten. Wir müssen auch die Zeichenfläche entfernen, wenn wir sie nicht mehr brauchen.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { mainCanvas.Destroy(); //--- Destroy canvas ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { static datetime lastBarTimestamp = 0; //--- Store last bar time datetime currentBarTimestamp = iTime(_Symbol, chartTimeframe, 0); //--- Get current bar time if (currentBarTimestamp > lastBarTimestamp) { //--- Check new bar if (loadSymbolClosePrices()) { //--- Reload prices if (computeLinearRegression()) { //--- Recalculate regression renderVisualization(); //--- Update visualization ChartRedraw(); //--- Redraw chart } } lastBarTimestamp = currentBarTimestamp; //--- Update last time } }
In OnDeinit führen wir eine Bereinigung durch, indem wir die Hauptzeichenfläche mit Destroy zerstören, um Ressourcen freizugeben, und anschließend das Chart mit ChartRedraw neu zeichnen, um etwaige visuelle Reste zu entfernen. In OnTick verwenden wir eine statische Variable lastBarTimestamp, um den Zeitpunkt der vorherigen Bar zu erfassen, diesen mit dem Zeitpunkt der aktuellen Bar aus iTime für das Symbol und den Zeitrahmen zu vergleichen, und, falls sich eine neue Bar gebildet hat, die Kurse über loadSymbolClosePrices neu zu laden, die Regression mit computeLinearRegression neu zu berechnen, die Visualisierung neu zu rendern und das Chart neu zu zeichnen, bevor der Zeitstempel für den nächsten Tick aktualisiert wird. Damit sind unsere Ziele vollständig umgesetzt. Nun bleibt nur noch, die Funktionsfähigkeit des Systems zu testen, was im folgenden Abschnitt behandelt wurde.
Backtests
Wir haben den Test durchgeführt; unten sehen Sie die resultierende Visualisierung als einzelne GIF-Animation.

Schlussfolgerung
Zusammenfassend lässt sich sagen, dass wir in MQL5 ein grafisches Tool auf der Basis von Canvas entwickelt haben, das statistische Korrelations- und lineare Regressionsanalysen zwischen zwei Symbolen ermöglicht und das über Funktionen zum Verschieben und zur Größenanpassung verfügt. Wir haben ALGLIB integriert, um Regressionsberechnungen, dynamische Achsenbeschriftungen, Datenpunkte sowie ein Statistikfeld zur Anzeige von Steigung, Achsenabschnitt, Korrelation und R-Quadrat zu ermöglichen. Diese interaktive Visualisierung liefert Einblicke in den Paarhandel und bietet anpassbare Designs, Rahmen sowie Echtzeit-Aktualisierungen bei neuen Bars. Im nächsten Teil werden wir dem Plot den Themenmodus Cyberpunk sowie Live-Animationen hinzufügen, um ihn modern und intuitiv zu gestalten. Bleiben Sie dran!
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/21303
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.
Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
Vom Neuling zum Experten: Erweiterung einer Liquiditätsstrategie mit Trendfiltern
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Swing-Extreme und Pullbacks in MQL5 (Teil 2): Automatisierung der Strategie mit einem Expert Advisor
- 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.