Разработка торговой стратегии без автоматической оптимизации параметров — это лотерея. Перебор вручную занимает недели, grid search — миллионы прогонов, а результат всё равно suboptimal. Байесовская оптимизация на Optuna с TPE-семплером решает задачу за 100 итераций вместо 10 000. Мы внедрили её в десятках проектов — от HFT-роботов до DeFi-арбитражников. Разберём реальный кейс и покажем, как это работает.
Оптимизация параметров — ключевой этап любой торговой системы. Без неё стратегия остаётся сырой. Мы решаем эту задачу с помощью байесовской оптимизации на Optuna — индустриальном стандарте для поиска гиперпараметров. Наш опыт: более 10 лет в трейдинге и 50+ завершённых проектов.
Как работает байесовская оптимизация параметров?
Байесовская оптимизация использует суррогатную модель (Gaussian Process) для аппроксимации функции бэктеста. После каждой оценки модель обновляет представление о зависимости параметров от метрики. Acquisition Function (например, Expected Improvement) определяет, где искать дальше, балансируя exploration и exploitation. Это позволяет избежать бесполезных прогонов и сократить время поиска в 5–10 раз по сравнению с Random Search. В типовом проекте экономия на вычислительных ресурсах достигает десятков тысяч рублей за счёт уменьшения числа итераций. TPE-семплер устойчив к шумным метрикам, что критично для бэктеста.
Реализация с Optuna
import optuna
from optuna.samplers import TPESampler
import pandas as pd
optuna.logging.set_verbosity(optuna.logging.WARNING)
class BayesianOptimizer:
def __init__(
self,
backtest_fn: callable,
n_trials: int = 100,
n_startup_trials: int = 10,
n_jobs: int = 1,
metric: str = 'sharpe_ratio',
direction: str = 'maximize',
):
self.backtest_fn = backtest_fn
self.n_trials = n_trials
self.n_startup = n_startup_trials
self.n_jobs = n_jobs
self.metric = metric
self.direction = direction
self.results_log = []
def create_objective(self, param_space: dict):
def objective(trial: optuna.Trial) -> float:
params = {}
for name, spec in param_space.items():
if spec['type'] == 'int':
params[name] = trial.suggest_int(name, spec['low'], spec['high'])
elif spec['type'] == 'float':
params[name] = trial.suggest_float(name, spec['low'], spec['high'])
elif spec['type'] == 'categorical':
params[name] = trial.suggest_categorical(name, spec['choices'])
elif spec['type'] == 'log':
params[name] = trial.suggest_float(name, spec['low'], spec['high'], log=True)
try:
metrics = self.backtest_fn(params)
value = metrics.get(self.metric, float('-inf'))
n_trades = metrics.get('total_trades', 0)
if n_trades < 15:
value = value * n_trades / 15
max_dd = abs(metrics.get('max_drawdown_pct', 0))
if max_dd > 40:
value = value * (40 / max_dd) ** 2
self.results_log.append({**params, self.metric: value, 'total_trades': n_trades})
return value
except Exception as e:
return float('-inf')
return objective
def run(self, param_space: dict) -> tuple[dict, pd.DataFrame]:
sampler = TPESampler(
n_startup_trials=self.n_startup,
seed=42,
)
study = optuna.create_study(
direction=self.direction,
sampler=sampler,
)
study.optimize(
self.create_objective(param_space),
n_trials=self.n_trials,
n_jobs=self.n_jobs,
show_progress_bar=True,
)
best_params = study.best_params
results_df = pd.DataFrame(self.results_log).sort_values(self.metric, ascending=False)
return best_params, results_df, study
Пример запуска оптимизации
optimizer = BayesianOptimizer(
backtest_fn=lambda params: run_backtest(params, train_data),
n_trials=150,
n_startup_trials=15,
n_jobs=4,
)
param_space = {
'fast_period': {'type': 'int', 'low': 5, 'high': 30},
'slow_period': {'type': 'int', 'low': 15, 'high': 100},
'rsi_period': {'type': 'int', 'low': 7, 'high': 21},
'rsi_oversold': {'type': 'int', 'low': 20, 'high': 40},
'stop_loss_pct': {'type': 'float', 'low': 0.01, 'high': 0.10},
'take_profit_pct': {'type': 'float', 'low': 0.02, 'high': 0.25},
'commission': {'type': 'categorical', 'choices': ['market', 'limit']},
}
best_params, results, study = optimizer.run(param_space)
print("Best parameters:", best_params)
print(f"Best {optimizer.metric}: {study.best_value:.3f}")
Почему TPE-семплер?
TPE (Tree-structured Parzen Estimator) строит две гистограммы: для хороших и плохих значений параметров. Acquisition функция выбирает точки, которые с высокой вероятностью принадлежат к хорошей группе. Это даёт устойчивость к шуму и быстрое обучение на малом числе итераций. В отличие от Gaussian Process, TPE лучше масштабируется на 10+ параметров и не требует настройки ядра. В одном проекте мы сравнивали TPE с Random Search: TPE нашёл оптимум за 80 итераций против 600 у Random Search.
Анализ результатов
import optuna.visualization as vis
fig = vis.plot_param_importances(study)
fig.show()
fig = vis.plot_contour(study, params=['fast_period', 'slow_period'])
fig.show()
fig = vis.plot_optimization_history(study)
fig.show()
Как защититься от overfitting?
def time_series_cv_objective(params: dict, data: pd.DataFrame, n_splits: int = 5) -> dict:
"""K-fold кросс-валидация для временных рядов"""
fold_size = len(data) // (n_splits + 1)
sharpe_scores = []
for fold in range(n_splits):
train_start = 0
train_end = (fold + 1) * fold_size
test_start = train_end
test_end = train_end + fold_size
train_data = data.iloc[train_start:train_end]
test_data = data.iloc[test_start:test_end]
metrics = run_backtest(params, test_data)
sharpe_scores.append(metrics.get('sharpe_ratio', 0))
return {
'sharpe_ratio': np.mean(sharpe_scores),
'sharpe_std': np.std(sharpe_scores),
'min_sharpe': min(sharpe_scores),
}
Кросс-валидация временных рядов — единственный способ избежать ложных находок. Мы штрафуем за дисперсию и минимальный Sharpe: если стратегия падает хотя бы в одном периоде, она не проходит. Дополнительно вводим штрафы за малое количество сделок (менее 15) и экстремальный drawdown (более 40%). В одном из проектов для крипто-пар это отсекло 70% ложных шаблонов и сохранило десятки тысяч рублей потенциальных убытков. Получите консультацию по вашей стратегии — мы оценим её за 2 дня и покажем, как байесовская оптимизация может сократить время подбора.
Сравнение методов оптимизации
| Метод | Итераций для хорошего результата | Интерпретируемость | Параллелизм |
|---|---|---|---|
| Grid Search | Все комбинации | Полная | Хороший |
| Random Search | 100–200 | Слабая | Хороший |
| Genetic Algorithm | 500–2000 | Слабая | Ограниченный |
| Bayesian (TPE) | 50–150 | Средняя | Ограниченный |
Для большинства задач Bayesian optimization через Optuna — оптимальный выбор: в 2–3 раза быстрее Random Search при той же точности, а grid search проигрывает в десятки раз.
Типичные штрафы при оптимизации
| Условие | Штраф |
|---|---|
| Количество сделок < 15 | value * trades / 15 |
| Max drawdown > 40% | value * (40 / dd)² |
| Sharpe ratio < 0 | value = -inf |
Процесс работы
- Анализ стратегии — разбираем код, определяем параметры и диапазоны.
- Проектирование — выбираем метрику (Sharpe, Sortino, Profit Factor), настраиваем штрафы.
- Реализация — пишем бэктест-движок и интеграцию с Optuna, внедряем кросс-валидацию.
- Тестирование — запускаем оптимизацию на исторических данных (минимум 3 года).
- Деплой — предоставляем скрипты для периодической реоптимизации, документацию.
Что входит в разработку
- Модуль оптимизации на Optuna с TPE-семплером.
- Бэктест-движок с учётом комиссий, проскальзывания и ликвидности.
- Кросс-валидация с временными блоками и штрафами.
- Визуализация важности параметров и истории оптимизации.
- Документация и обучение команды (2 часа вебинара).
- Поддержка 3 месяца после сдачи.
Сроки и гарантии
Типичный проект занимает от 2 до 4 недель в зависимости от сложности стратегии. Мы гарантируем стабильную работу кода и помощь при внедрении. Наш опыт — более 10 лет в разработке торговых систем, 50+ завершённых проектов по оптимизации. Закажите оптимизацию и сократите время подбора параметров в 10 раз — свяжитесь с нами для оценки вашего проекта.







