Look-Ahead Bias: Tek Barlık Bir Hata Saf Gürültüden Nasıl 15'lik Bir Sharpe Üretir
"Yanılsamasız Backtestler" serisinin bir parçası.
📄 Bu makale bir araştırma makalesine dönüştü. Üç ince look-ahead sızıntısı, bilinen zemin gerçeğine (ground truth) karşı kontrollü bir teste tabi tutuluyor (4,000 simüle edilmiş geçmiş). Makaleyi çevrimiçi olarak (interaktif sürüm + PDF) lookahead.marketmaker.cc adresinde, kod ve veriyi ise github.com/suenot/lookahead-inflation adresinde okuyabilirsiniz.
Birkaç hafta önce parametre-arama benchmarkımız bize yalan söylüyordu ve neredeyse fark etmiyorduk.
Motor temiz görünüyordu. Kapalı-bar mantığı, dürüst bir kayan walk-forward bölümü, parametre uzayı üzerinde bir Sobol/QMC araması, ayrılmış bir test penceresi. Arama, in-sample'da iyi görünen konfigürasyonlar buldu. Tek sorun: out-of-sample'da neredeyse her şey negatifti. Stratejinin basitçe zayıf olduğunu varsaydık.
Sonra bir satır bulduk. Sinyal, bar i'nin kapanışında karar veriliyordu, ama dolum, sonraki barın açılışı yerine aynı bar i üzerinde kaydediliyordu. Execution indeksinde bir off-by-one. Dolumu open[i+1]'e taşıdık — bar-i kapanışını gördükten sonra gerçekten işlem yapabileceğiniz tek fiyat — ve out-of-sample sonuç işaret değiştirdi. Sobol araması zarardan kâra geçti. Strateji hakkında hiçbir şey değişmedi. Biz sadece geçmişte işlem yapmayı bırakmıştık.
İşte look-ahead bias budur, ve rahatsız edici olan kısım hatanın ne kadar küçük, sonucun ise ne kadar büyük olduğudur. Bu makale kontrollü bir öz-denetim (self-audit): zemin gerçeğinin kurgu gereği bilindiği bir simülatör inşa ediyoruz, ince sızıntıları birer birer enjekte ediyoruz, ve her birinin backtesti tam olarak ne kadar şişirdiğini ölçüyoruz. Manşet: hiçbir gerçek avantaj olmadan, aynı-bar dolumu saf gürültüden yıllıklandırılmış +14.8'lik bir Sharpe üretiyor.
Look-ahead bias aslında nedir

Look-ahead bias, pipeline'ınızdaki herhangi bir noktada bir kararın veya ölçümün, kullanıldığı anda gerçek zamanlı olarak mevcut olmayacak bir bilgiyi kullanmasıdır. Ders kitabı örnekleri kabadır — bir hissenin Ocak ayında tam yıllık kazancını kullanmak, ya da henüz yayınlanmamış bir yeniden ifadeyi (restatement) kullanmak. Bunları fark etmek kolaydır. Kod incelemesinden sağ çıkanlar ise inceliklidir ve üç yerde saklanır:
- Execution — bar
iüzerinde karar verirsiniz ve yine bariüzerinde dolum (fill) yaparsınız (ya da sinyali üreten barın kendisinin high/low'unu stop için kullanırsınız). Sizi tetikleyen şeyle korelasyonlu bir fiyattan işlem yaparsınız. - Normalizasyon — bir özelliği, gelecek dahil tüm seri üzerinden hesaplanan istatistikler kullanarak z-score'lar, min-max yaparsınız veya başka şekilde ölçeklendirirsiniz. Ölçekleyici test setini "bilir."
- Göstergeler / özellikler — merkezlenmiş (ya da başka şekilde ileriye bakan) bir pencereyle yumuşatma veya filtreleme yaparsınız, böylece bar
i'deki değer zaten bari+1'in bir parçasını içerir.
Üçü de makine öğrenmesi literatüründe leakage (sızıntı) denen şeyin biçimleridir: eğitim/değerlendirmenin, hedefin geleceğinden gelen bilgiyle kirlenmesi (Kaufman ve ark., 2012; Kapoor & Narayanan, 2023). Finansta kanonik kaynak López de Prado'nun Advances in Financial Machine Learning (2018) kitabıdır — arındırılmış çapraz doğrulama, ambargo, backtesting'in tehlikeleri. Point-in-time disiplini en azından Fama & French'e (1992) kadar uzanır; onlar muhasebe verisini kasıtlı olarak altı ay geciktirir, böylece değişken açıkladığı getiriden önce bilinir hale gelir.
Bu makalenin cevapladığı soru niceldir: "sızıntı kötü müdür" değil (herkes bu konuda hemfikir), "her biçim size kaç Sharpe puanı kazandırır ve hangileri tehlikelidir?" Bir sayı olmadan bu konuda akıl yürütemezsiniz. +0.3'lük bir şişmenin gürültü mü, yoksa +14'lük bir şişmenin dumanı tüten bir silah mı olduğunu ayırt edemezsiniz.
Bilinen zemin gerçeğine sahip bir simülatör

Şişmeyi ölçmek için gerçeği bilmeniz gerekir. Gerçek veri size hiçbir zaman gerçeği söylemez — size tek bir gerçekleşme verir, kahin (oracle) vermez. Bu yüzden avantajı bizim belirlediğimiz sentetik bir piyasa kuruyoruz.
Veri üretme süreci kesinlikle nedensel (causal) ve patlamaz (non-explosive):
Burada , dışsal (exogenous) ve kalıcı gizli bir sürüklenmedir (drift) ( olan bir AR(1)), ve bar getirisi , bir bar önceden bilinen küçük bir sürüklenmeye sahiptir. geçmiş getirilere bağlı olmadığından, geri besleme yoktur ve hiçbir şey patlamaz. parametresi ne kadar gerçek avantaj olduğunun kadranıdır:
- — null: hiçbir avantaj yok. Herhangi bir pozitif backtest Sharpe'ı %100 yapaydır (artifact).
- — gerçek, işlem yapılabilir bir avantaj: dürüst bir momentum kuralı gerçekten para kazanır.
Strateji kasıtlı olarak basittir — bir momentum işaret kuralı. Özellik, getirilerin geriye dönük toplamıdır ( bar), ve pozisyon onun işaretidir:
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
Bu momentum özelliği, aynı-bar sızıntısını incelemek için mükemmel bir araçtır, çünkü gerçek göstergelerin paylaştığı bir özelliğe sahiptir: mekanik olarak mevcut barı içerir. mom[t], r[t]'yi içerir. Yani r[t]'yi işleminiz olarak kaydederseniz, kısmen zaten kendi sinyalinizin içinde olan bir niceliğe bahis oynuyorsunuzdur. Sızıntı budur, somutlaştırılmış hali.
Kurulum: (bar başına %1 volatilite), motorumuzla eşleşen tek yönlü 0.00045 ücret (round-trip %0.09), ile yıllıklandırılmış Sharpe (saatlik barlar), her biri 4,000 bar olan 4,000 bağımsız geçmiş. Her şey seed'li ve deterministiktir.
Dürüst pipeline (tek işlem yapılabilir olan)
Bar t'nin kapanışında karar verin, sonraki barın getirisini kazanın, pozisyon değişikliklerinde ücret ödeyin:
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
Üç sızıntı, her biri tek bir cerrahi değişiklik
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])
Her sızıntı, dürüst pipeline'dan tek bir satır uzaktadır. Bütün mesele bu: bunlar egzotik hatalar değil, incelemeden geçebilecek türden şeyler.
Sonuçlar: her sızıntının büyüklüğü

4,000 seed üzerinde çalıştırıldığında, her pipeline'ın raporladığı yıllıklandırılmış Sharpe, null (avantaj yok) altında ve gerçek bir avantaj altında (, dürüst Sharpe'ın inandırıcı bir +1.57 olacağı şekilde ayarlanmış) şöyledir:
| Pipeline | Null (avantaj yok) | Gerçek avantaj |
|---|---|---|
| Dürüst (gerçek) | −0.74 | +1.57 |
| Aynı-bar dolumu | +14.79 | +15.85 |
| Gösterge sızıntısı (1 bar) | +4.76 | +6.62 |
| Tüm-seri normalizasyonu | −0.84 | +1.46 |
Seed'ler genelinde %95 güven aralıkları her hücrede ±0.05 veya daha dar; şişme üzerindeki eşli t-testleri, etki gerçek olduğunda astronomik derecede anlamlıdır (t > 400, p ≈ 0).
Önce null sütununu okuyun, çünkü bu mümkün olan en temiz deneydir: avantaj yok, dolayısıyla dürüst pipeline doğru şekilde para kaybeder (−0.74, gürültü işlem yapmak için ücret ödemenin sürüklemesi). Şimdi sızıntıların bu hiçliğe ne yaptığına bakın:
- Aynı-bar dolumu: −0.74 → +14.79. Sıfır tahmin gücüne sahip, rastgele gürültü işlemi yapan bir strateji, neredeyse 15'lik bir yıllıklandırılmış Sharpe raporluyor. Bu ince bir yanlılık değil; bir uydurmadır. Mekanizma tam olarak inşa ettiğimiz mekanizmadır: momentum özelliği
r[t]'yi içerir, dolayısıylar[t]'yi kaydetmek kendi sinyalinize bahis oynamaktır. - Gösterge sızıntısı: −0.74 → +4.76. Yumuşatıcının bir bar ileriyi görmesine izin vermek, gürültüden 5'e yakın bir Sharpe üretir, çünkü
t'deki yumuşatılmış değer artık az sonra kazanacağınızr[t+1]ile korelasyonludur. - Tüm-seri normalizasyonu: −0.74 → −0.84. Neredeyse hiç şişme yok. Bu, dürüst ve göze çarpmayan bulgudur (aşağıda daha fazlası var).
Avantaj sütunu daha sinsi bir mesaj veriyor. Gerçek bir avantaj var olduğunda (dürüst +1.57), sızıntılar yalnızca sabit bir şey eklemekle kalmıyor — ölçülen Sharpe'ı, gerçekte işlem yapabileceğiniz +1.57'nin çok üzerinde, +15.85 ve +6.62'ye itiyorlar. Yani ölçülen sayı beceriyi sızıntıdan ayırt edemez. Sızdırılmış bir +6 ile dürüst bir +6, raporda birebir aynı görünür. Hangisine sahip olduğunuzu ancak sermayeyi devreye aldıktan sonra öğrenirsiniz.
Sızıntı bir anahtar değil, bir gradyandır

Doğal bir itiraz: "sinyal barının tamamını kaydetmek uç, gerçekçi olmayan bir hatadır." Bu yüzden dozu taradık — sızıntı tarafından yakalanan sinyal barının oranını, 0'dan (dürüst) 1'e (tam aynı-bar) kadar:
| Yakalama oranı | Null Sharpe | Avantaj Sharpe |
|---|---|---|
| 0.00 (dürüst) | −0.74 | +1.57 |
| 0.25 | +3.90 | +6.41 |
| 0.50 | +9.86 | +12.20 |
| 1.00 (tam sızıntı) | +14.79 | +15.85 |
Sinyal barının yalnızca dörtte birini yakalamak, avantajsız bir stratejiyi −0.74'ten +3.90'a taşır. Kandırılmak için tam bir off-by-one hatasına ihtiyacınız yok; biraz fazla lehinize olan bir dolum — sinyal barında bir tutam iyimser kayma (slippage), kendisini tetikleyen bara karşı kontrol edilen bar-içi bir stop — çoğu "işlem yapılabilir" eşiğini geçmeye yeter. Şişme, kendinize şimdiden ne kadar işlem yapma izni verdiğinizle düzgün ve monoton biçimde artar.
Bu, kaybettiren bir stratejiyi ne sıklıkla üretime sokuyor?
Bir uygulayıcıyı endişelendirmesi gereken sayı yanlış-devreye-alma oranıdır: bir sızıntının, gerçekte para kaybettiren bir konfigürasyonu, onu yeşil ışık yakmak için kullanacağınız çıtayı geçirme sıklığı. Devreye alma kriteri olarak "yıllıklandırılmış Sharpe ≥ 1.0" kullanıldığında, null altında:
- Aynı-bar dolumu: avantajsız stratejilerin %68'i işlem yapılabilir görünüyor ve gerçekte para kaybettiriyor. Üç saf-gürültü konfigürasyonundan ikisi bir Sharpe-≥-1 kapısını geçer ve canlıda para kaybeder. (Bu oran burada temiz biçimde tanımlıdır çünkü sızıntı tamamen execution'dadır — dürüst karşılığı, dürüst bir dolum ile aynı sinyaldir.)
- Gösterge sızıntısı: neredeyse her avantajsız konfigürasyonu da devreye alma çıtasının üzerine itiyor (%99.9'u Sharpe ≥ 1'i geçiyor) — gürültüyü doğrudan üretime sokardı.
- Tüm-seri normalizasyonu: %12'si çıtayı geçiyor — esasen gürültünün taban oranı, sızıntı primi yok.
Taksonomi ve her birini nasıl tespit edeceğiniz
Üç sızıntı eşit derecede tehlikeli değil ve aradaki farklar öğreticidir.
1. Execution sızıntısı (pahalı olan)

Belirti: dolum fiyatı sinyalle korelasyonludur, çünkü ikisi de aynı bardan gelir. Büyüklük: devasa (tam dozda gürültüden +15, çeyrek dozda +3.9). Neden en kötüsü: sinyaliniz, neredeyse tanım gereği, yakın zamandaki fiyat hareketinden inşa edilir, dolayısıyla sinyal barının getirisi tam olarak özelliğinizin en çok korelasyonlu olduğu şeydir. Onu kaydetmek, cevaba bakmaya yakındır.
Tespit — tek-bar kaydırma testi. Bu makaledeki en değerli tanı aracı budur. Backtestinizi alın ve her dolumu bir bar geriye (sonraya) kaydırın (i'de karar verin, open[i+1]'de dolum yapın). Sonuç neredeyse hiç değişmiyorsa, execution'ınız dürüsttü. Sonuç çöküyor veya işaret değiştiriyorsa, geçmişte işlem yapıyordunuz. Sobol aramamıza tam olarak bu oldu: dolumları kaydırın, "kârlı" bir OOS'un aslında bir zarar olduğu ortaya çıktı — ya da daha doğrusu, sızıntı kaldırıldığında gerçek ilişki yüzeye çıktı.
entry_price = open_[i + 1] # NOT close[i], NOT open[i]
2. Gösterge / özellik sızıntısı (sessiz olan)
Belirti: bar i'deki bir gösterge, i+1 veya sonrasından gelen veriye bağlıdır — merkezlenmiş bir hareketli ortalama, nedensel gecikmesi olmayan bir filtre, doğrulanmak için gelecekteki barlara ihtiyaç duyan bir tepe/dip etiketi, gelecekteki mumlarla beslenen Heikin-Ashi tarzı bir dönüşüm. Büyüklük: büyük (gürültüden +4.8). Neden gizlenir: sızıntı bir kütüphane çağrısının içine gömülüdür. scipy.signal.filtfilt sıfır-fazlıdır (zero-phase) — ve sıfır-faz, nedensel olmama anlamına gelir. "Bu bar yerel bir maksimumdur" özelliği, bir sonraki bar oluşana kadar bilinemez.
Tespit: her gösterge için, "okuduğu en yüksek indeks nedir?" diye sorun. t'deki değeri hesaplamak herhangi bir şekilde t+1'e dokunuyorsa, o nedensel değildir. Göstergeleri genişleyen/kayan nedensel bir pencerede hesaplayın ve t barındaki değerin, dizide t'den sonraki barlar olsun ya da olmasın aynı olduğunu doğrulayın. (HMA/ADX uygulamalarımız bunu geçiyor: t'deki her çıktı yalnızca ≤ t girdilerini okuyor.)
3. Normalizasyon sızıntısı (kanala özgü olan)
Belirti: bir ölçekleyici (StandardScaler, min-max, global bir z-score), test seti dahil olmak üzere tüm veri kümesi üzerinde fit edilir. Kanonik ML uyarıları bu konuda açıktır — Hastie, Tibshirani & Friedman'ın Elements of Statistical Learning §7.10.2'si ("çapraz doğrulamayı yapmanın yanlış ve doğru yolu"), ve scikit-learn'ün kendi common-pitfalls kılavuzu: "ortalama, tüm verinin ortalaması değil, eğitim alt kümesinin ortalaması olmalıdır."
Testimizdeki büyüklük: ≈ sıfır (−0.74 → −0.84). Bu şaşırtıcı, dürüst bir sonuçtur ve ezberlemek yerine anlamaya değer.
Neden şişmedi? Çünkü stratejimiz özelliği yalnızca işareti üzerinden kullanıyor (bir sıfır eşiği). Standart-sapma ölçeklendirmesi bir işareti asla değiştirmez, ve global-ortalama-merkezleme sıfır geçişini yalnızca hafifçe kaydırır. Yani saf bir işaret kuralının tüm-seri standardizasyonu neredeyse zararsızdır.
Bunu fazla genellemeyin. Normalizasyon sızıntısı kanala özgüdür. Stratejiniz özelliğin büyüklüğünü kullanmaya başladığı an — bir z-score'a orantılı pozisyon boyutlandırma, ölçeklenmiş dağılıma bakılarak seçilen sıfırdan farklı bir giriş eşiği, standardize girdiler tüketen bir sinir ağı — geleceği bilen ölçekleyici önemli hale gelir, ve global istatistikler nedensel olanlardan ne kadar farklıysa o kadar önemli hale gelir. Bizim sonucumuz "normalizasyon sızıntısı güvenlidir" değildir. "Sızıntı büyüklüğü, sızdırılan niceliğin karara girdiği kanala bağlıdır ve bunu varsaymak yerine ölçmelisiniz" sonucudur. İşaret kuralı, bu özel sızıntının ucuz olduğu tek durumdur.
Bunun bağlandığı yer
Look-ahead bias, bu serinin belgelediği zincirdeki ilk halkadır:
- Doğrulamanın girdisini kirletir. Sızıntılı bir backtest, bir walk-forward bölümünden sorunsuzca geçer ve aşırı uyumlu bir zirve yerine geniş bir plato gibi görünür — sızıntı katmanlar (fold) arasında tutarlıdır, dolayısıyla çapraz doğrulama onu yakalayamaz. Sızıntı, aşırı uyumun yukarısındaki bir başarısızlık modudur, ve aşağı akıştaki hiçbir miktarda dürüst doğrulama sizi kurtaramaz.
- Parametre aramasıyla etkileşime girer: sızıntılı veri üzerinde binlerce deneme yapan bir arama, sızıntıyı en agresif şekilde istismar eden konfigürasyonu bulur. "Kazanan", en büyük suçludur.
- Backtest-canlı eşitliğinin neden ayrıştığının nedeni budur. Bir sızıntı, backtest ile bot arasındaki %30–50'lik bir farkın en temiz açıklamasıdır, çünkü canlı işlem, mekanik olarak, gizlice bakamayacağınız tek yerdir.
Bunların hepsini yakalayan disiplin, akademik literatürün yıllardır ısrar ettiğiyle aynıdır: bir backtesti, katı bir bilgi sınırına sahip istatistiksel bir deney olarak ele alın. Bailey, Borwein, López de Prado & Zhu, aşırı uyumun sahte performansı ne kadar kolay ürettiğini gösterdi (2014); Arnott, Harvey & Markowitz'in backtesting protokolü (2019) bu hijyeni kurallara bağlar. Look-ahead bias, hepsinin en temel sınırıdır — zamandaki sınır — ve kazara ihlal edilmesi en ucuz olanıdır.
Çıkarımlar

- Look-ahead bias nicel olarak devasa, nitel olarak görünmezdir. Tek bir bar'lık execution hatası, −0.74'lük bir Sharpe'ı (saf gürültü, doğru şekilde kaybediyor) +14.79'a dönüştürdü. Hata bir satır; sonuç uydurma bir performans geçmişi.
- Bu bir gradyandır. Sinyal barının %25'ini bile yakalamak hiçlikten +3.90 üretir. Göze batan bir hataya ihtiyacınız yok — dolumlarınızdaki biraz fazla iyimserlik yeterlidir.
- Ölçülen sayı beceriyi sızıntıdan ayırt edemez. Gerçek bir avantaj var olduğunda, sızıntılar raporu işlem yapılabilir gerçeğin çok ötesine şişirir. Tek savunma metrik değil, süreçtir.
- Tek-bar kaydırma testi en hızlı tanı aracınızdır. Her dolumu bir bar sonraya taşıyın. Performans çökerse, geçmişte işlem yapıyordunuz demektir.
- Sızıntı büyüklüğü kanala özgüdür. Execution ve gösterge sızıntıları yıkıcıdır; bir işaret kuralının tüm-seri normalizasyonu neredeyse bedavadır. Sızıntıyı fiilen girdiği kanaldan ölçün — varsaymayın.
Tam kontrollü çalışma — üç sızıntının tamamı, doz taraması, yanlış-devreye-alma analizi, formal yöntemler ve tek bir deterministik betikten yeniden üretilebilir her sayı — lookahead.marketmaker.cc adresindeki eşlik eden makalede, kod ve veri ise github.com/suenot/lookahead-inflation adresinde.
Null deneyimizdeki strateji hiçbir avantaja sahip değildi. Yine de 15'lik bir Sharpe gösterdi. Backtestiniz fazla iyi görünüyorsa, şüphelenmeniz gereken ilk şey deha olmanız değildir — saatinizdir.
Yazarlar
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.