English 日本語
preview
Automatisieren von Handelsstrategien in MQL5 (Teil 39): Statistische Rückkehr zum Mittelwert mit Konfidenzintervallen und Dashboard

Automatisieren von Handelsstrategien in MQL5 (Teil 39): Statistische Rückkehr zum Mittelwert mit Konfidenzintervallen und Dashboard

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

Einführung

In unserem letzten Artikel (Teil 38) haben wir das Handelssystem von versteckten RSI-Divergenzen in MetaQuotes Language 5 (MQL5) entwickelt, das versteckte Auf- und Abwärts-Divergenzen mit Hilfe von Umkehrpunkten identifiziert, saubere Checks mit Balken-Bereichen und Toleranzen anwendet, Signale über anpassbare Steigungswinkel auf Preis- und RSI-Linien filtert, Handelssysteme mit Risikomanagement ausführt und visuelle Marker mit Winkelanzeigen auf Charts beinhaltet. In Teil 39 entwickeln wir das System von einer statistisches Rückkehr zum Mittelwert mit Konfidenzintervallen und einem Dashboard.

Dieses System analysiert Kursdaten über einen definierten Zeitraum, um statistische Momente wie Mittelwert, Varianz, Schiefe, Kurtosis und den Jarque-Bera-Test zu berechnen, generiert Reversionssignale auf der Grundlage von Konfidenzintervallen mit adaptiven Schwellenwerten und höherer Zeitrahmenbestätigung, verwaltet Handelsgeschäfte mit kapitalbasierten Volumengrößen, Trailing Stops, Teilschließungen und zeitbasierten Exits und bietet gleichzeitig ein On-Chart-Dashboard zur Echtzeitüberwachung. Wir werden die folgenden Themen behandeln:

  1. Verstehen der Strategie der Statistik der Rückkehr zum Mittelwert
  2. Implementation in MQL5
  3. Backtests
  4. Schlussfolgerung

Am Ende haben Sie eine funktionierende MQL5-Strategie für die Statistik des Mean-Reversion-Handel, die Sie anpassen können – legen wir los!


Verstehen der Strategie der Statistik der Rückkehr zum Mittelwert

Die Strategie der Statistik der Rückkehr zum Mittelwert macht sich die Tendenz der Kurse zunutze, nach erheblichen Abweichungen zu ihrem historischen Mittelwert zurückzukehren. Sie wird durch eine statistische Analyse ergänzt, um eine Nicht-Normalverteilungen zu ermitteln, bei denen solche Umkehrungen aufgrund von Asymmetrie und Randrisiken wahrscheinlicher sind.

Bei einem Kauf-Setup fällt der Kurs unter das untere Konfidenzintervall mit negativer Schiefe, was auf eine potenzielle Aufwärtsdynamik bei überverkauften Bedingungen hindeutet; bei einem Verkaufs-Setup übersteigt der Kurs das obere Intervall mit positiver Schiefe, was auf überkaufte Bedingungen hindeutet, die wahrscheinlich nach unten korrigiert werden. Beide Werte werden anhand des Jarque-Bera-Tests zur Bestätigung der Nicht-Normalität und der Kurtosis-Schwellenwerte gefiltert, um starke leptokurtische Märkte zu vermeiden.

Bei dieser Strategie verfeinern wir die Eingaben weiter, indem wir sie mit höheren Zeitrahmen für den Trendkontext abgleichen. Wir setzen dynamische Risikokontrollen ein, wie z. B. die prozentuale Dimensionierung des Eigenkapitals, Basisabstände für Stop-Loss und Take-Profit, Trailing-Stops zur Gewinnsicherung, Teilschließungen von Positionen bei vordefinierten Gewinnniveaus und zeitbasierte Ausstiege, um ein längeres Engagement zu minimieren. Durch die Integration dieser statistischen und risikorelevanten Elemente wollen wir in volatilen Märkten zuverlässige Reversionsmöglichkeiten nutzen. Im Folgenden sehen Sie die statistischen Verteilungen, die wir verwenden werden, in einem Diagramm dargestellt.

STATISTISCHE REVERSIONSDIAGRAMME

Der Jarque-Bera-Test ist eine Kombination aus den Momenten Schiefe und der Kurtosis. Wir werden sie bei der Umsetzung vertreten, keine Sorge. Unser Plan ist es, diese statistischen Momente, einschließlich Mittelwert, Varianz, Schiefe, Kurtosis und Jarque-Bera, über einen bestimmten Zeitraum zu berechnen, Kauf- oder Verkaufssignale zu generieren, wenn der Preis Konfidenzintervalle mit adaptiven Schiefe-Schwellenwerten und Nicht-Normalitäts-Filtern durchbricht, mit optionalen Daten mit höherem Zeitrahmen zu bestätigen, Handelsgeschäfte mit risikobasierten oder festen Lots mit Basis-SL/TP auszuführen, Trailing-Stops, Partial-Closes und Duration-Limits für das Management anzuwenden und Echtzeit-Metriken über ein On-Chart-Dashboard anzuzeigen, um ein umfassendes System für den statistischen Reversionshandel zu schaffen. Das ist, kurz gesagt, das, was wir am Ende des Artikels erreichen werden.

ZIELPLAN


Implementation 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 das erstellt ist, müssen wir in der Programmierumgebung einige Eingabeparameter und globale Variablen deklarieren, die wir im gesamten Programm verwenden werden.

//+------------------------------------------------------------------+
//|                                 StatisticalReversionStrategy.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>
#include <Math\Stat\Math.mqh>
#include <ChartObjects\ChartObjectsTxtControls.mqh>

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input group "=== Statistical Parameters ==="
input int InpPeriod = 50;                   // Period for statistical calculations
input double InpConfidenceLevel = 0.95;     // Confidence level for intervals (0.90-0.99)
input double InpJBThreshold = 2.0;          // Jarque-Bera threshold (lowered for more trades)
input double InpKurtosisThreshold = 5.0;    // Max excess kurtosis (relaxed)
input ENUM_TIMEFRAMES InpHigherTF = 0;      // Higher timeframe for confirmation (0 to disable)

input group "=== Trading Parameters ==="
input double InpRiskPercent = 1.0;          // Risk per trade (% of equity, 0 for fixed lots)
input double InpFixedLots = 0.01;           // Fixed lot size if InpRiskPercent = 0
input int InpBaseStopLossPips = 50;         // Base Stop Loss in pips
input int InpBaseTakeProfitPips = 100;      // Base Take Profit in pips
input int InpMagicNumber = 123456;          // Magic number for trades
input int InpMaxTradeHours = 48;            // Max trade duration in hours (0 to disable)

input group "=== Risk Management ==="
input bool InpUseTrailingStop = true;       // Enable trailing stop
input int InpTrailingStopPips = 30;         // Trailing stop distance in pips
input int InpTrailingStepPips = 10;         // Trailing step in pips
input bool InpUsePartialClose = true;       // Enable partial profit-taking
input double InpPartialClosePercent = 0.5;  // Percent of position to close at 50% TP

input group "=== Dashboard Parameters ==="
input bool InpShowDashboard = true;         // Show dashboard
input int InpDashboardX = 30;               // Dashboard X position
input int InpDashboardY = 30;               // Dashboard Y position
input int InpFontSize = 10;                 // Font size for dashboard text

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade trade;                                      //--- Trade object
datetime g_lastBarTime = 0;                        //--- Last processed bar time
double g_pointMultiplier = 1.0;                    //--- Point multiplier for broker digits
CChartObjectRectLabel* g_dashboardBg = NULL;       //--- Dashboard background object
CChartObjectRectLabel* g_headerBg = NULL;          //--- Header background object
CChartObjectLabel* g_titleLabel = NULL;            //--- Title label object
CChartObjectLabel* g_staticLabels[];               //--- Static labels array
CChartObjectLabel* g_valueLabels[];                //--- Value labels array
string g_staticNames[] = {
   "Symbol:", "Timeframe:", "Price:", "Skewness:", "Jarque-Bera:", "Kurtosis:",
   "Mean:", "Lower CI:", "Upper CI:", "Position:", "Lot Size:", "Profit:", "Duration:", "Signal:",
   "Equity:", "Balance:", "Free Margin:"
};                                                 //--- Static label names
int g_staticCount = ArraySize(g_staticNames);      //--- Number of static labels

Wir beginnen mit der Einbindung wichtiger Bibliotheken: „#include <Trade\Trade.mqh>“ für Handelsoperationen, „#include <Math\Stat\Math.mqh>“ für statistische Berechnungen wie Momente und Verteilungen und „#include <ChartObjects\ChartObjectsTxtControls.mqh>“ zur Handhabung von Chartobjekten für die Dashboard-Anzeige. Als Nächstes definieren wir gruppierte Eingabeparameter für die Nutzerkonfiguration. In der Gruppe „Statistische Parameter“ ist „InpPeriod“ standardmäßig auf 50 als Rückblick für die Statistik eingestellt, „InpConfidenceLevel“ auf 0,95 legt das Konfidenzintervall fest (Bereich 0,90-0,99), „InpJBThreshold“ auf 2.0 filtert Nicht-Normalität über Jarque-Bera (niedriger für mehr Signale), „InpKurtosisThreshold“ bei 5.0 kappt übermäßige Kurtosis (entspannter Schwellenwert), und „InpHigherTF“ als 0 deaktiviert oder wählt einen höheren Zeitrahmen für die Bestätigung.

Unter „Handelsparameter“ aktiviert „InpRiskPercent“ mit 1,0 das aktienbasierte Risiko (0 für feste Losgröße), „InpFixedLots“ mit 0.01 legt die statische Größe fest, wenn das Risiko ausgeschaltet ist, „InpBaseStopLossPips“ mit 50 und „InpBaseTakeProfitPips“ mit 100 definieren die Basisabstände zwischen Risiko und Gewinn, „InpMagicNumber“ mit 123456 identifiziert die Handelsgeschäfte und „InpMaxTradeHours“ mit 48 begrenzt die Dauer (0 zum Deaktivieren). Für „Risk Management“ aktiviert „InpUseTrailingStop“ als true dynamische Stops, mit „InpTrailingStopPips“ bei 30 für den Abstand und „InpTrailingStepPips“ bei 10 für die Anpassungsschritte; „InpUsePartialClose“ als true ermöglicht partielle Exits, wobei „InpPartialClosePercent“ bei 0,5 der Position bei 50% Gewinn.

Unter „Dashboard-Parameter“ schaltet „InpShowDashboard“ als true das visuelle Panel ein, wobei „InpDashboardX“ und „InpDashboardY“ auf 30 für die Positionierung und „InpFontSize“ auf 10 für die Textskalierung eingestellt sind.

Dann deklarieren wir globale Variablen: „trade“ als CTrade-Instanz für die Auftragsabwicklung, „g_lastBarTime“ initialisiert auf 0, um verarbeitete Bars zu verfolgen, „g_pointMultiplier“ auf 1.0, um die Unterschiede zwischen den Ziffern des Brokers auszugleichen, und Chartobjekte wie „g_dashboardBg“, „g_headerBg“, „g_titleLabel“ als NULL für Dashboard-Elemente, zusammen mit den Arrays „g_staticLabels[]“ und „g_valueLabels[]“ für Labels. Wir definieren „g_staticNames[]“ als ein String-Array, das unveränderliche Texte des Dashboards wie „Symbol:“, „Mean:“ usw. enthält, und setzen „g_staticCount“ über die Funktion ArraySize für die Iteration auf seine Größe. Damit können wir mit dem einfachsten Vorgang beginnen, nämlich mit der Erstellung des benötigten Dashboards. Wir wollen diese Logik in einer Funktion haben.

//+------------------------------------------------------------------+
//| Create Dashboard                                                 |
//+------------------------------------------------------------------+
bool CreateDashboard() {
   if (!InpShowDashboard) return true;             //--- Return if dashboard disabled

   Print("Creating dashboard...");                 //--- Log dashboard creation

// Create main background rectangle
   if (g_dashboardBg == NULL) {                    //--- Check if background exists
      g_dashboardBg = new CChartObjectRectLabel(); //--- Create background object
      if (!g_dashboardBg.Create(0, "StatReversion_DashboardBg", 0, InpDashboardX, InpDashboardY + 30, 300, g_staticCount * (InpFontSize + 6) + 30)) { //--- Create dashboard background
         Print("Error creating dashboard background: ", GetLastError()); //--- Log error
         return false;                             //--- Return failure
      }
      g_dashboardBg.Color(clrDodgerBlue);          //--- Set border color
      g_dashboardBg.BackColor(clrNavy);            //--- Set background color
      g_dashboardBg.BorderType(BORDER_FLAT);       //--- Set border type
      g_dashboardBg.Corner(CORNER_LEFT_UPPER);     //--- Set corner alignment
   }

// Create header background
   if (g_headerBg == NULL) {                       //--- Check if header background exists
      g_headerBg = new CChartObjectRectLabel();    //--- Create header background object
      if (!g_headerBg.Create(0, "StatReversion_HeaderBg", 0, InpDashboardX, InpDashboardY, 300, InpFontSize + 20)) { //--- Create header background
         Print("Error creating header background: ", GetLastError()); //--- Log error
         return false;                             //--- Return failure
      }
      g_headerBg.Color(clrDodgerBlue);             //--- Set border color
      g_headerBg.BackColor(clrDarkBlue);           //--- Set background color
      g_headerBg.BorderType(BORDER_FLAT);          //--- Set border type
      g_headerBg.Corner(CORNER_LEFT_UPPER);        //--- Set corner alignment
   }

// Create title label (centered)
   if (g_titleLabel == NULL) {                     //--- Check if title label exists
      g_titleLabel = new CChartObjectLabel();      //--- Create title label object
      if (!g_titleLabel.Create(0, "StatReversion_Title", 0, InpDashboardX + 75, InpDashboardY + 5)) { //--- Create title label
         Print("Error creating title label: ", GetLastError()); //--- Log error
         return false;                             //--- Return failure
      }
      if (!g_titleLabel.Font("Arial Bold") || !g_titleLabel.FontSize(InpFontSize + 2) || !g_titleLabel.Description("Statistical Reversion")) { //--- Set title properties
         Print("Error setting title properties: ", GetLastError()); //--- Log error
         return false;                             //--- Return failure
      }
      g_titleLabel.Color(clrWhite);                //--- Set title color
   }

// Initialize static labels (left-aligned)
   ArrayFree(g_staticLabels);                      //--- Free static labels array
   ArrayResize(g_staticLabels, g_staticCount);     //--- Resize static labels array
   int y_offset = InpDashboardY + 30 + 10;         //--- Set y offset for labels
   for (int i = 0; i < g_staticCount; i++) {       //--- Iterate through static labels
      g_staticLabels[i] = new CChartObjectLabel(); //--- Create static label object
      string label_name = "StatReversion_Static_" + IntegerToString(i); //--- Generate label name
      if (!g_staticLabels[i].Create(0, label_name, 0, InpDashboardX + 10, y_offset)) { //--- Create static label
         Print("Error creating static label: ", label_name, ", Error: ", GetLastError()); //--- Log error
         DeleteDashboard();                        //--- Delete dashboard
         return false;                             //--- Return failure
      }
      if (!g_staticLabels[i].Font("Arial") || !g_staticLabels[i].FontSize(InpFontSize) || !g_staticLabels[i].Description(g_staticNames[i])) { //--- Set static label properties
         Print("Error setting static label properties: ", label_name, ", Error: ", GetLastError()); //--- Log error
         DeleteDashboard();                        //--- Delete dashboard
         return false;                             //--- Return failure
      }
      g_staticLabels[i].Color(clrLightGray);       //--- Set static label color
      y_offset += InpFontSize + 6;                 //--- Update y offset
   }

// Initialize value labels (right-aligned, starting at center)
   ArrayFree(g_valueLabels);                       //--- Free value labels array
   ArrayResize(g_valueLabels, g_staticCount);      //--- Resize value labels array
   y_offset = InpDashboardY + 30 + 10;             //--- Reset y offset for values
   for (int i = 0; i < g_staticCount; i++) {       //--- Iterate through value labels
      g_valueLabels[i] = new CChartObjectLabel();  //--- Create value label object
      string label_name = "StatReversion_Value_" + IntegerToString(i); //--- Generate label name
      if (!g_valueLabels[i].Create(0, label_name, 0, InpDashboardX + 150, y_offset)) { //--- Create value label
         Print("Error creating value label: ", label_name, ", Error: ", GetLastError()); //--- Log error
         DeleteDashboard();                        //--- Delete dashboard
         return false;                             //--- Return failure
      }
      if (!g_valueLabels[i].Font("Arial") || !g_valueLabels[i].FontSize(InpFontSize) || !g_valueLabels[i].Description("")) { //--- Set value label properties
         Print("Error setting value label properties: ", label_name, ", Error: ", GetLastError()); //--- Log error
         DeleteDashboard();                        //--- Delete dashboard
         return false;                             //--- Return failure
      }
      g_valueLabels[i].Color(clrCyan);             //--- Set value label color
      y_offset += InpFontSize + 6;                 //--- Update y offset
   }

   ChartRedraw();                                  //--- Redraw chart
   Print("Dashboard created successfully");        //--- Log success
   return true;                                    //--- Return true on success
}

Wir definieren die Funktion „CreateDashboard“, um ein visuelles On-Chart-Panel für die Anzeige von Strategiemetriken einzurichten, wenn „InpShowDashboard“ true ist; andernfalls wird sofort true zurückgegeben. Wir protokollieren die Erstellung zunächst mit Print, prüfen dann, ob „g_dashboardBg“ NULL ist und erstellen die neue Instanz „CChartObjectRectLabel“, indem wir ihre Methode „Create“ mit Parametern wie Subwindow 0, Name „StatReversion_DashboardBg“, der Position basierend auf „InpDashboardX“ und „InpDashboardY + 30“, der Breite von 300 und einer dynamischen Höhe berechnet aus „g_staticCount“ mal (“InpFontSize + 6“) plus 30; wenn die Erstellung fehlschlägt, protokollieren wir den Fehler aus „GetLastError“ und geben false zurück. Wir setzen Eigenschaften wie „Color“ auf clrDodgerBlue für den Rahmen, „BackColor“ auf „clrNavy“ für die Füllung, „BorderType“ auf BORDER_FLAT und „Corner“ auf CORNER_LEFT_UPPER.

Ebenso wird für die Kopfzeile, wenn „g_headerBg“ NULL ist, ein weiteres „CChartObjectRectLabel“ an den Positionen „InpDashboardX“ und „InpDashboardY“ mit der Höhe „InpFontSize + 20“ erstellt, wobei die entsprechenden Farb- und Rahmeneinstellungen angewendet, protokolliert und im Fehlerfall false zurückgegeben werden. Für den Titel, wenn er NULL ist, instanziieren wir ein „CChartObjectLabel“ und erstellen es zentriert bei „InpDashboardX + 75“ und „InpDashboardY + 5“ mit dem Namen „StatReversion_Title“; wir konfigurieren dann „Font“ auf „Arial Bold“, „FontSize“ auf „InpFontSize + 2“, „Description“ auf „Statistical Reversion“ und „Color“ auf „clrWhite“, wobei Fehler durch Protokollierung und Rückgabe von false behandelt werden.

Wir geben „g_staticLabels“ frei und passen die Größe an „g_staticCount“ an, wobei wir einen anfänglichen „y_offset“ von „InpDashboardY + 30 + 10“ festlegen, und erstellen dann in einer Schleife jedes statische Label als neues „CChartObjectLabel“ mit dem Namen „StatReversion_Static_“ plus Index, positioniert bei „InpDashboardX + 10“ und aktuellem „y_offset“; setze „Font“ auf „Arial“, „FontSize“ auf „InpFontSize“, „Description“ von „g_staticNames[i]“, und „Color“ auf „clrLightGray“, ruft „DeleteDashboard“ auf und gibt bei jedem Erstellungs- oder Eigenschaftsfehler false zurück, wobei „y_offset“ jedes Mal um „InpFontSize + 6“ erhöht wird.

Ebenso geben wir „g_valueLabels“ frei und ändern die Größe, setzen „y_offset“ zurück und erstellen in einer Schleife die Kennzeichnungen der Werte mit dem Namen „StatReversion_Value_“ plus Index bei „InpDashboardX + 150“ und „y_offset“, mit leerer anfänglicher „Beschreibung“, „clrCyan“-Farbe und denselben Schrifteinstellungen, und behandeln Fehler auf ähnliche Weise. Schließlich rufen wir ChartRedraw auf, um die Anzeige zu aktualisieren, den Erfolg zu protokollieren und true zurückzugeben. Unter Verwendung desselben Formats können wir nun eine Funktion zum Aktualisieren und Löschen des Dashboards erstellen (siehe unten).

//+------------------------------------------------------------------+
//| Update Dashboard                                                 |
//+------------------------------------------------------------------+
void UpdateDashboard(double mean, double lower_ci, double upper_ci, double skewness, double jb_stat, double kurtosis,
                     double skew_buy, double skew_sell, string position, double lot_size, double profit, string duration, string signal) {
   if (!InpShowDashboard || ArraySize(g_valueLabels) != g_staticCount) { //--- Check if dashboard enabled and labels valid
      Print("Dashboard update skipped: Not initialized or invalid array size"); //--- Log skip
      return;                                      //--- Exit function
   }

   double balance = AccountInfoDouble(ACCOUNT_BALANCE); //--- Get account balance
   double equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Get account equity
   double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); //--- Get free margin
   double price = iClose(_Symbol, _Period, 0);     //--- Get current close price

   string values[] = {
      _Symbol, EnumToString(_Period), DoubleToString(price, _Digits),
      DoubleToString(skewness, 4), DoubleToString(jb_stat, 2), DoubleToString(kurtosis, 2),
      DoubleToString(mean, _Digits), DoubleToString(lower_ci, _Digits), DoubleToString(upper_ci, _Digits),
      position, DoubleToString(lot_size, 2), DoubleToString(profit, 2), duration, signal,
      DoubleToString(equity, 2), DoubleToString(balance, 2), DoubleToString(free_margin, 2)
   };                                              //--- Set value strings

   color value_colors[] = {
      clrWhite, clrWhite, (price > 0 ? clrCyan : clrGray), (skewness != 0 ? clrCyan : clrGray), (jb_stat != 0 ? clrCyan : clrGray), (kurtosis != 0 ? clrCyan : clrGray),
      (mean != 0 ? clrCyan : clrGray), (lower_ci != 0 ? clrCyan : clrGray), (upper_ci != 0 ? clrCyan : clrGray),
      clrWhite, clrWhite, (profit > 0 ? clrLimeGreen : profit < 0 ? clrRed : clrGray), clrWhite,
      (signal == "Buy" ? clrLimeGreen : signal == "Sell" ? clrRed : clrGray),
      (equity > balance ? clrLimeGreen : equity < balance ? clrRed : clrGray), clrWhite, clrWhite
   };                                              //--- Set value colors

   for (int i = 0; i < g_staticCount; i++) {       //--- Iterate through values
      if (g_valueLabels[i] != NULL) {              //--- Check if label exists
         g_valueLabels[i].Description(values[i]);  //--- Set value description
         g_valueLabels[i].Color(value_colors[i]);  //--- Set value color
      } else {                                     //--- Handle null label
         Print("Warning: Value label ", i, " is NULL"); //--- Log warning
      }
   }
   ChartRedraw();                                  //--- Redraw chart
   Print("Dashboard updated: Signal=", signal, ", Position=", position, ", Profit=", profit); //--- Log update
}

//+------------------------------------------------------------------+
//| Delete Dashboard                                                 |
//+------------------------------------------------------------------+
void DeleteDashboard() {
   if (g_dashboardBg != NULL) {                    //--- Check if background exists
      g_dashboardBg.Delete();                      //--- Delete background
      delete g_dashboardBg;                        //--- Free background memory
      g_dashboardBg = NULL;                        //--- Set background to null
      Print("Dashboard background deleted");       //--- Log deletion
   }
   if (g_headerBg != NULL) {                       //--- Check if header background exists
      g_headerBg.Delete();                         //--- Delete header background
      delete g_headerBg;                           //--- Free header background memory
      g_headerBg = NULL;                           //--- Set header background to null
      Print("Header background deleted");          //--- Log deletion
   }
   if (g_titleLabel != NULL) {                     //--- Check if title label exists
      g_titleLabel.Delete();                       //--- Delete title label
      delete g_titleLabel;                         //--- Free title label memory
      g_titleLabel = NULL;                         //--- Set title label to null
      Print("Title label deleted");                //--- Log deletion
   }
   for (int i = 0; i < ArraySize(g_staticLabels); i++) { //--- Iterate through static labels
      if (g_staticLabels[i] != NULL) {             //--- Check if label exists
         g_staticLabels[i].Delete();               //--- Delete static label
         delete g_staticLabels[i];                 //--- Free static label memory
         g_staticLabels[i] = NULL;                 //--- Set static label to null
      }
   }
   for (int i = 0; i < ArraySize(g_valueLabels); i++) { //--- Iterate through value labels
      if (g_valueLabels[i] != NULL) {              //--- Check if label exists
         g_valueLabels[i].Delete();                //--- Delete value label
         delete g_valueLabels[i];                  //--- Free value label memory
         g_valueLabels[i] = NULL;                  //--- Set value label to null
      }
   }
   ArrayFree(g_staticLabels);                      //--- Free static labels array
   ArrayFree(g_valueLabels);                       //--- Free value labels array
   ChartRedraw();                                  //--- Redraw chart
   Print("Dashboard labels cleared");              //--- Log clearance
}

Wir definieren die Funktion „UpdateDashboard“, um das On-Chart-Panel mit den aktuellen Strategiedaten zu aktualisieren, wobei wir Parameter wie „mean“, „lower_ci“, „upper_ci“, „skewness“, „jb_stat“, „kurtosis“, „skew_buy“, „skew_sell“, „position“, „lot_size“, „profit“, „duration“ und „signal“ als Anzeigewerte. Zunächst wird geprüft, ob die Anzeige erlaubt ist oder ob die Größe der Kennzeichnung nicht mit der Anzahl übereinstimmt; ist dies der Fall, wird ein Skip protokolliert und die Anzeige vorzeitig beendet. Wir rufen die Kontodetails ab: den „Saldo“ über AccountInfoDouble mit ACCOUNT_BALANCE, dasselbe für Kapital und freie Marge, und den aktuellen „Preis“ von der iClose-Funktion für das Symbol, den Zeitrahmen und die Verschiebung 0, für den aktuellen.

Wir füllen den String-Array „values“ mit formatierten Daten wie Symbol, Zeitrahmen-Enum über die Funktion EnumToString, Preis mit der Präzision _Digits über DoubleToString, statistische Metriken, die entsprechend gerundet sind, Positionsinformationen und Kontodaten. Wir setzen das Array „value_colors“ für die bedingte Einfärbung, z. B. Cyan für Nicht-Null-Statistiken, Lindgrün für positiven Gewinn oder Rot für negativen Gewinn, und eine ähnliche Logik für Signal und Kapital vs. Saldo. In einer Schleife durch „g_staticCount“ wird für jedes „g_valueLabels[i]“, wenn es nicht NULL ist, die „Beschreibung“ auf „values[i]“ und die „Farbe“ auf „value_colors[i]“ aktualisiert, andernfalls wird eine Warnung für ungültige Labels ausgegeben. Wir rufen die Funktion ChartRedraw auf, um das Bildmaterial zu aktualisieren und die Aktualisierung mit Signal, Position und Gewinn zu protokollieren.

Als Nächstes erstellen wir die Funktion „DeleteDashboard“ für die Bereinigung, indem wir prüfen, ob „g_dashboardBg“ nicht NULL ist, um seine „Delete“-Methode aufzurufen, den Speicher mit delete freizugeben, auf NULL zu setzen und die Löschung zu protokollieren; wiederholen Sie dies für „g_headerBg“ und „g_titleLabel“. Bei Arrays wird eine Schleife über die Größe von „g_staticLabels“ aus ArraySize gezogen, wobei jedes Element, das nicht NULL ist, gelöscht, freigegeben und gelöscht wird; dasselbe gilt für „g_valueLabels“. Anschließend geben wir beide Arrays mit der Funktion ArrayFree frei, zeichnen das Chart neu und protokollieren, dass die Beschriftungen gelöscht sind. Wir können diese Funktionen nun in der Ereignisbehandlung der Initialisierungen aufrufen.

//+------------------------------------------------------------------+
//| Expert Initialization Function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   trade.SetExpertMagicNumber(InpMagicNumber);     //--- Set magic number
   trade.SetDeviationInPoints(10);                 //--- Set deviation in points
   trade.SetTypeFilling(ORDER_FILLING_FOK);        //--- Set filling type

// Adjust point multiplier for broker digits
   if (_Digits == 5 || _Digits == 3)               //--- Check broker digits
      g_pointMultiplier = 10.0;                    //--- Set multiplier for 5/3 digits
   else                                            //--- Default digits
      g_pointMultiplier = 1.0;                     //--- Set multiplier for others

// Validate inputs (use local variables)
   double confidenceLevel = InpConfidenceLevel;    //--- Copy confidence level
   if (InpConfidenceLevel < 0.90 || InpConfidenceLevel > 0.99) { //--- Check confidence level range
      Print("Warning: InpConfidenceLevel out of range (0.90-0.99). Using 0.95."); //--- Log warning
      confidenceLevel = 0.95;                      //--- Set default confidence level
   }
   double riskPercent = InpRiskPercent;            //--- Copy risk percent
   if (InpRiskPercent < 0 || InpRiskPercent > 10) { //--- Check risk percent range
      Print("Warning: InpRiskPercent out of range (0-10). Using 1.0."); //--- Log warning
      riskPercent = 1.0;                           //--- Set default risk percent
   }

// Initialize dashboard (non-critical)
   if (InpShowDashboard && !CreateDashboard()) {   //--- Check if dashboard creation failed
      Print("Failed to initialize dashboard, continuing without it"); //--- Log failure
   }

   Print("Statistical Reversion Strategy Initialized. Period: ", InpPeriod, ", Confidence: ", confidenceLevel * 100, "% on ", _Symbol, "/", Period()); //--- Log initialization
   return(INIT_SUCCEEDED);                         //--- Return success
}

//+------------------------------------------------------------------+
//| Expert Deinitialization Function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   DeleteDashboard();                              //--- Delete dashboard
   Print("Statistical Reversion Strategy Deinitialized. Reason: ", reason); //--- Log deinitialization
}

In OnInit konfigurieren wir das Handelsobjekt, indem wir seine magische Zahl mit „trade.SetExpertMagicNumber“ über „InpMagicNumber“, die Abweichung auf 10 Punkte über „SetDeviationInPoints“ und den Füllungstyp auf ORDER_FILLING_FOK über „SetTypeFilling“ für eine präzise Auftragsausführung setzen. Wir passen den „g_pointMultiplier“ auf der Grundlage der Maklerziffern an: Wenn „_Digits“ 5 oder 3 ist, setzen Sie ihn auf 10,0 für eine korrekte Pip-Skalierung; andernfalls belassen Sie ihn bei 1,0. Zur Validierung der Eingaben wird „InpConfidenceLevel“ in ein lokales „confidenceLevel“ kopiert und geprüft, ob es außerhalb von 0,90 bis 0,99 liegt. Ist dies der Fall, wird eine Warnung protokolliert und auf 0,95 zurückgesetzt; ebenso wird für „InpRiskPercent“ eine Validierung zwischen 0 und 10 durchgeführt, wobei eine Warnung ausgegeben und bei Ungültigkeit auf 1,0 zurückgesetzt wird. Sie können sogar Ihre eigenen Einstellungen vornehmen. Dies sind nur willkürlich von uns gewählte Bereiche.

Wenn das Dashboard aktiviert ist und „CreateDashboard“ fehlschlägt, protokollieren wir das Problem und fahren ohne es fort. Zum Schluss wird eine Initialisierungsmeldung mit Periode, Konfidenzrate, Symbol und Zeitrahmen aus „Periode“ gedruckt und INIT_SUCCEEDED zurückgegeben. In OnDeinit, die eine konstante Ganzzahl „reason“ für den Grund annimmt, rufen wir „DeleteDashboard“ auf, um das Bildmaterial zu löschen und die Deinitialisierung mit dem angegebenen Grund zu protokollieren. Bei der Kompilierung ergibt sich folgendes Ergebnis.

ERSTLAUF

Aus dem Bild können wir ersehen, dass alles in Ordnung ist. Lassen Sie uns einige Hilfsfunktionen definieren, die wir für das Dashboard und die statistischen Aktualisierungen benötigen.

//+------------------------------------------------------------------+
//| Normal Inverse CDF Approximation                                 |
//+------------------------------------------------------------------+
double NormalInverse(double p) {
   double t = MathSqrt(-2.0 * MathLog(p < 0.5 ? p : 1.0 - p)); //--- Calculate t value
   double sign = (p < 0.5) ? -1.0 : 1.0;           //--- Determine sign
   return sign * (t - (2.515517 + 0.802853 * t + 0.010328 * t * t) /
                  (1.0 + 1.432788 * t + 0.189269 * t * t + 0.001308 * t * t * t)); //--- Return approximated inverse CDF
}

//+------------------------------------------------------------------+
//| Get Position Status                                              |
//+------------------------------------------------------------------+
string GetPositionStatus() {
   if (HasPosition(POSITION_TYPE_BUY)) return "Buy"; //--- Return "Buy" if buy position open
   if (HasPosition(POSITION_TYPE_SELL)) return "Sell"; //--- Return "Sell" if sell position open
   return "None";                                    //--- Return "None" if no position
}

//+------------------------------------------------------------------+
//| Get Current Lot Size                                             |
//+------------------------------------------------------------------+
double GetCurrentLotSize() {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions
      if (PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber) //--- Check symbol and magic
         return PositionGetDouble(POSITION_VOLUME); //--- Return position volume
   }
   return 0.0;                                      //--- Return 0 if no position
}

//+------------------------------------------------------------------+
//| Get Current Profit                                               |
//+------------------------------------------------------------------+
double GetCurrentProfit() {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions
      if (PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber) //--- Check symbol and magic
         return PositionGetDouble(POSITION_PROFIT); //--- Return position profit
   }
   return 0.0;                                      //--- Return 0 if no position
}

//+------------------------------------------------------------------+
//| Get Position Duration                                            |
//+------------------------------------------------------------------+
string GetPositionDuration() {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions
      if (PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber) { //--- Check symbol and magic
         datetime open_time = (datetime)PositionGetInteger(POSITION_TIME); //--- Get open time
         datetime current_time = TimeCurrent(); //--- Get current time
         int hours = (int)((current_time - open_time) / 3600); //--- Calculate hours
         return IntegerToString(hours) + "h"; //--- Return duration string
      }
   }
   return "0h";                                   //--- Return "0h" if no position
}

//+------------------------------------------------------------------+
//| Get Signal Status                                                |
//+------------------------------------------------------------------+
string GetSignalStatus(bool buy_signal, bool sell_signal) {
   if (buy_signal) return "Buy";                  //--- Return "Buy" if buy signal
   if (sell_signal) return "Sell";                //--- Return "Sell" if sell signal
   return "None";                                 //--- Return "None" if no signal
}

//+------------------------------------------------------------------+
//| Check for Open Position of Type                                  |
//+------------------------------------------------------------------+
bool HasPosition(ENUM_POSITION_TYPE pos_type) {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions
      if (PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber && PositionGetInteger(POSITION_TYPE) == pos_type) //--- Check details
         return true;                                //--- Return true if match
   }
   return false;                                     //--- Return false if no match
}

//+------------------------------------------------------------------+
//| Check for Any Open Position                                      |
//+------------------------------------------------------------------+
bool HasPosition() {
   return (HasPosition(POSITION_TYPE_BUY) || HasPosition(POSITION_TYPE_SELL)); //--- Check for buy or sell position
}

Zunächst wird die Funktion „NormalInverse“ implementiert, um die inverse Verteilungsfunktion (CDF) für eine Standardnormalverteilung zu approximieren. Dabei wird eine Wahrscheinlichkeit „p“ angenommen und ein „t“-Wert über MathSqrt auf den negativen Logarithmus des angepassten „p“ berechnet (gespiegelt, wenn über 0,5), Bestimmung eines „Vorzeichens“ auf der Grundlage, ob „p“ unter 0,5 liegt, und anschließende Rückgabe der vorzeichenbehafteten rationalen Näherung unter Verwendung von Polynomtermen für die Genauigkeit bei Konfidenzintervallberechnungen.

Als Nächstes definieren wir Hilfsfunktionen für Positions- und Signalabfragen. Die Funktion „GetPositionStatus“ prüft mittels „HasPosition“ mit POSITION_TYPE_BUY oder „POSITION_TYPE_SELL“, ob offene Käufe oder Verkäufe vorliegen, und gibt den Typ oder „None“ zurück, wenn dies nicht der Fall ist. “GetCurrentLotSize“ iteriert rückwärts durch die Positionen ab PositionsTotal minus 1, überprüft das Symbol mit PositionGetSymbol und die Magie über „PositionGetInteger“ gegen „InpMagicNumber“ und gibt das Volumen von „PositionGetDouble“ mit „POSITION_VOLUME“ zurück, wenn es übereinstimmt, oder sonst 0. In ähnlicher Weise ruft „GetCurrentProfit“ den Gewinn über „POSITION_PROFIT“ ab.

Für „GetPositionDuration“ suchen wir in einer Schleife nach einer passenden Position, holen uns deren Öffnungszeit als Datetime von PositionGetInteger mit „POSITION_TIME“, subtrahieren von TimeCurrent, um die Stunden zu berechnen, und formatieren sie als String wie „Xh“, standardmäßig „0h“, wenn es keine gibt. “GetSignalStatus“ gibt einfach „Buy“ oder „Sell“ zurück, wenn der entsprechende Boolesche Wert wahr ist, andernfalls „None“. Wir erstellen „HasPosition“, um offene Positionen eines bestimmten ENUM_POSITION_TYPE zu erkennen, indem wir die Positionen in einer Schleife durchlaufen und Symbol, Magie und Typ mit „PositionGetInteger“ auf „POSITION_TYPE“ überprüfen und bei Übereinstimmung true zurückgeben. Eine Überladung ohne Typ prüft, ob es eine gibt, indem sie die typisierte Version für Kauf oder Verkauf aufruft. Wir können nun diese Funktionen verwenden, die statistischen Berechnungen durchführen und Signale erzeugen. Hier ist die Logik, mit der wir das erreichen.

//+------------------------------------------------------------------+
//| Expert Tick Function                                             |
//+------------------------------------------------------------------+
void OnTick() {
// Check for new bar to avoid over-calculation
   if (iTime(_Symbol, _Period, 0) == g_lastBarTime) { //--- Check if new bar
      UpdateDashboard(0, 0, 0, 0, 0, 0, 0, 0, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), GetSignalStatus(false, false)); //--- Update dashboard with no signal
      return;                                        //--- Exit function
   }
   g_lastBarTime = iTime(_Symbol, _Period, 0);       //--- Update last bar time

// Check market availability
   if (!SymbolInfoDouble(_Symbol, SYMBOL_BID) || !SymbolInfoDouble(_Symbol, SYMBOL_ASK)) { //--- Check market data
      Print("Error: Market data unavailable for ", _Symbol); //--- Log error
      UpdateDashboard(0, 0, 0, 0, 0, 0, 0, 0, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), "None"); //--- Update dashboard with no signal
      return;                                        //--- Exit function
   }

// Copy historical close prices
   double prices[];                                  //--- Declare prices array
   ArraySetAsSeries(prices, true);                   //--- Set as series
   int copied = CopyClose(_Symbol, _Period, 1, InpPeriod, prices); //--- Copy close prices
   if (copied != InpPeriod) {                        //--- Check copy success
      Print("Error copying prices: ", copied, ", Error: ", GetLastError()); //--- Log error
      UpdateDashboard(0, 0, 0, 0, 0, 0, 0, 0, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), "None"); //--- Update dashboard with no signal
      return;                                        //--- Exit function
   }

// Calculate statistical moments
   double mean, variance, skewness, kurtosis;        //--- Declare statistical variables
   if (!MathMoments(prices, mean, variance, skewness, kurtosis, 0, InpPeriod)) { //--- Calculate moments
      Print("Error calculating moments: ", GetLastError()); //--- Log error
      UpdateDashboard(0, 0, 0, 0, 0, 0, 0, 0, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), "None"); //--- Update dashboard with no signal
      return;                                        //--- Exit function
   }

// Jarque-Bera test
   double n = (double)InpPeriod;                     //--- Set sample size
   double jb_stat = n * (skewness * skewness / 6.0 + (kurtosis * kurtosis) / 24.0); //--- Calculate JB statistic

// Log statistical values
   Print("Stats: Skewness=", DoubleToString(skewness, 4), ", JB=", DoubleToString(jb_stat, 2), ", Kurtosis=", DoubleToString(kurtosis, 2)); //--- Log stats

// Adaptive skewness thresholds
   double skew_buy_threshold = -0.3 - 0.05 * kurtosis; //--- Calculate buy skew threshold
   double skew_sell_threshold = 0.3 + 0.05 * kurtosis; //--- Calculate sell skew threshold

// Kurtosis filter
   if (kurtosis > InpKurtosisThreshold) {          //--- Check kurtosis threshold
      Print("Trade skipped: High kurtosis (", kurtosis, ") > ", InpKurtosisThreshold); //--- Log skip
      UpdateDashboard(mean, 0, 0, skewness, jb_stat, kurtosis, skew_buy_threshold, skew_sell_threshold, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), "None"); //--- Update dashboard with no signal
      return;                                      //--- Exit function
   }

   double std_dev = MathSqrt(variance);            //--- Calculate standard deviation

// Adaptive confidence interval
   double confidenceLevel = InpConfidenceLevel;    //--- Copy confidence level
   if (confidenceLevel < 0.90 || confidenceLevel > 0.99) //--- Validate confidence level
      confidenceLevel = 0.95;                      //--- Set default confidence level
   double z_score = NormalInverse(0.5 + confidenceLevel / 2.0); //--- Calculate z-score
   double ci_mult = z_score / MathSqrt(n);         //--- Calculate CI multiplier
   double upper_ci = mean + ci_mult * std_dev;     //--- Calculate upper CI
   double lower_ci = mean - ci_mult * std_dev;     //--- Calculate lower CI

// Current close price
   double current_price = iClose(_Symbol, _Period, 0); //--- Get current close price

// Higher timeframe confirmation (if enabled)
   bool htf_valid = true;                          //--- Initialize HTF validity
   if (InpHigherTF != 0) {                         //--- Check if HTF enabled
      double htf_prices[];                         //--- Declare HTF prices array
      ArraySetAsSeries(htf_prices, true);          //--- Set as series
      int htf_copied = CopyClose(_Symbol, InpHigherTF, 1, InpPeriod, htf_prices); //--- Copy HTF close prices
      if (htf_copied != InpPeriod) {               //--- Check HTF copy success
         Print("Error copying HTF prices: ", htf_copied, ", Error: ", GetLastError()); //--- Log error
         UpdateDashboard(mean, lower_ci, upper_ci, skewness, jb_stat, kurtosis, skew_buy_threshold, skew_sell_threshold, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), "None"); //--- Update dashboard with no signal
         return;                                   //--- Exit function
      }
      double htf_mean, htf_variance, htf_skewness, htf_kurtosis; //--- Declare HTF stats
      if (!MathMoments(htf_prices, htf_mean, htf_variance, htf_skewness, htf_kurtosis, 0, InpPeriod)) { //--- Calculate HTF moments
         Print("Error calculating HTF moments: ", GetLastError()); //--- Log error
         UpdateDashboard(mean, lower_ci, upper_ci, skewness, jb_stat, kurtosis, skew_buy_threshold, skew_sell_threshold, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), "None"); //--- Update dashboard with no signal
         return;                                   //--- Exit function
      }
      htf_valid = (current_price <= htf_mean && skewness <= 0) || (current_price >= htf_mean && skewness >= 0); //--- Check HTF validity
      Print("HTF Check: Price=", DoubleToString(current_price, _Digits), ", HTF Mean=", DoubleToString(htf_mean, _Digits), ", Valid=", htf_valid); //--- Log HTF check
   }

// Generate signals
   bool buy_signal = htf_valid && (current_price < lower_ci) && (skewness < skew_buy_threshold) && (jb_stat > InpJBThreshold); //--- Check buy signal conditions
   bool sell_signal = htf_valid && (current_price > upper_ci) && (skewness > skew_sell_threshold) && (jb_stat > InpJBThreshold); //--- Check sell signal conditions

// Fallback signal
   if (!buy_signal && !sell_signal) {              //--- Check no primary signal
      buy_signal = htf_valid && (current_price < mean - 0.3 * std_dev); //--- Check fallback buy
      sell_signal = htf_valid && (current_price > mean + 0.3 * std_dev); //--- Check fallback sell
      Print("Fallback Signal: Buy=", buy_signal, ", Sell=", sell_signal); //--- Log fallback signals
   }

// Log signal status
   Print("Signal Check: Buy=", buy_signal, ", Sell=", sell_signal, ", Price=", DoubleToString(current_price, _Digits),
         ", LowerCI=", DoubleToString(lower_ci, _Digits), ", UpperCI=", DoubleToString(upper_ci, _Digits),
         ", Skew=", DoubleToString(skewness, 4), ", BuyThresh=", DoubleToString(skew_buy_threshold, 4),
         ", SellThresh=", DoubleToString(skew_sell_threshold, 4), ", JB=", DoubleToString(jb_stat, 2)); //--- Log signal details

}

In OnTick überprüfen wir zunächst, ob sich ein neuer Balken gebildet hat, indem wir die aktuelle Balkenzeit aus iTime mit _Symbol, _Period und Shift 0 mit „g_lastBarTime“ vergleichen; Wenn nicht, aktualisieren wir das Dashboard mit Null als Platzhalter für die Statistiken, aktuellen Positionsdetails von Helfern wie „GetPositionStatus“ und „None“-Signalen über „GetSignalStatus“, die falsche Flags übergeben, und beenden dann vorzeitig. Wir aktualisieren „g_lastBarTime“ auf die aktuelle Zeit und stellen die Verfügbarkeit von Marktdaten sicher, indem wir SymbolInfoDouble auf SYMBOL_BID und „SYMBOL_ASK“ überprüfen; falls diese fehlen, protokollieren wir einen Fehler, aktualisieren das Dashboard auf ähnliche Weise und kehren zurück. Wir deklarieren ein Array „prices“, legen es mit ArraySetAsSeries als Serie fest und füllen es mit CopyClose aus Shift 1 für „InpPeriod“-Balken auf; wenn die kopierte Anzahl nicht übereinstimmt, protokollieren wir das Problem mit GetLastError, aktualisieren das Dashboard und beenden es.

Zur Berechnung der Statistiken werden Variablen für „mean“, „variance“, „skewness“ und „kurtosis“ deklariert und MathMoments für das Preis-Array von Index 0 bis „InpPeriod“ aufgerufen; bei Fehlschlag wird der Fehler protokolliert, das Dashboard aktualisiert und zurückgegangen. Wir berechnen den Jarque-Bera-Test als Stichprobengröße „n“ mal (Schiefe quadriert über 6 plus Kurtosis quadriert über 24) und loggen dann die Werte zur besseren Lesbarkeit gerundet. Es werden adaptive Schwellenwerte festgelegt: „skew_buy_threshold“ als -0,3 minus 0,05-fache Kurtosis für Käufe und „skew_sell_threshold“ als 0,3 plus 0,05-fache Kurtosis für Verkäufe. Wenn die Kurtosis den „InpKurtosisThreshold“ überschreitet, wird eine Auslassungsmeldung protokolliert, das Dashboard mit den aktuellen Statistiken und „None“ aktualisiert und beendet, um Szenarien mit hohem Risiko zu vermeiden.

Wir leiten „std_dev“ von MathSqrt der Varianz ab, validieren „confidenceLevel“ innerhalb der Grenzen (standardmäßig 0.95 wenn nicht), berechnen „z_score“ mit „NormalInverse“ auf halbem plus halbem Konfidenzniveau, dann „ci_mult“ als z über der Quadratwurzel von n, was „upper_ci“ und „lower_ci“ als Mittelwert plus/minus ci_mult mal std_dev ergibt. Der aktuelle Preis wird über iClose uns „shift“ 0 geholt. Für Bestätigungen mit höherem Zeitrahmen, wenn „InpHigherTF“ gesetzt ist, kopieren wir die HTF-Preise in ähnlicher Weise, berechnen ihre Momente und setzen „htf_valid“ auf true, wenn der Preis mit dem Mittelwert in Bezug auf das Schiefe-Zeichen übereinstimmt (unter dem Mittelwert mit negativer Schiefe oder über dem Mittelwert mit positiver Schiefe), wobei die Prüfung protokolliert wird; standardmäßig auf true, wenn deaktiviert.

Es werden Signale generiert: „buy_signal“, wenn der HTF gültig ist, der Preis unter dem unteren CI liegt, die Schiefe unter dem Kaufschwellenwert und JB über dem „InpJBThreshold“ liegt; „sell_signal“ wird für den oberen CI und den Verkaufsschwellenwert gespiegelt. Wenn kein primäres Signal vorliegt, wird auf HTF-valide Preisdurchbrüche des Mittelwerts minus/plus 0,3 std_dev für Kauf/Verkauf zurückgegriffen und diese protokolliert. Wir schließen das Segment mit der Aufzeichnung detaillierter Signalbedingungen, einschließlich Preis, KI, Schiefe mit Schwellenwerten und JB. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

SIGNALBESTÄTIGUNG

Anhand des Bildes können wir sehen, dass wir die Berechnungen durchführen und die Signalschwelle bestimmen können. Das ist jetzt erledigt. Wir müssen auf die Signale reagieren. Wir wollen jedoch die bestehenden Positionen schließen, wenn wir neue Signale erhalten. Hier ist die Logik, die wir verwenden, um dies in einer Funktion zu erreichen.

//+------------------------------------------------------------------+
//| Close All Positions of Type                                      |
//+------------------------------------------------------------------+
void CloseAllPositions(ENUM_POSITION_TYPE pos_type) {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions
      if (PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber && PositionGetInteger(POSITION_TYPE) == pos_type) { //--- Check details
         ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Get ticket
         double profit = PositionGetDouble(POSITION_PROFIT); //--- Get profit
         trade.PositionClose(ticket);                //--- Close position
      }
   }
}

Wir definieren die Funktion „CloseAllPositions“, um alle offenen Positionen eines bestimmten „ENUM_POSITION_TYPE“, wie z.B. Kauf oder Verkauf, zu schließen und so sicherzustellen, dass wir entgegengesetzte Handelsgeschäfte vor neuen Signalen löschen. Wir führen eine Rückwärtsschleife von „PositionsTotal“ minus 1 bis Null durch, um eine sichere Iteration ohne Indexprobleme während der Abschlüsse zu gewährleisten. Für jeden, wenn „PositionGetSymbol“ mit „_Symbol“ übereinstimmt, „PositionGetInteger“ mit „POSITION_MAGIC“ gleich „InpMagicNumber“ ist, und der Typ über „POSITION_TYPE“ mit „pos_type“ übereinstimmtWir rufen das „Ticket“ mit „PositionGetInteger“ auf „POSITION_TICKET“ und den Gewinn mit „PositionGetDouble“ auf „POSITION_PROFIT“ ab (nur für den Fall, dass Sie den Gewinn protokollieren und sehen möchten) und führen dann „trade.PositionClose“ für das Ticket aus, um es zu schließen. Wir können nun die entgegengesetzten Handelsgeschäfte schließen und neue Positionen eröffnen, indem wir die folgende Logik anwenden.

// Position management: Close opposite positions
if (HasPosition(POSITION_TYPE_BUY) && sell_signal) //--- Check buy position with sell signal
   CloseAllPositions(POSITION_TYPE_BUY);        //--- Close all buys
if (HasPosition(POSITION_TYPE_SELL) && buy_signal) //--- Check sell position with buy signal
   CloseAllPositions(POSITION_TYPE_SELL);       //--- Close all sells

// Calculate lot size
double lot_size = InpFixedLots;                 //--- Set default lot size
double riskPercent = InpRiskPercent;            //--- Copy risk percent
if (riskPercent > 0) {                          //--- Check if risk percent enabled
   double account_equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Get account equity
   double sl_points = InpBaseStopLossPips * g_pointMultiplier; //--- Calculate SL points
   double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); //--- Get tick value
   if (tick_value == 0) {                       //--- Check invalid tick value
      Print("Error: Invalid tick value for ", _Symbol); //--- Log error
      return;                                   //--- Exit function
   }
   lot_size = NormalizeDouble((account_equity * riskPercent / 100.0) / (sl_points * tick_value), 2); //--- Calculate risk-based lot size
   lot_size = MathMax(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN), MathMin(lot_size, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX))); //--- Clamp lot size
   Print("Lot Size: Equity=", account_equity, ", SL Points=", sl_points, ", Tick Value=", tick_value, ", Lot=", lot_size); //--- Log lot size calculation
}

// Open new positions
if (!HasPosition() && buy_signal) {             //--- Check no position and buy signal
   double sl = current_price - InpBaseStopLossPips * _Point * g_pointMultiplier; //--- Calculate SL
   double tp = current_price + InpBaseTakeProfitPips * _Point * g_pointMultiplier; //--- Calculate TP
   if (trade.Buy(lot_size, _Symbol, 0, sl, tp, "StatReversion Buy: Skew=" + DoubleToString(skewness, 4) + ", JB=" + DoubleToString(jb_stat, 2))) { //--- Open buy order
      Print("Buy order opened. Mean: ", DoubleToString(mean, 5), ", Current: ", DoubleToString(current_price, 5)); //--- Log buy open
   } else {                                     //--- Handle buy open failure
      Print("Buy order failed: ", GetLastError()); //--- Log error
   }
} else if (!HasPosition() && sell_signal) {     //--- Check no position and sell signal
   double sl = current_price + InpBaseStopLossPips * _Point * g_pointMultiplier; //--- Calculate SL
   double tp = current_price - InpBaseTakeProfitPips * _Point * g_pointMultiplier; //--- Calculate TP
   if (trade.Sell(lot_size, _Symbol, 0, sl, tp, "StatReversion Sell: Skew=" + DoubleToString(skewness, 4) + ", JB=" + DoubleToString(jb_stat, 2))) { //--- Open sell order
      Print("Sell order opened. Mean: ", DoubleToString(mean, 5), ", Current: ", DoubleToString(current_price, 5)); //--- Log sell open
   } else {                                     //--- Handle sell open failure
      Print("Sell order failed: ", GetLastError()); //--- Log error
   }
}

// Update dashboard
UpdateDashboard(mean, lower_ci, upper_ci, skewness, jb_stat, kurtosis, skew_buy_threshold, skew_sell_threshold,
                GetPositionStatus(), lot_size, GetCurrentProfit(), GetPositionDuration(), GetSignalStatus(buy_signal, sell_signal)); //--- Update dashboard with signals

Wir verwalten Positionen, indem wir auf Gegensätze achten: Wenn ein Kaufsignal offen ist und ein Verkaufssignal auftaucht, rufen wir „CloseAllPositions“ mit POSITION_TYPE_BUY auf, um alle Kaufpositionen zu schließen; umgekehrt wird bei einem Verkaufssignal ein Kaufsignal ausgelöst, um sicherzustellen, dass es keine widersprüchlichen Handelsgeschäfte gibt, bevor neue Positionen eingegangen werden. Für die Größenbestimmung wird „lot_size“ standardmäßig auf „InpFixedLots“ gesetzt, aber wenn „InpRiskPercent“ positiv ist, wird es dynamisch aus dem Kontoguthaben über AccountInfoDouble mit ACCOUNT_EQUITY, dem um „g_pointMultiplier“ bereinigten SL-Abstand und dem Tick-Wert aus SymbolInfoDouble mit SYMBOL_TRADE_TICK_VALUE berechnet – bei Ungültigkeit wird ein Logeintrag ausgegeben. Die Formel normalisiert das Kapital mal den Risikoprozentsatz (SL-Punkte mal Tick-Wert) auf zwei Dezimalstellen, berechnet unter Berücksichtigung des minimalen und maximalen das Volumen des Symbols mit MathMax und MathMin und protokolliert die Details.

Wenn keine Position existiert und ein Kaufsignal aktiv ist, leiten wir SL unterhalb des aktuellen Preises durch Basispips mal _Point und dem Multiplikator ab, TP oberhalb in ähnlicher Weise, dann versuchen wir „trade.Buy“ mit berechnetem Lot, Symbol, keinem Preis (Markt), SL, TP und einem Kommentar einschließlich Schiefe und JB-Statistiken; protokollieren Erfolg mit Mittelwert vs. aktuellem Preis oder Misserfolg mit der GetLastError-Funktion. Wir spiegeln dies für Verkäufe, indem Sie SL oberhalb und TP unterhalb anpassen. Zum Schluss aktualisieren wir das Dashboard, indem wir die berechneten Statistiken wie Mittelwert, KIs, Schiefe, JB, Kurtosis, Schwellenwerte, Positionsstatus, Lot, Gewinn, Dauer und Signal an die Funktion „UpdateDashboard“ übergeben, die nun mit allen Informationen gefüllt sein sollte. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

HANDELSBESTÄTIGUNG

Nun, da wir die Positionen eröffnen können, müssen wir sie verwalten, insbesondere durch Trailing, Teilschließungen und Schließung innerhalb des Zeitlimits, um ein Überengagement zu vermeiden. All dies sind nur Zusatzfunktionen, die Sie überspringen oder abschalten können, wenn Sie dies nicht wünschen. Wir wollen sie in Funktionen haben.

//+------------------------------------------------------------------+
//| Manage Trailing Stop                                             |
//+------------------------------------------------------------------+
void ManageTrailingStop() {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions
      if (PositionGetSymbol(i) != _Symbol || PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) //--- Check symbol and magic
         continue;                                   //--- Skip if not match

      ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Get ticket
      double current_sl = PositionGetDouble(POSITION_SL); //--- Get current SL
      ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); //--- Get position type
      double current_price = (pos_type == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Get current price

      double trail_distance = InpTrailingStopPips * _Point * g_pointMultiplier; //--- Calculate trail distance
      double trail_step = InpTrailingStepPips * _Point * g_pointMultiplier; //--- Calculate trail step

      if (pos_type == POSITION_TYPE_BUY) {            //--- Check buy position
         double new_sl = current_price - trail_distance; //--- Calculate new SL
         if (new_sl > current_sl + trail_step || current_sl == 0) { //--- Check if update needed
            trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP)); //--- Modify position
         }
      } else if (pos_type == POSITION_TYPE_SELL) {    //--- Check sell position
         double new_sl = current_price + trail_distance; //--- Calculate new SL
         if (new_sl < current_sl - trail_step || current_sl == 0) { //--- Check if update needed
            trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP)); //--- Modify position
         }
      }
   }
}

//+------------------------------------------------------------------+
//| Manage Partial Close                                             |
//+------------------------------------------------------------------+
void ManagePartialClose() {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions
      if (PositionGetSymbol(i) != _Symbol || PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) //--- Check symbol and magic
         continue;                                   //--- Skip if not match

      ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Get ticket
      double open_price = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get open price
      double tp = PositionGetDouble(POSITION_TP); //--- Get TP
      double current_price = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Get current price
      double volume = PositionGetDouble(POSITION_VOLUME); //--- Get position volume

      double half_tp_distance = MathAbs(tp - open_price) * 0.5; //--- Calculate half TP distance
      bool should_close = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && current_price >= open_price + half_tp_distance) ||
                          (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && current_price <= open_price - half_tp_distance); //--- Check if at half TP

      if (should_close) {                             //--- Check if partial close needed
         double close_volume = NormalizeDouble(volume * InpPartialClosePercent, 2); //--- Calculate close volume
         if (close_volume >= SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)) { //--- Check minimum volume
            trade.PositionClosePartial(ticket, close_volume); //--- Close partial position
         }
      }
   }
}

//+------------------------------------------------------------------+
//| Manage Time-Based Exit                                           |
//+------------------------------------------------------------------+
void ManageTimeBasedExit() {
   if (InpMaxTradeHours == 0) return;                //--- Exit if no max duration

   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions
      if (PositionGetSymbol(i) != _Symbol || PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) //--- Check symbol and magic
         continue;                                   //--- Skip if not match

      ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Get ticket
      datetime open_time = (datetime)PositionGetInteger(POSITION_TIME); //--- Get open time
      datetime current_time = TimeCurrent();         //--- Get current time
      if ((current_time - open_time) / 3600 >= InpMaxTradeHours) { //--- Check if duration exceeded
         double profit = PositionGetDouble(POSITION_PROFIT); //--- Get profit
         trade.PositionClose(ticket);                //--- Close position
      }
   }
}

Hier implementieren wir die Funktion „ManageTrailingStop“, um die Stop-Losses für offene Positionen dynamisch anzupassen, wenn sich die Kurse günstig entwickeln, wobei wir die Positionen von PositionsTotal minus 1 bis Null rückwärts durchlaufen. Bei Übereinstimmungen von Symbol und Magie werden das Ticket, der aktuelle Stop-Loss, der Typ als ENUM_POSITION_TYPE von PositionGetInteger auf „POSITION_TYPE“ und der entsprechende Preis (Bid für Käufe oder Ask für Verkäufe) abgerufen. Wir berechnen Trail-Abstand und Schritt mit Eingabe Pips mal „_Point“ und Multiplikator, dann für Käufe, schlagen eine neue Ebene unter dem aktuellen Preis von Abstand, Aktualisierung über „trade.PositionModify“, wenn es über aktuelle Stop-Loss plus Schritt oder wenn unset; Spiegel für Verkäufe, Verschieben Stop-Loss über, wenn unter aktuellen minus Schritt.

Als Nächstes behandelt die Funktion „ManagePartialClose“ Gewinnmitnahmen, indem sie einen Teil auf halbem Weg zum Take Profit (TP) schließt und auf ähnliche Weise iteriert, um qualifizierte Positionen zu finden. Wir erhalten ein Ticket, den Eröffnungskurs mit PositionGetDouble auf POSITION_PRICE_OPEN, den TP, den aktuellen Preis je nach Typ und das Volumen über „POSITION_VOLUME“; wir berechnen den halben TP-Abstand als absoluten TP minus Eröffnungskurs mal 0,5 und prüfen, ob der Preis ihn erreicht hat (bei Käufen darüber, bei Verkäufen darunter). Wenn dies der Fall ist, normalisieren Sie das Abschlussvolumen als Positionsgröße mal „InpPartialClosePercent“ auf zwei Dezimalstellen, und wenn das Mindestvolumen des Symbols aus SymbolInfoDouble mit SYMBOL_VOLUME_MIN erreicht oder überschritten wird, führen Sie „trade.PositionClosePartial“ für das Ticket und das Volumen aus.

Bei „ManageTimeBasedExit“ kehren wir vorzeitig zurück, wenn „InpMaxTradeHours“ gleich Null ist, andernfalls führen wir eine Schleife durch, um übereinstimmende Positionen zu identifizieren, holen das Ticket, die Eröffnungszeit als Datetime aus „PositionGetInteger“ mit „POSITION_TIME“ und vergleichen mit TimeCurrent, um festzustellen, ob die verstrichenen Stunden das Limit erreichen oder überschreiten. Wenn ja, notieren wir den Gewinn und schließen den Handel mit „trade.PositionClose“ auf dem Ticket, wobei wir die Laufzeitbegrenzung einhalten. Wir können nun diese Funktionen aufrufen, um die Positionsverwaltung durchzuführen.

if (InpUseTrailingStop)                      //--- Check trailing stop enabled
   ManageTrailingStop();                     //--- Manage trailing stop
if (InpUsePartialClose)                      //--- Check partial close enabled
   ManagePartialClose();                     //--- Manage partial close
ManageTimeBasedExit();                       //--- Manage time-based exit

Wenn wir kompilieren, erhalten wir folgendes Ergebnis.

POSITIONS-MANAGEMENT

Wir können sehen, dass wir die günstigen Positionen aktiv verändern und verfolgen. Da wir unsere Ziele erreicht haben, bleibt nur noch das Backtesting des Programms, das im nächsten Abschnitt behandelt wird.


Backtests

Nach einem gründlichen Backtest erhalten wir folgende Ergebnisse.

Backtest-Grafik:

GRAPH

Bericht des Backtest:

BERICHT


Schlussfolgerung

Zusammenfassend haben wir ein statistisches Mean-Reversion-System in MQL5 entwickelt, das Preisdaten für Momente wie Mittelwert, Varianz, Schiefe, Kurtosis und Jarque-Bera-Test analysiert, Signale auf der Grundlage von Konfidenzintervallverletzungen mit adaptiven Schwellenwerten und optionaler Bestätigung in einem höheren Zeitrahmen generiert, Handelsgeschäfte mit aktienbasierter oder fester Größe ausführt und Trailing Stops, Teilschließungen und zeitbasierte Ausstiege für eine umfassende Risikokontrolle einbezieht, die alle durch ein Echtzeit-Dashboard auf dem Chart verbessert werden.

Haftungsausschluss: Dieser Artikel ist nur für Bildungszwecke gedacht. Der Handel ist mit erheblichen finanziellen Risiken verbunden, und die Volatilität der Märkte kann zu Verlusten führen. Gründliche Backtests und sorgfältiges Risikomanagement sind entscheidend, bevor Sie dieses Programm auf den Live-Märkten einsetzen.

Mit dieser statistische Strategie der Rückkehr zum Mittelwert sind Sie bestens gerüstet, um Gelegenheiten mit nicht-normaler Verteilung zu nutzen und Ihren Handel weiter zu optimieren. Viel Spaß beim Handeln!

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

Risikobasierter Trade Placement EA mit On-Chart UI (Teil 2): Hinzufügen von Interaktivität und Logik Risikobasierter Trade Placement EA mit On-Chart UI (Teil 2): Hinzufügen von Interaktivität und Logik
Lernen Sie, wie man einen interaktiven MQL5 Expert Advisor mit einem Kontrollfeld auf dem Chart erstellt. Sie wissen, wie man risikobasierte Losgrößen berechnet und Handelsgeschäfte direkt vom Chart aus tätigt.
Entwicklung des Price Action Analysis Toolkit (Teil 49): Integration von Trend-, Momentum- und Volatilitätsindikatoren in ein MQL5-System Entwicklung des Price Action Analysis Toolkit (Teil 49): Integration von Trend-, Momentum- und Volatilitätsindikatoren in ein MQL5-System
Vereinfachen Sie Ihre MetaTrader 5 Charts mit dem Multi Indicator Handler EA. Dieses interaktive Dashboard fasst Trend-, Momentum- und Volatilitätsindikatoren in einem Echtzeit-Panel zusammen. Wechseln Sie im Handumdrehen zwischen den Profilen und konzentrieren Sie sich auf die Analyse, die Sie am meisten benötigen. Mit den Ein-Klick-Steuerelementen zum Ausblenden/Einblenden können Sie sich auf die Kursentwicklung konzentrieren. Lesen Sie weiter, um Schritt für Schritt zu erfahren, wie Sie es in MQL5 selbst erstellen und anpassen können.
Der MQL5 Standard Library Explorer (Teil 3): Experte für den Kanal der Standardabweichung Der MQL5 Standard Library Explorer (Teil 3): Experte für den Kanal der Standardabweichung
In dieser Diskussion werden wir einen Expert Advisor entwickeln, der die Klassen CTrade und CStdDevChannel verwendet und dabei mehrere Filter zur Verbesserung der Rentabilität anwendet. In dieser Phase wird unsere vorherige Diskussion in die Praxis umgesetzt. Außerdem werde ich einen weiteren einfachen Ansatz vorstellen, der Ihnen helfen soll, die MQL5-Standardbibliothek und die ihr zugrunde liegende Codebasis besser zu verstehen. Nehmen Sie an der Diskussion teil, um diese Konzepte in der Praxis zu erkunden.
Risk-Based Trade Placement EA mit On-Chart UI (Part 1): Gestaltung der Nutzeroberfläche Risk-Based Trade Placement EA mit On-Chart UI (Part 1): Gestaltung der Nutzeroberfläche
Lernen Sie, wie man ein sauberes und professionelles On-Chart-Kontrollpanel in MQL5 für einen Risk-Based Trade Placement Expert Advisor erstellt. Diese Schritt-für-Schritt-Anleitung erklärt, wie man eine funktionale GUI entwirft, die es Händlern ermöglicht, Handelsparameter einzugeben, die Losgröße zu berechnen und die automatische Auftragserteilung vorzubereiten.