Backtest Report Development: Sharpe, Drawdown, P&L Metrics

Professional Backtest Reports: Metrics and Dashboards You ran a backtest, got a final return of 340% — now what? Without a detailed report, it's just a number. We've seen strategies with a win rate over 80% that still lost 60% of capital in a month — because max drawdown wasn't calculated and tra

Blockchain Development Services

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1451
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1309
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    1005
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1270
  • image_logo-advance_0.webp
    B2B Advance company logo design
    719
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    1011

Professional Backtest Reports: Metrics and Dashboards

You ran a backtest, got a final return of 340% — now what? Without a detailed report, it's just a number. We've seen strategies with a win rate over 80% that still lost 60% of capital in a month — because max drawdown wasn't calculated and trade distribution wasn't analyzed. Developing turnkey backtest reports isn't about spitting out numbers; it's a system that uncovers hidden risks, compares hypotheses, and justifies decisions. Our experience — over 5 years in crypto trading and DeFi — allows us to deliver reports that are actually used in practice. We automated metric calculations so you don't waste time on manual spreadsheets. Many traders lose money relying solely on P&L — a detailed report with risk-adjusted metrics prevents that.

Why Standard Broker Reports Aren't Suitable

Standard broker reports only provide P&L and simple return. For algorithmic trading, you need risk-adjusted metrics: Sharpe, Sortino, drawdown duration, profit factor. Without them, you can't know if a strategy will survive a market shock or a losing streak. We use pandas, plotly, and our own dataclasses (see code below). The entire pipeline is automated — from trade import to an interactive HTML dashboard. The Sharpe ratio is widely used in finance (Sharpe ratio). We ensure every report passes a consistency check.

What Hidden Pitfalls Does a Typical Backtest Mask?

One client came with a Binance Futures strategy showing a Sharpe of 2.3. We ran a report with correct drawdown calculation — and found four drawdowns below -40%. The reason: the strategy wouldn't survive 2-3 consecutive losing trades. In the report, we added a "Max Drawdown Duration" and "Rolling Sharpe" section. The client reworked their risk management logic — and within a month the Sharpe rose to 3.8. Our approach is 3x faster than manual Excel calculations. With over 5 years of experience and 50+ completed projects, we deliver reliable reports. Our team of certified analysts guarantees accurate metric calculation.

What's Included

We deliver a complete package:

  • Interactive HTML dashboard (Plotly) with equity curve, drawdown, monthly returns heatmap, P&L distribution.
  • Source files in JSON/CSV for your risk management system.
  • Documentation: interpretation of every chart and metric.
  • Team training: how to update the report when strategy changes.
  • Adaptation support: we help integrate the report with your API.

This isn't a one-off job: we stay in touch to refine metrics as new requirements arise. Report development starts at $1,500 and includes a free project evaluation.

Report Composition

  • Dashboards: equity curve, drawdown, monthly returns heatmap, P&L distribution (built with plotly or matplotlib).
  • Metrics: Sharpe, Sortino, Calmar, profit factor, win rate, average win/loss, trade duration.
  • Formats: JSON, CSV, HTML for your risk management.
  • Documentation: how to interpret each chart.
  • Post-delivery support: we help adapt the report for new strategies.

How to Interpret Metrics?

Let's compare common mistakes. A high win rate won't save you if the average loss is 3x the average gain. Profit Factor (gross profit / gross loss) should be > 2. Sortino Ratio is better than Sharpe — it penalizes only negative volatility. Our interpretation table:

Metric Good Acceptable Poor
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% (trend-following) 45–55% < 45%

Important: win rate without risk/reward is a trap. Profit Factor and expectancy matter more. For advanced strategy analysis, we also incorporate walk-forward optimization and Monte Carlo simulation.

Report Format Comparison

Format Advantages Disadvantages
HTML (Plotly) Interactive charts, easy to share Not for automated processing
JSON Machine-readable, API integration Needs separate visualization
CSV Universal, opens in Excel No charts, large files

How We Build the Report: Step-by-Step

  1. Data import — trade export from API or CSV.
  2. Metric calculation — compute Sharpe, Sortino, drawdown, and other indicators.
  3. Chart generation — equity curve, drawdown, monthly heatmap, P&L distribution.
  4. Validation — cross-check with source data, consistency verification.
  5. Delivery — HTML dashboard + source files (JSON/CSV).

Contact us for a free project evaluation. Order your report today and get a consultation on metric optimization.

View Code
from dataclasses import dataclass
from typing import Optional
import pandas as pd
import numpy as np

@dataclass
class BacktestReport:
    # Summary metrics
    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
    
    # Trading
    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
    
    # Fees
    total_commission: float
    commission_as_pct_of_pnl: float
    
    # Time series
    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

    # Basic
    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 for crypto)
    sharpe = (returns.mean() * annual_factor) / (returns.std() * np.sqrt(annual_factor)) if returns.std() > 0 else 0

    # Sortino (only 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:
    """Create monthly returns matrix for heatmap"""
    monthly = equity_curve.resample('ME').last()
    monthly_returns = monthly.pct_change().dropna()

    # Create year × month matrix
    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  # in percent
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')