← Retour aux articles
June 27, 2026
5 min de lecture

Look-Ahead Bias : comment une erreur d'une bougie fabrique un Sharpe de 15 à partir de pur bruit

Look-Ahead Bias : comment une erreur d'une bougie fabrique un Sharpe de 15 à partir de pur bruit
#algotrading
#backtest
#look-ahead bias
#data leakage
#overfitting
#validation

Fait partie de la série « Backtests sans illusions ».

📄 Cet article est devenu un article de recherche. Trois fuites subtiles de look-ahead sont soumises à un test contrôlé face à une vérité terrain connue (4 000 historiques simulés). Lisez l'article en ligne (version interactive + PDF) sur lookahead.marketmaker.cc, code et données sur github.com/suenot/lookahead-inflation.

Il y a quelques semaines, notre benchmark de recherche de paramètres nous mentait, et nous avons failli ne pas le remarquer.

Le moteur semblait propre. Logique de bougie clôturée, un découpage walk-forward glissant honnête, une recherche Sobol/QMC sur l'espace des paramètres, une fenêtre de test réservée. La recherche trouvait des configurations qui semblaient bonnes in-sample. Le seul problème : hors échantillon, presque tout était négatif. Nous avons supposé que la stratégie était simplement faible.

Puis nous avons trouvé une ligne. Le signal était décidé à la clôture de la bougie i, mais l'exécution était enregistrée sur la même bougie i au lieu de l'ouverture de la bougie suivante. Un décalage d'un cran (off-by-one) dans l'indice d'exécution. Nous avons déplacé l'exécution vers open[i+1] — le seul prix auquel on pouvait réellement transiger après avoir vu la clôture de la bougie i — et le résultat hors échantillon a inversé son signe. La recherche Sobol est passée d'une perte à un profit. Rien dans la stratégie n'avait changé. Nous avions simplement arrêté de trader dans le passé.

C'est cela le look-ahead bias, et ce qui est troublant, c'est à quel point l'erreur était petite et la conséquence grande. Cet article est un auto-audit contrôlé : nous construisons un simulateur où la vérité terrain est connue par construction, injectons les fuites subtiles une à une, et mesurons exactement combien chacune gonfle le backtest. Le résultat principal : avec aucun edge réel du tout, une exécution sur la même bougie fabrique un Sharpe annualisé de +14,8 à partir de pur bruit.

Ce qu'est réellement le look-ahead bias

The three places look-ahead hides — execution, normalization, and indicators — as channels of unequal danger feeding one trading decision

Le look-ahead bias est tout point de votre pipeline où une décision ou une mesure utilise une information qui n'aurait pas été disponible, en temps réel, au moment où elle est utilisée. Les exemples de manuel sont grossiers — utiliser les résultats annuels complets d'une action en janvier, ou une révision comptable qui n'avait pas encore été publiée. Ceux-là sont faciles à repérer. Ceux qui survivent à une revue de code sont subtils, et se cachent à trois endroits :

  1. Exécution — vous décidez sur la bougie i et exécutez sur la bougie i (ou utilisez le haut/bas de la bougie i pour des stops sur la bougie même qui a généré le signal). Vous transigez à un prix corrélé avec ce qui vous a déclenché.
  2. Normalisation — vous calculez un z-score, un min-max, ou mettez à l'échelle une feature autrement en utilisant des statistiques calculées sur toute la série, futur inclus. Le scaler « connaît » l'ensemble de test.
  3. Indicateurs / features — vous lissez ou filtrez avec une fenêtre centrée (ou qui regarde autrement vers l'avant), de sorte que la valeur à la bougie i contient déjà un fragment de la bougie i+1.

Ces trois formes relèvent de ce que la littérature de machine learning appelle le leakage : la contamination de l'entraînement/évaluation par des informations issues du futur de la cible (Kaufman et al., 2012 ; Kapoor & Narayanan, 2023). En finance, la référence canonique est Advances in Financial Machine Learning de López de Prado (2018) — validation croisée purgée, embargo, les dangers du backtesting. La discipline point-in-time remonte au moins à Fama & French (1992), qui décalent délibérément les données comptables de six mois afin que la variable soit connue avant le rendement qu'elle explique.

La question à laquelle répond cet article est quantitative : pas « le leakage est-il mauvais » (tout le monde est d'accord), mais « combien de points de Sharpe chaque forme rapporte-t-elle, et lesquelles sont dangereuses ? » Sans chiffre, on ne peut pas raisonner dessus. Impossible de dire si une inflation de +0,3 est du bruit ou si une inflation de +14 est une preuve accablante.

Un simulateur à vérité terrain connue

A controlled synthetic market with a known edge dial: a null world with no real edge beside an edge world whose equity genuinely rises

Pour mesurer l'inflation, il faut connaître la vérité. Les données réelles ne révèlent jamais la vérité — elles donnent une réalisation et aucun oracle. Nous construisons donc un marché synthétique où nous fixons l'edge.

Le processus générateur de données est strictement causal et non explosif :

gt=ϕgt1+1ϕ2  ut,utN(0,1)g_t = \phi\, g_{t-1} + \sqrt{1-\phi^2}\; u_t, \qquad u_t \sim \mathcal{N}(0,1)

rt=agt1+σεt,εtN(0,1)r_t = a\, g_{t-1} + \sigma\, \varepsilon_t, \qquad \varepsilon_t \sim \mathcal{N}(0,1)

Ici gtg_t est une dérive latente persistante exogène (un AR(1) avec ϕ=0.95\phi = 0.95), et le rendement de la bougie rtr_t a une petite dérive agt1a\,g_{t-1} qui est connue une bougie à l'avance. Comme gg ne dépend pas des rendements passés, il n'y a pas de rétroaction et rien n'explose. Le paramètre aa est le curseur qui règle la quantité d'edge réel présente :

  • a=0a = 0 — le nul : aucun edge du tout. Tout Sharpe positif du backtest est 100% artefact.
  • a>0a > 0 — un edge réel et négociable : une règle de momentum honnête gagne effectivement de l'argent.

La stratégie est délibérément simple — une règle de signe de momentum. La feature est la somme glissante des rendements sur LL bougies (L=24L = 24 bougies), et la position est son signe :

csum = np.concatenate(([0.0], np.cumsum(r)))      # csum[k] = sum r[0..k-1]
mom = np.full(n, np.nan)
tt = np.arange(L - 1, n)
mom[tt] = csum[tt + 1] - csum[tt - L + 1]

signal = np.sign(mom)                              # position for the next bar

Cette feature de momentum est le véhicule parfait pour étudier la fuite de même-bougie, car elle possède une propriété que partagent les indicateurs réels : elle contient mécaniquement la bougie courante. mom[t] inclut r[t]. Donc si vous enregistrez r[t] comme votre trade, vous pariez en partie sur une quantité qui est déjà à l'intérieur de votre propre signal. C'est la fuite, rendue concrète.

Configuration : σ=0.01\sigma = 0.01 (1% de volatilité par bougie), des frais unidirectionnels de 0,00045 (aller-retour 0,09%, correspondant à notre moteur), Sharpe annualisé par 8760\sqrt{8760} (bougies horaires), 4 000 historiques indépendants de 4 000 bougies chacun. Tout est initialisé (seeded) et déterministe.

Le pipeline honnête (le seul négociable)

Décider à la clôture de la bougie t, gagner le rendement de la bougie suivante, payer des frais sur les changements de position :

def sharpe(sig, ret_booked):
    dpos = np.abs(np.diff(np.concatenate(([0.0], sig))))
    pnl  = sig * ret_booked - FEE_ONEWAY * dpos
    return pnl.mean() / pnl.std() * np.sqrt(8760)

honest = sharpe(signal[idx], r[idx + 1])           # earn r[t+1]: tradable

Les trois fuites, chacune un changement chirurgical unique

same_bar  = sharpe(signal[idx], r[idx])

z_full    = (mom - mom[valid].mean()) / mom[valid].std()
norm_full = sharpe(np.sign(z_full[idx]), r[idx + 1])

z_sm      = (mom[:-2] + mom[1:-1] + mom[2:]) / 3.0   # uses t-1, t, t+1
indicator = sharpe(np.sign(z_sm[idx]), r[idx + 1])

Chaque fuite n'est qu'à une ligne du pipeline honnête. C'est tout l'enjeu : ce ne sont pas des erreurs exotiques, ce sont le genre de choses qui passent une revue de code.

Résultats : l'ampleur de chaque fuite

Pure market noise funneled through a same-bar leak into a soaring, fake equity curve and a performance gauge pinned near its maximum

Exécuté sur 4 000 graines, voici le Sharpe annualisé rapporté par chaque pipeline, sous le nul (aucun edge) et sous un edge réel (a=0,0011a = 0,0011, réglé pour que le Sharpe honnête soit un +1,57 crédible) :

Pipeline Nul (aucun edge) Edge réel
Honnête (la vérité) −0,74 +1,57
Exécution même bougie +14,79 +15,85
Coup d'œil indicateur (1 bougie) +4,76 +6,62
Normalisation sur toute la série −0,84 +1,46

Les intervalles de confiance à 95% à travers les graines sont de ±0,05 ou plus serrés dans chaque cellule ; les tests t appariés sur l'inflation sont astronomiquement significatifs là où l'effet est réel (t > 400, p ≈ 0).

Lisez d'abord la colonne nulle, car c'est l'expérience la plus propre possible : il n'y a aucun edge, donc le pipeline honnête perd correctement de l'argent (−0,74, le frein des frais payés pour trader du bruit). Voyez maintenant ce que les fuites font à ce même néant :

  • Exécution même bougie : −0,74 → +14,79. Une stratégie sans aucun pouvoir prédictif, tradant du bruit aléatoire, rapporte un Sharpe annualisé de près de 15. Ce n'est pas un biais subtil ; c'est une fabrication. Le mécanisme est exactement celui que nous avons construit : la feature de momentum contient r[t], donc enregistrer r[t] revient à parier sur son propre signal.
  • Coup d'œil indicateur : −0,74 → +4,76. Laisser le lisseur voir une bougie dans le futur fabrique un Sharpe proche de 5 à partir de bruit, car la valeur lissée à t corrèle désormais avec le r[t+1] que vous êtes sur le point de gagner.
  • Normalisation sur toute la série : −0,74 → −0,84. Quasiment aucune inflation. C'est le résultat honnête et non évident (plus de détails ci-dessous).

La colonne edge livre le message le plus insidieux. Quand un edge réel existe (honnête +1,57), les fuites n'ajoutent pas juste une constante — elles poussent le Sharpe mesuré à +15,85 et +6,62, bien au-delà du +1,57 réellement négociable. Le chiffre mesuré ne peut donc pas distinguer le talent de la fuite. Un +6 avec fuite et un +6 honnête sont identiques sur le rapport. On ne découvre lequel des deux on avait qu'après avoir déployé du capital.

La fuite est un gradient, pas un interrupteur

The same-bar leak as a smooth dose-response: capturing a larger fraction of the signal bar lifts the equity curve monotonically across the deployable threshold

Une objection naturelle : « enregistrer la totalité de la bougie signal est une erreur extrême et irréaliste. » Nous avons donc balayé la dose — la fraction ff de la bougie signal capturée par la fuite, de 0 (honnête) à 1 (fuite complète même bougie) :

Fraction capturée ff Sharpe nul Sharpe avec edge
0,00 (honnête) −0,74 +1,57
0,25 +3,90 +6,41
0,50 +9,86 +12,20
1,00 (fuite complète) +14,79 +15,85

Capturer seulement un quart de la bougie signal fait passer une stratégie sans edge de −0,74 à +3,90. Il n'est pas nécessaire d'avoir le décalage complet pour être trompé ; une exécution légèrement trop favorable — un soupçon de slippage optimiste sur la bougie signal, un stop intrabar vérifié contre la bougie même qui l'a déclenché — suffit à franchir la plupart des seuils « déployables ». L'inflation est lisse et monotone selon la quantité de présent qu'on se permet de trader.

À quelle fréquence cela met-il en production une stratégie perdante ?

Le chiffre qui devrait inquiéter un praticien est le taux de déploiement erroné : à quelle fréquence une fuite fait franchir à une configuration réellement perdante le seuil qu'on utiliserait pour la valider. En utilisant « Sharpe annualisé ≥ 1,0 » comme critère de déploiement, sous le nul :

  • Exécution même bougie : 68% des stratégies sans edge semblent déployables et sont réellement perdantes. Deux configurations de pur bruit sur trois franchiraient un seuil Sharpe-≥-1 et perdraient de l'argent en réel. (Ce taux est proprement défini ici car la fuite est purement dans l'exécution — l'équivalent honnête est le même signal avec une exécution honnête.)
  • Coup d'œil indicateur : il pousse pratiquement toutes les configurations sans edge au-dessus du seuil de déploiement aussi (99,9% franchissent Sharpe ≥ 1) — il ferait passer du bruit tout droit en production.
  • Normalisation sur toute la série : 12% franchissent le seuil — essentiellement le taux de base du bruit, aucune prime de leakage.

La taxonomie, et comment détecter chacune

Les trois fuites ne sont pas également dangereuses, et les différences sont instructives.

1. Le leakage d'exécution (le coûteux)

The same-bar fill off-by-one: an execution arrow curling back into the bar that generated the signal, versus the honest fill on the next bar's open

Symptôme : le prix d'exécution est corrélé au signal parce qu'ils proviennent de la même bougie. Ampleur : énorme (+15 depuis du bruit à dose complète, +3,9 à dose quart). Pourquoi c'est le pire : votre signal est, presque par définition, construit à partir de l'action de prix récente, donc le rendement de la bougie signal est exactement ce avec quoi votre feature est le plus corrélée. L'enregistrer revient presque à consulter la réponse.

Détection — le test de décalage d'une bougie. C'est le diagnostic le plus précieux de cet article. Prenez votre backtest et décalez chaque exécution d'une bougie plus tard (décider sur i, exécuter sur open[i+1]). Si le résultat bouge à peine, votre exécution était honnête. Si le résultat s'effondre ou inverse son signe, vous tradiez dans le passé. C'est précisément ce qui est arrivé à notre recherche Sobol : décalez les exécutions, et un OOS « profitable » s'est révélé être une perte — ou plutôt, la relation réelle a émergé une fois la fuite retirée.

entry_price = open_[i + 1]      # NOT close[i], NOT open[i]

2. Le leakage d'indicateur / feature (le discret)

Symptôme : un indicateur à la bougie i dépend de données de i+1 ou plus tard — une moyenne mobile centrée, un filtre sans délai causal, un label de pic/creux nécessitant des bougies futures pour être confirmé, une transformation de type Heikin-Ashi alimentée par des chandelles futures. Ampleur : grande (+4,8 depuis du bruit). Pourquoi ça se cache : la fuite est enfouie dans un appel de bibliothèque. scipy.signal.filtfilt est à phase nulle — et phase nulle signifie non causal. Une feature « cette bougie est un maximum local » est inconnaissable tant que la bougie suivante n'est pas imprimée.

Détection : pour chaque indicateur, demandez-vous quel est le plus grand index qu'il lit ? Si le calcul de la valeur à t touche jamais t+1, il est non causal. Calculez les indicateurs sur une fenêtre causale expansive/glissante et vérifiez que la valeur à la bougie t est identique, que des bougies après t existent ou non dans le tableau. (Nos implémentations HMA/ADX passent ce test : chaque sortie à t ne lit que des entrées à ≤ t.)

3. Le leakage de normalisation (le spécifique au canal)

Symptôme : un scaler (StandardScaler, min-max, un z-score global) est ajusté sur l'ensemble du dataset, test inclus. Les mises en garde canoniques du ML sont explicites à ce sujet — le §7.10.2 d'Elements of Statistical Learning de Hastie, Tibshirani & Friedman (« the wrong and right way to do cross-validation »), et le guide des pièges courants de scikit-learn lui-même : « the average should be the average of the train subset, not the average of all the data. »

Ampleur dans notre test :zéro (−0,74 → −0,84). C'est le résultat surprenant et honnête, et il vaut la peine d'être compris plutôt que mémorisé.

Pourquoi n'a-t-il pas gonflé ? Parce que notre stratégie n'utilise la feature qu'à travers son signe (un seuil zéro). La mise à l'échelle par écart-type ne change jamais un signe, et le centrage sur la moyenne globale ne déplace que légèrement le point de croisement zéro. Ainsi, la standardisation sur toute la série d'une règle de signe pure est presque anodine.

Ne généralisez pas trop. Le leakage de normalisation est spécifique au canal. Dès que votre stratégie utilise la magnitude de la feature — un dimensionnement de position proportionnel à un z-score, un seuil d'entrée non nul choisi en observant la distribution mise à l'échelle, un réseau de neurones consommant des entrées standardisées —, le scaler conscient du futur commence à compter, et compte d'autant plus que les statistiques globales diffèrent des statistiques causales. Notre résultat n'est pas « le leakage de normalisation est sans danger. » C'est « l'ampleur du leakage dépend du canal par lequel la quantité fuitée entre dans la décision, et il faut la mesurer plutôt que la supposer. » Une règle de signe est le seul cas où cette fuite particulière est bon marché.

Où cela se raccorde

Le look-ahead bias est le premier maillon d'une chaîne que cette série documente :

  • Il corrompt l'entrée de la validation. Un backtest fuité traversera sans encombre un découpage walk-forward et ressemblera à un plateau large plutôt qu'un pic d'overfitting — la fuite est cohérente à travers les folds, donc la validation croisée ne peut pas la détecter. Le leakage est le mode de défaillance en amont de l'overfitting, et aucune validation honnête en aval ne vous sauvera.
  • Il interagit avec la recherche de paramètres : une recherche sur des milliers d'essais sur des données fuitées trouvera la configuration qui exploite la fuite le plus agressivement. Le « gagnant » est le pire contrevenant.
  • C'est pourquoi la parité backtest-live diverge. Une fuite est l'explication la plus nette d'un écart de 30 à 50% entre le backtest et le bot, car le trading en direct est, mécaniquement, le seul endroit où l'on ne peut pas tricher.

La discipline qui capture tout cela est la même que celle prônée par la littérature académique depuis des années : traiter un backtest comme une expérience statistique avec une frontière d'information stricte. Bailey, Borwein, López de Prado & Zhu ont montré avec quelle facilité l'overfitting fabrique une performance factice (2014) ; le protocole de backtesting d'Arnott, Harvey & Markowitz (2019) codifie cette hygiène. Le look-ahead bias est la frontière la plus fondamentale de toutes — la frontière dans le temps — et la moins chère à violer par accident.

À retenir

The one-bar shift diagnostic: nudging every fill one bar later deflates a fake soaring curve back down to the honest truth

  1. Le look-ahead bias est quantitativement énorme et qualitativement invisible. Une seule erreur d'exécution d'une bougie a transformé un Sharpe de −0,74 (pur bruit, perdant à juste titre) en +14,79. L'erreur tient en une ligne ; la conséquence est un historique de performance fabriqué.
  2. C'est un gradient. Capturer ne serait-ce que 25% de la bougie signal produit +3,90 à partir de rien. Pas besoin d'un bug flagrant — un peu trop d'optimisme dans vos exécutions suffit.
  3. Le chiffre mesuré ne peut pas distinguer le talent de la fuite. Quand un edge réel existe, les fuites gonflent le rapport bien au-delà de la vérité négociable. La seule défense est le processus, pas la métrique.
  4. Le test de décalage d'une bougie est votre diagnostic le plus rapide. Décalez chaque exécution d'une bougie plus tard. Si la performance s'effondre, vous tradiez dans le passé.
  5. L'ampleur du leakage est spécifique au canal. Les fuites d'exécution et d'indicateur sont dévastatrices ; la normalisation sur toute la série d'une règle de signe est presque gratuite. Mesurez la fuite à travers le canal par lequel elle entre réellement — ne le supposez pas.

L'étude contrôlée complète — les trois fuites, le balayage de dose, l'analyse de déploiement erroné, les méthodes formelles, et chaque chiffre reproductible à partir d'un unique script déterministe — se trouve dans l'article compagnon sur lookahead.marketmaker.cc, avec code et données sur github.com/suenot/lookahead-inflation.

La stratégie de notre expérience nulle n'avait aucun edge du tout. Elle affichait pourtant un Sharpe de 15. Si votre backtest semble trop beau, la première chose à suspecter n'est pas votre génie — c'est votre horloge.

blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Gardez une longueur d'avance sur le marché

Abonnez-vous à notre newsletter pour des insights exclusifs sur le trading IA, des analyses de marché et des mises à jour de la plateforme.

Nous respectons votre vie privée. Désabonnement possible à tout moment.