Validation
The time-series cross-validation splitters — walk-forward by count or by duration, purged k-fold, and combinatorial purged paths — and where the embargo sits.
Ordinary cross-validation shuffles observations. On a time series that is a way of training on the future, so crates/vike-analytics/src/validation.rs provides the splitters that respect time: walk-forward windows, purged k-fold, and combinatorial purged paths, ported from chapter 7 of Advances in Financial ML. Every splitter returns index ranges over a series of length n; nothing here touches bars or returns. They are the machinery behind walk-forward and the probability of backtest overfitting.
The embargo, and which side of the block it sits on
The header states the one modelling decision that shapes every purged splitter, and states only that: the embargo removes a window after each test block from training, because the observations are point-in-time, so there is no label overlap reaching backwards and no pre-block purge. Why the window sits on that side at all is not something the module says; the usual reason for an embargo is that a serially correlated series would otherwise leak the test period's information into a "future" training row, and that is the reading this page takes.
Walk-forward by count
walk_forward_splits(n, n_splits, mode) divides the series into n_splits + 1 chunks of n / (n_splits + 1) and returns one Split per test chunk from the second chunk on; the last split's test window runs to n. A Split is four indices with half-open semantics — train is [train_start, train_end), test is [test_start, test_end), and train_end == test_start. WalkMode picks the training window: Anchored is the expanding window, with train_start always 0; Rolling is a fixed-width window of exactly one chunk immediately preceding the test window.
The module's test pins the shape: n = 100, four splits, chunk 20. The first split trains on [0, 20) and tests on [20, 40); the last anchored split trains on [0, 80) and tests on [80, 100), while the last rolling split trains on [60, 80). This is the splitter vike_backtest::walkforward::walk_forward_strategy calls to run a strategy over each out-of-sample window and stitch the windows into one equity curve; its WalkForwardReport::wf_consistency — the fraction of windows profitable out-of-sample — is the walk-forward input to overfit_verdict.
Walk-forward by duration
purged_walk_forward_hours(n_hours, train_hours, val_hours, step_hours, purge_hours, expanding) is the sibling parameterised by duration rather than by split count, because a research protocol is specified that way — "train on 14 days, validate the next 24 hours, step a day" — and converting that to a count depends on how much history happens to exist. It returns the same half-open Split.
purge_hours must cover the forecast horizon
purge_hours is the gap between the end of training and the start of validation, and it exists because the labels are forward-looking: a row at time t carries the return realised at t + H, so the last H hours of any training window have labels that reach into the validation window. Without the gap the model is trained on the answer. Set purge_hours to at least the horizon. Because the boundaries are half-open, test_start − train_end == purge_hours exactly; a purge_hours of 0 leaves train and test contiguous with no skipped row, matching walk_forward_splits.
expanding anchors train_start at 0 and grows the window; otherwise the window slides at a fixed length. The doc's own reading: sliding is the more honest form for "the model adapts to the recent regime"; expanding gives later folds more history. Edge cases are stated rather than left to chance: a zero n_hours, train_hours, val_hours or step_hours yields an empty vector instead of a panic or an endless loop, and a trailing validation window that would run past n_hours is dropped rather than truncated. The pinned example is (100, 24, 10, 10, 5, false): seven splits, the first {0, 24, 29, 39} and the last {60, 84, 89, 99}.
Purged k-fold
purged_kfold_indices(n, k, embargo) cuts the series into k contiguous test folds and, for each, returns (train, test) index lists where train excludes the test fold and an embargo window of embargo observations after it. The test pins n = 10, k = 2, embargo = 1: fold 0 tests on 0..5, and its training set is [6, 7, 8, 9] — index 5 is the embargo.
Combinatorial purged paths
combinatorial_purged_splits(n, n_groups, n_test_groups, embargo) generates every way of choosing n_test_groups of n_groups contiguous groups as the test set — C(n_groups, n_test_groups) paths, six for C(4, 2) — with a per-test-group embargo applied after each chosen group. The enumeration comes from the module's combinations(n, k), which yields all index combinations of 0..n in lexicographic order (k = 0 gives one empty combination, k > n none) and is the same helper overfit::pbo_cscv iterates for its in-sample/out-of-sample paths.
Choosing among them
By count when a fixed number of out-of-sample windows is what you want reported; by duration when the protocol is written in hours and the series length should not change the fold geometry. Purged k-fold and the combinatorial paths take everything outside the fold and its embargo as training, so most of their folds train on rows both before and after the test window — the pinned n = 10, k = 2 example trains fold 0 on [6, 7, 8, 9], rows later than its test block. Only the last fold (and the one combinatorial path whose test groups are the final ones) tests on a window strictly later than all of its training data. They yield many train/test paths from one series; walk-forward yields one per window, each trained only on what came before it. walkforward.rs's header calls the walk-forward form "the honest way to backtest" a strategy without comparing it to the other two, so that contrast is this page's reading, not the header's.
Stability
A single score in [0, 1] for how broad the optimum of a parameter sweep is, and the three-way label that reads it.
Survival statistics
Whether a signal's Sharpe survives its own autocorrelation and the fact that it was the best of many — an autocorrelation-robust t-statistic, a false-discovery correction, a block bootstrap and a reality-check p-value.