vike
Scripting

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.0

The 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 — the StrategySpec to 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.

On this page