Оптимізація параметрів торговельної стратегії
Написати стратегію — половина роботи. Знайти параметри, при яких вона працює — інша половина. Оптимізація параметрів це не магія: це пошук по просторі значень з коректною валідацією результатів. Головний враг тут — 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. Вимагає менше ітерацій для хорошого результату.
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), візуалізація важливості параметрів.
Метрики для оптимізації
Не оптимізуй під 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 тестування та документування результатів.







