Quant Buffet API

Metrics API

compute_metrics: every output field, the exact formulas, and benchmark-relative stats.

`backtest.metrics.compute_metrics` turns an equity curve into the statistics shown in the lab UI.

Signature

python
metrics = compute_metrics(
    result.equity,               # pd.Series, required, positional
    benchmark=spy_buy_and_hold,  # optional pd.Series, reindexed + ffilled for you
    risk_free=0.0,               # annualised, subtracted before Sharpe
    trades_count=len(result.trades),
)
# keys: start, end, years, start_equity, end_equity, total_return, cagr,
#       volatility, sharpe, sortino, max_drawdown, calmar, daily_win_rate,
#       trades (+ benchmark_total_return, benchmark_cagr, alpha, beta)
ParameterTypeDefaultNotes
equitypd.SeriesPositional. NaNs dropped, cast to float.
benchmarkpd.Series | NoneNoneKeyword-only. Reindexed onto equity and forward-filled.
risk_freefloat0.0Keyword-only. Annualised; de-annualised internally.
trades_countint0Keyword-only. Passed straight through to output.

Core output fields

KeyTypeHow it is computed
start, endstrFirst and last equity dates as YYYY-MM-DD.
yearsfloatCalendar span in days / 365.25, rounded to 2 dp.
start_equity, end_equityfloatFirst and last equity values.
total_returnfloatend / start − 1, not annualised.
cagrfloat(end / start) ** (1 / years) − 1.
volatilityfloatDaily stdev (ddof=1) × √(annualisation factor).
sharpefloatMean excess return / stdev of excess return × √factor.
sortinofloatAnnualised mean return / annualised downside deviation.
max_drawdownfloatMinimum of equity / equity.cummax() − 1; negative.
calmarfloatcagr / abs(max_drawdown), or 0.0 if never in drawdown.
daily_win_ratefloatShare of days with a positive return.
tradesintEcho of trades_count.

The annualisation factor

Rather than hardcoding 252, the module infers the factor from your own index: len(index) / (span in days / 365.25). A pure ETF panel yields roughly 252; a crypto panel with weekend rows yields roughly 365. This keeps Sharpe comparable across asset types, but it also means a panel mixing both lands somewhere in between.

Panel contentApprox. factor
US ETFs only≈ 252
Crypto only≈ 365
ETFs + crypto mixed≈ 365 (crypto rows dominate the calendar)
Fewer than 2 rows252 (fallback)

Benchmark fields

Supplied only when benchmark is given and at least 6 aligned rows survive the join:

KeyHow it is computed
benchmark_total_returnBenchmark end / start − 1.
benchmark_cagrAnnualised with your years, for a like-for-like comparison.
betacov(strategy, benchmark) / var(benchmark) on daily returns.
alpha(mean(strategy) − beta × mean(benchmark)) × factor — annualised Jensen's alpha.
python
# How the lab builds its benchmark: buy-and-hold, scaled to your starting equity
bench_sym = "SPY" if "SPY" in prices.columns else ASSETS[0]
spy = prices[bench_sym].reindex(result.equity.index).ffill()
buy_hold = float(result.equity.iloc[0]) * (spy / spy.iloc[0])

metrics = compute_metrics(result.equity, benchmark=buy_hold,
                          trades_count=len(result.trades))

Edge cases

SituationResult
Fewer than 5 equity points{"error": "insufficient equity points"} and nothing else
Zero-variance returns (never traded)sharpe and sortino return 0.0 rather than dividing by zero
No losing dayssortino is 0.0, because downside deviation is zero
Equity never below its peakmax_drawdown is 0.0 and calmar is 0.0
Benchmark with under 6 aligned rowsBenchmark keys are omitted entirely

How the UI formats these

UI labelMetric keyFormatting
Annualised returncagrPercentage, 2 dp
VolatilityvolatilityPercentage, 2 dp
Sharpe ratiosharpeNumber, 2 dp
Sortino ratiosortinoNumber, 2 dp
Max drawdownmax_drawdownPercentage, 2 dp (negative)
BetabetaNumber, 2 dp
AlphaalphaPercentage, 2 dp
Win ratedaily_win_ratePercentage, 2 dp