The run pipeline
The Studio's resolve → backtest → score pipeline, headless so CI tests it directly — the one strategy vocabulary it branches on, the data slice it loads, and the run, sweep, walk-forward and compare entry points over one store handle.
crates/vike-studio-core/src/run.rs is the Run pipeline: resolve a StrategySpec (compile the
Rhai buffer, or build a native registry strategy), load the picked DataSlice from the history
store, backtest it, return a result. Its header states the property that shapes everything else —
headless, no egui, so CI tests it directly — and the two axes it widened past the original "one
Rhai script over one bar series": the strategy source and the data slice.
One strategy vocabulary
crates/vike-studio-core/src/spec.rs exists so "which kind of strategy is this" is decided in
exactly one place instead of at each of the four runner entry points. StrategySpec has two
sources: Rhai(String), the editor buffer compiled through vike_script::RhaiStrategy per run
(and per sweep point, with that point's overrides), and Native { name, params }, a Rust strategy
from the backtest harness registry resolved by name plus a toml::Value params table.
native_strategies() is the registry's own roster, so a strategy added to
the harness registry appears in the Studio for free.
Params are a bare toml::Value rather than a spec type, and the header argues why: there is no
parameter-description mechanism in vike-backtest; each registered strategy reads its knobs ad hoc
off a &toml::Value (BuyHold::from_params is the reader convention, and the reference
strategies with no harness-tunable knobs take Default::default() and ignore params entirely), so
a parallel description layer would be a second source of truth no strategy is obliged to honour.
The Studio edits params as free-form (key, value) text rows and params_from_rows serialises
them: each cell is parsed as a TOML scalar so 3, 2.5, true and "BTCUSDT" reach the strategy
as the type the author meant, and text that is not valid TOML on its own — the common unquoted
symbol — falls back to a plain string. native_from_toml_str
is the wire idiom the data service's RunSlice verb uses: params ride the wire as TOML text and
are parsed back at the boundary. build_strategy is the one place the Rhai/native branch is taken;
build_strategy_with applies one sweep point's overrides — Rhai gets them as script param()
bindings, native gets them merged into its table.
The data slice
DataSlice carries a venue, a symbol list, an interval, a range and a SliceKind: Bars
replays kind=bar series through StrategyEngine::run, the original path; Ticks routes to
vike_backtest::hist_replay::replay_ticks over the store's recorded quote, trade and book series.
DataSlice::bars is the single-symbol constructor and is exactly the old struct literal;
multi_bars and ticks are the widened shapes.
Two enumerators feed the Studio's picker over the trait, so a remote store lists exactly like a
local one: bar_series, the one place the "bars only, interval present" filter lives, and
tick_series, the distinct (venue, symbol) pairs with any replayable tick series
(REPLAYABLE_TICK_KINDS is quote, trade, book), deduplicated because the replay merges all
three for a symbol.
Depth is recorded, listed by the store, and not replayable
UNREPLAYABLE_TICK_KIND is depth, and its exclusion is reported rather than silent:
depth_only_series names the instruments whose only recorded tick data is depth. Two independent
reasons: the replay has no depth lane — replay_ticks never reads scan_depth, so a depth-only
row would pick a slice that loads nothing — and folding depth into the book lane is the thing the
store layout exists to prevent, since depth is the conflating L2 lane and a market-making backtest
over teleporting depth reports fills it could never have got. The fix for such an instrument is to
record book, trade or quote too.
load_slice_bars errors — never panics — on a tick slice, an empty symbol list, a symbol with no
bars in range, or symbols whose series lengths disagree; the last is StrategyEngine::new's own
assert, caught here so a mismatched multi-symbol pick is a red banner rather than a worker-thread
panic. The tick path is the raw venue-ordered replay — no bar seeding, no point-in-time properties
snap, no feed-latency reordering; those knobs belong to a run profile,
not a two-click picker — and an all-empty slice is a data error rather than a flat curve.
RunError keeps Compile, Data and Strategy separate so the UI can say "no such strategy"
instead of "compile error".
The entry points
Every runner takes a StoreHandle — Arc<dyn HistStore + Send + Sync>, owned because the tick
path needs an Arc it can clone into the engine's 'static properties closure.
run_sliceresolves, loads and backtests synchronously;spawn_runis its worker-thread twin.spawn_outcomeis the onechannel()+thread::spawn+tx.send(f())site behind everyspawn_*, and the UI polls the receiver withtry_recv()each frame so nothing blocks the egui update loop.run_walkforward_slicewalks the strategy overn_splitsanchored out-of-sample windows, bar slices only and a single symbol — a tick or multi-symbol slice is aRunError::Datarather than a silent first-symbol fallback. The_with_paramsform takes a per-callEngineParamsfactory, becauseEngineParamsis notCloneand every window must use the same cost knobs. Walk-forward covers the method.run_sweep_slicebuilds the Cartesian product of a(name, values)grid, builds the strategy per point, backtests each, and ranks best-first by annualised Sharpe through the harness's one ranking comparator, so aNaNSharpe — an equity curve poisoned by a blown-up strategy — sorts last and can never sit atbest_index0. A point that fails to build is skipped; all failing is an error.StudioSweepcarries the deflated Sharpe across the trial set (every input from the sharedsharpe_momentsderivation, never the annualised display quantity) and the probability of backtest overfitting from CSCV withN_SPLITS = 16—C(16,8) = 12,870combinations, and that second number alone isNaNwith fewer than two trials or fewer than 16 observations, the deflated Sharpe being computed over whatever trials survived. Overfitting explains both numbers.compare_all_sliceruns every(name, spec)strategy over one slice, each with its own fresh defaultEngineParams, Rhai and native rows mixing freely — the Saved pane's "Compare all".
The sweep is parallel on the harness's bounded pool (map_bounded, capped by
VIKE_SWEEP_THREADS, never rayon's global pool — a pool that fails to build logs and degrades to
the ambient one): each point materialises its own copy of the slice, and a tick point re-scans the
store outright, so concurrency is an N_threads × slice memory multiplier on boxes that also host
live trading. Bar slices load once and every point clones the series. Determinism does not rest on
scheduling — the pool reassembles by index, so entries reach the ranking sort in exact grid order,
and the sort is stable.
Data and slices is the picker over this pipeline, Sweeps and Tick replay are the methods, and Remote runs dispatches the same verbs to a data service.
Rust studies
The compiled study tier — a study as a folder under user_data compiled into the operator's own binary by a build script, the entry contract, the dependency table that is its whole API, and what a study is deliberately handed and denied.
Models
The learner seam in vike-ml — two traits that make a trainer substitutable, the capture contract that keeps one fit one fit, and the one learner that drives a real LightGBM binary.