English Русский
preview
MQL5 Trading Tools (Teil 20): Grafiken auf Zeichenflächen mit statistischer Korrelations- und linearer Regressionsanalyse

MQL5 Trading Tools (Teil 20): Grafiken auf Zeichenflächen mit statistischer Korrelations- und linearer Regressionsanalyse

MetaTrader 5Handelssysteme |
17 0
Allan Munene Mutiiria
Allan Munene Mutiiria

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:

  1. Grundlagen statistischer Korrelation und Regression als Grafiken auf Zeichenflächen
  2. Implementierung in MQL5
  3. Backtests
  4. 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.

ROADMAP ZUR DARSTELLUNG VON KORRELATION UND REGRESSION MIT DEM CANVAS-ANSATZ


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.

ERGEBNIS DES ERSTEN DURCHLAUFS

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:

CANVAS-KOPFZEILE UND HAUPTTEIL

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.

CANVAS-REGRESSIONS-DIAGRAMM

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.

ZUSÄTZLICHES STATISTIKEN- UND LEGENDENFELD

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.

ANZEIGEN FÜR GRÖSSENANPASSUNG

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.

BACKTEST-GIF


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

Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Vom Neuling zum Experten:  Erweiterung einer Liquiditätsstrategie mit Trendfiltern Vom Neuling zum Experten: Erweiterung einer Liquiditätsstrategie mit Trendfiltern
Der Artikel erweitert eine liquiditätsbasierte Strategie um einen einfachen Trendfilter: Liquiditätszonen sollen nur in Richtung des EMA(50) gehandelt werden. Darin werden Filterregeln erläutert, die wiederverwendbare Klasse „TrendFilter.mqh“ sowie die EA-Integration in MQL5 vorgestellt und Tests mit und ohne Filter miteinander verglichen. Die Leser profitieren von einer klaren Trendorientierung, weniger übermäßigen Handelsaktivitäten in Gegentrendphasen und einsatzbereiten Quelldateien.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Swing-Extreme und Pullbacks in MQL5 (Teil 2): Automatisierung der Strategie mit einem Expert Advisor Swing-Extreme und Pullbacks in MQL5 (Teil 2): Automatisierung der Strategie mit einem Expert Advisor
Der Indikator basiert auf der Marktstruktur niedrigerer Zeitrahmen und wird anschließend im höheren Zeitrahmen eingeordnet. Er erkennt Swing-Extreme, bei denen der Kurs statistisch gesehen anfällig für eine Trendumkehr wird. Er visualisiert Überdehnungs- und Rücksetzerbereiche und bietet so einen frühen Einblick in das Mean-Reversion-Verhalten.