vike
Analytics

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, as vike_model::Trade values. Empty when a kernel runs with build_trades = false.
  • equity_curve — equity after each bar. equity_ts carries the bar timestamps aligned to it, and is empty when not tracked; the calendar analytics on the periods page need it.
  • final_equity and n_trades — copied straight into the report.
  • per_symbol_pnl and per_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 is None (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 live RiskGate'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's Strategy::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 of vike_exec::Account.funding_paid, broken out of final_equity because a funding-carry strategy's P&L is mostly funding and seeing it separately matters. 0.0 when no funding was charged, for the tick path (the field doc notes run_ticks does 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.

On this page