Разработка торговой стратегии без автоматической оптимизации параметров — это лотерея. Перебор вручную занимает недели, 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% ложных шаблонов и сохранило около $90–130 потенциальных убытков. Получите консультацию по вашей стратегии — мы оценим её за 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 раз — свяжитесь с нами для оценки вашего проекта.







