Backtest Yaparken Yapılan 5 Büyük Hata
Lookahead bias, overfitting, survivorship bias, transaction cost ihmali ve veri kalitesi sorunları — backtest'i gerçeklikten koparan beş hatayı vectorbt örnekleriyle gösteriyoruz.
İlk backtest sonuçlarıma baktığımda Sharpe 2.4, max drawdown %-8, yıllık getiri %180 çıkmıştı. "Buldum" dedim. Gerçek hesapta ilk ay %-12. O deneyim bana şunu öğretti: iyi bir backtest sonucu, iyi bir stratejinin değil, iyi kurulmuş bir yanılsamanın göstergesi olabilir.
Neden Backtest'ler Yalan Söyler?
Backtest, geçmiş veriyle kurulan bir simülasyon. Gerçek piyasada karşılaşacağınız her şeyi yansıtmak zorunda değil. Çoğu hata teknik değil kavramsal — yanlış varsayım, yanlış veri, yanlış soru.
Hata 1: Lookahead Bias — Geleceği Görür Gibi Test Etmek
Lookahead bias, bir sinyalin hesaplanmasında o anın ilerisindeki veriyi kullanmaktır. Örneğin bugünkü kapanışı kullanarak bugün için al sinyali üretip işlemi de bugünün açılışında yapmak.
!pip install vectorbt yfinance --quiet
import vectorbt as vbt
import yfinance as yf
import pandas as pd
import numpy as np
df = yf.download("THYAO.IS", start="2022-01-01", end="2025-01-01", progress=False)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.droplevel(1)
close = df["Close"]
# YANLIŞ: shift(0) — bugünkü sinyali bugün kullan
ma_yanlis = close.rolling(20).mean()
giris_yanlis = close > ma_yanlis
# DOĞRU: shift(1) — dünkü sinyali bugün kullan
giris_dogru = (close > ma_yanlis).shift(1).fillna(False)
pf_yanlis = vbt.Portfolio.from_signals(
close, giris_yanlis, ~giris_yanlis,
init_cash=100_000, fees=0.001
)
pf_dogru = vbt.Portfolio.from_signals(
close, giris_dogru, ~giris_dogru,
init_cash=100_000, fees=0.001
)
print(f"Lookahead BİAS'LI getiri: %{pf_yanlis.total_return()*100:.1f}")
print(f"Lookahead DÜZELTİLMİŞ getiri: %{pf_dogru.total_return()*100:.1f}")
Hata 2: Overfitting — Geçmişe Mükemmel Uyan Strateji
Parametreleri geçmiş veriye göre optimize ettiğinizde strateji o veriye "ezberler" — yeni veriye genelleşmez.
windows = np.arange(10, 61, 5)
fast_ma, slow_ma = vbt.MA.run_combs(close, windows, r=2,
short_names=["fast", "slow"])
entries_grid = fast_ma.ma_crossed_above(slow_ma)
exits_grid = fast_ma.ma_crossed_below(slow_ma)
pf_grid = vbt.Portfolio.from_signals(
close, entries_grid, exits_grid,
init_cash=100_000, fees=0.001, freq="1D"
)
sharpe_tum = pf_grid.sharpe_ratio()
en_iyi_idx = sharpe_tum.idxmax()
print(f"En iyi parametreler: {en_iyi_idx}")
print(f"In-sample Sharpe: {sharpe_tum.max():.2f}")
print(f"Toplam test edilen: {len(sharpe_tum)} kombinasyon")
Hata 3: Survivorship Bias — Hayatta Kalanları Seçmek
yfinance'den bugün BIST-100 listesini alıp tüm tarihsel veriyi test ederseniz, tarihsel dönemde listede olmayan hisseler dahil olur.
bist_simdi = ["THYAO.IS", "GARAN.IS", "ASELS.IS", "EREGL.IS",
"SISE.IS", "KCHOL.IS", "SAHOL.IS", "TUPRS.IS"]
toplam_getiri_bias = []
for sembol in bist_simdi:
try:
fiyat = yf.download(sembol, start="2018-01-01",
end="2025-01-01", progress=False)["Close"]
if isinstance(fiyat, pd.DataFrame):
fiyat = fiyat.iloc[:, 0]
fiyat = fiyat.dropna()
getiri = (fiyat.iloc[-1] / fiyat.iloc[0] - 1) * 100
toplam_getiri_bias.append(getiri)
except:
pass
print(f"Ortalama getiri (survivor bias): %{np.mean(toplam_getiri_bias):.1f}")
Hata 4: Transaction Cost İhmali
Komisyon ve slippage'ı sıfır kabul etmek, özellikle çok işlem yapan stratejilerde sonuçları dramatik şekilde çarpıtır.
ma_fast = close.ewm(span=10, adjust=False).mean()
ma_slow = close.ewm(span=30, adjust=False).mean()
entries = (ma_fast > ma_slow).shift(1).fillna(False)
exits = (ma_fast < ma_slow).shift(1).fillna(False)
maliyet_senaryolari = {
"Sıfır maliyet": dict(fees=0.000, slippage=0.000),
"Düşük (%0.05+%0.05)": dict(fees=0.0005, slippage=0.0005),
"Gerçekçi (%0.1+%0.1)": dict(fees=0.001, slippage=0.001),
"Yüksek (%0.2+%0.2)": dict(fees=0.002, slippage=0.002),
}
for isim, params in maliyet_senaryolari.items():
pf = vbt.Portfolio.from_signals(
close, entries, exits,
init_cash=100_000, freq="1D", **params
)
print(f"{isim:<30} %{pf.total_return()*100:>8.1f} Sharpe: {pf.sharpe_ratio():>7.2f}")
Hata 5: Veri Kalitesi — Temizlenmemiş Veri
Temettü, bölünme, sermaye artırımı düzeltmesi yapılmamış fiyatlar strateji sinyallerini bozar.
duzeltilmis = yf.download("EREGL.IS", start="2020-01-01",
end="2025-01-01", auto_adjust=True,
progress=False)["Close"]
duzeltilmemis = yf.download("EREGL.IS", start="2020-01-01",
end="2025-01-01", auto_adjust=False,
progress=False)["Close"]
if isinstance(duzeltilmis, pd.DataFrame):
duzeltilmis = duzeltilmis.iloc[:, 0]
if isinstance(duzeltilmemis, pd.DataFrame):
duzeltilmemis = duzeltilmemis.iloc[:, 0]
getiri_d = (duzeltilmis.iloc[-1] / duzeltilmis.iloc[0] - 1) * 100
getiri_nd = (duzeltilmemis.iloc[-1] / duzeltilmemis.iloc[0] - 1) * 100
print(f"Düzeltilmiş fiyat getirisi : %{getiri_d:.1f}")
print(f"Düzeltilmemiş fiyat getirisi: %{getiri_nd:.1f}")
print(f"Fark : %{getiri_d - getiri_nd:.1f}")
Sonuç
- Bu beş hatanın hepsi aynı anda yapılabilir ve sonuçlar yine de güzel görünebilir.
- En sık görülen ve en kolay önleneni lookahead bias — her sinyale
.shift(1)uygulamak yeterli. - Transaction cost ihmali "kağıt üzerinde iyi" stratejilerin en büyük katili.
- Bir sonraki adım: bu hataların farkında olarak kurulmuş bir backtest çerçevesini walk-forward analizi ile güçlendirmek.
Bu yazıdaki kodlar eğitim amaçlıdır; yatırım tavsiyesi değildir.