Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Intervalli di confidenza bootstrap per il backtest

Una sola equity curve è un campione. Scopri come costruire e interpretare un intervallo bootstrap dello Sharpe, spesso abbastanza ampio da includere lo zero.

Un intervallo di confidenza del backtest risponde a una domanda: quanta parte della curva azionaria dipende dalla strategia e quanta dalla particolare porzione di storia su cui è stata testata. Il bootstrapping di un backtest costruisce questo intervallo ricampionando più volte la serie dei rendimenti, ricalcolando la statistica su ogni campione e osservando i percentili dei risultati ottenuti. Uno Sharpe di 1,4 calcolato su un solo anno di osservazioni giornaliere presenta un intervallo al 95% abbastanza ampio da includere lo zero. I pannelli seguenti mostrano il motivo.

Perché una sola equity curve è un solo campione

Un backtest restituisce un solo valore per ogni statistica: un indice di Sharpe, un rendimento annualizzato, il peggior drawdown, una percentuale di operazioni chiuse in profitto. Nessuno di questi valori rappresenta il vero valore della strategia. Ciascuno è una stima costruita su un numero finito di sedute di trading. Un altro periodo della stessa durata avrebbe prodotto risultati diversi.

Il pannello seguente elimina del tutto la strategia dall’analisi. Misura la posizione più semplice possibile: detenere SPY, un anno solare alla volta, sulla base dei rendimenti di prezzo close-to-close.

QueryUna posizione, un anno alla volta: Sharpe annualizzato di SPY per anno solare
L'esatto SQL dietro ogni numero
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

Ogni riga copre circa 250 sedute, in linea con un normale anno di trading. La posizione non è cambiata in nessuno dei periodi. In 2012 lo Sharpe annualizzato era pari a 1.06; in 2025 era pari a 0.88, su 14 anni rappresentati nel grafico. Un anno di detenzione di un indice ampio può modificare il valore principale di oltre quanto la maggior parte dei lettori considererebbe semplice rumore. Un backtest della stessa durata eredita almeno questa variabilità. La convenzione di annualizzazione alla base della colonna è illustrata nella nostra guida all’indice di Sharpe, mentre i meccanismi del rendimento di periodo sono spiegati in come si misurano i rendimenti mensili.

Quanto è ampia la fascia di variabilità attorno a uno Sharpe calcolato su backtest?

L’errore standard di una stima dello Sharpe diminuisce all’incirca con la radice quadrata del numero di osservazioni. Invece di affidarsi a questa formula, misuriamo direttamente lo spread. Dividiamo vent’anni di sedute in finestre non sovrapposte di lunghezza fissa, calcoliamo lo Sharpe annualizzato all’interno di ciascuna finestra e osserviamo quanto sono distanti tra loro i risultati.

QueryDispersione dello Sharpe misurato su finestre SPY non sovrapposte, 2006–2025
L'esatto SQL dietro ogni numero
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

Con 21 sessions per finestra, il 5° e il 95° percentile degli Sharpe misurati vanno da -3.37 a 6.97: uno spread di 10.34 punti di Sharpe su 238 finestre. Estendendo ogni finestra a 504 sessions, lo spread scende a 1.67 punti, misurati però su appena 8 finestre. I fattori che cambiano sono due. La stima diventa più precisa all’aumentare del campione, ma il numero di campioni indipendenti che la storia può fornire crolla. È questa tensione il tema centrale dell’analisi.

Come costruire un intervallo di confidenza per un backtest con il bootstrap

La procedura è abbastanza breve da poter essere riportata per intero.

  1. Parti dalla serie dei rendimenti realizzati dalla strategia, un valore per periodo, per un totale di N valori.
  2. Estrai casualmente N rendimenti da quella lista, con reinserimento. Alcuni compariranno due volte, altri mai.
  3. Ricalcola la statistica sul ricampionamento.
  4. Ripeti l’operazione diverse migliaia di volte, conservando ogni valore.
  5. Ordina i valori memorizzati e individua i percentili 2,5 e 97,5 per ottenere un intervallo al 95 per cento.

Imposta il seed del generatore di numeri casuali prima del passaggio 2 e riportalo accanto all’intervallo pubblicato. Un bootstrap è una stima Monte Carlo: due esecuzioni senza seed differiscono nelle ultime cifre, e un revisore che non può rieseguire il calcolo dell’intervallo non ha modo di verificarlo. È la stessa disciplina descritta in un setup riproducibile del backtest.

Il rendimento medio e lo Sharpe passano senza problemi dal passaggio 2, perché entrambi trattano la lista dei rendimenti come un insieme. Il maximum drawdown no. Il drawdown considera l’ordine del percorso. Un ricampionamento che riordina i rendimenti produce un drawdown massimo che nessun ordinamento della sequenza reale dei trade ha generato. Vale comunque la pena applicare il bootstrap, purché il risultato sia presentato per quello che è: la distribuzione del drawdown su percorsi rimescolati, non una previsione del prossimo drawdown. La definizione è disponibile in maximum drawdown.

Perché il bootstrap IID è errato per i rendimenti di mercato

Il passaggio 2 presume che ogni rendimento sia indipendente e identicamente distribuito: è l’ipotesi IID. I rendimenti giornalieri la violano in un modo che modifica l’ampiezza dell’intervallo. I rendimenti con segno conservano solo una debole memoria di un giorno. Le loro magnitudini si concentrano: grandi movimenti si susseguono, mentre le giornate tranquille arrivano in sequenza.

QueryAutocorrelazione al lag uno: rendimenti con segno rispetto ai rendimenti assoluti, 2016–2025
L'esatto SQL dietro ogni numero
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

Per SPY, l’autocorrelazione con ritardo di un giorno dei rendimenti giornalieri assoluti, misurata 0.366 su 2514 sedute, a fronte di -0.133 per i rendimenti con segno nelle stesse giornate. Confrontate le due barre per qualsiasi nome nel grafico. Mescolare casualmente una serie di rendimenti distrugge questa concentrazione, e un bootstrap IID eseguito su questi dati restituisce un intervallo più stretto di quello supportato dal campione. L’errore va nella direzione meno utile: fa apparire la strategia più solida di quanto sia.

Il moving block bootstrap e la scelta della lunghezza dei blocchi

La correzione consiste nel ricampionare blocchi contigui invece dei singoli rendimenti. Si sceglie una lunghezza dei blocchi L, si estraggono casualmente, con reinserimento, blocchi di L rendimenti consecutivi e li si concatena fino a ottenere una serie sintetica lunga N. La dipendenza all’interno del blocco rimane intatta: i rendimenti conservano l’ordine originale. Solo i punti di giunzione tra i blocchi sono artificiali.

La lunghezza dei blocchi contrappone due errori, senza consentire di evitarli entrambi. I blocchi brevi si comportano come il bootstrap IID e sottostimano l’intervallo: è un problema di bias. I blocchi lunghi preservano una maggiore dipendenza, ma lasciano meno blocchi distinti da cui effettuare l’estrazione. Ogni ricampionamento ripete quindi ampie porzioni della stessa storia e anche l’intervallo diventa rumoroso: è un problema di varianza. Le regole empiriche pubblicate fanno crescere la lunghezza con N elevato alla potenza di un terzo. Vanno considerate come punti di partenza.

Un controllo diagnostico meno oneroso consiste nel verificare come cambia la varianza in funzione dell’orizzonte. In condizioni di indipendenza, la varianza della somma dei rendimenti su k giorni è pari a k volte la varianza su un giorno, e il rapporto tra le due è pari a 1.

QueryRapporto di varianza per lunghezza del blocco: la varianza di SPY scala come estrazioni indipendenti?
L'esatto SQL dietro ogni numero
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

A 2 sessions il rapporto misurava 0.844; a 63 sessions misurava 0.584, calcolato su 78 blocchi non sovrapposti. Valori vicini a 1 indicano che la somma cresce come farebbero estrazioni indipendenti a quell’orizzonte. Valori diversi da 1 segnalano gli orizzonti in cui la dipendenza continua a incidere; è questo l’intervallo che la lunghezza dei blocchi deve coprire. Si sceglie il blocco più breve che lo copre, poi si verifica a quanto è sceso il numero di blocchi.

Ricampionamento per bucket e numerosità accanto a ogni intervallo

Il ricampionamento all’interno dei bucket, per regime di volatilità o per mese di calendario, preserva la condizione che rendeva interessante la domanda. Se l’ipotesi è che una strategia ottenga il proprio Sharpe nei regimi di alta volatilità, l’intervallo costruito estraendo dati solo dai giorni ad alta volatilità è quello che verifica tale ipotesi. Il costo è aritmetico. Suddividere 250 osservazioni in quattro bucket lascia circa 60 osservazioni per bucket, e un bootstrap su 60 osservazioni produce un intervallo circa due volte più ampio rispetto a quello calcolato sull’intero campione.

Occorre quindi riportare sempre la numerosità del bucket accanto a ogni intervallo. La colonna block_count nei pannelli sopra rende visibile questa prassi: un 5° percentile calcolato su nove finestre è un oggetto diverso da uno calcolato su duecento, anche quando entrambi sono mostrati con due decimali.

Cosa non può correggere un bootstrap

Un bootstrap quantifica una sola cosa: il rumore campionario di una statistica calcolata sui dati disponibili. Non dice nulla sul fatto che quei dati fossero effettivamente ottenibili.

Un backtest contaminato da look-ahead bias produce una serie di rendimenti che non sarebbe mai stata tradabile. Il relativo bootstrap restituisce quindi una banda stretta e apparentemente affidabile attorno a una finzione. Un universo composto dai membri attuali di un indice incorpora survivorship bias, e ogni ricampionamento di quell’universo lo eredita. Esiste poi un terzo problema: l’effetto selezione. Se si eseguono 200 varianti e si conserva la migliore, il suo intervallo bootstrap descrive il rumore campionario di quella sola variante, ignorando le 199 prove che hanno determinato la scelta. Le bande calcolate correttamente sono utili. Non sostituiscono i dati out-of-sample.

Note sui dati e sul metodo
  • I rendimenti sono rendimenti di prezzo close-to-close ricavati da barre giornaliere. I dividendi sono esclusi; questo sottostima il livello di ogni rendimento e di ogni valore di Sharpe di circa il dividend yield. La dispersione analizzata in questo articolo è invece quasi del tutto invariata.
  • Le finestre e i blocchi non si sovrappongono, quindi ogni statistica misurata utilizza una porzione distinta della serie storica. Finestre sovrapposte gonfierebbero il numero apparente di campioni.
  • I quantili utilizzano uno stimatore deterministico. Ripetendo l’elaborazione si ottiene quindi lo stesso percentile, non una nuova approssimazione.
  • Il pannello dell’autocorrelazione utilizza sei titoli large cap senza split azionari all’interno della finestra ed esclude ogni giorno con una variazione superiore al 35%, così da eliminare dalla correlazione gli artefatti dovuti agli aggiustamenti dei prezzi.

FAQ

Cosa indica un intervallo di confidenza bootstrap su un backtest?

Indica l’intervallo di valori che la statistica assumerebbe plausibilmente se lo stesso processo venisse campionato di nuovo per lo stesso numero di periodi. Un intervallo al 95 percento che include lo zero significa che il campione è troppo breve per distinguere la strategia dall’assenza di un vantaggio.

Quanti ricampionamenti bootstrap sono sufficienti?

Per un intervallo al 95 percento, alcune migliaia di ricampionamenti sono generalmente sufficienti a garantire stabilità, mentre 10.000 è un’impostazione predefinita comune e poco onerosa. Per i quantili estremi servono più osservazioni: il primo percentile di 1.000 estrazioni si ricava da circa dieci valori.

Quale lunghezza dei blocchi deve usare un moving block bootstrap?

Non esiste una lunghezza corretta in assoluto. Le regole empiriche la rapportano alla radice cubica della dimensione del campione. Un controllo pratico consiste nell’individuare l’orizzonte al quale la varianza smette di crescere linearmente: è quello misurato dal pannello del variance ratio riportato sopra. Indicate la lunghezza utilizzata insieme all’intervallo.

È possibile applicare il bootstrap al maximum drawdown?

Sì, con una precisazione sull’ordine delle osservazioni. Il drawdown dipende dalla sequenza dei rendimenti. Un ricampionamento ottenuto mescolando le osservazioni calcola quindi il drawdown di una storia riordinata. Un block bootstrap mantiene intatte le sequenze brevi ed è lo strumento più adatto per le statistiche legate al percorso.

Il bootstrap corregge l’overfitting?

No. Misura il rumore campionario all’interno di una singola serie di rendimenti. Il look-ahead bias e l’effetto di selezione derivante dal testare numerose varianti restano al di fuori di ciò che il bootstrap può rilevare.


Ogni pannello qui riportato contiene la query SQL che lo ha prodotto. Modificate il ticker o la lunghezza della finestra ed eseguite la query direttamente sul terminale Strasmore.

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