Walk-forward
Anchored out-of-sample evaluation that runs one fixed strategy over successive windows and stitches the results, and never re-fits.
Walk-forward evaluation runs a strategy on each out-of-sample window of a series and stitches those windows into one equity curve — "the honest way to backtest", in the words of crates/vike-backtest/src/walkforward.rs, which is the strategy twin of the original ml/walkforward.py: that one wrapped a model, this one wraps any Strategy through a caller-supplied run_window closure.
The runner
walk_forward_strategy(bars, n_splits, mode, cash, periods_per_year, run_window) asks vike_analytics::validation::walk_forward_splits for the windows, runs run_window over each window's test range, and rebases the resulting curves onto one running equity. Each window starts fresh at cash; its curve is scaled by start × (v / cash) and the running equity advances by final_equity / cash, mirroring the Python stitch. The contract, held by a debug_assert, is that every window's result starts its equity curve at exactly cash.
The report is a WalkForwardReport: one WfWindow { test_range, oos_return } per window, the stitched oos_equity_curve, the overall oos_return, oos_sharpe over the stitched curve at the supplied annualisation, and wf_consistency — the fraction of windows profitable out of sample, the input to the overfitting verdict. Both types are Serialize and deliberately not Deserialize, so the compute-to-data RunWalkforwardProfile verb can return the report as JSON text exactly as BacktestReport and SweepReport already cross the data-service wire, without coupling the wire schema to this crate's serde surface.
Nothing is re-fitted
Each Split carries a train range as well as a test range, and the runner never reads the train half: walk_forward_strategy slices bars[test_start..test_end] and nothing else. The harness closure constructs the strategy fresh for every window from the same strategy.name and strategy.params the profile names, so what walks forward is one fixed parameter set evaluated on successive unseen windows. To have the parameters chosen on the earlier data, choose them with a sweep first and walk the winner forward — the shape the sweep and walk-forward tutorial follows.
The splits
crates/vike-analytics/src/validation.rs ports the time-series cross-validation splitters of the original analysis/validation.py (López de Prado, Advances in Financial Machine Learning, chapter 7). walk_forward_splits(n, n_splits, mode) divides the series into chunks of n / (n_splits + 1) bars: window s tests [s·chunk, (s+1)·chunk), the last extending to the end of the series, and train ends where test starts. WalkMode::Anchored is the expanding window — train_start is always 0 — and WalkMode::Rolling a fixed-width train of one chunk immediately preceding the test window. A profile selects between them with walkforward.mode, and anchored is what an absent key means (this line read "the profile path always uses Anchored", which was the default mistaken for the only option). The wider splitter family — purged k-fold, combinatorial purged splits and the duration-parameterised purged_walk_forward_hours — is on validation.
From a profile
A [walkforward] table makes a run profile walkable, and it must declare exactly one window form: a split COUNT (n_splits), or DURATIONS (train with test, and optionally step). Declaring neither is refused by name, and so is declaring both — a count is defined by the range and moves when the range moves, while a duration does not, so the two cannot be reconciled by a precedence rule. crates/vike-backtest/src/harness/walkforward.rs's run_walkforward walks whichever form the table declares. The module doc calls it assembly rather than new engine code: it resolves the strategy through the same strategy_by_name registry, builds the same bar-lane EngineParams through bar_engine_params, and loads the same bars through load_profile_bars as a plain run_backtest of that profile — so a walk-forward can never disagree with a single run about what the profile's [engine] means. That buys the whole [engine] surface — the fee schedule, [engine.impact], [engine.resolution], [risk], snap_to_properties, attach_funding — where the Studio's DTO-shaped walk-forward (vike_studio_core::run_walkforward_slice, served by the RunWalkforward wire verb) carries only the three flat cash, fee_rate and slippage scalars.
⚠ This paragraph read "Only the split count is configurable, deliberately. The mode is always Anchored … one knob" and every clause of that is now false. The table carries nine fields: the two window forms above, plus purge and embargo (legal on the DURATION form only — the split-count walk stays gap-free, decision 0046), mode (anchored | rolling; absent = anchored, which is where the old claim came from — it is the DEFAULT, not the only value), search (none | sweep; absent = none, and "sweep" without a grid table is refused rather than silently selecting the one parameter set the profile already has) and rank_by (sharpe | return | max_dd | equity; absent = sharpe). Every one of those, with its default and the sentence that refuses a bad value, is rendered from the type itself on backtest --profile — which is why this page no longer lists them by hand.
What still holds: n_splits must be at least 1 (BacktestProfile::validate rejects 0), the section is deny_unknown_fields so a typo fails at load, and an absent section is byte-identical to every profile written before it existed — run_backtest and run_sweep ignore it exactly as they ignore a [paramscan] table they were not asked to expand. It lives in the profile rather than on a wire field because the profile is deny_unknown_fields: a TOML carrying [walkforward] must parse for the whole file to be shippable verbatim to a remote runner, which is the point of the compute-to-data verb.
The runner is bar mode over one series: walk_forward_strategy splits a single &[Bar] by index, which a tick replay does not produce, so a tick-mode or multi-series profile is a clean HarnessError::Validation, never a silent first-symbol fallback. Every other failure — no [walkforward] table, an unresolvable strategy, a store failure, an empty bar slice — is checked before the window loop, because the loop's closure cannot return an error. EngineParams is rebuilt per window because it is not Clone (it can carry a sizer box and a properties closure), and each window needs a fresh strategy.
The vike-cli walkforward verb ships a profile to a data service; remote runs is the Studio's dispatch of the same job.
Objectives
The pluggable scalar that ranks a sweep's rows, the two laws every objective obeys, and the composite multi-metric score.
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.