Automatisieren von Handelsstrategien in MQL5 (Teil 39): Statistische Rückkehr zum Mittelwert mit Konfidenzintervallen und Dashboard
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:
- Verstehen der Strategie der Statistik der Rückkehr zum Mittelwert
- Implementation in MQL5
- Backtests
- 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.

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.

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.

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:

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:

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.

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:

Bericht des Backtest:

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
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Risikobasierter Trade Placement EA mit On-Chart UI (Teil 2): Hinzufügen von Interaktivität und Logik
Entwicklung des Price Action Analysis Toolkit (Teil 49): Integration von Trend-, Momentum- und Volatilitätsindikatoren in ein MQL5-System
Der MQL5 Standard Library Explorer (Teil 3): Experte für den Kanal der Standardabweichung
Risk-Based Trade Placement EA mit On-Chart UI (Part 1): Gestaltung der Nutzeroberfläche
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.