Look-Ahead Bias: Come un Errore di un Singolo Bar Genera uno Sharpe di 15 dal Rumore Puro
Fa parte della serie "Backtest Senza Illusioni".
📄 Questo articolo è cresciuto fino a diventare un paper di ricerca. Tre sottili leak di look-ahead vengono messi alla prova in un test controllato contro una ground truth nota (4,000 storie simulate). Leggi il paper online (versione interattiva + PDF) su lookahead.marketmaker.cc, codice e dati su github.com/suenot/lookahead-inflation.
Qualche settimana fa il nostro benchmark di ricerca dei parametri ci stava mentendo, e per poco non ce ne siamo accorti.
Il motore sembrava pulito. Logica a bar chiuso, una divisione walk-forward rolling onesta, una ricerca Sobol/QMC sullo spazio dei parametri, una finestra di test tenuta da parte. La ricerca trovava configurazioni che sembravano buone in-sample. L'unico problema: out-of-sample, quasi tutto era negativo. Abbiamo pensato che la strategia fosse semplicemente debole.
Poi abbiamo trovato una riga. Il segnale veniva deciso sulla chiusura del bar i, ma il fill veniva registrato sullo stesso bar i invece che sull'apertura del bar successivo. Un off-by-one nell'indice di esecuzione. Abbiamo spostato il fill a open[i+1] — l'unico prezzo a cui si poteva realmente transare dopo aver visto la chiusura del bar i — e il risultato out-of-sample ha invertito segno. La ricerca Sobol è passata da una perdita a un profitto. Nella strategia non è cambiato nulla. Avevamo semplicemente smesso di fare trading nel passato.
Questo è il look-ahead bias, e la parte inquietante è quanto piccolo fosse l'errore e quanto grande la conseguenza. Questo articolo è un self-audit controllato: costruiamo un simulatore in cui la ground truth è nota per costruzione, iniettiamo i leak sottili uno alla volta, e misuriamo esattamente quanto ciascuno gonfia il backtest. Il risultato principale: con nessun vantaggio reale, un fill sullo stesso bar genera uno Sharpe annualizzato di +14.8 dal puro rumore.
Cos'è davvero il look-ahead bias

Il look-ahead bias è qualsiasi punto della tua pipeline in cui una decisione o una misurazione usa informazioni che non sarebbero state disponibili, in tempo reale, nel momento in cui vengono usate. Gli esempi da manuale sono grossolani — usare gli utili dell'intero anno di un'azione a gennaio, o una revisione contabile non ancora pubblicata. Questi sono facili da individuare. Quelli che sopravvivono alla code review sono sottili, e si nascondono in tre luoghi:
- Esecuzione — decidi sul bar
ie fai il fill sul bari(oppure usi il massimo/minimo del bariper gli stop sullo stesso bar che ha generato il segnale). Transi a un prezzo correlato con la cosa che ti ha innescato. - Normalizzazione — applichi uno z-score, un min-max, o comunque scali una feature usando statistiche calcolate sull'intera serie, futuro incluso. Lo scaler "conosce" il test set.
- Indicatori / feature — applichi uno smoothing o un filtro con una finestra centrata (o che comunque guarda in avanti), così il valore al bar
icontiene già un pezzo del bari+1.
Tutti e tre sono forme di ciò che la letteratura di machine learning chiama leakage: la contaminazione di training/valutazione con informazioni provenienti dal futuro del target (Kaufman et al., 2012; Kapoor & Narayanan, 2023). In finanza il trattamento canonico è Advances in Financial Machine Learning (2018) di López de Prado — cross-validation purged, embargo, i pericoli del backtesting. La disciplina point-in-time risale almeno a Fama & French (1992), che ritardano deliberatamente i dati contabili di sei mesi affinché la variabile sia nota prima del rendimento che spiega.
La domanda a cui risponde questo articolo è quantitativa: non "il leakage è dannoso" (tutti sono d'accordo) ma "quanti punti di Sharpe ti regala ciascuna forma, e quali sono pericolose?" Senza un numero non puoi ragionarci sopra. Non puoi dire se un'inflazione di +0.3 sia rumore o se un'inflazione di +14 sia una pistola fumante.
Un simulatore con ground truth nota

Per misurare l'inflazione devi conoscere la verità. I dati reali non ti dicono mai la verità — ti danno una sola realizzazione e nessun oracolo. Quindi costruiamo un mercato sintetico in cui noi fissiamo il vantaggio.
Il processo generatore dei dati è strettamente causale e non esplosivo:
Qui è un drift latente persistente esogeno (un AR(1) con ), e il rendimento del bar ha un piccolo drift che è noto con un bar di anticipo. Poiché non dipende dai rendimenti passati, non c'è feedback e nulla esplode. Il parametro è la manopola che regola quanto vantaggio reale esiste:
- — il caso nullo: nessun vantaggio. Qualsiasi Sharpe positivo nel backtest è un artefatto al 100%.
- — un vantaggio reale, negoziabile: una regola di momentum onesta guadagna davvero denaro.
La strategia è deliberatamente semplice — una regola di segno basata sul momentum. La feature è la somma trailing- dei rendimenti ( bar), e la posizione è il suo segno:
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
Questa feature di momentum è il veicolo perfetto per studiare il leak sullo stesso bar, perché ha una proprietà che gli indicatori reali condividono: contiene meccanicamente il bar corrente. mom[t] include r[t]. Quindi se registri r[t] come tuo trade, stai in parte scommettendo su una quantità che è già dentro il tuo stesso segnale. Questo è il leak, reso concreto.
Setup: (volatilità dell'1% per bar), una commissione one-way di 0.00045 (round-trip 0.09%, in linea con il nostro motore), Sharpe annualizzato da (bar orari), 4,000 storie indipendenti di 4,000 bar ciascuna. Tutto è seedato e deterministico.
La pipeline onesta (l'unica negoziabile)
Decidi sulla chiusura del bar t, guadagni il rendimento del bar successivo, paghi le commissioni sui cambi di posizione:
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
I tre leak, ciascuno un singolo cambiamento chirurgico
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])
Ogni leak dista una riga dalla pipeline onesta. Ed è proprio questo il punto: non sono errori esotici, sono il genere di cosa che supera la review.
Risultati: la magnitudine di ciascun leak

Eseguito su 4,000 seed, ecco lo Sharpe annualizzato riportato da ciascuna pipeline, nel caso nullo (nessun vantaggio) e con un vantaggio reale (, calibrato in modo che lo Sharpe onesto sia un credibile +1.57):
| Pipeline | Nullo (nessun vantaggio) | Vantaggio reale |
|---|---|---|
| Onesta (la verità) | −0.74 | +1.57 |
| Fill sullo stesso bar | +14.79 | +15.85 |
| Sbirciata dell'indicatore (1 bar) | +4.76 | +6.62 |
| Normalizzazione sull'intera serie | −0.84 | +1.46 |
Gli intervalli di confidenza al 95% tra i seed sono ±0.05 o più stretti in ogni cella; i t-test appaiati sull'inflazione sono astronomicamente significativi dove l'effetto è reale (t > 400, p ≈ 0).
Leggi prima la colonna nulla, perché è l'esperimento più pulito possibile: non c'è nessun vantaggio, quindi la pipeline onesta perde correttamente denaro (−0.74, il trascinamento dovuto al pagare commissioni per fare trading sul rumore). Ora guarda cosa fanno i leak a quello stesso nulla:
- Fill sullo stesso bar: −0.74 → +14.79. Una strategia con potere predittivo pari a zero, che fa trading su rumore casuale, riporta uno Sharpe annualizzato di quasi 15. Non è un bias sottile; è una fabbricazione. Il meccanismo è esattamente quello che abbiamo costruito: la feature di momentum contiene
r[t], quindi registrarer[t]significa scommettere sul proprio stesso segnale. - Sbirciata dell'indicatore: −0.74 → +4.76. Lasciare che lo smoother veda un bar nel futuro genera uno Sharpe vicino a 5 dal rumore, perché il valore smussato a
tora è correlato con l'r[t+1]che stai per guadagnare. - Normalizzazione sull'intera serie: −0.74 → −0.84. Praticamente nessuna inflazione. Questo è il risultato onesto e non ovvio (ne parliamo più avanti).
La colonna del vantaggio porta un messaggio ancora più insidioso. Quando un vantaggio reale esiste davvero (onesto +1.57), i leak non si limitano ad aggiungere una costante — spingono lo Sharpe misurato a +15.85 e +6.62, ben oltre il +1.57 che potresti realmente negoziare. Quindi il numero misurato non può distinguere l'abilità dal leak. Un +6 con leak e un +6 onesto sembrano identici nel report. Scopri quale dei due avevi solo dopo aver dispiegato il capitale.
Il leak è un gradiente, non un interruttore

Un'obiezione naturale: "registrare l'intero bar del segnale è un errore estremo e irrealistico." Quindi abbiamo esplorato la dose — la frazione del bar del segnale catturata dal leak, da 0 (onesto) a 1 (leak completo sullo stesso bar):
| Frazione di cattura | Sharpe nullo | Sharpe con vantaggio |
|---|---|---|
| 0.00 (onesto) | −0.74 | +1.57 |
| 0.25 | +3.90 | +6.41 |
| 0.50 | +9.86 | +12.20 |
| 1.00 (leak completo) | +14.79 | +15.85 |
Catturare solo un quarto del bar del segnale porta una strategia senza vantaggio da −0.74 a +3.90. Non serve l'intero off-by-one per essere ingannati; un fill leggermente troppo favorevole — un tocco di slippage ottimistico sul bar del segnale, uno stop intrabar verificato contro il bar che lo ha innescato — basta per superare la maggior parte delle soglie "deployabili". L'inflazione è liscia e monotona rispetto a quanto presente ti concedi di negoziare.
Con quale frequenza questo porta in produzione una strategia in perdita?
Il numero che dovrebbe preoccupare un praticante è il tasso di falso deployment: quanto spesso un leak fa superare a una configurazione realmente in perdita la soglia che useresti per darle il via libera. Usando "Sharpe annualizzato ≥ 1.0" come criterio di deploy, nel caso nullo:
- Fill sullo stesso bar: il 68% delle strategie senza vantaggio sembra deployabile e è realmente in perdita. Due configurazioni di puro rumore su tre supererebbero un gate Sharpe-≥-1 e perderebbero denaro dal vivo. (Questo tasso è definito in modo pulito qui perché il leak è puramente nell'esecuzione — la controparte onesta è lo stesso segnale con un fill onesto.)
- Sbirciata dell'indicatore: spinge praticamente ogni configurazione senza vantaggio oltre la soglia di deploy (il 99.9% supera Sharpe ≥ 1) — farebbe passare il rumore dritto in produzione.
- Normalizzazione sull'intera serie: il 12% supera la soglia — essenzialmente il tasso di base del rumore, nessun premio da leakage.
La tassonomia, e come rilevare ciascuno
I tre leak non sono ugualmente pericolosi, e le differenze sono istruttive.
1. Leakage di esecuzione (quello costoso)

Sintomo: il prezzo di fill è correlato con il segnale perché provengono dallo stesso bar. Magnitudine: enorme (+15 dal rumore a dose piena, +3.9 a un quarto di dose). Perché è il peggiore: il tuo segnale è, quasi per definizione, costruito a partire dall'azione di prezzo recente, quindi il rendimento del bar del segnale è esattamente la cosa con cui la tua feature è più correlata. Registrarlo equivale quasi a guardare la risposta.
Rilevamento — il test dello shift di un bar. Questa è la diagnostica più preziosa in assoluto di questo articolo. Prendi il tuo backtest e sposta ogni fill di un bar più tardi (decidi su i, fai il fill su open[i+1]). Se il risultato si muove appena, la tua esecuzione era onesta. Se il risultato crolla o inverte segno, stavi facendo trading nel passato. È esattamente ciò che è successo alla nostra ricerca Sobol: sposta i fill, e un OOS "profittevole" si è rivelato una perdita — o meglio, la relazione reale è emersa una volta rimosso il leak.
entry_price = open_[i + 1] # NOT close[i], NOT open[i]
2. Leakage di indicatori / feature (quello silenzioso)
Sintomo: un indicatore al bar i dipende da dati di i+1 o successivi — una media mobile centrata, un filtro senza ritardo causale, un'etichetta di picco/valle che richiede bar futuri per essere confermata, una trasformazione in stile Heikin-Ashi alimentata con candele future. Magnitudine: grande (+4.8 dal rumore). Perché si nasconde: il leak è sepolto dentro una chiamata di libreria. scipy.signal.filtfilt è zero-phase — e zero-phase significa non causale. Una feature "questo bar è un massimo locale" è inconoscibile finché il bar successivo non si forma.
Rilevamento: per ogni indicatore, chiediti qual è l'indice più alto che legge? Se il calcolo del valore a t tocca mai t+1, non è causale. Calcola gli indicatori su una finestra causale espandente/rolling e verifica che il valore al bar t sia identico indipendentemente dal fatto che nell'array esistano o meno bar successivi a t. (Le nostre implementazioni di HMA/ADX superano questo test: ogni output a t legge solo input a ≤ t.)
3. Leakage di normalizzazione (quello specifico del canale)
Sintomo: uno scaler (StandardScaler, min-max, uno z-score globale) viene fittato sull'intero dataset, test set incluso. Gli avvertimenti canonici del ML sono espliciti su questo — Elements of Statistical Learning §7.10.2 di Hastie, Tibshirani & Friedman ("the wrong and right way to do cross-validation"), e la guida ai common pitfalls di scikit-learn stessa: "the average should be the average of the train subset, not the average of all the data."
Magnitudine nel nostro test: ≈ zero (−0.74 → −0.84). Questo è il risultato sorprendente e onesto, e vale la pena capirlo piuttosto che memorizzarlo.
Perché non si è gonfiato? Perché la nostra strategia usa la feature solo attraverso il suo segno (una soglia a zero). Lo scaling per deviazione standard non cambia mai un segno, e il centraggio sulla media globale sposta solo leggermente l'attraversamento dello zero. Quindi la standardizzazione sull'intera serie di una regola di puro segno è pressoché innocua.
Non generalizzare troppo questo risultato. Il leakage di normalizzazione è specifico del canale. Nel momento in cui la tua strategia usa la magnitudine della feature — position sizing proporzionale a uno z-score, una soglia di ingresso non nulla scelta guardando la distribuzione scalata, una rete neurale che consuma input standardizzati — lo scaler consapevole del futuro inizia a contare, e conta tanto di più quanto più le statistiche globali differiscono da quelle causali. Il nostro risultato non è "il leakage di normalizzazione è sicuro." È "la magnitudine del leakage dipende dal canale attraverso cui la quantità con leak entra nella decisione, e dovresti misurarla invece di darla per scontata." Una regola di segno è l'unico caso in cui questo particolare leak è economico.
Dove si collega tutto questo
Il look-ahead bias è il primo anello di una catena che questa serie sta documentando:
- Corrompe l'input della validazione. Un backtest con leak attraverserà senza problemi una divisione walk-forward e sembrerà un plateau ampio piuttosto che un picco overfittato — il leak è coerente tra i fold, quindi la cross-validation non può coglierlo. Il leakage è la modalità di fallimento a monte dell'overfitting, e nessuna quantità di validazione onesta a valle ti salverà.
- Interagisce con la ricerca dei parametri: una ricerca su migliaia di trial su dati con leak troverà la configurazione che sfrutta il leak nel modo più aggressivo. Il "vincitore" è il peggiore colpevole.
- È il motivo per cui la parità backtest-live diverge. Un leak è la spiegazione più pulita per un divario del 30–50% tra backtest e bot, perché il trading live è, meccanicamente, l'unico luogo in cui non puoi sbirciare.
La disciplina che cattura tutto questo è la stessa che la letteratura accademica raccomanda da anni: trattare un backtest come un esperimento statistico con un confine informativo rigoroso. Bailey, Borwein, López de Prado & Zhu hanno mostrato quanto facilmente l'overfitting fabbrichi performance fittizie (2014); il protocollo di backtesting di Arnott, Harvey & Markowitz (2019) codifica questa igiene. Il look-ahead bias è il confine più elementare di tutti — il confine nel tempo — ed è il più economico da violare per errore.
Punti Chiave

- Il look-ahead bias è quantitativamente enorme e qualitativamente invisibile. Un singolo errore di esecuzione di un bar ha trasformato uno Sharpe di −0.74 (puro rumore, che perde correttamente) in +14.79. L'errore è una riga; la conseguenza è un track record fabbricato.
- È un gradiente. Catturare anche solo il 25% del bar del segnale produce +3.90 dal nulla. Non serve un bug plateale — basta un po' troppo ottimismo nei tuoi fill.
- Il numero misurato non può distinguere l'abilità dal leak. Quando esiste un vantaggio reale, i leak gonfiano il report ben oltre la verità negoziabile. L'unica difesa è il processo, non la metrica.
- Il test dello shift di un bar è la tua diagnostica più rapida. Sposta ogni fill di un bar più tardi. Se la performance crolla, stavi facendo trading nel passato.
- La magnitudine del leakage è specifica del canale. Esecuzione e sbirciate degli indicatori sono devastanti; la normalizzazione sull'intera serie di una regola di segno è quasi gratuita. Misura il leak attraverso il canale con cui entra realmente — non darlo per scontato.
Lo studio controllato completo — tutti e tre i leak, lo sweep della dose, l'analisi del falso deployment, i metodi formali, e ogni numero riproducibile da un singolo script deterministico — si trova nel paper di accompagnamento su lookahead.marketmaker.cc, con codice e dati su github.com/suenot/lookahead-inflation.
La strategia nel nostro esperimento nullo non aveva alcun vantaggio. Ha comunque mostrato uno Sharpe di 15. Se il tuo backtest sembra troppo bello, la prima cosa da sospettare non è il tuo genio — è il tuo orologio.
Autori
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.