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

When a backtest shows only the final return, it's easy to overlook hidden risks behind an attractive number. We develop professional reports with Sharpe, drawdown, and profit factor metrics that reveal the true robustness of a strategy. Our team delivers the project turnkey—from calculating indicators to an interactive dashboard—ensuring reliable analysis and ongoing support.

Blockchain Development Services

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1335
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1293
  • B2B Advance company logo design
    B2B Advance company logo design
    738
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1031
  • AIDER company logo development
    AIDER company logo development
    978
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1087

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')