Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Bootstrap : intervalle de confiance d’un backtest

Une courbe de capital ne forme qu’un échantillon. Un intervalle bootstrap du Sharpe d’un backtest peut être assez large pour inclure zéro. Méthode et lecture.

Un intervalle de confiance appliqué à un backtest répond à une question simple : quelle part de la courbe de capital vient de la stratégie, et quelle part tient à la période particulière sur laquelle elle a été testée ? Le bootstrap d’un backtest construit cet intervalle en rééchantillonnant à de nombreuses reprises la série de rendements, en recalculant la statistique sur chaque nouvel échantillon, puis en observant les percentiles des résultats obtenus. Un ratio de Sharpe de 1,4 mesuré sur une seule année de données quotidiennes présente un intervalle à 95 % suffisamment large pour inclure zéro. Les graphiques ci-dessous montrent pourquoi.

Pourquoi une seule courbe de performance ne constitue qu’un seul échantillon

Un backtest fournit un chiffre par statistique : un ratio de Sharpe, un rendement annualisé, un drawdown maximal et un taux de réussite. Aucun de ces chiffres ne représente la valeur réelle de la stratégie. Chacun est une estimation fondée sur une période limitée de séances de trading. Une autre période de même durée aurait produit un résultat différent.

Le graphique ci-dessous écarte complètement la stratégie de l’analyse. Il mesure la position la plus simple possible : la détention de SPY, une année civile à la fois, à partir des rendements de clôture à clôture.

RequêteUne position, une année à la fois : ratio de Sharpe annualisé de SPY par année civile
Le SQL exact derrière chaque chiffre
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

Chaque ligne couvre environ 250 séances, soit une durée proche d’une année standard de trading. La position n’a pas changé d’une période à l’autre. Pour 2012, le Sharpe annualisé s’établissait à 1.06 ; pour 2025, il atteignait 0.88, avec 14 années représentées sur le graphique. Une année de détention d’un indice large fait varier le chiffre de référence de manière plus importante que ce que la plupart des lecteurs classeraient comme du bruit. Un backtest de même durée est donc exposé à une variabilité au moins équivalente. La convention d’annualisation utilisée dans la colonne est présentée dans notre guide du ratio de Sharpe, et le calcul des rendements sur la période dans notre méthode de mesure des rendements mensuels.

Quelle est l’amplitude de la dispersion autour d’un Sharpe de backtest ?

L’erreur standard d’une estimation du Sharpe diminue approximativement selon la racine carrée du nombre d’observations. Plutôt que de s’appuyer sur cette formule, mesurez directement la dispersion. Découpez vingt ans de séances en fenêtres contiguës de longueur fixe, calculez le Sharpe annualisé dans chaque fenêtre, puis observez l’écart entre les résultats.

RequêteDispersion mesurée du ratio de Sharpe sur des fenêtres SPY non chevauchantes, de 2006 à 2025
Le SQL exact derrière chaque chiffre
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

À raison de 21 sessions par fenêtre, l’intervalle entre le 5e et le 95e percentile des Sharpe mesurés va de -3.37 à 6.97, soit une dispersion de 10.34 points de Sharpe sur 238 fenêtres. En portant chaque fenêtre à 504 sessions, la dispersion tombe à 1.67 points, mais elle est alors mesurée sur seulement 8 fenêtres. Deux facteurs évoluent simultanément. L’estimation gagne en précision avec la taille de l’échantillon, tandis que le nombre d’observations indépendantes que l’historique peut fournir s’effondre. Toute la question est là.

Comment construire un intervalle de confiance bootstrap pour un backtest

La procédure est suffisamment courte pour être présentée intégralement.

  1. Commencez par la série des rendements réalisés de la stratégie, avec une valeur par période, soit N valeurs.
  2. Tirez N rendements au hasard dans cette liste, avec remise. Certains apparaîtront deux fois, d’autres pas du tout.
  3. Recalculez la statistique sur ce nouvel échantillon.
  4. Répétez l’opération plusieurs milliers de fois en conservant chaque valeur.
  5. Triez les valeurs conservées, puis relevez les 2,5e et 97,5e percentiles pour obtenir un intervalle à 95 %.

Initialisez le générateur de nombres aléatoires avant l’étape 2 et consignez la seed à côté de l’intervalle publié. Un bootstrap est une estimation de Monte Carlo : deux exécutions sans seed produisent des résultats différents sur les derniers chiffres. Un reviewer qui ne peut pas reproduire votre intervalle ne peut pas le vérifier. C’est la même discipline que celle décrite dans une configuration de backtest reproductible.

Le rendement moyen et le Sharpe passent correctement par l’étape 2, car ils utilisent tous deux la liste des rendements comme un ensemble de valeurs. Ce n’est pas le cas du maximum drawdown. Le drawdown dépend de l’ordre de la trajectoire. Un nouvel échantillon qui réordonne les rendements peut produire un drawdown maximal qu’aucun ordre de la séquence réelle des trades n’a généré. Il reste pertinent de l’estimer par bootstrap, à condition de préciser ce que représente le résultat : la distribution du drawdown sur des historiques réordonnés, et non une prévision du prochain drawdown. La définition figure dans maximum drawdown.

Pourquoi le bootstrap IID est inadapté aux rendements de marché

L’étape 2 suppose que chaque rendement est indépendant et identiquement distribué, selon l’hypothèse IID. Les rendements quotidiens la remettent en cause d’une manière qui modifie la largeur de l’intervalle. Les rendements signés ne présentent qu’une faible mémoire à un jour. En revanche, leurs amplitudes se regroupent : les mouvements marqués se succèdent, tandis que les journées calmes surviennent par séries.

RequêteAutocorrélation au retard 1 : rendements signés par rapport aux rendements absolus, de 2016 à 2025
Le SQL exact derrière chaque chiffre
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

Pour SPY, l’autocorrélation à un décalage des rendements quotidiens absolus s’établit à 0.366 sur 2514 séances, contre -0.133 pour les rendements signés sur les mêmes journées. Comparez les deux barres pour n’importe quelle valeur du graphique. Mélanger aléatoirement une série de rendements détruit ce regroupement. Un bootstrap IID appliqué à ces données produit donc un intervalle plus étroit que ne le justifie l’échantillon. L’erreur va dans la direction la moins favorable : elle donne une image trop flatteuse de la stratégie.

Le moving block bootstrap et le choix de la longueur des blocs

La correction consiste à rééchantillonner des blocs contigus plutôt que des rendements isolés. Choisissez une longueur de bloc L, tirez aléatoirement, avec remise, des blocs de L rendements consécutifs, puis assemblez-les jusqu’à obtenir une série synthétique de longueur N. La dépendance à l’intérieur d’un bloc est préservée : les rendements restent dans leur ordre d’origine. Seules les jonctions entre les blocs sont artificielles.

La longueur des blocs met en balance deux erreurs, sans permettre de les éviter toutes les deux. Les blocs courts se comportent comme l’IID bootstrap et sous-estiment l’intervalle : c’est le biais. Les blocs longs préservent davantage la dépendance, mais laissent moins de blocs distincts à tirer. Chaque rééchantillonnage répète alors de grandes séquences du même historique, ce qui rend l’intervalle lui-même plus bruité : c’est la variance. Les règles empiriques publiées font évoluer la longueur avec N à la puissance un tiers. Considérez-les comme des points de départ.

Un diagnostic moins coûteux consiste à vérifier comment la variance évolue avec l’horizon. En cas d’indépendance, la variance d’une somme de rendements sur k jours est égale à k fois la variance sur un jour, et le ratio des deux est égal à 1.

RequêteRatio de variance selon la longueur du bloc : la variance de SPY évolue-t-elle comme celle de tirages indépendants ?
Le SQL exact derrière chaque chiffre
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

À 2 sessions, le ratio mesuré était de 0.844 ; à 63 sessions, il était de 0.584, calculé sur 78 blocs non chevauchants. Les valeurs proches de 1 indiquent que la somme évolue à cet horizon comme le feraient des tirages indépendants. Les valeurs éloignées de 1 signalent les horizons auxquels la dépendance continue de jouer un rôle, et c’est cette plage qu’une longueur de bloc doit couvrir. Choisissez le bloc le plus court qui la couvre, puis examinez le nombre de blocs restant.

Rééchantillonnage par compartiments et nombre d’observations pour chaque intervalle

Le rééchantillonnage au sein de compartiments définis par régime de volatilité ou par mois calendaire préserve le conditionnement qui rendait la question pertinente. Si l’hypothèse est qu’une stratégie obtient son Sharpe dans les régimes de forte volatilité, l’intervalle construit uniquement à partir des journées de forte volatilité est celui qui permet de tester cette hypothèse. La contrepartie est arithmétique. Répartir 250 observations entre quatre compartiments laisse environ 60 observations par compartiment. Un bootstrap portant sur 60 observations produit un intervalle environ deux fois plus large que celui calculé sur l’échantillon complet.

Indiquez donc systématiquement le nombre d’observations de chaque compartiment à côté de chaque intervalle. La colonne block_count des panneaux ci-dessus rend cette pratique visible : un 5e percentile calculé sur neuf fenêtres est un objet différent d’un 5e percentile calculé sur deux cents fenêtres, même si les deux valeurs sont affichées avec deux décimales.

Ce qu’un bootstrap ne peut pas corriger

Un bootstrap quantifie une seule chose : le bruit d’échantillonnage d’une statistique calculée à partir des données dont vous disposez. Il ne dit rien sur la possibilité d’avoir effectivement obtenu ces données.

Un backtest contaminé par un biais d’anticipation produit une série de rendements qui n’a jamais été négociable. Un bootstrap de cette série renvoie une bande étroite et sûre d’elle-même autour d’une fiction. Un univers constitué des membres actuels d’un indice comporte un biais de survie, que chaque rééchantillonnage de cet univers reproduit. Il existe un troisième problème : l’effet de sélection. Si vous testez 200 variantes et conservez la meilleure, son intervalle bootstrap décrit le bruit d’échantillonnage de cette seule variante, tout en ignorant les 199 tests qui ont servi à la sélectionner. Des bandes calculées honnêtement sont utiles. Elles ne remplacent pas des données hors échantillon.

Notes sur les données et la méthode
  • Les rendements sont calculés entre les cours de clôture à partir de données quotidiennes. Les dividendes sont exclus, ce qui sous-estime d’environ le rendement du dividende le niveau de chaque rendement et de chaque ratio de Sharpe. La dispersion analysée dans cet article est presque inchangée.
  • Les fenêtres et les blocs ne se chevauchent pas. Chaque statistique mesurée utilise donc une portion distincte de l’historique. Des fenêtres chevauchantes gonfleraient le nombre apparent d’observations.
  • Les quantiles utilisent un estimateur déterministe. Relancer un panel renvoie donc le même percentile, et non une nouvelle approximation.
  • Le panel d’autocorrélation utilise six grandes valeurs sans fractionnement d’actions pendant la fenêtre. Toute journée affichant une variation supérieure à 35 % est exclue, afin d’écarter de la corrélation les artefacts liés à l’ajustement des cours.

FAQ

Que vous indique un intervalle de confiance bootstrap sur un backtest ?

Il donne la plage de valeurs que la statistique pourrait plausiblement prendre si le même processus était à nouveau échantillonné sur le même nombre de périodes. Un intervalle à 95 % qui contient zéro signifie que l’échantillon est trop court pour distinguer la stratégie d’une absence totale d’edge.

Combien de rééchantillonnages bootstrap faut-il effectuer ?

Pour un intervalle à 95 %, quelques milliers de rééchantillonnages donnent généralement un résultat stable. 10 000 est un réglage courant, pour un coût limité. Les quantiles extrêmes nécessitent davantage de tirages : le 1er percentile de 1 000 tirages se lit sur environ dix valeurs.

Quelle longueur de bloc faut-il utiliser avec un moving block bootstrap ?

Il n’existe pas de longueur universellement correcte. Les règles empiriques la font généralement varier en fonction de la taille de l’échantillon, à la puissance un tiers. En pratique, vous pouvez aussi retenir l’horizon à partir duquel la variance cesse d’évoluer linéairement ; c’est ce que mesure le panneau du ratio de variance ci-dessus. Indiquez la longueur utilisée à côté de l’intervalle.

Peut-on appliquer un bootstrap à un drawdown maximal ?

Oui, avec une réserve concernant l’ordre des observations. Le drawdown dépend de la séquence des rendements. Un rééchantillonnage par permutation mesure donc le drawdown d’un historique réordonné. Un block bootstrap conserve les courtes séquences intactes et convient mieux aux statistiques dépendant du chemin suivi.

Un bootstrap corrige-t-il le surajustement ?

Non. Il mesure le bruit d’échantillonnage au sein d’une seule série de rendements. Le look-ahead bias et l’effet de sélection lié au test de nombreuses variantes ne sont pas pris en compte.


Chaque panneau contient la requête SQL qui l’a produit. Modifiez le ticker ou la longueur de la fenêtre, puis exécutez vous-même la requête sur le terminal Strasmore.

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