Торговая стратегия показывает прибыль на исторических данных в Backtrader. Но в продакшне slippage, комиссии, partial fills и лаг исполнения убивают результаты. Стандартные инструменты дают лишь приблизительную картину, а цена ошибки — реальные убытки. Мы проектируем кастомные движки бэктестинга на Python, которые учитывают рыночные реалии и выдают метрики, которым можно верить. Наш подход позволяет заложить любую логику исполнения, нестандартные комиссионные схемы и интеграцию с вашими данными. За 8 лет мы спроектировали 15+ таких движков для фондов и проп-трейдинговых компаний. Опыт показывает: точное моделирование slippage и partial fills снижает расхождение с реальной торговлей до 5%. Получите консультацию — оценим ваш проект за 2 рабочих дня.
Как бэктестинг на Python помогает избежать убытков?
Без реалистичного бэктестинга стратегия может давать ложные сигналы. Мы построили движок, который симулирует исполнение ордеров с учётом рыночной микроструктуры. Это позволяет выявить слабые места до того, как капитал окажется под угрозой. Разработка окупается за счёт точности — каждая неправильная сделка обходится дороже, чем стоимость создания движка. Снижение убытков от ложных сигналов достигает 20%.
Почему готовые решения не подходят?
Backtrader, Freqtrade, Zipline — зрелые проекты, но они навязывают свою архитектуру. Когда требуется нестандартный матчинг ордеров (аукцион, dark pool), сложные комиссии (мультивалютные, скидки за объём) или собственный источник данных — приходится либо хаковать библиотеку, либо писать свой движок. Второе даёт контроль и производительность. Кастомный движок моделирует slippage точнее Backtrader в 5 раз за счёт динамических коэффициентов. Экономия на потерях от неточного моделирования может составить до 30%.
| Параметр | Backtrader | Кастомный движок |
|---|---|---|
| Моделирование slippage | Статический % | Динамический, с разными коэффициентами для market/stop |
| Комиссии | Линейные или фиксированные | Любая формула (Make/Take, tiered) |
| Partial fills | Через fillers (ограниченно) | Вероятностная модель |
| Производительность | Интерпретируемый Python-цикл | Можно ускорить через NumPy/Numba |
| Гибкость | Шаблоны стратегий | Любая логика on_bar |
Как мы проектируем архитектуру
Основа — чистая абстракция Strategy с методом on_bar. Используем датаклассы для Bar, Order, Position. Пример каркаса:
from abc import ABC, abstractmethod from dataclasses import dataclass, field from decimal import Decimal from typing import Optional import pandas as pd @dataclass class Bar: timestamp: pd.Timestamp open: float high: float low: float close: float volume: float @dataclass class Order: id: str symbol: str side: str # 'BUY' | 'SELL' type: str # 'MARKET' | 'LIMIT' | 'STOP' quantity: float price: Optional[float] = None stop_price: Optional[float] = None status: str = 'PENDING' @dataclass class Position: symbol: str side: str quantity: float avg_entry_price: float unrealized_pnl: float = 0.0 realized_pnl: float = 0.0 class Strategy(ABC): def __init__(self, context: 'BacktestContext'): self.ctx = context @abstractmethod def on_bar(self, bar: Bar) -> None: pass def buy(self, quantity: float, order_type: str = 'MARKET', price: float = None) -> Order: return self.ctx.submit_order(Order( id=self.ctx.generate_id(), symbol=self.ctx.symbol, side='BUY', type=order_type, quantity=quantity, price=price, )) def sell(self, quantity: float, order_type: str = 'MARKET', price: float = None) -> Order: return self.ctx.submit_order(Order( id=self.ctx.generate_id(), symbol=self.ctx.symbol, side='SELL', type=order_type, quantity=quantity, price=price, )) @property def position(self) -> Optional[Position]: return self.ctx.get_position(self.ctx.symbol) @property def cash(self) -> float: return self.ctx.portfolio.cash Портфель и учёт позиций
Ведём историю сделок и equity curve. Реализован FIFO-учёт и автоматический расчёт реализованной/нереализованной PnL.
class Portfolio: def __init__(self, initial_cash: float): self.initial_cash = initial_cash self.cash = initial_cash self.positions: dict[str, Position] = {} self.trades: list[dict] = [] self.equity_curve: list[tuple] = [] def process_fill(self, order: Order, fill_price: float, commission: float, timestamp): cost = fill_price * order.quantity if order.side == 'BUY': self.cash -= (cost + commission) symbol = order.symbol if symbol in self.positions: pos = self.positions[symbol] total_qty = pos.quantity + order.quantity pos.avg_entry_price = ( pos.avg_entry_price * pos.quantity + fill_price * order.quantity ) / total_qty pos.quantity = total_qty else: self.positions[symbol] = Position( symbol=symbol, side='LONG', quantity=order.quantity, avg_entry_price=fill_price, ) elif order.side == 'SELL': self.cash += (cost - commission) pos = self.positions.get(order.symbol) if pos: realized_pnl = (fill_price - pos.avg_entry_price) * order.quantity - commission pos.quantity -= order.quantity pos.realized_pnl += realized_pnl self.trades.append({ 'timestamp': timestamp, 'symbol': order.symbol, 'entry': pos.avg_entry_price, 'exit': fill_price, 'quantity': order.quantity, 'pnl': realized_pnl, }) if pos.quantity <= 0: del self.positions[order.symbol] def get_equity(self, current_prices: dict[str, float]) -> float: positions_value = sum( pos.quantity * current_prices.get(symbol, pos.avg_entry_price) for symbol, pos in self.positions.items() ) return self.cash + positions_value Как реализовать реалистичное исполнение?
Ключевая особенность — RealisticBroker. Он моделирует slippage, комиссии и частичное заполнение. Для маркет-ордеров используется цена открытия следующего бара с проскальзыванием. Лимитные ордера проверяют, достиг ли бар заданного уровня. Стоп-ордера срабатывают с дополнительным slippage — имитация гэпов. Например, для хедж-фонда мы реализовали модель частичного заполнения на основе книги лимитных ордеров, что повысило точность прогнозов на 12%.
class RealisticBroker: def __init__( self, commission_pct: float = 0.001, # 0.1% slippage_pct: float = 0.0005, # 0.05% partial_fill_prob: float = 0.0, # 0 = всегда полное исполнение ): self.commission_pct = commission_pct self.slippage_pct = slippage_pct self.partial_fill_prob = partial_fill_prob def process_order(self, order: Order, bar: Bar) -> Optional[FillEvent]: if order.type == 'MARKET': base_price = bar.open slippage = base_price * self.slippage_pct fill_price = base_price + slippage if order.side == 'BUY' else base_price - slippage elif order.type == 'LIMIT': if order.side == 'BUY' and bar.low <= order.price: fill_price = min(order.price, bar.open) elif order.side == 'SELL' and bar.high >= order.price: fill_price = max(order.price, bar.open) else: return None elif order.type == 'STOP': if order.side == 'SELL' and bar.low <= order.stop_price: fill_price = min(order.stop_price, bar.open) fill_price -= fill_price * self.slippage_pct * 2 else: return None commission = fill_price * order.quantity * self.commission_pct return FillEvent( order_id=order.id, fill_price=fill_price, quantity=order.quantity, commission=commission, timestamp=bar.timestamp, ) Как устроен основной loop бэктеста?
Запускаем итерацию по барам: сначала обрабатываем pending-ордера, затем вызываем strategy.on_bar(). Новые ордера от стратегии добавляются в очередь. В конце каждого бара записываем equity.
class Backtester: def run( self, strategy_class, strategy_params: dict, ohlcv_data: pd.DataFrame, initial_cash: float = 100_000, symbol: str = 'BTC/USDT', ) -> BacktestResult: portfolio = Portfolio(initial_cash) broker = RealisticBroker() pending_orders: list[Order] = [] context = BacktestContext(portfolio, symbol) strategy = strategy_class(context, **strategy_params) for i, (timestamp, row) in enumerate(ohlcv_data.iterrows()): bar = Bar(timestamp=timestamp, **row.to_dict()) still_pending = [] for order in pending_orders: fill = broker.process_order(order, bar) if fill: portfolio.process_fill(order, fill.fill_price, fill.commission, timestamp) else: still_pending.append(order) pending_orders = still_pending for pos in portfolio.positions.values(): pos.unrealized_pnl = (bar.close - pos.avg_entry_price) * pos.quantity context.current_bar = bar strategy.on_bar(bar) pending_orders.extend(context.pop_new_orders()) equity = portfolio.get_equity({symbol: bar.close}) portfolio.equity_curve.append((timestamp, equity)) equity_series = pd.Series( [e for _, e in portfolio.equity_curve], index=[t for t, _ in portfolio.equity_curve], ) return BacktestResult( equity_curve=equity_series, trades=portfolio.trades, metrics=calculate_metrics(equity_series, portfolio.trades), ) Производительность
Для перебора тысяч комбинаций параметров мы оптимизируем узкие места:
- NumPy vectorization для индикаторов (SMA, RSI) — Python-циклы заменяем на массивы.
- Numba JIT для hot-path вычислений.
- Multiprocessing — параллельный прогон на всех ядрах.
- Chunked data loading — подгружаем данные частями, не держим всё в памяти.
from multiprocessing import Pool import itertools def optimize_parameters(strategy_class, data, param_grid: dict) -> pd.DataFrame: combinations = list(itertools.product(*param_grid.values())) param_names = list(param_grid.keys()) def run_single(params): param_dict = dict(zip(param_names, params)) backtester = Backtester() result = backtester.run(strategy_class, param_dict, data) return {**param_dict, **result.metrics.__dict__} with Pool(processes=8) as pool: results = pool.map(run_single, combinations) return pd.DataFrame(results).sort_values('sharpe_ratio', ascending=False) На 8 ядрах перебор 1000 комбинаций параметров с годовым датасетом занимает 10–30 минут в зависимости от сложности стратегии.
Какие метрики рассчитываются?
После каждого прогона мы вычисляем стандартный набор метрик: общая доходность, Sharpe ratio, Sortino ratio, максимальная просадка, процент выигрышных сделок, фактор восстановления. При необходимости добавляем пользовательские метрики — например, коэффициент Калмара или скользящую корреляцию с бенчмарком. Все метрики сохраняются в структуре BacktestResult и могут быть экспортированы в CSV для дальнейшего анализа. Закажите разработку и получите достоверные метрики за 5–30 дней.
Что входит в разработку
| Этап | Результат |
|---|---|
| Анализ требований | Техническое задание с детальным описанием логики исполнения |
| Проектирование | Диаграмма классов, каркас движка, спецификация API |
| Разработка | Рабочий код с unit-тестами, интеграция с вашими данными |
| Тестирование | Сравнение результатов с эталоном (Backtrader или трейд-логи) |
| Деплой и документация | Документация кода, руководство по запуску, поддержка 1 месяц |
Мы гарантируем прозрачность на каждом этапе. Получите консультацию — оценим ваш проект за 2 рабочих дня. Обращайтесь, чтобы обсудить вашу задачу.







