Skip to content
Araştırma
11 dk okuma

Bir backtest'in kopya çekmediğini kanıtlamak

İleriye bakma yanlılığı grafikte kendini belli etmez ve asıl tehlikeli olan türü sonucu iki puandan az şişirir. Yine de onu yakalayan beş satırlık bir test var, testin kendisinin çalıştığını doğrulamanın da bir yolu.

Hisse evreni
En likit 1,500 ABD hissesi, o günkü listeye göre
Test dönemi
2024–2026

İleriye bakma yanlılığı kendini hiçbir şekilde belli etmez ve asıl sorun da bu. Yarının bilgisini sessizce kullanan bir backtest de kullanmayan da birbirine benzeyen getiri eğrileri çizer: eğri yükselir, arada zirveden düşüşler yaşanır, işlem sayısı makul görünür. Grafiğe bakarak fark edemezsiniz, hataya yol açan kod satırı da çoğu zaman gayet mantıklı durur.

Bilinen önlemler sanıldığı kadar güçlü değil. Kodu okuyarak ancak zaten şüphelendiğiniz hatayı bulursunuz, aklınıza gelmeyeni kaçırırsınız. Yüksek bir Sharpe en fazla bir işarettir, çünkü sızıntısız pek çok strateji de yüksek Sharpe verir, sızıntılı pek çoğu da vermez. "Kullandığım altyapı buna zaten izin vermiyor" demek ise altyapı hakkında bir şey söyler, sizin onun üstüne kurduğunuz strateji hakkında değil.

Oysa bunu doğrudan test etmenin bir yolu var. Bu yazıda o testi, testi uyguladığım stratejiyi ve testin gerçekten çalıştığından emin olmak için aynı stratejiyi bilerek bozduğumda neler olduğunu anlatıyorum.

İleriye bakma yanlılığı nedir

Backtest kararları sırayla simüle eder: her gün, o gün bilinebilecek bilgiyle karar verip bir sonraki güne geçer. Sonradan ortaya çıkan bir bilginin daha önceki bir karara karıştığı her durum ileriye bakma yanlılığıdır.

En bariz örneğinden kaçınmak kolay: sinyali günün kapanış fiyatından hesaplayıp aynı günün açılışında alım yapmak. Çoğu backtest motoru bunu zaten zorlaştırıyor.

Tehlikeli olanı ise fiyatlara hiç dokunmuyor. Simülasyon başlamadan önce cevapladığınız soruların içine gizleniyor:

  • Hangi hisselerde işlem yapabilirim? Bu liste dönem sonundaki veriye bakılarak çıkarıldıysa simülasyondaki bütün kararlar geleceğe göre verilmiş olur.
  • Hangi hisseler yeterince likit? Aynı sorun, bir alt seviyede.
  • Hangi şirketler hâlâ ayakta? Bugün derlenmiş bir fiyat dosyasında yalnızca ayakta kalanlar vardır. Bu dosyayla 2020'ye dönüp işlem yaparsanız sıfırlanan hiçbir hisseyi portföye almamış olursunuz.

Bunlar sinyal hatası değil, kurulum kararları. Bir kez veriliyorlar, kodun stratejiden ayrı bir yerinde duruyorlar ve çoğu zaman aylar önce yazılmış oluyorlar. Stratejiyi ne kadar incelerseniz inceleyin onları bulamazsınız, çünkü hata stratejinin içinde değil.

Test edilen strateji

Testi gerçek bir strateji üzerinde çalıştırmak gerekiyor, yoksa sonuç gerçek stratejiler hakkında hiçbir şey kanıtlamaz. Yazının geri kalanındaki bütün koşular aynı stratejiyle yapıldı.

Kesitsel momentum. Hisseler son dönemde ne kadar yükseldiklerine göre sıralanıyor, en güçlüler portföye alınıyor ve sıralama belirli aralıklarla yenileniyor. Arkasındaki varsayım, göreli gücün birkaç ay boyunca sürdüğü.

Her yeniden dengeleme gününde sırasıyla:

  1. Hisse evreni belirleniyor. ABD'nin en likit 1,500 hissesi, ama bugüne göre değil, karar gününe göre kurulmuş hâliyle. Sonradan borsadan çıkan şirketler, işlem görmeyi bıraktıkları güne kadar evrende kalıyor.
  2. Her hisseye puan veriliyor: son altı ayın getirisi, en yakın ay hariç. Aradaki bu boşluğa atlama deniyor. Yeni fırlamış hisseler kazancın bir kısmını geri verme eğiliminde olduğu için son ay, sinyalin en az güvenilir kısmı.
  3. En güçlü 20 hisse portföye alınıyor ve oynaklığın tersiyle ağırlıklandırılıyor. İki kat oynak bir hisse yarı ağırlık alıyor.
  4. Satışta tampon bırakılıyor. Bir hisse ilk 20'den düştüğünde satılmıyor, ilk 40'ın dışına çıktığında satılıyor. Bu tampon olmasa sınırda gidip gelen bir hisse her ay alınıp satılır ve her seferinde alış satış makası ödenir.
  5. Portföy ayda bir yeniden dengeleniyor. Emirler ertesi günün açılışında gerçekleşiyor ve her işleme %0.25 maliyet yazılıyor.

Testin kapsadığı dönem 2024-01-02 ile 2026-07-31 arası, orta nokta ise 2025-06-30.

Bunların çoğu test açısından pek önemli değil. Önemli olan 1. adım, çünkü sızıntıyı oraya yerleştireceğim.

Hisse evreni nasıl seçiliyor, sızıntı nereden giriyor

"ABD'nin en likit 1,500 hissesi" sabit bir liste değil. Likiditeyi ölçmek gerekiyor, ölçmek için de bir zaman aralığı seçmek gerekiyor.

Doğru kurulmuş sürümde bu aralık testin başında sabitleniyor. Hangi hisselerin evrene gireceğine yalnızca o gün elde olan veriyle karar veriliyor ve sonradan ne kadar veri yüklenirse yüklensin aynı 1,500 hisse kullanılıyor.

Bozuk sürümde ise aralık, o an hangi veri yüklüyse ona göre kayıyor. Fiyatları 2026'ya kadar yüklerseniz likidite sıralaması da 2026'ya kadarki veriyle yapılıyor ve portföy, sonradan likit olduğu anlaşılan hisselerle işlem yapıyor.

Kısacası yapılan şu: sonradan çok işlem gördüğü ortaya çıkan hisseleri seç, sonra 2024'e dönüp bu hisselerle işlem yap. Bütün fark tek bir parametrede. Kodda son derece makul görünüyor ama kesitsel hisse çalışmalarında en sık rastlanan ileriye bakma hatası da bu.

Kesme testi

Backtest dönemi içinden bir tarih seçin, buna orta nokta diyelim.

  1. Fiyatları yalnızca orta noktaya kadar yükleyip stratejiyi başlangıçtan orta noktaya kadar çalıştırın.
  2. Bütün fiyatları yükleyip aynı stratejiyi aynı başlangıçtan verinin sonuna kadar çalıştırın.
  3. İki getiri eğrisini ortak oldukları günler üzerinden karşılaştırın.

İlk koşu orta noktadan sonraki hiçbir veriyi kullanamaz, çünkü o veri süreçte hiç yok. İkinci koşuda ise verinin tamamı var. Orta noktadan önceki herhangi bir karar geleceğe dokunduysa, iki eğri ortak dönemin bir yerinde ayrışır.

Kabaca şöyle, bütün test bundan ibaret:

MID = "2025-06-30"

# 1. a world that ends at the midpoint
prices_a = load_prices(start=START, end=MID)
curve_a  = backtest(strategy, prices_a, start=START, end=MID)

# 2. a world that knows everything
prices_b = load_prices(start=START, end=TODAY)
curve_b  = backtest(strategy, prices_b, start=START, end=TODAY)

# 3. the days they share must agree
shared = sorted(set(curve_a) & set(curve_b))
diffs  = [d for d in shared if abs(curve_a[d] - curve_b[d]) > TOL]

assert not diffs, f"first divergence at {diffs[0]}"

Testin kendisi son satırdaki assert. Hatanın ne olduğunu bilmenize, hatta bir hata olduğundan şüphelenmenize bile gerek yok. Zaten amaç da bu: kod incelemesinden sağ çıkan hatalar, kimsenin aramayı akıl etmediği hatalardır.

Temiz sürümün sonucu

İki koşunun ortak 374 işlem günü var ve bunların 4 tanesinde portföy değerleri bit düzeyinde birebir aynı değil.

Bunu başarısızlık sanmadan önce o dört farkın büyüklüğüne bakalım: yaklaşık $36,598 değerindeki bir portföyde her biri doların milyonda biri kadar, yani milyarda üç. Fark, kayan noktalı hesapların farklı sırayla toplanmasından geliyor, zamanda geriye giden bir bilgi yok. Geri kalan bütün günler birebir tutuyor ve ortak son günde iki koşu da kuruşu kuruşuna aynı değeri veriyor: $36,598.

Hisse evrenleri de aynı: 1500 hisse içinde farklı olan hisse sayısı 0.

Yani strateji testi geçiyor. Geçmişteki kararları geleceğin verisine bağlı değil.

Başarısız olamayan bir test işe yaramaz

Bu sonuç tek başına pek bir şey ifade etmiyor. Kesme testi ne olursa olsun "geçti" diyecek olsaydı, bana strateji hakkında hiçbir şey öğretmemiş, iyi habere ne kadar kolay inandığımı ise epey iyi göstermiş olurdu.

Ben de aynı stratejiyi yukarıda anlattığım şekilde bozdum ve başka hiçbir şeyi değiştirmeden aynı testi tekrar çalıştırdım.

Test sızıntıyı 2024-01-02 tarihinde, yani ilk günde yakalıyor ve iki eğri bir daha birleşmiyor. Aynı backtest aynı tarihler için iki kez çalıştırıldığında, ortak son günde koşulardan biri $37,008, diğeri $46,667 gösteriyor. Strateji de dönem de aynı ama aradaki fark $9,659. Değişen tek şey, diskte geleceğe ait ne kadar veri durduğu.

Frozen, data to midpoint1.48×Frozen, all data1.48×Leaky, data to midpoint1.49×Leaky, all data1.89×
1×1.2×1.4×1.6×1.8×20242025
Fig. 1Aynı 374 gün, her sürüm iki kez koşturuldu. Grafikte yalnızca üç çizgi görünüyor, çünkü seçimin sabit olduğu iki koşu birebir üst üste biniyor. Sızıntılı iki koşu ise birbirinden ayrışıyor.
Sabit seçim0.00%Sızıntılı seçim26.10%
0.0%10.0%20.0%20242025
Fig. 2Aynı karşılaştırma, her gün için tek bir sayıyla: gelecek geri eklendiğinde her backtest'in kendi geçmişi ne kadar değişiyor. Sıfır, iki koşunun aynı sonucu verdiği anlamına geliyor.

Orta noktaya gelindiğinde, gelecek veri de yüklenince sızıntılı backtest'in kendi geçmişi %26.1 daha yüksek çıkıyor. Arada fark en fazla %28.62 seviyesine kadar açılmış.

Evrene hangi hisseler giriyor

Mekanizma en net hisse evreninde görülüyor. Seçim sızdırdığında iki koşunun evreni arasında 220 hisselik fark oluyor, kabaca her yedi hisseden biri.

Bu hisseler rastgele değil. Yalnızca gelecek görünürken evrene girenler arasında AAOI, ACHR, ADT, ALM gibi isimler var. İşlem hacmi 2025 ve 2026'da patlayan şirketler bunlar. Ocak 2024'te ABD'nin en likit 1,500 hissesi arasında değillerdi ve o tarihte çalışan bir portföyün bunları elinde tutması mümkün değildi.

Evrenden çıkardıkları hisselerde ise dikkat çeken bir şey yok: ACAD, ADNT, AGL, AL. Bunlar 2024'te likit olan, sonra yavaş yavaş en likit 1,500 hissenin dışına kayan sıradan orta ölçekli şirketler.

Yani sızıntı küçük bir hesaplama kusuru değil. Portföy, fırsat kümesinin yedide birini sessizce, yakında gündeme oturacak hisselerle değiştirmiş.

Asıl endişe verici kısım

Şimdi bütün bunların manşet rakamlara etkisine bakalım.

EğitimTest
CAGRMaks. düşüşMARİşlem sayısıCAGRMaks. düşüşMARİşlem sayısı
Frozenseçim başta sabitlendi29.9%-35.3%0.8524844.1%-35.3%1.25410
Leakyseçim veriyle birlikte kayıyor30.9%-36.7%0.8422045.8%-38.9%1.18354

Bütün veriyle sonuna kadar çalıştırıldığında sızıntılı sürüm yıllık %45.78 getiri gösteriyor, temiz sürüm ise %44.13. Aradaki fark bir buçuk puan.

Bu sonucu inceleyen hiç kimse şüphelenmez. Ortada şüphe uyandıracak kadar pürüzsüz bir getiri eğrisi ya da imkânsız bir Sharpe yok. Zirveden düşüşü biraz daha derin, MAR'ı biraz daha düşük. Biraz farklı bir fikir test edilmiş gibi duruyor ve onu tehlikeli yapan da bu. Bir backtest'i %200 şişiren bir yanlılık hemen göze batar. Bu ise batmıyor ve son tabloya ne kadar bakarsanız bakın onu gerçek bir iyileşmeden ayırt edemezsiniz.

Kesme testi ise ikisini tek koşuda, neye bakacağı söylenmeden ayırıyor.

Sızıntılı sürümün sorununu daha kısa da anlatabiliriz: geçmişi değişiyor. Bu backtest'i Haziran 2025'te çalıştırsaydınız ilk on sekiz ay için yıllık %30.89 getiri görürdünüz. Aynı kodu hiç değiştirmeden bugün çalıştırdığınızda aynı on sekiz ay %26.1 daha fazla kazandırıyor. Geçmiş sonuçların değişmemesi gerekir. Değişiyorsa backtest görmemesi gereken bir veriye bakıyor demektir.

Testin yakalayamadıkları

Kesme testi bir denetim değil, tek bir test ve yalnızca bir şeyi kontrol ediyor: orta noktadan önceki kararların sonraki veriye bağlı olup olmadığını. Aşağıdaki üç hata testten rahatlıkla geçer.

  1. Verinin içine yerleşmiş yanlılık. Fiyat dosyasında borsadan çıkan şirketler zaten yoksa iki koşu da aynı şekilde yanlış olur ve birbirini kusursuz tutar. Kesme testi zamanda yolculuğu arar, kötü bir veri setini değil. Hayatta kalma yanlılığını ayrıca, batan şirketlerin izini sürerek kontrol etmek gerekir.
  2. Kısaltılmış dönemin içindeki ileriye bakma. İşlem yaptığı mumun kapanış fiyatını kullanan bir sinyal zaten hatalıdır, ama iki koşu da aynı şekilde hatalı olduğu için sonuçlar tutar. Orta noktayı kaydırmak farklı dönemleri yakalar, yine de emirlerin gerçekleşme zamanlamasını doğrudan test etmek daha iyi.
  3. İleriye bakma dışındaki her şey. Aşırı uyum, gerçekçi olmayan emir gerçekleşmeleri, fazla düşük tutulmuş işlem maliyetleri, o büyüklükte hiçbir zaman işlem yapamayacağınız bir hisse evreni. Bu test hiçbiri hakkında bilgi vermez.

Testi geçmek, "bu sınırın ötesinden hiçbir gelecek verisi sızmadı" demektir. Backtest'in doğru olduğu anlamına gelmez. İkisi ayrı şeyler ve ayrı tutulmaları gerekiyor.

Kendi backtest'lerinize uygulamak

Buradaki hiçbir şey belirli bir backtest motoruna bağlı değil. İki şeye ihtiyacınız var: motorun yüklediği veriyi sınırlayabilmek ve motordan gün gün portföy değeri serisi alabilmek.

Testi uygularken edindiğim üç pratik not var.

Orta noktayı stratejinin aktif olduğu bir döneme denk getirin. Portföyün boşta beklediği bir döneme düşen orta nokta hiçbir şeyi test etmez, çünkü sızıntının etkileyebileceği bir karar yoktur. Orta noktanın iki yanında da açık pozisyon olduğu sürece dönemin ortası makul bir seçim.

Veri büyükse iki yüklemeyi ayrı süreçlerde çalıştırın. İki tam fiyat seti belleğe birlikte sığmaz. O zaman test işe yarar bir sonuç vermek yerine süreç çöker.

Bir toleransla karşılaştırın, sonra toleransı aşan günlere bakın. Bit düzeyinde eşitlik aramak yanlış bir ölçüt. Onuncu ondalık basamaktaki farklar kayan nokta hesaplarının birikmesinden gelir, ilk günde ikinci ondalık basamakta çıkan bir fark ise hatadır. İkisi arasındaki mesafe o kadar büyük ki hangisine baktığınız konusunda kafa yormanız gerekmez.

Artık bir sonucu yayımlamadan önce bu testi her seferinde çalıştırıyorum. Ucuz ve mekanik bir kontrol ama bir backtest'e inanmakla onu gerçekten kontrol etmiş olmak arasındaki fark tam da bu.

Yazan

Koray GocmenKurucu

Buradaki araştırmaların arkasındaki sistematik stratejileri kuruyor ve çalıştırıyor.

Kaynak kodun tamamı

Bu yazının arkasındaki kod, veri ve parametre dosyaları.

Tüm araştırmalar