vike
Research

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_slice resolves, loads and backtests synchronously; spawn_run is its worker-thread twin. spawn_outcome is the one channel() + thread::spawn + tx.send(f()) site behind every spawn_*, and the UI polls the receiver with try_recv() each frame so nothing blocks the egui update loop.
  • run_walkforward_slice walks the strategy over n_splits anchored out-of-sample windows, bar slices only and a single symbol — a tick or multi-symbol slice is a RunError::Data rather than a silent first-symbol fallback. The _with_params form takes a per-call EngineParams factory, because EngineParams is not Clone and every window must use the same cost knobs. Walk-forward covers the method.
  • run_sweep_slice builds 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 a NaN Sharpe — an equity curve poisoned by a blown-up strategy — sorts last and can never sit at best_index 0. A point that fails to build is skipped; all failing is an error. StudioSweep carries the deflated Sharpe across the trial set (every input from the shared sharpe_moments derivation, never the annualised display quantity) and the probability of backtest overfitting from CSCV with N_SPLITS = 16 — C(16,8) = 12,870 combinations, and that second number alone is NaN with fewer than two trials or fewer than 16 observations, the deflated Sharpe being computed over whatever trials survived. Overfitting explains both numbers.
  • compare_all_slice runs every (name, spec) strategy over one slice, each with its own fresh default EngineParams, 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.

On this page