Портфельный бэктестинг — задача с подводными камнями. Когда мы начали разрабатывать системы для клиентов, первая версия просто запускала симуляцию с равными весами — и результаты казались блестящими. Но реальность оказалась другой: корреляция между 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 дня после описания задачи.
Получите консультацию по вашему проекту — опишите задачу, и мы предложим решение. Свяжитесь с нами, чтобы обсудить ваши требования.







