Walk-Forward Optimization: Satu-Satunya Ujian Strategi yang Jujur
Anda mengoptimumkan sebuah strategi. 12 parameter pemisahan, 9 meta-parameter — 21 kesemuanya. Backtest selama 25 bulan pada satu pasangan menunjukkan PnL +3342% pada MaxLev. Keluk ekuiti meningkat hampir tanpa drawdown. Sharpe melebihi 3. Segalanya kelihatan sempurna.
Anda melancarkan bot. Dua minggu kemudian, strategi itu kehilangan 18% modal. Sebulan kemudian — 34%. Parameter yang "berfungsi" pada data sejarah ternyata disesuaikan dengan urutan peristiwa pasaran tertentu. Anda tidak menemui corak — anda menghafal bunyi hingar.
Ini adalah overfitting klasik. Dan satu-satunya cara sistematik untuk mengesannya sebelum masuk ke pengeluaran adalah Walk-Forward Optimization (WFO).
Perangkap Pembahagian Train/Test Tunggal

Pendekatan standard: bahagikan data kepada 70% train dan 30% test. Optimumkan pada train, sahkan pada test. Jika hasilnya positif — lancarkan.
Masalahnya: ini adalah satu ujian pada satu pembahagian. Hasilnya bergantung pada tempat anda meletakkan sempadan. Alih sempadan sebulan — dan PnL out-of-sample boleh berubah daripada +40% kepada -15%.
Data: |===== Train (70%) =====|== Test (30%) ==|
Bahagian 1: |===2024-01..2025-09====|==2025-10..26-01==| → OOS PnL: +38%
Bahagian 2: |===2024-01..2025-06====|==2025-07..26-01==| → OOS PnL: -12%
Bahagian 3: |===2024-04..2025-12====|==2026-01..26-04==| → OOS PnL: +7%
Tiga pembahagian berbeza — tiga kesimpulan berbeza. Yang mana satu perlu dipercayai? Tiada satu pun. Satu pembahagian train/test tunggal adalah sama seperti anggaran titik tunggal yang masalahnya kami terangkan dalam Monte Carlo Bootstrap. Anda memerlukan bukan satu pemeriksaan, tetapi siri sistematik pemeriksaan pada segmen data berturut-turut.
Inilah sebabnya Walk-Forward Optimization wujud.
Apakah Walk-Forward Optimization

WFO adalah prosedur pengoptimuman berurutan dan pengesahan strategi pada tetingkap data yang meluncur (atau berkembang). Ideanya: mensimulasikan proses dagangan sebenar di mana anda secara berkala mengoptimumkan semula parameter pada data yang tersedia, kemudian berdagang sehingga pengoptimuman semula berikutnya.
Setiap "tetingkap" terdiri daripada dua bahagian:
- In-Sample (IS) — tempoh di mana parameter dioptimumkan
- Out-of-Sample (OOS) — tempoh di mana parameter yang ditemui diuji tanpa penyesuaian
Sifat utama: tempoh OOS tidak bertindih dan secara kolektif meliputi sebahagian besar data. Keluk ekuiti yang terhasil dibina hanya daripada segmen OOS — inilah penilaian jujur strategi.
Anchored WFO (Tetingkap Berkembang)

Dalam anchored WFO, permulaan tempoh train ditetapkan, dan penghujungnya berkembang dengan setiap tetingkap:
Tetingkap 1: Train [2024-01] → Test [2024-04]
Tetingkap 2: Train [2024-01..04] → Test [2024-07] (train berkembang)
Tetingkap 3: Train [2024-01..07] → Test [2024-10]
Tetingkap 4: Train [2024-01..10] → Test [2025-01]
Tetingkap 5: Train [2024-01..2025-01] → Test [2025-04]
Kelebihan:
- Setiap tempoh train berikutnya mengandungi lebih banyak data — pengoptimuman lebih stabil
- Corak awal tidak hilang — ia sentiasa berada dalam set latihan
- Lebih mudah dilaksanakan
Kelemahan:
- Data lama mungkin "mencairkan" corak semasa
- Jika pasaran telah berubah secara struktur — data lama adalah berbahaya
- Tempoh train berkembang tanpa had, meningkatkan masa pengoptimuman
Rolling WFO (Tetingkap Meluncur)
Dalam rolling WFO, tempoh train berpanjangan tetap "meluncur" merentasi data:
Tetingkap 1: Train [2024-01..06] → Test [2024-07..09]
Tetingkap 2: Train [2024-04..09] → Test [2024-10..12]
Tetingkap 3: Train [2024-07..12] → Test [2025-01..03]
Tetingkap 4: Train [2024-10..2025-03] → Test [2025-04..06]
Tetingkap 5: Train [2025-01..06] → Test [2025-07..09]
Kelebihan:
- Menyesuaikan diri dengan rejim pasaran semasa
- Masa pengoptimuman yang malar
- Data lama yang tidak relevan tidak mempengaruhi keputusan
Kelemahan:
- Kurang data untuk latihan — varians parameter optimum lebih tinggi
- Sensitif kepada pemilihan panjang tetingkap
- Mungkin "melupakan" peristiwa jarang tetapi penting (flash crash)
Combinatorial Purged Cross-Validation (CPCV)

Kaedah lanjutan yang dicadangkan oleh Marcos Lopez de Prado. Data dibahagikan kepada kumpulan, di mana dipilih untuk ujian. Perbezaan utama daripada cross-validation standard adalah purging (membuang data pada sempadan train/test) dan embargo (jurang tambahan untuk mencegah kebocoran data):
Dengan : 45 kombinasi train/test. Setiap kombinasi menghasilkan keputusan OOS, dan anggaran akhir adalah purata merentasi semua kombinasi.
from itertools import combinations
import numpy as np
def cpcv_splits(n_groups: int, k_test: int, purge_pct: float = 0.01):
"""
Jana pembahagian CPCV dengan purging.
Args:
n_groups: bilangan kumpulan
k_test: bilangan kumpulan ujian dalam setiap pembahagian
purge_pct: pecahan data untuk purging (pada sempadan train/test)
"""
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):
"""Kumpulan pada sempadan train/test untuk purging."""
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 lebih baik daripada rolling WFO apabila data terhad, tetapi lebih mahal dari segi pengiraan. Untuk strategi dengan 21 parameter dan 25 bulan data, kami mengesyorkan bermula dengan rolling WFO dan menggunakan CPCV sebagai pemeriksaan tambahan.
Parameter Utama WFO

Panjang Tempoh Train
Train yang terlalu pendek — data tidak mencukupi untuk pengoptimuman yang boleh dipercayai. Terlalu panjang — data lama mencairkan corak semasa.
Peraturan umum: train harus mengandungi sekurang-kurangnya 200-300 dagangan. Jika strategi membuat 2 dagangan sehari:
Untuk kripto dengan penukaran rejimnya, kami mengesyorkan tidak lebih daripada 6-12 bulan untuk tetingkap bergerak.
Panjang Tempoh Test
Tempoh ujian mesti mencukupi untuk penilaian yang signifikan secara statistik, tetapi tidak terlalu panjang — jika tidak, parameter akan mula merosot.
Peraturan: test = 20-33% daripada train. Jika train = 6 bulan, test = 1.5-2 bulan.
Pertindihan
Dalam rolling WFO, tetingkap boleh bertindih. Pertindihan meningkatkan bilangan titik data OOS tetapi memperkenalkan korelasi antara anggaran:
Tanpa pertindihan:
Train [01..06] → Test [07..09]
Train [07..12] → Test [01..03]
Dengan pertindihan 50%:
Train [01..06] → Test [07..09]
Train [04..09] → Test [10..12]
Train [07..12] → Test [01..03]
Cadangan: pertindihan 50% pada tempoh train — imbangan yang baik antara bilangan tetingkap dan kebebasan anggaran.
Kekerapan Pengoptimuman Semula
Menentukan seberapa kerap anda mengira semula parameter. Dalam pasaran kripto, kekerapan optimum adalah setiap 1-3 bulan. Pengoptimuman semula yang lebih kerap meningkatkan risiko overfitting kepada bunyi hingar; yang lebih jarang — risiko parameter menjadi usang.
Walk-Forward Efficiency Ratio dan Kadar Degradasi

Walk-Forward Efficiency Ratio (WFER)
Metrik utama WFO — nisbah pulangan OOS kepada pulangan IS:
Tafsiran:
| WFER | Tafsiran |
|---|---|
| > 0.8 | Keteguhan cemerlang. Parameter berpindah ke data baharu. |
| 0.5 — 0.8 | Keteguhan yang boleh diterima. Strategi berfungsi tetapi dengan degradasi. |
| 0.3 — 0.5 | Kes sempadan. Overfitting separa mungkin berlaku. |
| < 0.3 | Overfitting. Parameter disesuaikan dengan data IS. |
| < 0 | Strategi tidak menguntungkan OOS. Overfitting sepenuhnya atau ralat logik. |
Jika WFER < 0.5 — strategi kemungkinan besar overfit. Ini adalah penapis utama kami.
Kadar Degradasi
Menunjukkan seberapa cepat parameter optimum kehilangan keberkesanan dari masa ke masa:
Dalam amalan: bahagikan tempoh ujian kepada sub-selang dan jejaki dinamik PnL:
def degradation_rate(oos_returns: np.ndarray, n_subperiods: int = 4) -> float:
"""
Anggarkan kadar degradasi parameter.
Membahagikan tempoh OOS kepada sub-selang dan mengira kecerunan
regresi linear PnL terhadap nombor sub-selang.
Returns:
slope: negatif = degradasi, positif = penambahbaikan
"""
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
Jika kadar degradasi sangat negatif — parameter menjadi usang dengan cepat, dan anda memerlukan pengoptimuman semula yang lebih kerap atau tempoh train yang lebih pendek.
Pelaksanaan Penuh Pipeline WFO dalam Python

import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from typing import Callable, List, Optional
import warnings
@dataclass
class WFOWindow:
"""Satu tetingkap walk-forward."""
window_id: int
train_start: int # indeks mula train
train_end: int # indeks akhir train (eksklusif)
test_start: int # indeks mula test
test_end: int # indeks akhir test (eksklusif)
best_params: dict = field(default_factory=dict)
is_pnl: float = 0.0 # PnL in-sample
oos_pnl: float = 0.0 # PnL out-of-sample
oos_returns: np.ndarray = field(default_factory=lambda: np.array([]))
wfer: float = 0.0 # walk-forward efficiency ratio
@dataclass
class WFOResult:
"""Keputusan keseluruhan WFO."""
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 # sama ada strategi lulus penapis
class WalkForwardOptimizer:
"""
Pipeline Walk-Forward Optimization.
Menyokong mod anchored (berkembang) dan rolling (meluncur).
"""
def __init__(
self,
data: np.ndarray,
optimize_fn: Callable,
evaluate_fn: Callable,
mode: str = "rolling", # "rolling" atau "anchored"
train_size: int = 180, # hari
test_size: int = 60, # hari
step_size: int = 60, # saiz langkah tetingkap, hari
min_trades: int = 30, # bilangan minimum dagangan dalam OOS
wfer_threshold: float = 0.5, # ambang WFER untuk terima/tolak
):
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]:
"""Jana tetingkap walk-forward."""
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:
"""Jalankan pipeline WFO penuh."""
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:
"""Kecerunan OOS PnL merentasi nombor tetingkap."""
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
Contoh Penggunaan
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):
"""
Optimumkan strategi pada data train.
Mengembalikan (best_params, is_pnl).
"""
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):
"""
Nilai strategi pada data test dengan parameter tetap.
Mengembalikan (oos_pnl, oos_returns).
"""
pnl, returns = _run_strategy(test_data, params["fast"], params["slow"])
return pnl, returns
def _run_strategy(data, fast_period, slow_period):
"""Strategi MA crossover ringkas."""
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 bulan
test_size=60, # 2 bulan
step_size=60, # langkah = test
)
result = wfo.run()
print(f"Tetingkap: {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"Degradasi: {result.degradation_rate:.5f}")
print(f"Lulus: {result.passed}")
for w in result.windows:
print(f" Tetingkap {w.window_id}: IS={w.is_pnl:.4f} OOS={w.oos_pnl:.4f} "
f"WFER={w.wfer:.2f} params={w.best_params}")
Mentafsir Keputusan: Bila untuk Percaya, Bila untuk Tolak
Strategi Lulus WFO
Jika WFER >= 0.5 merentasi semua tetingkap, OOS PnL adalah positif dan stabil:
Tetingkap 0: IS=0.0812 OOS=0.0531 WFER=0.65 params={'fast': 10, 'slow': 50}
Tetingkap 1: IS=0.0744 OOS=0.0489 WFER=0.66 params={'fast': 10, 'slow': 50}
Tetingkap 2: IS=0.0698 OOS=0.0401 WFER=0.57 params={'fast': 15, 'slow': 50}
Tetingkap 3: IS=0.0823 OOS=0.0512 WFER=0.62 params={'fast': 10, 'slow': 60}
Tetingkap 4: IS=0.0756 OOS=0.0478 WFER=0.63 params={'fast': 10, 'slow': 50}
→ Agregat WFER: 0.63, semua tetingkap > 0.5, parameter stabil
Tanda-tanda baik:
- WFER stabil merentasi tetingkap (tiada lonjakan mendadak)
- Parameter serupa antara tetingkap (fast = 10-15, slow = 50-60)
- OOS PnL positif dalam kebanyakan tetingkap
- Kadar degradasi hampir sifar
Strategi Gagal WFO
Tetingkap 0: IS=0.2341 OOS=-0.0312 WFER=-0.13 params={'fast': 5, 'slow': 95}
Tetingkap 1: IS=0.1987 OOS=0.0089 WFER=0.04 params={'fast': 25, 'slow': 30}
Tetingkap 2: IS=0.2156 OOS=-0.0567 WFER=-0.26 params={'fast': 10, 'slow': 90}
Tetingkap 3: IS=0.1834 OOS=0.0234 WFER=0.13 params={'fast': 20, 'slow': 40}
→ Agregat WFER: -0.07, IS tinggi, OOS hampir sifar → overfitting
Tanda-tanda overfitting:
- IS PnL tinggi, OOS PnL rendah/negatif — overfitting klasik
- Parameter berbeza-beza dengan ketara antara tetingkap — tiada optimum yang stabil
- WFER < 0.3 dalam kebanyakan tetingkap — parameter tidak berpindah
- Kadar degradasi sangat negatif — degradasi pesat
Lanjut tentang analisis kestabilan parameter — dalam artikel Analisis plateau. Jika optimum adalah "tajam" (jatuh curam dengan perubahan parameter kecil) — ini adalah isyarat overfitting tambahan.
Kekhususan WFO untuk Mata Wang Kripto

Mata wang kripto mewujudkan masalah unik untuk WFO yang tidak wujud dalam pasaran tradisional.
Penukaran Rejim
Pasaran kripto bertukar antara rejim yang berbeza secara radikal: trend lembu, trend beruang, sisi dengan volatiliti tinggi/rendah. Parameter optimum dalam satu rejim boleh tidak menguntungkan dalam rejim lain.
Penyelesaian: gunakan rolling WFO (bukan anchored) dengan tetingkap 4-6 bulan. Ini membolehkan "melupakan" rejim lama. Selain itu — kelompokkan data mengikut volatiliti dan jalankan WFO secara berasingan untuk setiap kelompok.
Sejarah yang Pendek
Kebanyakan altcoin mempunyai kurang daripada 3 tahun sejarah dagangan. Dengan train = 6 bulan dan test = 2 bulan, anda hanya akan mendapat 4-5 tetingkap — anggaran yang lemah secara statistik.
Penyelesaian: gunakan CPCV sebagai ganti atau sebagai tambahan kepada rolling WFO. CPCV menjana lebih banyak kombinasi daripada data yang sama. Untuk 10 kumpulan dan k=2: 45 kombinasi berbanding 4-5 tetingkap.
Perubahan Kecairan Struktur
Kecairan pasangan kripto adalah tidak pegun: pasangan boleh cair selama 6 bulan, kemudian volum jatuh 10x. Parameter yang dioptimumkan pada pasaran cair tidak berfungsi pada pasaran yang tidak cair.
Penyelesaian: tambah penapis kecairan pada pipeline WFO. Kecualikan tetingkap di mana purata volum harian berada di bawah ambang. Sahkan bahawa kecairan dalam tempoh ujian setanding dengan tempoh train.
Kesan Kadar Pendanaan
Untuk strategi niaga hadapan berleveraj, kadar pendanaan boleh mengubah keputusan OOS secara asas. Strategi menunjukkan +5% OOS selama 2 bulan, tetapi pada leveraj 10x, pendanaan memakan 3.6%.
Analisis terperinci tentang kesan pendanaan — dalam artikel kami Kadar pendanaan membunuh leveraj anda. Pastikan untuk mengambil kira kos pendanaan apabila menilai OOS PnL dalam WFO.
Strategi Berbilang Parameter: Mengapa WFO Kritikal dengan 12+ Parameter

Strategi dengan 21 parameter (12 pemisahan + 9 meta) pada 25 bulan data daripada satu pasangan adalah model dengan ruang carian yang kolosal.
Kutukan Dimensi
Bilangan kombinasi parameter berkembang secara eksponen dengan bilangan parameter:
Jika setiap daripada 21 parameter mengambil sekurang-kurangnya 10 nilai:
Walaupun dengan pengoptimuman Bayesian (butiran dalam Coordinate Descent vs Bayesian), anda meneroka pecahan kecil ruang. Kebarangkalian bahawa optimum yang ditemui adalah artifak bunyi hingar dan bukannya corak sebenar bertambah dengan bilangan parameter.
Formula Bonferroni untuk Perbandingan Berganda
Jika anda menguji kombinasi parameter, kebarangkalian "penemuan" palsu (menemui hasil yang baik secara kebetulan):
Pada dan kombinasi yang dicuba:
Anda dijamin menemui parameter yang "berfungsi" — yang sebenarnya disesuaikan dengan bunyi hingar. Tanpa WFO, anda tidak mempunyai cara untuk membezakan kelebihan sebenar daripada artifak statistik.
Peraturan: Bilangan Titik Data OOS berbanding Bilangan Parameter
Peraturan umum untuk mempercayai keputusan WFO:
Untuk 21 parameter, anda memerlukan sekurang-kurangnya 210 dagangan OOS. Jika WFO anda menjana lebih sedikit — hasilnya tidak boleh dipercayai.
Strategi dengan +3342% PnL@ML: 21 parameter, 25 bulan data. Anggap 5 tetingkap OOS 60 hari, 2 dagangan/hari — jumlah dagangan OOS. Nisbah — boleh diterima, tetapi hanya jika WFER > 0.5.
Mengintegrasikan WFO dengan Optuna

Dalam setiap tetingkap WFO, anda perlu mengoptimumkan parameter. Untuk 21 parameter, carian grid adalah mustahil, coordinate descent adalah tidak cekap. Pilihan optimum adalah pengoptimuman Bayesian melalui Optuna.
import optuna
from optuna.samplers import TPESampler
def optuna_optimize(train_data: np.ndarray, n_trials: int = 500) -> tuple:
"""
Optimumkan parameter strategi menggunakan Optuna.
Digunakan di dalam setiap tetingkap WFO.
"""
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 # kombinasi tidak sah
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, # Optuna sebagai ganti carian grid
evaluate_fn=my_evaluate,
mode="rolling",
train_size=180,
test_size=60,
step_size=60,
)
result = wfo.run()
Penting: di dalam WFO, optimumkan Sharpe, bukan PnL. Pengoptimuman PnL mencari parameter yang memaksimumkan keuntungan pada urutan dagangan tertentu. Pengoptimuman Sharpe mencari parameter dengan nisbah pulangan-kepada-risiko terbaik — ia lebih teguh OOS.
Perbandingan terperinci Optuna TPE dengan coordinate descent — dalam artikel Coordinate Descent vs Bayesian.
Memvisualisasikan Keputusan WFO
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
def plot_wfo_results(result: WFOResult, data: np.ndarray):
"""Visualisasikan keputusan Walk-Forward Optimization."""
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='Titik pulang modal')
ax.set_title(f'Keluk Ekuiti OOS (WFER={result.wfer:.2f}, Sharpe={result.oos_sharpe:.2f})')
ax.set_ylabel('Ekuiti')
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='Ambang (0.5)')
ax.axhline(0, color='gray', linestyle='-', alpha=0.3)
ax.set_title('Walk-Forward Efficiency Ratio mengikut Tetingkap')
ax.set_xlabel('Tetingkap')
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('PnL In-Sample vs Out-of-Sample')
ax.set_xlabel('Tetingkap')
ax.set_ylabel('PnL')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('wfo_results.png', dpi=150)
plt.show()
Cadangan Praktikal
Senarai Semak Sebelum Melancarkan Strategi ke Pengeluaran
1. Jalankan WFO (rolling + anchored)
Bandingkan keputusan kedua-dua mod. Jika rolling WFO gagal tetapi anchored lulus — kemungkinan besar strategi hanya berfungsi pada data awal.
2. Semak WFER untuk setiap tetingkap
Bukan hanya WFER agregat, tetapi setiap tetingkap secara individu. Jika 2 daripada 6 tetingkap mempunyai WFER < 0 — itu adalah masalah, walaupun agregat > 0.5.
3. Bandingkan parameter antara tetingkap
Jika parameter optimum "melompat" dari tetingkap ke tetingkap — tiada kelebihan yang stabil. Gunakan Analisis plateau untuk mengesahkan kestabilan optimum.
4. Semak kadar degradasi
Kadar degradasi yang sangat negatif = parameter kehilangan keberkesanan dengan cepat. Anda memerlukan pengoptimuman semula yang lebih kerap atau pembaikan strategi.
5. Terapkan Monte Carlo bootstrap pada keputusan OOS
OOS PnL agregat juga merupakan anggaran titik tunggal. Terapkan Monte Carlo bootstrap pada tatasusunan pulangan OOS untuk mendapatkan selang keyakinan.
6. Ambil kira kos
OOS PnL mesti merangkumi komisen, slippage, dan kadar pendanaan. OOS PnL yang cantik tanpa kos adalah ilusi. Butiran lanjut — Kadar pendanaan membunuh leveraj anda.
Keperluan Data Minimum
| Bilangan parameter | Min dagangan OOS | Min tetingkap WFO | Min data (2 dagangan/hari) |
|---|---|---|---|
| 2-5 | 50 | 3 | ~6 bulan |
| 6-10 | 100 | 4 | ~12 bulan |
| 11-15 | 150 | 5 | ~18 bulan |
| 16-21 | 210 | 6 | ~24 bulan |
| 22+ | 300+ | 8+ | ~36+ bulan |
Strategi dengan 21 Parameter dan 25 Bulan Data
Mari kembali kepada soalan dari awal artikel: 21 parameter dioptimumkan pada 25 bulan data daripada satu pasangan. PnL@ML = +3342%. Bagaimana untuk mengesahkan?
Langkah 1. Rolling WFO: train = 8 bulan, test = 2 bulan, langkah = 2 bulan. Kita mendapat ~8 tetingkap.
Langkah 2. Anchored WFO: train pertama = 8 bulan, test = 2 bulan. Kita mendapat ~8 tetingkap.
Langkah 3. CPCV: 10 kumpulan ~2.5 bulan, k = 2. Kita mendapat 45 kombinasi.
Langkah 4. Untuk setiap kaedah, sahkan:
- WFER >= 0.5?
- Parameter stabil antara tetingkap?
- Kadar degradasi boleh diterima?
- Dagangan OOS / Parameter >= 10?
Langkah 5. Monte Carlo bootstrap pada pulangan OOS agregat. PnL persentil ke-5 > 0?
Jika mana-mana ujian ini gagal — strategi dengan +3342% kemungkinan besar overfit. 21 parameter pada 25 bulan satu pasangan — ini adalah nisbah parameter-kepada-data yang sangat tinggi. Tanpa lulus WFO, tidak ada kepercayaan yang boleh diberikan.
Kami juga mengesyorkan menyemak kecekapan strategi dengan mengambil kira PnL mengikut masa aktif — ini akan mendedahkan bahagian mana daripada +3342% yang disebabkan oleh masa dalam posisi berbanding kelebihan sebenar.
Kesimpulan
Walk-Forward Optimization bukan pilihan — ia adalah keperluan. Ia adalah satu-satunya kaedah yang secara sistematik mengesahkan kebolehpindahan parameter kepada data baharu. Pembahagian train/test tunggal adalah loteri. Backtest penuh pada semua data adalah penipuan diri.
Kesimpulan utama:
-
WFER < 0.5 = overfitting. Jika OOS PnL kurang daripada separuh IS — parameter disesuaikan.
-
Kestabilan parameter lebih penting daripada maksimum. Parameter yang menghasilkan +15% dalam setiap tetingkap lebih baik daripada parameter yang menghasilkan +40% dalam satu dan -10% dalam yang lain.
-
Rolling WFO untuk kripto. Penukaran rejim menjadikan anchored WFO kurang dipercayai. Tetingkap bergerak 4-6 bulan adalah imbangan optimum.
-
Lebih banyak parameter — keperluan yang lebih ketat. 21 parameter memerlukan sekurang-kurangnya 210 dagangan OOS dan 6+ tetingkap WFO. Tanpa ini, hasilnya tidak dapat disahkan.
-
WFO + Monte Carlo bootstrap + Analisis plateau — tiga lapisan perlindungan overfitting. Setiap lapisan menangkap apa yang terlepas oleh yang lain.
Strategi yang lulus WFO dengan WFER > 0.5 merentasi semua tetingkap, parameter yang stabil, dan persentil ke-5 bootstrap yang positif — itulah strategi yang boleh anda percayakan dengan wang sebenar. Segala-galanya yang lain adalah curve fitting dengan keluk ekuiti yang cantik.
Pautan Berguna
- 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
Petikan
@article{soloviov2026walkforwardoptimization,
author = {Soloviov, Eugen},
title = {Walk-Forward Optimization: Satu-Satunya Ujian Strategi yang Jujur},
year = {2026},
url = {https://marketmaker.cc/ms/blog/post/walk-forward-optimization},
version = {0.1.0},
description = {Mengapa satu pembahagian train/test tidak melindungi daripada overfitting, bagaimana walk-forward optimization mengesahkan keteguhan parameter secara sistematik, dan mengapa strategi dengan PnL +3342\% @ML pada 21 parameter adalah bom jangka masa tanpa WFO.}
}
Pengarang
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.