English Русский
preview
Statistische Arbitrage anhand kointegrierter Aktien (Abschluss): Datenanalyse mit einer spezialisierten Datenbank

Statistische Arbitrage anhand kointegrierter Aktien (Abschluss): Datenanalyse mit einer spezialisierten Datenbank

MetaTrader 5Handelssysteme |
24 0
Jocimar Lopes
Jocimar Lopes

Einführung

Anfang 2025 stellten wir uns der Herausforderung, ein Framework für statistische Arbitrage zu entwickeln, das sich an den durchschnittlichen Privathändler richtet – jenen einsamen Kämpfer mit nichts weiter als einem ganz normalen Notebook, einer normalen Internetverbindung und wenig Handelskapital. Seitdem haben wir zehn Artikel veröffentlicht, in denen wir unsere Forschungen und Experimente sowie die Erfolge und Misserfolge unserer Versuche beschreiben, den Markt mit Mathematik zu schlagen.

Alle diese Artikel wurden mit dem Ziel verfasst, einen leicht verständlichen Einstieg in das Thema statistische Arbitrage aus der Perspektive eines Traders zu bieten. Zusammen sollen sie das Minimum an Grundlagen abdecken, damit eine durchschnittliche Person mit Schulmathematik auf Oberstufenniveau und grundlegenden Programmierkenntnissen damit beginnen kann, Strategien des statistischen Arbitragehandels zu verstehen und umzusetzen.

Korrelations-, Kointegrations- und Stationaritätstests wurden gleich zu Beginn der Reihe behandelt, da diese Themen grundlegend sind. Dort haben wir über die Kointegrationstests nach Engle-Granger und Johansen sowie über den Augmented-Dickey-Fuller-Test („ADF“) und den Stationaritätstest nach Kwiatkowski-Phillips-Schmidt-Shin („KPSS“) gesprochen. Mit diesem Verständnis konnten wir von Anfang an damit beginnen, unsere Paare und Korbportfolios aufzubauen. Durch den Einsatz von Open-Source- und professionellen Python-Statistikbibliotheken wie statsmodels mussten wir uns nicht eingehend mit den mathematischen Grundlagen dieser Tests befassen und konnten uns stattdessen ganz auf ihre praktische Anwendung konzentrieren.

Wenn wir über statistische Arbitrage sprechen, geht es natürlich um Datenanalyse und um Datenbanken. Da MetaTrader 5 über eine integrierte SQLite-Datenbank verfügt, haben wir auch dafür eine Lösung parat. In Teil 3 dieser Reihe haben wir die Einrichtung der Datenbank besprochen, ein erstes Datenbankschema vorgeschlagen und gezeigt, wie dieses mithilfe eines MQL5-Dienstes auf dem neuesten Stand gehalten werden kann. Die Einführung dieses Dienstes war der erste Schritt, den wir unternommen haben, um unsere Datenanalyse von unserer Handelsumgebung zu entkoppeln. In diesem abschließenden Artikel werden wir die Entkopplung abschließen.

Nachdem wir eine Datenbank sowie eine stabile Pipeline für Kointegrations- und Stationaritätstests eingerichtet hatten, war der nächste logische Schritt, mit dem Aufbau der Portfolios zu beginnen. Daher haben wir in den folgenden drei Artikeln ein minimales Screening- und Bewertungssystem vorgeschlagen, das auf einer Hierarchie objektiver Kriterien wie der Stärke der Kointegration, der Stabilität der Portfoliogewichtungen und der Halbwertszeit der Rückkehr zum Mittelwert basiert. Die Hypothese, die wir für unser Screening zugrunde gelegt haben, betraf die Kointegration zwischen hochliquiden Aktien der Mikroprozessorbranche aus dem Nasdaq; ihre Grundsätze gelten jedoch für jedes andere kointegriertes Portfolio.

Schließlich haben wir in den letzten drei Artikeln einige weitverbreitete Methoden zur Überwachung des Live-Handels erörtert, wie beispielsweise den „Rolling Windows Eigenvector Comparison“ (RWEC) und den „In-Sample/Out-of-Sample ADF“ (IS/OOS ADF), die wir gemeinsam einsetzten, um die Notwendigkeit einer Neugewichtung der Portfoliogewichte zu erkennen, sowie den Chow-Test und die kumulative Quadratsumme (CUSUM) zur Erkennung struktureller Brüche in kointegrierten Beziehungen.

Die Forschungsergebnisse und Experimente, die wir in diesen zehn Artikeln zusammengefasst haben, trugen entscheidend dazu bei, aufzuzeigen, dass die Einstiegsbarriere für den durchschnittlichen Privathändler im Bereich der statistischen Arbitrage in den letzten zehn bis fünfzehn Jahren deutlich gesunken ist. Indem wir den Fokus von der Handelsgeschwindigkeit – also der Geschwindigkeit der Auftragsausführung insgesamt – auf die Entdeckung nahezu unendlicher Möglichkeiten in unerwarteten Zusammenhängen zwischen Wertpapieren aus verschiedenen Märkten und bei unterschiedlichen Zeitrahmenkombinationen verlagerten, haben wir lediglich die anfängliche Vermutung bestätigt, die wir hatten, als wir diese Herausforderung annahmen: Statistische Arbitrage-Strategien sind heutzutage für den durchschnittlichen Privathändler problemlos zugänglich. Da sie die unerschwinglichen Kosten im HFT-Bereich vermeiden, können sie im Jahr 2026 lukrative Möglichkeiten für die Umsetzung von Stat-Arb-Strategien finden. 

Das einzige fehlende Element in dem von uns beschriebenen Toolkit ist eine spezielle Datenbank, die SQLite den Aufwand der Datenanalyse abnimmt, sodass unsere liebgewonnene integrierte MetaTrader 5 Datenbank sich ganz auf die Aufgaben der Transaktionsverarbeitung konzentrieren kann, für die sie konzipiert wurde. Die Interaktionen mit dem EA – unserer sogenannten „Single Source of Truth“ für Ein- und Ausstiege am Markt – sollten weiterhin über SQLite abgewickelt werden, während die Datenanalyse, die als Grundlage für unser Screening-/Bewertungssystem und unsere Live-Handelsüberwachung dient, von einem spezialisierten System übernommen werden sollte. Der letzte Schritt dieser Einführung in die statistische Arbitrage für Privatanleger sollte unsere Datenanalyseumgebung für die Echtzeitüberwachung großer Datensätze vorbereiten und sie zudem zukunftssicher machen, da sie dem Wachstum unserer Geschäftstätigkeit gerecht wird, ohne dass Investitionen in teure Hardware erforderlich sind.


Die Bedeutung der Rechenleistung für die statistische Arbitrage

Jede Handelsaktivität erfordert Daten. Um eine Kauf- oder Verkaufsposition einzugehen, benötigt man zumindest den aktuellen Kurs des Symbols. In der Regel benötigen wir etwas mehr Daten; zumindest einige Darstellungen der Kurshistorie sind hilfreich, um einen Einblick in die Entwicklung des Kurses des Vermögenswerts im Laufe der Zeit zu gewinnen. Bald werden wir damit beginnen, Höchst- und Tiefstkurse, Kursspannen, Durchschnittswerte, durchschnittliche tatsächliche Kursspannen, volumengewichtete Durchschnittswerte sowie Darstellungen der Kurshistorie zu berechnen. Mit mehr Daten können wir besser nachvollziehen, wie der Preis seinen aktuellen Wert erreicht hat. Wenn wir über genügend Daten verfügen, können wir vielleicht auch Vermutungen darüber anstellen, wie sich der Kurs in den kommenden Stunden, Tagen oder Monaten entwickeln könnte. Wir können die Kursdaten nutzen, um Kerzenmuster zu erkennen und zu interpretieren, und wir können sie verwenden, um mehr oder weniger komplexe Indikatoren zu erstellen, die uns die Visualisierung der Kursentwicklung erleichtern. Es kommt nicht selten vor, dass wir für einen einzelnen Vermögenswert historische Kursdaten aus Dutzenden Monaten zusammenstellen müssen.

Beim Pairs-Trading benötigen wir mindestens doppelt so viele Daten. Wir müssen den mittleren Spread kontinuierlich berechnen, und wir benötigen noch mehr historische Daten, wenn wir überprüfen wollen, ob die Paarbeziehung stark genug ist und weiterhin besteht. Vielleicht möchten wir einen dynamischen Schwellenwert für die Standardabweichung berechnen, der auf der Volatilität basiert. Daher benötigen wir nicht nur mehr Daten, sondern auch mehr Rechenleistung, um die Daten für beide Symbole zu laden und den Spread, den Mittelwert, den Schwellenwert für die Standardabweichung sowie nun auch die Volatilität in Echtzeit zu berechnen.

Der Daten- und Rechenaufwand, den Pairs-Trading-Strategien erfordern, beträgt in der Regel nur einen Bruchteil des Datenbedarfs statistischer Arbitrage-Strategien. Wir haben Körbe, die aus einer unbegrenzten Anzahl von Aktien bestehen, für die wir den Kointegrationsvektor und die entsprechenden Portfoliogewichte berechnen müssen; wir führen einen „Rolling Windows Eigenvector Comparison“ durch, um die Stabilität der Portfoliogewichte zu überprüfen; wir führen einen Chow-Test mit einer kumulativen Quadratsumme durch, um strukturelle Brüche zu antizipieren; und all diese Berechnungen müssen wir in Echtzeit durchführen, um den Handel live zu überwachen. Letztendlich müssen wir all diese Berechnungen für mehrere Körbe gleichzeitig durchführen, das heißt, wir benötigen Kursdaten für Dutzende Wertpapiere und müssen diese so schnell wie möglich verarbeiten, um rechtzeitig auf Warnmeldungen, Kursbrüche und Marktstörungen reagieren zu können.

Als Jim Simons Ende der 1980er Jahre begann, statistische Arbitrage-Strategien anzuwenden, war die Rechenleistung, die zur Verarbeitung der riesigen Datenmengen erforderlich war, die täglich gesammelt wurden, sehr kostspielig. Die Kosten für die Ausrüstung und das Fachpersonal, das für die Entwicklung der Software und den laufenden Betrieb erforderlich war, waren für jeden Privatanleger, selbst für einen vermögenden, unerschwinglich. Die hohe Einstiegsbarriere für Privatanleger bestand nicht in Glasfaser-Hochgeschwindigkeitsverbindungen oder der Server-Colocation. Hochfrequenzhandel (HFT) gab es damals noch nicht. 

Die geschätzten Kosten für ein RDBMS beliefen sich Ende der 1980er Jahre auf rund vierzehn- bis fünfzehntausend Dollar für eine unbefristete Lizenz von Oracle, Ingres oder IBM. Inflationsbereinigt entspräche dies heute etwa hunderttausend Dollar. Wir berücksichtigen dabei weder das Personal noch die Hardware. Das sind lediglich die ungefähren Kosten für die Softwarelizenz. Statistische Arbitrage war eine Domäne institutioneller Händler, was vor allem auf die für die Datenanalyse erforderliche Rechenleistung und die hohen Kosten der dafür verwendeten Tools zurückzuführen war. Doch dies änderte sich in den folgenden Jahren grundlegend, wie wir im Folgenden sehen werden.

In einem Standard-MetaQuotes-Demokonto stehen etwa 9.000 verschiedene Symbole und mehr als 20 Zeitrahmen zur Verfügung.

Abb. 1 – Screenshot, der die Anzahl der in einem Standard-MetaQuotes-Demokonto verfügbaren Symbole zeigt

Abb. 1.  Screenshot, der die Anzahl der in einem Standard-MetaQuotes-Demokonto verfügbaren Symbole zeigt

Da es bei der statistischen Arbitrage darum geht, Zusammenhänge zwischen den Kursen von Vermögenswerten zu erkennen, würden wir im Idealfall alle möglichen Kombinationen aller verfügbaren Vermögenswerte in allen verfügbaren Zeitrahmen überprüfen. Die Suche nach Beziehungen in allen möglichen Kombinationen würde zu einer kombinatorischen Explosion führen. Deshalb wählen wir unsere Paare und Körbe nach einem Bewertungssystem aus.


Bisher haben wir ausschließlich SQLite verwendet

Der Einfachheit halber haben wir bisher das in MetaTrader 5 integrierte SQLite als Datenbank verwendet. Aber verstehen Sie mich nicht falsch. Neben dem Komfort bietet das Programm die weltweit am häufigsten genutzte Datenbank – eine brillante und vertrauenswürdige Software, die sich in der Praxis bewährt hat und äußerst zuverlässig ist. Da diese Funktionen bereits Teil von MetaTrader 5 sind – wodurch eine zusätzliche Installation und Konfiguration entfällt –, steht eine Reihe nativer SQLite-Datenbankfunktionen zur Verfügung, die die Arbeit mit der Datenbank innerhalb der Plattform erleichtern. Außerdem steht im MetaEditor eine native grafische Benutzeroberfläche (GUI) zur Abfrage und Verwaltung der in MetaTrader 5 integrierten SQLite-Datenbanken zur Verfügung.

Abb. 2 – Screenshot der MetaEditor-Benutzeroberfläche für SQLite

Abb. 2. Screenshot, der die MetaEditor-Benutzeroberfläche für SQLite zeigt

Diese Eigenschaften machten SQLite zur perfekten Wahl für den Aufbau unseres statistischen Arbitrage-Frameworks für den durchschnittlichen Privathändler. Es ist zuverlässig, skalierbar, integriert, verfügt über native MQL5-Funktionen und eine MetaEditor-Benutzeroberfläche. Innerhalb von MetaTrader 5 ist SQLite benutzerfreundlich einsetzbar. Aber wir können es noch besser machen, ohne SQLite aufzugeben. Wir können es für den Zweck nutzen, für den es entwickelt wurde, und für unsere Datenanalyse eine spezielle Datenbank verwenden.

SQLite ist ein OLTP-System (Online Transaction Processing), das darauf ausgelegt ist, kleine Transaktionen sehr schnell zu verarbeiten. SQLite leistet hier zwar gute Arbeit, aber wenn wir von einer spezialisierten Datenbank für die Datenanalyse sprechen, meinen wir ein OLAP-System (Online Analytical Processing), also ein System, das für komplexe und mathematisch anspruchsvolle Analysen historischer Daten – in der Regel großer Datensätze – ausgelegt ist. Auch wenn sich ihre Funktionen in gewisser Weise überschneiden mögen – das heißt, wir können ein OLAP-System zum Speichern und Abfragen von täglichen Transaktionsdaten nutzen und ein OLTP-System zur Analyse historischer Daten, wie wir es hier getan haben –, handelt es sich doch um unterschiedliche Systeme. 

Damit die Unterschiede zwischen ihnen sichtbar werden, müssten wir unsere Datenanalyse in vollem Umfang durchführen. Bislang haben wir in unseren Beispielen nur einen winzigen Ausschnitt der Historie analysiert. Unsere Betrachtungszeiträume betrugen in der Regel höchstens zwei bis drei Jahre. Wir haben unter keinen Umständen historische Daten aus zehn oder zwanzig Jahren ausgewertet. Wir haben auf Kointegration in Körben mit drei oder vier Wertpapieren geprüft, niemals mehr als das. Außerdem haben wir unser Screening- und Bewertungssystem gemäß unserer ursprünglichen Kointegrationshypothese für die Halbleiterindustrie auf einen einzigen Zeitrahmen (H4) beschränkt.

Wenn Sie Ihr Vorhaben im Bereich statistischer Arbitrage ernsthaft verfolgen – selbst als einfacher Privatanleger –, werden Sie irgendwann vor der Notwendigkeit stehen, längere Zeiträume zu analysieren, um Muster jenseits der Kointegration zu erkennen; oder Sie möchten Körbe aus fünf oder vielleicht zehn Vermögenswerten in vielen Kombinationen mit verschiedenen Zeiträumen testen, wobei Sie schließlich auch nicht-finanzielle Daten (wie beispielsweise Frachtgebühren oder Rohstoffproduktionsmengen) einbeziehen; wahrscheinlich wird diese Analyse die Grundlage für Ihre Live-Handelsüberwachung bilden. Sehen Sie, worauf das hinausläuft?

Sobald man mehr Symbole, mehr Zeitrahmen und mehr historische Daten einbezieht, hat man plötzlich seine eigenen Big Data, die es so schnell wie möglich zu verarbeiten gilt. Ein solches Wachstum ist bei der statistischen Arbitrage ganz normal. Es ist zu erwarten, dass sich Ihre Anforderungen an die Datenverarbeitung in dieser Weise weiterentwickeln werden, und Sie sollten bereit sein, sich mit speziellen Tools auseinanderzusetzen, um dieser Anforderung – der Verarbeitung größerer Datenmengen – gerecht zu werden. Die gute Nachricht ist, dass man heute nicht mehr den Acker verkaufen muss, um sich die Schaufel leisten zu können. Während man vor einiger Zeit noch eine teure Softwarelizenz kaufen musste, um Zugang zu einem High-End-OLAP-System zu erhalten, kann man heute auf ein hochwertiges System zugreifen, das nicht nur kostenlos, sondern auch Open Source ist: DuckDB.


DuckDB: ein kostenloses Open-Source-OLAP-System

Es herrscht die weitverbreitete Fehlannahme, dass statistische Arbitrage-Handelsstrategien eine Form des Hochfrequenzhandels (HFT) seien oder zumindest auf Hochgeschwindigkeitsverbindungen und eine Orderweiterleitung mit geringer Latenz angewiesen seien. Infolgedessen glauben viele Menschen, dass die statistische Arbitrage aufgrund dieser vermeintlichen Abhängigkeit von der Geschwindigkeit von institutionellen Händlern dominiert wird. Das stimmt nur zur Hälfte. 

Obwohl viele Stat-Arb-Strategien für den Einsatz in kürzeren Zeitrahmen (in der Regel unter 5 Minuten) konzipiert sind, ist dies keine zwingende Voraussetzung. Es ist völlig normal, dass man in höheren Zeitrahmen Korrelationen, Kointegrationen und andere statistische Zusammenhänge feststellt. Geschwindigkeit ist wichtig und bildet den Kern vieler Strategien, doch sie war nicht der einzige Grund, warum institutionelle Akteure diesen Handelsbereich bis vor einigen Jahren monopolisiert haben. Der Hauptgrund waren die hohen Kosten für die zur Datenanalyse erforderliche Rechenleistung sowie die Komplexität des Betriebs.

Um das Ganze etwas einzuordnen, habe ich kurz im Internet recherchiert und festgestellt, dass noch vor fünfzehn Jahren „die Kluft zwischen einem Privatanleger und einem Hedgefonds ein ‚Kapitalgraben‘ war, der auf firmeneigener OLAP-Hardware und -Software beruhte“. Mitte der 2000er Jahre gab es neben den hohen finanziellen Kosten auch eine hohe technische Einstiegsbarriere. Das heißt: Nachdem Sie allein für die Softwarelizenzen, die Ihnen die Abfrage von Daten in einem OLAP-System ermöglichen, etwa 100.000 bis 250.000 Dollar investiert haben, müssten Sie in manchen Fällen auch noch ein Server-Rack mit Spezialservern betreiben und eine jährliche Wartungsgebühr von rund 20 % tragen. Zu den technischen Einstiegshürden gehörten auch die Vorverarbeitung der OLAP-Cubes – da diese alten Systeme auf multidimensionalem OLAP basierten – sowie die Verwendung von Multidimensional Expressions (MDX) zur Bearbeitung und Abfrage dieser multidimensionalen Aggregationen. Mit diesen Systemen auf Unternehmensebene war es nicht möglich, eine CSV- oder Parquet-Rohdatei mit dem guten alten SQL zu durchsuchen, wie wir weiter unten sehen werden.

Die Kluft zwischen einem Privatanleger und einem Hedgefonds hat sich in den letzten fünfzehn Jahren deutlich verringert. Zwar ist die Geschwindigkeit nach wie vor ein Problem und HFT ist für Privatanleger immer noch Neuland, doch können wir nun ein voll funktionsfähiges OLAP-System auf unseren Laptops betreiben. Es spielt keine Rolle, ob wir unsere Analyse auf einem Mac, einem Windows- oder einem Linux-Rechner durchführen; wir können DuckDB nutzen, indem wir einfach eine etwa 50 MB große ausführbare Datei herunterladen und im Handumdrehen wie ein Profi mit der Analyse unserer großen Datensätze beginnen.

Kompatibilität der Datenformate

Eine der wohl wichtigsten Funktionen von DuckDB ist die Fähigkeit, mit den im Bereich der Datenanalyse am häufigsten verwendeten Datenbanken, Dataframe-Engines, Cloud-Speichern und Dateiformaten zu kommunizieren (und deren Daten zu verarbeiten). Dies wird mithilfe von Erweiterungen erreicht. Sobald Sie einen Client installiert haben, können Sie die verfügbaren Erweiterungen überprüfen. DuckDB verfügt über ein vom Kernteam gepflegtes Erweiterungssystem und wird durch Beiträge der Community ergänzt. Damit können wir folgende Daten lesen und schreiben:

  • Dataframe-Engines wie Pandas, Apache Arrow, NumPy und Polars; 
  • Datenbanken wie MySQL, Postgres und SQLite; 
  • Cloud-Speicherdienste wie Amazon S3, Google Cloud Storage und Azure Blob Storage;
  • Die Dateiformate CSV, JSON, Parquet und Iceberg.

Zum Zeitpunkt der Abfassung dieses Artikels ist mir kein anderes Open-Source-System bekannt, das ein derart hohes Maß an Flexibilität bietet. 

Einfache Bedienung

DuckDB bringt dieselbe einfache Handhabung wie SQLite in unsere Pipeline ein. Genau wie SQLite ist auch DuckDB eine prozessinterne, wartungsfreie, eingebettete Datenbank, die nicht nur unsere Datenanalyse durch eine höhere Leistung vereinfacht, sondern auch die Interoperabilität zwischen verschiedenen Datenquellen und Dateiformaten fördert. Dadurch lassen sich Daten aus mehreren unterschiedlichen Quellen problemlos zusammenführen und von lokalen Systemen in die Cloud sowie umgekehrt übertragen.

Wir schlagen KEINEN Ersatz für SQLite vor. Stattdessen integrieren wir DuckDB in unser System mit einem ganz bestimmten Anwendungsbereich: der analytischen Verarbeitung. Während SQLite eine universell einsetzbare relationale Datenbank ist, die auf die Transaktionsverarbeitung ausgerichtet ist, handelt es sich bei DuckDB um ein spezialisiertes OLAP-Tool. Die Interaktion mit dem EA bleibt unverändert und läuft weiterhin über die integrierte SQLite-Datenbank und die native MQL5-Bibliothek.



Der Unterschied zwischen OLAP und OLTP in der Praxis

Um den Unterschied zwischen dem SQLite-OLTP-System und dem DuckDB-OLAP-System besser zu verstehen, wollen wir uns ansehen, wie sie mit typischen Handelsdaten unterschiedlicher Größenordnung umgehen. Zu diesem Zweck haben wir ein sehr einfaches Python-Skript in drei Schritten erstellt, das einige synthetische Daten für eine Reihe von Aktienkürzeln (Symbolen) generiert und eine gängige Aggregation darüber durchführt, um den Durchschnittskurs pro Aktienkürzel zu berechnen. So können wir den architektonischen Unterschied in der Praxis besser erkennen.

Es gibt DuckDB Client-Pakete für verschiedene Sprachen. Um es in Python nutzen zu können, können wir einfach folgenden Aufruf verwenden:

pip install duckdb

Es gibt keine externen Abhängigkeiten, außer unter Windows, wo das C++-Redistributable-Paket mit den Komponenten der Laufzeitbibliothek benötigt wird.

Dann können wir es einfach wie jedes andere Python-Paket in unsere Skripte importieren.

import sqlite3
import duckdb
import pandas as pd
import numpy as np
import time

Um die Unterschiede zwischen SQLite und DuckDB bei der Datenanalyse zu veranschaulichen, werden wir einen vereinfachten Benchmark mit synthetischen Daten durchführen. Zunächst generieren wir eine Million Zeilen mit synthetischen Kurs- und Volumendaten für acht Ticker. Wir generieren ab der allerersten Sekunde des Jahres 2023 eine Million „Ticks“, was insgesamt 11,5 Handelstagen entspricht. Beachten Sie, dass es sich hierbei um Daten aus fast einem halben Monat handelt. In der Realität müssen wir mit Daten rechnen, die um mehrere Größenordnungen größer sind als diese.

def generate_data(filename="finance_data.csv"):

    print("Generating synthetic data...")
    tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'NVDA', 'META', 'NFLX']
    n_rows = 1_000_000

    df = pd.DataFrame({
        'timestamp': pd.date_range(start='2023-01-01', periods=n_rows, freq='s'),
        'ticker': np.random.choice(tickers, n_rows),
        'price': np.random.uniform(100, 500, n_rows),
        'volume': np.random.randint(1, 1000, n_rows)
    })
    df.to_csv(filename, index=False)
    print(f"Created {filename} with {n_rows} rows.")

Dadurch wird ein DataFrame mit einer Million Zeilen wie folgt erzeugt:

Zeitstempel Ticker Preis Volumen
2023-01-01 00:00:00
TSLA 432.15 842
2023-01-01 00:00:01
AAPL 120.50 12
2023-01-01 00:00:02
MSFT 315.88 500

Tabelle 1. Beispiel für synthetische Tabellendaten zum Vergleich von SQLite und DuckDB

Nachdem wir diese tabellarischen Daten als CSV-Datei gespeichert haben, beauftragen wir eine im Arbeitsspeicher laufende SQLite-Datenbank, den Durchschnittspreis für jeden Ticker zu berechnen.

def benchmark_sqlite(filename):

    conn = sqlite3.connect(":memory:") # Using memory for a fair speed test
    cursor = conn.cursor()

    # Load data

    df = pd.read_csv(filename)
    df.to_sql("prices", conn, index=False)

    start_time = time.perf_counter()

    query = "SELECT ticker, AVG(price) FROM prices GROUP BY ticker"

    cursor.execute(query)
    results = cursor.fetchall()

    end_time = time.perf_counter()
    conn.close()

    return end_time - start_time

Wir übermitteln dieselbe CSV-Datei an DuckDB, um dieselbe Berechnung durchzuführen, doch dieses Mal müssen wir die Daten nicht laden, da DuckDB die CSV-Datei direkt abfragen kann.

def benchmark_duckdb(filename):

    conn = duckdb.connect(database=':memory:')  
    start_time = time.perf_counter()
    query = f"SELECT ticker, AVG(price) FROM '{filename}' GROUP BY ticker"
    results = conn.execute(query).fetchall()
    end_time = time.perf_counter()

    return end_time - start_time

if __name__ == "__main__":

    csv_file = "finance_data.csv"
    generate_data(csv_file)  

    print("\nStarting Benchmarks...")

    sqlite_time = benchmark_sqlite(csv_file)
    print(f"SQLite execution time: {sqlite_time:.4f} seconds")

    duckdb_time = benchmark_duckdb(csv_file)
    print(f"DuckDB execution time: {duckdb_time:.4f} seconds")

    speedup = sqlite_time / duckdb_time
    print(f"\nDuckDB was {speedup:.1f}x faster than SQLite for this query.")

Dieses Skript ist hier als bench.py angehängt. Wenn Sie das Skript für eine Million Zeilen ausführen, sollte eine CSV-Datei mit einer Größe von etwa 50 MB erstellt werden. Auf einem Notebook der unteren Preisklasse mit nur zwei Kernen habe ich den folgenden Zusammenhang zwischen der Laufzeit von SQLite und der von DuckDB festgestellt.

Generating synthetic data...
Created finance_data.csv with 1000000 rows.

Starting Benchmarks...
SQLite execution time: 1.3408 seconds
DuckDB execution time: 1.3442 seconds

DuckDB was 1.0x faster than SQLite for this query.

Beide Systeme waren bei dieser Datenmenge identisch. Als wir die Daten um das Zehnfache skalierten, indem wir dasselbe Skript für zehn Millionen Zeilen ausführten und eine CSV-Datei von etwa 500 MB erzeugten, wurde der Unterschied deutlich.

Generating synthetic data...
Created finance_data.csv with 10000000 rows.

Starting Benchmarks...
SQLite execution time: 18.0329 seconds
DuckDB execution time: 9.0836 seconds

DuckDB was 2.0x faster than SQLite for this query.

Zufälligerweise haben wir hier gerundete Zahlen. Das ist nur ein Zufall. Sicher ist jedenfalls, dass Ihre Ergebnisse nach mehrmaliger Ausführung dieses Skripts auf drei verschiedenen Rechnern andere Werte für die absolute Ausführungszeit liefern werden als die hier gezeigten. Natürlich hängen diese Ergebnisse stark von den Spezifikationen Ihres Computers ab, insbesondere von der Anzahl der CPU-Kerne. Konzentrieren Sie sich also nicht auf die absoluten Werte. Achten Sie einfach auf die relativen Werte, also das Verhältnis zwischen der Ausführungszeit von SQLite und DuckDB, wenn Sie den Datenumfang ändern. Ich lade Sie ein, es einmal mit hundert Millionen Zeilen zu versuchen.

Was hier passiert, ist, dass SQLite keine Startzeit benötigt. Wenn wir das Programm anweisen, eine Million Zeilen zu verarbeiten, beginnt es sofort mit der Verarbeitung. Andererseits verfügt DuckDB über einen Abfrageoptimierer und eine vektorisierte Ausführungs-Engine, die einige Millisekunden benötigt (bzw. Sekunden auf diesen Low-End-Rechnern mit zwei Kernen). Außerdem liest DuckDB als spaltenorientiertes Speichersystem natürlich nur die Spalte, die zur Berechnung des Durchschnittspreises benötigt wird, nämlich die Spalte „price“, während SQLite als zeilenorientiertes System jede vollständige Zeile mit den Feldern „timestamp“, „ticker“, „price“ und „volume“ lädt. DuckDB ignoriert sie physisch auf der Festplatte. Wenn wir also von einer Million auf zehn Millionen Zeilen erhöhen, liest DuckDB deutlich weniger Daten als SQLite.

Schließlich verarbeitet SQLite jeweils eine Zeile nach der anderen und läuft bei einzelnen Abfragen in der Regel im Single-Thread-Modus, während DuckDB die sogenannte vektorisierte Ausführung nutzt, bei der Daten in Stapeln an die CPU gesendet und gleichzeitig alle verfügbaren Kerne für vollständige Parallelität genutzt werden. Auch hier gilt: Mit zunehmendem Datenvolumen vergrößert sich auch die Leistungslücke zwischen diesen beiden Systemen. Um fair zu sein und zu zeigen, dass SQLite die Nase vorn hat, könnten wir es bitten, eine bestimmte Transaktions-ID unter diesen zehn Millionen Zeilen abzufragen – und dabei wird es DuckDB weit hinter sich lassen, denn genau das ist die Domäne von SQLite, die Funktion, für die es entwickelt wurde.

Es ist erwähnenswert, dass wir hier zwar eine CSV-Datei verwenden, die Fähigkeit von DuckDB, eine Parquet-Datei oder das eigene native .duckdb-Format abzufragen, den Vorsprung jedoch noch weiter vergrößern würde. Die Leistungsfähigkeit bei der Datenanalyse und die allgemeine Geschwindigkeit sind jedoch nur einer der Vorteile, die wir durch den Einsatz eines spezialisierten OLAP-Systems in unserem statistischen Arbitrage-System erzielen. Ein weiterer großer Vorteil ist eine nativer zeitbezogene Verknüpfung.

ASOF JOIN

Bei der statistischen Arbitrage ist Zeit der entscheidende Faktor. Das ist keine Redewendung. Das ist wörtlich gemeint. Wenn Sie sich das SQLite-Datenbankschema ansehen, das wir in dieser Serie entwickelt haben, werden Sie feststellen, dass die meisten Tabellen einen Zeitstempel als Primärschlüssel haben. Da es sich bei den meisten unserer Daten um Zeitreihen handelt, ist dies eine naheliegende Wahl, wodurch die Zeitstempel nicht nur in der statistischen Arbitrage, sondern im Handel insgesamt zu einer Art natürlichem Schlüssel werden 

Angesichts dieser Tatsache nehmen wir einmal an, wir müssten die beiden folgenden Tabellen heranziehen, um die folgende Frage zu beantworten: Wie hoch war der Preis von Symbol A, als Symbol B gehandelt wurde? Da der Kurs des Symbols A in der Tabelle „market_data“ gespeichert ist und der Zeitstempel des Trades für Symbol B in der Tabelle „trade“ zu finden ist, müssten wir eine Verknüpfung zwischen diesen beiden Tabellen herstellen. 

Abb. 3 – Diagramm, das die Tabellen „market_data“ und „trade“ aus „schema-0.5“ zeigt

Abb. 3. Diagramm, das die Tabellen „market_data“ und „trade“ aus schema-0.5 zeigt

Wie Sie wahrscheinlich wissen, setzt eine JOIN-Klausel in SQL übereinstimmende Felder in den beteiligten Tabellen voraus, andernfalls würden wir als Ergebnis null Zeilen erhalten. In diesem Fall ist der Zeitstempel das gesuchte Verknüpfungsfeld. Unsere Abfrage würde in etwa so aussehen:

SELECT 
    t.ticket,
    t.side,
    t.price AS trade_price,
    m.price_open,
    m.price_high,
    m.price_low,
    m.price_close,
    m.timeframe
FROM trade t
JOIN market_data m ON t.tstamp = m.tstamp AND t.symbol_id = m.symbol_id
WHERE t.tstamp = 1708531200  -- The timestamp of our reference trade
AND m.timeframe = 'M1';    -- We need the timeframe because of the market_data composite primary key

Das Problem ist, dass diese Abfrage nur dann funktioniert, wenn der Zeitstempel der Marktdaten (m.tstamp) genau in derselben Sekunde mit dem Zeitstempel des Handels (t.tstamp) übereinstimmt. Im oben genannten Beispiel bräuchten wir einen Kursstand mit dem Zeitstempel 21. Februar 2024, 16:00:00 Uhr UTC. Genau 16:00:00 Uhr UTC. Wie Sie vielleicht bereits bemerkt haben, ist es unrealistisch, zu erwarten, dass Trades und Kursnotierungen genau in derselben Sekunde stattfinden. Wenn unser System von Abfragen wie diesen abhängen würde, wäre es ein sehr anfälliges Handelssystem, da die meisten, wenn nicht sogar alle Abfragen fehlschlagen würden. Außerdem ist das in der Praxis nicht das, was wir wissen wollen. Wir wollen wissen, was die letzte Kursnotierung von Symbol A war, als Symbol B gehandelt wurde.

Dies lässt sich mithilfe von Unterabfragen oder durch Verwendung einer LIMIT-Klausel bewerkstelligen, um den aktuellsten Kurs vor oder zum Zeitpunkt des Handels zu ermitteln.

SELECT t.*, m.*
FROM trade t
JOIN market_data m ON m.symbol_id = t.symbol_id
WHERE t.tstamp = 1708531200
  AND m.timeframe = 'M1'
  AND m.tstamp <= t.tstamp
ORDER BY m.tstamp DESC
LIMIT 1;

Diese Standard-SQL-Methode funktioniert in fast jeder Datenbank, einschließlich SQLite, hat jedoch einige Nachteile. Der entscheidende Faktor ist wiederum die Leistung. Um den nächstgelegenen Kurs zu ermitteln, indem wir nach dem spätesten Zeitstempel suchen, der kleiner oder gleich dem Handelszeitpunkt ist, weisen wir die Datenbank manuell an, alle historischen Datensätze für ein Wertpapier zu sortieren und den jeweils jüngsten auszuwählen – und das ist rechenintensiv. Für jeden einzelnen Handel muss die Datenbank eine Suche und eine Sortierung durchführen. Wenn wir 1.000 Trades mit 1.000.000 Kursnotierungen abgleichen – was in der Datenanalyse ein häufiges Szenario ist –, kann dies zu erheblichen Verzögerungen führen.

DuckDB und andere spezialisierte OLAP-Systeme lösen dieses Problem mit dem sogenannten ASOF-JOIN, einem zeitbezogenen Join, der für die Zeitreihenanalyse in spaltenorientierten Datenbanken entwickelt wurde und es ermöglicht, Tabellen zu verknüpfen, deren Zeitstempel nicht exakt übereinstimmen. Um Ihnen den Unterschied zwischen temporären Joins mit Standard-SQL-Unterabfragen und der Verwendung des speziellen DuckDB-ASOF-JOIN zu veranschaulichen, haben wir ein weiteres einfaches Benchmark-Skript erstellt, das die Leistung folgender Optionen vergleicht:

SQLite-Abfrage mit Unterabfrage und LIMIT

    sql_query = """
    SELECT SUM(t.quantity * (
        SELECT q.bid_price FROM q 
        WHERE q.ticker = t.ticker AND q.timestamp <= t.timestamp 
        ORDER BY q.timestamp DESC LIMIT 1
   )) FROM t
"""

DuckDB-Abfrage mit ASOF-JOIN

duck_query = """
    SELECT SUM(t.quantity * q.bid_price) 
    FROM t ASOF JOIN q ON t.ticker = q.ticker AND t.timestamp >= q.timestamp
    """

Dieses Skript ist hier als „bench_asof.py“ angehängt. Wenn Sie das Programm auf einem Low-End-Rechner ausführen – unserer Zielhardware –, sollten Sie ein ähnliches Ergebnis wie dieses erhalten.

--- Generating 1,000,000 trades and 100,000 quotes ---

--- Running Engine-Only Benchmark (Computing SUM) ---

DuckDB Time: 1.1637s
SQLite Time: 2.1329s

[WINNER]: DuckDB is 1.8x faster at this scale.

Auch hier gilt: Wenn wir das Datenvolumen verzehnfachen, vergrößert sich auch die Lücke. Denken Sie daran: Ich führe diesen Benchmark bewusst auf einem Low-End-Rechner mit zwei Kernen durch. Diese Entscheidung dient bewusst dazu, unserem Hauptziel treu zu bleiben, dieses Stat-Arb-Framework für den durchschnittlichen Privathändler zu entwickeln; gleichzeitig schränkt diese Entscheidung jedoch die native Parallelität von DuckDB ein, das standardmäßig alle verfügbaren Kerne nutzt. Wenn Sie Ihre Analyse also auf einem modernen Desktop-PC oder Notebook durchführen, werden Sie feststellen, dass diese Zahlen für die DuckDB-Laufzeit drastisch sinken.

--- Generating 10,000,000 trades and 1,000,000 quotes ---

--- Running Engine-Only Benchmark (Computing SUM) ---

DuckDB Time: 8.6416s
SQLite Time: 22.2207s

[WINNER]: DuckDB is 2.6x faster at this scale.

Die Verfügbarkeit von ASOF-JOINs ist ein entscheidender Wendepunkt für Privatanleger, die im Bereich der statistischen Arbitrage tätig sind. Bedenken Sie, dass es sich bei dem obigen Beispiel und dem Benchmark um ein sehr einfaches Beispiel handelt. Wenn wir der Abfrage jedoch weitere Merkmale hinzufügen, wie beispielsweise den Geldkurs, das Volumen, die Volatilität und andere, wird die Abfrage unter Verwendung von Standard-SQL ohne den von DuckDB bereitgestellten ASOF-JOIN schwer zu debuggen und zu warten, und die Leistung leidet noch stärker darunter. Für jeden einzelnen Handel muss die Datenbank eine separate Abfrage in der Kurs-Tabelle durchführen.

Andererseits unterstützt ein OLAP-System zeitliche Verknüpfungen als vollwertigen Bestandteil und nutzt einen speziellen Algorithmus, der eigens für diese Point-in-Time-Logik entwickelt wurde. Die Abfrage ist elegant und recheneffizient. DuckDB führt keine verschachtelten Schleifen aus. An dieser Stelle kommt die oben erwähnte sogenannte vektorisierte Ausführung zum Einsatz. Es verarbeitet Datenblöcke über den CPU-Cache und kann diese Verknüpfungen auf einem handelsüblichen Laptop innerhalb von Millisekunden für Millionen von Zeilen durchführen. Da DuckDB spaltenorientiert ist, liest es nur die Spalten „timestamp“, „symbol“ und „price“ aus, anstatt den gesamten Datensatz.

Die vektorisierte Ausführung spielt ihre Stärken besonders bei der Berechnung des „Rolling Windows Eigenvector Comparison“ (RWEC) für die Portfolio-Neugewichtung aus, den wir in Teil 8 dieser Reihe beschrieben haben.

RWEC

DuckDB verfügt über integrierte Array-Funktionen, die die Leistung unserer RWEC-Berechnung erheblich verbessern können, insbesondere wenn unsere Datenbank wächst. Dies ist die Python-Methode, die wir zuvor zur Berechnung der Kosinus-Ähnlichkeit zwischen aufeinanderfolgenden Vektoren verwendet haben:

def vector_similarity(self, vectors_df):
        """Compute cosine similarity between consecutive vectors"""
        similarities = []
        for i in range(1, len(vectors_df)):
            vec1 = vectors_df.iloc[i-1].values
            vec2 = vectors_df.iloc[i].values
            cos_sim = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
            angle_deg = np.degrees(np.arccos(np.clip(cos_sim, -1, 1)))
            similarities.append({

                'date': vectors_df.index[i],
                'cosine_similarity': cos_sim,
                'angle_degrees': angle_deg,
                'stable': angle_deg < 30  # Threshold
            })

        return pd.DataFrame(similarities).set_index('date')

Unter den Array-Funktionen von DuckDB gibt es einen direkten Ersatz für diese Python-Logik: array_cosine_similarity(v1, v2). Beachten Sie, dass wir zur Berechnung der Kosinusähnlichkeit das Skalarprodukt der beiden Vektoren durch das Produkt ihrer Beträge dividieren. Dies ist die genaue mathematische Definition der Kosinus-Ähnlichkeit. Um unsere Methode vector_similarity in DuckDB nachzubilden, können wir Fensterfunktionen (LAG) verwenden, um den aktuellen Vektor mit dem vorherigen zu vergleichen, sodass DuckDB diese Array-Operationen mithilfe von SIMD (Single Instruction, Multiple Data) verarbeiten kann, was den Vorgang deutlich schneller macht als eine Python-Schleife, je größer unser Repository an historischen Daten und Trades wird.

Wir müssen außerdem berücksichtigen, dass DuckDB die Ausführung außerhalb des Arbeitsspeichers (Out-of-Core) unterstützt, d. h., es kann Datenmengen verarbeiten, die größer sind als unser verfügbarer Arbeitsspeicher. Andererseits ist unser derzeitiger Pandas-Ansatz durch den Speicher begrenzt. Um den Unterschied zwischen einer zeilenbasierten Python-Iteration und einer vektorisierten Engine eines OLAP-Systems zu verdeutlichen, können wir einen dritten Benchmark durchführen. Wir werden die native Python-/NumPy-Schleife (die in unserem beigefügten Skript „rwec.py“ verwendet wird) mit der DuckDB-Funktion „array_cosine_similarity“ vergleichen.

import duckdb
import pandas as pd
import numpy as np
import time

def generate_eigen_dataset(n_rows=1_000_000, vec_dim=2):
    """Generates a large set of synthetic eigenvectors for testing."""

    print(f"--- Generating {n_rows:,} eigenvectors (dim={vec_dim}) ---")
   
    # Generate random vectors and normalize them
    data = np.random.randn(n_rows, vec_dim)
    norms = np.linalg.norm(data, axis=1, keepdims=True)
    normalized_vecs = data / norms

    df = pd.DataFrame({
        'date': pd.date_range(start='2000-01-01', periods=n_rows, freq='h'),
        'vec': list(normalized_vecs)
    })

    return df

Denken Sie daran, die generierten Vektoren stets zu normieren, so wie wir es bei einem Johansen-Kointegrationstest tun würden. Bei der RWEC-Berechnung ist dies ein erforderlicher Schritt, da die Kosinus-Ähnlichkeitsformel den Winkel zwischen zwei Vektoren misst und davon abhängt, dass deren Beträge normiert sind.

Bei der statistischen Arbitrage stellt der Kointegrationsvektor das Absicherungsverhältnis zwischen den Vermögenswerten dar. Ein Eigenvektor wie [1, -2] stellt genau dieselbe Beziehung dar wie [10, -20]. Beide beschreiben ein Verhältnis von 1:2. Wenn unsere Berechnung mit dem gleitenden Fenster in einer Periode [1, -2] und in der nächsten Periode [10, -20] ergibt, hat sich die Beziehung eigentlich nicht verändert. Die Normalisierung stellt sicher, dass unser Benchmark die Richtung (die Hedge-Ratio) misst und nicht die willkürliche Skala des Ergebnisses des Johansen-Tests.

def benchmark_rwec(df):
    # --- SETUP DUCKDB ---

    con = duckdb.connect(":memory:")

    # Register the dataframe as a virtual table
    con.register("eigen_table", df)

    print("\n--- Running RWEC Benchmark (Cosine Similarity + Angle) ---")

    # 1. DuckDB Benchmark (Vectorized SQL)
    # Uses array_cosine_similarity and LAG to compare consecutive rows

    duck_query = """
    SELECT AVG(DEGREES(ACOS(inner_sim)))
    FROM (
        SELECT 
            LEAST(GREATEST(array_cosine_similarity(
                vec::DOUBLE[2], 
                LAG(vec::DOUBLE[2]) OVER (ORDER BY date)
            ), -1), 1) as inner_sim
        FROM eigen_table
    ) 
    WHERE inner_sim IS NOT NULL
    """ 

    start = time.perf_counter()
    duck_res = con.execute(duck_query).fetchone()[0]
    duck_time = time.perf_counter() - start
    print(f"DuckDB Time: {duck_time:.4f}s (Result Avg Angle: {duck_res:.2f}°)")

    # 2. Python/NumPy Loop Benchmark
    # We iterate through the dataframe as in the rwec.py script
    start = time.perf_counter()
    
    similarities = []

    # Replicating the logic from rwec.py vector_similarity()
    vecs = np.stack(df['vec'].values)

    for i in range(1, len(vecs)):
        vec1 = vecs[i-1]
        vec2 = vecs[i]

        # Manual cosine similarity calculation

        cos_sim = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
        angle_deg = np.degrees(np.arccos(np.clip(cos_sim, -1, 1)))
        similarities.append(angle_deg)
      
    py_res = np.mean(similarities)
    py_time = time.perf_counter() - start

    print(f"Python Loop Time: {py_time:.4f}s (Result Avg Angle: {py_res:.2f}°)")

    print(f"\n[WINNER]: DuckDB is {py_time/duck_time:.1f}x faster for RWEC logic.")

if __name__ == "__main__":

    # Test with 500,000 rows to see the gap

    eigen_df = generate_eigen_dataset(n_rows=500_000)
    benchmark_rwec(eigen_df)

Dieses Skript ist hier als bench_rwec.py angehängt. Wenn Sie das Programm auf einem leistungsschwachen Rechner ausführen, sollten Sie in etwa solche Ergebnisse erhalten.

--- Generating 500,000 eigenvectors (dim=2) ---

--- Running RWEC Benchmark (Cosine Similarity + Angle) ---

DuckDB Time: 0.9603s (Result Avg Angle: 90.01°)
Python Loop Time: 21.9560s (Result Avg Angle: 90.01°)

[WINNER]: DuckDB is 22.9x faster for RWEC logic.

Im Gegensatz zu den beiden anderen Benchmarks, die wir oben gesehen haben, ist der Unterschied hier von Anfang an enorm, ohne dass die Eingabedaten über 500.000 zweidimensionale Eigenvektoren hinaus skaliert wurden. Die Funktion array_cosine_similarity ersetzt sowohl den manuellen np.dot als auch die Normierung (norm), die wir in Python verwendet haben, und durch den Wegfall der Python-Schleife (for i in range) wird der Overhead vermieden, der dadurch entsteht, dass der Interpreter bei jeder Zeile zwischen dem Python-Code und den zugrunde liegenden C-Bibliotheken hin- und herspringt. Außerdem verarbeitet DuckDB, wie oben bereits erwähnt, diese Arrays in Blöcken. Es berechnet nicht jeweils nur einen Winkel. Stattdessen berechnet es sie in parallelen Verarbeitungsblöcken, weshalb wir im Vergleich zu unserem bisherigen Python/SQLite-Algorithmus diese enormen Leistungssteigerungen feststellen.

Das oben Gesagte gilt für zweidimensionale Eigenvektoren, d. h. für zwei Symbole. Wenn wir Körbe mit drei oder mehr Symbolen betrachten möchten, können wir die Dimension einfach als Vektorvariable übergeben. Die Typumwandlung ::DOUBLE[n] muss mit der tatsächlichen Anzahl der Vermögenswerte in unserem Kointegrationsvektor übereinstimmen. Wir können eine Python-F-Zeichenkette verwenden, um die Dimension (vec_dim) direkt in die Abfrage einzufügen.

# vec_dim is the number of assets (2 for a pair)

vec_dim = len(df['vec'].iloc[0]) 

duck_query = f"""
SELECT AVG(DEGREES(ACOS(inner_sim)))
FROM (
    SELECT 
        LEAST(GREATEST(array_cosine_similarity(
            vec::DOUBLE[{vec_dim}], 
            LAG(vec::DOUBLE[{vec_dim}]) OVER (ORDER BY date)
        ), -1), 1) as inner_sim
    FROM eigen_table
) 
WHERE inner_sim IS NOT NULL
"""

Das ist wichtig, da die Funktion „array_cosine_similarity“ von DuckDB voraussetzt, dass die Eingabe-Arrays eine feste Länge haben. Wenn wir versuchen, ein DOUBLE[2] mit einem DOUBLE[3] zu vergleichen, löst die Engine einen Binder-Fehler aus.


Schlussfolgerung

In diesem abschließenden Artikel unserer Einführungsreihe zum Thema statistische Arbitrage anhand kointegrierter Aktien haben wir vorgeschlagen, eine spezielle Datenbank für die Datenanalyse in unsere Pipeline aufzunehmen. Konkret empfehlen wir den Einsatz des kostenlosen, quelloffenen und für den institutionellen Einsatz geeigneten OLAP-Systems DuckDB.

Wir haben darauf hingewiesen, dass bis vor einigen Jahren die größte Einstiegsbarriere für Privatanleger im Bereich der statistischen Arbitrage die Rechenleistung für die Echtzeit-Datenanalyse war und nicht die Ausführungsgeschwindigkeit. Um diesen Punkt zu untermauern, haben wir einige Schätzungen zu den Kosten ähnlicher Systeme um das Jahr 2010 herum vorgelegt und dabei kurz auf die technischen Herausforderungen bei deren Umsetzung eingegangen.

Um schließlich zu veranschaulichen, wie unser Schritt der Echtzeit-Datenanalyse von der Einführung des OLAP-Systems profitieren könnte, haben wir drei Benchmark-Tests vorgestellt, in denen die Leistung von DuckDB und SQLite bei den in unserem System am häufigsten verwendeten Berechnungen verglichen wurde: eine einfache Aggregation zur Berechnung des Durchschnittspreises, ein Join zwischen zwei Tabellen zum Vergleich historischer Kursdaten sowie eine RWEC-Berechnung unter Verwendung nativer DuckDB-Funktionen anstelle einer verschachtelten Python-Schleife.

Die Benchmark-Ergebnisse auf einem Low-End-Rechner zeigen, dass die Berechnungen mit DuckDB zwischen dem 2- und 23-Fachen schneller sind, wobei die Geschwindigkeit mit steigendem Volumen der Beispieldaten stets zunimmt. Dies bestätigt den Hauptgrund, warum wir die Einbindung dieses spezialisierten Systems in unsere Pipeline vorschlagen: Damit soll diese für Wachstum gerüstet und für große Datensätze zukunftssicher gemacht werden.

Dateiname Beschreibung
bench.py Python-Skript zur Durchführung eines einfachen Aggregations-Benchmarks zwischen SQLite und DuckDB
bench_asof.py Python-Skript zum Durchführen eines Benchmarks zwischen dem zeitbezogenen Standard-SQL-Join von SQLite und dem ASOF-JOIN von DuckDB
bench_rwec.py Python-Skript zur Durchführung eines Benchmarks zwischen einer verschachtelten Schleife in NumPy und der Kosinus-Ähnlichkeitsfunktion von DuckDB zur Berechnung des RWEC

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

Beigefügte Dateien |
Einführung in MQL5 (Teil 41): Leitfaden für Anfänger zur Dateiverwaltung in MQL5 (III) Einführung in MQL5 (Teil 41): Leitfaden für Anfänger zur Dateiverwaltung in MQL5 (III)
Erfahren Sie, wie Sie eine CSV-Datei in MQL5 einlesen und deren Handelsdaten in dynamischen Arrays organisieren. Dieser Artikel zeigt Schritt für Schritt, wie man Dateielemente zählt, alle Daten in einem einzigen Array speichert und jede Spalte in eigene Arrays aufteilt, wodurch die Grundlage für fortgeschrittene Analysen und die Visualisierung der Handelsperformanz geschaffen wird.
Erstellen benutzerdefinierter Indikatoren in MQL5 (Teil 8): Integration von Volumendaten für eine tiefere Marktprofilanalyse Erstellen benutzerdefinierter Indikatoren in MQL5 (Teil 8): Integration von Volumendaten für eine tiefere Marktprofilanalyse
In diesem Artikel erweitern wir den hybriden Marktprofil-Indikator Time Price Opportunity (TPO) in MQL5 durch die Einbindung von Volumendaten, um volumenbasierte Kontrollpunkte, Wertbereiche und den volumengewichteten Durchschnittspreis mit anpassbaren Hervorhebungsoptionen zu berechnen. Das System bietet erweiterte Funktionen wie die Erkennung der Initial Balance, Projektionslinien wichtiger Kursniveaus, Split-Profile sowie alternative TPO-Symbole wie Quadrate oder Kreise, um die visuelle Analyse über mehrere Zeitrahmen hinweg zu verbessern.
Trading-Disziplin in Code umsetzen (Teil 2): Entwicklung eines MQL5-Systems zur Durchsetzung eines täglichen Handelslimits für alle Trades in MQL5 Trading-Disziplin in Code umsetzen (Teil 2): Entwicklung eines MQL5-Systems zur Durchsetzung eines täglichen Handelslimits für alle Trades in MQL5
Wir haben ein System entwickelt, das ein tägliches Handelslimit durchsetzt, damit Sie Ihre Handelsregeln einhalten können. Es überwacht alle über das Konto ausgeführten Trades und greift automatisch ein, sobald das festgelegte Limit erreicht ist, wodurch weitere Aktivitäten verhindert werden. Durch die direkte Integration der Kontrollmechanismen in die Plattform stellt das System sicher, dass die Disziplin auch bei steigendem Marktdruck gewahrt bleibt.
Larry Williams’ Marktgeheimnisse (Teil 12): Kontextbezogener Handel von „Smash Day“-Umkehrungen Larry Williams’ Marktgeheimnisse (Teil 12): Kontextbezogener Handel von „Smash Day“-Umkehrungen
Dieser Artikel zeigt, wie man die Umkehrmuster nach Larry Williams’ Smash Day in MQL5 innerhalb eines strukturierten Kontexts automatisieren kann. Wir implementieren einen Expert Advisor, der Setups über ein begrenztes Zeitfenster validiert, Einstiegspunkte anhand der auf dem Supertrend basierenden Trendrichtung und Wochentagsfiltern abstimmt und Einstiege entweder beim Durchbruch eines Niveaus oder nach bestätigtem Schlusskurs der Bar unterstützt. Der Code erlaubt jeweils nur eine offene Position und unterstützt sowohl risikobasierte als auch feste Positionsgrößen. Es werden eine schrittweise Entwicklung, ein Backtesting-Verfahren sowie reproduzierbare Einstellungen bereitgestellt.