The portable registry
Name to boxed strategy, generic over the broker — the one table that serves both the simulator and the live daemon, and the four refusals it makes possible.
vike_strategy::registry resolves a strategy NAME plus a TOML params table to a boxed strategy —
strategy_by_name<B: HftBroker> returns Box<dyn Strategy<B> + Send> — and the bound is the whole
point. HftBroker is a sub-trait of Broker, and BOTH the simulator's SimBroker and the live
core's LiveBroker implement it, so ONE table serves a backtest and a live mount. The box is
+ Send because the live core moves the strategy onto its own thread; it unsize-coerces to the
plain Box<dyn Strategy<SimBroker>> the simulator's callers want. It did not always live here: the
registry used to be the backtester's, returning a type only the simulator can name, so a daemon
could not run a strategy it had backtested without linking the whole simulator. The fix was the
BOUND, not a new crate.
PORTABLE_STRATEGIES is the roster of ten names, kept in step with the resolver's own match by
registry_lists_every_match_arm. Two further tables state, as DATA rather than prose, what is NOT
here: SIMULATOR_ONLY (seven names, each with its written reason — see
simulator-only strategies) and SCRIPT_ONLY (the inline-source
rhai arm; scripts mount live by PATH instead). capability(name) folds all
three into the one answer a caller needs before mounting: Live, NotLive(reason),
SimulatorOnly(reason), or Unknown.
Resolving is not trading
LIVE_CAPABLE is the per-name live-mount verdict, one row per name with a written reason for every
"no", machine-checked exhaustive against the roster by live_capable_table_is_exhaustive. It
exists because the failure mode is a SILENT NO-OP rather than a compile error or a panic: a strategy
whose input never arrives mounts cleanly, logs nothing unusual, and simply never submits. Each "no"
names the missing input — a funding series no live kline lane carries, a second mount leg no mount
surface declares — so flipping a row is a factual claim somebody can check.
Four refusals over one params table
A from_params reader almost never fails: every one in the workspace but the Avellaneda–Stoikov
maker's ignores what it does not recognise, so qtyy = 0.005 is not an error — it is qty at the
strategy's compiled default. (The one exception is narrow: vike_mm::SpreadMaker::from_params,
which both spread_maker and gueant_maker resolve through, refuses a PRESENT enum key —
spread_model, kappa_mode, style and five siblings — whose value names no variant or is not a
string at all, and the registry raises RegistryError::BadParams so the profile stops at load. An
absent key still keeps its default, and its numeric keys stay as lenient as everyone else's.) In a
backtest a knob at its compiled default is a wrong chart; on a mount it is a live order at a size
nobody typed, behind at most the optional policy.max_notional_per_order ceiling.
crates/vike-tradehub/src/config.rs's DaemonProfile::validate_strategy is the consumer that
refuses, with the SAME strictness on a paper mount as on a live one — a rehearsal that silently ran
different parameters would conceal exactly what it is for.
| Reader | The case it catches |
|---|---|
unknown_params | a key no reader reads |
mistyped_params | a declared key whose VALUE no reader can take |
misrouted_params | a key that is read, well-typed, and then OVERRIDDEN BY THE MOUNT |
unarmable_params | a whole table that passes all three and still describes no order at all |
A wrong type fails as silently as a wrong key
Every accessor but the maker's eight enum readers is an and_then(Value::as_…), which yields None
for a value of the wrong type exactly as it does for an absent one, so .unwrap_or(default) lands
on the compiled default either way. size = "2" — quoting a number, the most ordinary TOML slip
there is — mounts size = 1 just as surely as sizee = 2 does. Those eight refuse a wrongly-typed
value outright, and the maker's numeric knobs default as silently as everyone else's. That is why
PARAM_KEYS declares a ParamType per key, not just a name.
PARAM_KEYS names, per strategy, exactly the keys its reader looks at and the TOML type that reader
accepts; crates/vike-strategy/tests/param_keys_gate.rs scans the readers' own source in BOTH
directions, keys and types alike. A row may decline to enumerate — the Avellaneda–Stoikov maker's
roughly 60-key bag does, with a written reason — and its consumer then owes its own stricter rule.
The daemon's is to refuse a [strategy.params] table under those two names outright and build the
maker from the profile's own fields instead, which is strictly stronger than key-checking.
Route keys
PARAM_ROUTES declares which declared keys name a ROUTE — an instrument, a venue, or a
SYMBOL = "venue" routing table — rather than a knob of the strategy's own arithmetic. A route key
is the one kind of declared key that can be well-spelled, well-typed, genuinely read and still
configure nothing: crates/vike-core/src/runtime/strategy_drive.rs's resolve_intent_symbol and
resolve_intent_venue stamp the MOUNT's own pair onto every intent while no mount declares legs,
which is every mount this workspace builds. MEASURED on prod2 before misrouted_params existed: a
buy_hold profile whose params named a different symbol than the mount loaded, announced the params
symbol, and filled on the mounted one. Multi-leg names are declined rather than judged — their keys
name legs, not this mount's market — and are refused by NAME one step earlier anyway.
The honest echo
resolved_params re-runs the same reader the mount used and reports what each declared key LANDED
ON. That is not the same as echoing the profile: a reader may clamp, fall back on an unrecognised
string, drop a row, or supply a default the profile never mentions. Printing the operator's own TOML
back is a claim about the operator, and it is false in exactly the case worth logging.
One class stays deliberately un-refused: a key that passes all four checks and is still read by
nobody, because another key in the same table sent the reader down a different branch. Closing that
class is a test rather than a table — crates/vike-strategy/tests/param_gates.rs's
an_ungated_key_moves_the_strategy drives every enumerated strategy over a scripted market and
fails, by name, when a declared key does not change the call trace.
Strategies
The one Strategy trait, the Broker handle it trades through, and why a strategy written against the common denominator runs unchanged on the simulator and on the live core.
The controller seam
The Controller decides whether to open a position; the ControllerHarness is itself a Strategy and owns the executors, the routing and the cooldown.