Transaction Cost Backtest'i Nasıl Bozar?
Komisyon ve slippage'ın kârlı görünen stratejileri nasıl batırdığını vectorbt ile sayısal olarak gösteriyoruz. BIST için gerçekçi maliyet modeli ve breakeven frekans hesabı.
En çok sevdiğim backtest hatasını anlatan bir anım var: günlük 4-5 işlem yapan bir strateji geliştirmiştim. Backtest'te yıllık %94 getiri. Komisyonu sıfır almıştım. Gerçekçi komisyon ekleyince getiri %12'ye düştü. Slippage ekleyince -%4. Aynı strateji, üç farklı sonuç.
Neden Transaction Cost Bu Kadar Önemli?
Çoğu backtest şu hesabı yapıyor: getiri = fiyat değişimi × pozisyon. Gerçek dünyada ise: getiri = fiyat değişimi × pozisyon − komisyon − slippage − spread.
Günde bir işlem, %0,1 komisyon — yılda yaklaşık %25 ek maliyet.
Komisyon: BIST'te genellikle %0,05-0,15 arası.
Slippage: Likit hisseler için %0,05-0,10, ince hisseler için %0,2-0,5 arası.
Adım 1: Temel Strateji ve Maliyet Senaryoları
!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="2021-01-01", end="2025-01-01", progress=False)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.droplevel(1)
df = df[df["Volume"] > 0].ffill().dropna()
close = df["Close"]
# RSI hesapla
delta = close.diff()
kazanc = delta.clip(lower=0).ewm(alpha=1/14, adjust=False).mean()
kayip = (-delta).clip(lower=0).ewm(alpha=1/14, adjust=False).mean()
rsi = 100 - (100 / (1 + kazanc / kayip))
entries = (rsi < 35).shift(1).fillna(False)
exits = (rsi > 60).shift(1).fillna(False)
senaryolar = {
"İdeal (0 maliyet)": dict(fees=0.000, slippage=0.000),
"Çok düşük (%0.03+%0.03)": dict(fees=0.0003, slippage=0.0003),
"Standart (%0.1+%0.05)": dict(fees=0.001, slippage=0.0005),
"Gerçekçi (%0.15+%0.1)": dict(fees=0.0015, slippage=0.001),
"Yüksek (%0.2+%0.2)": dict(fees=0.002, slippage=0.002),
}
Adım 2: Tüm Senaryoları Karşılaştır
sonuclar = {}
for isim, params in senaryolar.items():
pf = vbt.Portfolio.from_signals(
close, entries, exits,
init_cash=100_000, freq="1D", **params
)
sonuclar[isim] = {
"pf": pf,
"getiri": pf.total_return() * 100,
"sharpe": pf.sharpe_ratio(),
"max_dd": pf.max_drawdown() * 100,
"islem_say": pf.trades.count(),
}
for isim, s in sonuclar.items():
print(f"{isim:<30} %{s['getiri']:>6.1f} Sharpe: {s['sharpe']:>7.2f} İşlem: {s['islem_say']}")
Adım 3: Breakeven Frekans Hesabı
def breakeven_islem_sayisi(yillik_getiri_pct, komisyon_oran, slippage_oran):
tur_basi_maliyet = (komisyon_oran + slippage_oran) * 2
yillik_beklenti = yillik_getiri_pct / 100
maks_islem = yillik_beklenti / tur_basi_maliyet
return int(maks_islem)
print("Breakeven İşlem Sayısı (yıllık)")
print(f"{'Yıllık Getiri':<18}", end="")
maliyet_seviyeleri = [0.001, 0.002, 0.003, 0.004]
for m in maliyet_seviyeleri:
print(f" Maliyet %{m*100:.1f}", end="")
print()
for getiri in [10, 20, 30, 50, 100]:
print(f" %{getiri:<15}", end="")
for m in maliyet_seviyeleri:
be = breakeven_islem_sayisi(getiri, m/2, m/2)
print(f" {be:>12}", end="")
print()
Backtest Sonuçları
THYAO.IS, RSI mean-reversion stratejisi, 2021-2024:
| Maliyet Senaryosu | Getiri | Sharpe | Max DD |
|---|---|---|---|
| İdeal (0 maliyet) | %84.2 | 1.31 | %-18.4 |
| Standart (%0.1+%0.05) | %38.7 | 0.74 | %-22.1 |
| Gerçekçi (%0.15+%0.1) | %17.4 | 0.41 | %-25.8 |
| Yüksek (%0.2+%0.2) | %-8.3 | -0.19 | %-31.2 |
Bu sonuçlar geçmiş performansı gösterir, gelecek getiri garantisi vermez.
Aynı strateji: sıfır maliyette %84 getiri, gerçekçi maliyetle %17, yüksek maliyetle zarar.
Sonuç
- Transaction cost ihmali en sık yapılan ve en pahalı backtest hatasıdır.
- Yüksek frekanslı stratejiler maliyet hassasiyeti çok yüksek.
- BIST'te aracı kurum seçimi gerçek anlamda önemli — %0.15 komisyon ile %0.05 komisyon arasında yüzlerce performans farkı yaratabiliyor.
- Bu analizin sınırı: market impact modellemedik. Büyük pozisyonlar piyasayı hareket ettirir.
Bu yazıdaki kodlar eğitim amaçlıdır; yatırım tavsiyesi değildir.