English
preview
Klassische Strategien neu interpretieren (Teil 21): Entdeckung einer Ensemble-Strategie aus Bollinger-Bändern und RSI

Klassische Strategien neu interpretieren (Teil 21): Entdeckung einer Ensemble-Strategie aus Bollinger-Bändern und RSI

MetaTrader 5Beispiele |
22 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

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_sklearn
Die 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

MQL5-Werkzeuge für den Handel (Teil 12): Erweiterung des Korrelationsmatrix-Dashboards um interaktive Funktionen MQL5-Werkzeuge für den Handel (Teil 12): Erweiterung des Korrelationsmatrix-Dashboards um interaktive Funktionen
In diesem Artikel erweitern wir das Dashboard für die Korrelationsmatrix in MQL5 um interaktive Funktionen wie das Ziehen des Panels, Minimieren/Maximieren, Hover-Effekte auf Schaltflächen und Zeitrahmen sowie die Behandlung von Mausereignissen für ein verbessertes Nutzungserlebnis. Wir ergänzen eine Sortierung der Symbole nach durchschnittlicher Korrelationsstärke in auf- und absteigender Reihenfolge, schalten zwischen der Matrixdarstellung der Korrelationen und den p-Werten um und integrieren einen Wechsel zwischen hellen und dunklen Farbschemen mit dynamischen Farbaktualisierungen.
Entwicklung eines Toolkits zur Preisaktionsanalyse (Teil 55): Entwurf eines Overlays der CPI-Minikerzen für Intra-Bar-Druck Entwicklung eines Toolkits zur Preisaktionsanalyse (Teil 55): Entwurf eines Overlays der CPI-Minikerzen für Intra-Bar-Druck
Dieser Artikel stellt das Design und die MetaTrader 5-Implementierung des Candle Pressure Index (CPI) vor – ein CLV-basiertes Overlay, das den Kauf- und Verkaufsdruck innerhalb einer Kerze direkt auf dem Preischart anzeigt. Die Diskussion konzentriert sich auf die Kerzenstruktur, die Druckklassifizierung, die Visualisierungsmechanismen und ein übergangsloses, übergangsorientiertes Warnsystem, das für ein konsistentes Verhalten über Zeitrahmen und Instrumente hinweg konzipiert ist.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Einführung in MQL5 (Teil 36): Beherrschen der API und der Funktion WebRequest in MQL5 (X) Einführung in MQL5 (Teil 36): Beherrschen der API und der Funktion WebRequest in MQL5 (X)
Dieser Artikel stellt die grundlegenden Konzepte hinter HMAC-SHA256 und API-Signaturen in MQL5 vor und erklärt, wie Nachrichten und geheime Schlüssel kombiniert werden, um Anfragen sicher zu authentifizieren. Sie bildet die Grundlage für das Signieren von API-Aufrufen, ohne sensible Daten preiszugeben.