English 日本語
preview
Erstellen von selbstoptimierenden Expert Advisor in MQL5 (Teil 6): Selbstanpassende Handelsregeln (II)

Erstellen von selbstoptimierenden Expert Advisor in MQL5 (Teil 6): Selbstanpassende Handelsregeln (II)

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

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

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

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.

Unsere Daten

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.

Unser Backtest

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.

ONNX-Modell

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.

Einzelheiten zum ONNX-Modell

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

Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Erforschung fortgeschrittener maschineller Lerntechniken bei der Darvas Box Breakout Strategie Erforschung fortgeschrittener maschineller Lerntechniken bei der Darvas Box Breakout Strategie
Die von Nicolas Darvas entwickelte Darvas-Box-Breakout-Strategie ist ein technischer Handelsansatz, der potenzielle Kaufsignale erkennt, wenn der Kurs einer Aktie über einen festgelegten Bereich der „Box“ ansteigt, was auf eine starke Aufwärtsdynamik hindeutet. In diesem Artikel werden wir dieses Strategiekonzept als Beispiel anwenden, um drei fortgeschrittene Techniken des maschinellen Lernens zu untersuchen. Dazu gehören die Verwendung eines maschinellen Lernmodells zur Generierung von Signalen anstelle von Handelsfiltern, die Verwendung von kontinuierlichen Signalen anstelle von diskreten Signalen und die Verwendung von Modellen, die auf verschiedenen Zeitrahmen trainiert wurden, um Handelsgeschäfte zu bestätigen.
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.
MQL5-Assistent-Techniken, die Sie kennen sollten (Teil 57): Überwachtes Lernen mit gleitendem Durchschnitt und dem stochastischen Oszillator MQL5-Assistent-Techniken, die Sie kennen sollten (Teil 57): Überwachtes Lernen mit gleitendem Durchschnitt und dem stochastischen Oszillator
Der gleitende Durchschnitt und der Stochastik-Oszillator sind sehr gängige Indikatoren, die von manchen Händlern aufgrund ihres verzögerten Charakters nicht oft verwendet werden. In einer dreiteiligen Miniserie, die sich mit den drei wichtigsten Formen des maschinellen Lernens befasst, gehen wir der Frage nach, ob die Voreingenommenheit gegenüber diesen Indikatoren gerechtfertigt ist, oder ob sie vielleicht einen Vorteil haben. Wir führen unsere Untersuchung mit Hilfe eines Assistenten durch, der Expert Advisors zusammenstellt.