
Erstellen von selbstoptimierenden Expert Advisor in MQL5 (Teil 6): Selbstanpassende Handelsregeln (II)
In unserer letzten Diskussion über sich selbst anpassende Handelsregeln, die hier verlinkt ist, haben wir uns mit den Problemen befasst, mit denen ein algorithmischer Händler konfrontiert ist, wenn er versucht, den RSI-Indikator nach bewährten Verfahren zu verfolgen.
Wir haben festgestellt, dass der Indikator nicht immer standardisierte Ergebnisse liefert, die von verschiedenen Faktoren wie dem Zeitraum, dem Zeitrahmen und auch dem jeweiligen Markt abhängen.
Um dieses Problem zu lösen, postulierten wir, dass algorithmische Händler eher die tatsächliche Spanne des Indikators untersuchen könnten, sodass sie den Mittelpunkt des Indikators auf die Mitte seiner beobachteten Spanne und nicht auf seine gesamte mögliche Spanne einstellen können. Auf diese Weise erhalten wir einige Garantien für die Generierung von Handelssignalen, die wir mit den traditionellen Regeln des RSI nicht erhalten können. Wir erhielten zusätzliche Kontrolle über das neue Signal, indem wir eine durchschnittliche Abweichung vom Mittelpunkt aufzeichneten und nur Signale registrierten, die durch ein Vielfaches der durchschnittlichen Abweichung erzeugt wurden.
Wir werden nun über unseren ersten Versuch, eine praktische Lösung zu finden, hinausgehen. Gegenüber unserem letzten Versuch können wir einige Verbesserungen vornehmen. Die integrale Verbesserung, die wir anstreben, ist die Möglichkeit, den Wert der von uns gewählten RSI-Stufen zu schätzen.
In unserer letzten Diskussion haben wir einfach angenommen, dass Abweichungen, die deutlich größer sind als die durchschnittliche Abweichung, tendenziell profitabler sein könnten. Wir haben jedoch nicht versucht zu messen, ob dies der Fall ist. Wir haben nicht versucht, den Wert der neuen Stufen, die wir vorschlagen, zu quantifizieren und sie mit dem Wert der traditionellen Stufen 70 und 30 zu vergleichen.
Außerdem haben wir bei unserer letzten Diskussion den Fall betrachtet, dass der RSI-Zeitraum festgelegt wurde. Diese vereinfachende Annahme machte unseren Rahmen leichter verständlich. Heute wollen wir uns dem entgegengesetzten Ende des Problems zuwenden, wenn der Praktiker unsicher ist, welchen Zeitraum er verwenden soll.
Visualisierung des Problems
Für den Fall, dass neue Leser zu uns stoßen, haben wir in Abbildung 1 unten einen Screenshot des EURUSD-Tagescharts mit einem 2-Perioden-RSI beigefügt.
Abb. 1: Visualisierung unserer RSI die Qualität der Signale durch einen kurzen Zeitraum erzeugt.
Unterhalb von Abb. 1 haben wir einen RSI mit einer Periode von 70 auf denselben Abschnitt des Charts in Abb. 2 angewendet. Wir können beobachten, dass das RSI-Signal langsam zu einer flachen Linie wird, die sich um den RSI-Wert von 50 bewegt. Welche Kommentare könnten wir machen, wenn wir die beiden Bilder vergleichen? Erwähnenswert ist, dass der EURUSD-Kurs in dem in den beiden Abbildungen erfassten Zeitraum von 1,121 am 18. September 2024 auf einen Tiefststand von 1,051 am 2. Dezember 2024 fiel. Allerdings änderte der 2-Perioden-RSI seine Werte im gleichen Zeitraum zu häufig, und der 70-Perioden-RSI änderte seine Werte überhaupt nicht.
Bedeutet dies, dass Händler bei der Verwendung des RSI für immer auf einen engen Bereich von Periodenlängen beschränkt sein sollten? Was müssen wir tun, um Algorithmen zu entwickeln, die automatisch und ohne menschliches Zutun eine gute RSI-Periodenlänge auswählen? Wie können wir außerdem Algorithmen schreiben, die uns helfen, gute Handelsniveaus zu finden, unabhängig davon, mit welcher Periodenlänge wir beginnen?
Abb. 2: Visualisierung unseres RSI-Indikators, der mit einer langen Periodenlänge arbeitet.
Erste Schritte in MQL5
Es gibt viele Möglichkeiten, wie wir dieses Problem angehen können. Wir könnten Bibliotheken in Python verwenden, um RSI-Werte mit verschiedenen Perioden zu generieren und unsere Perioden- und RSI-Werte auf diese Weise zu optimieren. Dies birgt jedoch auch mögliche Nachteile. Die größte Einschränkung könnte sich aus den geringfügigen Unterschieden hinter den Berechnungen ergeben, die zur Ermittlung dieser technischen Indikatorwerte durchgeführt werden.Um dies zu vermeiden, werden wir unsere Lösung in MQL5 implementieren. Durch den Aufbau einer RSI-Klasse können wir schnell mehrere Instanzen von RSI-Werten in einer CSV-Datei aufzeichnen und diese Werte zur Durchführung unserer numerischen Analyse in Python verwenden, um eine optimale RSI-Periode und alternative Werte neben 70 und 30 zu schätzen.
Wir beginnen mit der Erstellung eines Skripts, mit dem wir zunächst die RSI-Werte manuell abrufen und die Veränderung der RSI-Werte berechnen können. Dann werden wir eine Klasse erstellen, die die benötigten Funktionen kapselt. Wir möchten ein Raster von RSI-Werten erstellen, deren Perioden in 5er-Schritten von 5 bis 70 ansteigen. Doch bevor wir dies erreichen können, müssen wir unsere Klasse implementieren und testen.
Die Erstellung der Klasse in einem Skript ermöglicht es uns, die Ausgabe der Klasse schnell mit der Standardausgabe zu vergleichen, die manuell aus dem Indikator gewonnen wird. Wenn wir die Klasse gut spezifiziert haben, sollte die von beiden Methoden erzeugte Ausgabe die gleiche sein. Damit erhalten wir eine nützliche Klasse für die Erstellung von RSI-Indikatoren mit unterschiedlichen Perioden, wobei wir die Veränderung jeder Instanz des RSI über jedes andere Symbol, mit dem wir handeln möchten, verfolgen können.
Angesichts der Verwendung dieser RSI-Klasse ist es sinnvoll, dafür zu sorgen, dass die Klasse über einen Mechanismus verfügt, der verhindert, dass wir versuchen, den Wert des Indikators zu lesen, bevor wir den Puffer entsprechend eingestellt haben. Wir werden zunächst diesen Teil unserer Klasse aufbauen. Wir müssen private Mitglieder unserer Klasse deklarieren. Diese privaten booleschen Flags verhindern, dass wir RSI-Werte lesen, bevor wir sie aus dem Indikatorpuffer kopieren.
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //--- The RSI class will manage our indicator settings and provide useful transformations we need class RSI { //--- Private members private: //--- Have the indicator values been copied to the buffer? bool indicator_values_initialized; bool indicator_differenced_values_initialized;
Ich habe auch eine Methode eingebaut, die Zeichenketten zurückgibt, um den Nutzer zu zeigen, was innerhalb des Objekts vor sich geht und wie Probleme gelöst werden können. Die Methode nimmt einen ganzzahligen Parameter entgegen, der angibt, an welcher Stelle des Codes der Fehler aufgetreten ist. Daher lassen sich die Lösungen in der Regel leicht als gedruckte Meldung im Terminal vorschlagen.
//--- Give the user feedback string user_feedback(int flag) { string message; //--- Check if the RSI indicator loaded correctly if(flag == 0) { //--- Check the indicator was loaded correctly if(IsValid()) message = "RSI Indicator Class Loaded Correcrtly \n"; return(message); //--- Something went wrong message = "Error loading RSI Indicator: [ERROR] " + (string) GetLastError(); return(message); } //--- User tried getting indicator values before setting them if(flag == 1) { message = "Please set the indicator values before trying to fetch them from memory"; return(message); } //--- We sueccessfully set our differenced indicator values if(flag == 2) { message = "Succesfully set differenced indicator values."; return(message); } //--- Failed to set our differenced indicator values if(flag == 3) { message = "Failed to set our differenced indicator values: [ERROR] " + (string) GetLastError(); return(message); } //--- The user is trying to retrieve an index beyond the buffer size and must update the buffer size first if(flag == 4) { message = "The user is attempting to use call an index beyond the buffer size, update the buffer size first"; return(message); } //--- No feedback else return(""); }
Nun werden wir die geschützten Mitglieder unserer Klasse definieren. Diese Mitglieder bilden die beweglichen Teile, die für die Initialisierung einer Instanz der Klasse iRSI() und die Interaktion mit dem Indikatorpuffer benötigt werden.
//--- Protected members protected: //--- The handler for our RSI int rsi_handler; //--- The Symbol our RSI should be applied on string rsi_symbol; //--- Our RSI period int rsi_period; //--- How far into the future we wish to forecast int forecast_horizon; //--- The buffer for our RSI indicator double rsi_reading[]; vector rsi_differenced_values; //--- The current size of the buffer the user last requested int rsi_buffer_size; int rsi_differenced_buffer_size; //--- The time frame our RSI should be applied on ENUM_TIMEFRAMES rsi_time_frame; //--- The price should the RSI be applied on ENUM_APPLIED_PRICE rsi_price;
Weiter geht es mit den öffentlichen Klassenmitgliedern. Die erste Funktion, die wir benötigen, informiert uns darüber, ob unser Indikator-Handle gültig ist. Wenn unser Indikator-Handle nicht korrekt eingerichtet ist, können wir den Nutzer sofort informieren.
//--- Now, we can define public members: public: //--- Check if our indicator handler is valid bool IsValid(void) { return((this.rsi_handler != INVALID_HANDLE)); }
Unser Standardkonstruktor erstellt ein iRSI-Objekt, das auf den EURUSD im Tageschart für einen Zeitraum von 5 Tagen eingestellt ist. Um sicher zu gehen, dass dies die vom Nutzer beabsichtigte Wahl ist, gibt unsere Klasse aus, mit welchem Markt und welchem Zeitraum sie arbeitet. Außerdem gibt der Standardkonstruktor dem Nutzer ausdrücklich aus, dass die aktuelle Instanz des RSI-Objekts vom Standardkonstruktor erstellt wurde.
//--- Our default constructor void RSI(void): indicator_values_initialized(false), rsi_symbol("EURUSD"), rsi_time_frame(PERIOD_D1), rsi_period(5), rsi_price(PRICE_CLOSE), rsi_handler(iRSI(rsi_symbol,rsi_time_frame,rsi_period,rsi_price)) { //--- Give the user feedback on initilization Print(user_feedback(0)); //--- Remind the user they called the default constructor Print("Default Constructor Called: ",__FUNCSIG__," ",&this); }
Andernfalls erwarten wir, dass der Nutzer den parametrischen Konstruktor für das RSI-Objekt aufruft und alle erforderlichen Parameter angibt.
//--- Parametric constructor void RSI(string user_symbol,ENUM_TIMEFRAMES user_time_frame,int user_period,ENUM_APPLIED_PRICE user_price) { indicator_values_initialized = false; rsi_symbol = user_symbol; rsi_time_frame = user_time_frame; rsi_period = user_period; rsi_price = user_price; rsi_handler = iRSI(rsi_symbol,rsi_time_frame,rsi_period,rsi_price); //--- Give the user feedback on initilization Print(user_feedback(0)); }
Wir brauchen auch einen Destruktor, um die nicht mehr benötigten Systemressourcen freizugeben, damit wir hinter uns aufräumen können.
//--- Destructor void ~RSI(void) { //--- Free up resources we don't need and reset our flags if(IndicatorRelease(rsi_handler)) { indicator_differenced_values_initialized = false; indicator_values_initialized = false; Print("RSI System logging off"); } }
Die Methoden, die für die Interaktion mit dem Indikatorpuffer benötigt werden, sind nun die Schlüsselkomponente unserer Klasse. Wir bitten den Nutzer anzugeben, wie viele Werte aus dem Puffer kopiert werden sollen und ob die Werte als Serie angeordnet werden sollen. Anschließend wird geprüft, ob die RSI-Werte vor Beendigung des Methodenaufrufs nicht an uns zurückgegeben werden.
//--- Copy readings for our RSI indicator bool SetIndicatorValues(int buffer_size,bool set_as_series) { rsi_buffer_size = buffer_size; CopyBuffer(this.rsi_handler,0,0,buffer_size,rsi_reading); if(set_as_series) ArraySetAsSeries(this.rsi_reading,true); indicator_values_initialized = true; //--- Did something go wrong? vector rsi_test; rsi_test.CopyIndicatorBuffer(rsi_handler,0,0,buffer_size); if(rsi_test.Sum() == 0) return(false); //--- Everything went fine. return(true); }
Eine einfache Funktion, um den aktuellen RSI-Wert zu ermitteln.
//--- Get the current RSI reading double GetCurrentReading(void) { double temp[]; CopyBuffer(this.rsi_handler,0,0,1,temp); return(temp[0]); }
Möglicherweise müssen wir auch Werte mit einem bestimmten Index abrufen, nicht nur den neuesten Wert. Diese Funktion, GetReadingAt(), stellt dieses Dienstprogramm für uns bereit. Die Funktion prüft zunächst, ob die Größe des Puffers, den wir vom Indikator kopiert haben, nicht überschritten wird. Bei korrekter Anwendung gibt die Funktion den Indikatorwert am angegebenen Index zurück. Andernfalls wird eine Fehlermeldung ausgegeben.
//--- Get a specific RSI reading double GetReadingAt(int index) { //--- Is the user trying to call indexes beyond the buffer? if(index > rsi_buffer_size) { Print(user_feedback(4)); return(-1e10); } //--- Get the reading at the specified index if((indicator_values_initialized) && (index < rsi_buffer_size)) return(rsi_reading[index]); //--- User is trying to get values that were not set prior else { Print(user_feedback(1)); return(-1e10); } }
Unser Interesse liegt auch in der Veränderung der RSI-Werte. Es reicht nicht aus, dass wir Zugang zum aktuellen RSI-Wert haben. Wir möchten auch Zugriff auf die Veränderung der RSI-Werte haben, die über eine beliebige Fenstergröße erfolgt, die wir angeben. Wie zuvor rufen wir einfach die CopyBuffer-Funktionen für den Nutzer hinter den Kulissen auf, um das Wachstum des RSI-Niveaus zu berechnen, aber die Klasse enthält auch eine zusätzliche Prüfung, um sicherzustellen, dass die Ausgabe der Berechnung nicht ein Vektor von 0 ist, bevor die gefundene Antwort an den Nutzer zurückgegeben wird.
//--- Let's set the conditions for our differenced data bool SetDifferencedIndicatorValues(int buffer_size,int differencing_period,bool set_as_series) { //--- Internal variables rsi_differenced_buffer_size = buffer_size; rsi_differenced_values = vector::Zeros(rsi_differenced_buffer_size); //--- Prepare to record the differences in our RSI readings double temp_buffer[]; int fetch = (rsi_differenced_buffer_size + (2 * differencing_period)); CopyBuffer(rsi_handler,0,0,fetch,temp_buffer); if(set_as_series) ArraySetAsSeries(temp_buffer,true); //--- Fill in our values iteratively for(int i = rsi_differenced_buffer_size;i > 1; i--) { rsi_differenced_values[i-1] = temp_buffer[i-1] - temp_buffer[i-1+differencing_period]; } //--- If the norm of a vector is 0, the vector is empty! if(rsi_differenced_values.Norm(VECTOR_NORM_P) != 0) { Print(user_feedback(2)); indicator_differenced_values_initialized = true; return(true); } indicator_differenced_values_initialized = false; Print(user_feedback(3)); return(false); }
Schließlich benötigen wir eine Methode, um den differenzierten RSI-Wert bei einem bestimmten Indexwert abzurufen. Auch hier stellt unsere Funktion sicher, dass der Nutzer nicht versucht, den Bereich des kopierten Puffers zu überschreiten. In einem solchen Fall sollte der Nutzer zunächst die Puffergröße aktualisieren und dann den gewünschten Indexwert entsprechend kopieren.
//--- Get a differenced value at a specific index double GetDifferencedReadingAt(int index) { //--- Make sure we're not trying to call values beyond our index if(index > rsi_differenced_buffer_size) { Print(user_feedback(4)); return(-1e10); } //--- Make sure our values have been set if(!indicator_differenced_values_initialized) { //--- The user is trying to use values before they were set in memory Print(user_feedback(1)); return(-1e10); } //--- Return the differenced value of our indicator at a specific index if((indicator_differenced_values_initialized) && (index < rsi_differenced_buffer_size)) return(rsi_differenced_values[index]); //--- Something went wrong. return(-1e10); } };
Der Rest unseres Tests ist einfach zu gestalten. Wir werden eine ähnliche Instanz des RSI-Indikators manuell steuern, die mit denselben Einstellungen initialisiert wurde. Wenn wir beide Messwerte in dieselbe Datei schreiben, sollten wir doppelte Informationen feststellen. Andernfalls hätten wir einen Fehler in unserer Implementierung der Klasse gemacht.
//--- How far we want to forecast #define HORIZON 10 //--- Our handlers for our indicators int rsi_5_handle; //--- Data structures to store the readings from our indicators double rsi_5_reading[]; //--- File name string file_name = Symbol() + " Testing RSI Class.csv"; //--- Amount of data requested input int size = 3000;
Für den Rest unseres Skripts müssen wir lediglich unsere RSI-Klasse initialisieren und mit denselben Parametern einrichten, die wir auch für eine duplizierte, aber manuell gesteuerte Version des RSI verwenden werden.
//+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { //--- Testing the RSI Class //--- Initialize the class RSI my_rsi(Symbol(),PERIOD_CURRENT,5,PRICE_CLOSE); my_rsi.SetIndicatorValues(size,true); my_rsi.SetDifferencedIndicatorValues(size,10,true); //---Setup our technical indicators rsi_5_handle = iRSI(Symbol(),PERIOD_CURRENT,5,PRICE_CLOSE); int fetch = size + (2 * HORIZON); //---Set the values as series CopyBuffer(rsi_5_handle,0,0,fetch,rsi_5_reading); ArraySetAsSeries(rsi_5_reading,true); //---Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i=size;i>=1;i--) { if(i == size) { FileWrite(file_handle,"Time","RSI 5","RSI 5 Class","RSI 5 Difference","RSI 5 Class Difference"); } else { FileWrite(file_handle, iTime(_Symbol,PERIOD_CURRENT,i), rsi_5_reading[i], my_rsi.GetReadingAt(i), rsi_5_reading[i] - rsi_5_reading[i + HORIZON], my_rsi.GetDifferencedReadingAt(i) ); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+ #undef HORIZON
Wenn unsere Klasse korrekt implementiert wurde, wird unser Testskript eine Datei mit dem Titel „EURUSD Testing RSI Class“ erzeugen, die doppelte RSI-Werte enthält. Wie Sie in Abbildung 3 sehen können, hat unsere RSI-Klasse den Test bestanden. Diese Klasse spart uns Zeit, da wir die gleichen Methoden nicht mehrfach in vielen Projekten implementieren müssen. Wir können einfach unsere RSI-Klasse importieren und die Methoden aufrufen, die wir brauchen.
Abb. 3: Unsere RSI-Klasse hat unseren Test bestanden, ihre Verwendung ist identisch mit der manuellen Arbeit mit der RSI-Indikatorenklasse.
Da wir nun von unserer Implementierung der Klasse überzeugt sind, sollten wir die Klasse in eine eigene Include-Datei schreiben, damit wir die Klasse mit unserem Expert Advisor und allen anderen Übungen, die wir in Zukunft durchführen und die dieselbe Funktionalität erfordern, gemeinsam nutzen können. So sieht unsere Klasse in ihrer jetzigen Form aus, wenn sie vollständig zusammengestellt ist.
//+------------------------------------------------------------------+ //| RSI.mqh | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| This class will provide us with usefull functionality | //+------------------------------------------------------------------+ class RSI { private: //--- Have the indicator values been copied to the buffer? bool indicator_values_initialized; bool indicator_differenced_values_initialized; //--- Give the user feedback string user_feedback(int flag); protected: //--- The handler for our RSI int rsi_handler; //--- The Symbol our RSI should be applied on string rsi_symbol; //--- Our RSI period int rsi_period; //--- How far into the future we wish to forecast int forecast_horizon; //--- The buffer for our RSI indicator double rsi_reading[]; vector rsi_differenced_values; //--- The current size of the buffer the user last requested int rsi_buffer_size; int rsi_differenced_buffer_size; //--- The time frame our RSI should be applied on ENUM_TIMEFRAMES rsi_time_frame; //--- The price should the RSI be applied on ENUM_APPLIED_PRICE rsi_price; public: RSI(); RSI(string user_symbol,ENUM_TIMEFRAMES user_time_frame,int user_period,ENUM_APPLIED_PRICE user_price); ~RSI(); bool SetIndicatorValues(int buffer_size,bool set_as_series); bool IsValid(void); double GetCurrentReading(void); double GetReadingAt(int index); bool SetDifferencedIndicatorValues(int buffer_size,int differencing_period,bool set_as_series); double GetDifferencedReadingAt(int index); }; //+------------------------------------------------------------------+ //| Our default constructor for our RSI class | //+------------------------------------------------------------------+ void RSI::RSI() { indicator_values_initialized = false; rsi_symbol = "EURUSD"; rsi_time_frame = PERIOD_D1; rsi_period = 5; rsi_price = PRICE_CLOSE; rsi_handler = iRSI(rsi_symbol,rsi_time_frame,rsi_period,rsi_price); //--- Give the user feedback on initilization Print(user_feedback(0)); //--- Remind the user they called the default constructor Print("Default Constructor Called: ",__FUNCSIG__," ",&this); } //+------------------------------------------------------------------+ //| Our parametric constructor for our RSI class | //+------------------------------------------------------------------+ void RSI::RSI(string user_symbol,ENUM_TIMEFRAMES user_time_frame,int user_period,ENUM_APPLIED_PRICE user_price) { indicator_values_initialized = false; rsi_symbol = user_symbol; rsi_time_frame = user_time_frame; rsi_period = user_period; rsi_price = user_price; rsi_handler = iRSI(rsi_symbol,rsi_time_frame,rsi_period,rsi_price); //--- Give the user feedback on initilization Print(user_feedback(0)); } //+------------------------------------------------------------------+ //| Our destructor for our RSI class | //+------------------------------------------------------------------+ void RSI::~RSI() { //--- Free up resources we don't need and reset our flags if(IndicatorRelease(rsi_handler)) { indicator_differenced_values_initialized = false; indicator_values_initialized = false; Print(user_feedback(5)); } } //+------------------------------------------------------------------+ //| Get our current reading from the RSI indicator | //+------------------------------------------------------------------+ double RSI::GetCurrentReading(void) { double temp[]; CopyBuffer(this.rsi_handler,0,0,1,temp); return(temp[0]); } //+------------------------------------------------------------------+ //| Set our indicator values and our buffer size | //+------------------------------------------------------------------+ bool RSI::SetIndicatorValues(int buffer_size,bool set_as_series) { rsi_buffer_size = buffer_size; CopyBuffer(this.rsi_handler,0,0,buffer_size,rsi_reading); if(set_as_series) ArraySetAsSeries(this.rsi_reading,true); indicator_values_initialized = true; //--- Did something go wrong? vector rsi_test; rsi_test.CopyIndicatorBuffer(rsi_handler,0,0,buffer_size); if(rsi_test.Sum() == 0) return(false); //--- Everything went fine. return(true); } //+--------------------------------------------------------------+ //| Let's set the conditions for our differenced data | //+--------------------------------------------------------------+ bool RSI::SetDifferencedIndicatorValues(int buffer_size,int differencing_period,bool set_as_series) { //--- Internal variables rsi_differenced_buffer_size = buffer_size; rsi_differenced_values = vector::Zeros(rsi_differenced_buffer_size); //--- Prepare to record the differences in our RSI readings double temp_buffer[]; int fetch = (rsi_differenced_buffer_size + (2 * differencing_period)); CopyBuffer(rsi_handler,0,0,fetch,temp_buffer); if(set_as_series) ArraySetAsSeries(temp_buffer,true); //--- Fill in our values iteratively for(int i = rsi_differenced_buffer_size;i > 1; i--) { rsi_differenced_values[i-1] = temp_buffer[i-1] - temp_buffer[i-1+differencing_period]; } //--- If the norm of a vector is 0, the vector is empty! if(rsi_differenced_values.Norm(VECTOR_NORM_P) != 0) { Print(user_feedback(2)); indicator_differenced_values_initialized = true; return(true); } indicator_differenced_values_initialized = false; Print(user_feedback(3)); return(false); } //--- Get a differenced value at a specific index double RSI::GetDifferencedReadingAt(int index) { //--- Make sure we're not trying to call values beyond our index if(index > rsi_differenced_buffer_size) { Print(user_feedback(4)); return(-1e10); } //--- Make sure our values have been set if(!indicator_differenced_values_initialized) { //--- The user is trying to use values before they were set in memory Print(user_feedback(1)); return(-1e10); } //--- Return the differenced value of our indicator at a specific index if((indicator_differenced_values_initialized) && (index < rsi_differenced_buffer_size)) return(rsi_differenced_values[index]); //--- Something went wrong. return(-1e10); } //+------------------------------------------------------------------+ //| Get a reading at a specific index from our RSI buffer | //+------------------------------------------------------------------+ double RSI::GetReadingAt(int index) { //--- Is the user trying to call indexes beyond the buffer? if(index > rsi_buffer_size) { Print(user_feedback(4)); return(-1e10); } //--- Get the reading at the specified index if((indicator_values_initialized) && (index < rsi_buffer_size)) return(rsi_reading[index]); //--- User is trying to get values that were not set prior else { Print(user_feedback(1)); return(-1e10); } } //+------------------------------------------------------------------+ //| Check if our indicator handler is valid | //+------------------------------------------------------------------+ bool RSI::IsValid(void) { return((this.rsi_handler != INVALID_HANDLE)); } //+------------------------------------------------------------------+ //| Give the user feedback on the actions he is performing | //+------------------------------------------------------------------+ string RSI::user_feedback(int flag) { string message; //--- Check if the RSI indicator loaded correctly if(flag == 0) { //--- Check the indicator was loaded correctly if(IsValid()) message = "RSI Indicator Class Loaded Correcrtly \nSymbol: " + (string) rsi_symbol + "\nPeriod: " + (string) rsi_period; return(message); //--- Something went wrong message = "Error loading RSI Indicator: [ERROR] " + (string) GetLastError(); return(message); } //--- User tried getting indicator values before setting them if(flag == 1) { message = "Please set the indicator values before trying to fetch them from memory, call SetIndicatorValues()"; return(message); } //--- We sueccessfully set our differenced indicator values if(flag == 2) { message = "Succesfully set differenced indicator values."; return(message); } //--- Failed to set our differenced indicator values if(flag == 3) { message = "Failed to set our differenced indicator values: [ERROR] " + (string) GetLastError(); return(message); } //--- The user is trying to retrieve an index beyond the buffer size and must update the buffer size first if(flag == 4) { message = "The user is attempting to use call an index beyond the buffer size, update the buffer size first"; return(message); } //--- The class has been deactivated by the user if(flag == 5) { message = "Goodbye."; return(message); } //--- No feedback else return(""); } //+------------------------------------------------------------------+
Lassen Sie uns nun die benötigten Marktdaten mithilfe einer Sammlung von Instanzen unserer RSI-Klasse abrufen. Wir werden Zeiger auf jede Instanz unserer Klasse in einem Array des von uns definierten nutzerdefinierten Typs speichern. MQL5 ermöglicht es uns, Objekte automatisch zu generieren, wenn wir sie brauchen. Diese Flexibilität hat jedoch den Preis, dass wir immer hinter uns aufräumen müssen, um Probleme im Zusammenhang mit Speicherlecks zu vermeiden.
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define HORIZON 10 //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <VolatilityDoctor\Indicators\RSI.mqh> //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ RSI *my_rsi_array[14]; string file_name = Symbol() + " RSI Algorithmic Input Selection.csv"; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input int size = 3000; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { //--- How much data should we store in our indicator buffer? int fetch = size + (2 * HORIZON); //--- Store pointers to our RSI objects for(int i = 0; i <= 13; i++) { //--- Create an RSI object my_rsi_array[i] = new RSI(Symbol(),PERIOD_CURRENT,((i+1) * 5),PRICE_CLOSE); //--- Set the RSI buffers my_rsi_array[i].SetIndicatorValues(fetch,true); my_rsi_array[i].SetDifferencedIndicatorValues(fetch,HORIZON,true); } //---Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i=size;i>=1;i--) { if(i == size) { FileWrite(file_handle,"Time","True Close","Open","High","Low","Close","RSI 5","RSI 10","RSI 15","RSI 20","RSI 25","RSI 30","RSI 35","RSI 40","RSI 45","RSI 50","RSI 55","RSI 60","RSI 65","RSI 70","Diff RSI 5","Diff RSI 10","Diff RSI 15","Diff RSI 20","Diff RSI 25","Diff RSI 30","Diff RSI 35","Diff RSI 40","Diff RSI 45","Diff RSI 50","Diff RSI 55","Diff RSI 60","Diff RSI 65","Diff RSI 70"); } else { FileWrite(file_handle, iTime(_Symbol,PERIOD_CURRENT,i), iClose(_Symbol,PERIOD_CURRENT,i), iOpen(_Symbol,PERIOD_CURRENT,i) - iOpen(Symbol(),PERIOD_CURRENT,i + HORIZON), iHigh(_Symbol,PERIOD_CURRENT,i) - iHigh(Symbol(),PERIOD_CURRENT,i + HORIZON), iLow(_Symbol,PERIOD_CURRENT,i) - iLow(Symbol(),PERIOD_CURRENT,i + HORIZON), iClose(_Symbol,PERIOD_CURRENT,i) - iClose(Symbol(),PERIOD_CURRENT,i + HORIZON), my_rsi_array[0].GetReadingAt(i), my_rsi_array[1].GetReadingAt(i), my_rsi_array[2].GetReadingAt(i), my_rsi_array[3].GetReadingAt(i), my_rsi_array[4].GetReadingAt(i), my_rsi_array[5].GetReadingAt(i), my_rsi_array[6].GetReadingAt(i), my_rsi_array[7].GetReadingAt(i), my_rsi_array[8].GetReadingAt(i), my_rsi_array[9].GetReadingAt(i), my_rsi_array[10].GetReadingAt(i), my_rsi_array[11].GetReadingAt(i), my_rsi_array[12].GetReadingAt(i), my_rsi_array[13].GetReadingAt(i), my_rsi_array[0].GetDifferencedReadingAt(i), my_rsi_array[1].GetDifferencedReadingAt(i), my_rsi_array[2].GetDifferencedReadingAt(i), my_rsi_array[3].GetDifferencedReadingAt(i), my_rsi_array[4].GetDifferencedReadingAt(i), my_rsi_array[5].GetDifferencedReadingAt(i), my_rsi_array[6].GetDifferencedReadingAt(i), my_rsi_array[7].GetDifferencedReadingAt(i), my_rsi_array[8].GetDifferencedReadingAt(i), my_rsi_array[9].GetDifferencedReadingAt(i), my_rsi_array[10].GetDifferencedReadingAt(i), my_rsi_array[11].GetDifferencedReadingAt(i), my_rsi_array[12].GetDifferencedReadingAt(i), my_rsi_array[13].GetDifferencedReadingAt(i) ); } } //--- Close the file FileClose(file_handle); //--- Delete our RSI object pointers for(int i = 0; i <= 13; i++) { delete my_rsi_array[i]; } } //+------------------------------------------------------------------+ #undef HORIZON
Analysieren der Daten in Python
Wir können nun mit der Analyse der Daten beginnen, die wir von unserem MetaTrader 5 Terminal gesammelt haben. Unser erster Schritt wird sein, die benötigten Standardbibliotheken zu laden.
import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt import plotly
Nun lesen wir die Marktdaten ein. Wir können auch ein Flag erstellen, das angibt, ob der aktuelle Preis höher oder niedriger ist als der Preis, der vor 10 Tagen angeboten wurde. Erinnern Sie sich, dass 10 Tage derselbe Zeitraum ist, den wir in unserem Skript zur Berechnung der Veränderung der RSI-Werte verwendet haben.
#Let's read in our market data data = pd.read_csv("EURUSD RSI Algorithmic Input Selection.csv") data['Bull'] = np.NaN data.loc[data['True Close'] > data['True Close'].shift(10),'Bull'] = 1 data.loc[data['True Close'] < data['True Close'].shift(10),'Bull'] = 0 data.dropna(inplace=True) data.reset_index(inplace=True,drop=True)
Wir müssen auch die tatsächlichen Marktrenditen berechnen.
#Estimate the market returns #Define our forecast horizon HORIZON = 10 data['Target'] = 0 data['Return'] = data['True Close'].shift(-HORIZON) - data['True Close'] data.loc[data['Return'] > 0,'Target'] = 1 #Drop missing values data.dropna(inplace=True)
Am wichtigsten ist, dass wir alle Daten löschen müssen, die sich mit dem von uns geplanten Backtest-Zeitraum überschneiden.
#No cheating boys. _ = data.iloc[((-365 * 3) + 95):,:] data = data.iloc[:((-365 * 3) + 95),:] data
Abb. 4: Unser Datensatz enthält keine Daten mehr, die sich mit unserem Backtest-Zeitraum überschneiden.
Wir können die Verteilung der EURUSD-10-Tage-Marktrenditen visualisieren, und wir können schnell erkennen, dass die Marktrenditen um 0 herum festgelegt sind. Diese allgemeine Verteilungsform ist wenig überraschend und gilt nicht nur für das EURUSD-Paar.
plt.title('Distribution of EURUSD 10 Day Returns') plt.grid() sns.histplot(data['Return'],color='black')
Abb. 5: Visualisierung der 10-Tage-Ergebnisverteilung des EURUSD.
Diese Übung bietet uns die einmalige Gelegenheit, den Unterschied zwischen der Verteilung der RSI-Werte über einen kurzen Zeitraum und den RSI-Werten über einen langen Zeitraum visuell zu erkennen. Die gestrichelten vertikalen roten Linien markieren die standardisierten 30- und 70-Stufen. Die schwarzen Balken zeigen die Verteilung der RSI-Werte, wenn die Periodenlänge auf 5 gesetzt ist. Wir können sehen, dass der 5-Perioden-RSI viele Signale jenseits der standardisierten Niveaus generieren wird. Die weißen Balken stellen jedoch die Verteilung der RSI-Niveaus dar, wenn der Zeitraum auf 70 gesetzt wird. Wir können visuell sehen, dass fast keine Signale erzeugt werden. Diese Veränderung in der Form der Verteilung macht es für algorithmische Händler schwierig, immer die „besten Praktiken“ für die Verwendung eines Indikators zu befolgen.
plt.title('Comapring The Distribution of RSI Changes Across Different RSI Periods') sns.histplot(data['RSI 5'],color='black') sns.histplot(data['RSI 70'],color='white') plt.xlabel('RSI Level') plt.legend(['RSI 5','RSI 70']) plt.axvline(30,color='red',linestyle='--') plt.axvline(70,color='red',linestyle='--') plt.grid()
Abb. 6: Vergleich der Verteilung der RSI-Werte über verschiedene RSI-Perioden.
Die Erstellung eines Streudiagramms mit der 10-Periodenlängen-Änderung des 60-Perioden-RSI auf der x- und y-Achse ermöglicht es uns, zu visualisieren, ob es eine Beziehung zwischen der Änderung des Indikators und dem Ziel gibt. Es scheint, dass eine Veränderung von 10 RSI-Stufen ein vernünftiges Handelssignal für Verkaufspositionen sein kann, wenn der RSI-Wert um 10 Stufen fällt. Oder gehen wir Kaufpositionen ein, wenn der RSI-Wert um 10 gestiegen ist.
plt.title('Scatter Plot of 10 Day Change in 50 Period RSI & EURUSD 10 Day Return') sns.scatterplot(data,y='Diff RSI 60',x='Diff RSI 60',hue='Target') plt.xlabel('50 Period RSI') plt.ylabel('50 Period RSI') plt.grid() plt.axvline(-10,color='red',linestyle='--') plt.axvline(10,color='red',linestyle='--')
Abb. 7: Visualisierung der Beziehung zwischen der Veränderung des 60-Perioden-RSI und dem Ziel.
Der Versuch, die durch RSI-Änderungen erzeugten Signale mit verschiedenen Perioden zu kombinieren, mag eine vernünftige Idee sein. Sie scheint jedoch wenig dazu beizutragen, unsere beiden Interessengruppen besser voneinander zu trennen.
plt.title('Scatter Plot of 10 Day Change in 5 Period RSI & EURUSD 10 Day Return') sns.scatterplot(data,y='Diff RSI 60',x='Diff RSI 5',hue='Target') plt.xlabel('5 Period RSI') plt.ylabel('5 Period RSI') plt.grid()
Abb. 8: Es scheint, dass die einfache Verwendung von RSI-Indikatoren mit unterschiedlichen Periodenlängen eine schlechte Quelle der Bestätigung ist.
Wir haben 14 verschiedene RSI-Indikatoren zur Auswahl. Anstatt 14 Backtests durchzuführen, um zu entscheiden, welche Periodenlänge am besten geeignet ist, können wir die optimale Periodenlänge schätzen, indem wir die Leistung eines Modells bewerten, das mit allen 14 RSI-Indikatoren als Eingaben trainiert wurde, und dann die Bedeutung der Merkmale bewerten, die das statistische Modell aus den Daten gelernt hat, mit denen es trainiert wurde.
Beachten Sie, dass wir immer die Wahl haben, ob wir statistische Modelle für die Vorhersagegenauigkeit oder für Interpretationen und Erkenntnisse einsetzen. Heute führen wir Letzteres durch. Wir werden ein Ridge-Modell auf die Unterschiede in allen 14 RSI-Indikatoren anwenden. Das Ridge-Modell hat eigene Tuning-Parameter. Wir werden daher eine Rastersuche über den Eingaberaum für das statistische Ridge-Modell durchführen. Wir werden vor allem die Tuning-Parameter anpassen:
- Alpha: Das Ridge-Modell erfordert, dass eine Strafe hinzugefügt wird, um die Koeffizienten des Modells zu kontrollieren.
- Toleranz: Bestimmt die kleinste Änderung, die erforderlich ist, um die Anhaltebedingungen/andere Bedingungen für Unterprogramme in Abhängigkeit von dem vom Nutzer gewählten Solver festzulegen.
Für unsere Diskussion werden wir ein Ridge-Modell mit dem Solver „sparse_cg“ verwenden. Dem Leser steht es frei, das Modell auch zu optimieren, wenn er dies möchte.
Das Ridge-Modell ist für uns besonders hilfreich, weil es seine Koeffizienten auf 0 schrumpft, um den Verlust des Modells zu verringern. Daher werden wir einen weiten Raum von Anfangseinstellungen für unser Modell durchsuchen und uns dann auf die Anfangseinstellungen konzentrieren, die den geringsten Fehler bewirken. Die Konfiguration der Gewichte des Modells in seinem leistungsstärksten Modus kann uns schnell Aufschluss darüber geben, von welchem RSI-Zeitraum unser Modell am meisten abhing. In unserem heutigen Beispiel war es die 10-Perioden-Veränderung des 55-Perioden-RSI, die die größten Koeffizienten unseres leistungsstärksten Modells ergab.
#Set the max levels we wish to check ALPHA_LEVELS = 10 TOL_LEVELS = 10 #DataFrame labels r_c = 'TOL_LEVEL_' r_r = 'ALHPA_LEVEL_' results_columns = [] results_rows = [] for c in range(TOL_LEVELS): n_c = r_c + str(c) n_r = r_r + str(c) results_columns.append(n_c) results_rows.append(n_r) #Create a DataFrame to store our results results = pd.DataFrame(columns=results_columns,index=results_rows) #Cross validate our model for i in range(TOL_LEVELS): tol = 10 ** (-i) error = [] for j in range(ALPHA_LEVELS): #Set alpha alpha = 10 ** (-j) #Its good practice to generally check the 0 case if(i == 0 & j == 0): model = Ridge(alpha=j,tol=i,solver='sparse_cg') #Otherwise use a float model = Ridge(alpha=alpha,tol=tol,solver='sparse_cg') #Store the error levels error.append(np.mean(np.abs(cross_val_score(model,data.loc[:,['Diff RSI 5', 'Diff RSI 10', 'Diff RSI 15', 'Diff RSI 20', 'Diff RSI 25', 'Diff RSI 30', 'Diff RSI 35', 'Diff RSI 40', 'Diff RSI 45', 'Diff RSI 50', 'Diff RSI 55', 'Diff RSI 60', 'Diff RSI 65', 'Diff RSI 70',]],data['Return'],cv=tscv)))) #Record the error levels results.iloc[:,i] = error results
Die nachstehende Tabelle fasst unsere Ergebnisse zusammen. Wir stellen fest, dass die niedrigsten Fehlerquoten erzielt wurden, wenn wir die Anfangsparameter unseres Modells beide auf 0 gesetzt haben.
Einstellparameter | Modellfehler |
---|---|
ALHPA_LEVEL_0 | 0.053509 |
ALHPA_LEVEL_1 | 0.056245 |
ALHPA_LEVEL_2 | 0.060158 |
ALHPA_LEVEL_3 | 0.062230 |
ALHPA_LEVEL_4 | 0.061521 |
ALHPA_LEVEL_5 | 0.064312 |
ALHPA_LEVEL_6 | 0.073248 |
ALHPA_LEVEL_7 | 0.079310 |
ALHPA_LEVEL_8 | 0.081914 |
ALHPA_LEVEL_9 | 0.085171 |
Es ist auch möglich, unsere Ergebnisse mit Hilfe eines Konturdiagramms zu visualisieren. Wir wollen Modelle in dem Bereich der Grafik verwenden, der mit einem geringen Fehler verbunden ist, also in den blauen Bereichen. Dies sind unsere bisher leistungsstärksten Modelle. Veranschaulichen wir uns nun die Größe der einzelnen Koeffizienten im Modell. Der größte Koeffizient wird dem Input zugewiesen, von dem unser Modell am meisten abhängt.
import plotly.graph_objects as go fig = go.Figure(data = go.Contour( z=results, colorscale='bluered' )) fig.update_layout( width = 600, height = 400, title='Contour Plot Of Our Error Forecasting EURUSD Using Grid Search ' ) fig.show()
Abb. 9: Wir haben optimale Eingabeeinstellungen für unser Ridge-Modell zur Vorhersage der 10-Tage-Rendite des EURUSD gefunden.
Wenn wir die Daten visuell darstellen, können wir schnell erkennen, dass dem Koeffizienten, der mit dem 55-Perioden-RSI verbunden ist, der größte absolute Wert zugewiesen wurde. Dies gibt uns eine gewisse Zuversicht, unseren Fokus auf dieses bestimmte RSI-Setup zu beschränken.
#Let's visualize the importance of each column model = Ridge(alpha=0,tol=0,solver='sparse_cg') model.fit(data.loc[:,['Diff RSI 5', 'Diff RSI 10', 'Diff RSI 15', 'Diff RSI 20', 'Diff RSI 25', 'Diff RSI 30', 'Diff RSI 35', 'Diff RSI 40', 'Diff RSI 45', 'Diff RSI 50', 'Diff RSI 55', 'Diff RSI 60', 'Diff RSI 65', 'Diff RSI 70',]],data['Return']) #Clearly our model relied on the 25 Period RSI the most, from all the data it had available at training sns.barplot(np.abs(model.coef_),color='black') plt.title('Rleative Feature Importance') plt.ylabel('Coefficient Value') plt.xlabel('Coefficient Index') plt.grid()
Abb. 10: Der Koeffizient, der mit der Differenz des 55-Perioden-RSI verbunden ist, erhielt den größten Wert.
Nachdem wir nun den Zeitraum, der für uns von Interesse ist, identifiziert haben, wollen wir auch bewerten, wie sich der Fehler unseres Modells ändert, wenn wir den RSI-Wert von 10 erhöhen. Wir werden 3 zusätzliche Spalten in unserem Datenrahmen erstellen. Spalte 1 erhält den Wert 1, wenn der RSI-Wert größer ist als der erste Wert, den wir überprüfen wollen. Andernfalls, wenn der RSI-Wert unter einem bestimmten Wert liegt, den wir überprüfen wollen, wird in Spalte 2 der Wert 1 angezeigt. In allen anderen Fällen hat die Spalte 3 den Wert 1.
def objective(x): data = pd.read_csv("EURUSD RSI Algorithmic Input Selection.csv") data['0'] = 0 data['1'] = 0 data['2'] = 0 HORIZON = 10 data['Return'] = data['True Close'].shift(-HORIZON) - data['True Close'] data.dropna(subset=['Return'],inplace=True) data.iloc[data['Diff RSI 55'] > x[0],12] = 1 data.iloc[data['Diff RSI 55'] < x[1],13] = 1 data.iloc[(data['Diff RSI 55'] < x[0]) & (data['RSI 55'] > x[1]),14] = 1 #Calculate or RMSE When using those levels model = Ridge(alpha=0,tol=0,solver='sparse_cg') error = np.mean(np.abs(cross_val_score(model,data.iloc[:,12:15],data['Return'],cv=tscv))) return(error)
Bewerten wir den Fehler, den unser Modell erzeugt, wenn wir 0 als kritischen Wert festlegen.
#Bad rules for using the RSI objective([0,0])
0.026897725573317266
Wenn wir die Stufen 70 und 30 in unsere Funktion eingeben, erhöht sich unser Fehler. Wir führen eine Rastersuche in 10er-Schritten durch, um Änderungen der RSI-Niveaus zu finden, die besser für den 55-Perioden-RSI geeignet sind. Unsere Ergebnisse zeigen, dass das optimale Änderungsniveau in der Nähe von 10 RSI-Stufen liegt.
#Bad rules for using the RSI objective([70,30])
0.031258730612736006
LEVELS = 10 results = [] for i in np.arange(0,(LEVELS)): results.append(objective([i * 10,-(i * 10)])) plt.plot(results,color='black') plt.ylabel('Error Rate') plt.xlabel('Change in RSI as multiples of 10') plt.grid() plt.scatter(results.index(min(results)),min(results),color='red') plt.title('Measuring The Strength of Changes In RSI Levels')
Abb. 11: Visualisierung des optimalen Veränderungsniveaus in unserem RSI-Indikator.
Führen wir nun eine weitere, differenziertere Suche durch, zwischen dem Intervall der RSI-Änderungen im Bereich von 0 und 20. Bei näherer Betrachtung stellt man fest, dass das wahre Optimum bei dem Wert 9 zu liegen scheint. Wir führen solche Übungen jedoch nicht durch, um historische Daten perfekt anzupassen, das nennt man Overfitting und ist eine schlechte Praxis. Unser Ziel ist es nicht, eine Übung in Kurvenanpassung durchzuführen. Anstatt den optimalen Wert genau dort anzusetzen, wo er in unserer Analyse der historischen Daten auftaucht, nehmen wir die Tatsache in Kauf, dass sich die Lage der Optima ändern kann, und streben stattdessen an, auf beiden Seiten innerhalb eines bestimmten Bruchteils einer Standardabweichung vom optimalen Wert entfernt zu sein, der uns als Konfidenzintervall dient.
LEVELS = 20 coef = 0.5 results = [] for i in np.arange(0,(LEVELS),1): results.append(objective([i,-(i)])) plt.plot(results) plt.ylabel('Error Rate') plt.xlabel('Change in RSI') plt.grid() plt.scatter(results.index(min(results)),min(results),color='red') plt.title('Measuring The Strength of Changes In RSI Levels') plt.axvline(results.index(min(results)),color='red') plt.axvline(results.index(min(results)) - (coef * np.std(data['Diff RSI 55'])),linestyle='--',color='red') plt.axvline(results.index(min(results)) + (coef * np.std(data['Diff RSI 55'])),linestyle='--',color='red')
Abb. 12: Visualisierung der Fehlerquote im Zusammenhang mit der Festlegung verschiedener Schwellenwerte für RSI-Level-Änderungen.
Wir können den Bereich, die wir für optimal halten, visuell über die historische Verteilung der RSI-Änderungen legen.
sns.histplot(data['Diff RSI 55'],color='black') coef = 0.5 plt.axvline((results.index(min(results))),linestyle='--',color='red') plt.axvline(results.index(min(results)) - (coef * np.std(data['Diff RSI 55'])),color='red') plt.axvline(results.index(min(results)) + (coef * np.std(data['Diff RSI 55'])),color='red') plt.axvline(-(results.index(min(results))),linestyle='--',color='red') plt.axvline(-(results.index(min(results)) - (coef * np.std(data['Diff RSI 55']))),color='red') plt.axvline(-(results.index(min(results)) + (coef * np.std(data['Diff RSI 55']))),color='red') plt.title("Visualizing our Optimal Point in The Distribution")
Abb. 13: Visualisierung der optimalen Bereiche, die wir für unsere RSI-Handelssignale ausgewählt haben.
Lassen Sie uns die Werte unserer Schätzung der guten Konfidenzintervalle ermitteln. Diese werden als kritische Werte in unserem Expert Advisor dienen, die Kauf- und Verkaufs-Signale auslösen.
results.index(min(results)) + ( coef * np.std(data['Diff RSI 55']))
10.822857254027287
Und unsere untere Grenze.
results.index(min(results)) - (coef * np.std(data['Diff RSI 55']))
7.177142745972713
Lassen Sie uns eine Erklärung von unserem Ridge-Modell erhalten, um den RSI-Indikator auf eine Weise zu interpretieren, an die wir vielleicht nicht intuitiv gedacht haben.
def explanation(x): data = pd.read_csv("EURUSD RSI Algorithmic Input Selection.csv") data['0'] = 0 data['1'] = 0 data['2'] = 0 HORIZON = 10 data['Return'] = data['True Close'].shift(-HORIZON) - data['True Close'] data.dropna(subset=['Return'],inplace=True) data.iloc[data['Diff RSI 55'] > x[0],12] = 1 data.iloc[data['Diff RSI 55'] < x[1],13] = 1 data.iloc[(data['Diff RSI 55'] < x[0]) & (data['RSI 55'] > x[1]),14] = 1 #Calculate or RMSE When using those levels model = Ridge(alpha=0,tol=0,solver='sparse_cg') model.fit(data.iloc[:,12:15],data['Return']) return(model.coef_.copy())
Wenn sich der RSI-Indikator um mehr als 9, unserem optimalen Wert, verändert, erfährt unser Modell positive Koeffizienten, was bedeutet, dass wir Kaufpositionen eingehen sollten. Andernfalls, so das Modell, sollten wir unter allen anderen Bedingungen verkaufen.
opt = 9 print(explanation([opt,-opt]))
[ 1.97234840e-04 -1.64215118e-04 -7.55222156e-05]
Aufbau unseres Expert Advisors
Können wir unter der Annahme, dass unser Wissen aus der Vergangenheit ein gutes Modell für die Zukunft ist, eine Anwendung entwickeln, mit der wir den EURUSD gewinnbringend handeln können, indem wir das nutzen, was wir jetzt über den Markt gelernt haben? Zu Beginn werden wir zunächst wichtige Systemkonstanten definieren, die wir in unserem Programm und in allen anderen Versionen, die wir erstellen, benötigen werden.
//+------------------------------------------------------------------+ //| Algorithmic Input Selection.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define RSI_PERIOD 55 #define RSI_TIME_FRAME PERIOD_D1 #define SYSTEM_TIME_FRAME PERIOD_D1 #define RSI_PRICE PRICE_CLOSE #define RSI_BUFFER_SIZE 20 #define TRADING_VOLUME SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)
Lassen Sie uns unsere Bibliotheken laden.
//+------------------------------------------------------------------+ //| Load our RSI library | //+------------------------------------------------------------------+ #include <VolatilityDoctor\Indicators\RSI.mqh> #include <Trade\Trade.mqh>
Wir werden einige globale Variablen benötigen.
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ CTrade Trade; RSI rsi_55(Symbol(),RSI_TIME_FRAME,RSI_PERIOD,RSI_PRICE); double last_value; int count; int ma_o_handler,ma_c_handler; double ma_o[],ma_c[]; double trade_sl;
Unsere Ereignisbehandlung rufen jeweils ihre eigene Methode auf, um die für die Erfüllung ihrer Aufgaben erforderlichen Teilprozesse zu bearbeiten.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- setup(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(ma_c_handler); IndicatorRelease(ma_o_handler); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- update(); } //+------------------------------------------------------------------+
Die Aktualisierungsfunktion aktualisiert alle unsere Systemvariablen und prüft, ob wir entweder eine Position eröffnen oder unsere offenen Positionen verwalten müssen.
//+------------------------------------------------------------------+ //| Update our system variables | //+------------------------------------------------------------------+ void update(void) { static datetime time_stamp; datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0); if(time_stamp != current_time) { time_stamp = current_time; CopyBuffer(ma_c_handler,0,0,1,ma_c); CopyBuffer(ma_o_handler,0,0,1,ma_o); if((count == 0) && (PositionsTotal() == 0)) { rsi_55.SetIndicatorValues(RSI_BUFFER_SIZE,true); last_value = rsi_55.GetReadingAt(RSI_BUFFER_SIZE - 1); count = 1; } if(PositionsTotal() == 0) check_signal(); if(PositionsTotal() > 0) manage_setup(); } }
Die Stop-Loss-Werte unserer Positionen müssen ständig angepasst werden, um sicherzustellen, dass wir unser Risiko so weit wie möglich reduzieren.
//+------------------------------------------------------------------+ //| Manage our open trades | //+------------------------------------------------------------------+ void manage_setup(void) { double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); if(PositionSelect(Symbol())) { double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); double new_sl = (current_tp > current_sl) ? (bid-trade_sl) : (ask+trade_sl); double new_tp = (current_tp < current_sl) ? (bid+trade_sl) : (ask-trade_sl); //--- Buy setup if((current_tp > current_sl) && (new_sl < current_sl)) Trade.PositionModify(Symbol(),new_sl,new_tp); //--- Sell setup if((current_tp < current_sl) && (new_sl > current_sl)) Trade.PositionModify(Symbol(),new_sl,new_tp); } }
Unsere Einrichtungsfunktion ist dafür verantwortlich, unser System von Grund auf in Gang zu bringen. Sie bereitet die benötigten Indikatoren vor und setzt unsere Zähler zurück.
//+------------------------------------------------------------------+ //| Setup our global variables | //+------------------------------------------------------------------+ void setup(void) { ma_c_handler = iMA(Symbol(),SYSTEM_TIME_FRAME,2,0,MODE_EMA,PRICE_CLOSE); ma_o_handler = iMA(Symbol(),SYSTEM_TIME_FRAME,2,0,MODE_EMA,PRICE_OPEN); count = 0; last_value = 0; trade_sl = 1.5e-2; }
Schließlich wurden unsere Handelsregeln mit Hilfe der Koeffizientenwerte erstellt, die unser Modell aus den Trainingsdaten gelernt hat. Dies ist die letzte Funktion, die wir definieren, bevor wir die Systemvariablen, die wir zu Beginn unseres Programms erstellt haben, wieder zurücksetzen.
//+------------------------------------------------------------------+ //| Check if we have a trading setup | //+------------------------------------------------------------------+ void check_signal(void) { double current_reading = rsi_55.GetCurrentReading(); Comment("Last Reading: ",last_value,"\nDifference in Reading: ",(last_value - current_reading)); double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); double cp_lb = 7.17; double cp_ub = 10.82; if((((last_value - current_reading) <= -(cp_lb)) && ((last_value - current_reading) > (cp_ub)))|| ((((last_value - current_reading) > -(cp_lb))) && ((last_value - current_reading) < (cp_lb)))) { if(ma_o[0] > ma_c[0]) { if(PositionsTotal() == 0) { Trade.Sell(TRADING_VOLUME,Symbol(),bid,(ask+trade_sl),(ask-trade_sl)); count = 0; } } } if(((last_value - current_reading) >= (cp_lb)) < ((last_value - current_reading) < cp_ub)) { if(ma_c[0] < ma_o[0]) { if(PositionsTotal() == 0) { Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid-trade_sl),(bid+trade_sl)); count = 0; } } } } //+------------------------------------------------------------------+ #undef RSI_BUFFER_SIZE #undef RSI_PERIOD #undef RSI_PRICE #undef RSI_TIME_FRAME #undef SYSTEM_TIME_FRAME #undef TRADING_VOLUME //+------------------------------------------------------------------+
Lassen Sie uns nun mit unserem Backtest des Handelssystems beginnen. Erinnern Sie sich, dass wir in Abb. 4 alle Daten gelöscht haben, die sich mit unserem Backtest überschneiden. Dies kann uns daher als ungefähre Annäherung an die Leistung unserer Strategie in Echtzeit dienen. Wir werden einen 3-Jahres-Backtest unserer Strategie mit täglichen Daten vom 1. Januar 2022 bis März 2025 durchführen.
Abb. 14: Die Daten, die wir für unseren Backtest der Handelsstrategie verwenden werden.
Die besten Ergebnisse können erzielt werden, wenn die Option „Jeder Tick anhand realer Ticks“ gewählt wird, da dies die genaueste Simulation vergangener Marktbedingungen auf der Grundlage der historischen Ticks ist, die das MetaTrader 5 Terminal in Ihrem Namen von Ihrem Broker gesammelt hat.
Abb. 15: Die Bedingungen, unter denen wir unsere Tests durchführen, spielen eine große Rolle und verändern die Rentabilität unserer Strategie.
Die von unserem Strategietester erstellte Kapitalkurve erscheint vielversprechend. Aber lassen Sie uns die detaillierten Statistiken gemeinsam interpretieren, um einen vollständigen Überblick über die Leistung unserer Strategie zu erhalten.
Abb. 16: Die von der EURUSD 55 Period RSI Handelsstrategie erzeugte Kapitalkurve.
Die Ausführung unserer Strategie führte zu den folgenden Statistiken in unserem Strategietester:
- Sharpe Ratio: 0.92
- Expected payoff: 2.49
- Gesamtnettogewinn: $151,87
- Handelsgeschäfte mit Gewinn: 57.38%
Es ist jedoch zu beachten, dass von den insgesamt 61 platzierten Handelsgeschäften nur 4 Käufe waren. Warum ist unsere Strategie so unverhältnismäßig stark auf den Verkauf ausgerichtet? Wie können wir dieses Vorurteil korrigieren? Werfen wir noch einmal einen Blick auf unser EURUSD-Chart und versuchen wir gemeinsam, einen Grund dafür zu finden.
Abb. 17: Die detaillierten Statistiken der historischen Performance unseres Expert Advisors für den täglichen EURUSD-Wechselkurs.
Verbesserung unseres Expertenberaters
In Abbildung 18 habe ich einen Screenshot des monatlichen EURUSD-Wechselkurses beigefügt. Die beiden roten vertikalen Linien markieren den Beginn des Jahres 2009 bzw. das Ende des Jahres 2021. Die grüne vertikale Linie stellt den Beginn der Trainingsdaten dar, die wir für unser statistisches Modell verwendet haben. Angesichts des anhaltenden Abwärtstrends, der 2008 begann, wird schnell klar, warum das Modell eine Neigung zu Verkaufsposition gelernt hat.
Abb. 18: Verständnis für die gelernte Abwärtstendenz unseres Modell
Wir brauchen nicht immer mehr historische Daten, um zu versuchen, dies zu korrigieren. Vielmehr können wir ein flexibleres Modell anwenden als das Ridge-Modell, mit dem wir begonnen haben. Der stärkere Lerner, wird dann unsere Strategie mit zusätzlichen Kaufsignale versorgen.
Es ist uns möglich, unserem Expert Advisor eine vernünftige Vorstellung von der Wahrscheinlichkeit zu geben, dass der Wechselkurs in den nächsten 10 Tagen steigen wird. Wir können einen Random Forest Regressor trainieren, um die Wahrscheinlichkeit einer steigende Kursentwicklung vorherzusagen. Wenn die erwartete Wahrscheinlichkeit 0,5 übersteigt, wird unser Expert Advisor eine Kaufposition eingehen. Andernfalls werden wir der Strategie folgen, die wir aus unserem Ridge-Modell gelernt haben.
Modellierung von Wahrscheinlichkeiten in Python
Um mit unseren Korrekturen zu beginnen, werden wir zunächst einige Bibliotheken importieren.
from sklearn.metrics import accuracy_score from sklearn.ensemble import RandomForestRegressor
Dann werden wir unsere abhängigen und unabhängigen Variablen definieren.
#Independent variable X = data[['Diff RSI 55']] #Dependent variable y = data['Target']
Passen wir das Modell an.
model = RandomForestRegressor() model.fit(X,y)
Bereiten wir es auf den Export nach ONNX vor.
import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType
Definieren der Eingabeformen.
inital_params = [("float_input",FloatTensorType([1,1]))]
Wir erstellen den ONNX-Prototyp und speichern ihn auf der Festplatte.
onnx_proto = convert_sklearn(model=model,initial_types=inital_params,target_opset=12) onnx.save(onnx_proto,"EURUSD Diff RSI 55 D1 1 1.onnx")
Sie können Ihr Modell auch mit Hilfe der netron-Bibliothek visualisieren, um sicherzustellen, dass die richtigen Eingabe- und Ausgabeattribute an Ihr ONNX-Modell übergeben wurden.
import netron
netron.start("EURUSD Diff RSI 55 D1 1 1.onnx")
Dies ist eine grafische Darstellung unseres Random Forest Regressors und der Attribute, die das Modell hat. Netron kann auch zur Visualisierung von Neuronalen Netzen und vielen anderen Typen von ONNX-Modellen verwendet werden.
Abb. 19: Visualisierung unseres ONNX Random Forest Regressor Modells.
Die Input- und Output-Formen wurden von ONNX korrekt spezifiziert, sodass wir nun dazu übergehen können, den Random Forest Regressor anzuwenden, um unserem Expert Advisor dabei zu helfen, die Wahrscheinlichkeit eines steigenden Kursverlaufs in den nächsten 10 Tagen vorherzusagen.
Abb. 20: Die Details unseres ONNX-Modells entsprechen den erwarteten Spezifikationen, die wir überprüfen wollten.
Verbesserung unseres Expertenberaters
Nachdem wir nun ein probabilistisches Modell des EURUSD-Marktes exportiert haben, können wir unser ONNX-Modell in unseren Expert Advisor importieren.
//+------------------------------------------------------------------+ //| Algorithmic Input Selection.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD Diff RSI 55 D1 1 1.onnx" as uchar onnx_model_buffer[];
Wir werden auch einige neue Makros benötigen, die die Form unseres ONNX-Modells spezifizieren.
#define ONNX_INPUTS 1 #define ONNX_OUTPUTS 1
Darüber hinaus verdient das ONNX-Modell einige globale Variablen, da wir sie schnell in verschiedenen Teilen unseres Codes benötigen können.
long onnx_model; vectorf onnx_output(1); vectorf onnx_input(1);
Im folgenden Codeschnipsel haben wir Teile der Codebasis, die sich nicht geändert haben, ausgeklammert und zeigen nur die Änderungen, die zur Initialisierung des ONNX-Modells und zur Einstellung seiner Parameterformen vorgenommen wurden.
//+------------------------------------------------------------------+ //| Setup our global variables | //+------------------------------------------------------------------+ bool setup(void) { //--- Setup our technical indicators ... //--- Create our ONNX model onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DATA_TYPE_FLOAT); //--- Validate the ONNX model if(onnx_model == INVALID_HANDLE) { return(false); } //--- Define the I/O signature ulong onnx_param[] = {1,1}; if(!OnnxSetInputShape(onnx_model,0,onnx_param)) return(false); if(!OnnxSetOutputShape(onnx_model,0,onnx_param)) return(false); return(true); }
Darüber hinaus wurde die Prüfung auf gültigen Handel dahingehend gekürzt, dass nur noch der hinzugefügte Code hervorgehoben wird und eine Duplizierung desselben Codes vermieden wird.
//+------------------------------------------------------------------+ //| Check if we have a trading setup | //+------------------------------------------------------------------+ void check_signal(void) { rsi_55.SetDifferencedIndicatorValues(RSI_BUFFER_SIZE,HORIZON,true); onnx_input[0] = (float) rsi_55.GetDifferencedReadingAt(RSI_BUFFER_SIZE - 1); //--- Our Random forest model if(!OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_input,onnx_output)) Comment("Failed to obtain a forecast from our model!"); else { if(onnx_output[0] > 0.5) if(ma_o[0] < ma_c[0]) Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid-trade_sl),(bid+trade_sl)); Print("Model Bullish Probabilty: ",onnx_output); } }
Alles in allem sieht unsere zweite Version der Handelsstrategie so aus, wenn sie vollständig zusammengestellt ist.
//+------------------------------------------------------------------+ //| Algorithmic Input Selection.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD Diff RSI 55 D1 1 1.onnx" as uchar onnx_model_buffer[]; //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define RSI_PERIOD 55 #define RSI_TIME_FRAME PERIOD_D1 #define SYSTEM_TIME_FRAME PERIOD_D1 #define RSI_PRICE PRICE_CLOSE #define RSI_BUFFER_SIZE 20 #define TRADING_VOLUME SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN) #define ONNX_INPUTS 1 #define ONNX_OUTPUTS 1 #define HORIZON 10 //+------------------------------------------------------------------+ //| Load our RSI library | //+------------------------------------------------------------------+ #include <VolatilityDoctor\Indicators\RSI.mqh> #include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ CTrade Trade; RSI rsi_55(Symbol(),RSI_TIME_FRAME,RSI_PERIOD,RSI_PRICE); double last_value; int count; int ma_o_handler,ma_c_handler; double ma_o[],ma_c[]; double trade_sl; long onnx_model; vectorf onnx_output(1); vectorf onnx_input(1); //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- setup(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(ma_c_handler); IndicatorRelease(ma_o_handler); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- update(); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Update our system variables | //+------------------------------------------------------------------+ void update(void) { static datetime time_stamp; datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0); if(time_stamp != current_time) { time_stamp = current_time; CopyBuffer(ma_c_handler,0,0,1,ma_c); CopyBuffer(ma_o_handler,0,0,1,ma_o); if((count == 0) && (PositionsTotal() == 0)) { rsi_55.SetIndicatorValues(RSI_BUFFER_SIZE,true); rsi_55.SetDifferencedIndicatorValues(RSI_BUFFER_SIZE,HORIZON,true); last_value = rsi_55.GetReadingAt(RSI_BUFFER_SIZE - 1); count = 1; } if(PositionsTotal() == 0) check_signal(); if(PositionsTotal() > 0) manage_setup(); } } //+------------------------------------------------------------------+ //| Manage our open trades | //+------------------------------------------------------------------+ void manage_setup(void) { double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); if(PositionSelect(Symbol())) { double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); double new_sl = (current_tp > current_sl) ? (bid-trade_sl) : (ask+trade_sl); double new_tp = (current_tp < current_sl) ? (bid+trade_sl) : (ask-trade_sl); //--- Buy setup if((current_tp > current_sl) && (new_sl < current_sl)) Trade.PositionModify(Symbol(),new_sl,new_tp); //--- Sell setup if((current_tp < current_sl) && (new_sl > current_sl)) Trade.PositionModify(Symbol(),new_sl,new_tp); } } //+------------------------------------------------------------------+ //| Setup our global variables | //+------------------------------------------------------------------+ bool setup(void) { //--- Setup our technical indicators ma_c_handler = iMA(Symbol(),SYSTEM_TIME_FRAME,2,0,MODE_EMA,PRICE_CLOSE); ma_o_handler = iMA(Symbol(),SYSTEM_TIME_FRAME,2,0,MODE_EMA,PRICE_OPEN); count = 0; last_value = 0; trade_sl = 1.5e-2; //--- Create our ONNX model onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DATA_TYPE_FLOAT); //--- Validate the ONNX model if(onnx_model == INVALID_HANDLE) { return(false); } //--- Define the I/O signature ulong onnx_param[] = {1,1}; if(!OnnxSetInputShape(onnx_model,0,onnx_param)) return(false); if(!OnnxSetOutputShape(onnx_model,0,onnx_param)) return(false); return(true); } //+------------------------------------------------------------------+ //| Check if we have a trading setup | //+------------------------------------------------------------------+ void check_signal(void) { rsi_55.SetDifferencedIndicatorValues(RSI_BUFFER_SIZE,HORIZON,true); last_value = rsi_55.GetReadingAt(RSI_BUFFER_SIZE - 1); double current_reading = rsi_55.GetCurrentReading(); Comment("Last Reading: ",last_value,"\nDifference in Reading: ",(last_value - current_reading)); double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); double cp_lb = 7.17; double cp_ub = 10.82; onnx_input[0] = (float) rsi_55.GetDifferencedReadingAt(RSI_BUFFER_SIZE - 1); //--- Our Random forest model if(!OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_input,onnx_output)) Comment("Failed to obtain a forecast from our model!"); else { if(onnx_output[0] > 0.5) if(ma_o[0] < ma_c[0]) Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid-trade_sl),(bid+trade_sl)); Print("Model Bullish Probabilty: ",onnx_output); } //--- The trading rules we learned from our Ridge Regression Model //--- Ridge Regression Sell if((((last_value - current_reading) <= -(cp_lb)) && ((last_value - current_reading) > (cp_ub)))|| ((((last_value - current_reading) > -(cp_lb))) && ((last_value - current_reading) < (cp_lb)))) { if(ma_o[0] > ma_c[0]) { if(PositionsTotal() == 0) { Trade.Sell(TRADING_VOLUME,Symbol(),bid,(ask+trade_sl),(ask-trade_sl)); count = 0; } } } //--- Ridge Regression Buy else if(((last_value - current_reading) >= (cp_lb)) < ((last_value - current_reading) < cp_ub)) { if(ma_c[0] < ma_o[0]) { if(PositionsTotal() == 0) { Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid-trade_sl),(bid+trade_sl)); count = 0; } } } } //+------------------------------------------------------------------+ #undef RSI_BUFFER_SIZE #undef RSI_PERIOD #undef RSI_PRICE #undef RSI_TIME_FRAME #undef SYSTEM_TIME_FRAME #undef TRADING_VOLUME #undef ONNX_INPUTS #undef ONNX_OUTPUTS //+------------------------------------------------------------------+
Wir werden die Strategie unter den gleichen Bedingungen testen, die in Abb. 14 und Abb. 15 dargestellt sind. Hier sind die Ergebnisse, die wir mit der zweiten Version unserer Handelsstrategie erzielt haben. Die Aktienkurve, die sich aus unserer zweiten Version des Handels ergibt, scheint mit der ersten Aktienkurve, die wir erstellt haben, ziemlich identisch zu sein.
Abb. 21: Visualisierung der Rentabilität unserer zweiten Handelsstrategie.
Erst wenn wir uns die detaillierten Statistiken ansehen, werden die Unterschiede zwischen den beiden Strategien deutlich. Unsere Sharpe Ratio und die erwartete Auszahlung sind gesunken. Unsere neue Strategie platzierte 85 Handelsgeschäfte, das sind 39% mehr als die 61 Handelsgeschäfte, die unsere ursprüngliche Strategie platzierte. Außerdem stieg die Gesamtzahl unserer Kaufpositionen von nur 4 im ersten Test auf 42 in unserem zweiten Test. Das ist ein Anstieg von 950 %. Wenn wir also bedenken, dass wir ein erhebliches zusätzliches Risiko auf uns nehmen und unsere Genauigkeits- und Rentabilitätsstatistiken nur geringfügig sinken, dann fangen wir an, positive Erwartungen an diese Strategie zu knüpfen. In unserem vorherigen Test waren 57,38 aller unserer Handelsgeschäfte profitabel, jetzt sind es 56,47%, das ist eine Verringerung der Genauigkeit um etwa 1,59%.
Abb. 22: Unsere detaillierten Statistiken über die Performance unserer überarbeiteten Version der Handelsstrategie.
Schlussfolgerung
Nach der Lektüre dieses Artikels weiß der Leser, wie er Grid-Suchtechniken zusammen mit statistischen Modellen einsetzen kann, um einen optimalen Zeitraum für Indikatoren auszuwählen, ohne mehrere Backtests durchführen und jeden einzelnen möglichen Zeitraum manuell durchsuchen zu müssen. Darüber hinaus hat der Leser eine Möglichkeit kennengelernt, den Wert neuer RSI-Werte, die er handeln möchte, einzuschätzen und mit dem Wert der traditionellen 70- und 30-Werte zu vergleichen, sodass er mit neuem Vertrauen in seine Fähigkeiten unter ungünstigen Marktbedingungen handeln kann.
Dateiname | Beschreibung |
---|---|
Algorithmic Inputs Selection.ipynb | Das Jupyter-Notebook, das wir zur Durchführung numerischer Analysen mit Python verwendet haben. |
EURUSD Testing RSI Class.mql5 | Das MQL5-Skript, das wir verwendet haben, um unsere Implementierung der nutzerdefinierten RSI-Klasse zu testen. |
EURUSD RSI Algorithmic Input Selection.mql5 | Das Skript, das wir zum Abrufen historischer Marktdaten verwendet haben. |
Algorithmic Input Selection.mql5 | Unsere erste Version der von uns entwickelten Handelsstrategie. |
Algorithmic Input Selection 2.mql5 | Unsere verfeinerte Version der Handelsstrategie, die die Verzerrungen unserer ursprünglichen Handelsstrategie korrigiert. |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/17571





- 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.