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.
A profile's [strategy] name resolves through crates/vike-backtest/src/harness/registry.rs's
strategy_by_name: a registry name plus its TOML params table becomes a
Box<dyn Strategy<SimBroker>>. An unknown name is a validation error at load time — the harness
fails the profile fast rather than silently no-opping on a typo. Dispatch through the box is
deliberately dynamic: the harness runs one strategy per profile, so a single vtable indirection
per bar or tick is noise next to loading and replaying the data slice.
Two halves of one registry
This file is the simulator half. The portable half moved down to
crates/vike-strategy/src/registry.rs, whose strategy_by_name<B: HftBroker> is generic over the
broker so the same table serves the backtest simulator and the live daemon. The reason is a
dependency one: a return type naming SimBroker made "run the strategy I backtested" impossible
for the headless daemon without depending on vike-backtest, which would drag the whole
simulator — and, under the DataFusion feature, the Arrow tree — into the binary that signs real
orders.
What stays here is what genuinely cannot leave: five reference strategies implemented against
SimBroker by design, two portable strategies that reach this crate's own internal modules, and
the rhai arm, whose vike-script dependency sits at the same layer rank as vike-strategy and
so cannot be named from there. Everything else falls through to the portable registry,
monomorphised at SimBroker.
STRATEGIES is the union of both halves, in this order:
buy_hold rotation_top_k bracket_per_symbol gated_weights caps_sizers_mask
tick_pair_mse cheap_catch_updown_fair_value sport_copy_follower grid
dca_accumulate spread_maker gueant_maker trailing_scalper momentum
funding_carry funding_capture pairs_zscoreThe order is wire-visible — the data service's strategy listing documents "in its declared
order" — so the split kept it byte-identical rather than grouping the halves. Every name on it
resolves with default params; backtest --list prints it, and the Studio consumes it as its
native-strategy list. The ten portable names each have a page of their own, from
buy_hold to
pairs_zscore; the registry they come from is
the portable registry.
The seven simulator-only names
vike_strategy::registry's SIMULATOR_ONLY carries, as data any consumer can read, the names that
cannot leave vike-backtest and the reason for each — so a daemon refusing one can say why
rather than "unknown strategy". Two distinct reasons, and the distinction matters:
| name | why it stays |
|---|---|
rotation_top_k | simulator-bound: reads the SimBroker symbols/schedule fields directly |
bracket_per_symbol | simulator-bound: measures the attached protective stop the SimBroker fill loop resolves |
gated_weights | simulator-bound: drives the SimBroker cash-gate weight submit argument |
caps_sizers_mask | simulator-bound: drives the position sizer via SimBroker's raw=false submit path (a 999.0 sentinel size) |
tick_pair_mse | simulator-bound: measures SimBroker::position_of against the engine's latency shadow |
cheap_catch_updown_fair_value | lives in the simulator crate: reaches its internal cheap_np_ask/fair_value modules — portable in principle, not yet moved |
sport_copy_follower | lives in the simulator crate: reads a signal series the replay harness synthesizes — portable in principle, not yet moved |
The first five name simulator machinery that no registry work makes live; the last two are
genuinely portable impl<B: Broker> strategies that merely live here, and moving them is future
work rather than a law. The backtest side asserts the table names exactly its retained arms, so
the two cannot drift. The same table renders
simulator-only strategies.
The rhai arm
rhai resolves here and is deliberately absent from STRATEGIES: it requires a src param —
the inline script source — so it is not default-resolvable, and listing it would break every
consumer that enumerates the roster and resolves each name with empty params. A missing src is
a fail-fast validation error; every other numeric param is a param(name, default) override,
the same knobs a [sweep] varies, baked into the script scope at compile through
RhaiStrategy::compile_with_params. A Rhai parse or compile error surfaces as a validation error
at load, not a mid-backtest panic. Integers and floats are both accepted for a numeric knob, so
size = 1 reads the same as size = 1.0.
Scripts do mount live — by path, not by this name
vike_strategy::registry's SCRIPT_ONLY classifies the name, not the script path. The daemon
links vike-script directly and mounts a script by path, [strategy] rhai = "<path>", never
through this registry; the inline-src arm here remains the backtest spelling. That scripts mount
live at all is a recorded architecture verdict, and the SCRIPT_ONLY row carries it as the reason
a refusal on the name redirects to the path spelling rather than claiming the strategy does not
exist. See writing a strategy.
User strategies, consulted last
When neither half knows a name, the resolver consults the user registry —
vike_user_strategies::user_strategy_by_name, compiled from user_data/strategies/rust and empty
in any checkout without one — before giving up. Built-in arms are tried first, so a user folder
can never shadow a registry name. The final error names both rosters, the built-ins and the
user's, so an operator can tell "not compiled in" from "typo'd a built-in". Authoring one is
authoring in Rust.
The five reference strategies are #[derive(Default)] with no harness-tunable knobs today —
Default::default() is their documented constructor. cheap_catch_updown_fair_value and
sport_copy_follower are genuinely param-driven and read their tables the way buy_hold does.
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.
Runs on disk
What a finished run leaves under user_data/runs — a common manifest every producer writes identically beside a report only its kind understands — and how the report's Sharpe is annualized from the profile's interval.