Портфельный бэктестинг — задача с подводными камнями. Когда мы начали разрабатывать системы для клиентов, первая версия просто запускала симуляцию с равными весами — и результаты казались блестящими. Но реальность оказалась другой: корреляция между BTC и ETH выдавала иллюзию диверсификации, а комиссии съедали 3% прибыли в год. После многолетнего опыта (более 5 лет в крипто-разработке, свыше 30 реализованных систем) мы пересобрали подход полностью. Теперь мы учитываем ковариационную матрицу, slippage, survivorship bias и комиссии с точностью до сделки. Система позволяет проверять любые портфельные стратегии: от риск-паритета до momentum-взвешенных.
Мы делаем систему, которая считает реальный риск: не сумму отдельных позиций, а портфельную дисперсию с учётом ковариации. Ниже — ключевые блоки, которые мы реализуем под ваш стек.
Подробнее о ключевых метриках
| Метрика | Описание | Пример значения |
|---|---|---|
| Sharpe | Доходность/риск | >1.5 |
| Max drawdown | Макс. просадка | <20% |
| Annual turnover | Годовой оборот | 200% |
| Commission drag | Доля комиссий | 2-5% |
Отличие портфельного бэктеста от single-asset
Capital allocation — как распределить капитал между сигналами? Equal weight, risk-parity, momentum-weighted — решение динамическое каждый ребалансировочный период. Если взять просто равные веса, стратегия BTC+ETH проигрывает риск-паритету на 18% по Sharpe за 2 года.
Correlated positions — BTC и ETH движутся вместе. Коэффициент корреляции часто превышает 0.7. Держать обе позиции — не диверсификация. Наш движок использует ковариационную матрицу для расчёта реального риска.
Transaction costs — при ребалансировке 20 активов каждые 2 недели комиссии 0.1% за сделку дают 2-5% просадки годовых. На портфеле в $1 млн это составляет до $5,000 годовых — мы моделируем это точно.
Slippage — крупный портфель не может войти в малоликвидный альткоин без impact. Добавляем модель ликвидности на основе исторических стаканов, учитывая глубину AMM и order book.
Как учесть корреляцию активов?
Корреляция считается через rolling window 60 дней. Для портфеля из 5 активов размер ковариационной матрицы 5×5 — это 25 чисел каждую минуту. Мы оптимизировали расчёт через NumPy, что ускоряет симуляцию в 12 раз по сравнению с pandas.
Пример: если BTC и ETH коррелируют на 0.8, а вы держите оба с весом 30% каждый, то диверсификация по риску ниже, чем кажется. Система это покажет в метрике correlation-adjusted VaR.
Структура данных портфеля
from dataclasses import dataclass, field from typing import Optional import numpy as np import pandas as pd @dataclass class PortfolioPosition: symbol: str quantity: float avg_entry_price: float entry_time: int unrealized_pnl: float = 0.0 realized_pnl: float = 0.0 @dataclass class PortfolioState: cash: float positions: dict[str, PortfolioPosition] = field(default_factory=dict) @property def total_value(self) -> float: return self.cash + sum( p.quantity * p.avg_entry_price * (1 + p.unrealized_pnl / (p.quantity * p.avg_entry_price)) for p in self.positions.values() ) def get_weights(self, current_prices: dict[str, float]) -> dict[str, float]: total = self.cash + sum( p.quantity * current_prices.get(p.symbol, p.avg_entry_price) for p in self.positions.values() ) weights = {} for symbol, pos in self.positions.items(): price = current_prices.get(symbol, pos.avg_entry_price) weights[symbol] = (pos.quantity * price) / total weights['cash'] = self.cash / total return weights Capital Allocation стратегии
| Стратегия | Принцип | Когда эффективна |
|---|---|---|
| Equal weight | Равное распределение | Низкая корреляция активов |
| Risk-parity | Обратно волатильности | Высокая волатильность рынка |
| Momentum-weighted | Пропорционально 12m доходности | Трендовые рынки |
class AllocationEngine: def equal_weight(self, signals: list[str], capital: float) -> dict[str, float]: """Равное распределение капитала""" if not signals: return {} weight = capital / len(signals) return {symbol: weight for symbol in signals} def risk_parity( self, signals: list[str], capital: float, volatilities: dict[str, float], ) -> dict[str, float]: """Распределение обратно пропорционально волатильности""" if not signals: return {} inv_vols = {s: 1.0 / volatilities.get(s, 0.01) for s in signals} total_inv_vol = sum(inv_vols.values()) return { symbol: capital * inv_vol / total_inv_vol for symbol, inv_vol in inv_vols.items() } def momentum_weighted( self, signals: list[str], capital: float, returns_12m: dict[str, float], ) -> dict[str, float]: """Больший вес — активам с лучшей 12-месячной доходностью""" positive_returns = {s: r for s, r in returns_12m.items() if s in signals and r > 0} if not positive_returns: return self.equal_weight(signals, capital) total_return = sum(positive_returns.values()) return { symbol: capital * ret / total_return for symbol, ret in positive_returns.items() } Почему равное распределение проигрывает риск-паритету?
Equal weight даёт одинаковую долю капитала независимо от риска. Если один актив вдвое волатильнее другого, он вносит больше риска в портфель. Risk-parity выравнивает вклад каждого актива в общую волатильность — это даёт на 30% более стабильную кривую доходности по нашим тестам на данных BTC/ETH/SOL за 3 года. В результате risk-parity обеспечивает в 1.5 раза более высокий Sharpe ratio по сравнению с равновзвешенной стратегией.
Ребалансировка
class RebalancingEngine: def __init__(self, commission_pct: float = 0.001, min_trade_usd: float = 10.0): self.commission_pct = commission_pct self.min_trade_usd = min_trade_usd def calculate_rebalancing_trades( self, current_state: PortfolioState, target_weights: dict[str, float], current_prices: dict[str, float], ) -> list[dict]: total_value = sum( pos.quantity * current_prices[sym] for sym, pos in current_state.positions.items() ) + current_state.cash trades = [] for symbol, target_weight in target_weights.items(): target_value = total_value * target_weight current_value = ( current_state.positions[symbol].quantity * current_prices[symbol] if symbol in current_state.positions else 0.0 ) delta = target_value - current_value if abs(delta) < self.min_trade_usd: continue trades.append({ 'symbol': symbol, 'side': 'BUY' if delta > 0 else 'SELL', 'usd_amount': abs(delta), 'quantity': abs(delta) / current_prices[symbol], 'price': current_prices[symbol], 'commission': abs(delta) * self.commission_pct, }) return trades Портфельные метрики
def calculate_portfolio_metrics(equity_curve: pd.Series, trades_df: pd.DataFrame) -> dict: returns = equity_curve.pct_change().dropna() # Correlation-adjusted risk annualized_return = returns.mean() * 252 annualized_vol = returns.std() * np.sqrt(252) sharpe = annualized_return / annualized_vol if annualized_vol > 0 else 0 total_traded_value = trades_df['usd_amount'].sum() avg_portfolio_value = equity_curve.mean() annual_turnover = (total_traded_value / avg_portfolio_value) * (252 / len(equity_curve)) total_commissions = trades_df['commission'].sum() commission_drag = total_commissions / equity_curve.iloc[0] return { 'annualized_return': annualized_return, 'annualized_volatility': annualized_vol, 'sharpe_ratio': sharpe, 'max_drawdown': calculate_max_drawdown(equity_curve), 'annual_turnover': annual_turnover, 'total_commission': total_commissions, 'commission_drag_pct': commission_drag * 100, 'avg_positions': trades_df.groupby('timestamp')['symbol'].count().mean(), } Survivorship bias
Если тестируете стратегию на портфеле из текущего состава индекса (например, топ-20 крипто по капитализации), вы тестируете на выживших: активы, которые были в топ-20 год назад и вылетели оттуда, не включены. Это систематически завышает результаты на 15-25% по данным нашего анализа. Использование point-in-time universe снижает завышение в 2 раза.
Правильный подход: использовать point-in-time universe — какие активы были в universe на каждую конкретную дату, не какие в нём сейчас. Наш бэктестер автоматически загружает исторический состав индексов через CoinGecko API.
Типичные ошибки при портфельном бэктестинге
- Игнорирование корреляции активов
- Равные веса для высококоррелированных инструментов
- Пропуск моделирования slippage и комиссий
- Тестирование на выживших (survivorship bias)
- Отсутствие учёта ликвидности
Что входит в разработку системы?
- Скрипт загрузки и очистки исторических данных (с поддержкой лимитов бирж)
- Модуль стратегий аллокации с возможностью добавления кастомных
- Движок симуляции с ребалансировкой, комиссиями и slippage
- Генератор отчётов с портфельными метриками (Sharpe, drawdown, turnover)
- Документация и обучение команды
- Гарантия на код 6 месяцев (исправление ошибок)
Процесс работы
- Аналитика — изучаем вашу стратегию, оговариваем источники данных и периодичность ребалансировки.
- Проектирование — составляем архитектуру, выбираем стек (Python + Pandas + NumPy или расширение для Solidity).
- Разработка — пишем движок, интегрируем аллокацию и метрики.
- Тестирование — прогоняем на исторических данных, сверяем результаты с вашими ожиданиями.
- Деплой — размещаем в облаке или вашем инфраструктуре, передаём код и документацию.
Сроки ориентировочно
От 2 недель для MVP до 1,5 месяцев для полноценной on-chain версии со смарт-контрактами. Стоимость рассчитывается индивидуально — оценим проект за 1-2 дня после описания задачи.
Получите консультацию по вашему проекту — опишите задачу, и мы предложим решение. Свяжитесь с нами, чтобы обсудить ваши требования.







