Професійні звіти бектестів: метрики та дашборди

Професійні звіти бектестів: метрики та дашборди Зібрали бектест, отримали фінальну дохідність 340% — і що далі? Без детального звіту це просто число. Ми стикалися зі стратегіями, де win rate перевищував 80%, але стратегія зливала 60% капіталу за місяць. Тому що не рахували max drawdown і не аналі

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

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

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

  • 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

Професійні звіти бектестів: метрики та дашборди

Зібрали бектест, отримали фінальну дохідність 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 Немає графіків, великі файли

Як ми будуємо звіт: покроковий процес

  1. Завантаження даних — вивантаження угод з API або CSV.
  2. Розрахунок метрик — обчислення Sharpe, Sortino, drawdown та інших показників.
  3. Генерація графіків — equity curve, drawdown, monthly heatmap, P&L distribution.
  4. Валідація — звірка з вихідними даними, перевірка консистентності.
  5. Здача — 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')