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
| function | definition (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 returns | 0.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 − rb | 0.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 bars | 0.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 bars | 0.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.
Periods
Calendar-period returns, the year-by-month matrix and the top-N drawdown table — bucketed on the same UTC day law the history store partitions by.
Excursions
MAE and MFE per trade from the bars between entry and exit, the edge ratio over them, and the running trade metrics — one excursion primitive that the live tearsheet back-fills through.