Bootstrap-Konfidenzintervalle für Backtests verstehen
Eine Aktienkurve ist nur eine Stichprobe. So berechnen und interpretieren Sie ein Bootstrap-Konfidenzintervall für die Sharpe-Ratio eines Backtests.
Ein Konfidenzintervall für einen Backtest beantwortet eine Frage: Wie viel der Aktienkurve ist auf die Strategie zurückzuführen, und wie viel auf den konkreten historischen Zeitraum, über den sie lief? Beim Bootstrapping eines Backtests wird dieses Intervall ermittelt, indem die Renditeserie wiederholt neu gezogen wird. Für jede neue Stichprobe wird die Kennzahl erneut berechnet. Anschließend werden die Perzentile der Ergebnisse ausgewertet. Eine über ein einziges Jahr täglicher Beobachtungen gemessene Sharpe Ratio von 1,4 weist ein 95-Prozent-Intervall auf, das breit genug ist, um null einzuschließen. Die folgenden Grafiken zeigen, warum.
Warum eine Aktienkurskurve nur eine Stichprobe ist
Ein Backtest liefert für jede Kennzahl genau einen Wert: eine Sharpe-Ratio, eine annualisierte Rendite, den größten Drawdown und die Trefferquote. Keine dieser Kennzahlen entspricht dem wahren Wert der Strategie. Jede ist eine Schätzung auf Basis einer begrenzten Zahl von Handelstagen. Ein anderer Zeitraum gleicher Länge hätte ein anderes Ergebnis geliefert.
Die folgende Darstellung nimmt die Strategie vollständig aus der Betrachtung. Sie misst die einfachstmögliche Position: das Halten von SPY, jeweils für ein Kalenderjahr, auf Basis der Kursrenditen von Schlusskurs zu Schlusskurs.
Das exakte SQL hinter jeder Zahl
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2011-12-01'
AND date < '2026-01-01'
)
SELECT
toString(toYear(date)) AS year,
count() AS obs_count,
round(avg(ret) * 252 * 100, 2) AS ann_return_pct,
round(stddevSamp(ret) * sqrt(252) * 100, 2) AS ann_vol_pct,
round(avg(ret) / stddevSamp(ret) * sqrt(252), 2) AS sharpe_ratio
FROM daily
WHERE date >= '2012-01-01'
AND ret IS NOT NULL
GROUP BY year
ORDER BY yearJede Zeile umfasst etwa 250 Handelssitzungen und entspricht damit ungefähr einem Standard-Handelsjahr. Die Position wurde in keinem dieser Zeiträume verändert. In 2012 lag die annualisierte Sharpe-Ratio bei 1.06. In 2025 lag sie bei 0.88. Insgesamt wurden 14 Jahre in der Grafik dargestellt. Das Halten eines breiten Index über ein Jahr verändert die zentrale Kennzahl stärker, als die meisten Leser noch als bloßes Rauschen einordnen würden. Ein Strategiე-Backtest gleicher Länge übernimmt diese Unsicherheit mindestens in diesem Umfang. Die der Spalte zugrunde liegende Annualisierungskonvention wird in unserem Leitfaden zur Sharpe-Ratio erläutert. Die Berechnung der Periodenrenditen wird in So werden Monatsrenditen gemessen beschrieben.
Wie groß ist die Bandbreite eines Backtest-Sharpe?
Der Standardfehler einer Sharpe-Schätzung sinkt grob mit der Quadratwurzel der Zahl der Beobachtungen. Statt sich auf diese Formel zu stützen, messen Sie die Streuung direkt. Teilen Sie zwanzig Jahre Handelstage in nicht überlappende Zeitfenster mit fester Länge. Berechnen Sie für jedes Fenster die annualisierte Sharpe Ratio. Prüfen Sie anschließend, wie weit diese Werte auseinanderliegen.
Das exakte SQL hinter jeder Zahl
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2005-12-01'
AND date < '2026-01-01'
),
indexed AS
(
SELECT
ret,
row_number() OVER (ORDER BY date) AS i
FROM daily
WHERE date >= '2006-01-01'
AND ret IS NOT NULL
),
blocks AS
(
SELECT
w,
intDiv(i, w) AS blk,
count() AS n,
avg(ret) / nullIf(stddevSamp(ret), 0) * sqrt(252) AS sharpe
FROM indexed
CROSS JOIN (SELECT arrayJoin([21, 63, 126, 252, 504]) AS w) AS ws
GROUP BY w, blk
HAVING n = w
)
SELECT
concat(toString(w), ' sessions') AS horizon,
count() AS block_count,
round(quantileDeterministic(0.05)(sharpe, blk), 2) AS sharpe_p05,
round(quantileDeterministic(0.50)(sharpe, blk), 2) AS sharpe_p50,
round(quantileDeterministic(0.95)(sharpe, blk), 2) AS sharpe_p95,
round(quantileDeterministic(0.95)(sharpe, blk)
- quantileDeterministic(0.05)(sharpe, blk), 2) AS sharpe_band_width
FROM blocks
WHERE sharpe IS NOT NULL
GROUP BY w
ORDER BY wBei 21 sessions pro Fenster reicht der 5. bis 95. Perzentilbereich der gemessenen Sharpe Ratios von -3.37 bis 6.97. Das entspricht einer Spanne von 10.34 Sharpe-Punkten über 238 Fenster. Verlängern Sie jedes Fenster auf 504 sessions, sinkt die Spanne auf 1.67 Punkte. Die Messung basiert dann nur noch auf 8 Fenstern. Zwei Dinge verändern sich gleichzeitig. Mit wachsender Stichprobengröße wird die Schätzung präziser. Gleichzeitig bricht die Zahl der unabhängigen Stichproben ein, die die Historie liefern kann. Diese Spannung ist das gesamte Thema.
So erstellen Sie ein Bootstrap-Konfidenzintervall für einen Backtest
Das Verfahren ist kurz genug, um es vollständig aufzuschreiben.
- Beginnen Sie mit der realisierten Renditeserie der Strategie. Sie enthält pro Periode einen Wert, insgesamt N Werte.
- Ziehen Sie zufällig N Renditen aus dieser Liste, mit Zurücklegen. Einige Werte erscheinen doppelt, andere gar nicht.
- Berechnen Sie die Kennzahl für diese Stichprobe erneut.
- Wiederholen Sie den Vorgang mehrere tausend Mal und speichern Sie jeden Wert.
- Sortieren Sie die gespeicherten Werte. Lesen Sie das 2,5. und 97,5. Perzentil für ein 95-Prozent-Intervall ab.
Setzen Sie den Zufallszahlengenerator vor Schritt 2 auf einen festen Seed und notieren Sie den Seed neben dem veröffentlichten Intervall. Ein Bootstrap ist eine Monte-Carlo-Schätzung. Zwei Läufe ohne festen Seed unterscheiden sich in den letzten Stellen. Ein Reviewer, der Ihr Intervall nicht erneut berechnen kann, hat keine Möglichkeit, es zu überprüfen. Das entspricht derselben Disziplin, die in einem reproduzierbaren Backtest-Setup beschrieben wird.
Die durchschnittliche Rendite und die Sharpe Ratio lassen sich in Schritt 2 problemlos verarbeiten, weil beide die Renditeliste als Menge von Werten verwenden. Für den maximalen Drawdown gilt das nicht. Der Drawdown berücksichtigt die Reihenfolge der Renditen. Eine Stichprobe, die Renditen neu anordnet, kann einen maximalen Drawdown ergeben, der in keiner Reihenfolge der tatsächlich ausgeführten Trades aufgetreten ist. Es lohnt sich trotzdem, den Drawdown zu bootstrappen, sofern das Ergebnis klar bezeichnet wird: Es zeigt die Verteilung des Drawdowns über neu angeordnete Historien und ist keine Prognose des nächsten Drawdowns. Die Definition finden Sie unter maximaler Drawdown.
Warum der IID-Bootstrap für Marktrenditen falsch ist
Schritt 2 unterstellt, dass jede Rendite unabhängig und identisch verteilt ist. Das ist die IID-Annahme. Tagesrenditen verletzen diese Annahme auf eine Weise, die die Intervallbreite verändert. Vorzeichenbehaftete Renditen weisen nur eine schwache Eintagespersistenz auf. Ihre Beträge clustern jedoch: Auf große Bewegungen folgen häufig weitere große Bewegungen, während ruhige Tage in Serien auftreten.
Das exakte SQL hinter jeder Zahl
WITH daily AS
(
SELECT
ticker,
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('SPY', 'MSFT', 'KO', 'XOM', 'JNJ', 'PG')
AND date >= '2015-11-01'
AND date < '2026-01-01'
),
lagged AS
(
SELECT
ticker,
date,
ret,
lagInFrame(ret, 1) OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS ret_prev
FROM daily
WHERE ret IS NOT NULL
AND abs(ret) < 0.35
)
SELECT
ticker,
count() AS obs_count,
round(corr(ret, ret_prev), 3) AS return_autocorr,
round(corr(abs(ret), abs(ret_prev)), 3) AS abs_return_autocorr
FROM lagged
WHERE date >= '2016-01-01'
AND ret_prev IS NOT NULL
GROUP BY ticker
ORDER BY abs_return_autocorr DESCFür SPY beträgt die Autokorrelation erster Ordnung der absoluten Tagesrenditen 0.366 über 2514 Sitzungen. Zum Vergleich: Bei den vorzeichenbehafteten Renditen an denselben Tagen liegt sie bei -0.133. Vergleichen Sie im Chart die beiden Balken für jeden Titel. Durch das Mischen einer Renditeserie wird dieses Clustering zerstört. Ein IID-Bootstrap mit diesen Daten weist daher ein engeres Intervall aus, als es die Stichprobe rechtfertigt. Der Fehler wirkt in die ungünstigste Richtung: Er lässt die Strategie besser erscheinen.
Der Moving-Block-Bootstrap und die Wahl der Blocklänge
Die Lösung besteht darin, zusammenhängende Blöcke statt einzelner Renditen neu zu ziehen. Wählen Sie eine Blocklänge L. Ziehen Sie zufällig und mit Zurücklegen Blöcke aus jeweils L aufeinanderfolgenden Renditen. Fügen Sie sie aneinander, bis die synthetische Zeitreihe N Beobachtungen umfasst. Die Abhängigkeit innerhalb eines Blocks bleibt vollständig erhalten. Die Renditen behalten ihre ursprüngliche Reihenfolge. Nur die Übergänge zwischen den Blöcken sind künstlich.
Die Blocklänge führt zu einem Zielkonflikt zwischen zwei Fehlern. Keine Einstellung vermeidet beide. Kurze Blöcke verhalten sich wie beim IID-Bootstrap und unterschätzen das Intervall. Das ist ein Bias. Lange Blöcke erhalten mehr Abhängigkeit. Allerdings stehen dann weniger unterschiedliche Blöcke zur Auswahl. Dadurch wiederholt jedes Resampling große Teile derselben Historie. Das Intervall selbst wird verrauscht. Das ist Varianz. Veröffentlichten Faustregeln zufolge sollte die Länge mit der Kubikwurzel von N skalieren. Betrachten Sie sie als Ausgangspunkt.
Eine kostengünstigere Diagnose besteht darin zu prüfen, wie sich die Varianz über verschiedene Zeithorizonte skaliert. Bei Unabhängigkeit entspricht die Varianz einer Renditesumme über k Tage dem k-Fachen der Eintagesvarianz. Das Verhältnis beträgt dann 1.
Das exakte SQL hinter jeder Zahl
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2005-12-01'
AND date < '2026-01-01'
),
indexed AS
(
SELECT
ret,
row_number() OVER (ORDER BY date) AS i
FROM daily
WHERE date >= '2006-01-01'
AND ret IS NOT NULL
),
base AS
(
SELECT varSamp(ret) AS var_1d FROM indexed
),
blocks AS
(
SELECT
k,
intDiv(i, k) AS blk,
count() AS n,
sum(ret) AS block_ret
FROM indexed
CROSS JOIN (SELECT arrayJoin([2, 3, 5, 10, 21, 42, 63]) AS k) AS ks
GROUP BY k, blk
HAVING n = k
)
SELECT
concat(toString(k), ' sessions') AS block_length,
count() AS block_count,
round(varSamp(block_ret) / (k * any(var_1d)), 3) AS variance_ratio
FROM blocks
CROSS JOIN base
GROUP BY k
ORDER BY kBei 2 sessions betrug das Verhältnis 0.844. Bei 63 sessions betrug es 0.584. Die Berechnung basiert auf 78 nicht überlappenden Blöcken. Werte nahe 1 zeigen, dass sich die Summe über diesen Zeithorizont so skaliert, wie es bei unabhängigen Ziehungen der Fall wäre. Werte abseits von 1 markieren die Zeithorizonte, über die die Abhängigkeit noch wirkt. Dieser Bereich muss von der Blocklänge abgedeckt werden. Wählen Sie den kürzesten Block, der den gesamten Bereich erfasst. Prüfen Sie anschließend, wie stark die Zahl der Blöcke gesunken ist.
Resampling nach Gruppen und die Anzahl neben jedem Intervall
Resampling innerhalb von Gruppen, nach Volatilitätsregime oder nach Kalendermonat, bewahrt die Bedingung, die die Fragestellung relevant gemacht hat. Wenn die These lautet, dass eine Strategie ihr Sharpe-Ratio in Regimen mit hoher Volatilität erzielt, prüft ein Intervall, das ausschließlich aus Tagen mit hoher Volatilität gezogen wird, genau diese These. Der Preis dafür ist die geringere Datenbasis. Werden 250 Beobachtungen in vier Gruppen aufgeteilt, bleiben pro Gruppe ungefähr 60 Beobachtungen. Ein Bootstrap mit 60 Beobachtungen liefert ein Intervall, das etwa doppelt so breit ist wie die Variante auf Basis der vollständigen Stichprobe.
Geben Sie deshalb jedes Mal neben jedem Intervall die Anzahl der Beobachtungen in der jeweiligen Gruppe an. Die Spalte block_count in den obigen Tabellen macht diese Praxis sichtbar: Ein 5. Perzentil, das aus neun Zeitfenstern abgeleitet wird, ist ein anderes Ergebnis als eines, das auf zweihundert Zeitfenstern basiert, selbst wenn beide auf zwei Dezimalstellen gerundet denselben Wert ausweisen.
Was ein Bootstrap-Verfahren nicht beheben kann
Ein Bootstrap-Verfahren quantifiziert nur eine Größe: das Stichprobenrauschen einer aus den vorliegenden Daten berechneten Statistik. Ob diese Daten überhaupt realisierbar waren, beantwortet es nicht.
Ein durch Look-ahead-Bias kontaminierter Backtest erzeugt eine Renditeserie, die nie handelbar war. Ein Bootstrap-Verfahren liefert dafür ein enges, scheinbar verlässliches Intervall um eine Fiktion. Ein aus den heutigen Indexmitgliedern zusammengestelltes Universum enthält Survivorship-Bias. Jede neue Stichprobe aus diesem Universum übernimmt ihn. Eine dritte Lücke ist der Selektionseffekt: Sie führen 200 Varianten aus, behalten die beste und beschreiben mit ihrem Bootstrap-Intervall das Stichprobenrauschen dieser einen Variante. Die 199 Auswertungen, die in ihre Auswahl eingeflossen sind, bleiben unberücksichtigt. Ehrlich berechnete Intervalle sind nützlich. Sie ersetzen keine Out-of-Sample-Daten.
Datenhinweise und Methodik
- Die Renditen sind Close-to-Close-Kursrenditen auf Basis von Tagesbalken. Dividenden sind nicht berücksichtigt. Dadurch wird das Niveau jeder Rendite- und Sharpe-Kennzahl um ungefähr die Dividendenrendite unterschätzt. Die Streuung, um die es in diesem Beitrag geht, bleibt davon weitgehend unberührt.
- Die Zeitfenster und Blöcke überlappen nicht. Jede gemessene Statistik verwendet daher einen getrennten Abschnitt der Historie. Überlappende Zeitfenster würden die scheinbare Zahl der Stichproben erhöhen.
- Für Quantile wird ein deterministischer Schätzer verwendet. Bei einer erneuten Ausführung liefert ein Panel daher dasselbe Perzentil und keine neue Approximation.
- Das Autokorrelations-Panel verwendet sechs große Titel ohne Aktiensplit innerhalb des Zeitfensters. Tage mit einer Kursbewegung von mehr als 35 Prozent werden ausgeschlossen. Dadurch bleiben Artefakte der Kursanpassung bei der Korrelation unberücksichtigt.
FAQ
Was sagt ein Bootstrap-Konfidenzintervall über einen Backtest aus?
Es zeigt den Wertebereich, den die Kennzahl plausiblerweise annehmen würde, wenn derselbe Prozess über dieselbe Anzahl von Perioden erneut als Stichprobe beobachtet würde. Enthält ein 95-Prozent-Intervall den Wert null, ist die Stichprobe zu kurz, um die Strategie von einem vollständig fehlenden Vorteil zu unterscheiden.
Wie viele Bootstrap-Wiederholungsstichproben sind ausreichend?
Für ein 95-Prozent-Intervall sind einige tausend Wiederholungsstichproben in der Regel stabil. 10.000 sind ein gängiger Standardwert und verursachen nur geringe zusätzliche Kosten. Für extremere Quantile werden mehr Wiederholungen benötigt: Das 1. Perzentil von 1.000 Ziehungen wird ungefähr anhand der zehn kleinsten Werte bestimmt.
Welche Blocklänge sollte ein Moving-Block-Bootstrap verwenden?
Es gibt keine allgemein richtige Blocklänge. Faustregeln skalieren sie mit der Stichprobengröße hoch ein Drittel. Eine praktische Prüfung ist der Zeithorizont, ab dem die Varianz nicht mehr linear skaliert. Das misst das oben dargestellte Varianzverhältnis-Panel. Veröffentlichen Sie die verwendete Blocklänge zusammen mit dem Intervall.
Kann man einen maximalen Drawdown per Bootstrap bestimmen?
Ja, allerdings mit einem Vorbehalt hinsichtlich der Reihenfolge. Der Drawdown hängt von der Abfolge der Renditen ab. Eine durch zufälliges Mischen erstellte Wiederholungsstichprobe ermittelt daher den Drawdown einer neu geordneten Historie. Ein Block-Bootstrap erhält kurze Sequenzen und ist deshalb für pfadabhängige Kennzahlen besser geeignet.
Korrigiert ein Bootstrap Overfitting?
Nein. Er misst das Stichprobenrauschen innerhalb einer einzelnen Renditeserie. Look-ahead-Bias und der Selektionseffekt aus dem Testen vieler Varianten liegen außerhalb dessen, was er erfassen kann.
Jedes Panel enthält hier das SQL, mit dem es erstellt wurde. Ändern Sie den ticker oder die Fensterlänge und führen Sie die Abfrage selbst auf dem Strasmore-Terminal aus.