Професійні звіти бектестів: метрики та дашборди
Зібрали бектест, отримали фінальну дохідність 340% — і що далі? Без детального звіту це просто число. Ми стикалися зі стратегіями, де win rate перевищував 80%, але стратегія зливала 60% капіталу за місяць. Тому що не рахували max drawdown і не аналізували розподіл угод. Розробка звітів з бектестів під ключ — це не просто виведення цифр, а система, яка виявляє приховані ризики, порівнює гіпотези та обґрунтовує рішення. Наш досвід — понад 5 років у криптотрейдингу та DeFi — дозволяє збирати звіти, які реально використовують у роботі. Ми автоматизували розрахунки метрик, щоб ви не витрачали час на ручні ексельки. Багато трейдерів втрачають гроші, покладаючись лише на P&L — а детальний звіт з risk-adjusted метриками дозволяє уникнути цього.
Чому стандартні брокерські звіти не підходять?
Стандартні брокерські звіти дають лише P&L та simple return. Для алготрейдингу потрібні risk-adjusted метрики: Sharpe, Sortino, drawdown duration, profit factor. Без них ви не зможете зрозуміти, чи переживе стратегія ринковий шок або серію збиткових угод. Ми використовуємо pandas, plotly та власні dataclass-и (див. код нижче). Весь пайплайн автоматизований — від завантаження угод до HTML-дашборду з інтерактивними графіками. Метрика Sharpe широко використовується у фінансах (Sharpe ratio). Ми гарантуємо, що кожен звіт проходить перевірку на консистентність.
Які підводні камені приховує типовий бектест?
Одного разу клієнт прийшов зі стратегією на Binance Futures, яка показувала Sharpe 2.3. Ми запустили звіт з коректним розрахунком drawdown — і виявили чотири занурення нижче -40%. Причина — стратегія не пережила б 2-3 невдалих угод поспіль. У звіті ми додали секцію "Max drawdown duration" та "Rolling Sharpe". Клієнт переробив логіку ризик-менеджменту — і через місяць Sharpe зріс до 3.8. Наш підхід у 3 рази швидший за ручний розрахунок в Excel.
Що входить у роботу
Ми постачаємо повний пакет:
- Інтерактивний HTML-дашборд (Plotly) з equity curve, drawdown, monthly returns heatmap, P&L distribution.
- Вихідні файли в JSON/CSV для завантаження у ваш ризик-менеджмент.
- Документація: інтерпретація кожного графіка та метрики.
- Навчання команди: як оновлювати звіт при зміні стратегії.
- Підтримка на етапі адаптації — допомагаємо інтегрувати звіт з вашим API.
Це не разова робота: ми залишаємося на зв'язку, щоб доопрацювати метрики під нові вимоги.
Склад звіту
- Дашборди: equity curve, drawdown, monthly returns heatmap, P&L distribution (на
plotlyабоmatplotlib). - Метрики: Sharpe, Sortino, Calmar, profit factor, win rate, average win/loss, trade duration.
- JSON/CSV/HTML формати під ваш ризик-менеджмент.
- Документація: як інтерпретувати кожен графік.
- Підтримка після здачі: ми допомагаємо адаптувати звіт під нові стратегії.
Як інтерпретувати метрики?
Порівняємо типові помилки. Високий win rate не рятує, якщо середній збиток у 3 рази більший за середній прибуток. Profit Factor (валовий прибуток / валовий збиток) має бути > 2. А Sortino Ratio кращий за Sharpe — він штрафує лише від'ємну волатильність. Наша таблиця інтерпретації:
| Метрика | Добре | Прийнятно | Погано |
|---|---|---|---|
| Sharpe Ratio | > 2.0 | 1.0–2.0 | < 1.0 |
| Sortino Ratio | > 2.5 | 1.5–2.5 | < 1.5 |
| Max Drawdown | < 15% | 15–30% | > 30% |
| Profit Factor | > 2.0 | 1.5–2.0 | < 1.5 |
| Win Rate | > 55% (для трендових) | 45–55% | < 45% |
Важливо: win rate без урахування risk/reward — пастка. Profit Factor та expectancy важливіші.
Порівняння форматів звіту
| Формат | Переваги | Недоліки |
|---|---|---|
| HTML (Plotly) | Інтерактивні графіки, легко ділитися | Не підходить для автоматичної обробки |
| JSON | Машиночитаний, інтеграція з API | Потребує візуалізації окремо |
| CSV | Універсальний, відкривається в Excel | Немає графіків, великі файли |
Як ми будуємо звіт: покроковий процес
- Завантаження даних — вивантаження угод з API або CSV.
- Розрахунок метрик — обчислення Sharpe, Sortino, drawdown та інших показників.
- Генерація графіків — equity curve, drawdown, monthly heatmap, P&L distribution.
- Валідація — звірка з вихідними даними, перевірка консистентності.
- Здача — HTML-дашборд + вихідні файли (JSON/CSV).
Зв'яжіться з нами для безкоштовної оцінки вашого проекту. Замовте розробку звіту вже сьогодні та отримайте консультацію з оптимізації метрик.
Подивитися код
from dataclasses import dataclass
from typing import Optional
import pandas as pd
import numpy as np
@dataclass
class BacktestReport:
# Зведені метрики
initial_capital: float
final_capital: float
total_return_pct: float
annual_return_pct: float
# Risk-adjusted
sharpe_ratio: float
sortino_ratio: float
calmar_ratio: float
# Drawdown
max_drawdown_pct: float
avg_drawdown_pct: float
max_drawdown_duration_days: int
# Торгівля
total_trades: int
win_rate: float
profit_factor: float
avg_win_pct: float
avg_loss_pct: float
best_trade_pct: float
worst_trade_pct: float
avg_trade_duration_hours: float
# Комісії
total_commission: float
commission_as_pct_of_pnl: float
# Часові серії
equity_curve: pd.Series
monthly_returns: pd.DataFrame
trade_list: pd.DataFrame
def compute_all_metrics(equity_curve: pd.Series, trades: list[dict]) -> BacktestReport:
returns = equity_curve.pct_change().dropna()
annual_factor = 252
# Базові
total_return = (equity_curve.iloc[-1] / equity_curve.iloc[0]) - 1
days = (equity_curve.index[-1] - equity_curve.index[0]).days
annual_return = (1 + total_return) ** (365 / max(days, 1)) - 1
# Sharpe (risk-free rate = 0 для крипто)
sharpe = (returns.mean() * annual_factor) / (returns.std() * np.sqrt(annual_factor)) if returns.std() > 0 else 0
# Sortino (лише downside volatility)
downside = returns[returns < 0].std()
sortino = (returns.mean() * annual_factor) / (downside * np.sqrt(annual_factor)) if downside > 0 else 0
# Drawdown
rolling_max = equity_curve.cummax()
drawdown_series = (equity_curve - rolling_max) / rolling_max
max_dd = drawdown_series.min()
# Max drawdown duration
in_drawdown = drawdown_series < 0
dd_start = None
max_duration = 0
for date, is_dd in in_drawdown.items():
if is_dd and dd_start is None:
dd_start = date
elif not is_dd and dd_start is not None:
duration = (date - dd_start).days
max_duration = max(max_duration, duration)
dd_start = None
# Calmar
calmar = annual_return / abs(max_dd) if max_dd != 0 else 0
# Trade-level
trades_df = pd.DataFrame(trades)
if not trades_df.empty:
winning = trades_df[trades_df['pnl'] > 0]
losing = trades_df[trades_df['pnl'] < 0]
win_rate = len(winning) / len(trades_df)
gross_profit = winning['pnl'].sum()
gross_loss = abs(losing['pnl'].sum())
profit_factor = gross_profit / gross_loss if gross_loss > 0 else float('inf')
avg_win_pct = (winning['pnl'] / winning['entry_value'] * 100).mean() if not winning.empty else 0
avg_loss_pct = (losing['pnl'] / losing['entry_value'] * 100).mean() if not losing.empty else 0
else:
win_rate = profit_factor = avg_win_pct = avg_loss_pct = 0
return BacktestReport(
initial_capital=equity_curve.iloc[0],
final_capital=equity_curve.iloc[-1],
total_return_pct=total_return * 100,
annual_return_pct=annual_return * 100,
sharpe_ratio=round(sharpe, 3),
sortino_ratio=round(sortino, 3),
calmar_ratio=round(calmar, 3),
max_drawdown_pct=max_dd * 100,
avg_drawdown_pct=drawdown_series[drawdown_series < 0].mean() * 100,
max_drawdown_duration_days=max_duration,
total_trades=len(trades),
win_rate=win_rate,
profit_factor=profit_factor,
avg_win_pct=avg_win_pct,
avg_loss_pct=avg_loss_pct,
equity_curve=equity_curve,
trade_list=trades_df,
)
def compute_monthly_returns(equity_curve: pd.Series) -> pd.DataFrame:
"""Створюємо матрицю місячних дохідностей для heatmap"""
monthly = equity_curve.resample('ME').last()
monthly_returns = monthly.pct_change().dropna()
# Створюємо матрицю рік × місяць
matrix = monthly_returns.groupby([
monthly_returns.index.year,
monthly_returns.index.month
]).first().unstack()
matrix.columns = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
return matrix * 100 # у відсотках
def generate_html_report(report: BacktestReport, strategy_name: str) -> str:
import plotly.graph_objects as go
from plotly.subplots import make_subplots
fig = make_subplots(
rows=3, cols=2,
subplot_titles=['Equity Curve', 'Drawdown', 'Monthly Returns', 'Trade P&L Distribution', 'Win/Loss', 'Rolling Sharpe'],
)
# Equity curve
fig.add_trace(go.Scatter(x=report.equity_curve.index, y=report.equity_curve.values,
name='Portfolio', line=dict(color='#00C853')), row=1, col=1)
# Drawdown
rolling_max = report.equity_curve.cummax()
drawdown = (report.equity_curve - rolling_max) / rolling_max * 100
fig.add_trace(go.Scatter(x=drawdown.index, y=drawdown.values,
fill='tozeroy', name='Drawdown', line=dict(color='#FF5252')), row=1, col=2)
# P&L distribution
if not report.trade_list.empty:
pnl_pct = report.trade_list['pnl'] / report.trade_list['entry_value'] * 100
fig.add_trace(go.Histogram(x=pnl_pct, name='Trade P&L %', nbinsx=30), row=2, col=1)
fig.update_layout(
title=f'Backtest Report: {strategy_name}',
height=1000,
showlegend=False,
template='plotly_dark',
)
return fig.to_html(include_plotlyjs='cdn')







