← Kembali ke artikel
March 15, 2026
Bacaan 5 minit

Walk-Forward Optimization: Satu-Satunya Ujian Strategi yang Jujur

#algotrading
#backtest
#walk-forward
#overfitting
#pengesahan
#pengoptimuman

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

Visualisasi perangkap pembahagian train/test

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

Rajah tetingkap bergerak walk-forward

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)

Visualisasi tetingkap berkembang anchored WFO

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)

Visualisasi combinatorial purged cross-validation

Kaedah lanjutan yang dicadangkan oleh Marcos Lopez de Prado. Data dibahagikan kepada NN kumpulan, di mana kk 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):

Bilangan kombinasi=(Nk)\text{Bilangan kombinasi} = \binom{N}{k}

Dengan N=10,k=2N = 10, k = 2: 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

Visualisasi 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:

Tmin=300 dagangan2 dagangan/hari=150 hari5 bulanT_{min} = \frac{300\ \text{dagangan}}{2\ \text{dagangan/hari}} = 150\ \text{hari} \approx 5\ \text{bulan}

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

Visualisasi walk-forward efficiency ratio dan degradasi

Walk-Forward Efficiency Ratio (WFER)

Metrik utama WFO — nisbah pulangan OOS kepada pulangan IS:

WFER=PnLOOSPnLIS\text{WFER} = \frac{\text{PnL}_{OOS}}{\text{PnL}_{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:

Kadar degradasi=d(OOS PnL)dt\text{Kadar degradasi} = \frac{d(\text{OOS PnL})}{dt}

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

Visualisasi seni bina pipeline WFO

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

Visualisasi kekhususan WFO 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

Kutukan dimensi dalam pengoptimuman berbilang 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:

Kombinasi=i=1nPi\text{Kombinasi} = \prod_{i=1}^{n} |P_i|

Jika setiap daripada 21 parameter mengambil sekurang-kurangnya 10 nilai:

1021=10 sextilion kombinasi10^{21} = 10\ \text{sextilion kombinasi}

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 MM kombinasi parameter, kebarangkalian "penemuan" palsu (menemui hasil yang baik secara kebetulan):

P(penemuan palsu)=1(1α)M1eαMP(\text{penemuan palsu}) = 1 - (1 - \alpha)^M \approx 1 - e^{-\alpha M}

Pada α=0.05\alpha = 0.05 dan M=10000M = 10000 kombinasi yang dicuba:

P1e5001.0P \approx 1 - e^{-500} \approx 1.0

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:

Dagangan OOSParameter>10\frac{\text{Dagangan OOS}}{\text{Parameter}} > 10

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 5×60×2=6005 \times 60 \times 2 = 600 dagangan OOS. Nisbah 600/21=28.6600/21 = 28.6 — boleh diterima, tetapi hanya jika WFER > 0.5.

Mengintegrasikan WFO dengan Optuna

Pengoptimuman Bayesian dengan integrasi 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:

  1. WFER < 0.5 = overfitting. Jika OOS PnL kurang daripada separuh IS — parameter disesuaikan.

  2. 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.

  3. Rolling WFO untuk kripto. Penukaran rejim menjadikan anchored WFO kurang dipercayai. Tetingkap bergerak 4-6 bulan adalah imbangan optimum.

  4. 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.

  5. 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

  1. Pardo, R. — The Evaluation and Optimization of Trading Strategies (Wiley)
  2. Lopez de Prado, M. — Advances in Financial Machine Learning, Chapter 12: Backtesting
  3. Bailey, D.H. et al. — The Probability of Backtest Overfitting
  4. Lopez de Prado, M. — The Combinatorial Purged Cross-Validation (CPCV)
  5. Aronson, D.R. — Evidence-Based Technical Analysis
  6. Optuna: A Next-generation Hyperparameter Optimization Framework
  7. Kevin Davey — Building Winning Algorithmic Trading Systems: Walk-Forward Analysis
  8. White, H. — A Reality Check for Data Snooping (2000)
  9. Harvey, C.R. & Liu, Y. — Backtesting (2015)
  10. 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.}
}
Penafian: Maklumat yang disediakan dalam artikel ini adalah untuk tujuan pendidikan dan maklumat sahaja dan bukan merupakan nasihat kewangan, pelaburan, atau dagangan. Dagangan mata wang kripto melibatkan risiko kerugian yang ketara.

Pengarang

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

Kekal Mendahului Pasaran

Langgan surat berita kami untuk pandangan dagangan AI eksklusif, analisis pasaran, dan kemas kini platform.

Kami menghormati privasi anda. Berhenti melanggan pada bila-bila masa.