Walk-Forward Optimizasyonu: Tek Dürüst Strateji Testi
Bir stratejiyi optimize ettiniz. 12 ayrım parametresi, 9 meta-parametre — toplamda 21. Tek bir parite üzerinde 25 aylık backtest, MaxLev'de PnL +%3342 gösteriyor. Öz sermaye eğrisi neredeyse hiç düşüş yaşamadan yükseliyor. Sharpe 3'ün üzerinde. Her şey mükemmel görünüyor.
Botu başlatıyorsunuz. İki hafta sonra strateji sermayenin %18'ini kaybediyor. Bir ay sonra — %34. Tarihsel verilerde "çalışan" parametreler, belirli bir piyasa olayları dizisine uyacak şekilde ayarlanmış çıktı. Bir kalıp bulmadınız — gürültüyü ezberlediniz.
Bu klasik aşırı uyum (overfitting). Ve bunu üretime geçmeden önce tespit etmenin tek sistematik yolu Walk-Forward Optimizasyonu'dur (WFO).
Tek Eğitim/Test Bölümü Tuzağı

Standart yaklaşım: veriyi %70 eğitim ve %30 test olarak bölmek. Eğitim üzerinde optimize etmek, test üzerinde doğrulamak. Sonuç olumlu ise — başlatmak.
Sorun şu: bu tek bir bölüm üzerindeki tek bir testtir. Sonuç, sınırı nereye çizdiğinize bağlıdır. Sınırı bir ay kaydırın — örnek dışı (out-of-sample) PnL +%40'tan -%15'e değişebilir.
Veri: |===== Eğitim (%70) =====|== Test (%30) ==|
Bölüm 1: |===2024-01..2025-09====|==2025-10..26-01==| → OOS PnL: +%38
Bölüm 2: |===2024-01..2025-06====|==2025-07..26-01==| → OOS PnL: -%12
Bölüm 3: |===2024-04..2025-12====|==2026-01..26-04==| → OOS PnL: +%7
Üç farklı bölüm — üç farklı sonuç. Hangisine güvenilir? Hiçbirine. Tek bir eğitim/test bölümü, Monte Carlo Bootstrap yazısında açıkladığımız problemlere sahip tek noktalı tahminin ta kendisidir. Bir kontrol değil, ardışık veri segmentleri üzerinde sistematik bir kontroller serisi gereklidir.
Walk-Forward Optimizasyonu tam olarak bunun için vardır.
Walk-Forward Optimizasyonu Nedir?

WFO, kayan (veya genişleyen) veri pencereleri üzerinde bir stratejinin sıralı olarak optimize edilmesi ve doğrulanması prosedürüdür. Fikir şudur: periyodik olarak mevcut veriler üzerinde parametreleri yeniden optimize ettiğiniz ve ardından bir sonraki yeniden optimizasyona kadar işlem yaptığınız gerçek işlem sürecini simüle etmek.
Her "pencere" iki bölümden oluşur:
- In-Sample (IS) — parametrelerin optimize edildiği dönem
- Out-of-Sample (OOS) — bulunan parametrelerin uyum olmaksızın test edildiği dönem
Temel özellik: OOS dönemleri çakışmaz ve toplu olarak verinin önemli bir bölümünü kapsar. Elde edilen öz sermaye eğrisi yalnızca OOS segmentlerinden oluşturulur — bu stratejinin dürüst değerlendirmesidir.
Sabit Başlangıçlı WFO (Genişleyen Pencere)

Sabit başlangıçlı WFO'da eğitim döneminin başlangıcı sabittir, sonu ise her pencereyle genişler:
Pencere 1: Eğitim [2024-01] → Test [2024-04]
Pencere 2: Eğitim [2024-01..04] → Test [2024-07] (büyüyen eğitim)
Pencere 3: Eğitim [2024-01..07] → Test [2024-10]
Pencere 4: Eğitim [2024-01..10] → Test [2025-01]
Pencere 5: Eğitim [2024-01..2025-01] → Test [2025-04]
Avantajları:
- Her sonraki eğitim dönemi daha fazla veri içerir — optimizasyon daha kararlıdır
- Erken kalıplar kaybolmaz — her zaman eğitim setindedir
- Uygulaması daha kolaydır
Dezavantajları:
- Eski veriler mevcut kalıpları "sulandırabilir"
- Piyasa yapısal olarak değişmişse — eski veriler zararlıdır
- Eğitim dönemi süresiz büyür, optimizasyon süresini artırır
Kayan Pencereli WFO (Sliding Window)
Kayan pencereli WFO'da sabit uzunluklu bir eğitim dönemi veri üzerinde "kayar":
Pencere 1: Eğitim [2024-01..06] → Test [2024-07..09]
Pencere 2: Eğitim [2024-04..09] → Test [2024-10..12]
Pencere 3: Eğitim [2024-07..12] → Test [2025-01..03]
Pencere 4: Eğitim [2024-10..2025-03] → Test [2025-04..06]
Pencere 5: Eğitim [2025-01..06] → Test [2025-07..09]
Avantajları:
- Mevcut piyasa rejimine uyum sağlar
- Sabit optimizasyon süresi
- Eski, alakasız veriler sonuçları etkilemez
Dezavantajları:
- Eğitim için daha az veri — optimal parametrelerin yüksek varyansı
- Pencere uzunluğu seçimine duyarlı
- Nadir ama önemli olayları "unutabilir" (ani çöküşler)
Birleşimsel Arındırılmış Çapraz Doğrulama (CPCV)

Marcos Lopez de Prado tarafından önerilen gelişmiş bir yöntem. Veri gruba bölünür, bunlardan tanesi test için seçilir. Standart çapraz doğrulamadan temel farkı arındırma (eğitim/test sınırındaki veriyi kaldırma) ve ambargo (veri sızıntısını önlemek için ek bir boşluk):
ile: 45 eğitim/test kombinasyonu. Her kombinasyon bir OOS sonucu üretir ve nihai tahmin tüm kombinasyonların ortalamasıdır.
from itertools import combinations
import numpy as np
def cpcv_splits(n_groups: int, k_test: int, purge_pct: float = 0.01):
"""
Arındırma ile CPCV bölümleri oluşturur.
Args:
n_groups: grup sayısı
k_test: her bölümdeki test grubu sayısı
purge_pct: arındırma için veri fraksiyonu (eğitim/test sınırında)
"""
groups = list(range(n_groups))
splits = []
for test_groups in combinations(groups, k_test):
train_groups = [g for g in groups if g not in test_groups]
splits.append({
"train": train_groups,
"test": list(test_groups),
"purge_groups": _get_purge_groups(train_groups, test_groups),
})
return splits
def _get_purge_groups(train, test):
"""Arındırma için eğitim/test sınırındaki gruplar."""
purge = set()
for t in test:
if t - 1 in train:
purge.add(t - 1)
if t + 1 in train:
purge.add(t + 1)
return list(purge)
CPCV, veri az olduğunda kayan WFO'dan daha iyidir, ancak hesaplama açısından daha maliyetlidir. 21 parametreli ve 25 aylık veriye sahip bir strateji için kayan WFO ile başlamanızı ve CPCV'yi ek bir kontrol olarak kullanmanızı öneririz.
Temel WFO Parametreleri

Eğitim Dönemi Uzunluğu
Çok kısa eğitim — güvenilir optimizasyon için yetersiz veri. Çok uzun — eski veriler mevcut kalıpları sulandırır.
Pratik kural: eğitim en az 200-300 işlem içermelidir. Strateji günde 2 işlem yapıyorsa:
Rejim geçişleri olan kripto için kayan pencerede en fazla 6-12 ay öneriyoruz.
Test Dönemi Uzunluğu
Test dönemi istatistiksel olarak anlamlı bir değerlendirme için yeterli olmalı, ancak çok uzun olmamalı — aksi takdirde parametrelerin bozulması için zaman kalır.
Kural: test = eğitimin %20-33'ü. Eğitim = 6 ay ise, test = 1,5-2 ay.
Örtüşme
Kayan WFO'da pencereler örtüşebilir. Örtüşme OOS veri noktalarının sayısını artırır ancak tahminler arasında korelasyon getirir:
Örtüşmesiz:
Eğitim [01..06] → Test [07..09]
Eğitim [07..12] → Test [01..03]
%50 örtüşme ile:
Eğitim [01..06] → Test [07..09]
Eğitim [04..09] → Test [10..12]
Eğitim [07..12] → Test [01..03]
Öneri: eğitim döneminde %50 örtüşme — pencere sayısı ve tahminlerin bağımsızlığı arasında iyi bir denge.
Yeniden Optimizasyon Sıklığı
Parametreleri ne sıklıkta yeniden hesapladığınızı belirler. Kripto piyasasında optimal sıklık her 1-3 aydır. Daha sık yeniden optimizasyon gürültüye aşırı uyum riskini artırır; daha az sık — parametre bayatlaması riskini artırır.
Walk-Forward Verimlilik Oranı ve Bozulma Hızı

Walk-Forward Verimlilik Oranı (WFER)
Temel WFO metriği — OOS getirilerinin IS getirilerine oranı:
Yorumlama:
| WFER | Yorum |
|---|---|
| > 0.8 | Mükemmel sağlamlık. Parametreler yeni veriye aktarılıyor. |
| 0.5 — 0.8 | Kabul edilebilir sağlamlık. Strateji çalışıyor ancak bozulmayla. |
| 0.3 — 0.5 | Sınır durum. Kısmi aşırı uyum olası. |
| < 0.3 | Aşırı uyum. Parametreler IS verisine uydurulmuş. |
| < 0 | Strateji OOS'ta karsız. Tam aşırı uyum veya mantık hatası. |
WFER < 0.5 ise — strateji büyük olasılıkla aşırı uyumlu. Bu bizim birincil filtremizdir.
Bozulma Hızı
Optimal parametrelerin zaman içinde ne kadar hızlı etkinliğini kaybettiğini gösterir:
Pratikte: test dönemini alt aralıklara bölün ve PnL dinamiklerini takip edin:
def degradation_rate(oos_returns: np.ndarray, n_subperiods: int = 4) -> float:
"""
Parametre bozulma hızını tahmin eder.
OOS dönemini alt aralıklara böler ve PnL'nin alt aralık numarasına göre
doğrusal regresyonunun eğimini hesaplar.
Döndürür:
eğim: negatif = bozulma, pozitif = iyileşme
"""
chunk_size = len(oos_returns) // n_subperiods
subperiod_pnls = []
for i in range(n_subperiods):
start = i * chunk_size
end = start + chunk_size
sub_pnl = np.sum(oos_returns[start:end])
subperiod_pnls.append(sub_pnl)
x = np.arange(n_subperiods)
slope = np.polyfit(x, subperiod_pnls, 1)[0]
return slope
Bozulma hızı güçlü biçimde negatifse — parametreler hızla bayatlar ve daha sık yeniden optimizasyon veya daha kısa eğitim dönemi gerekir.
Python'da Tam WFO Pipeline Uygulaması

import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from typing import Callable, List, Optional
import warnings
@dataclass
class WFOWindow:
"""Tek bir walk-forward penceresi."""
window_id: int
train_start: int # eğitim başlangıç indeksi
train_end: int # eğitim bitiş indeksi (dahil değil)
test_start: int # test başlangıç indeksi
test_end: int # test bitiş indeksi (dahil değil)
best_params: dict = field(default_factory=dict)
is_pnl: float = 0.0 # örnek içi PnL
oos_pnl: float = 0.0 # örnek dışı PnL
oos_returns: np.ndarray = field(default_factory=lambda: np.array([]))
wfer: float = 0.0 # walk-forward verimlilik oranı
@dataclass
class WFOResult:
"""Tüm WFO'nun sonucu."""
windows: List[WFOWindow]
aggregate_oos_pnl: float
aggregate_is_pnl: float
wfer: float
degradation_rate: float
oos_equity: np.ndarray
oos_sharpe: float
oos_max_dd: float
n_windows: int
passed: bool # stratejinin filtreyi geçip geçmediği
class WalkForwardOptimizer:
"""
Walk-Forward Optimizasyon pipeline'ı.
Sabit başlangıçlı (genişleyen) ve kayan (sliding) modları destekler.
"""
def __init__(
self,
data: np.ndarray,
optimize_fn: Callable,
evaluate_fn: Callable,
mode: str = "rolling", # "rolling" veya "anchored"
train_size: int = 180, # gün
test_size: int = 60, # gün
step_size: int = 60, # pencere adım büyüklüğü, gün
min_trades: int = 30, # OOS'ta min işlem sayısı
wfer_threshold: float = 0.5, # kabul/ret için WFER eşiği
):
self.data = data
self.optimize_fn = optimize_fn
self.evaluate_fn = evaluate_fn
self.mode = mode
self.train_size = train_size
self.test_size = test_size
self.step_size = step_size
self.min_trades = min_trades
self.wfer_threshold = wfer_threshold
def generate_windows(self) -> List[WFOWindow]:
"""Walk-forward pencerelerini oluşturur."""
n = len(self.data)
windows = []
window_id = 0
if self.mode == "rolling":
start = 0
while start + self.train_size + self.test_size <= n:
w = WFOWindow(
window_id=window_id,
train_start=start,
train_end=start + self.train_size,
test_start=start + self.train_size,
test_end=min(start + self.train_size + self.test_size, n),
)
windows.append(w)
start += self.step_size
window_id += 1
elif self.mode == "anchored":
train_end = self.train_size
while train_end + self.test_size <= n:
w = WFOWindow(
window_id=window_id,
train_start=0,
train_end=train_end,
test_start=train_end,
test_end=min(train_end + self.test_size, n),
)
windows.append(w)
train_end += self.step_size
window_id += 1
return windows
def run(self) -> WFOResult:
"""Tam WFO pipeline'ını çalıştırır."""
windows = self.generate_windows()
all_oos_returns = []
for w in windows:
train_data = self.data[w.train_start:w.train_end]
test_data = self.data[w.test_start:w.test_end]
best_params, is_pnl = self.optimize_fn(train_data)
w.best_params = best_params
w.is_pnl = is_pnl
oos_pnl, oos_returns = self.evaluate_fn(test_data, best_params)
w.oos_pnl = oos_pnl
w.oos_returns = oos_returns
if is_pnl != 0:
w.wfer = oos_pnl / is_pnl
else:
w.wfer = 0.0
all_oos_returns.extend(oos_returns)
all_oos = np.array(all_oos_returns)
oos_equity = np.cumprod(1 + all_oos)
peak = np.maximum.accumulate(oos_equity)
max_dd = ((oos_equity - peak) / peak).min()
aggregate_is = sum(w.is_pnl for w in windows)
aggregate_oos = sum(w.oos_pnl for w in windows)
wfer = aggregate_oos / aggregate_is if aggregate_is != 0 else 0
if np.std(all_oos) > 0:
oos_sharpe = np.mean(all_oos) / np.std(all_oos) * np.sqrt(252)
else:
oos_sharpe = 0
deg_rate = self._degradation_rate(windows)
passed = wfer >= self.wfer_threshold and aggregate_oos > 0
return WFOResult(
windows=windows,
aggregate_oos_pnl=aggregate_oos,
aggregate_is_pnl=aggregate_is,
wfer=wfer,
degradation_rate=deg_rate,
oos_equity=oos_equity,
oos_sharpe=oos_sharpe,
oos_max_dd=max_dd,
n_windows=len(windows),
passed=passed,
)
def _degradation_rate(self, windows: List[WFOWindow]) -> float:
"""Pencere numaralarına göre OOS PnL'nin eğimi."""
if len(windows) < 3:
return 0.0
pnls = [w.oos_pnl for w in windows]
x = np.arange(len(pnls))
slope = np.polyfit(x, pnls, 1)[0]
return slope
Kullanım Örneği
import numpy as np
np.random.seed(42)
prices = 100 * np.cumprod(1 + np.random.normal(0.0002, 0.02, 750))
def my_optimize(train_data):
"""
Eğitim verisi üzerinde stratejiyi optimize et.
(best_params, is_pnl) döndürür.
"""
best_pnl = -np.inf
best_params = {}
for fast in range(5, 30, 5):
for slow in range(20, 100, 10):
if fast >= slow:
continue
pnl, _ = _run_strategy(train_data, fast, slow)
if pnl > best_pnl:
best_pnl = pnl
best_params = {"fast": fast, "slow": slow}
return best_params, best_pnl
def my_evaluate(test_data, params):
"""
Sabit parametrelerle test verisi üzerinde stratejiyi değerlendir.
(oos_pnl, oos_returns) döndürür.
"""
pnl, returns = _run_strategy(test_data, params["fast"], params["slow"])
return pnl, returns
def _run_strategy(data, fast_period, slow_period):
"""Basit MA kesişim stratejisi."""
fast_ma = pd.Series(data).rolling(fast_period).mean().values
slow_ma = pd.Series(data).rolling(slow_period).mean().values
position = 0
returns = []
for i in range(slow_period, len(data) - 1):
if fast_ma[i] > slow_ma[i] and position <= 0:
position = 1
elif fast_ma[i] < slow_ma[i] and position >= 0:
position = -1
daily_ret = (data[i + 1] - data[i]) / data[i]
returns.append(position * daily_ret)
total_pnl = np.sum(returns)
return total_pnl, np.array(returns)
wfo = WalkForwardOptimizer(
data=prices,
optimize_fn=my_optimize,
evaluate_fn=my_evaluate,
mode="rolling",
train_size=180, # 6 ay
test_size=60, # 2 ay
step_size=60, # adım = test
)
result = wfo.run()
print(f"Pencereler: {result.n_windows}")
print(f"OOS PnL: {result.aggregate_oos_pnl:.4f}")
print(f"IS PnL: {result.aggregate_is_pnl:.4f}")
print(f"WFER: {result.wfer:.3f}")
print(f"OOS Sharpe: {result.oos_sharpe:.2f}")
print(f"OOS MaxDD: {result.oos_max_dd:.2%}")
print(f"Bozulma: {result.degradation_rate:.5f}")
print(f"Geçti: {result.passed}")
for w in result.windows:
print(f" Pencere {w.window_id}: IS={w.is_pnl:.4f} OOS={w.oos_pnl:.4f} "
f"WFER={w.wfer:.2f} params={w.best_params}")
Sonuçları Yorumlamak: Ne Zaman Güvenilir, Ne Zaman Reddedilir
Strateji WFO'yu Geçti
Tüm pencerelerde WFER >= 0.5, OOS PnL pozitif ve kararlıysa:
Pencere 0: IS=0.0812 OOS=0.0531 WFER=0.65 params={'fast': 10, 'slow': 50}
Pencere 1: IS=0.0744 OOS=0.0489 WFER=0.66 params={'fast': 10, 'slow': 50}
Pencere 2: IS=0.0698 OOS=0.0401 WFER=0.57 params={'fast': 15, 'slow': 50}
Pencere 3: IS=0.0823 OOS=0.0512 WFER=0.62 params={'fast': 10, 'slow': 60}
Pencere 4: IS=0.0756 OOS=0.0478 WFER=0.63 params={'fast': 10, 'slow': 50}
→ Toplam WFER: 0.63, tüm pencereler > 0.5, parametreler kararlı
İyi işaretler:
- WFER pencerelerde kararlı (keskin sıçramalar yok)
- Parametreler pencereler arasında benzer (fast = 10-15, slow = 50-60)
- Çoğu pencerede OOS PnL pozitif
- Bozulma hızı sıfıra yakın
Strateji WFO'yu Geçemedi
Pencere 0: IS=0.2341 OOS=-0.0312 WFER=-0.13 params={'fast': 5, 'slow': 95}
Pencere 1: IS=0.1987 OOS=0.0089 WFER=0.04 params={'fast': 25, 'slow': 30}
Pencere 2: IS=0.2156 OOS=-0.0567 WFER=-0.26 params={'fast': 10, 'slow': 90}
Pencere 3: IS=0.1834 OOS=0.0234 WFER=0.13 params={'fast': 20, 'slow': 40}
→ Toplam WFER: -0.07, IS yüksek, OOS sıfıra yakın → aşırı uyum
Aşırı uyum belirtileri:
- Yüksek IS PnL, düşük/negatif OOS PnL — klasik aşırı uyum
- Parametreler pencereler arasında önemli ölçüde değişiyor — kararlı bir optimum yok
- Çoğu pencerede WFER < 0.3 — parametreler aktarılmıyor
- Bozulma hızı güçlü biçimde negatif — hızlı bozulma
Parametre kararlılığı analizi hakkında daha fazla bilgi — Plato analizi makalesinde. Optimum "keskin" ise (küçük parametre değişiklikleriyle dik biçimde düşüyorsa) — bu ek bir aşırı uyum sinyalidir.
Kripto Para Birimleri için WFO Özellikleri

Kripto para birimleri, geleneksel piyasalarda olmayan benzersiz WFO sorunları yaratır.
Rejim Geçişleri
Kripto piyasası kökten farklı rejimler arasında geçiş yapar: boğa trendi, ayı trendi, yüksek/düşük volatiliteli yatay hareket. Bir rejimde optimal parametreler diğerinde karsız olabilir.
Çözüm: 4-6 aylık pencereyle kayan WFO (sabit başlangıçlı değil) kullanın. Bu eski rejimleri "unutmaya" izin verir. Ek olarak — volatiliteye göre veriyi kümeleyip her küme için WFO'yu ayrı çalıştırın.
Kısa Tarih
Çoğu altcoin'in 3 yıldan az işlem geçmişi vardır. Eğitim = 6 ay ve test = 2 ay ile yalnızca 4-5 pencere elde edersiniz — istatistiksel olarak zayıf bir tahmin.
Çözüm: kayan WFO yerine veya ek olarak CPCV kullanın. CPCV aynı veriden daha fazla kombinasyon üretir. 10 grup ve k=2 için: 4-5 pencere yerine 45 kombinasyon.
Yapısal Likidite Değişiklikleri
Kripto parite likiditesi durağan değildir: bir parite 6 ay likit olabilir, ardından hacimler 10 kat düşer. Likit bir piyasada optimize edilen parametreler likit olmayan birinde çalışmaz.
Çözüm: WFO pipeline'ına bir likidite filtresi ekleyin. Ortalama günlük hacmin eşiğin altında olduğu pencereleri dışarıda bırakın. Test dönemindeki likiditenin eğitim dönemiyle karşılaştırılabilir olduğunu doğrulayın.
Fonlama Oranı Etkisi
Kaldıraçlı vadeli işlem stratejileri için fonlama oranları OOS sonuçlarını temelden değiştirebilir. Bir strateji 2 ay boyunca OOS'ta +%5 gösterir, ancak 10x kaldıraçta fonlama %3,6 yer.
Fonlama etkisinin ayrıntılı analizi — Fonlama oranları kaldıracınızı öldürüyor makalemizde. WFO'da OOS PnL değerlendirirken fonlama maliyetlerini hesaba katmaya dikkat edin.
Çok Parametreli Stratejiler: 12+ Parametreyle WFO Neden Kritik

Tek bir pariteden 25 aylık veri üzerinde 21 parametreli (12 ayrım + 9 meta) bir strateji, devasa bir arama uzayına sahip bir modeldir.
Boyutluluk Laneti
Parametre kombinasyonlarının sayısı parametre sayısıyla üstel olarak büyür:
21 parametrenin her biri en az 10 değer alırsa:
Bayesian optimizasyonu ile bile (ayrıntılar Koordinat İnişi vs Bayesian makalesinde), uzayın ihmal edilebilir bir fraksiyonunu keşfedersiniz. Bulunan optimumun gerçek bir kalıp yerine gürültü eseri olma olasılığı parametre sayısıyla birlikte büyür.
Çoklu Karşılaştırmalar için Bonferroni Formülü
parametre kombinasyonu test ederseniz, yanlış "keşif" (şans eseri iyi bir sonuç bulma) olasılığı:
ve denenen kombinasyonda:
"Çalışan" parametreleri bulmak garantilidir — aslında gürültüye uydurulmuşlardır. WFO olmadan gerçek bir avantajı istatistiksel bir eserden ayırt etmenin yolu yoktur.
Kural: OOS Veri Noktaları vs Parametre Sayısı
WFO sonuçlarına güvenmek için pratik kural:
21 parametre için en az 210 OOS işlemi gerekir. WFO'nuz daha az üretiyorsa — sonuca güvenilemez.
+%3342 PnL@ML stratejisi: 21 parametre, 25 aylık veri. Diyelim ki 60 günlük 5 OOS penceresi, günde 2 işlem — toplam OOS işlemi. oranı — kabul edilebilir, ancak yalnızca WFER > 0.5 ise.
WFO'yu Optuna ile Entegre Etmek

Her WFO penceresinde parametreleri optimize etmeniz gerekir. 21 parametre için ızgara araması imkansız, koordinat inişi verimsizdir. Optimal seçim, Optuna aracılığıyla Bayesian optimizasyonudur.
import optuna
from optuna.samplers import TPESampler
def optuna_optimize(train_data: np.ndarray, n_trials: int = 500) -> tuple:
"""
Optuna kullanarak strateji parametrelerini optimize et.
Her WFO penceresi içinde kullanılır.
"""
def objective(trial):
fast = trial.suggest_int("fast_period", 3, 50)
slow = trial.suggest_int("slow_period", 20, 200)
atr_period = trial.suggest_int("atr_period", 5, 50)
atr_mult = trial.suggest_float("atr_multiplier", 0.5, 4.0)
rsi_period = trial.suggest_int("rsi_period", 5, 30)
rsi_upper = trial.suggest_int("rsi_upper", 60, 85)
rsi_lower = trial.suggest_int("rsi_lower", 15, 40)
vol_window = trial.suggest_int("vol_window", 10, 100)
position_size = trial.suggest_float("position_size", 0.1, 1.0)
take_profit = trial.suggest_float("take_profit", 0.005, 0.05)
stop_loss = trial.suggest_float("stop_loss", 0.003, 0.03)
trailing_pct = trial.suggest_float("trailing_pct", 0.002, 0.02)
if fast >= slow:
return -1e6 # geçersiz kombinasyon
params = {
"fast_period": fast, "slow_period": slow,
"atr_period": atr_period, "atr_multiplier": atr_mult,
"rsi_period": rsi_period, "rsi_upper": rsi_upper,
"rsi_lower": rsi_lower, "vol_window": vol_window,
"position_size": position_size,
"take_profit": take_profit, "stop_loss": stop_loss,
"trailing_pct": trailing_pct,
}
pnl, _ = run_strategy(train_data, params)
_, returns = run_strategy(train_data, params)
if len(returns) < 30 or np.std(returns) == 0:
return -1e6
sharpe = np.mean(returns) / np.std(returns) * np.sqrt(252)
return sharpe
optuna.logging.set_verbosity(optuna.logging.WARNING)
study = optuna.create_study(
direction="maximize",
sampler=TPESampler(seed=42),
)
study.optimize(objective, n_trials=n_trials, show_progress_bar=False)
best_params = study.best_params
best_pnl, _ = run_strategy(train_data, best_params)
return best_params, best_pnl
wfo = WalkForwardOptimizer(
data=prices,
optimize_fn=optuna_optimize, # ızgara araması yerine Optuna
evaluate_fn=my_evaluate,
mode="rolling",
train_size=180,
test_size=60,
step_size=60,
)
result = wfo.run()
Önemli: WFO içinde Sharpe'ı optimize edin, PnL'yi değil. PnL optimizasyonu belirli bir işlem dizisinde kârı maksimize eden parametreleri bulur. Sharpe optimizasyonu en iyi getiri/risk oranına sahip parametreleri bulur — bunlar OOS'ta daha sağlamdır.
Optuna TPE ile koordinat inişinin ayrıntılı karşılaştırması — Koordinat İnişi vs Bayesian makalesinde.
WFO Sonuçlarını Görselleştirme
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
def plot_wfo_results(result: WFOResult, data: np.ndarray):
"""Walk-Forward Optimizasyon sonuçlarını görselleştir."""
fig, axes = plt.subplots(3, 1, figsize=(16, 14))
ax = axes[0]
ax.plot(result.oos_equity, color='#4FC3F7', linewidth=1.5)
ax.axhline(1.0, color='#FF5252', linestyle='--', alpha=0.5, label='Başabaş')
ax.set_title(f'OOS Öz Sermaye Eğrisi (WFER={result.wfer:.2f}, Sharpe={result.oos_sharpe:.2f})')
ax.set_ylabel('Öz Sermaye')
ax.legend()
ax.grid(True, alpha=0.3)
ax = axes[1]
wfers = [w.wfer for w in result.windows]
colors = ['#69F0AE' if w >= 0.5 else '#FFAB40' if w >= 0.3 else '#FF5252'
for w in wfers]
ax.bar(range(len(wfers)), wfers, color=colors, edgecolor='#1A237E', alpha=0.8)
ax.axhline(0.5, color='#E040FB', linestyle='--', label='Eşik (0.5)')
ax.axhline(0, color='gray', linestyle='-', alpha=0.3)
ax.set_title('Pencere Başına Walk-Forward Verimlilik Oranı')
ax.set_xlabel('Pencere')
ax.set_ylabel('WFER')
ax.legend()
ax = axes[2]
x = np.arange(len(result.windows))
width = 0.35
ax.bar(x - width/2, [w.is_pnl for w in result.windows],
width, label='IS PnL', color='#7C4DFF', alpha=0.7)
ax.bar(x + width/2, [w.oos_pnl for w in result.windows],
width, label='OOS PnL', color='#4FC3F7', alpha=0.7)
ax.set_title('Örnek İçi vs Örnek Dışı PnL')
ax.set_xlabel('Pencere')
ax.set_ylabel('PnL')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('wfo_results.png', dpi=150)
plt.show()
Pratik Öneriler
Stratejiyi Üretime Almadan Önce Kontrol Listesi
1. WFO'yu çalıştırın (kayan + sabit başlangıçlı)
Her iki modun sonuçlarını karşılaştırın. Kayan WFO başarısız olurken sabit başlangıçlı geçiyorsa — strateji büyük olasılıkla yalnızca erken verilerde çalışıyor.
2. Her pencere için WFER'ı kontrol edin
Yalnızca toplam WFER değil, her pencereyi ayrı ayrı. 6 pencerenin 2'sinde WFER < 0 ise — bu bir sorundur, toplam > 0.5 olsa bile.
3. Pencereler arasında parametreleri karşılaştırın
Optimal parametreler pencereden pencereye "atlıyorsa" — kararlı bir avantaj yok. Optimum kararlılığını doğrulamak için Plato analizi kullanın.
4. Bozulma hızını kontrol edin
Güçlü biçimde negatif bozulma hızı = parametreler hızla etkinlik kaybediyor. Daha sık yeniden optimizasyon veya strateji revizyonu gerekir.
5. OOS sonuçlarına Monte Carlo bootstrap uygulayın
Toplam OOS PnL de tek noktalı bir tahmindir. Güven aralıkları elde etmek için OOS getiriler dizisine Monte Carlo bootstrap uygulayın.
6. Maliyetleri hesaba katın
OOS PnL komisyonları, kayma ve fonlama oranlarını içermelidir. Maliyetsiz güzel bir OOS PnL bir yanılsamadır. Daha fazla ayrıntı — Fonlama oranları kaldıracınızı öldürüyor.
Minimum Veri Gereksinimleri
| Parametre sayısı | Min OOS işlemleri | Min WFO pencereleri | Min veri (günde 2 işlem) |
|---|---|---|---|
| 2-5 | 50 | 3 | ~6 ay |
| 6-10 | 100 | 4 | ~12 ay |
| 11-15 | 150 | 5 | ~18 ay |
| 16-21 | 210 | 6 | ~24 ay |
| 22+ | 300+ | 8+ | ~36+ ay |
21 Parametreli ve 25 Aylık Veriye Sahip Strateji
Makalenin başındaki soruya dönelim: tek bir pariteden 25 aylık veri üzerinde optimize edilmiş 21 parametre. PnL@ML = +%3342. Nasıl doğrulanır?
Adım 1. Kayan WFO: eğitim = 8 ay, test = 2 ay, adım = 2 ay. ~8 pencere elde ederiz.
Adım 2. Sabit başlangıçlı WFO: ilk eğitim = 8 ay, test = 2 ay. ~8 pencere elde ederiz.
Adım 3. CPCV: ~2,5 aylık 10 grup, k = 2. 45 kombinasyon elde ederiz.
Adım 4. Her yöntem için doğrulayın:
- WFER >= 0.5?
- Parametreler pencereler arasında kararlı mı?
- Bozulma hızı kabul edilebilir mi?
- OOS işlemleri / Parametreler >= 10?
Adım 5. Toplam OOS getirileri üzerinde Monte Carlo bootstrap. 5. yüzdelik dilim PnL > 0?
Bu testlerden herhangi biri başarısız olursa — +%3342 stratejisi büyük olasılıkla aşırı uyumludur. Tek bir pariteden 25 ay üzerinde 21 parametre — bu son derece yüksek bir parametre/veri oranıdır. WFO'yu geçmeden güven olamaz.
Strateji verimliliğini aktif zamana göre PnL hesabıyla da kontrol etmenizi ek olarak öneririz — bu +%3342'nin ne kadarının pozisyonda geçirilen süreye, ne kadarının gerçek avantaja bağlı olduğunu ortaya çıkarır.
Sonuç
Walk-Forward Optimizasyonu isteğe bağlı değil — bir zorunluluktur. Parametrelerin yeni veriye aktarılabilirliğini sistematik olarak doğrulayan tek yöntemdir. Tek bir eğitim/test bölümü bir piyangodur. Tüm veriler üzerinde tam backtest kendini kandırmaktır.
Temel çıkarımlar:
-
WFER < 0.5 = aşırı uyum. Örnek dışı PnL örnek içinin yarısından azsa — parametreler uydurulmuştur.
-
Parametre kararlılığı maksimumdan daha önemlidir. Her pencerede +%15 veren parametreler, birinde +%40 diğerinde -%10 verenlerden daha iyidir.
-
Kripto için kayan WFO. Rejim geçişleri sabit başlangıçlı WFO'yu daha az güvenilir kılar. 4-6 aylık kayan pencere optimal dengedir.
-
Daha fazla parametre — daha sıkı gereksinimler. 21 parametre en az 210 OOS işlemi ve 6+ WFO penceresi gerektirir. Bunu olmadan sonuç doğrulanamaz.
-
WFO + Monte Carlo bootstrap + Plato analizi — aşırı uyuma karşı üç katmanlı koruma. Her katman diğerlerinin kaçırdığını yakalar.
Tüm pencereler boyunca WFER > 0.5, kararlı parametreler ve pozitif 5. yüzdelik dilim bootstrap ile WFO'yu geçen bir strateji — gerçek parayla güvenebileceğiniz bir stratejidir. Geri kalan her şey, güzel bir öz sermaye eğrisiyle eğri uydurma işlemidir.
Faydalı Bağlantılar
- Pardo, R. — The Evaluation and Optimization of Trading Strategies (Wiley)
- Lopez de Prado, M. — Advances in Financial Machine Learning, Chapter 12: Backtesting
- Bailey, D.H. et al. — The Probability of Backtest Overfitting
- Lopez de Prado, M. — The Combinatorial Purged Cross-Validation (CPCV)
- Aronson, D.R. — Evidence-Based Technical Analysis
- Optuna: A Next-generation Hyperparameter Optimization Framework
- Kevin Davey — Building Winning Algorithmic Trading Systems: Walk-Forward Analysis
- White, H. — A Reality Check for Data Snooping (2000)
- Harvey, C.R. & Liu, Y. — Backtesting (2015)
- NumPy — numpy.cumprod
Atıf
@article{soloviov2026walkforwardoptimization,
author = {Soloviov, Eugen},
title = {Walk-Forward Optimizasyonu: Tek Dürüst Strateji Testi},
year = {2026},
url = {https://marketmaker.cc/tr/blog/post/walk-forward-optimization},
version = {0.1.0},
description = {Tek bir eğitim/test bölümünün aşırı uyuma karşı neden koruma sağlamadığı, walk-forward optimizasyonunun parametre sağlamlığını nasıl sistematik olarak doğruladığı ve 21 parametreyle +\%3342 PnL@ML gösteren bir stratejinin WFO olmadan neden zamanlı bir bomba olduğu.}
}
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.