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.
A backtest is described by one TOML file. crates/vike-backtest/src/harness/profile.rs's
BacktestProfile names a venue/symbol/interval data slice, an engine cost and cash
configuration, and a strategy by registry name plus arbitrary TOML params. Unknown top-level keys
are a hard parse error — the struct is deny_unknown_fields — so a misspelt section cannot
silently no-op.
name = "bar-demo" # a free-form label; logs and report titles only
[data]
venue = "binance"
symbols = ["BTCUSDT", "ETHUSDT"]
kind = "bar" # "bar" or "tick"
interval = "1h" # bars only; defaults to "1d"
from = "2026-01-01T00" # epoch-ms as a string, or YYYY-MM-DDTHH UTC
to = "2026-01-02T00"
[engine]
cash = 100000.0
fee_rate = 0.001
slippage = 0.0
[strategy]
name = "buy_hold"
[strategy.params]
size = 1.0from and to accept a bare epoch-millisecond integer written as a string or a YYYY-MM-DDTHH
UTC hour label; BacktestProfile::range resolves both to an inclusive range, and validate
rejects from > to.
The data slice, in one of two forms
DataCfg accepts either the single-venue pair venue = "…" plus symbols = [ … ] or an
[[data.series]] array whose entries each carry their own venue, symbol and — in tick mode —
lane kind. Exactly one form must be present; mixing them is a validation error, not a
precedence rule. The array form is the cross-venue one, and its order is meaningful: the header's
example puts a spot reference series first so that, on an equal-timestamp tie in the tick merge,
the reference sample is delivered before the print it should inform.
Symbols must be unique across the resolved series — the engine routes events to a symbol slot by
name, and two series sharing one symbol would silently land in one slot. A bar-mode profile may
not set a series kind: a bar series has no quote/trade/book lanes to filter.
The engine table
EngineCfg carries cash (must be > 0), a flat fee_rate, a flat slippage, and opt-in knobs
whose absence is byte-identical to the profile that never mentioned them. Several are
mode-bound, and for five of them — attach_funding, [engine.impact], feed_latency,
queue_model and equity_sample_every — BacktestProfile::validate rejects a profile that sets
one in the wrong mode rather than ignoring it. The other tick-only knobs carry no such check:
run_backtest's bar arm never reads them, so a bar profile that sets one runs as if it had not.
| key | mode | what it does |
|---|---|---|
attach_funding | bar only — rejected on tick | window-joins the stored funding series onto the price bars, one rate per bar whose window contains the event — never forward-filled |
[engine.impact] | bar only — rejected on tick | market-impact slippage on top of the flat slippage; the tick lane replays a real book and needs no model |
feed_latency | tick only — rejected on bar | deliver ticks in recorded arrival order while matching stays on venue time |
order_latency_ms, fill_latency_ms | tick only — inert on bar | the entry leg and the response leg of order latency, in milliseconds; either > 0 arms the model |
fill_model = "l2book" | tick only — inert on bar | the depth-capped book-walk fill instead of the default L1 Tick model; pair with slippage = 0.0 |
queue_model | tick only — rejected on bar | "risk_adverse", "prob_power" / "prob_power:N" or "prob_log" — a resting limit fills only once flow has consumed the size ahead of it |
queue_seed_depth, queue_min_hold_ms | tick only — inert on bar | the queue lane's seed depth, and a minimum-hold floor on the tagged maker lane's closing fills; both read only when queue_model is set |
equity_sample_every | tick only — rejected on bar | thin the equity curve to one sample per N ticks (0 records nothing) |
The string-valued knobs are resolved strictly. EngineCfg::fill_model_kind accepts exactly one
spelling, case-insensitively, and anything else is a fail-fast validation error — the earlier
lenience let a typo like "l2_book" quietly select the optimistic Tick model and undo the
depth-cap it was meant to enable. queue_model_kind follows the same rule.
Thinning the equity curve changes the report
equity_sample_every exists for a sweep over a very long tape — the curve costs 16 bytes per
priced tick, so a 100M-tick replay carries 1.6 GB of samples per run whether or not anything reads
them. But every curve-derived figure (max drawdown, Sharpe, the return series) becomes an
approximation, and 0 degenerates them entirely. final_equity, n_trades, the trade log and
per_symbol_pnl never move.
Three sub-tables have their own pages. [engine.fee] selects a vike_model::FeeSchedule — one
kind today, "probability_scaled", the prediction-market curve qty × rate × p(1−p) that a flat
rate cannot express; setting it beside a non-zero fee_rate is a validation error, because two
cost models would be configured and only one could win (the cost
model). [engine.resolution] builds the binary-outcome settlement
source (the sim broker). [engine.impact] names the
"almgren_chriss" model with its exec_time and window (market
impact).
emulator_release_stops defaults to true in the harness — a fired conditional stop is
released as a market order that fills the next event, as the live emulator does — even though the
low-level EngineParams default is false, so a profile run reproduces live fill timing without
an explicit knob. snap_to_properties wires the store's point-in-time instrument grid into the fill
path and is single-venue only: the grid is looked up under one default venue, so validate refuses
it beside a cross-venue slice rather than snapping every series to the first series' venue.
Snapping re-tags your bars
With snap_to_properties on, bar-mode bars are re-tagged SYMBOL.VENUE for the grid lookup, so a
symbol-inferring strategy — buy_hold reading bar.symbol is the example
crates/vike-backtest/src/harness/run.rs's
bar_engine_params gives — needs an explicit strategy.params.symbol to route.
Strategy, risk, sweep, walk-forward
[strategy] is a name and a params table the strategy's own constructor interprets; which
names resolve is the harness registry.
[risk] carries the same schema and the same compile-checked converter
(vike_exec::ProfileRisk::to_risk_limits) the live run profile uses, so a limit proven here by a
real SimBroker denial carries unchanged into paper and live. It is the only way a harness
profile arms the pre-trade gate — [engine] has no leverage knob. One field is rejected:
risk.max_orders_per_window is a wall-clock order-rate throttle, and a replayed month can execute
in under a second.
[paramscan] turns the profile into a grid — each key a strategy.params field, each value an
array to cross-product over; absent or empty means "not a search". ⚠ The table is named
paramscan, and sweep is an ALIAS of it — a permanent one, because the profile is
deny_unknown_fields and every file written before the rename would otherwise stop loading. Both
spellings work; paramscan is the field.
[walkforward] turns it into a sequence of train/validate windows. Both tables live in the profile
rather than on the wire because a deny_unknown_fields profile must parse for the whole TOML to be
shippable verbatim to a remote runner, and both are inert for a plain single run.
⚠ This paragraph said [walkforward] "carries one knob, n_splits >= 1" — it carries nine
fields, and two of them (purge, embargo) are legal on one of its two window forms only. That
sentence was true when it was written, which is why the table is no longer described by hand here:
backtest --profile renders every key, default,
accepted value and refusal from the Rust types that declare them. See also
sweeps and walk-forward for what
the two tables are FOR.
When a profile is loaded from a file, BacktestProfile::from_path records the file's directory as
base_dir, and relative paths inside the profile — today [engine.resolution].path — resolve
against it rather than the caller's working directory, so a profile and its sidecar move together.
Backtesting
One event engine and one vectorized kernel, both fed by the same fill primitives the paper exchange uses, driven by a TOML profile through a harness that runs bar or tick replay against the history store.
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.