English
preview
Statistische Arbitrage durch kointegrierte Aktien (Teil 9): Backtests, Portfolio-Gewichtungen, Updates

Statistische Arbitrage durch kointegrierte Aktien (Teil 9): Backtests, Portfolio-Gewichtungen, Updates

MetaTrader 5Handelssysteme |
18 0
Jocimar Lopes
Jocimar Lopes

Einführung

Der Markt befindet sich in einem ständigen Wandel. Dies ist das Mantra, das uns auf unserem Weg zum Aufbau eines statistischen Arbitrage-Rahmens für den durchschnittlichen Einzelhändler leitet. Wir halten uns daran, indem wir die üblichen Begriffe von Bären- und Bullenmärkten, direktionalen Trends oder korrelierten Anlagen vermeiden. Stattdessen verwenden wir statistische Methoden, um die Wahrscheinlichkeit zu schätzen, dass Paare oder Gruppen von Vermögenswerten über einen vorhersehbaren Zeithorizont eine Art von Beziehung aufrechterhalten. Für den Moment befassen wir uns mit der Kointegrationsbeziehung aufgrund ihrer Flexibilität und ihrer fast universellen Anwendbarkeit auf den Finanzmärkten. Wir können nach Kointegration zwischen beliebigen Vermögenswerten suchen, auch zwischen Vermögenswerten verschiedener Klassen und sogar zwischen finanziellen Vermögenswerten und nichtfinanziellen Daten, wie einem Aktiensymbol und der Entwicklung der Versandkosten. Sobald eine Kointegration gefunden ist, kann sie fast sicher gehandelt werden.

Der Nachteil ist, dass es aus statistischer Sicht keine Garantie dafür gibt, dass die Kointegration auch für die nächste Stunde, den nächsten Tag oder die nächste Woche gilt. Es wird immer eine gewisse Restwahrscheinlichkeit bestehen, dass es ab dem nächsten Tick zu einem Bruch kommt. Es besteht eine fast hundertprozentige Wahrscheinlichkeit, dass sich die Kurse der Ticker ändern werden.

Wir berechnen den Kointegrationsvektor aus den aktuellen Kursen der Ticker. Aus dem Kointegrationsvektor ergeben sich die relativen Portfoliogewichte, d. h. das Volumen der in jeder Reihenfolge zu kaufenden oder zu verkaufenden Vermögenswerte. Da sich die Kurse der Ticker ständig ändern, ändern sich auch die Portfoliogewichte. Wir können mit nahezu hundertprozentiger Wahrscheinlichkeit sagen, dass das optimale Auftragsvolumen von gestern nicht das optimale Auftragsvolumen für heute ist. Die relativen Preise haben sich geändert, sodass die Portfoliogewichte aktualisiert werden müssen.

Im letzten Artikel haben wir gesehen, wie wir eine kontinuierliche Überwachung der Stabilität der Portfoliogewichte durchführen können, indem wir die In-Sample/Out-of-Sample ADF (IS/OOS ADF) Validierung in Verbindung mit dem Rolling Windows Eigenvector Comparison (RWEC) verwenden. Mit dieser bekannten Technik lassen sich vergangene Kointegrationsbrüche aufdecken und die Wahrscheinlichkeit künftiger Brüche in der Beziehung zwischen Vermögenswerten schätzen. Diese Funktionen haben sowohl im Rahmen der Datenanalyse für die Portfoliobildung als auch als Risikomanagement-Tool im Rahmen der Live-Handelsüberwachung ihre Vorteile. Während der Erstellung des Portfolios können wir bewerten, wie das Modell bei Variationen der wichtigsten Parameter der einzelnen Methoden abschneidet, und während der Überwachung können wir diese Parameter entsprechend der neuesten Datenanalyse feinabstimmen. Da unsere RWEC-Implementierung dieselbe Methode verwendet, die wir in unserem Bewertungssystem für die Einstufung der Kointegrationsstärke einsetzen – den Johansen-Kointegrationstest –, können die daraus resultierenden Kointegrationsvektoren zur Aktualisierung unserer Portfoliogewichte verwendet werden.

Im Live-Handel liest unser EA die Portfolio-Gewichtungen aus der Strategietabelle, wie wir das bei der Einrichtung der Datenbank als einzige Quelle der Wahrheit besprochen haben. Bei den Backtests haben wir jedoch keinen Zugriff auf die Datenbank. Backtests sind aber die einzige praktische Möglichkeit, die Feinabstimmung dieser Parameter für Dutzende, möglicherweise Hunderte Anlagepaaren und -körbe (baskets) in einem angemessenen Zeitraum zu simulieren. Durch Neugewichtung können wir unsere Testmethoden für die Signalstabilität und auch die Wirksamkeit unserer Algorithmen zur Neugewichtung bewerten. Wir müssen prüfen, ob die EA-Logik für das Neugewichten wie erwartet funktioniert und inwieweit die gewählten Parameter für das Neugewichten unsere Ergebnisse verbessern würden. Die einfachste Methode, um im Tester von MetaTrader 5 auf Datenbankdaten zuzugreifen, besteht darin, die benötigten Daten in eine Datei zu exportieren und sie direkt aus dem EA zu lesen. Hier exportieren wir die vollständige Strategietabelle als CSV-Datei (kommaseparierte Werte) und laden die „neuen“ Portfolio-Gewichte in eine spezielle Test-Helferfunktion.


Füllen der Datenbank mit Beispieldaten

Bevor wir unsere Datenbank der Strategietabelle exportieren können, benötigen wir die RWEC-Daten darin. Dazu verwenden wir ein einfaches Python-Skript, das die RWEC-Analyse durchführt und die Ergebnisse in unserer Datenbank speichert. Auch hier ist zu beachten, dass es sich um eine Simulation für den Backtest handelt. Unter normalen Umständen wird die RWEC-Analyse Teil unserer täglichen Routine der Live-Handelsüberwachung sein, und ihre Ergebnisse werden vom EA direkt aus der Datenbank übernommen.

Das Skript holt die erforderlichen Daten aus dem MetaTrader 5-Terminal. Wie üblich verwenden wir in unseren Beispielen nur die Symbole, die im Meta Quotes-Demokonto verfügbar sind, sodass es Ihnen leicht fallen sollte, diese Experimente selbst durchzuführen.

Sie finden dieses Skript hier als rwec2db.py angehängt. 

Abb. 1 – Bildschirmfoto mit einer gefalteten Übersicht über die Methoden und Funktionen von rwec2db.py.

Abb. 1. Bildschirmfoto mit einer gefalteten Übersicht über die Methoden und Funktionen von rwec2db.py.

Wenn Sie dieses Skript ausführen, sollten Sie eine Ausgabe wie diese sehen:

Abb. 2 – Bildschirmfoto mit der erwarteten Ausgabe von rwec2db.py.

Abb. 2. Bildschirmfoto mit der erwarteten Ausgabe von rwec2db.py

Wenn kein Kointegrationsvektor gefunden wird, wird eine entsprechende Meldung ausgegeben.

Abb. 3 – Bildschirmfoto mit der Ausgabe von rwec2db.py, wenn kein Kointegrationsvektor gefunden wurde.

Abb. 3. Bildschirmfoto der Ausgabe von rwec2db.py, wenn kein Kointegrationsvektor gefunden wurde.

Beachten Sie, dass der oben beschriebene Fall eintreten kann, wenn Sie dieselben Symbole über einen relativ kurzen Zeitraum testen, z. B. 30 Balken. Das liegt daran, dass möglicherweise nicht genügend historische Daten für einen Eigenvektorvergleich mit rollierenden Fenstern vorhanden sind. Wenn dies der Fall ist, können Sie mehr Daten anfordern und/oder die Länge des rollenden Fensters erhöhen, und wenn alles gut läuft, sollte Ihre Strategietabelle am Ende etwa so aussehen.

Abb. 4 – Bildschirmfoto der in MetaEditor integrierten SQLite-Datenbank mit der mit Beispieldaten gefüllten Strategietabelle

Abb. 4. Bildschirmfoto der in MetaEditor integrierten SQLite-Datenbank mit der Strategietabelle, die mit Beispieldaten gefüllt ist

Jetzt sind wir bereit, die Tabelle zu exportieren.


Exportieren der Datenbanktabelle

MetaEditor hat eine eingebaute Exportfunktion für Tabellen. Klicken Sie einfach mit der rechten Maustaste auf den Tabellennamen.

Abb. 5 – Bildschirmfoto des MetaEditor-Datenbank-Kontextmenüs

Abb. 5. Bildschirmfoto des MetaEditor-Datenbank-Kontextmenüs

Damit Ihr Export vollständig mit dem beigefügten Beispielcode kompatibel ist, wählen Sie im folgenden Dialogfeld die folgenden Optionen.

Abb. 6 – Bildschirmfoto des MetaEditor-Dialogs für Datenbankexportoptionen mit hervorgehobenen empfohlenen Optionen

Abb. 6. Bildschirmfoto des MetaEditor-Dialogs für Datenbankexportoptionen mit hervorgehobenen empfohlenen Optionen 

Wählen Sie das Tabulatorzeichen als Trennzeichen. Wir werden also mit einer Datei mit tabulatorgetrennten Werten – TSV – arbeiten. Dies soll das Parsen unserer Portfolio-Gewichte erleichtern, da sie als JSON-Array in der Datenbank gespeichert werden. Diese Arrays enthalten bereits Kommas. 

[1.0, -0.045459, 0.021855, -0.033486]

Durch die Wahl des Tabulatorzeichens als Trennzeichen können wir dieses Parsing erleichtern. Wählen Sie außerdem, dass die Spaltennamen und die in Anführungszeichen gesetzten Zeichenfolgen erhalten bleiben.

Denken Sie daran, dass Ihre TSV-Datei im TERMINAL_DATA_PATH („Ordner, in dem Terminaldaten gespeichert werden“) oder im TERMINAL_COMMONDATA_PATH („Gemeinsamer Pfad für alle auf einem Computer installierten Terminals“) gespeichert sein muss, damit die Testerumgebung darauf zugreifen kann. In diesem Beispiel verwenden wir die erste Variante.

Wenn Sie die TSV-Datei nicht in dem oben erwähnten gemeinsamen Pfad speichern, müssen Sie die Eigenschaft tester_file in Ihre MQL5-Hauptdatei aufnehmen.

//+------------------------------------------------------------------+
//|                                                  CointNasdaq.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Expert Advisor - Cointegration Statistical Arbitrage             |
//| Assets: Dynamic assets allocation                                |
//| Strategy: Mean-reversion on Johansen cointegration portfolio     |
//+------------------------------------------------------------------+
#property tester_file "StatArb\\strategy_202512041731.csv"

„Eigenschaften, die in eingeschlossenen Dateien beschrieben sind, werden vollständig ignoriert. Die Eigenschaften müssen in der mq5-Hauptdatei angegeben werden. (...) Dateiname für ein Prüfgerät mit Angabe der Erweiterung, in Anführungszeichen (als konstante Zeichenfolge). Die angegebene Datei wird an den Prüfer weitergegeben. Zu prüfende Eingabedateien müssen immer angegeben werden, wenn es solche gibt.“ (MQL5-Dokumentation)

Ohne diese Eigenschaft ist es nicht möglich, die Datei aus der Testerumgebung zu lesen.

Abb. 7 – Bildschirmfoto des MetaTrader 5-Journals, das einen Fehler beim Öffnen der Datei auf dem Tester meldet.

Abb. 7. Bildschirmfoto des MetaTrader 5-Journals, das einen Fehler beim Öffnen der Datei im Tester meldet

Dies geschieht, weil die Umgebung des Testers aus Sicherheitsgründen als isolierte Sandbox arbeitet. Dieser Artikel enthält eine kurze und klare Beschreibung des internen Prozesses bei der Arbeit mit Dateien im Tester. In der MetaTrader 5-Dokumentation finden Sie zahlreiche Informationen zum Lesen und Schreiben von Dateien. Das AlgoBook enthält eine umfassende Anleitung zur Arbeit mit Dateien in MQL5. Hier werden wir uns auf die Anforderungen für unseren speziellen Anwendungsfall konzentrieren.


Laden der Strategieparameter aus der Datei

Nach diesen vorbereitenden Maßnahmen sind wir bereit, die Funktion LoadStrategyFromFile() vorzustellen, die nun in unserem Beispiel-EA CointNasdaq.mq5 enthalten ist, den Sie am Ende dieses Artikels finden. Der beigefügte Code ist absichtlich „unbearbeitet“. Ich habe ihn nicht zurechtgemacht, indem ich Fehlersuchbilder entfernt habe. Stattdessen habe ich sie zusammen mit allen Kommentaren so belassen, wie sie mich beim Schreiben geleitet haben, damit Sie besser folgen und nach einfacheren, saubereren oder effizienteren Lösungen suchen können. Die meisten der von mir verwendeten Debug-Ausdrucke sind auskommentiert. Wenn Sie also beim Testen Ihrer eigenen Änderungen Probleme haben, können Sie den Code einfach an den erforderlichen Stellen auskommentieren.

Die Anweisung LoadStrategyFromFile() kann direkt in der Ereignisbehandlung von OnInit() aufgerufen werden, abhängig von der laufenden Umgebung.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   ResetLastError();
// Check if all symbols are available
   for(int i = 0; i < ArraySize(symbols); i++)
     {
      if(!SymbolSelect(symbols[i], true))
        {
         Print("Error: Symbol ", symbols[i], " not found!");
         return(INIT_FAILED);
        }
     }
// Initialize spread buffer
   ArrayResize(spreadBuffer, InpLookbackPeriod);
// Set a timer for spread, mean, stdev calculations
// and strategy parameters update (check DB)
   EventSetTimer(InpUpdateFreq * 60); // min one minute
// check if we are backtesting
   if(!MQLInfoInteger(MQL_TESTER))
     {
      // Load strategy parameters from database
      if(!LoadStrategyFromDB(InpDbFilename,
                             InpStrategyName,
                             symbols,
                             weights,
                             timeframe,
                             InpLookbackPeriod))
        {
         // Handle error - maybe use default values
         printf("Error at " + __FUNCTION__ + " %s ",
                getUninitReasonText(GetLastError()));
         return INIT_FAILED;
        }
     }
   else
     {
      // Load strategy parameters from CSV file
      if(!LoadStrategyFromFile(InpTesterStrategyFilename, symbols, weights))
        {
         // Handle error - maybe use default values
         printf("Error at " + __FUNCTION__ + " %s ",
                getUninitReasonText(GetLastError()));
         return INIT_FAILED;
        }
     }
   return(INIT_SUCCEEDED);
  }

Wenn der EA in der Testumgebung ausgeführt wird, laden wir die Strategie aus der Datei. Andernfalls wird die Strategie im normalen Handel aus der Datenbank geladen.

Um unsere EA-Hauptdatei nicht zu überladen, ist die Funktion in einer zusätzlichen Header-Datei, TestHelper.mqh, implementiert, die ebenfalls hier beigefügt ist.

//+------------------------------------------------------------------+
//|  Load the strategy parameters from CSV/TSV file                  |
//+------------------------------------------------------------------+
bool LoadStrategyFromFile(string filename,
                          string &strat_symbols[],
                          double &strat_weights[])
 {
   Print("Running on tester");
// Instantiate the hash map
   CHashMap<ulong, CArrayDouble*> updates;
// Load the weights from the CSV file
   LoadWeights(filename, updates);
   Print("Updates count ", updates.Count());
(...)

Anhand der Funktionsparameter können Sie erkennen, dass die Funktion nur die Portfoliogewichte behandelt. Dies steht im Gegensatz zu seinem Gegenstück, das die Strategie aus der Datenbank lädt und den Namen der Strategie, den Zeitrahmen und den Rückblickzeitraum enthält. Das liegt daran, dass diese Implementierung noch in Arbeit ist. Später, wenn es um die Rotation des Portfolios geht, wird sie auch alle Strategieparameter umfassen.

Die Funktion beginnt mit der Instanziierung einer dynamischen Hash-Tabelle aus der MQL5-Standardbibliothek genauer gesagt der generischen Datensammlung, einer CHashMap, in der die Schlüssel unsere Portfolio-Updates mit Zeitstempeln speichern und die Werte unsere Portfolio-Gewichte als Arrays von Doubles sind. Die Objektinstanz wird dann zusammen mit dem Dateinamen als Verweis an eine spezielle Funktion LoadWeights() übergeben, um die CSV-Datei korrekt zu lesen.

//+------------------------------------------------------------------+
//|    Load portfolio weights updates from CSV/TSV file              |
//+------------------------------------------------------------------+
void LoadWeights(string filename,
                 CHashMap<ulong, CArrayDouble*> &updates)
  {
   ResetLastError();
   int filehandle = FileOpen(filename,
                             FILE_ANSI | FILE_CSV | FILE_READ, '\\t', CP_ACP);
   if(filehandle != INVALID_HANDLE)
     {
      printf("Data Path: %s Filename: %s", TerminalInfoString(TERMINAL_DATA_PATH), filename);
      // Read and discard the header line
      string first_line = FileReadString(filehandle);
      Print(first_line);
(...)

Die Funktion LoadWeights() folgt der üblichen Vorgehensweise beim Lesen von Textdateien in MQL5. Auch hier ist zu beachten, dass wir beim Öffnen der Datei NICHT das FILE_COMMON-Flag verwenden, d. h., wir verwenden den Terminal-Datenpfad, was die Verwendung von #property tester_file in unserem EA erfordert, wie oben kommentiert. Wir verwerfen die erste Zeile, den CSV-Header, und drucken sie zur Überprüfung in das Journal.

Abb. 8 – Bildschirmfoto des MetaTrader 5-Journals mit den ersten Zeilen der Testprotokollierung

Abb. 8. Bildschirmfoto des MetaTrader 5-Journals mit den ersten Zeilen der Testprotokollierung

Dann beginnen wir mit der Iteration über die Zeilen der CSV-Datei außer der Kopfzeile, um sie durch das Tabulatorzeichen zu trennen. Die sich daraus ergebenden Segmente sind die Werte, nach denen wir suchen. Wir speichern sie also in dem String-Array fields[].

// iterate over lines
      while(!FileIsEnding(filehandle))
        {
         string line = FileReadString(filehandle);
         string fields[]; // fields[0] -> tstamp   fields[4] -> weights
         int count = StringSplit(line, '\t', fields);
         //printf("fields => %s  %s %s %s %s %s %s",
         //       fields[0], fields[1], fields[2],
         //       fields[3], fields[4], fields[5], fields[6]);
         //—
(...)

Wir erstellen ein CArrayDouble-Objekt, um das Array der Gewichte für jede gelesene Zeile zu erhalten. Dies ist das von unserer CHashMap-Klasse benötigte Objekt.

// Create the CArrayDouble object for this timestamp
         CArrayDouble *current_weights_arr = new CArrayDouble();
         ulong tstamp = 0;
(...)

Da unsere Portfoliogewichts-Arrays als JSON-Arrays in der Datenbank gespeichert sind, müssen wir sie bereinigen, indem wir die Klammern entfernen, bevor wir sie an das CArrayDouble-Objekt übergeben.

// Ensure we have at least the tstamp (0) and weights (4) fields
         if(count > 4)
           {
            // weights string
            string weights_str = fields[4];
            StringReplace(weights_str, "[", "");
            StringReplace(weights_str, "]", "");
            // weights strings array
            string weights_str_arr[];
            int weights_count = StringSplit(weights_str, ',', weights_str_arr);
            //---
            if(current_weights_arr == NULL)
              {
               Print("Err creating CArrayDouble for timestamp ", fields[0]);
               continue; // Skip to the next line
              }
            // Populate the new CArrayDouble
            for(int i = 0; i < weights_count; i++)
              {
               //printf("weights_str_arr %s", weights_str_arr[i]);
               double weight_value = StringToDouble(weights_str_arr[i]);
               //printf("weight_value %.6f", weight_value);
               tstamp = (ulong)StringToInteger(fields[0]);
               //printf("tstamp %I64u ", tstamp);
               current_weights_arr.Add(weight_value);
               //printf("current_weights_arr Total: %d", current_weights_arr.Total());
              }
(...)

Nun, da unser CArrayDouble-Objekt ausgefüllt ist, können wir es zu unserer Hash-Map hinzufügen.           

// 4. Add to the HashMap once per line
            if(updates.Add(tstamp, current_weights_arr))
              {
               printf("Added tstamp %I64u -> %s ", tstamp, TimeToString(tstamp));
              }
            else
              {
               Print("Failed adding record");
              }
           }
        }
     }
   else
     {
      printf("Error opening file %s. Error: %i", filename, GetLastError());
     }
   FileClose(filehandle);
  }

Abb. 9 – Bildschirmfoto des MetaTrader 5-Journals mit den Zeitstempeln der Aktualisierungen, die der Hash-Map hinzugefügt wurden

Abb. 9. Bildschirmfoto des MetaTrader 5-Journals mit den Zeitstempeln der Aktualisierungen, die der Hash-Map hinzugefügt wurden

Zurück zu unserer Funktion LoadStrategyFromFile(): Überprüfen wir die Anzahl der geladenen Portfolio-Updates. Dies ist die Anzahl der Zeilen in Ihrer CSV-Datei minus eine (die Kopfzeile).

// Load the weights from the CSV file
   LoadWeights(filename, updates);
   Print("Updates count ", updates.Count());
// copy the values to iterable arrays
   ulong tstamp_keys[];
   CArrayDouble *weights_values[];
   updates.CopyTo(tstamp_keys, weights_values);
// check if everything was copied
   Print("Keys size: ", tstamp_keys.Size());
   Print("Values size: ", weights_values.Size());
(...)

Abb. 10 – Bildschirmfoto des MetaTrader 5-Journals mit der Anzahl der zu verarbeitenden Aktualisierungen

Abb. 10. Bildschirmfoto des MetaTrader 5-Journals, das die Anzahl der zu verarbeitenden Aktualisierungen anzeigt

Wir prüfen dann, ob die Datei veraltete Updates enthält. 

// check for outdated updates on file
   ulong first_tstamp_on_file = tstamp_keys[0];
   printf("first_tstamp_on_file %I64u", first_tstamp_on_file);
   ulong update_to_apply = 0;
   if(FileHasOutdatedUpdates(first_tstamp_on_file))
      FileCleanUpdates(tstamp_keys, updates, update_to_apply);
(...)

Denken Sie daran, dass wir exportierte Datenbankdaten verwenden, um die Aktualisierungen der Portfoliogewichte zu simulieren, die wir beim Live-Handel aus der Datenbank beziehen werden. Die einzige relevante Aktualisierung ist also die letzte, die mit dem früheren Zeitstempel, direkt vor der aktuellen Zeit. Beim Exportieren der Daten kann unsere Datenbank jedoch ältere RWEC-Daten enthalten – und wird dies wahrscheinlich auch tun –, die viele Tage oder Wochen vor dem Start unseres Backtests liegen. Diese älteren Daten müssen entfernt werden, damit die zeitliche Übereinstimmung zwischen unseren Beispieldaten und unseren Backtest-Einstellungen erhalten bleibt.

//+------------------------------------------------------------------+
//|   check if the CSV file has outdated updates                     |
//+------------------------------------------------------------------+
bool FileHasOutdatedUpdates(ulong updates_start_time)
  {
   datetime test_start_time = TimeCurrent();
   if((datetime)updates_start_time < test_start_time)
     {
      Print("Warning! Updates starts before test start time.");
      printf("Test start time: %s", TimeToString(test_start_time));
      printf("Updates start time: %s", TimeToString(updates_start_time));
      Print("Will REMOVE outdated updates.");
      return true;
     }
   return false;
  }

Abb. 11 – Bildschirmfoto des MetaTrader 5-Journals mit der Warnung vor veralteten Updates, die entfernt werden müssen

Fig. 11. Bildschirmfoto des MetaTrader 5-Journals mit der Warnung vor veralteten Updates, die entfernt werden müssen

Die Funktion FileCleanUpdates() durchläuft die Zeitstempel, um alle veralteten Aktualisierungen außer der letzten zu entfernen.

//+------------------------------------------------------------------+
//| iterate over keys to remove outdated updates                     |
//+------------------------------------------------------------------+
void FileCleanUpdates(ulong &tstamp_keys[],
                      CHashMap<ulong, CArrayDouble*> &updates,
                      ulong &update_to_apply)
  {
   int outdated_count = 0;
   ulong outdated_keys[];
   for(int i = 0; i < ArraySize(tstamp_keys); i++)
     {
      if((datetime)tstamp_keys[i] < TimeCurrent()) // look for outdated updates
        {
         printf("Outdated updates at: %s", TimeToString(tstamp_keys[i]));
         outdated_keys.Push(tstamp_keys[i]);
         outdated_count++;
        }
      while(outdated_count > 1) // preserve the newest one to be applied
        {
         if(updates.Remove(tstamp_keys[i - 1]))
           {
            outdated_count--;
            printf("Removed outdated update from %s ", TimeToString(tstamp_keys[i - 1]));
           }
        }
     }
   printf("Removed %i outdated updates:", outdated_keys.Size() - 1);
   update_to_apply = outdated_keys[outdated_keys.Size() - 1];
   printf("Update from %s to be applied", TimeToString(update_to_apply));
  }

Sowohl die entfernten veralteten Updates als auch das anzuwendende Update werden im Journal aufgeführt.

Abb. 12 – Bildschirmfoto des MetaTrader 5-Journals mit dem Hinweis auf entfernte Updates

Abb. 12. Bildschirmfoto des MetaTrader 5-Journals mit dem Hinweis auf entfernte Updates

Schließlich kann unsere Funktion LoadStrategyFromFile() ihre Arbeit beenden, indem sie die Gewichte des EA-Portfolios festlegt.

//---
   CArrayDouble *new_weights = new CArrayDouble();
   if(updates.TryGetValue(update_to_apply, new_weights))
     {
      ArrayResize(strat_weights, 4);
      //---
      strat_weights[0] = new_weights[0];
      strat_weights[1] = new_weights[1];
      strat_weights[2] = new_weights[2];
      strat_weights[3] = new_weights[3];
      //---
      ArrayResize(strat_symbols, 4);
      //---
      strat_symbols[0] = "INTC";
      strat_symbols[1] = "AMD";
      strat_symbols[2] = "AVGO";
      strat_symbols[3] = "MU";
      //---
      printf("New weights: %s %.6f | %s %.6f | %s %.6f | %s %.6f",
             strat_symbols[0], new_weights[0],
             strat_symbols[1], new_weights[1],
             strat_symbols[2], new_weights[2],
             strat_symbols[3], new_weights[3]
            );
      return true;
     }
   return false;
  }

Die neuen Portfolio-Gewichtungen werden in der Zeitschrift veröffentlicht.

Abb. 13 – Bildschirmfoto des Journals von MetaTrader 5 mit der Anzeige der neuen Portfolio-Gewichtungen

Abb. 13. Bildschirmfoto des MetaTrader 5-Journals mit der Anzeige der neuen Portfoliogewichte 

Alle oben genannten Schritte werden einmal vom EA-Ereignishandler OnInit() aufgerufen. Beim Start des Backtests lädt unser EA die Strategieparameter aus der CSV-Datei, speichert sie in einem Hash-Map-Objekt, entfernt die Aktualisierungen, deren Zeitstempel kleiner als der Startzeitpunkt des Backtests ist (die „veralteten Aktualisierungen“), falls vorhanden, und wendet die neuesten davon an. Nun, da der Backtest zeitlich vorwärts geht, müssen wir die folgenden Aktualisierungen anwenden, deren Zeitstempel größer ist als die Startzeit des Backtests, um die Aktualisierungen der Portfoliogewichte zu simulieren, die kommen, während der EA im Live-Handel läuft. Dies geschieht mithilfe der Ereignisbehandlung von OnTimer().

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
   ResetLastError();
   if(!MQLInfoInteger(MQL_TESTER))
     {
      // Wrapper around LoadStrategyFromDB: for clarity
      if(!UpdateModelParams(InpDbFilename,
                            InpStrategyName,
                            symbols,
                            weights,
                            timeframe,
                            InpLookbackPeriod))
        {
         printf("%s failed: Error %i", __FUNCTION__, GetLastError());
        }
     }
   else
     {
      if(!UpdateModelParamsFromFile(symbols, weights))
        {
         printf("%s failed: Error %i", __FUNCTION__, GetLastError());
        }
     }
   printf("Actual weights: %s %.6f | %s %.6f | %s %.6f | %s %.6f",
          symbols[0], weights[0],
          symbols[1], weights[1],
          symbols[2], weights[2],
          symbols[3], weights[3]
         );

Die Funktion UpdateModelParamsFromFile() ist sehr einfach. Da wir unsere HashMap-Schlüssel in ein separates Array kopiert haben, müssen wir sie nur bei jedem Funktionsaufruf nacheinander abrufen. Jeder Schlüssel ist der Zeitstempel einer Aktualisierung. Ist sie kleiner als die aktuelle Zeit, werden die Korbgewichte entsprechend aktualisiert. Ist der Schlüssel/Zeitstempel größer als die aktuelle Zeit, handelt es sich um eine Aktualisierung, die noch nicht existiert. In diesem Fall gibt die Funktion true zurück, weil kein Fehler aufgetreten ist, aber es wird keine Aktualisierung vorgenommen. Es wird nur dann false/error zurückgegeben, wenn die Methode TryGetValue() von HashMap<> den entsprechenden Schlüssel im HashMap-Objekt nicht finden kann.

ulong tstamp_keys[];
CHashMap<ulong, CArrayDouble*> *updates = new CHashMap<ulong, CArrayDouble*>();

//+------------------------------------------------------------------+
//|  get the earlier tstamp on the updates hash map;                 |
//|  then update the model params (symbols and weights)              |
//|  with its values                                                 |
//+------------------------------------------------------------------+ 
bool UpdateModelParamsFromFile(string &curr_symbols[], double &curr_weights[])
  {
// Print("Updating model params from file");

// get the earlier tstamp on the updates hash map
   int static i = 0;
   if((datetime)tstamp_keys[i] < TimeCurrent())
     {
      curr_symbols[0] = "INTC";
      curr_symbols[1] = "AMD";
      curr_symbols[2] = "AVGO";
      curr_symbols[3] = "MU";
//—--
      CArrayDouble *new_weights = new CArrayDouble();
      if(!updates.TryGetValue(tstamp_keys[i], new_weights))
        {
         return false;
        }
      curr_weights[0] = new_weights[0];
      curr_weights[1] = new_weights[1];
      curr_weights[2] = new_weights[2];
      curr_weights[3] = new_weights[3];
//—-- increment the idx
      i++;
      delete new_weights;
     }
   else
     {
      Print("No update to apply");
     }
   return true;
  }

Wenn eine Aktualisierung im Backtest auftritt, wird sie wie folgt im Journal protokolliert.

Abb. 14 – Bildschirmfoto des MetaTrader 5-Journals mit der Aktualisierung der Portfoliogewichte im Backtest

Abb. 14. Bildschirmfoto des MetaTrader 5-Journals, das die Aktualisierung der Portfoliogewichte im Backtest zeigt

Unser Backtest beginnt hier, nachdem wir die aus der Datenbank exportierte CSV/TSV-Datei geladen, geparst und bereinigt haben.

Wie in der Einleitung dieses Artikels erwähnt, müssen wir prüfen, ob die EA-Logik für das Neugewichten wie erwartet funktioniert und inwieweit die gewählten RWEC-Parameter für das Neugewichten unsere Ergebnisse verbessern würden.


Die zu prüfenden RWEC-Parameter

Ziel des Backtests ist es, sich den optimalen Werten der rollierenden Kointegrationsparameter anzunähern. Man beachte, dass der RWEC einen Johansen-Kointegrationstest verwendet, um das Vorhandensein von Kointegration zu beurteilen. Dieser Test hat seine eigenen Parameter, die wir hier aber nicht berücksichtigen, da wir davon ausgehen, dass wir bereits mit einem kointegrierten Korb arbeiten. Die Aufgabe des RWEC besteht nun darin, die Portfoliogewichte zu aktualisieren, die zuvor durch den Johansen-Test in den Screening-/Scoring-Schritten unserer Pipeline berechnet wurden.Die wichtigsten RWEC-Parameter, die wir backtesten wollen, sind:

  1. Der Zeithorizont der angeforderten Verlaufsdaten
  2. Die Länge des Fensters für den Kointegrationstest
  3. Die Länge der überlappenden Fenster 

Wie Sie unten sehen können, wo wir jeden dieser Parameter beschreiben, gibt es immer einen Kompromiss zwischen Aktualität und Genauigkeit, wenn man sie optimiert. Unser Ziel ist es, das richtige Gleichgewicht anzustreben. Zunächst werden wir die RWEC-Metrik für mindestens drei Fenstergrößen backtesten, um zu sehen, welche die beste Anpassung der Portfoliogewichte für den H4-Zeitrahmen bietet. Das ist der Zeitrahmen, an dem wir von Anfang an gearbeitet haben. Unser Bewertungskriterium ist der relative Drawdown.

WARNUNG: Zu diesem Zeitpunkt führen wir ein Backtesting durch, um die optimalen RWEC-Parameter zu finden. Wir konzentrieren uns NICHT auf die Rentabilität der Strategie. Wir haben bereits ein objektives Kriterium zur Bewertung: den relativen Drawdown. Sobald wir die optimalen Parameter für den Korb ausgewählt haben, werden wir sie im Live-Handel und nicht mehr in Backtests verwenden. Es ist wichtig zu verstehen, dass dies das Ziel des Backtests ist.


Die Backtest-Einstellungen

Beginnen wir mit dem üblichen Zeitraum von einem Jahr, d. h. mit ungefähr 252 Handelstagen.
Abb. 15 – Bildschirmfoto der Einstellungen für einen einjährigen Backtest (ca. 252 Handelstage)

Abb. 15. Bildschirmfoto der Einstellungen für einen einjährigen Backtest (ca. 252 Handelstage)


Der RWEC-Zeithorizont (n_bars) sollte mindestens dem Startdatum unseres Backtests plus der Fensterlänge entsprechen, damit wir den Live-Handel besser simulieren können, indem wir gleich zu Beginn des Backtests frühe Aktualisierungen vornehmen. Da wir uns im H4-Zeitrahmen befinden und bei Aktien 2 H4-Balken pro Tag haben, benötigen wir mindestens 504 Balken. Wir werden 90 Balken unserer Fensterlänge hinzufügen.     

def fetch_data(self, symbols, timeframe=mt5.TIMEFRAME_H4, n_bars=504+90):
  """Fetch OHLC data from MT5"""

TIPP: Ich schlage vor, dass Sie Ihre Strategietabelle vor jedem RWEC-Lauf löschen, um den Export zu erleichtern. Denken Sie daran, dass diese Tabelle nur eine Brücke zwischen unserer Datenanalyse und unserem EA ist. Ihr einziger Zweck ist es, aktuelle Strategieparameter für den Live-Handel bereitzustellen. Dabei kann es sich um eine beliebige externe Datenquelle handeln, einschließlich Textdateien und Web-APIs. Wenn Sie diese Daten aufbewahren möchten, können Sie auch eine temporäre Tabelle für Backtests verwenden. Der entscheidende Punkt ist, dass diese Daten verfügbar sind.

Nach dem Ausführen des Skripts rwec2db.py mit den oben genannten n_bars, window=90 und step=22 sollte unsere Strategietabelle diesen Inhalt haben.

Abb. 16 – Bildschirmfoto der Strategietabelle mit RWEC-Vektoren für einen einjährigen Backtest
Abb. 16. Bildschirmfoto der Strategietabelle mit RWEC-Vektoren für einen einjährigen Backtest

Nach dem Exportieren dieser Tabelle, wie im obigen Abschnitt beschrieben, sollten wir eine TSV-Datei wie diese in unserem TERMINAL_DATA_PATH haben.

"tstamp"        "test_id"       "name"  "symbols"       "weights"       "timeframe"     "lookback"
1733155200      1       RWEC_CointNasdaq        INTC,AMD,AVGO,MU        [1.0, 0.19818, -0.385289, 0.29447]      H4      90
1734451200      1       RWEC_CointNasdaq        INTC,AMD,AVGO,MU        [1.0, -1.166557, -0.762914, 3.44909]    H4      90
(...)
1764360000      1       RWEC_CointNasdaq        INTC,AMD,AVGO,MU        [1.0, -0.072521, -0.063501, 0.023864]   H4      90

Beachten Sie, dass das Startdatum unseres Tests der 13. Dezember 2024 ist und der erste von RWEC berechnete Vektorzeitstempel den Zeitstempel 1733155200 hat, was dem 2. Dezember 2024 entspricht. Das ist fast zwei Handelswochen vor dem Starttermin unseres Backtests. Das bedeutet, dass wir mit einem bereits vorhandenen Kointegrationsvektor beginnen, ohne dass veraltete Einträge entfernt werden müssen.

Außerdem hat der zweite Vektor den Zeitstempel 1734451200, was dem 17. Dezember 2024 entspricht, also direkt nach dem Startdatum unseres Backtests. Das bedeutet, dass wir die Portfoliogewichte in einer etwas größeren Frequenz aktualisieren als bei unserer Strategie des Swing-Tradings. Dies ist nicht ideal. Vielleicht können wir ein besseres Aktualisierungsintervall finden.

Achten Sie auf jeden Fall auf diese Frequenzen. Sie werden sich ein wenig ändern, wenn wir anfangen, die Fensterlänge und den Schritt zu ändern. Im Verhältnis zu diesen Häufigkeiten sind die Folgen dieser Veränderungen für unsere Analyse aufschlussreich.

Der Zeithorizont

Der Zeithorizont ist wahrscheinlich der kritischste Parameter, der für das Neugewichten des Live-Handelsportfolios getestet und fein abgestimmt werden muss. Denn sie ist auch der kritischste Parameter bei der Bewertung der Portfoliostabilität mit RWEC. Der Grund dafür ist ziemlich intuitiv: Je länger der Zeithorizont, desto genauer die Bewertung, aber mit geringerer Aktualität, d. h. die resultierende Bewertung ist weniger empfindlich gegenüber der aktuellen Marktstruktur. Andererseits ermöglicht ein kürzerer Zeithorizont zwar eine Bewertung mit mehr Gewicht für die aktuelle Marktstruktur, ist aber auch anfälliger für Störungen. 

Es gibt nicht den einen „optimalen“ Zeithorizont. Sie hängt von der Häufigkeit unserer Handelsstrategie und von der Halbwertszeit der Rückkehr zum Mittelwert unseres Spreads ab. Wenn sich unser Spread innerhalb von Stunden oder Tagen umkehrt, könnte ein kürzerer Zeithorizont wie 20 bis 60 Balken ausreichen, um die aktuelle Beziehungsdynamik zu erfassen. Für die Halbwertszeiten der Rückkehr zum Mittelwert im Bereich von Wochen oder Monaten könnte ein längeres Fenster von 120 bis 250 Balken oder mehr erforderlich sein, um sicherzustellen, dass die Gewichte zuverlässig geschätzt und nicht durch Rauschen beeinflusst werden.

Dies sind die Ergebnisse, die wir bei der Neugewichtung mit RWEC 504/90/22 (n_bars/window/step) erhalten haben:

Abb. 17 – Bildschirmfoto des Backtest-Berichts für die Neugewichtung des Portfolios gemäß RWEC 504/90/22
Abb. 17. Bildschirmfoto des Backtest-Berichts für die Neugewichtung des Portfolios gemäß RWEC 504/90/22

Dies ist die sich daraus ergebende Salden-/Kapitalkurve.

Abb. 18 – Bildschirmfoto der Backtest-Grafik Balance/Equity für die Neugewichtung des Portfolios gemäß RWEC 504/90/22

Abb. 18. Bildschirmfoto der Backtest-Grafik Balance/Equity für die Neugewichtung des Portfolios gemäß RWEC 504/90/22

Die Länge des Fensters für den Kointegrationstest

Sehen wir uns an, was wir mit demselben Zeithorizont und Schritt, aber mit einem 45-Tage-Fenster erreichen können.

def rolling_cointegration(self, data, window=45, step=22):
        """Compute rolling cointegration vectors"""

Im Allgemeinen gelten die oben gemachten Ausführungen zu den Kompromissen bei der Wahl des optimalen Zeithorizonts auch für die Länge des Testfensters. Dieser Parameter ist jedoch direkt an der Berechnung der Eigenvektoren beteiligt und wirkt sich daher unmittelbar auf die Werte der Portfoliogewichte aus. Beim Scoring wirkt sie sich direkt auf die Berechnung der Stabilität der Portfoliogewichte aus. Aber hier, bei der Aktualisierung im Live-Handel, spiegelt sich diese Auswirkung im Portfolioumsatz wider. Ein kürzeres Zeitfenster führt zu einem höheren Umsatz. Sie ist anpassungsfähiger, fängt aber auch mehr kurzfristiges Rauschen ein. Dieses Rauschen führt zu größeren Schwankungen in den geschätzten Eigenvektoren von einem Fenster zum nächsten, was durch den RWEC (Kosinusabstand) gemessen wird. Wenn der RWEC-Schwellenwert häufiger erreicht wird, deutet dies auf einen höheren Portfolioumschlag aufgrund häufiger Neugewichtung hin.

Andererseits führt ein längeres Zeitfenster zu einem geringeren Umsatz, was das Risiko erhöht, ein dysfunktionales Paar zu halten. Ein langes Fenster führt zu sich langsamer verändernden, glatteren Eigenvektoren, aber es bedeutet auch, dass die Strategie langsam reagiert, wenn die Kointegrationsbeziehung zusammenbricht.

Der einzige Weg, die optimale Länge zu finden, sind Backtests. Wir sollten den Zeitrahmen und die mittlere Umkehrung des Korbs zur Halbzeit berücksichtigen. Das objektive Kriterium für die Backtest-Bewertung wird uns bei der Suche nach der optimalen Fensterlänge helfen. Hier verwenden wir den relativen Drawdown als Kriterium, den wir beim Neugewichten mit RWEC 504/45/22 (n_bars/window/step) erhalten haben:

Abb. 19 – Bildschirmfoto des Backtest-Berichts für die Neugewichtung des Portfolios gemäß RWEC 504/45/22
Abb. 19. Bildschirmfoto des Backtest-Berichts für die Neugewichtung des Portfolios gemäß RWEC 504/45/22

Dies ist die resultierende Salden-/Kapitalkurve, wenn wir den Fensterparameter halbieren.

Abb. 20 – Bildschirmfoto der Backtest-Grafik von Saldo/Kapital für die Neugewichtung des Portfolios gemäß RWEC 504/45/22

Abb. 20. Bildschirmfoto der Backtest-Grafik von Saldo/Kapital für die Neugewichtung des Portfolios gemäß RWEC 504/45/22

Die Länge der überlappenden Fenster

Jetzt behalten wir das 90-Tage-Fenster bei, verkürzen aber die Länge der sich überschneidenden Fenster auf eine Handelswoche.

def rolling_cointegration(self, data, window=90, step=5):
        """Compute rolling cointegration vectors"""

Dieser Parameter gibt die Anzahl der Zeitperioden (Handelstage) an, um die das Fenster zwischen aufeinanderfolgenden Eigenvektorberechnungen vorrückt. Die Schrittweite steuert die zeitliche Auflösung des Signals und die Häufigkeit der Neubewertung. Diese Häufigkeit bestimmt, wie oft ein neuer Satz von Portfoliogewichten (Eigenvektoren) berechnet und mit dem vorherigen Satz verglichen wird. 

Wenn wir eine kleine Schrittweite wie 1 Tag wählen, arbeiten wir mit einem hochauflösenden Signal. Wir erhalten fast jeden Tag einen neuen RWEC-Vergleichswert. Auf diese Weise lässt sich die Stabilität der langfristigen Beziehung täglich überprüfen. Wir können einen Zusammenbruch der Kointegrationsbeziehung (eine starke Veränderung des RWEC) sofort erkennen. Wir können fast sofort reagieren, indem wir aus dem Handel aussteigen oder das Neugewichten akzeptieren, das beim Backtest automatisch erfolgt. Der Nachteil ist, dass wir bei einer sehr kleinen Schrittweite eine verhältnismäßig hochfrequente Berechnung der Eigenvektoren benötigen. Dies kann ein Problem für ein großes Portfolio sein, ist aber wahrscheinlich kein Problem für den durchschnittlichen Einzelhändler (auf den wir uns hier konzentrieren), der normalerweise nicht mit großen Portfolios handelt.

Eine größere Schrittweite, etwa ein Handelsmonat (~22 Tage), bedeutet ein Signal mit relativ geringer Auflösung. Wir werden die Gewichtung des Portfolios und das RWEC-Signal einmal im Monat neu bewerten. Dadurch verringert sich der Rechenaufwand erheblich, aber das Signal verliert an Aktualität. Wenn die Kointegration am Tag 1 bricht, werden wir die Instabilität oder die Notwendigkeit einer Neugewichtung erst am Tag 20 feststellen. Diese Verzögerung kann in einem schnelllebigen Markt zu erheblichen Verlusten führen.

Die Schrittweite steht also in direktem Zusammenhang mit der Häufigkeit, mit der wir die Portfoliogewichte neu ausbalancieren werden. Wir können uns dafür entscheiden, die Gewichtung des Portfolios immer auf der Grundlage der aktuellsten Marktdaten vorzunehmen, wobei die Absicherung so nah wie möglich am Optimum liegt. In diesem Fall müssen wir mit höheren Transaktionskosten (Provisionen, Slippage) rechnen, die die geringen Margen, die wir normalerweise bei statistischer Arbitrage erzielen, beeinträchtigen können. Oder wir können uns für einen geringeren Umsatz und niedrigere Transaktionskosten entscheiden. Unser Korb wird für die Dauer des Schritts mit veralteten Gewichten bestückt bleiben. Auf volatilen Märkten werden wir unser Risiko erheblich erhöhen.

Abb. 21 – Bildschirmfoto des Backtest-Berichts für die Neugewichtung des Portfolios gemäß RWEC 504/90/5

Abb. 21. Bildschirmfoto des Backtest-Berichts für die Neugewichtung des Portfolios gemäß RWEC 504/90/5

Abb. 22 – Bildschirmfoto der Backtest-Grafik Balance/Equity für die Neugewichtung des Portfolios gemäß RWEC 504/90/5

Abb. 22. Bildschirmfoto der Backtest-Grafik von Saldo/Kapital für die Neugewichtung des Portfolios gemäß RWEC 504/90/5

Vergleichende Tabelle der relativen Absenkungen für denselben Zeithorizont mit unterschiedlicher RWEC-Fensterlänge und -Stufe.

n_bars Fensterlänge Überlappungsstufe resultierende Datenpunkte relativer Drawdown
504+90
90 22 23 18.18 %
504+90
45 22 25 36.08 %
504+90
90 5 101 85.11 %

Tabelle 1. Vergleich der relativen Drawdown-Metrik der Backtests für verschiedene RWEC-Fensterlängen und Schritte

Hier soll vorrangig gezeigt werden, wie sich die Hauptparameter der einzelnen RWEC auf die Neugewichtung auswirken. Wenn Sie anfangen, mit verschiedenen Körben zu experimentieren, wird Ihnen schnell klar, dass Sie nur durch ausgiebige Tests den optimalen Parametersatz finden können.

Doch selbst dieser einfache Vergleich zeigt uns, dass sich unsere Ergebnisse um hundert Prozent verschlechtern, wenn wir unsere Fensterlänge auf die Hälfte reduzieren, nämlich von 18,08 % auf 36,08 % des relativen Drawdowns, was uns deutlich zeigt, dass die erste Option besser zu unserem Korb passt.

Als wir unseren Schritt von einem Handelsmonat (22 Tage) auf eine Handelswoche (5 Tage) reduzierten, verschlechterten sich unsere Ergebnisse drastisch von 18,8 % auf 85,11 %. Da wir jedoch fast fünfmal so viele Datenpunkte haben (von 23 auf 101), haben wir auch einen Fehler in der Kointegration festgestellt. In diesem letzten Durchlauf zeigt das Ergebnisdiagramm einen strukturellen Bruch, der mit dem größeren Rolling-Fenster-Schritt nicht erkannt wurde. 

Ich hoffe, dass diese schnellen Auswertungen mit drei RWEC-Parameterkombinationen Ihnen sowohl numerische als auch visuelle Beweise für die Auswirkungen liefern können, die jede von ihnen in unserem Backtest verursachen kann, und auch die Rolle des überlappenden Fensterschritts bei der frühzeitigen Erkennung von Strukturbrüchen verstärken können.


Schlussfolgerung

In diesem Artikel haben wir eine mögliche Methode für die Backtests der Aktualisierung der Portfoliogewichte eines Korbs kointegrierter Aktien vorgestellt. Wir haben gezeigt, dass wir durch das Laden von CSV/TSV-Daten in eine generische HashMap-Sammlung und deren sequenzielles Lesen mit Backtest-ausgerichteten Zeitstempeln Live-Handelsaktualisierungen simulieren können.

Die zur Berechnung der neuen Gewichte verwendete Methode ist der Rolling Windows Eigenvektorvergleich (RWEC). Wir haben eine kurze Beschreibung der relativen Auswirkungen der einzelnen Parameter auf die Backtest-Ergebnisse gegeben: den Zeithorizont oder Backtest-Zeitraum, die Zeitspanne für die Berechnung des Kointegrationsvektors oder die Fensterlänge und den Zeitraum der überlappenden Fenster oder den Vorwärtsschritt. Für jeden dieser Parameter haben wir die Ergebnisse eines Backtests vorgestellt, um zu zeigen, wie ihre Analyse uns bei der Auswahl der optimalen Parameter für den Live-Handel helfen kann.

Wir stellen ein Python-Skript für die Ausführung des RWEC und die Speicherung der Ergebnisse in einer speziellen Datenbanktabelle zur Verfügung, die als CSV/TSV exportiert werden kann, sowie eine Kopfdatei mit den MQL-Funktionen, die zum Lesen der Daten im Tester erforderlich sind.

Dateiname Beschreibung
 Experts\StatArb\CointNasdaq.mq5 Beispiel der Expert Advisor Haupt-MQL5-Datei
 Include\StatArb\CointNasdaq.mqh Beispiel der Expert Advisor Haupt-MQL5-Header-Datei
 Include\StatArb\TestHelper.mqh Beispiel der Expert Advisor-Testhilfedatei
 Files\StatArb\strategy_*.csv Aus der Datenbank exportierte TSV-Dateien, die in dem Artikel verwendet werden
 rwec2db Python-Skript zur Ausführung von RWEC und zur Speicherung der Ergebnisse in der integrierten SQLite-Datenbank 
 CointNasdaq.INTC.H4.20241213_20251213.000 Im Artikel verwendete Backtest-Einstellungen

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/20657

Beigefügte Dateien |
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.
Datenbanken sind einfach (Teil 1): Ein leichtes ORM-Framework für MQL5 unter Verwendung von SQLite Datenbanken sind einfach (Teil 1): Ein leichtes ORM-Framework für MQL5 unter Verwendung von SQLite
Dieser Artikel stellt einen strukturierten Weg zur Verwaltung von SQLite-Daten in MQL5 durch eine ORM-Schicht für MetaTrader 5 vor. Es führt Kernklassen für die Entitätsmodellierung und den Datenbankzugriff ein, eine flüssige CRUD-API, Reflection Hooks für OnGet/OnSet und Makros zur schnellen Definition von Modellen. Der praxisnahe Code zeigt das Erstellen von Tabellen, das Binden von Feldern, Einfügen, Aktualisieren, Abfragen und Löschen von Datensätzen. Entwickler erhalten wiederverwendbare, typsichere Komponenten, die wiederholtes SQL auf ein Minimum reduzieren.
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.
Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 5): WaveTrend Crossover Evolution mit einer Leinwand für Nebelverläufe, Signalblasen und Risikomanagement Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 5): WaveTrend Crossover Evolution mit einer Leinwand für Nebelverläufe, Signalblasen und Risikomanagement
In diesem Artikel verbessern wir den Indikator Smart WaveTrend Crossover in MQL5 durch die Integration von Canvas-basiertem Zeichnen für Überlagerung mit Nebelverläufen, Signalkästchen, die Ausbrüche erkennen, und anpassbaren Kauf-/Verkaufsblasen oder Dreiecken für visuelle Warnungen. Wir integrieren Funktionen für das Risikomanagement mit dynamischen Take-Profit- und Stop-Loss-Niveaus, die über Kerzenmultiplikatoren oder Prozentsätze berechnet und in Form von Linien und einer Tabelle angezeigt werden, sowie Optionen für Trendfilterung und Box-Erweiterungen.