vike
Backtesting

Running a backtest

One dispatcher runs a profile in bar or tick mode against any history store; the standalone backtest binary drives it locally, and vike-cli backtest ships the profile's text to a data service and brings back the report.

There is one function that turns a profile into a result, and two front ends that reach it. crates/vike-backtest/src/harness/run.rs's run_backtest loads a BacktestProfile's data slice from an Arc<dyn HistStore> — any backend that implements the trait: the DataFusion store in production, the ClickHouse-reading store the poly_ch_backtest bin feeds through the same dispatcher, an in-memory store in tests — resolves the strategy by name, and runs it in bar mode (StrategyEngine) or tick mode (replay_ticks) according to profile.data.kind.

Both modes load the profile's resolved series, so a cross-venue [[data.series]] slice and the single-venue pair take the same code path. The fee schedule and the resolution source are built once for both. The bar arm's engine parameters and bar loading are factored into bar_engine_params and load_profile_bars so the walk-forward runner builds identical parameters and loads identical bars — the divergent-default bug shape this workspace keeps paying for, closed by construction. One difference is load-bearing: in bar mode nothing else would wire the point-in-time instrument grid, so run_backtest sets it itself when snap_to_properties is on; in tick mode the flag goes to the replay loader, which wires it from its own closure.

The backtest binary

crates/vike-backtest/src/bin/backtest.rs is a thin shim over crates/vike-backtest/src/backtest_cli.rs's run. The shim is the composition root and holds the two things a library may not: the one std::env::vars() sweep, and the clock. Its usage:

backtest --help | --version
backtest --list
backtest --seed-demo [--store DIR]
backtest --profile run.toml [--store DIR] [--json]
backtest --profile sweep.toml [--rank-by sharpe|return|max_dd|equity|multi]
                              [--search grid|euler] [--euler-depth N]
                              [--optimizer tpe [--trials N] [--seed S]]
backtest --fetch VENUE:SYMBOL:INTERVAL (--days N | --from LABEL --to LABEL) [--store DIR]
backtest --fetch-starter [--store DIR]
backtest --export VENUE:SYMBOL:INTERVAL --out FILE [--from LABEL --to LABEL] [--store DIR]

--store picks the history-store root. Without it the binary resolves $VIKE_HIST_STORE, then <project>/market_data/hist for a project $VIKE_SETTINGS_DIR declares outright, then <repo>/market_data/hist while the checkout that built the binary still exists, then the project the working directory sits in, and finally a per-user data directory — logging the root it chose and the rung that chose it. See the history store. --list prints every registered strategy name and exits, needing no store and no profile. --json prints the BacktestReport — or, for a sweep, the SweepReport — as pretty JSON instead of the human table, and that stream is a machine contract: every diagnostic goes to stderr so stdout stays parseable. --help and --version are answered before the log subscriber is initialised, so neither creates a log directory.

A profile with a [sweep] table runs run_sweep instead of a single run; --rank-by picks the ranking metric (default sharpe, multi for the composite objective), --search euler replaces the exhaustive grid with a bounded successive-halving refinement, and --optimizer tpe is the Bayesian search, which takes precedence over --search and is reproducible under --seed. See sweeps, search and objectives.

The empty-store answer

A clean install has an empty history store, so the shipped example profile reports a run with no trades, and nothing distinguishes that from a strategy that never fired. backtest data seed-demo writes a synthetic tape under the venue demo — a closed-form curve, not market data — which is the slice the shipped user_data/profiles/backtest.toml names, so a fresh install can run it immediately; re-running writes nothing. backtest data fetch-starter downloads a published starter dataset over plain HTTPS, for a box a venue cannot be reached from at all. Both need the venue-fetch feature, which the release binary and the container image have; a build without it refuses by name rather than silently doing nothing.

⚠ These were FLAGS — --seed-demo, --fetch, --fetch-starter, --export, --rm-series — and they are subcommands now (crates/vike-backtest/src/backtest_cli.rs's RETIRED_DATA_FLAGS refuses each old spelling by name and prints the new one). ⚠ --fetch did not move, it was deleted: crates/vike-backfill/src/kline_source.rs records that its venue roster was retired on 2026-09-19 by deleting the producer, because that verb reached a venue directly from the compute plane. Fetching real bars is vike-cli data hist fetch, which asks a datahub that dispatches through the kline registry — one place that knows which venues serve bars, instead of a second roster inside the backtester.

Before the profile is loaded, the binary installs the user's own Rhai indicators from user_data (honouring VIKE_USER_DATA_DIR), because a rhai strategy compiled later may call them and nothing else installs them; a rejected file is reported on stderr and is never fatal. After a single run prints, the binary persists it to <project>/user_data/runs/<run_id>/ — after printing, never changing the exit code; a sweep, a euler refinement or a TPE run writes nothing. See runs on disk.

vike-cli backtest: the remote path

crates/vike-cli/src/cmd/backtest.rs is a different path, not a wrapper. It runs a real backtest against a vike-datahub server from a machine with no DataFusion in its build: it reads a profile's TOML text locally, ships that text verbatim, and prints the compact report the server returns. The history never crosses the wire, and the server is the only thing that parses the profile, with BacktestProfile::from_toml_str.

vike-cli backtest --profile <run.toml> [--preset <p.toml>] [--script <s.rhai>] [--addr 127.0.0.1:7878] [--json]
vike-cli backtest --list-params --script <s.rhai>

--addr defaults to 127.0.0.1:7878; the server refuses a non-loopback bind unless it is explicitly opted into with node keys configured, so a remote one is reached over an SSH tunnel. The exit code is 0 when a report comes back and 1 on any failure — bad arguments, an unreadable file, a transport error, or a server-side error — with the message on stderr. --json prints the server's JSON verbatim; without it the JSON is pretty-printed.

Two client-side rewrites happen before the request. --script injects a Rhai file's source into [strategy.params].src, so an authored strategy ships self-contained to a server that cannot see the client's filesystem. --preset merges a flat table of knobs into [strategy.params], last-wins — resolved here rather than named in the profile, because a preset = "fast" field would be resolved against the server's user_data, not the author's. Two preset shapes are refused because accepting them would do nothing visible: a [params] wrapper (the strategy would receive one parameter called params that no reader looks at) and a src key (a script smuggled past --script). --list-params is offline — it prints a script's param(name, default) knobs and opens no connection. vike-cli sweep and vike-cli walkforward take the same compute-to-data shape (crates/vike-backtest/src/harness/sweep.rs and crates/vike-cli/src/cmd/walkforward.rs).

The profile is shipped as bytes, not re-parsed

crates/vike-backtest/src/harness/sweep.rs's module doc records why: sweep and walkforward once re-parsed the profile into client-side DTOs, and the mapping silently dropped [engine] costs, [risk], tick slices and cross-venue slices — so a sweep ran under different costs than the same profile's backtest. Only the --script and --preset rewrites touch the TOML, each a targeted edit of [strategy.params].

The quickstart walks this path end to end; the flag tables are on the CLI reference and the backtest binary page, and the server on vike-datahub.

On this page