English 日本語
preview
Statistische Arbitrage durch kointegrierte Aktien (Teil 3): Datenbank-Einrichtung

Statistische Arbitrage durch kointegrierte Aktien (Teil 3): Datenbank-Einrichtung

MetaTrader 5Handelssysteme |
111 0
Jocimar Lopes
Jocimar Lopes

Einführung

Im letzten Artikel dieser Serie (Teil 2) haben wir die Backtests einer statistischen Arbitragestrategie durchgeführt, die aus einem Korb kointegrierter Aktien aus dem Mikroprozessor-Sektor (Nasdaq-Aktien) bestand. Wir haben begonnen, aus Hunderten von Aktiensymbolen diejenigen herauszufiltern, die am stärksten mit Nvidia korrelieren. Anschließend haben wir die gefilterte Gruppe mit dem Johansen-Test auf Kointegration und mit dem ADF- und dem KPSS-Test auf Stationarität des Spreads geprüft und schließlich die relativen Portfoliogewichte durch Extraktion des Johansen-Eigenvektors für den ersten Rang ermittelt. Die Backtest-Ergebnisse waren vielversprechend.

Es kann jedoch sein, dass zwei oder mehr Vermögenswerte in den letzten zwei Jahren kointegriert waren und ab morgen nicht mehr kointegriert sind. Das heißt, es gibt keine Garantie, dass ein kointegriertes Paar oder eine Gruppe von Vermögenswerten kointegriert bleibt. Änderungen in der Unternehmensführung, das makroökonomische Szenario oder sektorspezifische Veränderungen können die Fundamentaldaten beeinflussen, die ursprünglich für die Kointegration der Vermögenswerte verantwortlich waren. Und vice versa. Vermögenswerte, die zuvor nicht kointegriert waren, können aus denselben Gründen in der nächsten Minute einen kointegrierten Pfad einschlagen. Der Markt ist „ein Rätsel, das sich ständig verändert“. Wir müssen mit diesem Wandel fertig werden. [PALOMAR, 2025]

Ein Korb kointegrierter Aktien ändert seine relativen Portfoliogewichte fast kontinuierlich, und die Portfoliogewichte bestimmen nicht nur das Volumen (Menge) unserer Aufträge, sondern auch ihre Richtung (Kauf oder Verkauf). Wir müssen also auch mit diesem Wandel fertig werden. Während die Kointegration eine eher langfristige Beziehung darstellt, ändern sich die Portfoliogewichte ständig. Wir müssen also häufiger nach ihnen suchen und unser Modell aktualisieren, sobald sie sich ändern. Wenn wir feststellen, dass unser Modell veraltet ist, müssen wir sofort handeln; wir wollen eine sofortige Ersetzung des veralteten Modells.

Unser Expert Advisor muss in Echtzeit wissen, ob die von uns verwendeten Portfolio-Gewichtungen noch gelten oder sich geändert haben. Wenn sie sich geändert haben, muss der EA so schnell wie möglich über die neue Gewichtung des Portfolios informiert werden. Außerdem muss unser EA wissen, ob das Modell selbst gültig bleibt. Ist dies nicht der Fall, sollte der EA darüber informiert werden, welche Vermögenswerte ersetzt werden müssen, und die Rotation muss so schnell wie möglich im aktiven Portfolio durchgeführt werden.

Wir haben die Metatrader 5 Python-Integration und die professionell entwickelten statistischen Funktionen aus der statsmodels-Bibliothek verwendet, aber bis jetzt haben wir nur mit Echtzeitdaten gearbeitet und die Kurse (die Preisdaten) heruntergeladen, wenn wir sie brauchten. Dieser Ansatz ist wegen seiner Einfachheit in der Sondierungsphase nützlich. Wenn wir jedoch unser Portfolio umstellen, unsere Modelle oder die Portfoliogewichte aktualisieren wollen, sollten wir über Datenpersistenz nachdenken. Das heißt, wir müssen anfangen, über die Speicherung unserer Daten in einer Datenbank nachzudenken, weil es nicht praktisch ist, die Daten jedes Mal herunterzuladen, wenn wir sie brauchen. Darüber hinaus müssen wir möglicherweise nach Beziehungen zwischen verschiedenen Anlageklassen und zwischen Symbolen suchen, die bei unseren ersten Kointegrationstests nicht berücksichtigt wurden.

Eine qualitativ hochwertige, skalierbare und metadatenreiche Datenbank ist das Herzstück jeder ernsthaften statistischen Arbitrage-Bemühung. Unter Berücksichtigung der Tatsache, dass Datenbankdesign eine sehr eigenwillige Aufgabe ist, in dem Sinne, dass eine gute Datenbank diejenige ist, die zu den Anforderungen eines jeden Unternehmens passt, werden wir in diesem Artikel einen möglichen Ansatz für den Aufbau unserer auf statistische Arbitrage ausgerichteten Datenbank sehen.


Welche Fragen muss unsere Datenbank beantworten?

Bei unserer Suche nach einem „statistischen Arbitrage-Framework für den armen Mann“, d. h. ein System, das für den durchschnittlichen Einzelhändler mit einem normalen Consumer-Notebook und einer durchschnittlichen Netzwerkbandbreite geeignet ist, sehen wir uns mit mehreren Herausforderungen konfrontiert, die mit der mangelnden Spezialisierung in den erforderlichen Bereichen, wie Statistik und Softwareentwicklung, zusammenhängen. Datenbankdesign ist KEINE Ausnahme in dieser Liste der erforderlichen Fachkenntnisse. Datenbankdesign ist an sich schon ein großes Feld. Man kann ganze Bücher über Datenbankdesign schreiben, ohne das Thema zu erschöpfen. Die ideale Lösung wäre es, einen spezialisierten Fachmann, möglicherweise mehr als eine Person, mit der Entwicklung, Implementierung und Pflege unserer Datenbank zu beauftragen.

Aber da wir dieses Stat-Arb Framework für den durchschnittlichen Einzelhändler entwickeln, müssen wir mit dem arbeiten, was wir haben, indem wir in Büchern und spezialisierten Internetforen und -kanälen recherchieren, von erfahrenen Fachleuten lernen, durch Versuch und Irrtum lernen, Experimente machen, Risiken eingehen und bereit sein, unser Design zu ändern, wenn es sich als unzureichend für die anstehende Aufgabe erweist. Wir müssen flexibel sein, um Dinge zu ändern, und wir müssen klein anfangen und von unten nach oben arbeiten, anstatt von oben nach unten, um ein Over-Engineering zu vermeiden.

Am Ende des Tages sollte unsere Datenbank eine ganz einfache Frage beantworten: Was sollten wir jetzt handeln, um die größtmögliche Rendite zu erzielen?

In Anlehnung an den letzten Artikel dieser Serie, in dem wir die Richtung und das Volumen unserer Aufträge auf der Grundlage der Gewichtung des Aktienkorbs definiert haben, könnte eine mögliche Antwort etwa so lauten:

Symbole Gewichte Zeitrahmen
 

"MU", "NVDA", "MPWR", "MCHP"

 2.699439, 1.000000, -1.877447, -2.505294  D1

Tabelle 1 – Beispiel für die erwartete Antwort auf eine Abfrage zur Aktualisierung eines Echtzeitmodells

Wenn unsere Datenbank uns diese recht einfachen Informationen liefern kann, die in angemessenen Abständen aktualisiert werden, haben wir alles, was wir brauchen, um kontinuierlich auf dem bestmöglichen Niveau zu handeln.



Datenbank-Updates als Service

Bis jetzt haben wir unsere Datenanalyse mit Hilfe von Echtzeit-Kursen aus dem Metatrader 5 Terminal über Python-Code durchgeführt (technisch gesehen haben wir die meiste Zeit die von der zugrunde liegenden Terminal-Engine gespeicherten Kurse verwendet). Sobald die Symbole und Portfoliogewichte definiert waren, haben wir unseren Expert Advisor manuell mit dem neuen Symbol und/oder den neuen Portfoliogewichten aktualisiert.

Von nun an werden wir unsere Datenanalyse vom Terminal entkoppeln und die in unserer Datenbank gespeicherten Daten verwenden, um unseren Expert Advisor zu aktualisieren, sobald wir neue Portfoliogewichte haben, den Handel zu stoppen, wenn die Kointegrationsbeziehung verloren geht oder eine andere Gruppe von Symbolen als vielversprechender erachtet wird. Das heißt, wir wollen unser Marktengagement für jedes Symbol verbessern, indem wir die Portfoliogewichte in Echtzeit aktualisieren und/oder unser Portfolio jedes Mal drehen, wenn unsere Datenanalyse dies empfiehlt.

Um die Datenbank zu aktualisieren, werden wir einen Metatrader 5 Service implementieren.

In der Metatrader 5-Dokumentation erfahren wir, dass 

  • Dienste nicht an einen bestimmten Chart gebunden sind,

  • Dienste direkt nach dem Starten des Terminals geladen werden, wenn sie zum Zeitpunkt des Herunterfahrens des Terminals gestartet wurden,

  • Dienste in einem eigenen Thread laufen.

Daraus können wir schließen, dass ein Dienst die ideale Methode ist, um unsere Datenbank aktuell zu halten. Wenn er läuft, wenn wir eine Handelssitzung beenden und das Terminal schließen, wird er fortgesetzt, sobald wir das Terminal für eine neue Sitzung neu starten, unabhängig von einem Chart/Symbol, das wir zu diesem Zeitpunkt verwenden. Da er auf einem eigenen Thread läuft, wird er außerdem nicht von anderen Diensten, Indikatoren, Skripten oder Expert Advisors, die wir möglicherweise ausführen, beeinflusst oder beeinträchtigt.

Unser Arbeitsablauf wird also folgendermaßen aussehen:

  1. Die gesamte Datenanalyse erfolgt in Python, außerhalb der Metatrader 5-Umgebung. Um die Analyse durchzuführen, laden wir historische Daten herunter und fügen sie in die Datenbank ein.
  2. Jedes Mal, wenn wir unser aktives Portfolio ändern, indem wir ein Symbol hinzufügen oder entfernen, aktualisieren wir die Service-Eingabeparameter mit einem Array von Symbolen und einem Array von Zeitfenstern.
  3. Jedes Mal, wenn wir unser aktives Portfolio ändern und ein Symbol hinzufügen oder entfernen, aktualisieren wir die Eingabeparameter des Expert Advisors.

Wir werden die Aktualisierungen in den Schritten 2 und 3 zunächst manuell durchführen. Später werden wir es automatisieren.


Einrichtung der Datenbank

Bei der Recherche für diesen Artikel wurde ich daran erinnert, dass das ideale Werkzeug für diese Aufgabe eine auf Zeitreihen spezialisierte, spaltenorientierte Datenbank ist. Auf dem Markt gibt es viele Produkte, die diese Anforderung erfüllen, sowohl kostenpflichtige als auch kostenlose, proprietäre und Open-Source-Alternativen. Sie sollen hochspezialisierte Arbeitslasten unterstützen, eine riesige Datenmenge in einer Reaktionszeit von weniger als einer Sekunde, sowohl bei der Datenaufnahme als auch bei der Beantwortung von Abfragen in Echtzeit.

Aber unser Schwerpunkt liegt hier nicht auf dem Umfang. Unser Hauptaugenmerk liegt auf der Einfachheit und Anwendbarkeit durch eine Einzelperson, nicht durch ein Team von hochqualifizierten professionellen Datenbankadministratoren (DBAs) und Designern für das Zeitserien-Datenmanagement. Wir werden also mit der einfachsten Lösung beginnen, wobei wir uns ihrer Grenzen bewusst sind und bedenken, dass sich das System in Zukunft je nach Bedarf weiterentwickeln wird.

Wir werden mit der in Metatrader 5 integrierten SQLite-Datenbank beginnen. Es gibt eine Fülle von Informationen darüber, wie man die integrierte SQLite-Datenbank in der Metatrader 5-Umgebung erstellt und verwendet. Sie können es finden:

Wenn Sie ernsthaft planen, mit der integrierten SQLite-Datenbank zu arbeiten, sollten Sie sich mit all diesen Links eingehend beschäftigen. Im Folgenden finden Sie eine Zusammenfassung der Schritte für unseren sehr speziellen Anwendungsfall in diesem Artikel, mit einigen Anmerkungen zu den Gründen für die Auswahl.


Das Schema

Im Anhang zu diesem Artikel finden Sie die Datei db-setup.mq5, die ein MQL5-Skript ist. Es akzeptiert als Eingabeparameter den Dateinamen der SQLite-Datenbank und den Dateinamen des Datenbankschemas. Diese Parameter sind standardmäßig auf statarb-0.1.db bzw. schema-0.1.sql eingestellt. 

WARNUNG: Es wird dringend empfohlen, dass Sie Ihre Datenbankschemadatei unter Versionskontrolle halten und sie nicht in Ihrem Dateisystem duplizieren. Metatrader 5 bietet ein robustes Versions-Kontrollsystem, das bereits in die Plattform integriert ist.

Wenn Sie dieses Skript mit den Standard-Eingabeparametern starten, wird eine SQLite-Datenbank im Ordner MQL5/Files/StatArb Ihres Terminals (NICHT im allgemeinen Ordner) erstellt, mit allen unten kommentierten Tabellen und Feldern. Das Skript erzeugt auch eine Datei output.sql in Ihrem MQL/Files-Ordner, die nur zu Debugging-Zwecken dient. Sollten Sie Probleme haben, können Sie in dieser Datei nachsehen, wie Ihr System die Schemadatei liest. Wenn alles gut geht, können Sie diese Datei sicher löschen.

Alternativ können Sie die Datenbank auch auf andere Weise erstellen und die Schemadatei aus der Datenbank auf einem beliebigen SQLite3-Client, auf der Metaeditor-Nutzeroberfläche, in der Windows PowerShell oder auf der SQLite3-Befehlszeile lesen. Ich empfehle Ihnen, zumindest beim ersten Mal die Datenbank zu erstellen, indem Sie das beigefügte Skript ausführen. Sie können Ihr Verfahren später jederzeit anpassen.

Datenbankschema

Unser anfängliches Datenbankschema besteht aus nur vier Tabellen, von denen zwei Platzhalter für die nächsten Schritte sind. Das heißt, dass wir zu diesem Zeitpunkt nur die Tabellen „symbol“ und „market_data“ verwenden.

Abbildung 1 zeigt das Entity-Relationship-Diagramm (ERD) dieses ursprünglichen Schemas.

Abb.1 – Entity-Relationship-Diagramm (ERD) für das ursprüngliche Datenbankschema

Abb. 1 – Das ursprüngliche Datenbankschema ERD (Entity-Relationship Diagram)

Die Tabelle „corporate_event“ ist überraschenderweise dazu gedacht, Ereignisse im Zusammenhang mit unseren Portfoliounternehmen zu speichern, wie z. B. Dividendenausschüttungen, Aktiensplits, Aktienrückkäufe, Fusionen und andere. Wir werden uns damit vorerst nicht befassen.

In der Tabelle „trade“ werden unsere Geschäfte (Deals) gespeichert. Durch die Erfassung dieser Daten erhalten wir eine einzigartige Datensammlung zur Aggregation und Analyse. Wir werden es erst verwenden, wenn wir mit dem Handel beginnen.

In der Tabelle „market_data“ werden unsere OHLC-Daten mit allen MqlRates-Feldern gespeichert. Sie hat einen zusammengesetzten Primärschlüssel (symbol_id, timeframe und timestamp), um sicherzustellen, dass jeder Eintrag eindeutig ist. Die Tabelle „market_data“ ist über einen Fremdschlüssel mit der Tabelle „symbol“ verbunden.

Wie Sie sehen können, ist dies die einzige Tabelle, die einen zusammengesetzten Primärschlüssel verwendet. Alle anderen Tabellen verwenden als Primärschlüssel den als INTEGER gespeicherten Zeitstempel. Für diese Entscheidung gibt es einen Grund. Laut der Dokumentation von SQLite3,

„Die Daten für rowid-Tabellen werden als B-Baum-Struktur gespeichert, die einen Eintrag für jede Tabellenzeile enthält, wobei der rowid-Wert als Schlüssel verwendet wird. Das bedeutet, dass das Abrufen oder Sortieren von Datensätzen nach rowid schnell geht. Die Suche nach einem Datensatz mit einer bestimmten rowid oder nach allen Datensätzen mit rowids innerhalb eines bestimmten Bereichs ist etwa doppelt so schnell wie eine ähnliche Suche durch Angabe eines anderen PRIMARY KEY oder indizierten Werts.

(...) wenn eine rowid-Tabelle einen Primärschlüssel hat, der aus einer einzigen Spalte besteht und der deklarierte Typ dieser Spalte „INTEGER“ in einer beliebigen Mischung aus Groß- und Kleinbuchstaben ist, dann wird die Spalte ein Alias für die rowid. Eine solche Spalte wird gewöhnlich als „ganzzahliger Primärschlüssel“ bezeichnet.

(...) „Sie können Abfragen 'etwa doppelt so schnell' erhalten, wenn Sie einen INTEGER als Primärschlüssel verwenden. Unix-Epochen-Zeitstempel können als INTEGER in SQLite3-Datenbanken eingefügt werden.“

(...) „SQLite speichert Ganzzahlwerte im 64-Bit-Zweierkomplement-Format¹. Dies ergibt einen Speicherbereich von (einschließlich) -9223372036854775808 bis +9223372036854775807. Ganzzahlen innerhalb dieses Bereichs sind exakt. (SQLite3 docs)”

Wir können also unsere Datumszeiten aus der Zeichenkette in Unix-Epochen-Zeitstempel ersetzen und diese als Primärschlüssel einfügen, um einen Geschwindigkeitsschub zu erhalten. 🙂

Die nachstehenden Tabellen enthalten die vollständige Schemadokumentation (Datenwörterbuch) als Referenz für unser Team und uns selbst in der Zukunft.

Tabelle: „symbol“

Speichert Metadaten über gehandelte oder verfolgte Finanzinstrumente.

Feld Datentyp Null Schlüssel Beschreibung
symbol_id INTEGER NO PK Eindeutige Kennung für jedes Finanzinstrument.
ticker TEXT(≤10) NO
Tickersymbol des Vermögenswerts (z. B. „AAPL“, „MSFT“).
exchange TEXT(≤50) NO
Börse, an der der Vermögenswert notiert ist (z. B. „NASDAQ“, „NYSE“).
asset_type TEXT(≤50) YES
Art des Vermögenswerts (z. B. „Aktien“, „ETF“, „FX“, „Krypto“).
sector TEXT(≤50) YES

Klassifizierung des Wirtschaftssektors (z. B. „Technologie“, „Gesundheitswesen“).
industry TEXT(≤50) YES

Klassifizierung der Industrie innerhalb eines Sektors.
 currency TEXT(≤50) YES   Währung, auf die der Vermögenswert lautet (z. B. „EUR“, „USD“).

Tabelle 2 – Beschreibung des Datenwörterbuchs der Tabelle „Symbol“ (v0.1)

Tabelle: corporate_event

Erfasst Ereignisse, die sich auf Vermögenswerte auswirken, wie z. B. Dividenden, Aktiensplits oder Gewinnankündigungen.

Feld Datentyp Null Schlüssel Beschreibung Beispiel
tstamp INTEGER NO PK Unix-Zeitstempel, wann das Ereignis wirksam wird. 1678905600
event_type TEXT ENUM {'dividend', 'split', 'earnings'} NO
Art der Kapitalmaßnahme. “dividend“
event_value REAL YES
Numerischer Wert des Ereignisses:- Dividendenbetrag pro Aktie- Splitverhältnis- Gewinn pro Aktie (EPS). 0.85, 2.0, 1.35
details TEXT(≤255) YES
Zusätzliche Hinweise oder Kontext. "Q2 dividend payout"
 symbol_id  INTEGER NO   FK  Verweis auf Symbol (symbol_id); verknüpft Ereignis mit Vermögenswert.  1

Tabelle 3 – Beschreibung des Datenwörterbuchs der Tabelle „corporate_event“ (v0.1)

Tabelle: market_data

Speichert OHLCV (Open, High, Low, Close, Volume) und zugehörige Zeitreihendaten für Vermögenswerte.

Feld Datentyp Null Schlüssel Beschreibung Beispiel
tstamp INTEGER NO PK*
Unix-Zeitstempel von bar/candle. 1678905600
timeframe TEXT ENUM {M1,M2,M3,M4,M5,M6, M10,M12,M15,M20,M30,H1,H2,H3, H4,H6,H8,H12,D1,W1,MN1} NO
PK*
Granularität der Zeitreihendaten. "M5", "D1"
price_open REAL NO

Eröffnungspreis zu Beginn des Balkens. 145.20
price_high REAL
NO

Das Hoch des Balkens. 146.00
price_low REAL
NO

Dat Tief des Balkens. 144.80
price_close REAL
NO

Schlusskurs am Ende des Balkens. 145.75
tick_volume INTEGER
YES
Anzahl der Ticks (Kursaktualisierungen) innerhalb des Zeitrahmens. 200
real_volume INTEGER
YES

Anzahl der gehandelten Einheiten/Kontrakte (falls verfügbar). 15000
spread REAL
YES

Durchschnittliche oder Momentaufnahme der Geld-Brief-Spanne während des Balkens. 0.02
symbol_id  INTEGER
 NO PK*, FK  Verweis auf Symbol(symbol_id).  1

Tabelle 4 – Beschreibung des Datenwörterbuchs der Tabelle „market_data“ (v0.1)

Tabelle: Handel

Verfolgt Live- oder simulierte Handelsgeschäfte der Strategien.

Feld Datentyp Null Schlüssel Beschreibung Beispiel
tstamp INTEGER
NO PK Unix-Zeitstempel der Handelsausführung. 1678905600
ticket INTEGER
NO

Ticketnummer des Handelsgeschäfts / Auftrags-ID. 20230001
side TEXT ENUM {'buy', 'sell'} NO

Richtung des Handels. "buy"
quantity INTEGER (>0)
NO

Anzahl der gehandelten Aktien/Kontrakte. 100
price
NO

Ausgeführter Preis. 145.50
strategy
YES
Kennung für die Handelsstrategie, die den Handel erzeugt. "StatArb_Pairs"
symbol_id   INTEGER  NO  FK  Verweis auf Symbol(symbol_id).  1

Tabelle 5 – Beschreibung des Datenwörterbuchs der Tabelle „trade“ (v0.1)

STRICT

Wenn Sie sich das Schema der Datei ansehen, werden Sie feststellen, dass alle Tabellen STRICT-Tabellen sind.

CREATE TABLE symbol(
    symbol_id INTEGER PRIMARY KEY,
    ticker TEXT CHECK(LENGTH(ticker) <= 10) NOT NULL,
    exchange TEXT CHECK(LENGTH(exchange) <= 50) NOT NULL,
    asset_type TEXT CHECK(LENGTH(asset_type) <= 50),
    sector TEXT CHECK(LENGTH(sector) <= 50),
    industry TEXT CHECK(LENGTH(industry) <= 50),
    currency TEXT CHECK(LENGTH(currency) <= 10)
) STRICT;

Das bedeutet, dass wir uns für eine strenge Typisierung in unseren Tabellen entscheiden, statt für die bequeme SQLite-Typaffinität. Es ist eine Entscheidung, von der wir glauben, dass sie später Probleme vermeiden kann.

„Wenn in einer CREATE TABLE-Anweisung das Schlüsselwort „STRICT“ für die Tabellenoption am Ende, nach dem abschließenden „)“, hinzugefügt wird, dann gelten für diese Tabelle strenge Schreibregeln.“ (SQLite docs).

PRÜFEN DER LÄNGE

Außerdem benötigen wir eine Prüfung der Länge mehrerer TEXT-Felder. Das liegt daran, dass SQLite keine feste Länge erzwingt oder Zeichenketten auf der Grundlage des in CHAR(n) oder VARCHAR(n) angegebenen (n) abschneidet, wie es in anderen RDBMS üblich ist.

„Beachten Sie, dass numerische Argumente in Klammern, die dem Typnamen folgen (z. B: „VARCHAR(255)“) werden von SQLite ignoriert – SQLite hat keine Längenbeschränkungen (außer der großen globalen SQLITE_MAX_LENGTH Grenze) für die Länge von Strings, BLOBs oder numerischen Werten.“ (SQLite docs)
Wir setzen ein Limit durch, um Probleme mit unverhältnismäßig langen Strings aus unbekannten Quellen zu vermeiden.

Was ist mit Indizes?

Sie fragen sich vielleicht, warum wir keine Indizes erstellt haben. Das werden wir tun, sobald wir mit den Abfragen beginnen, damit wir sicher wissen, wo sie benötigt werden.


Erste Dateneingabe

Es wird erwartet, dass die Datenbank transparent gefüllt wird, während wir unsere Datenanalyse von der MQL5-Python-Integration aus durchführen. Der Einfachheit halber ist jedoch ein Python-Skript beigefügt (db_store_quotes.ipynb), das Ihnen als Hilfsmittel dient, um die Kurse einer Liste von Symbolen, eines bestimmten Zeitrahmens und eines ausgewählten Zeitintervalls zu speichern. Von nun an werden wir unsere Datenanalyse (Korrelations-, Kointegrations- und Stationaritätstests) mit diesen gespeicherten Daten durchführen.

Abbildung 2 – Tabelle „Symbol“ nach dem ersten Einfügen von Daten mithilfe des Python-Skripts

Abbildung 2 – Tabelle „Symbol“ nach der ersten Dateneingabe mit dem Python-Skript

Wie Sie sehen, ist der größte Teil der Metadaten in „symbol“ „UNKNOWN“. Das liegt daran, dass die Python-Funktionen für SymbolInfo nicht alle in der MQL5-API verfügbaren Symbol-Metadaten abdecken. Diese Lücken werden wir später füllen.

   # Insert new symbol
    cursor.execute("""
    INSERT INTO Symbol (ticker, exchange, asset_type, sector, industry, currency)
    VALUES (?, ?, ?, ?, ?, ?)
    """, (
        mt5_symbol,
        # some of this data will be filled by the MQL5 DB Update Service
        # because some of them are not provided by the Python MT5 API 
        symbol_info.exchange or 'UNKNOWN',
        symbol_info.asset_class or 'UNKNOWN',
        symbol_info.sector or 'UNKNOWN',
        symbol_info.industry or 'UNKNOWN',
        symbol_info.currency_profit or 'UNKNOWN'
    ))

Der Datenbankpfad sollte als Umgebungsvariable übergeben werden, und wir verwenden das Modul python-dotenv, um diese Variable zu laden. Dies sollte Probleme mit Terminal- und/oder PowerShell-Umgebungsvariablen vermeiden, die von Ihrem Editor nicht erkannt werden.

Sie finden das Jupyter-Notizbuch, das die Erweiterung python-dotenv und die entsprechende *.env-Datei lädt, gleich zu Beginn des Skripts.

%load_ext dotenv
%dotenv .env

Ein Beispiel für eine *.env-Datei ist diesem Artikel ebenfalls beigefügt.

# keep this file at the root of your project 
# or in the same folder of the Python script that uses it
STATARB_DB_PATH="your/db/path/here"

Der Hauptaufruf von db_store_quotes.ipynb befindet sich am Ende des Skripts.

symbols = ['MPWR', 'AMAT', 'MU']  # Symbols from Market Watch
timeframe = mt5.TIMEFRAME_M5    # 5-minute timeframe
start_date = '2024-02-01'
end_date = '2024-03-31'
db_path = os.getenv('STATARB_DB_PATH')  # Path to your SQLite database

if db_path is None:
        print("Error: STATARB_DB_PATH environment variable is not set.")
else:
        print("db_path: " + db_path)
# Download historical quotes and store them in the database
        download_mt5_historical_quotes(symbols, timeframe, start_date, end_date, db_path)


Aktualisierungen

Der Kern unserer Datenbankpflege für die automatische Modellaktualisierung und Portfoliorotation liegt auf unserem im „Hintergrund“ laufenden MQL5 Dienst. Da sich unsere Datenbank weiterentwickelt, muss auch dieser Dienst aktualisiert werden.

Der Dienst stellt eine Verbindung zu einer lokalen SQLite-Datenbankdatei her (oder erstellt diese). 

WARNUNG: Wenn Sie eine brandneue Datenbank mit dem Datenbank-Update-Service erstellen, denken Sie daran, die Datenbank mit dem db_setup-Skript zu initialisieren, wie oben erwähnt.

Dann richtet der Dienst die erforderlichen Einschränkungen für die Datenintegrität ein und tritt dann in eine Endlosschleife ein, die auf

neue Marktdaten prüft. Für jedes Symbol und jeden Zeitrahmen wird 

  1. der letzte abgeschlossenen Kursbalken abgerufen (einschließlich Eröffnungs-, Höchst-, Tiefst-, Schlusskurs, Volumen und Spread),
  2. geprüft, ob sie bereits in der Datenbank vorhanden ist, 
  3. um gegebenenfalls die neue(n) Kurse einzufügen.

Der Dienst verpackt das Einfügen in eine Datenbanktransaktion, um die Atomarität zu gewährleisten. Wenn etwas schief geht (z. B. ein Datenbankfehler), wird der Vorgang bis zu einer bestimmten Grenze (standardmäßig 3 Mal) mit Pausen von einer Sekunde wiederholt. Die Protokollierung ist optional. Die Schleife pausiert zwischen den Aktualisierungen und hält nur an, wenn der Dienst manuell angehalten wird.

Werfen wir also einen Blick auf einige seiner Bestandteile.

In den Eingaben können Sie den Datenbankpfad im Dateisystem, die Aktualisierungshäufigkeit in Minuten, die maximale Anzahl der Wiederholungsversuche bei fehlgeschlagener Einfügung und die Angabe, ob Sie die Erfolgs-/Fehlermeldungen im EA-Protokoll ausgeben möchten, wählen. Dieser letzte Parameter kann nützlich sein, wenn wir nach der Entwicklung einen stabilen Code erreichen.

//+------------------------------------------------------------------+
//|   Inputs                                                         |
//+------------------------------------------------------------------+
input string   InpDbPath      = "StatArb\\statarb-0.1.db";  // Database filename
input int      InpUpdateFreq  = 1;     // Update frequency in minutes
input int      InpMaxRetries  = 3;     // Max retries
input bool     InpShowLogs    = true; // Enable logging?

Abb. 3 – Metatrader 5 Dialog für die Eingabeparameter des Datenbank-Update-Service

Abbildung 3 – Metatrader 5 Dialog für Datenbank Update Service Eingabeparameter

Sie müssen die zu aktualisierenden Symbole und die entsprechenden Zeitrahmen auswählen. Diese Einträge werden in der nächsten Phase automatisiert.

//+------------------------------------------------------------------+
//|   Global vars                                                    |
//+------------------------------------------------------------------+
string symbols[] = {"EURUSD", "GBPUSD", "USDJPY"};
ENUM_TIMEFRAMES timeframes[] = {PERIOD_M5};

Hier initialisieren wir das Datenbank-Handle als INVALID_HANDLE. Es wird als Nächstes geprüft, wenn wir die Datenbank öffnen.

// Database handle
int dbHandle = INVALID_HANDLE;

OnStart()

Im einzigartigen Ereignis-Handler OnStart des Dienstes setzen wir nur die Endlosschleife und rufen die Funktion UpdateMarketData auf, wo die eigentliche Arbeit beginnt. Der sleep-Funktion wird die Anzahl der Millisekunden übergeben, die zwischen jeder Schleife (jeder Anforderung zur Aktualisierung von Zitaten) gewartet werden soll. Wir konvertieren sie in Minuten, um sie nutzerfreundlicher zu machen. Außerdem erwarten wir keine Aktualisierungen unterhalb der Ein-Minuten-Grenze.

//+------------------------------------------------------------------+
//| Main Service function                                            |
//| Parameters:                                                      |
//|   symbols    - Array of symbol names to update                   |
//|   timeframes - Array of timeframes to update                     |
//|   InpMaxRetries - Maximum number of retries for failed operations    |
//+------------------------------------------------------------------+
void OnStart()
  {
   do
     {
      printf("Updating db: %s", InpDbPath);
      UpdateMarketData(symbols, timeframes, InpMaxRetries);
      Sleep(1000 * 60 * InpUpdateFreq); // 60 secs
     }
   while(!IsStopped());
  }

UpdateMarketData()

Hier beginnen wir mit dem Aufruf der Funktion, die die Datenbank initialisiert. Wenn etwas schief geht, geben wir 'false' zurück und die Kontrolle geht zurück an die Hauptschleife. Wenn Sie also Probleme mit der Datenbankinitialisierung haben, können Sie den Dienst ruhig laufen lassen, während Sie das Problem mit der Datenbankinitialisierung beheben. In der nächsten Schleife wird es erneut versucht.

//+------------------------------------------------------------------+
//| Update market data for multiple symbols and timeframes           |
//+------------------------------------------------------------------+
bool UpdateMarketData(string &symbols_array[], ENUM_TIMEFRAMES &time_frames[], int max_retries = 3)
  {
// Initialize database
   if(!InitializeDatabase())
     {
      LogMessage("Failed to initialize database");
      return false;
     }
   bool allSuccess = true;

If the database is initialized (open), we start processing each symbol and timeframe.

// Process each symbol
   for(int i = 0; i < ArraySize(symbols_array); i++)
     {
      string symbol = symbols_array[i];
      // Process each timeframe
      for(int j = 0; j < ArraySize(time_frames); j++)
        {
         ENUM_TIMEFRAMES timeframe = time_frames[j];
         int retryCount = 0;
         bool success = false;

In dieser while-Schleife kontrollieren wir die maximale Anzahl der Wiederholungsversuche und rufen die Funktion zur Aktualisierung der Datenbank effektiv auf.

         // Retry logic
         while(retryCount < max_retries && !success)
           {
            success = UpdateSymbolTimeframeData(symbol, timeframe);
            if(!success)
              {
               retryCount++;
               Sleep(1000); // Wait before retry
              }
           }
         if(!success)
           {
            LogMessage(StringFormat("Failed to update %s %s after %d retries",
                                    symbol, TimeframeToString(timeframe), max_retries));
            allSuccess = false;
           }
        }
     }
   DatabaseClose(dbHandle);
   return allSuccess;
  }

UpdateSymbolTimeframeData()

Da unsere Tabelle market_data eine symbol_id als Fremdschlüssel benötigt, müssen wir sie zuerst haben. Wir prüfen also, ob es existiert, oder erstellen eine neue symbol_id in der Tabelle „symbol“, wenn es sich um ein neues Symbol handelt.

//+------------------------------------------------------------------+
//| Update market data for a single symbol and timeframe              |
//+------------------------------------------------------------------+
bool UpdateSymbolTimeframeData(string symbol, ENUM_TIMEFRAMES timeframe)
  {
   ResetLastError();
// Get symbol ID (insert if it doesn't exist)
   long symbol_id = GetOrInsertSymbol(symbol);
   if(symbol_id == -1)
     {
      LogMessage(StringFormat("Failed to get symbol ID for %s", symbol));
      return false;
     }

Wir konvertieren den Zeitrahmen von MQL5 ENUM_TIMEFRAMES in den für unsere Tabelle erforderlichen Typ String (TEXT).

   string tfString = TimeframeToString(timeframe);
   if(tfString == "")
     {
      LogMessage(StringFormat("Unsupported timeframe for symbol %s", symbol));
      return false;
     }

Wir kopieren die letzten Schlusskurse der Balken.

// Get the latest closed bar
   MqlRates rates[];
   if(CopyRates(symbol, timeframe, 1, 1, rates) != 1)
     {
      LogMessage(StringFormat("Failed to get rates for %s %s: %d", symbol, tfString, GetLastError()));
      return false;
     }

Wir prüfen, ob dieser Datenpunkt bereits existiert. Wenn ja, protokollieren wir es und geben „true“ zurück.

   if(MarketDataExists(symbol_id, rates[0].time, tfString))
     {
      LogMessage(StringFormat("Data already exists for %s %s at %s",
                              symbol, tfString, TimeToString(rates[0].time)));
      return true;
     }

Wenn es sich um einen neuen Datenpunkt, ein neues Angebot handelt, starten wir eine Datenbanktransaktion, um die Atomarität sicherzustellen. Wenn etwas fehlschlägt, machen wir es rückgängig. Wenn alles gut geht, wird die Transaktion bestätigt und „true“ zurückgegeben.

// Start transaction
   if(!DatabaseTransactionBegin(dbHandle))
     {
      LogMessage(StringFormat("Failed to start transaction: %d", GetLastError()));
      return false;
     }
// Insert the new data
   if(!InsertMarketData(symbol_id, tfString, rates[0]))
     {
      DatabaseTransactionRollback(dbHandle);
      return false;
     }
// Commit transaction
   if(!DatabaseTransactionCommit(dbHandle))
     {
      LogMessage(StringFormat("Failed to commit transaction: %d", GetLastError()));
      return false;
     }
   LogMessage(StringFormat("Successfully updated %s %s data for %s",
                           symbol, tfString, TimeToString(rates[0].time)));
   return true;
  }

InitializeDatabase()

Hier öffnen wir die Datenbank und validieren das Handle.

//+------------------------------------------------------------------+
//| Initialize database connection                                   |
//+------------------------------------------------------------------+
bool InitializeDatabase()
  {
   ResetLastError();
// Open database (creates if it doesn't exist)
   dbHandle = DatabaseOpen(InpDbPath, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      LogMessage(StringFormat("Failed to open database: %d", GetLastError()));
      return false;
     }

Diese „PRAGMA“-Direktive, die foreign_keys auf SQLite aktiviert, ist nicht unbedingt notwendig, wenn man mit der integrierten SQLite-Datenbank arbeitet, da wir wissen, dass sie mit aktivierter Funktion kompiliert worden ist. Dies ist nur eine Sicherheitsmaßnahme für den Fall, dass Sie mit einer externen Datenbank arbeiten.

// Enable foreign key constraints
   if(!DatabaseExecute(dbHandle, "PRAGMA foreign_keys = ON"))
     {
      LogMessage(StringFormat("Failed to enable foreign keys: %d", GetLastError()));
      return false;
     }
   LogMessage("Database initialized successfully");
   return true;
  }

TimeframeToString()

Die Funktion zur Umwandlung von MQL5 ENUM_TIMEFRAMES in einen Stringtyp ist ein einfacher Schalter.

//+------------------------------------------------------------------+
//| Convert MQL5 timeframe to SQLite format                          |
//+------------------------------------------------------------------+
string TimeframeToString(ENUM_TIMEFRAMES tf)
  {
   switch(tf)
     {
      case PERIOD_M1:
         return "M1";
      case PERIOD_M2:
         return "M2";
      case PERIOD_M3:
         return "M3";
(...)
      case PERIOD_MN1:
         return "MN1";
      default:
         return "";
     }
  }

MarketDataExists()

Um zu prüfen, ob die Marktdaten bereits existieren, führen wir eine einfache Abfrage über den zusammengesetzten Primärschlüssel der Tabelle market_data aus.

//+------------------------------------------------------------------+
//| Check if market data exists for given timestamp and timeframe     |
//+------------------------------------------------------------------+
bool MarketDataExists(long symbol_id, datetime tstamp, string timeframe)
  {
   ResetLastError();
   int stmt = DatabasePrepare(dbHandle, "SELECT 1 FROM market_data WHERE symbol_id = ? AND tstamp = ? AND timeframe = ? LIMIT 1");
   if(stmt == INVALID_HANDLE)
     {
      LogMessage(StringFormat("Failed to prepare market data existence check: %d", GetLastError()));
      return false;
     }
   if(!DatabaseBind(stmt, 0, symbol_id) ||
      !DatabaseBind(stmt, 1, (long)tstamp) ||
      !DatabaseBind(stmt, 2, timeframe))
     {
      LogMessage(StringFormat("Failed to bind parameters for existence check: %d", GetLastError()));
      DatabaseFinalize(stmt);
      return false;
     }
   bool exists = DatabaseRead(stmt);
   DatabaseFinalize(stmt);
   return exists;
  }

InsertMarketData()

Um schließlich die neuen Marktdaten einzufügen (die Aktualisierung), führen wir eine Insert-Abfrage mit der Funktion MQL5 StringFormat aus, wie in der Dokumentation vorgeschlagen.

Seien Sie vorsichtig mit den Zeichenfolgen in der VALUES-Ersetzung. Sie müssen sie zitieren. Danken Sie mir später. 🙂

//+------------------------------------------------------------------+
//| Insert market data into database                                 |
//+------------------------------------------------------------------+
bool InsertMarketData(long symbol_id, string timeframe, MqlRates &rates)
  {
   ResetLastError();
   string req = StringFormat(
                   "INSERT INTO market_data ("
                   "tstamp, timeframe, price_open, price_high, price_low, price_close, "
                   "tick_volume, real_volume, spread, symbol_id) "
                   "VALUES(%d, '%s', %G, %G, %G, %G, %d, %d, %d, %d)",
                   rates.time, timeframe, rates.open, rates.high, rates.low, rates.close,
                   rates.tick_volume, rates.real_volume, rates.spread, symbol_id);
   if(!DatabaseExecute(dbHandle, req))
     {
      LogMessage(StringFormat("Failed to insert market data: %d", GetLastError()));
      return false;
     }
   return true;
  }

Während der Dienst läuft...

Abb.5 – Metaeditor Navigator mit laufendem DB Update Dienst

Abbildung 5 – Metaeditor Navigator mit laufendem DB Update Dienst

... sollten Sie auf der Registerkarte „Expertenprotokoll“ etwas wie dieses sehen.

Abb.7 – Metatrader 5 Experts Log-Registerkarte mit Datenbank-Update-Service-Ausgabe

Abbildung 7 – Registerkarte „Expertenprotokoll“ von Metatrader 5 mit der Ausgabe des Datenbankaktualisierungsdienstes

Ihre „market_data“-Tabelle sollte folgendermaßen aussehen. Beachten Sie, dass wir alle Marktdaten, für alle Symbole und Zeitrahmen, in einer einzigen Tabelle speichern. Später werden wir dies verbessern, aber nur, wenn es nötig ist. Für den Moment ist das mehr als genug, um mit unserer Datenanalyse nachhaltiger zu beginnen.

Abb.8 – In Metaeditor integrierte SQLite-Registerkarte mit Datenbankaktualisierungen

Abbildung 8 – Metaeditor integrierte SQLite-Registerkartenansicht mit Datenbankaktualisierungen


Schlussfolgerung

In diesem Artikel haben wir gesehen, wie wir von kurzlebig heruntergeladenen Preisdaten zur ersten Datenbankversion für unseren statistischen Arbitrage-Rahmen übergegangen sind. Wir haben die Gründe für den ursprünglichen Entwurf des Schemas, die Initialisierung und die Eingabe der ersten Daten kennen gelernt.

Alle Tabellen, Felder und Beziehungen des Schemas sind jetzt mit Tabellenbeschreibungen, Beschränkungen und Beispielen dokumentiert.

Wir haben auch die Schritte zur Aktualisierung der Datenbank durch den Aufbau eines Metatrader 5-Dienstes beschrieben und eine Implementierung dieses Dienstes zusammen mit Python-Skripten zum Einfügen von Anfangsdaten für jedes verfügbare Symbol (Ticker) und jeden Zeitrahmen bereitgestellt.

Mit diesen Werkzeugen und ohne eine einzige Zeile Code schreiben zu müssen, kann der durchschnittliche Kleinhändler – die Zielgruppe unseres statistischen Arbitrage-Frameworks – nicht nur sofortige Marktdaten (Kursnotierungen) speichern, sondern auch ein paar Metadaten über die an unserer Kointegrationsstrategie beteiligten Aktien und die Geschichte der Geschäfte.

Dieses anfängliche Design wird sich gleich im nächsten Schritt weiterentwickeln, wenn wir die Portfoliogewichte in Echtzeit aktualisieren und das Portfolio rotieren, wenn die Kointegration des Korbs schwach wird, indem wir Symbole ohne manuelle Eingriffe ersetzen und/oder hinzufügen.


Referenzen

Daniel P. Palomar (2025). Portfolio Optimization: Theory and Application. Cambridge University Press.

* Eine bemerkenswerte Einschränkung von SQLite im Vergleich zu einigen zeitreihenorientierten Datenbanken ist das Fehlen von „as-of joins“. Da wir unsere Marktdaten anhand von Zeitstempeln indizieren, werden wir in Zukunft mit ziemlicher Sicherheit keine „Joins“ zwischen Tabellen verwenden können, da wir Zeitstempel als Primärschlüssel verwenden und diese selten, wenn überhaupt, in zwei oder mehr Tabellen ausgerichtet werden, und „Joins“ (innere, linke und äußere Joins) von dieser Indexausrichtung abhängig sind. In diesem Beitrag von blog post wird das Problem im Detail erläutert.

Dateiname Beschreibung
StatArb/db-setup.mq5

MQL5-Skript zum Erstellen und Initialisieren der SQLite-Datenbank durch Einlesen der Schemadatei schema-0.1.sql.

StatArb/db-update-statarb-0.1.mq5 MQL5 Service zur Aktualisierung der SQLite-Datenbank mit den letzten geschlossenen Preisbalken.
StatArb/schema-0.1.sql SQL-Schemadatei (DDL) zur Initialisierung der Datenbank (Erzeugung von Tabellen, Feldern und Beschränkungen).
db_store_quotes.ipynb Jupyter-Notizbuch mit Python-Code. Hilfedatei zum Füllen der SQLite-Datenbank mit Symbolen aus einem bestimmten Zeitbereich und Zeitrahmen.
.env Beispieldatei mit Umgebungsvariablen, die aus der obigen Python-Hilfsdatei gelesen werden sollen. (optional)

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

Beigefügte Dateien |
MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 79): Verwendung von Gator-Oszillator und Akkumulations-/Distributions-Oszillator mit überwachtem Lernen MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 79): Verwendung von Gator-Oszillator und Akkumulations-/Distributions-Oszillator mit überwachtem Lernen
Im letzten Beitrag haben wir die Paarung von Gator-Oszillator und Akkumulations-/Distributions-Oszillator in ihrer typischen Einstellung der von ihnen erzeugten Rohsignale betrachtet. Diese beiden Indikatoren sind als Trend- bzw. Volumenindikatoren zu verstehen. Im Anschluss an diesen Teil untersuchen wir die Auswirkungen, die das überwachte Lernen auf die Verbesserung einiger der von uns untersuchten Merkmalsmuster haben kann. Unser überwachter Lernansatz ist ein CNN, der mit Kernelregression und Skalarproduktähnlichkeit arbeitet, um seine Kernel und Kanäle zu dimensionieren. Wie immer tun wir dies in einer nutzerdefinierten Signalklassendatei, die mit dem MQL5-Assistenten arbeitet, um einen Expert Advisor zusammenzustellen.
MetaTrader 5 Machine Learning Blueprint (Teil 2): Kennzeichnung von Finanzdaten für maschinelles Lernen MetaTrader 5 Machine Learning Blueprint (Teil 2): Kennzeichnung von Finanzdaten für maschinelles Lernen
In diesem zweiten Teil der MetaTrader 5 Machine Learning Blueprint-Serie erfahren Sie, warum einfache Bezeichnungen Ihre Modelle in die Irre führen können und wie Sie fortgeschrittene Techniken wie die Triple-Barrier- und Trend-Scanning-Methode anwenden, um robuste, risikobewusste Ziele zu definieren. Dieser praktische Leitfaden ist vollgepackt mit praktischen Python-Beispielen, die diese rechenintensiven Techniken optimieren, und zeigt Ihnen, wie Sie verrauschte Marktdaten in zuverlässige Kennzeichnungen umwandeln können, die die realen Handelsbedingungen widerspiegeln.
Automatisieren von Handelsstrategien in MQL5 (Teil 28): Erstellen eines Price Action Bat Harmonic Patterns mit visuellem Feedback Automatisieren von Handelsstrategien in MQL5 (Teil 28): Erstellen eines Price Action Bat Harmonic Patterns mit visuellem Feedback
In diesem Artikel entwickeln wir ein Bat-Pattern-System in MQL5, das Auf- und Abwärtsmuster von Bat-Harmonic unter Verwendung von Umkehrpunkten und Fibonacci-Verhältnissen identifiziert und Handelsgeschäfte mit präzisen Einstiegs-, Stop-Loss- und Take-Profit-Levels auslöst, ergänzt durch visuelles Feedback durch Chart-Objekte
Beherrschung von Protokollaufzeichnungen (Teil 10): Vermeidung von Log Replay durch Implementierung einer Unterdrückung Beherrschung von Protokollaufzeichnungen (Teil 10): Vermeidung von Log Replay durch Implementierung einer Unterdrückung
Wir haben ein System zur Unterdrückung von Protokollen in der Logify-Bibliothek erstellt. Es wird beschrieben, wie die Klasse CLogifySuppression das Konsolenrauschen durch Anwendung konfigurierbarer Regeln reduziert, um sich wiederholende oder irrelevante Meldungen zu vermeiden. Wir behandeln auch das externe Konfigurations-Framework, Validierungsmechanismen und umfassende Tests, um Robustheit und Flexibilität bei der Protokollerfassung während der Bot- oder Indikatorentwicklung zu gewährleisten.