Look-Ahead Bias: hoe een fout van één bar een Sharpe van 15 fabriceert uit pure ruis
Onderdeel van de serie "Backtests Without Illusions".
📄 Dit artikel is uitgegroeid tot een onderzoekspaper. Drie subtiele look-ahead-lekken worden getest tegen bekende ground truth (4.000 gesimuleerde historieën). Lees de paper online (interactieve versie + PDF) op lookahead.marketmaker.cc, code en data op github.com/suenot/lookahead-inflation.
Een paar weken geleden loog onze parameter-search-benchmark tegen ons, en we hadden het bijna niet gemerkt.
De engine zag er schoon uit. Closed-bar-logica, een eerlijke rollende walk-forward-split, een Sobol/QMC-zoekopdracht over de parameterruimte, een apart gehouden testvenster. De zoekopdracht vond configuraties die er in-sample goed uitzagen. Het enige probleem: out-of-sample was bijna alles negatief. We namen aan dat de strategie gewoon zwak was.
Toen vonden we één regel. Het signaal werd bepaald op de close van bar i, maar de fill werd geboekt op diezelfde bar i in plaats van op de open van de volgende bar. Eén off-by-one in de execution-index. We verplaatsten de fill naar open[i+1] — de enige prijs waartegen je daadwerkelijk kon handelen nadat je de close van bar i had gezien — en het out-of-sample-resultaat draaide van teken om. De Sobol-zoekopdracht ging van een verlies naar een winst. Er was niets aan de strategie veranderd. We waren gewoon gestopt met handelen in het verleden.
Dat is look-ahead bias, en het verontrustende deel is hoe klein de fout was en hoe groot het gevolg. Dit artikel is een gecontroleerde zelfaudit: we bouwen een simulator waarbij de ground truth bekend is door constructie, injecteren de subtiele lekken één voor één, en meten precies hoeveel elk lek de backtest opblaast. De hoofdconclusie: met helemaal geen echte edge fabriceert een fill op dezelfde bar een geannualiseerde Sharpe van +14,8 uit pure ruis.
Wat look-ahead bias eigenlijk is

Look-ahead bias is elk punt in je pipeline waar een beslissing of meting informatie gebruikt die, in real time, op het moment dat ze gebruikt wordt, niet beschikbaar zou zijn geweest. De schoolvoorbeelden zijn grof — het gebruiken van de volledige jaarwinst van een aandeel in januari, of een herziening die nog niet gepubliceerd was. Die zijn gemakkelijk te herkennen. Degene die een codereview overleven zijn subtiel, en verstoppen zich op drie plekken:
- Executie — je beslist op bar
ien fillt op bari(of gebruikt de high/low van barivoor stops op precies de bar die het signaal genereerde). Je verhandelt tegen een prijs die gecorreleerd is met datgene wat je triggerde. - Normalisatie — je berekent een z-score, min-max, of schaalt een feature anderszins met statistieken berekend over de hele reeks, inclusief de toekomst. De scaler "kent" de testset.
- Indicatoren / features — je gladstrijkt of filtert met een venster dat gecentreerd is (of op een andere manier vooruitkijkt), zodat de waarde op bar
ial een stukje van bari+1bevat.
Alle drie zijn vormen van wat de machine-learning-literatuur leakage noemt: de verontreiniging van training/evaluatie met informatie uit de toekomst van het doel (Kaufman et al., 2012; Kapoor & Narayanan, 2023). In de financiële wereld is de canonieke behandeling López de Prado's Advances in Financial Machine Learning (2018) — purged cross-validation, embargoing, de gevaren van backtesting. De point-in-time-discipline gaat minstens terug tot Fama & French (1992), die boekhoudkundige data bewust zes maanden vertragen zodat de variabele bekend is voordat het rendement dat ze verklaart, plaatsvindt.
De vraag die dit artikel beantwoordt is kwantitatief: niet "is leakage slecht" (daar is iedereen het over eens), maar "hoeveel Sharpe-punten levert elke vorm op, en welke zijn gevaarlijk?" Zonder een getal kun je er niet over redeneren. Je kunt niet vaststellen of een inflatie van +0,3 ruis is of een inflatie van +14 een overtuigend bewijs is.
Een simulator met bekende ground truth

Om inflatie te meten moet je de waarheid kennen. Echte data verklapt nooit de waarheid — het geeft je één realisatie en geen orakel. Dus bouwen we een synthetische markt waarin wij de edge instellen.
Het datagenererende proces is strikt causaal en niet-explosief:
Hier is een exogene persistente latente drift (een AR(1) met ), en het bar-rendement heeft een kleine drift die één bar van tevoren bekend is. Omdat niet afhangt van eerdere rendementen, is er geen feedback en explodeert niets. De parameter is de regelaar voor hoeveel echte edge er bestaat:
- — de nul: helemaal geen edge. Elke positieve backtest-Sharpe is 100% artefact.
- — een echte, verhandelbare edge: een eerlijke momentumregel verdient daadwerkelijk geld.
De strategie is bewust eenvoudig — een momentum-tekenregel. De feature is de trailing--som van rendementen ( bars), en de positie is het teken ervan:
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
Deze momentum-feature is het perfecte voertuig om het lek op dezelfde bar te bestuderen, omdat het een eigenschap heeft die echte indicatoren delen: het bevat mechanisch de huidige bar. mom[t] bevat r[t]. Dus als je r[t] boekt als je trade, wed je deels op een grootheid die al binnenin je eigen signaal zit. Dat is het lek, concreet gemaakt.
Opzet: (1% volatiliteit per bar), een enkelrichting-fee van 0,00045 (round-trip 0,09%, overeenkomend met onze engine), Sharpe geannualiseerd met (uurlijkse bars), 4.000 onafhankelijke historieën van elk 4.000 bars. Alles is seeded en deterministisch.
De eerlijke pipeline (de enige verhandelbare)
Beslissen op de close van bar t, het rendement van de volgende bar verdienen, fees betalen bij positiewisselingen:
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
De drie lekken, elk één chirurgische wijziging
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])
Elk lek is één regel verwijderd van de eerlijke pipeline. Dat is precies het punt: dit zijn geen exotische fouten, het zijn precies het soort dingen dat een review doorstaat.
Resultaten: de omvang van elk lek

Uitgevoerd over 4.000 seeds, hier is de geannualiseerde Sharpe die elke pipeline rapporteert, onder de nul (geen edge) en onder een echte edge (, zo ingesteld dat de eerlijke Sharpe een geloofwaardige +1,57 is):
| Pipeline | Nul (geen edge) | Echte edge |
|---|---|---|
| Eerlijk (de waarheid) | −0,74 | +1,57 |
| Fill op dezelfde bar | +14,79 | +15,85 |
| Indicator-vooruitblik (1 bar) | +4,76 | +6,62 |
| Normalisatie over hele reeks | −0,84 | +1,46 |
95%-betrouwbaarheidsintervallen over de seeds zijn ±0,05 of strakker in elke cel; gepaarde t-toetsen op de inflatie zijn astronomisch significant waar het effect echt is (t > 400, p ≈ 0).
Lees eerst de nulkolom, want dat is het schoonste mogelijke experiment: er is geen edge, dus de eerlijke pipeline verliest terecht geld (−0,74, de wrijving van fees betalen om ruis te verhandelen). Kijk nu wat de lekken met datzelfde niets doen:
- Fill op dezelfde bar: −0,74 → +14,79. Een strategie zonder enige voorspellende kracht, die willekeurige ruis verhandelt, rapporteert een geannualiseerde Sharpe van bijna 15. Dit is geen subtiele vertekening; het is een fabricage. Het mechanisme is precies dat wat we hebben ingebouwd: de momentum-feature bevat
r[t], dusr[t]boeken is wedden op je eigen signaal. - Indicator-vooruitblik: −0,74 → +4,76. De gladstrijker één bar in de toekomst laten kijken fabriceert een Sharpe dicht bij 5 uit ruis, omdat de gladgestreken waarde bij
tnu correleert met der[t+1]die je op het punt staat te verdienen. - Normalisatie over hele reeks: −0,74 → −0,84. Vrijwel geen inflatie. Dit is de eerlijke, niet-voor-de-hand-liggende bevinding (hierover verderop meer).
De edge-kolom levert de nog verradelijkere boodschap. Wanneer een echte edge wél bestaat (eerlijk +1,57), tellen de lekken niet alleen een constante op — ze duwen de gemeten Sharpe naar +15,85 en +6,62, ver boven de +1,57 die je daadwerkelijk kon verhandelen. Het gemeten getal kan dus geen onderscheid maken tussen vaardigheid en lek. Een gelekt +6 en een eerlijk +6 zien er identiek uit in het rapport. Je komt er pas achter welke van de twee je had nadat je kapitaal hebt ingezet.
Het lek is een gradiënt, geen schakelaar

Een voor de hand liggend bezwaar: "de hele signaalbar boeken is een extreme, onrealistische fout." Dus hebben we de dosis doorlopen — de fractie van de signaalbar die door het lek wordt gevangen, van 0 (eerlijk) tot 1 (volledig lek op dezelfde bar):
| Vangfractie | Sharpe nul | Sharpe edge |
|---|---|---|
| 0,00 (eerlijk) | −0,74 | +1,57 |
| 0,25 | +3,90 | +6,41 |
| 0,50 | +9,86 | +12,20 |
| 1,00 (volledig lek) | +14,79 | +15,85 |
Slechts een kwart van de signaalbar vangen brengt een strategie zonder edge van −0,74 naar +3,90. Je hebt de volledige off-by-one niet nodig om misleid te worden; een fill die iets te gunstig is — een vleugje optimistische slippage op de signaalbar, een intrabar-stop gecontroleerd tegen precies de bar die hem triggerde — is genoeg om de meeste "inzetbare" drempels te halen. De inflatie is glad en monotoon in hoeveel van het heden je jezelf toestaat te verhandelen.
Hoe vaak brengt dit een verliesgevende strategie in productie?
Het getal dat een praktijkbeoefenaar zorgen zou moeten baren is de false-deployment rate: hoe vaak een lek een daadwerkelijk geldverliezende configuratie de drempel laat halen die je zou gebruiken om ze groen licht te geven. Met "geannualiseerde Sharpe ≥ 1,0" als deploy-criterium, onder de nul:
- Fill op dezelfde bar: 68% van de strategieën zonder edge lijkt inzetbaar en verliest daadwerkelijk geld. Twee op de drie pure-ruis-configuraties zouden een Sharpe-≥-1-poort halen en live geld verliezen. (Dit percentage is hier netjes gedefinieerd omdat het lek puur in de executie zit — het eerlijke equivalent is hetzelfde signaal met een eerlijke fill.)
- Indicator-vooruitblik: het duwt vrijwel elke configuratie zonder edge ook over de deploy-drempel (99,9% haalt Sharpe ≥ 1) — het zou ruis regelrecht naar productie wuiven.
- Normalisatie over hele reeks: 12% haalt de drempel — in wezen het basispercentage van ruis, geen leakage-premie.
De taxonomie, en hoe elk lek op te sporen
De drie lekken zijn niet even gevaarlijk, en de verschillen zijn leerzaam.
1. Executie-leakage (de dure)

Symptoom: de fillprijs is gecorreleerd met het signaal omdat ze uit dezelfde bar komen. Omvang: enorm (+15 uit ruis bij volledige dosis, +3,9 bij kwart dosis). Waarom het het ergste is: je signaal is, bijna per definitie, opgebouwd uit recente prijsactie, dus het rendement van de signaalbar is precies datgene waarmee je feature het sterkst correleert. Het boeken ervan komt neer op het opzoeken van het antwoord.
Detectie — de one-bar-shift-test. Dit is de meest waardevolle diagnose in dit artikel. Neem je backtest en verschuif elke fill één bar later (beslis op i, fill op open[i+1]). Als het resultaat nauwelijks verandert, was je executie eerlijk. Als het resultaat instort of van teken wisselt, handelde je in het verleden. Dit is precies wat er gebeurde met onze Sobol-zoekopdracht: verschuif de fills, en een "winstgevende" OOS bleek een verlies te zijn — of beter gezegd, de echte relatie kwam bovendrijven zodra het lek was verwijderd.
entry_price = open_[i + 1] # NOT close[i], NOT open[i]
2. Indicator-/feature-leakage (de stille)
Symptoom: een indicator op bar i hangt af van data uit i+1 of later — een gecentreerd voortschrijdend gemiddelde, een filter zonder causale vertraging, een piek/dal-label dat toekomstige bars nodig heeft om te bevestigen, een Heikin-Ashi-achtige transformatie die met toekomstige candles wordt gevoed. Omvang: groot (+4,8 uit ruis). Waarom het zich verstopt: het lek zit diep verborgen in een bibliotheekaanroep. scipy.signal.filtfilt is zero-phase — en zero-phase betekent niet-causaal. Een feature "deze bar is een lokaal maximum" is onkenbaar totdat de volgende bar wordt afgedrukt.
Detectie: vraag voor elke indicator: welke hoogste index leest hij? Als het berekenen van de waarde op t ooit t+1 raakt, is het niet-causaal. Bereken indicatoren over een uitbreidend/rollend causaal venster en verifieer dat de waarde op bar t identiek is, ongeacht of er bars na t in de array bestaan. (Onze HMA/ADX-implementaties doorstaan deze test: elke output op t leest alleen inputs op ≤ t.)
3. Normalisatie-leakage (de kanaalspecifieke)
Symptoom: een scaler (StandardScaler, min-max, een globale z-score) wordt gefit op de hele dataset, testset inbegrepen. De canonieke ML-waarschuwingen zijn hier expliciet over — Hastie, Tibshirani & Friedmans Elements of Statistical Learning §7.10.2 ("the wrong and right way to do cross-validation"), en scikit-learns eigen common-pitfalls-gids: "the average should be the average of the train subset, not the average of all the data."
Omvang in onze test: ≈ nul (−0,74 → −0,84). Dit is de verrassende, eerlijke bevinding, en het is de moeite waard om te begrijpen in plaats van uit het hoofd te leren.
Waarom blies het niet op? Omdat onze strategie de feature alleen gebruikt via het teken ervan (een nuldrempel). Standaarddeviatie-schaling verandert nooit een teken, en globale-gemiddelde-centrering verschuift de nuldoorgang slechts licht. Dus whole-series-standaardisatie van een pure tekenregel is bijna onschadelijk.
Generaliseer dit niet te veel. Normalisatie-leakage is kanaalspecifiek. Op het moment dat je strategie de grootte van de feature gebruikt — positiegrootte proportioneel aan een z-score, een niet-nul-instapdrempel gekozen door naar de geschaalde verdeling te kijken, een neuraal netwerk dat gestandaardiseerde inputs consumeert — begint de toekomstbewuste scaler ertoe te doen, en des te meer naarmate de globale statistieken meer afwijken van de causale. Onze bevinding is niet "normalisatie-leakage is veilig." Het is "de omvang van leakage hangt af van het kanaal waardoor de gelekte grootheid de beslissing binnenkomt, en je moet het meten in plaats van het aan te nemen." Een tekenregel is het ene geval waarin dit specifieke lek goedkoop is.
Waar dit op aansluit
Look-ahead bias is de eerste schakel in een keten die deze serie documenteert:
- Het verontreinigt de input van validatie. Een gelekte backtest zeilt moeiteloos door een walk-forward-split en ziet eruit als een breed plateau in plaats van een overfit-piek — het lek is consistent over de folds, dus cross-validatie kan het niet vangen. Leakage is de faalmodus voorafgaand aan overfitting, en geen hoeveelheid eerlijke validatie stroomafwaarts zal je redden.
- Het interageert met parameter search: een zoekopdracht over duizenden trials op gelekte data vindt de configuratie die het lek het agressiefst uitbuit. De "winnaar" is de ergste overtreder.
- Het is de reden waarom backtest-live-pariteit uiteenloopt. Een lek is de duidelijkste verklaring voor een kloof van 30–50% tussen backtest en bot, omdat live trading, mechanisch gezien, de enige plek is waar je niet kunt spieken.
De discipline die dit alles vangt is dezelfde die de academische literatuur al jaren aanmoedigt: behandel een backtest als een statistisch experiment met een strikte informatiegrens. Bailey, Borwein, López de Prado & Zhu toonden aan hoe gemakkelijk overfitting nepprestaties fabriceert (2014); Arnott, Harvey & Markowitz' backtesting-protocol (2019) codificeert deze hygiëne. Look-ahead bias is de meest fundamentele grens van allemaal — de grens in de tijd — en de goedkoopste om per ongeluk te overtreden.
Belangrijkste conclusies

- Look-ahead bias is kwantitatief enorm en kwalitatief onzichtbaar. Eén enkele executiefout van één bar veranderde een Sharpe van −0,74 (pure ruis, terecht verliesgevend) in +14,79. De fout is één regel; het gevolg is een gefabriceerd trackrecord.
- Het is een gradiënt. Slechts 25% van de signaalbar vangen levert +3,90 op uit het niets. Je hebt geen flagrante bug nodig — een beetje te veel optimisme in je fills is genoeg.
- Het gemeten getal kan geen onderscheid maken tussen vaardigheid en lek. Wanneer een echte edge bestaat, blazen lekken het rapport ver voorbij de verhandelbare waarheid op. De enige verdediging is het proces, niet de metriek.
- De one-bar-shift-test is je snelste diagnose. Verschuif elke fill één bar later. Als de prestatie instort, handelde je in het verleden.
- De omvang van leakage is kanaalspecifiek. Executie- en indicator-vooruitblikken zijn verwoestend; whole-series-normalisatie van een tekenregel is bijna gratis. Meet het lek via het kanaal waardoor het daadwerkelijk binnenkomt — neem het niet aan.
De volledige gecontroleerde studie — alle drie de lekken, de dosis-sweep, de false-deployment-analyse, de formele methoden, en elk getal reproduceerbaar vanuit één enkel deterministisch script — staat in de begeleidende paper op lookahead.marketmaker.cc, met code en data op github.com/suenot/lookahead-inflation.
De strategie in ons null-experiment had helemaal geen edge. Toch liet ze een Sharpe van 15 zien. Als je backtest er te goed uitziet, is het eerste wat je moet verdenken niet je genialiteit — het is je klok.
Auteurs
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.