Stats glossary

Backtest.show_stats() and qtrade.utils.stats.calculate_stats() print a dict with ~25 metrics. This page explains what each one means, how it’s computed, and a rough guide to interpretation.

For per-asset breakdowns in a portfolio backtest, see calculate_stats_per_asset(broker) — it returns the trade-level subset of these metrics keyed by asset symbol.

Time / value

Metric

Meaning

Start

First bar timestamp (from equity_history.index[0]).

End

Last bar processed (broker.current_time).

Duration

End - Start as a pd.Timedelta.

Start Value

Initial equity (typically equal to starting cash).

End Value

Equity at the end of the backtest.

Returns

Metric

Formula

Notes

Total Return [%]

(End - Start) / Start × 100

Raw, not annualized.

Total Commission Cost[%]

Sum of all commissions paid over the run

Helpful for spotting strategies that bleed on fees.

Buy & Hold Return [%]

(close[-1] - close[0]) / close[0] × 100 for the asset (single) or equal-weighted average across assets (multi)

Benchmark to beat. Strategies losing to B&H typically aren’t worth running over passive holding.

Return (Ann.) [%]

Geometric mean of daily returns, annualized to 252 (stocks) or 365 (crypto/FX) days

Compounded annual growth rate.

Volatility (Ann.) [%]

std(daily_returns) × √annual_days × 100

Standard deviation of returns, scaled to a year.

The 252 vs 365 annualization factor is auto-detected: if the equity curve includes weekends as trading days (>≈ 60% of weekend bars are filled), it’s treated as 365-day; otherwise 252.

Risk

Metric

Formula

Notes

Max Drawdown [%]

min((equity - cummax(equity)) / cummax(equity)) × 100

The biggest peak-to-trough decline. Always ≤ 0.

Max Drawdown Duration

Length of the longest contiguous “below peak” period

Returned as a pd.Timedelta. The drawdown that took longest to recover from, not the deepest one.

Trade statistics

Metric

Notes

Total Trades

Count of closed trades across all assets.

Win Rate [%]

Percentage of closed trades with positive profit.

Best Trade [%]

Largest single trade profit (in account currency, not %). The label says “[%]” for legacy reasons — it’s a P&L value.

Worst Trade [%]

Same, but the most negative profit.

Avg Winning Trade [%]

Mean profit of winning trades.

Avg Losing Trade [%]

Mean profit of losing trades (negative).

Avg Winning Trade Duration

Mean time-in-market for winners.

Avg Losing Trade Duration

Mean time-in-market for losers.

If “winners hold longer than losers” you have a “let winners run, cut losers short” pattern. Reverse means you’re cutting winners early.

Performance ratios

These compress return-and-risk into a single number. Different ratios penalize different things.

Profit Factor

sum(wins) / sum(|losses|)

Higher is better. > 1 means you’re net positive. > 2 is rare and good. NaN if there are no losing trades.

Expectancy

(sum(wins) - sum(|losses|)) / total_trades

Average dollar profit per trade. Positive expectancy means each trade is worth taking on average.

Sharpe Ratio

mean(daily_returns) / std(daily_returns) × √annual_days

Risk-free rate is assumed to be 0 (override by editing __calculate_performance_ratios in stats.py if you need otherwise). Rough guide:

  • < 0: losing strategy.

  • 0–1: marginal; might be in-sample noise.

  • 1–2: solid.

  • > 2: excellent (and usually too good — check for overfitting or look-ahead bias).

Sortino Ratio

mean(daily_returns) / std(daily_returns[< 0]) × √annual_days

Like Sharpe, but only penalizes downside volatility. Strategies with asymmetric return distributions (most positive trend-followers) score better here than on Sharpe.

Calmar Ratio

abs(annualized_return) / abs(max_drawdown)

How much annual return per unit of worst-case drawdown. > 1 is decent (your annual gains exceed your worst dip).

Omega Ratio

sum(returns > threshold) / |sum(returns < threshold)|

Threshold is 0 (so: gains divided by losses, summed across all daily return observations). Captures the full distribution of returns rather than mean/std summary. > 1 is positive.

Sanity-checking your stats

A few things to verify before getting excited about a backtest:

  • Total Trades is reasonable — single-digit trade counts can produce Sharpe = 5 by accident. You want enough trades for the metrics to be statistically meaningful.

  • Total Return > Buy & Hold — beating B&H is the bar. Losing to it on a long-only strategy means your timing is hurting more than helping.

  • Win Rate × Avg Win > (1 - Win Rate) × |Avg Loss| — the basic expectancy condition. If this fails, your strategy is structurally losing.

  • Use walk_forward_optimize before trusting optimize results — in-sample Sharpe is almost always too high. See Walk-forward optimization.