vike
Backtesting

Sweeps

How a profile's [sweep] table expands into a cartesian grid of single runs, how the grid is ranked, and why the parallel and sequential paths are byte-identical.

A sweep is a run profile with a [sweep] table. Each key names a strategy.params field and each value is a TOML array of values to cross-product over; crates/vike-backtest/src/harness/sweep.rs's expand_sweep turns the profile into the cartesian product of single-run profiles, each with its params overridden per point and its own sweep field cleared, so every expanded point is itself a plain runnable profile. A profile with no [sweep] table, or an empty one, expands to exactly one point with no overrides — the non-sweep path is unchanged.

Two rules apply at expansion, each a HarnessError::Validation: every entry must be a non-empty array (sweep_axis_array), and the product is returned in a deterministic order — axes sorted by key, values in array order, the last axis varying fastest. The grid keeps each raw toml::Value, so a string or boolean axis is fine here; the search lanes narrow the same table to numeric axes.

Running and ranking

run_sweep runs every SweepPoint through run_backtest — the same dispatcher a single run uses — and collects a BacktestReport, or the error string, per point. A per-point failure does not fail the sweep; it is recorded as that row's error so the report still shows every point and which ones failed. Successful rows are ranked by a RankMetric — sharpe (the default), return, max_dd or equity — with failed rows last. RankMetric::cmp_reports orders a NaN sort key last rather than first, so a Sharpe over a zero-variance curve cannot win the sweep and feed downstream selection. run_sweep_with ranks the same rows by any objective instead, stamping each row's score.

The result is a SweepReport { rows, rank_by }, each SweepRow carrying its overrides, its report or error — never both — and an optional score. A non-finite score serialises as "score": null, which the module pins as the documented meaning of "unrankable" independent of the serialiser. Sharpe is annualised through harness::report::periods_per_year for the single run and every sweep row alike, so both rank on the same scale.

Parallel by default, byte-identical either way

Every expanded point is an independent single run over one shared Arc<dyn HistStore>, so the points run on a rayon pool. Determinism does not rest on scheduling: par_iter().map().collect::<Vec<_>>() is order-preserving, so rows come back in expand_sweep order regardless of which point finished first, and the ranking sort is slice::sort_by, which is stable, over that same order — the identical bytes the one-at-a-time loop produced, pinned by parallel_and_sequential_sweeps_are_byte_identical. crates/vike-backtest/src/harness/sweep.rs's sort_scored_rows names the stable sort as part of what keeps the parallel and sequential execution paths byte-identical, and its SweepExec doc calls the choice purely an execution-strategy knob: swap either property for an unordered collect or an unstable sort and byte-identical output dies.

Parallelism multiplies the data slice, so it is bounded

The shared Arc shares the store handle, not the loaded data: each point calls run_backtest, which re-scans and materialises its own Vec<Bar> or Vec<Tick>. Peak RSS and store I/O are N_threads × slice, not 1 × slice, and these sweeps run on boxes that also host live trading. The points therefore never run on rayon's global pool: install_bounded builds a pool of sweep_threads workers — VIKE_SWEEP_THREADS when it parses to a positive integer, else min(4, available_parallelism()), with DEFAULT_SWEEP_THREADS of 4 kept small on purpose and never zero, because rayon reads zero as one worker per core. Size the variable against the slice, not the core count.

A pool that fails to build — the OS refusing a thread spawn — is logged and the work runs on the ambient pool instead; the results are identical, only the bound is lost. VIKE_SWEEP_SEQUENTIAL=1 (the exact string "1") pins the whole process to the one-at-a-time loop, and every entry point has an _exec twin taking SweepExec explicitly, which is what the determinism tests use so they never touch the process environment. map_bounded is the public front door for another crate's sweep — crates/vike-studio-core/src/run.rs's run_sweep_slice reaches the same bounded pool through it without taking a rayon dependency of its own.

On a long tick tape the equity curve is the other memory multiplier. EngineCfg::equity_sample_every exists as the sweep knob for it: the curve costs 16 bytes per priced tick, so a 100M-tick replay carries 1.6 GB of samples per run, paid by every point whether or not anything reads them — at the price of making every curve-derived figure an approximation. It is tick-mode only and rejected on a bar profile.

Shared optimizer glue

This module also owns the per-point plumbing the search lanes fold through instead of re-rolling it: numeric_sweep_axes (the one [sweep] reader, adding the numeric and type-homogeneity narrowing), profile_with_overrides (base plus overrides into the single-run profile every point becomes), eval_scored_point (point to scored row plus steering score) and sort_scored_rows (the best-first ordering over cmp_scores_desc). One implementation means the grid, successive halving and TPE cannot drift apart on validation, evaluation or ranking, regression-gated by euler_and_tpe_rank_rows_exactly_like_run_sweep_with.

From the command line

backtest --profile sweep.toml [--rank-by sharpe|return|max_dd|equity|multi] [--search grid|euler] [--optimizer tpe [--trials N] [--seed S]] — the standalone backtest binary detects the [sweep] table itself; --rank-by is ignored, not an error, on a non-sweep profile. A sweep prints a ranked table (or SweepReport JSON under --json) and, unlike a single run, writes nothing under user_data/runs/ — it is a different document and a different run kind, deliberately not smuggled into the manifest yet. See runs on disk, the vike-cli sweep verb, running a sweep on the data service, and the sweep and walk-forward tutorial.

On this page