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
plotlyormatplotlib). - 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
- Data import — trade export from API or CSV.
- Metric calculation — compute Sharpe, Sortino, drawdown, and other indicators.
- Chart generation — equity curve, drawdown, monthly heatmap, P&L distribution.
- Validation — cross-check with source data, consistency verification.
- 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')







