Walk-forward на IMOEX: как не обмануть себя бэктестом
Backtest показывает Sharpe 2.4, а на реальных деньгах стратегия сваливается в drawdown с первой недели. Разбираем, почему in-sample оптимизация почти всегда лжёт и как walk-forward возвращает реалистичные ожидания.
TL;DR
- Любая оптимизация параметров на одной выборке завышает доходность
в 2–5 раз — это optimization bias, и он не лечится «здравым
смыслом». - Walk-forward analysis (WFA) — это процедура, которая разбивает
историю на последовательные in-sample / out-of-sample окна и
считает результат только по out-of-sample. - На IMOEX с 2018 по 2026 типичная momentum-стратегия теряет
60–80% «бумажного» Sharpe при честном WFA. Если после этого
что-то остаётся — это можно нести в прод.
Почему обычный бэктест врёт
Допустим, вы тестируете стратегию вида «купить, если цена выше
SMA(N), продать, если ниже» на IMOEX. Перебираете N от 5 до 200
и выбираете тот, который даёт лучший Sharpe. Поздравляю — вы
только что подогнали параметр под конкретную реализацию шума.
Это легко проверить: сгенерируйте случайные ряды с тем же
volatility profile, что у IMOEX, и прогоните ту же оптимизацию.
На большинстве синтетических серий «лучший» N выдаст Sharpe 0.8–1.5
— на чистом шуме. На реальных данных вы увидите ровно те же
«красивые» цифры, и не сможете отличить настоящий сигнал от
артефакта оптимизации.
Формальная причина
Если вы перебираете K вариантов параметров, ожидаемое значение
максимального Sharpe из K случайных стратегий растёт как
sqrt(2 ln K). Уже при K=100 это даёт +3 sigma «бесплатного»
Sharpe. Любое мало-мальски сложное пространство параметров
(две скользящие, ATR-фильтр, трейлинг-стоп) — это тысячи
комбинаций.
Walk-forward по шагам
Идея простая: оптимизируем на куске истории, торгуем на
следующем (out-of-sample), сдвигаемся вперёд, повторяем.
├─── train 2018–2019 ───┤├─ test Q1'20 ─┤
├─── train 2018Q2–2020Q1 ───┤├─ test Q2'20 ─┤
├─── train 2018Q4–2020Q3 ───┤├─ test Q3'20 ─┤
...
Параметры оптимизируются только на train, метрики берутся
только с test. Конкатенация всех test-окон даёт честную
equity-curve.
Ключевые решения
- Размер train-окна. Слишком короткое — переподгонка под
локальный режим (2022 vs 2024 — разные миры). Слишком длинное
— параметры усредняются по разным режимам и не работают
нигде. Для IMOEX практичный диапазон — 18–36 месяцев. - Размер test-окна. Чем меньше, тем чаще rebalance
параметров — и тем больше transaction cost. Для дневок
удобный шаг — 1 месяц. - Anchored vs rolling. Anchored расширяет train-окно
(всё больше истории), rolling сдвигает (фиксированная
длина). Anchored устойчивее к шуму, rolling быстрее
адаптируется к смене режима.
Реализация на Python
Минимальный каркас без зависимостей сверх pandas:
import pandas as pd
import numpy as np
def walk_forward(prices: pd.Series,
train_months: int = 24,
test_months: int = 1,
optimize_fn=None):
results = []
start = prices.index[0]
end = prices.index[-1]
cursor = start + pd.DateOffset(months=train_months)
while cursor + pd.DateOffset(months=test_months) <= end:
train = prices[cursor - pd.DateOffset(months=train_months) : cursor]
test = prices[cursor : cursor + pd.DateOffset(months=test_months)]
best_params = optimize_fn(train)
pnl = simulate(test, best_params)
results.append({
"from": test.index[0],
"to": test.index[-1],
"params": best_params,
"pnl": pnl.sum(),
"sharpe": pnl.mean() / pnl.std() * np.sqrt(252),
})
cursor += pd.DateOffset(months=test_months)
return pd.DataFrame(results)
optimize_fn принимает train-серию и возвращает лучшие
параметры. simulate — это ваш бэктест-движок, который по
ценам и параметрам строит equity. Принципиально, что
optimize_fn не видит test-выборку.
Что обычно отваливается
Прогонял три классики на IMOEX (2018-01 → 2026-04, дневные
закрытия, без учёта дивидендов):
| Стратегия | IS Sharpe | OOS Sharpe (WFA) | Drop |
|---|---|---|---|
| SMA-cross (N1, N2) | 1.82 | 0.34 | -81% |
| Donchian breakout (N) | 1.61 | 0.58 | -64% |
| RSI mean-reversion | 2.07 | -0.12 | -106% |
RSI mean-reversion — классический пример: на IS красиво
«ловит откаты», на OOS работает хуже buy&hold. Это не
«плохая стратегия» — это подобранные под прошлое
параметры, которые на новых данных не воспроизводятся.
Чек-лист «честного» WFA
- Train и test никогда не пересекаются по времени.
- Все производные фичи (скользящие, волатильность, бэты)
считаются с lookback внутри train-окна, без peeking. - Optimization metric — это то, что вы реально хотите
максимизировать в проде (например, Sharpe minus turnover
cost), а не просто PnL. - Из «лучших» параметров на train вы выбираете не один
глобальный максимум, а робастную область (top-decile,
усреднение по top-N). Иначе ловите шум. - Учтены реальные комиссии и проскальзывание Мосбиржи
(минимум 0.04% per trade на ликвидных бумагах,
до 0.15% на втором эшелоне). - Период тестирования включает разные режимы:
2020 (ковид), 2022 (приостановка торгов), 2024–2025
(восстановление). Если стратегия работает только в
трендовый 2023 — это не стратегия.
Что делать с результатом
Если после WFA Sharpe > 1.0 на OOS, drawdown < 25%, и метрика
устойчива к смещению окон на ±2 месяца — стратегию можно
нести в paper trading. Если красиво только при одном конкретном
размере окна — это последний звоночек переподгонки.
И ещё одно: walk-forward не панацея. Он защищает от
оптимизации параметров, но не от переборного отбора стратегий.
Если вы протестировали 200 идей и выбрали ту, что прошла WFA —
у вас всё ещё есть selection bias. Лекарство — заранее
фиксировать гипотезу и считать поправку Бонферрони на
количество тестов.