Оптимизация параметров торговой стратегии
Написать стратегию — половина работы. Найти параметры, при которых она работает — другая половина. Оптимизация параметров это не магия: это поиск по пространству значений с корректной валидацией результатов. Главный враг здесь — overfitting.
Что такое overfitting и почему это критично
Стратегия с EMA(9, 21) даёт Sharpe 1.2 на истории. Оптимизатор перебирает все комбинации EMA(5–50) и находит EMA(13, 34) с Sharpe 2.8. Отличный результат? Нет — это overfitting. Параметры подогнаны под конкретный исторический период. На реальных данных стратегия покажет результат, близкий к случайному.
Правило: оптимизируй на train set, валидируй на hold-out (out-of-sample) test set. Если на test set результат сильно хуже — overfitting.
Walk-forward optimization
Самый надёжный метод для временных рядов:
def walk_forward_optimization(data: pd.DataFrame,
strategy_class,
param_grid: dict,
train_periods: int = 180, # дней
test_periods: int = 30) -> list:
results = []
start = 0
while start + train_periods + test_periods <= len(data):
train = data.iloc[start:start + train_periods]
test = data.iloc[start + train_periods:start + train_periods + test_periods]
# Оптимизация на train
best_params = optimize_on_period(strategy_class, train, param_grid)
# Валидация на test (OOS)
oos_result = run_backtest(strategy_class, test, best_params)
results.append({
'period': test.index[0],
'params': best_params,
'oos_sharpe': oos_result.sharpe,
'oos_return': oos_result.total_return,
})
start += test_periods # сдвигаем окно
return results
Walk-forward: оптимизируешь на 6 месяцах, тестируешь на следующем месяце, сдвигаешься на месяц вперёд, повторяешь. Финальный результат — медиана OOS Sharpe по всем окнам.
Методы поиска параметров
Grid Search
Полный перебор всех комбинаций. Прост, но экспоненциально дорог при большом числе параметров.
from itertools import product
import vectorbt as vbt
param_grid = {
'rsi_period': range(7, 21), # 14 значений
'rsi_lower': range(20, 40, 5), # 4 значения
'rsi_upper': range(65, 80, 5), # 3 значения
}
# Итого: 14 * 4 * 3 = 168 комбинаций — приемлемо
# Vectorbt — векторизованный backtesting, 168 комбинаций за секунды
RSI = vbt.IndicatorFactory.from_pandas_ta("rsi")
rsi = RSI.run(close, length=vbt.Param(param_grid['rsi_period']))
Bayesian Optimization
Умнее grid search: строит суррогатную модель функции качества и выбирает следующую точку для проверки на основе exploration/exploitation balansce. Требует меньше итераций для хорошего результата.
from optuna import create_study
def objective(trial):
rsi_period = trial.suggest_int('rsi_period', 5, 30)
rsi_lower = trial.suggest_int('rsi_lower', 20, 40)
rsi_upper = trial.suggest_int('rsi_upper', 60, 85)
result = backtest_strategy(data, rsi_period, rsi_lower, rsi_upper)
return result.sharpe_ratio # максимизируем
study = create_study(direction='maximize', sampler=optuna.samplers.TPESampler())
study.optimize(objective, n_trials=200, n_jobs=4)
print(f"Best params: {study.best_params}")
print(f"Best Sharpe: {study.best_value:.3f}")
Optuna — отличная библиотека для Bayesian optimization. Параллельный поиск, pruning (ранняя остановка плохих trials), визуализация importance параметров.
Метрики для оптимизации
Не оптимизируй под total return — это провоцирует высокий риск. Лучшие targets:
| Метрика | Формула | Комментарий |
|---|---|---|
| Sharpe Ratio | (Return - Rf) / Std | Золотой стандарт |
| Calmar Ratio | Annual Return / Max Drawdown | Хорош для трендовых |
| Sortino Ratio | Return / Downside Std | Штрафует только убытки |
| Profit Factor | Gross Profit / Gross Loss | Простой, интуитивный |
Комбинируй метрики: score = sharpe * 0.5 + calmar * 0.3 + win_rate * 0.2. Это снижает шанс выбрать стратегию, хорошую по одному параметру и плохую по другим.
Параметрическая робастность
Хорошая стратегия работает при небольших отклонениях параметров от оптимума. Проверка:
def check_robustness(best_params: dict, data: pd.DataFrame, delta_pct: float = 0.2):
"""Проверяем, работает ли стратегия при ±20% изменении параметров"""
results = []
for param, value in best_params.items():
for multiplier in [0.8, 0.9, 1.0, 1.1, 1.2]:
test_params = best_params.copy()
test_params[param] = int(value * multiplier)
result = backtest_strategy(data, **test_params)
results.append({'param': param, 'multiplier': multiplier, 'sharpe': result.sharpe})
return pd.DataFrame(results)
Если Sharpe резко падает при изменении RSI с 14 на 13 или 15 — это признак overfitting. Хорошая стратегия показывает плавную sensitivity кривую.
Процесс оптимизации
- Сбор данных: минимум 3–5 лет истории, разные рыночные режимы (бычий, медвежий, боковик)
- Разбивка: 70% train, 30% test (хронологически, не случайно)
- Optimization на train: grid/Bayesian, 100–500 итераций
- Валидация на test: если OOS Sharpe < 50% от IS Sharpe — вероятно overfitting
- Walk-forward check: дополнительная валидация устойчивости
- Sensitivity analysis: проверка робастности параметров
Оптимизация параметров занимает 2–4 недели: сбор и подготовка данных, реализация optimization framework, walk-forward тестирование и документирование результатов.







