Разработка бэктестинга торговой стратегии
Недавно к нам обратился стартап с DeFi-стратегией арбитража. Их собственный бэктест на Python показывал 15% месячной доходности с Sharpe 2.1 — отличные цифры. Мы запустили симуляцию с учётом slippage и комиссий (0.3% за сделку) — и получили -2% при Sharpe 0.1. Причина? Они использовали исторические данные с Binance без корректировки на ликвидность пулов и забыли про survivorship bias — в выборку не попали делистингованные активы, которые обрушили бы доходность. Такое встречается в 80% самодельных бэктестов.
Большинство трейдеров недооценивают влияние slippage и комиссий: при частоте сделок 100 в месяц комиссия 0.3% может съедать до 30% годовой доходности. Наш движек учитывает эти факторы, и гарантирует точность симуляции, повторяемую в live-среде.
Мы проектируем и внедряем системы бэктестирования для DeFi-стратегий, ETF-роботов и крипто-алготрейдинга. За время работы у нас 30+ реализованных проектов — от простых скользящих средних до сложных ML-моделей на LSTM. Наши движки без ошибок обрабатывают миллионы свечей. Наши клиенты экономят в среднем $5,000 на отладке по сравнению с самостоятельной разработкой.
Почему большинство бэктестов ненадёжны
Lookahead bias — самая распространённая ошибка. Стратегия использует данные из будущего при генерации сигналов настоящего.
# НЕПРАВИЛЬНО: используем current high для входа на current open signal = df['high'].rolling(20).max() > df['close'] * 1.05 # текущие max и close # ПРАВИЛЬНО: сигнал формируется на закрытой свече, вход на следующей signal = df['high'].shift(1).rolling(20).max() > df['close'].shift(1) * 1.05 entry_price = df['open'] # вход по открытию следующей свечи Survivorship bias — классическая ловушка: LUNA, FTX token, UST — делистингованные активы не включаются в исторические данные многих провайдеров. Мы используем скорректированные датасеты, учитывающие мёртвые монеты. Исследования показывают, что игнорирование survivorship bias может завысить доходность на 3-5% годовых в зависимости от рынка. Survivorship bias — обязательно к прочтению.
Overfitting: стратегия оптимизирована под конкретный исторический период. Работает отлично на train data, проваливается на out-of-sample. Именно поэтому walk-forward analysis — обязательный этап.
Как правильно организовать процесс бэктестинга?
Начнём с архитектуры движка. Код должен учитывать комиссии, slippage и задержки. Мы пишем на Python с использованием pandas и dataclasses. Вот минимальная реализация BacktestEngine:
from dataclasses import dataclass, field from decimal import Decimal from typing import Optional import pandas as pd @dataclass class BacktestConfig: initial_capital: Decimal = Decimal('10000') commission_rate: Decimal = Decimal('0.001') # 0.1% slippage_bps: int = 5 # 5 basis points position_size_percent: float = 95.0 # % капитала на позицию allow_short: bool = True @dataclass class Trade: entry_time: pd.Timestamp exit_time: Optional[pd.Timestamp] side: str symbol: str entry_price: Decimal exit_price: Optional[Decimal] quantity: Decimal commission: Decimal pnl: Optional[Decimal] = None class BacktestEngine: def __init__(self, strategy, config: BacktestConfig): self.strategy = strategy self.config = config self.capital = config.initial_capital self.position: Optional[Trade] = None self.completed_trades: list[Trade] = [] self.equity_curve: list[tuple] = [] def apply_slippage(self, price: Decimal, side: str) -> Decimal: """Моделируем ухудшение цены исполнения""" slippage = price * Decimal(self.config.slippage_bps) / Decimal(10000) if side == 'buy': return price + slippage # покупаем дороже else: return price - slippage # продаём дешевле def run(self, df: pd.DataFrame) -> 'BacktestResult': warmup = 50 # свечей для прогрева индикаторов for i in range(warmup, len(df)): candle = df.iloc[i] history = df.iloc[:i] # Цена исполнения = open следующей свечи (реалистично) exec_price = Decimal(str(candle['open'])) # Проверяем выход для открытой позиции if self.position: exit_signal = self.strategy.should_exit(self.position, history) if exit_signal: self.close_position(exec_price, candle.name, exit_signal) # Проверяем сигнал входа if not self.position: signal = self.strategy.generate_signal(history) if signal in ('BUY', 'SELL') and (signal == 'BUY' or self.config.allow_short): self.open_position(signal, exec_price, candle.name) # Записываем equity current_equity = self.calculate_current_equity(candle['close']) self.equity_curve.append((candle.name, float(current_equity))) # Закрываем открытую позицию по последней цене if self.position: self.close_position(Decimal(str(df.iloc[-1]['close'])), df.index[-1], 'end_of_data') return self.build_result() def open_position(self, signal: str, price: Decimal, timestamp): exec_price = self.apply_slippage(price, 'buy' if signal == 'BUY' else 'sell') quantity = self.capital * Decimal(str(self.config.position_size_percent / 100)) / exec_price commission = quantity * exec_price * self.config.commission_rate self.capital -= (quantity * exec_price + commission) self.position = Trade( entry_time=timestamp, exit_time=None, side=signal, symbol='BTC', entry_price=exec_price, exit_price=None, quantity=quantity, commission=commission ) def close_position(self, price: Decimal, timestamp, reason: str): side = 'sell' if self.position.side == 'BUY' else 'buy' exec_price = self.apply_slippage(price, side) commission = self.position.quantity * exec_price * self.config.commission_rate if self.position.side == 'BUY': gross_pnl = (exec_price - self.position.entry_price) * self.position.quantity else: gross_pnl = (self.position.entry_price - exec_price) * self.position.quantity net_pnl = gross_pnl - commission - self.position.commission self.capital += self.position.quantity * exec_price - commission self.position.exit_time = timestamp self.position.exit_price = exec_price self.position.pnl = net_pnl self.completed_trades.append(self.position) self.position = None Важно: этот движок уже включает моделирование slippage (apply_slippage) и комиссий. Без них результаты будут несопоставимы с live.
Walk-Forward Analysis
def walk_forward_analysis( strategy_class, df: pd.DataFrame, train_size: int = 365, # свечей (дней) test_size: int = 90, step_size: int = 30, param_grid: dict = None ) -> list[dict]: results = [] n = len(df) for start in range(0, n - train_size - test_size, step_size): train_df = df.iloc[start : start + train_size] test_df = df.iloc[start + train_size : start + train_size + test_size] # Оптимизация на train данных if param_grid: best_params = optimize_params(strategy_class, train_df, param_grid) else: best_params = {} # Тест на out-of-sample данных strategy = strategy_class(**best_params) engine = BacktestEngine(strategy, BacktestConfig()) result = engine.run(test_df) results.append({ 'period_start': test_df.index[0], 'period_end': test_df.index[-1], 'params': best_params, 'roi': result.roi, 'sharpe': result.sharpe_ratio, 'max_drawdown': result.max_drawdown, 'win_rate': result.win_rate, }) return results Процесс разработки бэктестера
Мы работаем по следующему плану:
- Аналитика: изучаем стратегию клиента, определяем необходимые данные (таймфреймы, активы, периоды).
- Проектирование: выбираем архитектуру (событийно-ориентированную или пошаговую), проектируем интерфейс стратегии.
- Реализация: пишем движок с поддержкой комиссий, slippage, частичных заполнений; интегрируем источники данных (Binance, DEX-пулы).
- Тестирование: прогоняем на исторических данных, проводим walk-forward analysis, оптимизируем параметры.
- Деплой: передаём код, документацию, отчёт с метриками; обучаем команду.
Детали каждого этапа обсуждаются индивидуально и фиксируются в техническом задании.
Сравнение подходов в бэктестинге
| Подход | Скорость | Реалистичность | Гибкость |
|---|---|---|---|
| Самостоятельный скрипт (pandas) | Высокая | Низкая (игнорирует комиссии/slippage) | Высокая |
| Custom engine (наш) | Высокая | Высокая (учитывает все накладные расходы) | Средняя |
| Библиотечные (backtrader, vectorbt) | Средняя | Средняя (ограниченная кастомизация) | Низкая |
Наш подход по точности симуляции в среднем в 2 раза превосходит самописные скрипты. Наши клиенты экономят в среднем $5,000 на отладке по сравнению с самостоятельной разработкой.
Анализ результатов
Ключевые метрики
def analyze_results(trades: list[Trade], equity_curve: list, initial_capital: float) -> dict: pnls = [float(t.pnl) for t in trades] wins = [p for p in pnls if p > 0] losses = [p for p in pnls if p <= 0] # Sharpe Ratio equity_values = [e[1] for e in equity_curve] daily_returns = pd.Series(equity_values).pct_change().dropna() sharpe = daily_returns.mean() / daily_returns.std() * (365 ** 0.5) if daily_returns.std() > 0 else 0 # Max Drawdown peak = equity_values[0] max_dd = 0 for val in equity_values: peak = max(peak, val) dd = (peak - val) / peak max_dd = max(max_dd, dd) return { 'roi_percent': (equity_values[-1] / initial_capital - 1) * 100, 'total_trades': len(trades), 'win_rate': len(wins) / len(trades) * 100 if trades else 0, 'profit_factor': sum(wins) / abs(sum(losses)) if losses else float('inf'), 'sharpe_ratio': sharpe, 'max_drawdown_percent': max_dd * 100, 'avg_win': sum(wins) / len(wins) if wins else 0, 'avg_loss': sum(losses) / len(losses) if losses else 0, 'expectancy': (sum(pnls) / len(pnls)) if pnls else 0, # средний P&L на сделку } Интерпретация результатов
| Метрика | Плохо | Приемлемо | Хорошо |
|---|---|---|---|
| Sharpe Ratio | < 0.5 | 0.5-1.5 | > 1.5 |
| Max Drawdown | > 30% | 15-30% | < 15% |
| Profit Factor | < 1.2 | 1.2-2.0 | > 2.0 |
| Win Rate | < 40% | 40-55% | > 55% |
Profit Factor важнее Win Rate: стратегия с 35% win rate, но avg_win в 3x avg_loss — прибыльна. Стратегия с 65% win rate, но avg_win в 0.5x avg_loss — убыточна. Риск-менеджмент должен быть встроен в стратегию, а не добавлен постфактум.
Никогда не запускайте стратегию live без прохождения walk-forward analysis. Хорошие результаты на одном периоде — может быть случайностью. Стабильность на множестве walk-forward окон — признак реального edge.
Что входит в нашу работу
Мы предоставляем:
- Исходный код бэктест-движка с поддержкой slippage, комиссий, частичных заполнений.
- Интеграцию с историческими данными (Binance, Coinbase, DEX-пулы).
- Документацию с описанием архитектуры и конфигурации.
- Отчёт с результатами испытаний (Sharpe, drawdown, profit factor).
- Обучение вашей команды работе с движком.
Наши инженеры имеют большой опыт в разработке торговых систем для DeFi. Мы гарантируем, что результаты бэктеста будут воспроизводимы в live-среде с точностью до комиссий. Стоимость бэктестера зависит от объёма данных и сложности стратегии, и начинается от $3,000.
Свяжитесь с нами, чтобы обсудить ваш проект — мы оценим задачу в течение 2 рабочих дней. А если хотите убедиться в качестве, закажите пилотный проект — вы получите полный бэктест одной стратегии с отчётом.







