Klassische Strategien neu interpretieren (Teil 21): Entdeckung einer Ensemble-Strategie aus Bollinger-Bändern und RSI
Die Bollinger-Bänder sind ein typischer technischer Indikator, der von Händlern aller Erfahrungsstufen verwendet wird. Sie werden am häufigsten verwendet, um entweder Unterstützungs- und Widerstandsniveaus zu ermitteln, oder sie können auch verwendet werden, um Mean-Reverting-Handelsstrategien (Rückkehr zum Mittelwert) zu erleichtern. Die vorherrschende Überzeugung, die ihrer Verwendung zugrunde liegt, ist, dass die Preisniveaus dazu tendieren, sich einem Gleichgewichtspreisniveau anzunähern. Der Indikator wird durch einen gleitenden Durchschnitt definiert, der von einem oberen und einem unteren Band umschlossen wird, die jeweils auf eine bestimmte Standardabweichung über und unter dem gleitenden Durchschnitt festgelegt sind. Die Breite dieser Standardabweichung ist ein wichtiger Einstellparameter des Indikators.
In der klassischen Konstellation wird erwartet, dass der Kurs in Richtung des zentralen gleitenden Durchschnitts zurückkehrt, wenn er das obere Bollinger-Band durchbricht. Umgekehrt wird bei einem Durchbruch des Preises unter das untere Band ein Anstieg in Richtung des Gleichgewichts erwartet. In der Praxis verhalten sich die Märkte jedoch nicht immer innerhalb dieser klar definierten Grenzen. In manchen Regimen zeigen die Märkte ein Mean-Reverting-Verhalten, bei dem die klassische Verwendung der Bollinger-Bänder profitabel sein kann. In anderen Zeiten folgen die Märkte starken Trends, und Händler, die sich auf diese klassischen Regeln verlassen, können anhaltende Verluste erleiden. Dies wirft die offene Frage auf, wie Bollinger-Bänder trotz ständiger Veränderungen des Marktregimes gewinnbringend eingesetzt werden können.
Eine mögliche Lösung besteht darin, die Bollinger-Bänder mit einem anderen technischen Indikator zu kombinieren, um zwischen mittleren Umkehrbewegungen und Trendbedingungen zu unterscheiden. Ein guter Kandidat für diese Rolle ist der Relative Strength Index (RSI). Durch die Kopplung dieser beiden Indikatoren werden Long-Trades nur dann in Betracht gezogen, wenn der Kurs unter die untere Extremwertbande fällt und der RSI gleichzeitig in den überverkauften Bereich eintritt. Dies ist eine weitere Bestätigung dafür, dass sich der Preis wahrscheinlich wieder dem Gleichgewicht annähern wird. Ähnlich verhält es sich, wenn der Kurs über das obere Extremband ausbricht: Short-Trades werden nur dann in Betracht gezogen, wenn der RSI ebenfalls in überkaufte Regionen eintritt, was die Wahrscheinlichkeit einer Rückkehr zum Mittelwert erhöht.
In diesem Artikel wird die Machbarkeit der vorgeschlagenen Kopplung untersucht und ein Verfahren zur Verfeinerung der Strategie skizziert. Aus unseren Beobachtungen geht hervor, dass die Bollinger-Bänder und der RSI zwar Handelssignale mit hoher Wahrscheinlichkeit erzeugen, diese Signale jedoch mit einer zu geringen Frequenz auftreten, um systematische Handelsziele zu unterstützen. Um dieser Einschränkung zu begegnen, werden fünf Varianten der Strategie bewertet, um zusätzliche Signale zu extrahieren und gleichzeitig das Rauschen zu minimieren. Dieser Prozess erwies sich zwar als schwierig, doch mithilfe statistischer Modellierungstechniken konnten wir Handelssignale identifizieren, die durch manuell erstellte Regeln nicht sofort erkennbar waren. Dieser Artikel zeigt, wie klassische Handelskonzepte mit modernen algorithmischen Ansätzen angepasst und erweitert werden können.
Wichtige Informationen zu beachten
Wie wir bereits in der Einleitung unseres Artikels erwähnt haben, werden fünf Versionen dieser Handelsstrategie iterativ umgesetzt. Für die Leser, die Ihnen folgen möchten, empfehlen wir, dass Sie Ihre Anwendung in der gleichen Struktur organisieren, wie in Abbildung 1 unten dargestellt ist.

Abbildung 1: Die Dateistruktur, die wir in diesem Artikel verwenden werden
Um unnötige Wiederholungen derselben Informationen zu vermeiden, werden wir nun wichtige Aspekte des Backtests hervorheben, die wir durchführen und die über alle fünf Iterationen der Anwendung hinweg unverändert bleiben werden. Die erste Einstellung, die wir skizzieren müssen und die wir beibehalten werden, ist die Zeitspanne des Backtests. Unser Backtest wird über drei Jahre laufen, von Januar 2023 bis Januar 2026. Alle unsere Backtests werden für das Währungspaar EURUSD auf dem täglichen Zeitrahmen durchgeführt.

Abbildung 2: Die Backtest-Daten, die wir für alle 4 Iterationen unserer Anwendungen verwenden werden
Ferner werden wir zufällige Verzögerungseinstellungen verwenden, um die Unsicherheit des Live-Handels zu imitieren. Dadurch erhalten wir eine realistische Vorstellung von den Verzögerungen, die bei der Übermittlung von Trades über reale Netze auftreten. Ferner wird unsere Modellierung auf realen Ticks basieren, um sicherzustellen, dass die Ergebnisse, die wir erhalten, so realitätsnah wie möglich sind und dennoch auf historischen Daten beruhen.

Abbildung 3: Die oben genannten Backtest-Bedingungen werden für alle unsere Tests festgelegt
Festlegung einer Basislinie
Wir beginnen mit der Erstellung einer Basisversion unserer Anwendung, um Rentabilitätsschwellen festzulegen, die wir mit allen nachfolgenden Implementierungen der Anwendung übertreffen wollen. Als Erstes müssen wir die für unsere Anwendung erforderlichen Handelsbibliotheken laden. Wir beginnen mit dem Laden der Handelsbibliothek, die uns bei der Verwaltung unserer Positionen hilft. Ferner laden wir eine nutzerdefinierte Bibliothek namens TradeInfo, die wir geschrieben haben, um Aufgaben wie das Abrufen der aktuellen Geld- und Briefkurse und des zulässigen Mindestvolumens auf dem Markt zu bewältigen.//+------------------------------------------------------------------+ //| Version 1.mq5 | //| Copyright 2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade/Trade.mqh> #include <VolatilityDoctor/Trade/TradeInfo.mqh> CTrade Trade; TradeInfo *TradeHelper;
Der nächste wichtige Schritt ist nun die Definition wichtiger Systemkonstanten, die in allen Iterationen unserer Anwendung konsistent gehalten werden müssen. Diese Systemdefinitionen sind wichtige Einstellungsparameter unserer Handelsstrategie, und eine Änderung dieser Systemdefinitionen wird die Leistung der Handelsstrategie drastisch verändern. Um sicherzustellen, dass alle Verbesserungen, die wir beobachten werden, auf Verbesserungen unserer Handelslogik zurückzuführen sind, ist es daher von größter Bedeutung, dass wir diese wichtigen Abstimmungsparameter festlegen.
//+------------------------------------------------------------------+ //| System definitions | //+------------------------------------------------------------------+ #define ATR_PERIOD 14 #define ATR_MULTIPLE 2 #define BB_PERIOD 30 #define BB_SD 2 #define BB_PRICE PRICE_CLOSE #define RSI_PERIOD 15 #define RSI_PRICE PRICE_CLOSE #define RSI_LEVEL_MAX 70 #define RSI_LEVEL_MIN 30 #define SYMBOL "EURUSD" #define TF_MAIN PERIOD_D1 #define TF_TRADING PERIOD_H4 #define SHIFT 0
Nachdem wir unsere Abstimmungsparameter festgelegt haben, definieren wir nun globale Variablen, die in zahlreichen verschiedenen Bereichen unserer Handelsanwendung verwendet werden. Diese globalen Variablen bilden die Handler und Puffer unserer technischen Indikatoren.
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ int bb_handler,rsi_handler,atr_handler; double bb_upper[],bb_mid[],bb_lower[],rsi[],atr[];
Wenn unsere Handelsanwendung zum ersten Mal initialisiert wird, beginnen wir damit, unsere Indikator-Handler zu definieren. Nachdem wir die entsprechenden technischen Indikatoren geladen haben, müssen wir als Nächstes sicherstellen, dass jeder unserer technischen Indikatoren korrekt geladen wurde und keiner von ihnen ungültig ist. Wir tun dies, indem wir einfach prüfen, ob jedes Handle gleich dem Makro „INVALID_HANDLE“ ist, das in der MQL5-API definiert ist. Wenn einer der Indikatoren nicht korrekt geladen wird, geben wir dem Nutzer eine Rückmeldung und brechen dann den Initialisierungsprozess ab. Andernfalls, wenn alles in Ordnung ist, laden wir unsere nutzerdefinierte Klasse und geben eine erfolgreiche Initialisierung zurück.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Setup the technical indicators bb_handler = iBands(SYMBOL,TF_MAIN,BB_PERIOD,SHIFT,BB_SD,BB_PRICE); rsi_handler = iRSI(SYMBOL,TF_MAIN,RSI_PERIOD,RSI_PRICE); atr_handler = iATR(SYMBOL,TF_MAIN,ATR_PERIOD); //--- Validate the indicators were setup correctly if(bb_handler == INVALID_HANDLE) { //--- Failed to sertup the Bollinger Bands Comment("Failed to setup the Bollinger Bands Indicator: ",GetLastError()); return(INIT_FAILED); } else if(rsi_handler == INVALID_HANDLE) { //--- Failed to setup the RSI indicator Comment("Failed to setup the RSI Indicator: ",GetLastError()); return(INIT_FAILED); } else if(atr_handler == INVALID_HANDLE) { //--- Failed to setup the ATR indicator Comment("Failed to setup the ATR Indicator: ",GetLastError()); return(INIT_FAILED); } else { //--- User defined types TradeHelper = new TradeInfo(SYMBOL,TF_MAIN); //--- Good news: no errors return(INIT_SUCCEEDED); } }
Wenn unsere Anwendung nicht mehr verwendet wird, geben wir die Speicherressourcen frei, die das Terminal für die Indikatoren und die von uns geladene Bibliothek zugewiesen hat. Dies ist eine gute Programmierpraxis in MQL5, da sie sicherstellt, dass wir hinter uns aufräumen.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release the indicators IndicatorRelease(bb_handler); IndicatorRelease(rsi_handler); IndicatorRelease(atr_handler); delete TradeHelper; }
Immer wenn wir neue Kurse vom Broker erhalten, beginnen wir damit, die Zeit zu messen. In MQL5 ist es einfach, die Bildung einer neuen Kerze algorithmisch zu erkennen. Wenn sich eine neue Kerze gebildet hat, aktualisieren wir unseren letzten aufgezeichneten Zeitstempel und fahren fort, unsere Indikatorpuffer zu aktualisieren. Zu guter Letzt behalten wir, bevor wir unsere Handelsregeln überprüfen, auch den Schlusskurs im Auge.
Wir lassen nur jeweils eine Stelle in unserer Bewerbung zu. Daher prüfen wir zunächst, ob wir keine offenen Positionen haben. Wenn dies der Fall ist, dann handeln wir nach den in der Einleitung des Artikels beschriebenen Regeln. Das heißt, wenn der Schlusskurs über dem obersten extremen Bollinger-Band liegt und der RSI sich im überkauften Bereich befindet, werden wir verkaufen. Für Long-Positionen gilt das Gegenteil: Wir warten darauf, dass der Kurs unter das unterste Bollinger-Band fällt und der RSI in überverkaufte Bereiche eintritt.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Keep track of the time static datetime time_stamp; datetime time_current = iTime(SYMBOL,TF_TRADING,0); //--- Check if a new candle has formed if(time_stamp != time_current) { //--- Update the time time_stamp = time_current; //--- Update our indicator readings CopyBuffer(bb_handler,0,0,1,bb_mid); CopyBuffer(bb_handler,1,0,1,bb_upper); CopyBuffer(bb_handler,2,0,1,bb_lower); CopyBuffer(rsi_handler,0,0,1,rsi); CopyBuffer(atr_handler,0,0,1,atr); //--- Update current price levels double close = iClose(SYMBOL,TF_MAIN,SHIFT); //--- If we have no open positions if(PositionsTotal() == 0) { //--- Check for our trading signal if((close > bb_upper[0]) && (rsi[0] > RSI_LEVEL_MAX)) { Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),""); } else if((close < bb_lower[0]) && (rsi[0] < RSI_LEVEL_MIN)) { Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),""); } } } } //+------------------------------------------------------------------+
Damit ist unsere Bewerbung abgeschlossen. Der letzte Schritt besteht darin, alle Systemdefinitionen, die wir in die Kopfzeile der Anwendung eingegeben haben, rückgängig zu machen. Auch dies ist eine weitere gute Programmierpraxis in MQL5.
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef ATR_PERIOD #undef ATR_MULTIPLE #undef BB_PERIOD #undef BB_SD #undef BB_PRICE #undef RSI_PERIOD #undef RSI_PRICE #undef SYMBOL #undef TF_MAIN #undef SHIFT #undef TF_TRADING //+------------------------------------------------------------------+
Die Kapitalkurve, die sich aus den von uns definierten Handelsregeln ergibt, ist in Abbildung 4 dargestellt. Wie wir sehen können, zeigt die Kapitalkurve einen Aufwärtstrend. Auch wenn es immer wieder zu Rückschlägen und einer nicht akzeptablen Volatilität kommt, ist der Trend insgesamt positiv.

Abbildung 4: Die Kapitalkurve, die sich aus unserem ersten Versuch ergibt, die Bollinger-Bänder und den RSI zu kombinieren
Wenn wir die detaillierten Statistiken der von uns definierten Handelsregeln analysieren, stellen wir eine Mischung aus positiven und negativen Ergebnissen fest. Wir beginnen mit den positiven Aspekten der Handelsstrategie. Wir stellen fest, dass 64 % aller nach den Regeln getätigten Trades profitabel waren. Dies steht für erstklassige, hochwertige Handelssignale, und die erwartete Auszahlung beträgt 4,37. Diese Statistiken sind sehr ermutigend.
Die Gesamtzahl der platzierten Trades betrug jedoch nur 14. Der Leser sollte sich daran erinnern, dass dieser Backtest über einen Zeitraum von drei Jahren lief. Das bedeutet, dass im Durchschnitt weniger als fünf Abschlüsse pro Jahr getätigt wurden. Dies ist in jeder Hinsicht bedauerlich und inakzeptabel. Daher möchten wir auf intelligente Weise mehr Signale entdecken und zusätzliche Handelsmöglichkeiten aufdecken, ohne die Qualität der Strategie zu verschlechtern. Dies ist ein heikles Gleichgewicht, und die entsprechenden Regeln sind nicht sofort ersichtlich.

Abbildung 5: Die detaillierten Statistiken unserer ersten Iteration der Handelsanwendung
Verbesserung der Ausgangssituation
Um unser anfängliches Leistungsniveau zu verbessern, haben wir viele verschiedene handschriftliche Regeln ausprobiert, um sowohl das Signal-Rausch-Verhältnis als auch die Häufigkeit der Trades zu verbessern. Unsere Intuition ließ uns vermuten, dass wir durch die Suche nach einer starken Marktdynamik in der Lage sein könnten, hochwertige Handelsmöglichkeiten zu entdecken. Viele Fundamentaltrader verwenden Kerzenmuster, um die Marktstimmung zu analysieren. Deshalb haben wir nach Kerzenmustern gesucht, die auf starke Marktbewegungen hinweisen.//--- Update current price levels double close = iClose(SYMBOL,TF_MAIN,SHIFT); double open_current = iOpen(SYMBOL,TF_MAIN,SHIFT); double open_previous = iOpen(SYMBOL,TF_MAIN,1); double low_current = iLow(SYMBOL,TF_MAIN,SHIFT); double low_previous = iLow(SYMBOL,TF_MAIN,1); double high_current = iHigh(SYMBOL,TF_MAIN,SHIFT); double high_previous = iHigh(SYMBOL,TF_MAIN,1);
Eine häufig zitierte Regel, an die sich Händler halten, ist die Identifizierung höherer Hochs oder tieferer Tiefs. Wenn der höchste Kurs des aktuellen Tages höher ist als das Hoch des Vortages, deutet dies auf eine starke Aufwärtsbewegung hin. Ist der niedrigste Kurs des aktuellen Tages hingegen niedriger als das Tief des Vortages, deutet dies auf eine starke Abwärtsdynamik hin. Wir waren daher überzeugt, dass die Kopplung dieser beiden Handelsstrategien unsere Eingaben verfeinern und zuverlässigere Handelsmöglichkeiten bieten könnte.
//--- If we have no open positions if(PositionsTotal() == 0) { //--- Check for our trading signal if(((close > bb_upper[0]) && (rsi[0] > RSI_LEVEL_MAX)) || ((low_current<low_previous) && (high_current>high_previous) && (open_current<open_previous) && (close < bb_mid[0]))) { Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),""); } else if(((close < bb_lower[0]) && (rsi[0] < RSI_LEVEL_MIN)) || ((high_current>high_previous) && (low_current<low_previous) && (open_current>open_previous) && (close > bb_mid[0]))) { Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),""); } }
Wie der Leser jedoch aus der resultierenden Kapitalkurve ersehen kann, war die Strategie zwar profitabel, blieb aber hinter unseren Erwartungen zurück. Die neue Handelsstrategie erreichte ein Gleichgewichtsniveau, das niedriger war als alle zuvor beobachteten. In der ersten Konstellation erreichte die Strategie ein Tief von 84 $, während sie in der aktuellen Konstellation ein neues Tief von 71 $ erreichte. Dies ist unerwünscht.
Außerdem lag der Endsaldo der ursprünglichen Handelsstrategie über 150 $, während die neue Strategie unter 140 $ endete. Infolgedessen spricht nichts von dem, was in der Kapitalkurve zu beobachten ist, für die weitere Anwendung der neuen Regeln, die unserer Intuition zufolge funktionieren würden.

Abbildung 6: Die von der zweiten Iteration unseres Expert Advisors erstellte Kapitalkurve entsprach nicht unseren Erwartungen
Bei der Analyse der detaillierten Statistiken der neuen Handelsstrategie stellen wir fest, dass die Gesamtzahl der Trades von ursprünglich 14 auf 29 gestiegen ist. Dies stellt eine Verbesserung um 100 % dar und bestätigt, dass es uns gelungen ist, zusätzliche Signale aufzudecken. Der Gesamtnettogewinn sank jedoch von 61 $ auf 35 $, was darauf hindeutet, dass zusätzliches Rauschen in die Strategie eingeführt wurde. Obwohl die Strategie rentabel blieb, kann sie im Vergleich zu den Benchmarkergebnissen nicht als Erfolg betrachtet werden.

Abbildung 7: Die detaillierten Statistiken, die von der zweiten Iteration unserer Handelsanwendung erstellt wurden, offenbaren tiefe Mängel in unserer Handelslogik
Abruf historischer Marktdaten
Obwohl nur zwei Versionen der Handelsstrategie abgebildet wurden, kann der Leser sicher sein, dass viele weitere Iterationen getestet wurden. Nachdem wir alle manuellen, regelbasierten Ansätze ausgeschöpft hatten, kamen wir zu dem Schluss, dass die Nutzung statistischer Modelle uns helfen könnte, Handelsregeln zu entdecken, die über das hinausgehen, was unsere Intuition allein hervorbringen könnte. Zu diesem Zweck haben wir zunächst ein Skript geschrieben, um historische Daten aus dem Terminal in eine CSV-Datei zu extrahieren. Unter Verwendung der gleichen Systemdefinitionen wie zuvor haben wir die historischen Werte für Eröffnung, Höchst-, Tiefst- und Schlusskurs und die technischen Indikatoren auf die Festplatte geschrieben.//+------------------------------------------------------------------+ //| Fetch Data Bollinger Bands RSI Strategy | //| Copyright 2026, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //+------------------------------------------------------------------+ //| System definitions | //+------------------------------------------------------------------+ #define BB_PERIOD 30 #define BB_SD 2 #define BB_PRICE PRICE_CLOSE #define RSI_PERIOD 15 #define RSI_PRICE PRICE_CLOSE #define RSI_LEVEL_MAX 70 #define RSI_LEVEL_MIN 30 #define TF_MAIN PERIOD_D1 #define SHIFT 0 //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ double bb_upper[],bb_mid[],bb_lower[],rsi[]; //--- Setup the technical indicators int bb_handler = iBands(Symbol(),TF_MAIN,BB_PERIOD,SHIFT,BB_SD,BB_PRICE); int rsi_handler = iRSI(Symbol(),TF_MAIN,RSI_PERIOD,RSI_PRICE); //--- File name string file_name = Symbol() + " Bollinger Band RSI Data.csv"; //--- Amount of data requested input int size = 365; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { //--- Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); CopyBuffer(bb_handler,0,0,size,bb_mid); ArraySetAsSeries(bb_mid,true); CopyBuffer(bb_handler,1,0,size,bb_upper); ArraySetAsSeries(bb_upper,true); CopyBuffer(bb_handler,2,0,size,bb_lower); ArraySetAsSeries(bb_lower,true); CopyBuffer(rsi_handler,0,0,size,rsi); ArraySetAsSeries(rsi,true); for(int i=size;i>=1;i--) { if(i == size) { FileWrite(file_handle, //--- Time "Time", //--- OHLC "Open", "High", "Low", "Close", //--- Technical Indicators "BB Upper", "BB Mid", "BB Lower", "RSI" ); } else { FileWrite(file_handle, iTime(_Symbol,PERIOD_CURRENT,i), //--- OHLC iOpen(_Symbol,PERIOD_CURRENT,i), iHigh(_Symbol,PERIOD_CURRENT,i), iLow(_Symbol,PERIOD_CURRENT,i), iClose(_Symbol,PERIOD_CURRENT,i), //--- Technical Indicators bb_upper[i], bb_mid[i], bb_lower[i], rsi[i] ); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+
Erreichen neuer Leistungsniveaus
Nach dem Speichern wurden die Daten mit statistischen Bibliotheken in Python analysiert. Der erste Schritt bestand darin, die erforderlichen analytischen Bibliotheken zu laden, gefolgt vom Einlesen der vom MQL5-Skript erzeugten CSV-Datei.
#Load the analytical libraries import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt
Um Datenverluste zu vermeiden, haben wir sichergestellt, dass das KI-Modell nicht an denselben Daten trainiert wurde, die für das Backtesting verwendet wurden. Der dreijährige Backtest-Zeitraum wurde daher aus dem Trainingsdaten entfernt.
#Read in the data data = pd.read_csv("/ENTER/YOUR/PATH/HERE/EURUSD Bollinger Band RSI Data.csv")
Ein Schnappschuss der resultierenden Trainingsdaten wurde als Referenz zur Verfügung gestellt.
#Drop the dates that overlap with our backtest train = data.iloc[:((-365 * 2) - 90),:] test = data.iloc[((-365 * 2) - 90):,:] #Check the dates left train

Abbildung 8: Wir haben alle Beobachtungen herausgefiltert, die sich mit unserem Backtest-Zeitraum überschneiden. Achten Sie darauf, dass Sie dasselbe für die beste Praxis tun.
Um die Korrektheit der exportierten Daten zu überprüfen, haben wir die Werte aufgezeichnet. Wie gezeigt, umhüllen die Bollinger-Bänder den Preis korrekt, was bestätigt, dass die Daten richtig geschrieben wurden.
plt.plot(data['Close'],color='green') plt.plot(data['BB Upper'],color='red') plt.plot(data['BB Lower'],color='blue') plt.grid() plt.title('Visualizing Historical EURUSD Exchange Rates') plt.ylabel('Exchange Rate') plt.xlabel('Historical Time') plt.legend(['EURUSD Close','BB Upper','BB Lower'])

Abbildung 9: Visuelle Überprüfung, ob unser MQL5-Skript die gewünschten historischen EURUSD-Daten korrekt erfasst hat.
Da nicht sofort ersichtlich ist, welches statistische Modell am besten abschneidet, haben wir mehrere Kandidatenmodelle mittels Kreuzvalidierung bewertet.
#Load our machine learning training libraries from sklearn.linear_model import LinearRegression,Ridge,Lasso,ARDRegression from sklearn.neighbors import KNeighborsRegressor,RadiusNeighborsRegressor from sklearn.svm import LinearSVR from sklearn.ensemble import RandomForestRegressor,BaggingRegressor,AdaBoostRegressor from sklearn.model_selection import TimeSeriesSplit,cross_val_score
Wir haben den Prognosehorizont festgelegt.
#Define our forecast horizon HORIZON = 5
Und dann die Daten mit dem zukünftigen Schlusskurs gekennzeichnet.
#Label the data train['Target'] = train['Close'].shift(-HORIZON)
Es wurde ein Wörterbuch der infrage kommenden Modelle erstellt.
#List all the models we wish to evaluate
models = [LinearRegression(),Ridge(),Lasso(),ARDRegression(),KNeighborsRegressor(),RadiusNeighborsRegressor(),LinearSVR(),RandomForestRegressor(),BaggingRegressor(),AdaBoostRegressor()] Ihre Leistung wurde anhand einer Zeitreihen-Kreuzvalidierung bewertet. Da das Mischen von Beobachtungen für Zeitreihenvorhersagen inakzeptabel ist, haben wir das Objekt „TimeSeriesSplit“ aus der Bibliothek scikit-learn verwendet.
#Define a time series cross validation object tscv = TimeSeriesSplit( n_splits=5, gap=HORIZON )
Wir bereiten uns nun darauf vor, die von jedem Modell, das wir aus unserer Bibliothek für maschinelles Lernen ausgewählt haben, erzielten Leistungswerte zu speichern.
#Store the performance of each model
scores = [] Der mittlere quadratische Fehler jedes Modells wurde berechnet und aufgezeichnet.
#Evaluate each model for model in models: #User feedback print("Evaluating model: ",model) #Store the current score current_score = np.mean(np.abs(cross_val_score(model,train.iloc[:,1:-1],train.iloc[:,-1],cv=tscv,scoring='neg_mean_squared_error'))) scores.append(current_score)
Bei der Visualisierung der Ergebnisse erwies sich Modell 3 als das beste Ergebnis. Obwohl Modell 5 einen Fehler von nahezu null zu produzieren schien, fiel es bei der Kreuzvalidierung durch und lieferte NaN-Werte. Diese Informationen sind in der nachstehenden Tabelle im Einzelnen aufgeführt.
sns.barplot(scores)
plt.ylabel('Cross Validated RMSE')
plt.xlabel('Model')
plt.title('Model Selection For The EURUSD Market')
plt.axhline(scores[3],linestyle=':',color='red') 
Abbildung 10: Das ARD-Regressionsmodell war das leistungsfähigste Modell, das wir bei dieser Übung ermittelt haben.
Modell 3 entspricht dem ARD-Regressor. Daher werden wir dieses Modell in das ONNX-Format exportieren.
| Modell | Fehler |
|---|---|
| Linear Regression | 0.0001957533746919363 |
| Ridge | 0.000550907245398377 |
| Lasso | 0.014059369238373157 |
| ARDRegression | 0.00018190369036281064 |
| KNeighborsRegressor | 0.005387854064255319 |
| RadiusNeighborsRegressor | nan |
| LinearSVR | 0.0002872914823846638 |
| RandomForestRegressor | 0.0015833296216492855 |
| BaggingRegressor | 0.0016147744161974461 |
| AdaBoostRegressor | 0.0018082307134142561 |
Exportieren unseres Modells in das ONNX-Format
Das ARD-Modell wurde mithilfe der ONNX-Bibliothek exportiert. Die Bibliothek Open Neural Network Exchange (ONNX) ermöglicht die Bereitstellung von Modellen für maschinelles Lernen in einem sprachunabhängigen Format, was es Entwicklern erleichtert, schnell Prototypen für maschinelle Lernmodelle beliebiger Komplexität zu erstellen und einzusetzen.
import onnx from skl2onnx.common.data_types import FloatTensorType from skl2onnx import convert_sklearnDie Form der Modelleingabe wurde als 1 × 8 Floats definiert.
initial_types = [('float_input',FloatTensorType([1,8]))]
Unser ARD-Modell wurde dann auf die gesamte Trainingsmenge angewendet.
model = ARDRegression()
model.fit(train.iloc[:,1:-1],train.iloc[:,-1]) Anschließend haben wir das Modell in den ONNX-Prototyp umgewandelt.
onnx_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12) Und schließlich haben wir die ONNX-Datei auf der Festplatte gespeichert.
onnx.save(onnx_proto,"EURUSD D1 ARDRegression.onnx") Umsetzung der von uns vorgeschlagenen Verbesserungen
Das ONNX-Modell, das wir aus Python exportiert haben, wurde dann als Ressource in den Expert Advisor geladen.
//+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD D1 ARDRegression.onnx" as const uchar onnx_buffer[];
Außerdem wurden neue Systemdefinitionen eingeführt, um die Eingabe- und Ausgabedimensionen des Modells zu definieren.
#define ONNX_INPUTS 8 #define ONNX_OUTPUTS 1
Doch das ist nicht alles, was wir machen müssen; wir müssen auch neue globale Variablen für das Modell und seine Vorhersagen berücksichtigen.
long onnx_model; vectorf onnx_outputs;
Während der Initialisierung wurden die Eingabe- und Ausgabeformen des Modells validiert. Wenn die Validierung fehlschlug, wurde die Initialisierung abgebrochen und eine Rückmeldung gegeben. Andernfalls wurden die Modellausgaben mit Null initialisiert.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Set up the ONNX model onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DATA_TYPE_FLOAT); //--- Define the model I/O shapes ulong onnx_input_shape[] = {1,ONNX_INPUTS}; ulong onnx_output_shape[] = {1,ONNX_OUTPUTS}; //--- Validate the ONNX model else if(!OnnxSetInputShape(onnx_model,0,onnx_input_shape)) { Comment("Failed to define the ONNX model input shape: ",GetLastError()); return(INIT_FAILED); } else if(!OnnxSetOutputShape(onnx_model,0,onnx_output_shape)) { Comment("Failed to define the ONNX model output shape: ",GetLastError()); return(INIT_FAILED); } else if(onnx_model == INVALID_HANDLE) { Comment("Error occured setting up the ONNX model: ",GetLastError()); return(INIT_FAILED); } //--- Final settings else { //--- Initialize the ONNX model outputs with a zero onnx_outputs = vectorf::Zeros(ONNX_OUTPUTS); //--- Good news: no errors return(INIT_SUCCEEDED); } }
Wenn die Anwendung beendet wird, werden das ONNX-Modell und die zugewiesenen Ressourcen freigegeben und die Terminal-Kommentare werden gelöscht.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { OnnxRelease(onnx_model); Comment(""); }
Wenn neue Preisdaten eintreffen, werden die Modelleingaben als Fließkommazahlen gespeichert, und die Prognosen werden mithilfe der ONNX-Laufzeitumgebung erstellt. Der Handel kann auf der Grundlage der vom Modell prognostizierten Richtung eröffnet werden.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Check if a new candle has formed if(time_stamp != time_current) { //--- Prepare our ONNX model inputs vectorf onnx_inputs = {(float)iOpen(SYMBOL,TF_MAIN,SHIFT), (float)iHigh(SYMBOL,TF_MAIN,SHIFT), (float)iLow(SYMBOL,TF_MAIN,SHIFT), (float)iClose(SYMBOL,TF_MAIN,SHIFT), (float)bb_upper[0], (float)bb_mid[0], (float)bb_lower[0], (float)rsi[0]}; //--- Obtain a forecast from our ONNX model OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_outputs); Comment("EURUSD Model Forecast: ",onnx_outputs[0]); //--- If we have no open positions if(PositionsTotal() == 0) { //--- Check for our trading signal if(((close > bb_upper[0]) && (rsi[0] > RSI_LEVEL_MAX)) || (onnx_outputs[0] < close)) { Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),""); } else if(((close < bb_lower[0]) && (rsi[0] < RSI_LEVEL_MIN)) || (onnx_outputs[0] > close)) { Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),""); } } } } //+------------------------------------------------------------------+
Wir dürfen nicht vergessen, alle Systemkonstanten am Ende der Anwendung zu deklarieren.
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef ONNX_INPUTS #undef ONNX_OUTPUTS //+------------------------------------------------------------------+
Leider zeigte die daraus resultierende Kapitalkurve eine weitere Verschlechterung der Leistung.

Abbildung 11: Die durch die dritte Iteration unseres Expert Advisors erhaltene Kapitalkurve fordert uns auf, unsere Methodik noch sorgfältiger anzuwenden
Der Nettogewinn erreichte mit 6 $ einen historischer Tiefstwert, was auf ein übermäßiges Rauschen trotz erhöhter Handelsfrequenz hindeutet.

Abbildung 12: Unsere detaillierten Statistiken zeigen deutlich, dass die von uns vorgeschlagenen Änderungen zu keinerlei Verbesserungen geführt haben
Tiefer graben für bessere Leistung
Aufgrund der schlechten Leistung, die wir beobachteten, mussten wir unseren Modellierungsansatz von Grund auf überarbeiten. In unserer früheren Diskussion über die statistische Modellierung von Finanzmärkten haben wir empirisch festgestellt, dass wir eine höhere Genauigkeit erzielen können, wenn wir bestimmte technische Indikatoren modellieren, anstatt die rohen Preisniveaus direkt zu modellieren; ein Link zu diesem Artikel wurde hier zur Bequemlichkeit des Lesers bereitgestellt. Infolgedessen haben wir den Schwerpunkt von der direkten Preisvorhersage auf die Vorhersage der technischen Indikatoren verlagert, die in unsere Handelsstrategie einbezogen sind.#Label the data train['Target 1'] = train['BB Upper'].shift(-HORIZON) train['Target 2'] = train['BB Mid'].shift(-HORIZON) train['Target 3'] = train['BB Lower'].shift(-HORIZON) train['Target 4'] = train['RSI'].shift(-HORIZON) #Drop missing labels train = train.iloc[:-HORIZON,:]
Das neue Modell lieferte vier Outputs anstelle von einem.
final_types = [('float_output',FloatTensorType([1,4]))]Außerdem haben wir uns die Zeit genommen, das Rauschen in unserem System zu reduzieren. Zu diesem Zweck haben wir eine Z-Score-Normalisierung vorgenommen, um die Daten entsprechend zu skalieren.
Z1 = train.iloc[:,1:-4].mean() Z2 = train.iloc[:,1:-4].std() train.iloc[:,1:-4] = ((train.iloc[:,1:-4] - Z1) / Z2)
Da sich das ARD-Modell als unzureichend geeignet erwies, wählten wir den „Random Forest Regressor“, um nichtlineare Beziehungen zu erfassen.
model = RandomForestRegressor() model.fit(train.iloc[:,1:-4],train.iloc[:,-4:])
Das Modell wurde in ONNX konvertiert und mit beschreibenden Namenskonventionen gespeichert.
onnx_proto = convert_sklearn(model,initial_types=initial_types,final_types=final_types,target_opset=12)
onnx.save(onnx_proto,"EURUSD D1 RandomForestRegressor.onnx") Umsetzung von Verbesserungen
Von dort aus haben wir das neue ONNX-Modell wieder in die Anwendung geladen.//+------------------------------------------------------------------+ //| Version 4.mq5 | //| Copyright 2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD D1 RandomForestRegressor.onnx" as const uchar onnx_buffer[];
Die Werte für Mittelwert und Standardabweichung, die wir in unserer Python-Analyse historischer Marktdaten erhalten haben, wurden sorgfältig als Fließkommazahlen gespeichert, um ein Abschneiden zu verhindern.
//+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ //--- Column Mean Values const float Z1[] = { (float)1.18132371, (float)1.18577335, (float)1.17706596, (float)1.1812953 , (float)1.20514458, (float)1.18303579, (float)1.16092701, (float)48.60276562}; //--- Column Standard Deviation const float Z2[] = { (float)0.09684736, (float)0.09665192, (float)0.09686825, (float)0.09684589, (float)0.09614994, (float)0.09556366, (float)0.09612185, (float)11.10783131};
Alle unsere Eingaben wurden vor der Inferenz skaliert, um sicherzustellen, dass unser Modell sich nicht zu stark an das durch Größenunterschiede verursachte Rauschen anpasst. Und es wurden neue Handelsregeln eingeführt, die die prognostizierte RSI-Richtung und die prognostizierte Steigung des mittleren Bollinger-Bandes mit dem ursprünglichen Handelssignal, mit dem wir begonnen haben, kombinieren.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Check if a new candle has formed if(time_stamp != time_current) { //--- Update the time time_stamp = time_current; //--- Prepare our ONNX model inputs vectorf onnx_inputs = {(float)iOpen(SYMBOL,TF_MAIN,SHIFT), (float)iHigh(SYMBOL,TF_MAIN,SHIFT), (float)iLow(SYMBOL,TF_MAIN,SHIFT), (float)iClose(SYMBOL,TF_MAIN,SHIFT), (float)bb_upper[0], (float)bb_mid[0], (float)bb_lower[0], (float)rsi[0]}; //--- Scale the model inputs appropriately for(int i = 0; i < ONNX_INPUTS;i++) { onnx_inputs[i] = ((onnx_inputs[i]-Z1[i])/Z2[i]); } //--- Obtain a forecast from our ONNX model OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_outputs); Comment("EURUSD Model Forecast: ",onnx_outputs); //--- If we have no open positions if(PositionsTotal() == 0) { //--- Check for our trading signal if(((close > bb_upper[0]) && (rsi[0] > RSI_LEVEL_MAX)) || ((onnx_outputs[3] < rsi[0]) && (onnx_outputs[1] < bb_mid[0]))) { Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),""); } else if(((close < bb_lower[0]) && (rsi[0] < RSI_LEVEL_MIN)) || ((onnx_outputs[3] > rsi[0]) && (onnx_outputs[1] > bb_mid[0]))) { Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),""); } } } } //+------------------------------------------------------------------+
Die sich daraus ergebende Kapitalkurve zeigte eine deutliche Verbesserung und erreichte neue Höchststände in der Nähe von 300 $ bei gleichzeitiger Beibehaltung höherer Mindestaktienwerte.

Abbildung 13: Die Kapitalkurve, die durch die vierte Iteration der Handelsanwendung erzeugt wird, liefert schließlich die gewünschten Ergebnisse
Die Gesamtzahl der Trades erhöhte sich auf 78, und der Nettogewinn stieg auf 95 Dollar. Obwohl die Gewinnquote von 64 % auf 55 % sank, wurden beide Ziele – höhere Handelsfrequenz und höhere Rentabilität – erreicht.

Abbildung 14: Unsere detaillierten Statistiken haben sich im Vergleich zu den von uns aufgestellten Benchmarks erheblich verbessert
Die Analyse unserer Verbesserungen
In den meisten unserer Diskussionen verwenden wir statistische Bibliotheken, um Rückschlüsse auf Marktdaten zu ziehen. Moderne statistische Bibliotheken ermöglichen jedoch weit mehr als nur Schlussfolgerungen; sie erlauben uns auch, Daten auf eine Weise zu analysieren, die die zugrunde liegende Struktur des Marktes aufdeckt und erklärt.
Wir beginnen daher damit, zu prüfen, ob die Beziehung, die wir zwischen dem RSI und der künftigen Kursentwicklung vermuten, tatsächlich besteht. Um dies zu untersuchen, haben wir ein Streudiagramm zwischen dem Schlusskurs und dem RSI-Wert erstellt. Jeder Datenpunkt wurde blau eingefärbt, wenn die Preisentwicklung bärisch war, und orange, wenn die Preisentwicklung bullisch war. Eine rote Linie wurde eingezeichnet, um den Bereich zu verallgemeinern, in dem RSI-Werte als überkauft gelten und somit Verkaufsaufträge auslösen, während eine grüne Linie den überverkauften Bereich abgrenzt, in dem Long-Positionen eingegangen werden würden.
Wir gehen davon aus, dass es eine natürliche Grenze gibt, die eine klare Trennung zwischen Aufwärts- und Abwärtsmustern gewährleistet. Stattdessen beobachten wir eine Mischung aus Kursbewegungen oberhalb und unterhalb der erwarteten Regionen. Dies ist ein deutlicher Hinweis auf das Vorhandensein von unvermindertem Rauschen in den vom RSI generierten Handelssignalen.
sns.scatterplot(data=train,x='Close',y='RSI',hue='Bin Target Threshold Price') plt.axhline(data['RSI'].mean()+data['RSI'].std(),color='red') plt.axhline(data['RSI'].mean()-data['RSI'].std(),color='green') plt.grid() plt.ylabel('RSI Reading') plt.xlabel('EURUSD Close Price') plt.title('Relationship Between RSI & EURUSD Return')

Abbildung 15: Der RSI-Indikator ist nicht in der Lage, auf natürliche Weise zwischen steigenden und fallenden Kursen zu unterscheiden.
Wir haben dieselbe Analyse auf Handelssignale ausgedehnt, die von den Bollinger-Bändern generiert werden. Wir begannen mit der Analyse der Differenz zwischen dem oberen Band und dem Schlusskurs, wobei wir diese gegen die historische Zeit auftrugen. Punkte oberhalb der roten Linie zeigen an, dass der Kurs das obere Band überschritten hat. In dieser Region würden wir nur bärische Muster erwarten. Stattdessen beobachten wir erneut eine Mischung aus Aufwärts- und Abwärtsbewegungen, was darauf hindeutet, dass der Kurs entweder zum Mittelwert zurückkehren oder seinen Aufwärtstrend fortsetzen könnte. Dennoch ist das Verhältnis von bärischen zu bullischen Mustern beim Bollinger-Band deutlich stärker als beim RSI.
sns.scatterplot(data=train,y=train['Close']-train['BB Upper'],x=np.arange(train.shape[0]),hue='Bin Target Threshold Price') plt.grid() plt.axhline(0,color='red') plt.ylabel('Difference Between Price & BB Upper') plt.xlabel('Historical Time') plt.title('Relationship Between EURUSD Close & BB Upper')

Abbildung 16: Das obere Bollinger-Band scheint für uns eine bessere Entscheidungsgrenze zu bilden als der RSI
Das gleiche Verhalten ist für das untere Bollinger-Band zu beobachten. Wenn der Kurs unter das untere Band bricht, werden wir voraussichtlich Long-Positionen eingehen. Es ist zwar ermutigend, dass die meisten Stichproben unterhalb der grünen Linie bullisch sind, aber das anhaltende Vorhandensein gemischter Ergebnisse bestätigt, dass keiner der betrachteten technischen Indikatoren perfekt zwischen bullischen und bärischen Kursen unterscheiden kann. Dennoch scheint das Bollinger-Band eine verlässlichere Entscheidungsgrenze zu sein als der RSI.
sns.scatterplot(data=train,y=train['Close']-train['BB Lower'],x=np.arange(train.shape[0]),hue='Bin Target Threshold Price') plt.axhline(0,color='green') plt.grid() plt.ylabel('Difference Between Price & BB Lower') plt.xlabel('Historical Time') plt.title('Relationship Between EURUSD Close & BB Lower')

Abbildung 17: Sowohl das obere als auch das untere Bollinger-Band scheinen bessere Entscheidungsgrenzen zu setzen als der RSI
Suche nach hochdimensionalen Handelsstrategien mit unüberwachtem maschinellem Lernen
An diesem Punkt können wir beginnen, die Möglichkeit in Betracht zu ziehen, dass das Marktverhalten durch mehr Dimensionen der Variation bestimmt wird, als wir uns ohne Weiteres vorstellen können. Uns Menschen fällt es schwer, über drei Dimensionen hinauszudenken, die üblicherweise entlang der X-, Y- und Z-Achse visualisiert werden. Die in dieser Studie verwendeten Marktdaten umfassen jedoch acht Dimensionen: open, high, low, close, das obere, mittlere und untere Bollinger-Band und den RSI. Dies wirft die Möglichkeit auf, dass es eine hochdimensionale Handelsstrategie gibt, die nicht direkt beobachtet oder intuitiv verstanden werden kann.Unüberwachte Algorithmen des maschinellen Lernens sind gut geeignet, um solche hochdimensionalen Strukturen zu erkennen und in einer für den Menschen interpretierbaren Form darzustellen. Es gibt zwar viele unüberwachte Techniken, aber unsere Diskussion konzentriert sich auf eine leistungsstarke nichtlineare Projektionsmethode, die als Isometric Mapping (Isomap) bekannt ist. Diese Techniken werden gemeinhin als „Manifold Learning“ oder Algorithmen mit einer Reduzierung der Dimensionen bezeichnet. Ihr Ziel ist es, ähnliche Beobachtungen eng zu gruppieren und unähnliche Beobachtungen so deutlich wie möglich zu trennen.
Ein bekanntes Verfahren zur Dimensionenreduktion ist die Hauptkomponentenanalyse (PCA), die in der Literatur bereits ausführlich diskutiert wurde. Die PCA ist jedoch eine lineare Technik, die komplexe nichtlineare Beziehungen in Marktdaten möglicherweise nicht erfassen kann. Im Gegensatz dazu kann Isomap – implementiert mit der sklearn.manifold-Bibliothek – nichtlineare, hochdimensionale Beziehungen aufdecken, die eine gültige Handelsstrategie jenseits der menschlichen Intuition darstellen könnten.
Zu Beginn laden wir die Isomap-Bibliothek.
from sklearn.manifold import Isomap
Als Nächstes instanziieren wir den Encoder und wenden die fit_transform-Methode an, um unseren ursprünglichen achtdimensionalen Datensatz auf eine zweidimensionale Darstellung zu reduzieren.
enc = Isomap() manifold = pd.DataFrame(enc.fit_transform(train.iloc[:,1:9])) manifold

Abbildung 18: Wir haben eine isometrische Abbildung verwendet, um alle unsere Marktdaten auf nur zwei Spalten zu projizieren
Diese gelernten Verteiler werden dann an die ursprüngliche Trainingsmenge angehängt.
train['Iso 1'] = manifold.iloc[:,0] train['Iso 2'] = manifold.iloc[:,1]
Als Nächstes analysieren wir die Korrelation zwischen den erlernten vielfältigen Komponenten und den ursprünglichen Marktvariablen. Die erste Komponente weist eine positive Korrelation zwischen allen ursprünglichen Preisvariablen auf, was bedeutet, dass ein Anstieg dieser Komponente mit steigenden Eröffnungs-, Höchst-, Tiefst- und Schlusskursen einhergeht. Im Gegensatz dazu zeigt die zweite Komponente eine negative Korrelation, was darauf hindeutet, dass ein Rückgang dieser Komponente mit einem steigenden Preisniveau einhergeht.
sns.heatmap(train.iloc[:,1:].corr())
plt.title('EURUSD Training Data Correlation Heatmap') 
Abbildung 19: Die Korrelationsmatrix unseres neuen Trainingsdatensatzes
Mithilfe eines Streudiagramms werden die von acht auf zwei Dimensionen reduzierten Marktdaten visualisiert. Die sich daraus ergebende Struktur ist zwar nicht perfekt getrennt, aber es zeichnen sich bereits sinnvolle Muster ab. Bestimmte Regionen zeigen ein klar definiertes bärisches Verhalten, während andere bullische Merkmale aufweisen. Es bleibt jedoch eine Überlappung, die auf ein Restrauschen hinweist, das die nachgelagerte Leistung beeinträchtigen kann.
sns.scatterplot(data=train,x='Iso 1',y='Iso 2',hue='Bin Target Threshold Price')
plt.grid()
plt.title('Visualizing Our High Dimensional Data in 2 Dimensions') 
Abbildung 20: Algorithmen zur Dimensionalitätsreduzierung ermöglichen es uns, hochdimensionale Daten zu visualisieren, die sonst nicht vollständig dargestellt werden könnten.
Traditionell werden gelernte, vielfältige Merkmale verwendet, um eine ursprüngliche Zielvariable vorherzusagen. In unserer Neukonzeption dieses Ansatzes behandeln wir stattdessen die erlernten vielfältigen Komponenten als Ersatzziele. Der Grund dafür ist, dass diese Komponenten möglicherweise besser vorhersehbar sind als der Preis selbst. Um diese Hypothese zu testen, bewerten wir die Vorhersagegenauigkeit für verschiedene Ziele: den Preis, das mittlere Bollinger-Band, den RSI und die beiden erlernten vielfältigen Komponenten.
scores = [] from sklearn.ensemble import RandomForestClassifier scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target Threshold Price'],cv=tscv,scoring='accuracy')))) scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target Threshold BB Mid'],cv=tscv,scoring='accuracy')))) scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target Threshold RSI'],cv=tscv,scoring='accuracy')))) scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target 1'],cv=tscv,scoring='accuracy')))) scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target 2'],cv=tscv,scoring='accuracy'))))
Abbildung 21 zeigt das resultierende Balkendiagramm. Die durch die direkte Preisvorhersage erreichte Genauigkeit ist als rote gepunktete Linie dargestellt. Erwartungsgemäß ist die Vorhersage des mittleren Bollinger-Bandes wesentlich genauer, da es einen gleitenden Durchschnitt darstellt und von Natur aus glatter ist als der Preis. Bemerkenswert ist, dass das zweitbeste Ziel nicht der RSI ist, sondern die erste Isomap-Komponente. Dieses Ziel befindet sich in einem hochdimensionalen Raum, der für das menschliche Denken unzugänglich ist, was die Leistungsfähigkeit von Techniken zur Dimensionalitätsreduzierung verdeutlicht. Wie wir jedoch bereits in früheren Artikeln erörtert haben, führen Verbesserungen bei den statistischen Kennzahlen nicht unbedingt zu einer verbesserten Handelsleistung; einen Link zu dieser Diskussion finden Sie hier.
sns.barplot(scores) plt.xticks([0,1,2,3,4],['Price','BB Mid','RSI','Iso 1','Iso 2']) plt.axhline(scores[0],color='red',linestyle=':') plt.ylabel('Cross Validation Accuracy 100%') plt.xlabel('Candidate Target') plt.title('Our Accuracy Predicting Different Targets Related to The EURUSD')

Abbildung 21: Das zweitbeste Ziel, das wir hätten modellieren können, war in Dimensionen eingebettet, die für jede Form von menschlichem Bewusstsein zu hoch sind
Anschließend bewerten wir den materiellen Nutzen der Vorhersage dieser erlernten Vielfältigkeit anstelle des Preises. Nach einem vertrauten Arbeitsablauf passen wir den „Random Forest Regressor“ an, um die erste Isomap-Komponente zu prognostizieren, und exportieren das trainierte Modell in das ONNX-Format. Der Name des Modells ist bewusst gewählt, um zu verdeutlichen, dass es die vom Isomap-Algorithmus erzeugte Mannigfaltigkeit vorhersagt.
initial_types = [('float_input',FloatTensorType([1,8]))] final_types = [('float_output',FloatTensorType([1,1]))] model = RandomForestRegressor() model.fit(train.iloc[:,1:9],train['Bin Target 1']) onnx_proto = convert_sklearn(model,initial_types=initial_types,final_types=final_types,target_opset=12) onnx.save(onnx_proto,"EURUSD D1 Iso 1 RandomForestRegressor.onnx")
Letzte Verbesserungsversuche
Nachdem diese Änderungen vorgenommen wurden, testen wir die überarbeitete Anwendung. Wir beginnen mit dem Laden des neu exportierten Random-Forest Modells.//+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD D1 Iso 1 RandomForestRegressor.onnx" as const uchar onnx_buffer[];
Dann müssen wir die Inputs und Outputs des Modells festlegen. Beachten Sie, dass dieses Modell im Gegensatz zur vorherigen Iteration, die 4 Ausgänge hatte, nur einen einzigen Ausgang hat.
#define ONNX_INPUTS 8 #define ONNX_OUTPUTS 1
Die Handelsregeln werden entsprechend angepasst: Wenn die Prognose über 0,5 liegt, gehen wir Long-Positionen ein; wenn sie unter 0,5 fällt, gehen wir Short-Positionen ein.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Check if a new candle has formed if(time_stamp != time_current) { //--- Prepare our ONNX model inputs vectorf onnx_inputs = {(float)iOpen(SYMBOL,TF_MAIN,SHIFT), (float)iHigh(SYMBOL,TF_MAIN,SHIFT), (float)iLow(SYMBOL,TF_MAIN,SHIFT), (float)iClose(SYMBOL,TF_MAIN,SHIFT), (float)bb_upper[0], (float)bb_mid[0], (float)bb_lower[0], (float)rsi[0]}; //--- Scale the model inputs appropriately for(int i = 0; i < ONNX_INPUTS;i++) { onnx_inputs[i] = ((onnx_inputs[i]-Z1[i])/Z2[i]); } //--- Obtain a forecast from our ONNX model OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_outputs); Comment("EURUSD Model Forecast: ",onnx_outputs); //--- Update current price levels double close = iClose(SYMBOL,TF_MAIN,SHIFT); //--- If we have no open positions if(PositionsTotal() == 0) { //--- Check for our trading signal if(onnx_outputs[0] < 0.5) { Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),""); } else if(onnx_outputs[0] > 0.5) { Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),""); } } } } //+------------------------------------------------------------------+
Die daraus resultierende Kapitalkurve bleibt hinter den Erwartungen zurück. Während sporadische Ausschläge in der Rentabilität zu beobachten sind, bleibt der Gesamttrend instabil und weitgehend stationär um den Ausgangssaldo. Trotzdem deutet das Vorhandensein von nicht realisierten Aktienspitzen darauf hin, dass in der hochdimensionalen Struktur ein latentes Signal eingebettet bleibt.

Abbildung 22: Die Kapitalkurve, die sich aus der fünften Iteration unserer Handelsanwendung ergibt, offenbart Schwächen in unserer hochdimensionalen Handelsstrategie
Die Untersuchung der detaillierten Leistungsstatistiken schließlich zeigt gemischte Ergebnisse. Die Gesamtzahl der Abschlüsse hat sich von ursprünglich 14 auf 83 deutlich erhöht. Die Nettorentabilität verschlechterte sich jedoch, und der Anteil der rentablen Trades ging auf 48 % zurück. Erfreulicherweise waren die Long-Trades überwiegend profitabel. Bei weiterer Iteration und verfeinerter Analyse deuten diese Ergebnisse darauf hin, dass ein bedeutendes Potenzial zur Aufdeckung hochdimensionaler Handelsstrategien besteht, die andernfalls unentdeckt bleiben würden.

Abbildung 23: Eine detaillierte Analyse der endgültigen Version unserer Handelsanwendung zeigt, dass ein inakzeptables Maß an Rauschen vorhanden ist
Schlussfolgerung
Abschließend zeigt dieser Artikel, wie klassische Handelsstrategien mit modernen statistischen Algorithmen verjüngt werden können, um neue Leistungsniveaus zu erreichen. Der algorithmische Handel ist kein formelhafter Prozess; unser Erfolg hängt von Ausdauer, logischem Denken, Kreativität und energischer Iteration ab. Ferner erfährt der Leser, dass angesichts der ständig wachsenden Verfügbarkeit umfangreicher Datensätze des MetaTrader 5-Terminals einige Handelsstrategien dem menschlichen Bewusstsein verborgen bleiben können, da sie in Dimensionen eingebettet sind, die für eine direkte Beobachtung zu hoch sind.
Indem er die Anwendung unüberwachter statistischer Algorithmen neu vorstellt, präsentiert dieser Artikel eine numerisch fundierte Methodik zur Identifizierung hochdimensionaler Handelsstrategien innerhalb Ihrer Kopie des MetaTrader 5 Terminals. Moderne Computer sind in der Lage, komplexe hochdimensionale Strukturen in historischen Finanzdaten zu erkennen und dadurch Handelsstrategien zu entdecken und zu erlernen, die die menschliche Vorstellungskraft übersteigen. Dies ist eine wirklich spannende Information, die weitere Untersuchungen rechtfertigt. Letztlich zeigt dieser Artikel, dass der größte Nutzen des algorithmischen Handels in seiner Fähigkeit liegen könnte, zu enthüllen, was wahrscheinlich wahr ist – und was wahrscheinlich nicht wahr ist – an den Finanzmärkten, die wir bereits zu verstehen glaubten.
| Dateiname | Beschreibung der Datei |
|---|---|
| Version_1.mq5 | Der erste regelbasierte Versuch, die Bollinger-Bänder und den Relative-Stärke-Indikator zu kombinieren, wurde von uns unternommen. Diese Anwendung erzeugte Handelssignale mit hoher Wahrscheinlichkeit, aber die Signale wurden mit geringer Häufigkeit erzielt. |
| Version_2.mq5 | Bei der zweiten Iteration der ursprünglichen Strategie wurden mehr handschriftliche Regeln verwendet, die jedoch zu unerwünschten Leistungswerten führten und das Signal unserer Handelsstrategie drastisch reduzierten. |
| Version_3.mq5 | In der dritten Iteration unseres Expert Advisors wurden keine statistischen Modelle verwendet, um geeignete Handelssignale zu erkennen. |
| Version_4.mq5 | Die profitabelste Version der Handelsanwendung, die wir in unserer Übung erstellen konnten. |
| Version_5.mq5 | In der letzten Iteration unserer Anwendung wurde versucht, hochdimensionale Handelsstrategien aus den historischen Daten zu lernen, was jedoch nicht erfolgreich war. |
| Fetch_Data_Bollinger_Bands_RSI_Strategy.mq5 | Das Jupyter-Notebook, das wir für die Analyse unserer Marktdaten verwendet haben. |
| Bollinger_Band_RSI_Strategy.ipynb | Das MQL5-Skript, das wir verwendet haben, um historische Marktdaten in CSV für die weitere Analyse in Python zu schreiben. |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/20933
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.
MQL5-Werkzeuge für den Handel (Teil 12): Erweiterung des Korrelationsmatrix-Dashboards um interaktive Funktionen
Entwicklung eines Toolkits zur Preisaktionsanalyse (Teil 55): Entwurf eines Overlays der CPI-Minikerzen für Intra-Bar-Druck
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Einführung in MQL5 (Teil 36): Beherrschen der API und der Funktion WebRequest in MQL5 (X)
- 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.