Розробка торгової стратегії без автоматичної оптимізації параметрів — це лотерея. Перебір вручну займає тижні, 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 разів — зв'яжіться з нами для оцінки вашого проєкту.







