Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

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.

AbfrageEine Position, jeweils ein Jahr: annualisierter Sharpe von SPY nach Kalenderjahr
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 year
Run this yourself

Jede 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.

AbfrageGemessene Sharpe-Streuung über nicht überlappende SPY-Zeiträume, 2006 bis 2025
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 w
Run this yourself

Bei 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.

  1. Beginnen Sie mit der realisierten Renditeserie der Strategie. Sie enthält pro Periode einen Wert, insgesamt N Werte.
  2. Ziehen Sie zufällig N Renditen aus dieser Liste, mit Zurücklegen. Einige Werte erscheinen doppelt, andere gar nicht.
  3. Berechnen Sie die Kennzahl für diese Stichprobe erneut.
  4. Wiederholen Sie den Vorgang mehrere tausend Mal und speichern Sie jeden Wert.
  5. 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.

AbfrageAutokorrelation erster Ordnung: signierte Renditen gegenüber absoluten Renditen, 2016 bis 2025
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 DESC
Run this yourself

Fü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.

AbfrageVarianzverhältnis nach Blocklänge: Skaliert die SPY-Varianz wie bei unabhängigen Ziehungen?
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 k
Run this yourself

Bei 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.

#backtesting#bootstrap#statistics#confidence intervals#sharpe ratio