Reproduzierbarer Backtest in Python ohne API-Schlüssel
Reproduzierbarer Backtest in Python ohne API-Schlüssel: feste Installation, deterministische Beispieldaten und eine nüchterne Einordnung dessen, was eine Equity-Kurve verschweigt.
Ein reproduzierbarer Backtest in Python lässt sich auf einem sauberen Rechner von einer fremden Person erneut ausführen und liefert exakt dieselben Zahlen. Dabei stehen weder ein Benutzerkonto noch ein API-Schlüssel im Weg. Die meisten Tutorials scheitern bereits in der ersten Zeile. Ein Live-Download liefert dem nächsten Leser eine leicht andere Kurshistorie als zuvor dem Autor. Dieser Leitfaden legt eine Open-Source-Engine fest: quantjourney-bt in Version 0.12.4. Anschließend wird das mitgelieferte Beispiel ohne Zugangsdaten ausgeführt. Danach zeigen wir anhand realer Marktdaten, was selbst ein einzelner sauberer Durchlauf nicht beantworten kann.
Was macht einen Backtest reproduzierbar?
Reproduzierbarkeit hat hier eine eng gefasste, überprüfbare Bedeutung: Eine zweite Person führt auf einem sauberen Rechner einen Befehl aus und erhält Ihre Zahlen bis auf die Nachkommastelle. Zwei alltägliche Faktoren können das verhindern.
Der erste Faktor ist der Code. Eine Bibliothek mit einer Version im 0.x-Stadium bietet zwischen Minor-Releases keine Kompatibilitätszusage. Eine Umbenennung, ein geänderter Standardwert oder eine andere Spaltenreihenfolge kann dazu führen, dass Ihr Skript weiterhin läuft, aber unbemerkt andere Ergebnisse meldet.
Der zweite Faktor sind die Daten. Ein Tutorial, dessen erste Zeile einen Live-Download ausführt, war nie reproduzierbar. Datenanbieter überarbeiten historische Daten, passen sie an Aktiensplits an und schließen Lücken nachträglich. Dadurch liefert dasselbe Skript einen Monat später andere Zahlen. Im Nachhinein lässt sich nicht trennen, ob eine Codeänderung oder eine Datenänderung die Ursache ist.
Das nachahmenswerte Muster dieses Projekts ist die Kombination aus einer festgelegten Engine-Version und einem kleinen Datensatz, der direkt im Paket mitgeliefert wird.
Installation festschreiben: pip install quantjourney-bt==0.12.4
quantjourney-bt ist der QuantJourney-Backtester. Er steht unter der Apache License 2.0 und erfordert Python 3.11 oder neuer. Version 0.12.4 wurde am 21. Juli 2026 veröffentlicht. Auf diese Version beziehen sich alle folgenden Befehle, wie im August 2026 dokumentiert.
Arbeiten Sie in einer isolierten Umgebung. python3 -m venv .venv erstellt eine solche Umgebung, source .venv/bin/activate aktiviert sie, und python -m pip install -U pip aktualisiert darin das Installationsprogramm. Installieren Sie anschließend exakt diese Version: pip install quantjourney-bt==0.12.4.
In der Projektdokumentation ist die ungepinnte Form pip install quantjourney-bt angegeben. Für den Bestandteil ==0.12.4 sind Sie verantwortlich. Bei Versionen vor 1.0 ist diese Festlegung besonders wichtig. Halten Sie den Pin dort fest, wo die nächste Person ihn findet: pip freeze > requirements.txt erfasst alle aufgelösten Abhängigkeiten, einschließlich derjenigen, die Sie nicht ausdrücklich angegeben haben.
Es gibt zwei optionale Extras. pip install "quantjourney-bt[wf]" fügt Optuna für die Beispiele zu Walk-forward-Analysen und Optimierung hinzu. pip install "quantjourney-bt[data]" fügt einen auf yfinance basierenden Fallback hinzu, der für Benchmarks verwendet wird.
Apache-2.0 ist eine freizügige Lizenz. Sie dürfen den Code kommerziell nutzen und verändern. Bei jeder Weitergabe müssen Sie die Lizenz- und Hinweisdateien beibehalten. Außerdem gewähren die Mitwirkenden ausdrücklich Patentrechte.
So führen Sie das gebündelte SMA-Beispiel ohne API-Key aus
Das Repository enthält neben fünfzig ausführbaren Beispielstrategien auch ein Startskript. Die Beispiele sind auf einen gewichts- und einen orderbasierten Pfad verteilt. Darunter befinden sich fünf Walk-Forward-Workflows. ./strategy.sh --list gibt den Katalog aus. ./strategy.sh example_weights_01_sma_daily --check importiert eine einzelne Strategie und greift überhaupt nicht auf Daten zu. Damit lässt sich am schnellsten prüfen, ob die Installation fehlerfrei ist.
Der eigentliche Demolauf besteht aus einer Zeile: ./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample
Auf den Parameter --sample-data kommt es an. Das Projekt beschreibt den dahinterliegenden Datensatz so:
Der Beispieldatensatz ist bewusst klein und reproduzierbar. Er eignet sich zur Prüfung der Installation, zur Berichtserstellung und zum Nachvollziehen des Engine-Ablaufs, ohne ein Konto anzulegen.
Quelle: quantjourney-bt README, Version 0.12.4, abgerufen am 6. August 2026.
Der Lauf schreibt ein Verzeichnis statt eines Ergebnisses in der Konsole: summary.txt und summary.json, eine metrics.csv, eine equity_curve.csv neben ihrer equity_curve.png, eine dashboard.html, ein Verzeichnis plots/ sowie eine run_metadata.json, in der die Konfiguration des Laufs festgehalten wird. Die letzte Datei wird von den meisten Nutzern übersprungen. Sie sorgt jedoch dafür, dass sich ein Ergebnis auch ein Jahr später noch prüfen lässt.
Interpretieren Sie die resultierenden Kennzahlen sachlich. Der gebündelte Datensatz ist klein und dient nur der Veranschaulichung. Die in summary.txt ausgegebene Sharpe Ratio und der maximale Drawdown beschreiben daher lediglich eine Beispieldatei. Sie sind kein Beleg für die Qualität einer Strategie. Sie als Ergebnis zu behandeln, wäre der erste mögliche Fehler.
Der Lauf bestätigt dennoch wichtige Punkte: Die Installation funktioniert. Außerdem erzeugt der vollständige Engine-Ablauf auf Ihrem Rechner alle Artefakte – vom Signal über die Zielgewichtungen bis zum rekonstruierten Portfoliowert – ohne eine einzige Zugangsdaten. Für Backtests mit realen historischen Daten gibt es einen authentifizierten Zugang zum eigenen Datenservice des Projekts. Dieser Weg ist dokumentiert. Dieser Leitfaden endet hier, bei dem Teil, der keine Verbindung zu einem externen Dienst benötigt.
Was eine einzelne In-Sample-Equity-Kurve nicht aussagt
Der Testlauf ergibt eine einzelne Equity-Kurve. Gemessen an realen Marktdaten statt an einer Demodatei zeigt sich, was diese Kurve nicht leisten kann.
Das folgende Panel verwendet dieselbe Idee wie die Beispielstrategie: einen gleitenden Durchschnitt über 20 Handelssitzungen, der einen gleitenden Durchschnitt über 50 Handelssitzungen kreuzt. Die Regel wird auf SPY angewendet. Die Ergebnisse werden für jedes Kalenderjahr von 2017 bis 2025 separat ausgewiesen. Die Position für jede Sitzung wird anhand des Schlusskurses der vorherigen Sitzung festgelegt. Die Regel handelt daher nie auf Basis einer Zahl, die zum Entscheidungszeitpunkt noch nicht vorlag.
Das exakte SQL hinter jeder Zahl
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
countIf(long_today != long_prior) AS crossover_count
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearEine unveränderte Regel, gemessen 9 separate Male. Lesen Sie zunächst die beiden Prozent-Spalten von oben nach unten, bevor Sie etwas anderes betrachten. In 2017 beendete die Regel das Jahr bei 16% gegenüber 19.4% beim Halten von SPY über denselben Zeitraum. In 2025 lauten dieselben beiden Spalten 10.4% und 16.4%. Der Code ist in beiden Zeilen identisch. Nur das Zeitfenster wurde geändert.
Die Spalte zu den Kreuzungen zeigt, wie schmal die Datengrundlage ist. 4 Positionsänderungen über 2025 bedeuten, dass eine vollständige Jahres-Equity-Kurve auf einer Handvoll Entscheidungen beruht. Das ist eine sehr kleine Stichprobe, um daraus ein Ergebnis abzuleiten.
Funktioniert dieselbe Regel bei anderen Titeln genauso?
Das Zeitfenster zu verändern, ist eine Möglichkeit, eine einzelne Kurve zu untersuchen. Das Anlageuniversum zu verändern, ist die andere. Das folgende Panel hält die Parameter konstant und wendet dieselbe Regel auf fünf liquide Titel in den fünf Kalenderjahren von 2021 bis 2025 an.
Das exakte SQL hinter jeder Zahl
WITH daily AS
(
SELECT
ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'KO')
AND window_start >= '2020-07-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY ticker, d
),
averaged AS
(
SELECT
ticker,
d,
px,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (PARTITION BY ticker ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
ticker,
d,
px,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
ticker AS symbol,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
round(avg(long_prior) * 100, 0) AS days_long_pct
FROM positioned
WHERE px_prior > 0
AND d >= toDate('2021-01-01')
GROUP BY symbol
ORDER BY rule_pct DESCQQQ steht im Panel mit 40.2% an der Spitze. Die unterste Zeile, KO, kommt auf 3.8%. Die Spalte days_long_pct zeigt, in welchem Anteil des Zeitfensters die jeweilige Variante überhaupt investiert war. Für die oberste Zeile sind es 67%. Ein Parametersatz, fünf Anlageuniversen und eine ausreichend große Streuung: Wer den Gewinner im Nachhinein auswählt, sagt damit nichts über den noch nicht durchgeführten Lauf aus.
Das ist keine Empfehlung, eine Crossover-Strategie zu handeln. Der Crossover dient im Backtest als Messlatte. Und der Backtest ist das, was wir messen.
Wo sich Look-ahead Bias in einem Backtest mit Zielgewichtungen einschleicht
Eine gewichtsbasierte Engine wandelt ein Signal in Zielgewichtungen um, simuliert Ausführungen anhand dieser Gewichtungen und berechnet anschließend den Portfoliowert aus den resultierenden Positionen neu. Der Fehler versteckt sich in der Verknüpfung von Signal und Gewichtung. Wenn die heutige Gewichtung aus dem heutigen Schlusskurs abgeleitet wird und anschließend die heutige Rendite erzielt, hat der Backtest mit Informationen gehandelt, die zum Zeitpunkt der Orderaufgabe noch nicht vorlagen. Das ist Look-ahead Bias. Der Fehler löst keine Meldung aus. Er lässt lediglich alles besser aussehen.
Die folgende Übersicht führt beide Varianten derselben Regel über dieselbe SPY-Historie aus.
Das exakte SQL hinter jeder Zahl
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS next_bar_pct,
round((exp(sum(log(if(long_today = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS same_bar_pct,
round(abs(exp(sum(log(if(long_today = 1, px / px_prior, 1.0))))
- exp(sum(log(if(long_prior = 1, px / px_prior, 1.0))))) * 100, 1) AS gap_pp
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearIn 2017 wies die Version mit der vorherigen Sitzung 16% aus, während die Version mit derselben Sitzung 17.1% auswies. Der Abstand betrug 1.1 Prozentpunkte. In 2025 maß der Abstand zwischen beiden 1.6 Prozentpunkte. Nur eine dieser Spalten kann von einer Maschine erzeugt worden sein, die noch nicht wusste, wo die Sitzung schließen würde. Die Differenz zwischen beiden ist reine Abrechnung. Dahinter stehen weder eine Anlageidee noch Können oder ein tatsächlicher Trade.
Diese Engine legt ihre eigene Position zur Timing-Frage offen. Das ist mehr wert als ein Versprechen:
Bei Ausführungen zur Eröffnung berücksichtigt die auf der Handelsspanne basierende Slippage nur den zuvor abgeschlossenen Balken. Die verfügbare Volumenkapazität wird aus verzögerten Beobachtungen prognostiziert. Die Engine verwendet weder das spätere Tageshoch, Tagestief oder den Schlusskurs noch das Tagesvolumen der betreffenden Sitzung.
Quelle: quantjourney-bt README, Version 0.12.4, abgerufen am 6. August 2026.
Eine dokumentierte Annahme lässt sich anhand des bereits installierten Quellcodes überprüfen. Eine undokumentierte Annahme ist eine Vermutung.
Warum das Walk-forward-Extra existiert
Die Walk-forward-Beispiele WF01 bis WF05 werden zusammen mit dem [wf]-Extra und dessen Optuna-Abhängigkeit bereitgestellt. Beim Walk-forward-Verfahren werden Parameter anhand eines historischen Zeitabschnitts angepasst und anschließend am darauffolgenden Abschnitt geprüft. Danach wird das Paar weitergerollt und der Vorgang wiederholt. Die rollierenden und die expandierenden Varianten unterscheiden sich darin, ob das Anpassungsfenster beim Vorrücken seine ältesten Daten verliert. Ein weiteres Beispiel fügt an jeder Grenze eine Purge- und Embargo-Phase ein. Dabei werden die Beobachtungen unmittelbar an der Trennstelle entfernt. So kann der Anpassungsabschnitt nicht in den Abschnitt hineinleaken, an dem er geprüft wird.
Nichts davon macht aus einer schwachen Idee eine funktionierende Strategie. Statt einer einzelnen Kennzahl erhalten Sie eine Verteilung von Kennzahlen, die Sie hinterfragen können. Das ist der gesamte Fortschritt. Der nächste Schritt ist weiterhin kein Handel mit echtem Geld: Paper-Trading vor dem Einsatz von echtem Geld misst, was ein Backtest strukturell nicht erfassen kann. Dazu gehört zunächst die Frage, ob Ihre Order annähernd zu dem Preis ausgeführt wird, den der Simulator angenommen hat. Schutzschalter für Trading-Bots decken ab, was Ihr Code an dem Tag tut, an dem er es nicht tut. Für die statistischen Grundlagen dahinter gehen unsere Hinweise zum Open-Source-Buch über quantitativen Handel noch eine Ebene tiefer.
FAQ
Können Sie eine Strategie ohne API-Schlüssel backtesten?
Ja. quantjourney-bt liefert hinter einem --sample-data-Flag einen gebündelten Beispieldatensatz mit. Die Beispielstrategien laufen damit ohne Konto und ohne Zugangsdaten. Der Datensatz ist klein und dient der Veranschaulichung. Betrachten Sie diesen Lauf daher als Prüfung der Installation und der Datenpipeline, nicht als Beleg für die Qualität einer Strategie.
Warum sollte die Version eines Python-Backtesting-Pakets festgeschrieben werden?
Ein Paket der Version 0.x bietet zwischen Minor-Releases keine Kompatibilitätsgarantie. Eine geänderte Standardeinstellung oder eine umbenannte Kennzahl wird zudem nicht unbedingt angekündigt. Wenn Sie die Version mit pip install quantjourney-bt==0.12.4 festschreiben und die Umgebung in einer Requirements-Datei dokumentieren, können Sie ein heute erzieltes Ergebnis im nächsten Jahr auf der Engine reproduzieren, mit der es ursprünglich erzeugt wurde.
Unter welcher Lizenz wird quantjourney-bt veröffentlicht?
Unter der Apache License 2.0. Sie erlaubt die kommerzielle Nutzung und Änderung. Bei einer Weiterverbreitung müssen die Lizenz- und Hinweisdateien erhalten bleiben. Außerdem umfasst sie eine ausdrückliche Patenterteilung durch die Mitwirkenden. Version 0.12.4 wurde am 21. Juli 2026 veröffentlicht und erfordert Python 3.11 oder neuer.
Bedeutet ein starkes Backtest-Ergebnis, dass die Strategie funktioniert?
Nein. Ein Backtest ist eine einzelne Messung über einen bestimmten Zeitraum und für ein bestimmtes Anlageuniversum. Die obigen Panels zeigen, dass dieselbe unveränderte Regel für einen Ticker sehr unterschiedliche Jahreswerte und für fünf verschiedene Namen ebenfalls deutlich unterschiedliche Ergebnisse liefert. Genau diese Unterschiede sollen Walk-forward-Validierung und Out-of-Sample-Tests sichtbar machen.
So wurden die obigen Panels berechnet
Die Tagesschlusskurse entsprechen für jedes Datum dem letzten regulären Minuten-Print der Sitzung. Sie wurden nach New Yorker Ortszeit zwischen 9:30 Uhr und 16:00 Uhr ermittelt. Dadurch werden Handelstage mit vorzeitigem Börsenschluss korrekt berücksichtigt, ohne eine feste Sitzungslänge zu hinterlegen. Der schnelle gleitende Durchschnitt umfasst 20 Sitzungen, der langsame 50 Sitzungen. Beide sind einfache gleitende Durchschnitte. Die ersten 49 Sitzungen jeder Zeitreihe dienen als Aufwärmphase, in der keine Position gehalten wird. Die Jahreswerte berechnen den kumulierten Close-to-Close-Ertrag jeder Sitzung, in der die Regel long war. Die Hold-Spalte kumuliert dagegen zum Vergleich jede Sitzung desselben Jahres. Die fünf Namen im Querschnitt wurden anhand durchgängiger Historien ohne Aktiensplit innerhalb des Zeitfensters ausgewählt. Deshalb muss die Schlusskursreihe nicht angepasst werden. Die Zeitfenster liegen fest in der Vergangenheit. Daher liefern diese Panels bei jeder erneuten Generierung dieselben Zahlen.
Unter jedem Panel steht die zugehörige SQL-Abfrage vollständig. Dadurch lassen sich die Zahlen auf dieser Seite ebenso erneut berechnen wie die Ergebnisse einer Installation mit festgeschriebener Version. Um eine Regel über Ihr eigenes Zeitfenster zu messen, bevor Sie Backtest-Code schreiben, stellen Sie die Frage in einfachem Englisch auf dem Strasmore-Terminal.