Presets
How a strategy's preset.toml becomes the params it is constructed with — the flat-table rule, the two roads a Rhai and a built-in strategy take, and the keys that are named rather than dropped.
A preset is a named parameter set belonging to exactly one strategy: <strategy>/<preset>.toml,
flat beside the entry file or under presets/. crates/vike-studio-core/src/user_strategies/preset.rs
is the resolver — the header calls it the half that was missing while Preset was a type nothing
ran — and its whole job is turning that file into the params a strategy is constructed with without
silently doing nothing.
A preset is the params table
The shipped sma_cross/fast.toml (crates/vike-cli/src/cmd/init/content.rs's PRESET_SMA_FAST):
# Preset: fast — a short-horizon sma_cross, for intraday bars (1m–15m).
fast = 5.0
slow = 15.0
qty = 1.0The keys are flat: they are the keys a profile's [strategy.params] receives one for one.
load.rs's check_preset_shape enforces that at scan time and refuses two shapes that would
otherwise be accepted and then quietly do nothing. A [params] wrapper — { params = { fast = 5 } }
— merged into [strategy.params] gives the strategy one key called params that no reader looks
at, so every knob silently keeps its default, the worst outcome available because the user did
everything else right; silently unwrapping it instead would make two shapes legal and pick between
them by an invisible rule. And a src key is the strategy's source, not a knob: allowing it would
let a params file smuggle a whole script past --script. Only the exact wrapper shape is refused,
so a legitimately nested knob such as the [venues] table funding_carry reads is untouched.
Preset names are matched case-insensitively (fast.toml and Fast.toml are one file on Windows
and macOS), and the flat position wins a clash with presets/, the loser reported as
ShadowedPreset.
Two roads to one destination
There is exactly one destination: a TOML table, the same one a profile's [strategy.params]
carries and the harness registry's strategy_by_name hands to each strategy's from_params
reader. The header is explicit that the two StrategyBody kinds reach it differently, and that
pretending otherwise is how a preset would silently do nothing.
A built-in strategy takes the table whole. StrategySpec::Native { name, params } is the
construction call, so every key survives with its TOML type — size = 3 an integer,
symbol = "BTCUSDT" a string, a nested table a table.
A Rhai script can only receive numbers. Its knobs are param(name, default) calls, and
RhaiStrategy::compile_with_params bakes an IndexMap<String, f64> into the script scope — the
same lane a [sweep] grid point uses. A string or bool in a preset for a Rhai strategy has
nowhere to go.
The Rhai type gap is reported, not swallowed
The harness's rhai_overrides drops a non-numeric param in silence — the lenient convention for a
table somebody typed into a profile. The header calls that the wrong answer for a preset, whose
whole promise is "these values, applied": a user who wrote symbol = "BTCUSDT" in a preset for a
Rhai script must be told it cannot arrive. PresetRun::dropped carries those key names, and a
caller must surface them.
The resolution
resolve_preset(report, strategy, preset) resolves against an already-scanned LoadReport — the
module reads no filesystem — matching the strategy case-insensitively across both trees, so a
preset for a built-in under strategies/rust/buy_hold/ is found exactly as a Rhai one is. Both
errors name what was found, because the question a person is asking is "then what should I have
typed": UnknownStrategy lists every strategy that did load (an absent name is how a user discovers
their folder failed to load; the reason is a diagnostic in the same report), and UnknownPreset
lists the presets the strategy does have.
The result is a PresetRun — deliberately not a boxed strategy, because a caller usually wants
to show the resolution before or instead of building, and a sweep builds one strategy per grid
point from the same resolution:
spec— theStrategySpecto construct: Native carries the preset's table verbatim; Rhai carries the script source, with the params riding separately.overrides— the(name, f64)pairs for the Rhai path, in the table's own sorted order so two runs of one preset produce identical lists. Always empty for a built-in.dropped— the keys that could not be carried. Always empty for a built-in.
PresetRun::build constructs the strategy through run.rs's build_strategy_with, the same call
a Run makes, so "the preset resolved" and "the preset runs" are one claim; is_lossless() is the
one-line test a caller gates its warning on.
The typo a numeric key can hide
split_numeric accepts a TOML float or integer and names everything else, but it cannot see the
dangerous case: fastt = 5.0 is perfectly good TOML and perfectly numeric, so it reaches the
engine, matches no param(...) call, and the run silently uses the default — a result for a
configuration nobody asked for. unread_keys catches it on the Rhai path, and only there: a script
declares its knobs by calling param(), and vike_script::discover_params runs the top level to
collect them, so any override key the script never declares joins dropped. A built-in strategy
has no such declaration — the registry has no param spec — so its keys cannot be checked, and
UserStrategy::run_with deliberately does not pretend to. A script that fails to compile yields no
declared set, and one whose top-level run calls param() nowhere yields an empty one; either way
the answer is "cannot tell", so nothing is flagged rather than everything. The empty case is
deliberate — discover_params runs only the top level, so a script whose knobs are declared inside
a hook body would otherwise have every key false-flagged.
Presets on the command line
vike-cli backtest --preset <p.toml> reads a preset file locally and merges its keys into the
shipped profile's [strategy.params], last-wins, before --script injects the source. The verb's
header explains why the preset is a local path rather than a name in the profile: only the profile
text crosses the wire, and the server is typically another machine, so a [strategy] preset = "fast"
field would resolve against the server's user_data/, not the author's. merge_preset_params
restates the same two refusals — the [params] wrapper and the src key — in the CLI crate,
because that crate cannot depend on vike-studio-core without dragging DataFusion into a laptop
binary. See vike-cli backtest.
On the live daemon the same posture applies in the script's vocabulary: under [strategy] rhai = "<path>",
crates/vike-tradehub/src/config.rs's DaemonProfile::validate refuses a non-numeric
[strategy.params] value at load — param takes an f64, so a quoted number configures nothing —
and resolve_script refuses any key the script's top level never passes to param(…), so a
preset's knobs that survive resolution here are the ones that would mount there — except in the
knob-less case above, where the daemon has no such exemption and refuses outright the keys
unread_keys passes over.
What a preset configures is on Writing a strategy for a script and under the strategy templates for a built-in; where the files live is User strategies; the profile they merge into is Run profiles, and the grid that varies the same knobs is Sweeps.
User strategies
A user's strategies are files under user_data/strategies, not rows in a blob — the layout, the two trees, the scan that compiles every script, and its named diagnostics.
Migration
The one-time move of the Studio's studio_strategies.json rows into user_data/strategies/ — what each row becomes, what is reported instead of written, and why the JSON is never touched.