vike
Concepts

Mounting

A run profile names the whole assembly in one auditable file, a strategy mount has a deterministic identity with a durable state sidecar beside it, and runtime mounts survive a restart through a sidecar of their own rather than through the journal.

Three modules in vike-core answer three questions about a mounted strategy: what assembly it runs inside (crates/vike-core/src/run_profile.rs), what it is called and where its durable state lives (crates/vike-core/src/strategy_state.rs), and how a mount added at runtime comes back after a restart (crates/vike-core/src/mount_topology.rs).

The run profile

RunProfile is the single TOML-deserializable artifact that names a full runtime assembly — event source, broker, sinks, and the risk and guard stack — so that backtest, paper and live become one auditable file instead of code-only CoreConfig construction by each binary. mode is backtest, paper or live; [event_source] is hist, replay or live_venue; [broker] is paper or venue. RunProfile::validate refuses the combinations that do not make sense — backtest needs a paper broker and cannot take a live venue feed, live needs a venue broker and a live venue feed. Every struct is deny_unknown_fields, so a typo'd key is a hard parse error rather than, in the module's words, a silently ignored max_levarage.

The sections map onto real types and the mapping is compile-checked: [risk] becomes vike_exec::RiskLimits, [guards] fills CoreConfig knobs — the stuck-order watchdog timeouts, the drawdown latch, the margin-call config — and [sinks.journal] becomes the journal config. apply_guards_and_sinks returns a GuardsReport naming every key that is set and still reaches nothing, and evaluates the confirm-grace pairing on the final pair the mount will run with; that check warns rather than refuses, and ConfirmGraceHazard's doc argues why against the degrade-versus-refuse record.

The venue owns the instrument grid; the operator owns the risk budget

RiskLimits holds both concerns in one struct — tick_size/lot_size/min_qty/min_notional from the venue's fetched grid, and the notional, exposure, rate and leverage caps from the operator. RunProfile::grid_source derives which is allowed from mode alone: a backtest/paper profile never fetches a grid and may supply one, a live profile always fetches one and may not, and validate rejects a live profile whose [risk] sets any venue-owned field, unconditionally, at load. A live mount additionally refuses to start unless max_notional_per_order and max_total_exposure were supplied — vike_mount::MountError::MissingRiskBudget names every missing key in one message, because no universal safe default exists for an account-dependent cap.

resolve_profile takes an explicit path (a --profile flag) over VIKE_RUN_PROFILE from an injected variables map, never the process environment, and a resolved path that fails to read, parse or validate is an error rather than a silent fall-through to built-in defaults. Three sample profiles ship as string constants under run_profile::samples, one per mode.

Mount identity and the state sidecar

strategy_state::mount_id_of(venue, symbol, interval) sanitizes each part (every non-alphanumeric character becomes _) and joins them with __ — mount_id_of("binance", "BTC/USDT", "1m") is binance__BTC_USDT__1m. It is pure and total, so a restart derives the same id a prior run wrote. mount_id_with adds the one thing the derivation cannot express: an explicit controller_id, when the mount carries one, is the mount id, sanitized the same way — which is how two strategies can sit on the same (venue, symbol, interval) triple without sharing one sidecar and one journal identity. None, empty and whitespace-only ids fall back to the derivation verbatim.

The sidecar is <state_dir>/<mount_id>.json. The runtime loads it when a strategy is mounted (Strategy::load_state) and writes it on a clean shutdown (Strategy::save_state), both gated behind the opt-in CoreConfig::state_dir — None by default, in which case nothing in the module is ever called. write_json_atomic writes a .tmp sibling, fsyncs it, then renames over the final path with a remove-then-retry fallback for platforms where rename refuses to replace. read_json is fail-open: a missing or corrupt sidecar reads as "no saved state", never a load-time crash. A periodic save also runs while any mount exists when CoreConfig::state_save is set — and unlike the equity sampler it does not wait for an open position, because a breaker or an estimator matters whether the book is flat or not.

Runtime mounts survive a restart — through a sidecar, not the journal

A mount added over the wire (WireCommand::MountStrategy, behind the node's strategy-mount-verbs feature) is not a journal record. Command::MountStrategy and Command::UnmountStrategy are excluded from the journaled set, and mount_topology.rs explains why the exclusion stands: the journal is the replay determinism fence, and the replay core spawns with no strategy factory — vike-core sits below the strategy crates and can resolve nothing — so a journaled mount could only ever replay as a refusal note. Topology is session state, not order state.

So it lives in runtime_mounts.json beside the state sidecars, under the same opt-in state_dir, invisible to the state hash and the replay core. The daemon reads the file after its core spawns and before feeds arm, and re-sends each record as an ordinary Command::MountStrategy through the same command lane a wire mount takes, meeting the same refusals. A record holds the spec exactly as the command carried it — what to ask the factory for, never what the factory answered — so a resurrected mount meets today's registry, today's script and today's refusals.

The lifecycle is asymmetric on purpose. A successful mount upserts its record; an explicit unmount removes it; a clean shutdown removes nothing, so a runtime mount survives systemctl restart exactly as it survives a crash — "the operator said stop the daemon" is not "the operator said unmount". Profile mounts never enter the file, because the profile re-creates them at every boot. An absent file is the ordinary empty state; a file that exists but does not parse is warned about loudly and read as empty, so a corrupt topology can never refuse a boot, and the next successful mount rewrites the whole list.

Nothing locks these files

The only writer is the single-writer fold thread of one core. Two cores pointed at one state_dir already share and clobber each other's <mount_id>.json sidecars and would share the topology file the same way; the journal's duplicate-instance interlock is what makes that deployment shape loud. An unmount over the wire cancels that mount's attributed live orders and saves its durable state before removal; positions are deliberately not flattened. Which account a mount trades on is part of its spec and is persisted with it.

On this page