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.
The control boundary
The live core has exactly one external control surface — commands and order intents in, snapshot queries out — and every front end, in-process or remote, is a translator over it.
Accounts
More than one account per venue — a credential-key grammar that cannot misread an existing key, a per-account arming ceiling that inherits nothing for a second account, and a shared-book rule that reports instead of refusing.