Розробка системи бектестування портфельних криптостратегій

Портфельний бектестинг — завдання з підводними каменями. Коли ми почали розробляти системи для клієнтів, перша версія просто запускала симуляцію з рівними вагами — і результати здавалися блискучими. Але реальність виявилася іншою: кореляція між BTC та ETH створювала ілюзію диверсифікації, а комісії

Напрямки блокчейн-розробки

Часті запитання

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1451
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1309
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1005
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1270
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    719
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    1011

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

Процес роботи

  1. Аналітика — вивчаємо вашу стратегію, обговорюємо джерела даних та періодичність ребалансування.
  2. Проектування — складаємо архітектуру, обираємо стек (Python + Pandas + NumPy або розширення для Solidity).
  3. Розробка — пишемо рушій, інтегруємо алокацію та метрики.
  4. Тестування — прогоняємо на історичних даних, звіряємо результати з вашими очікуваннями.
  5. Деплой — розміщуємо в хмарі або вашій інфраструктурі, передаємо код та документацію.

Строки орієнтовно

Від 2 тижнів для MVP до 1,5 місяців для повноцінної on-chain версії зі смарт-контрактами. Вартість розраховується індивідуально — оцінимо проект за 1-2 дні після опису завдання.

Отримайте консультацію по вашому проекту — опишіть задачу, і ми запропонуємо рішення. Зв'яжіться з нами, щоб обговорити ваші вимоги.