Statistische Arbitrage durch kointegrierte Aktien (Teil 9): Backtests, Portfolio-Gewichtungen, Updates
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.
Wenn Sie dieses Skript ausführen, sollten Sie eine Ausgabe wie diese sehen:

Abb. 2. Bildschirmfoto mit der erwarteten Ausgabe von rwec2db.py
Wenn kein Kointegrationsvektor gefunden wird, wird eine entsprechende Meldung ausgegeben.

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 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
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
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 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
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
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, 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; }

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
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 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, 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:
- Der Zeithorizont der angeforderten Verlaufsdaten
- Die Länge des Fensters für den Kointegrationstest
- 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)
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
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
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
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
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
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. 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
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
Datenbanken sind einfach (Teil 1): Ein leichtes ORM-Framework für MQL5 unter Verwendung von SQLite
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 5): WaveTrend Crossover Evolution mit einer Leinwand für Nebelverläufe, Signalblasen und Risikomanagement
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.