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.
Every backtest in Vike Trader ends in one type. crates/vike-analytics/src/result.rs's header
calls BacktestResult the one backtest result type: it unifies the former RunResult,
MultiSymbolResult, FastResult and PortfolioFastResult into a flat superset where each
producer fills what it has and leaves the rest empty or zero — decision A of the
engine-unification spec. The portfolio engine, the
vector engine and the live tearsheet's reconstruction all
produce this shape, and everything downstream — the report, the
metrics, the zero-trade diagnoser — reads it.
The core fields
trades— the closed trades, asvike_model::Tradevalues. Empty when a kernel runs withbuild_trades = false.equity_curve— equity after each bar.equity_tscarries the bar timestamps aligned to it, and is empty when not tracked; the calendar analytics on the periods page need it.final_equityandn_trades— copied straight into the report.per_symbol_pnlandper_symbol_curves— multi-symbol event runs only (PnL per symbol, and the cumulative curve per symbol); single-symbol and vector runs leave them empty.intrabar_both_hit— event engines only: how many bars saw both the stop-loss and the take-profit hit. Kernels leave it at 0. The adverse-first ordering that resolves such a bar is on the fill resolution page.
What a Trade is
crates/vike-model/src/position.rs's Trade is a completed round-trip: pnl is gross price
PnL and fees is the round-trip cost, kept separate. entry_ts and exit_ts are the fill
timestamps of the opening and closing orders; symbol is the originating symbol (empty for the
single-symbol engine); is_long is true when the opening side was a buy. mae and mfe are the
maximum adverse and favourable excursions as fractions of entry price — portfolio mode only; 0.0
means untracked. The excursions page covers how they are filled
and how they are recomputed from bars when they were not.
The diagnostic counters, and why they ride on the result
Several fields exist for one reason the field docs state repeatedly: the harness and
backtest-bin entry points drop the engine once a run finishes, so anything a caller might
need in order to tell "zero trades because the strategy never traded" apart from "zero trades for
a mechanical reason" has to travel on the result. Each is a single copy taken after the fold
loop — never new counting on the hot path.
stale_deferrals— how many times the opt-in stale-price wait discipline (vike_backtest::EngineParams::max_price_staleness_ms, the staleness page) deferred a market order instead of filling it against stale data. Always 0 when that knob isNone(the default) and for every vector-kernel run. It separates "the tape never printed" from "the strategy never traded".session_deferrals— how many times the opt-in session gate (EngineParams::session_gate, the sessions law) refused a fill because the venue was closed: resting orders deferred plus tagged-maker and armed protective-stop passes skipped. Same defaults, same purpose: "few trades because the venue was shut most of the tape" stays distinguishable.dropped— the engine's gate-drop channel (vike_backtest::SimBroker::dropped) mirrored off the engine at end of run:(symbol, reason, size, weight)for every order a gate refused. The reasons are the liveRiskGate's own strings ("insufficient-margin","below-min-qty", …), order-kind cash-gate drops,"volume_cap"and"latency_reject". Empty for every vector-kernel run and for any run where nothing was dropped. This is what the zero-trade analyzer reads to say "every order was denied".warmup— the strategy'sStrategy::warmup()requirement, in bars or ticks, captured once at end of run; 0 for a strategy with no warm-up and for every vector run. It lets a zero-trade run say "the strategy stayed in warm-up the whole window (lookback > bars)".funding_paid— the net perp funding cashflow over the run, received-positive and paid-negative: the backtest twin ofvike_exec::Account.funding_paid, broken out offinal_equitybecause a funding-carry strategy's P&L is mostly funding and seeing it separately matters.0.0when no funding was charged, for the tick path (the field doc notesrun_ticksdoes not yet accrue funding), and for a run whose funding netted to zero. It is summed as ΣMirrorFunding.amount, the same quantity the R4 parity reconciles against the live account.
The one counter that is a measurement, not a channel
below_min_reversals is expected to stay zero
below_min_reversals counts fills this backtest executed that the live gate would have
denied — the field doc calls it a measurement of the one known live-vs-backtest divergence still
open, not a refusal channel. The condition is a below-minimum reversal: an order that opposes the
position, exceeds it (so it flips through flat), and falls below a venue floor. The simulator's
opening/closing split is direction-only, so it calls the whole thing closing and executes it; the
live RiskGate requires coverage and refuses.
Why it should read zero: a flip must exceed the position, so the condition needs
|position| < flip < min_qty — the position must already be below the floor, which an opening
order cannot produce. Against the real venue grids (the doc cites a min_notional of 5.0 for binance
and bybit and 1.0 for okx) that means reversing a position worth under about $5.
The counter exists so that reasoning is falsifiable: a non-zero reading on a real profile means
the argument is wrong. It does not need fixing — the obvious repair would change fill decisions
and, worse, would not converge the two engines. The source is SimBroker::below_min_reversals,
and the "KNOWN RESIDUAL DIVERGENCE" note lives with the live gate in vike-exec's risk.rs; the
risk and arming page carries that side.
Consumers
BacktestReport::from_result composes the report from a result and an
annualization factor. On the live side, the tearsheet
binary's LiveTearsheet builds a BacktestResult from reconstructed trades and an aligned equity
curve so that the same functions run over the same shape — the reason the type lives in an
engine-free crate.
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.
The report
BacktestReport composes the metrics catalog over a result into one flat, serializable summary — no math of its own, one JSON contract for the sentinel, and an annualization rule derived from the bar interval.