vike
Analytics

Benchmark

Beta, alpha, correlation, tracking error, information ratio, the capture ratios and Treynor over two aligned equity curves — and the length rule every one of them enforces.

crates/vike-analytics/src/benchmark.rs answers one question: how did a strategy's equity curve behave relative to another curve — an index, a buy-and-hold, a second strategy. Every function takes two curves, strat_eq and bench_eq, and most take the annualization factor (periods_per_year, the same one the report uses) and a risk-free rate rf.

The one rule: the curves must be the same length

Every public function begins by asserting the two curves have equal length and panics with equity curves must be the same length (got a vs b) when they do not. The doc on beta records where that came from: the ported module raised a ValueError there, and the panic mirrors it. The module header is careful about what "port of" means now — provenance, not a standing obligation: the old implementation is retired, no exporter survives, and no test in this crate compares these functions against it. The parity gate page carries that decision.

So align first. The module's internal per-step returns is length-preserving: it pads a zero-denominator step with 0.0 rather than skipping it, and its doc says why — unlike metrics::returns, benchmark comparisons pair two curves element-wise and must not shorten.

The statistics

functiondefinition (from its doc)degenerate answer
beta(s, b)cov(rs, rb) / var(rb)0.0 when the benchmark has zero variance
alpha(s, b, ppy, rf)annualised Jensen's alpha: CAGR_s − [rf + β · (CAGR_b − rf)]—
correlation(s, b)Pearson correlation of per-step returns0.0 when either series has zero variance
r_squared(s, b)correlation²—
tracking_error(s, b, ppy)annualised standard deviation of the return differential rs − rb0.0 below 2 returns
information_ratio(s, b, ppy)annualised mean(rs − rb) / std(rs − rb)0.0 when tracking error is zero
up_capture(s, b)mean of rs on the bars where rb > 0, over the mean of rb on those bars0.0 when the benchmark has no positive-return bars
down_capture(s, b)mean of rs on the bars where rb < 0, over the mean of rb on those bars0.0 when the benchmark has no negative-return bars
treynor_ratio(s, b, ppy, rf)(annualized_return − rf_annual) / β0.0 below 2 aligned returns, non-positive growth, or β = 0

benchmark_stats(s, b, ppy, rf) computes the first eight in one pass and returns them as a BenchmarkStats struct with one field per statistic — beta, alpha, correlation, r_squared, tracking_error, information_ratio, up_capture, down_capture. Treynor is a separate call.

The variance under beta, correlation and the tracking-error family is the same metrics::mean_variance the rest of the crate uses (sample variance, ddof = 1), and the covariance folds through vike_model::py_sum, so the summation discipline on the metrics page applies here unchanged.

Treynor, stated fully

treynor_ratio's doc spells out its terms because they differ from the others'. The numerator's annualized_return is the geometric annualization of the strategy's own per-step returns — the product of 1 + r over the aligned returns, raised to periods_per_year / n — and rf_annual = (1 + rf)^periods_per_year − 1. A β of zero (a flat or degenerate benchmark) is the sentinel case, returning 0.0 rather than dividing.

rf is not one unit across the module

Read each doc's formula before passing a rate. alpha places rf directly beside CAGR_s and CAGR_b, both annual figures. treynor_ratio compounds it — (1 + rf)^periods_per_year − 1 — so there it is a per-period rate. The two functions are documented independently and neither converts for you; a rate that is right for one is wrong for the other by a factor of the annualization.

Portability

The header names the three sites that reach a power — cagr, r_squared, treynor_ratio — and states that each is libm::pow, never f64::powf, citing the decision record that made the libm crate the source of every transcendental. It also draws the conclusion that follows for this module in particular: because these numbers no longer track the platform library the retired implementation computed with, even the spirit of a cross-implementation comparison is gone here, not just the mechanism. The analytics index carries what the portability costs and the gate that holds it.

What this is not

These are curve-versus-curve statistics over two equity series. The two-series indicators — spread, correlation and the rest computed bar by bar over two price series — live behind their own seam in the indicator crate; the pairs page covers them.

On this page