Analytics
The pure, engine-free performance-analytics cluster — every number a backtest report or a live tearsheet prints, computed in one place from vike-model and nothing else.
Vike Trader's analytics live in one crate, vike-analytics, whose header states the design in a
sentence: the pure performance-analytics cluster, extracted verbatim out of vike-backtest.
Nothing in it runs a simulation, signs an order or opens a store. It takes an equity curve and a
trade list and returns numbers — and because the backtest runner and the live tearsheet both call
the same functions, a backtest number and a live number over the same fills are the same number.
Why it is a crate of its own
crates/vike-analytics/src/lib.rs's header carries the measurement that justified the split.
vike-report, the reader that renders a tearsheet from the live journal, reimplements no metric:
every figure it prints is a call into this catalog. While the catalog lived inside vike-backtest,
that reader compiled 32,259 lines to reach about 3,900 — 89% of what it built it could not
call, including the simulator, the paper exchange and the whole feature-gated harness tree. The
seam was already there: these modules name vike_model and nothing else (the one vike_exec
mention in the cluster was a doc comment in result.rs). Splitting on it lets a consumer take the
numbers without the machine that produced them. The header is explicit that the extraction moved
code, not behaviour.
The rule the crate holds
vike-model is the only Vike dependency — no vike-exec, no vike-core, no vike-data.
The header calls that the property the split exists to create, and says a would-be dependency on
any of the three is a signal that the item belongs in vike-backtest, not here. The manifest says
the same thing structurally: crates/vike-analytics/Cargo.toml declares layer = 20 under
[package.metadata.vike], the domain tier ("one hop over the vocabulary: a single-purpose pure
core"), and the layer gate fails any normal dependency that does not sit strictly lower. The
architecture page carries the tiers.
The non-Vike dependencies are few, and each carries its reason in the manifest: serde (the
report and the zero-trade report serialize), chrono (calendar bucketing), indexmap (insertion
order where it affects a floating-point sum), rand (seeded resampling), libm (the manifest's
row names what first brought it in — the erfc normal CDF and the inverse normal CDF in
overfit.rs; the rule that every transcendental in the crate now goes through it is the header's,
below).
What is in it
| module | page | what it is |
|---|---|---|
metrics | Metrics | the catalog — sharpe, sortino, drawdown, CAGR, VaR, ES, SQN and the rest; the one home for every performance number |
result | The result type | BacktestResult, the run's raw output |
report | The report | BacktestReport, the flat serializable summary composed from it by metrics:: calls only |
periods | Periods | calendar bucketing, the monthly matrix, the drawdown table |
benchmark | Benchmark | benchmark-relative statistics over two aligned curves |
excursions | Excursions | MAE/MFE per trade, the edge ratio, running trade metrics |
montecarlo | Monte Carlo | seeded resampling of trade P&L |
overfit | Overfitting | PBO and the deflated Sharpe |
stability | Stability | rolling parameter-stability scoring |
validation | Validation | walk-forward and purged k-fold split generators |
stats | Survival statistics | the autocorrelation-robust t-statistic |
signal, signal_backtest | Signals, The signal backtest | probability to position; the vectorized signal backtest |
sizing, zero_trade | Sizing, and zero-trade diagnosis | the PositionSizer registry (eight sizers); the zero-trade cause analyzer |
A last module, binutil, is the pure argv glue the analytics-family binaries share; the header
notes that store_root, the one function that reads the environment, deliberately stayed in
vike-backtest.
Numbers that do not depend on the box
The header's "Cross-platform determinism" section is the part of this crate most worth carrying
into your own reading of a report. Every transcendental in the crate is the libm crate, never
f64's inherent method — libm::pow, libm::log, libm::exp, libm::erfc. IEEE 754 does not
require pow, log or exp to be correctly rounded, so f64::powf is whatever the platform's
maths library does: the MSVC CRT on the Windows desktop, glibc on the Linux servers. The two
disagree by up to one ulp, and the equity curve produced by signal_backtest::equity_from_pnl was
measurably among the casualties, so a backtest's headline number depended on which machine ran it.
crates/vike-analytics/tests/libm_platform_probe.rs is the measurement that established it, and
converted_functions_are_platform_invariant in that file is the gate that now holds it. The
decision record behind the rule is dated 2026-08-25 and accepted.
Two refinements the header states because each was once got wrong: sqrt stays f64::sqrt,
because IEEE 754 does require it correctly rounded; and powi is banned at every arity, because
on MSVC in a dev build it becomes the CRT's pow() — the cure is a plain multiplication.
What the portability costs
The header states the trade rather than hiding it: the libm crate does not agree with glibc
either (the probe measures 0.07%–10% of samples differing per domain), so the conversion moved
values on Linux too — where CI and live trading run. The numbers also no longer track the platform
library the retired reference implementation computed with. That is a deliberate choice of
reproducibility over bit-parity with an oracle that no longer exists; the
parity gate page carries what retiring that oracle did and did
not license.
The parity rules the crate inherited are unchanged: f64 end to end, the same expression order,
no fused multiply-add or fast-math; % on timestamps is floor-mod (rem_euclid); IndexMap
wherever insertion order affects the order of a floating-point sum.
Two conventions you will meet on every page
Degenerate inputs return a sentinel, not NaN. A ratio with nothing to divide is 0.0, and
a ratio whose denominator is zero while the numerator is positive is f64::INFINITY —
profit_factor with no losing trades is the canonical case. The report
page shows how that sentinel reaches JSON.
One percentile. Everything that quotes a quantile — VaR, the tail ratio, the Monte Carlo
bands — goes through metrics::percentile, and metrics::PERCENTILE_METHOD ("numpy_linear")
names that convention wherever an artifact has to say which percentile produced its numbers.
Where the numbers surface
The standalone backtest binary prints a BacktestReport and ranks sweeps on it; the
read a report guide walks one. The
tearsheet binary builds a BacktestResult from
reconstructed live trades and fills its LiveTearsheet from the same catalog. And vike-backtest
re-exports metrics, overfit, periods, report, result, sizing, validation and
zero_trade at its own root so every pre-split path still resolves — while benchmark,
excursions, montecarlo and stability are deliberately not re-exported, because, as
crates/vike-backtest/src/lib.rs's header records, no path of either spelling ever resolved
through that crate.
Walk-forward
Anchored out-of-sample evaluation that runs one fixed strategy over successive windows and stitches the results, and never re-fits.
The result type
BacktestResult is the one shape every engine fills — the trades, the equity curve, and the diagnostic counters that let a run with no trades explain why.