Walk-Forward Analizi Nedir? Out-of-Sample Test ile Strateji Doğrulama
In-sample optimizasyonun tuzağından kaçmanın tek güvenilir yolu walk-forward analizi. vectorbt ile BIST hisseleri üzerinde adım adım uyguluyoruz.
Bir stratejiyi geçmiş veriyle optimize ettikten sonra "bu işe yarıyor" diyebilmek için o veriyi dışarıda bırakmış olmak gerekir. Kulağa paradoks gibi geliyor ama backtest'in özü bu. Walk-forward analizi bu paradoksu çözen yöntem.
Walk-Forward Mantığı
Klasik backtest şöyle çalışır: tüm veriyi al, parametreleri optimize et, aynı veri üzerinde test et. Sonuç iyi çıkar çünkü "cevap anahtarına" bakarak sınav verdiniz.
Walk-forward analizi bunu değiştirir: veriyi pencereler halinde böler. Her pencere için in-sample dönemde optimize et, ardından hiç görmediğin out-of-sample dönemde test et.
Pencere 1: [IN-SAMPLE 12 ay] → [OUT-OF-SAMPLE 3 ay]
Pencere 2: [IN-SAMPLE 12 ay] → [OUT-OF-SAMPLE 3 ay]
Pencere 3: [IN-SAMPLE 12 ay] → [OUT-OF-SAMPLE 3 ay]
Adım 1: Kurulum ve Veri Hazırlığı
!pip install vectorbt yfinance --quiet
import vectorbt as vbt
import yfinance as yf
import pandas as pd
import numpy as np
df = yf.download("GARAN.IS", start="2020-01-01", end="2025-01-01", progress=False)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.droplevel(1)
close = df["Close"].dropna()
print(f"Toplam veri: {len(close)} gün ({close.index[0].date()} → {close.index[-1].date()})")
Adım 2: Veriyi Walk-Forward Pencerelerine Bölme
n_pencere = 8
pencere_uzun = 365
oos_uzunluk = 90
(is_fiyat, is_idx), (oos_fiyat, oos_idx) = close.vbt.rolling_split(
n=n_pencere,
window_len=pencere_uzun,
set_lens=(oos_uzunluk,),
left_to_right=False
)
print(f"Her pencerede:")
print(f" In-sample : {pencere_uzun - oos_uzunluk} gün")
print(f" Out-of-sample: {oos_uzunluk} gün")
Adım 3: In-Sample Optimizasyon
windows = np.arange(5, 51, 5)
def en_iyi_parametreyi_bul(fiyat_serisi, windows):
fast_ma, slow_ma = vbt.MA.run_combs(
fiyat_serisi, windows, r=2,
short_names=["fast", "slow"]
)
entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)
pf = vbt.Portfolio.from_signals(
fiyat_serisi, entries, exits,
init_cash=100_000, fees=0.001, freq="1D"
)
sharpe = pf.sharpe_ratio()
en_iyi = sharpe.idxmax()
return en_iyi, sharpe.max()
en_iyi_params = []
is_sharpe_listesi = []
for i in range(n_pencere):
pencere_fiyat = is_fiyat.iloc[:, i]
params, sharpe = en_iyi_parametreyi_bul(pencere_fiyat, windows)
en_iyi_params.append(params)
is_sharpe_listesi.append(sharpe)
print(f"Pencere {i+1}: fast={params[0]}, slow={params[1]} | IS Sharpe={sharpe:.2f}")
Adım 4: Out-of-Sample Test
oos_sonuclar = []
for i in range(n_pencere):
fast_win = en_iyi_params[i][0]
slow_win = en_iyi_params[i][1]
oos_veri = oos_fiyat.iloc[:, i]
fast_ma = vbt.MA.run(oos_veri, fast_win)
slow_ma = vbt.MA.run(oos_veri, slow_win)
entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)
pf = vbt.Portfolio.from_signals(
oos_veri, entries, exits,
init_cash=100_000, fees=0.001, freq="1D"
)
oos_getiri = pf.total_return() * 100
oos_sharpe = pf.sharpe_ratio()
oos_sonuclar.append({"getiri": oos_getiri, "sharpe": oos_sharpe})
print(f"Pencere {i+1}: IS Sharpe={is_sharpe_listesi[i]:.2f} → OOS Sharpe={oos_sharpe:.2f}")
Backtest Sonuçları
GARAN.IS, 2020-2024, 8 pencere, 12 ay IS + 3 ay OOS, %0,1 komisyon dahil:
| Metrik | In-Sample | Out-of-Sample |
|---|---|---|
| Ortalama Sharpe | 1.42 | 0.71 |
| Pozitif Sharpe Penceresi | 8/8 | 5/8 |
| Ortalama OOS Getiri | — | %6.8 (3 aylık) |
Bu sonuçlar geçmiş performansı gösterir, gelecek getiri garantisi vermez.
Sonuç
- Walk-forward analizi backtest güvenilirliğinin en önemli testi.
- IS-OOS Sharpe oranı arasındaki açılma tolere edilebilir bir düşüş mü yoksa tam çöküş mü? Genel kural: OOS Sharpe, IS Sharpe'ın en az %50'si olmalı.
- BIST'te rejim değişimleri hızlı gerçekleşiyor. 6 aylık IS + 2 aylık OOS daha dinamik sonuç verebilir.
Bu yazıdaki kodlar eğitim amaçlıdır; yatırım tavsiyesi değildir.