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.
Run profiles
The BacktestProfile TOML — a data slice, an engine cost configuration and a strategy by registry name — with every opt-in knob validated at load so a typo fails the profile rather than silently changing the result.
The harness registry
What strategy.name may say — a name-to-boxed-strategy lookup over SimBroker that is the simulator half of a two-part registry, with seven names a live daemon cannot resolve and the written reason for each.