Stop, liquidate, restart
What a stop leaves at the venue, what the kill switch does and in which order, and what a restarted node actually recovers.
Three verbs, three outcomes. A stop ends the process and, by default, leaves both your resting orders and your positions at the venue. A liquidation is the kill switch: cancel everything, flatten everything, halt. A restart recovers less than most operators assume, and the venue's books come back through reconciliation, not replay. None of the three is a side effect of another.
Stop: the process, or one mount
Both headless daemons share one stop primitive (crates/vike-ops/src/stop.rs): a single
AtomicBool raised by any of several triggers — the shutdown/quit word on a TTY stdin,
SIGTERM/SIGINT (so systemctl stop), and on Windows the console control events plus a SHUTDOWN
file under the state directory (vike_ops::stop::STOP_FILE_NAME), because a detached Windows
process has no console to be Ctrl-C'd in. The handler stores a bool and nothing else; the teardown
runs on the daemon's own path. Before the handler existed, systemctl stop ran not one line of
Rust: the resting book was abandoned at the venue on every restart of every box, while
systemctl status reported a clean stop.
The teardown, in order (CoreThread::run in crates/vike-core/src/runtime/mod.rs): drain what is
queued; if flags.cancel_orders_on_shutdown is on, run cancel_resting_on_shutdown
(crates/vike-core/src/runtime/watchdog.rs) — a MassCancel through the one order-write path,
marked risk-off so no venue budget sheds it, ungated by trading state so it works from a Halted
core; detach the venue clients; write the exit Snap if journaling is on; save each mount's
strategy-state sidecar. The whole sequence sits inside [daemon] shutdown_deadline_ms (default
5_000, crates/vike-tradehub/src/config.rs's DaemonSettings) via
vike_ops::shutdown::run_with_deadline: a wedged venue delays the stop, it cannot prevent it, and
whatever is still in flight at the deadline is abandoned. The Telegram grammar has no stop verb at
all.
A stop never closes a position
cancel_orders_on_shutdown cancels; it does not flatten. The sweep's own log line says it:
"POSITIONS ARE NOT CLOSED — this leaves no ORDERS, not a flat book." And the flag is OFF by default
(crates/vike-core/src/runtime/mod.rs's CoreConfig::cancel_orders_on_shutdown, the standing
statement of the stop policy): teardown detaches and exits, and your quotes stay resting at the
venue with nothing left running to manage them — which is
what an operator restarting for a deploy usually wants, and why the safer-sounding value is not the
default. Set it in <project>/settings/flags.toml (flags).
A hard kill — kill -9, taskkill — runs none of this: no cancel sweep, no state save, no terminal
journal snapshot.
Stopping one strategy without stopping the process is the runtime unmount
(Command::UnmountStrategy; crates/vike-core/src/runtime/mod.rs's unmount_strategy_runtime).
It cancels exactly the mount's attributed orders before removal — an order left resting after
nothing routes fills to its strategy is an unmanaged book — and, like the shutdown sweep, it does
not flatten: the position stays open, visible in the residual attribution row, and closing it is a
Flatten you issue.
Liquidate: the kill switch
market-exit (vike-cli trade's Verb::MarketExit; /marketexit on
Telegram) is OrderIntent::MarketExit
(crates/vike-exec/src/lanes.rs): a compound verb the runtime expands at apply time into one
MassCancel first, then one Flatten per non-flat net position derived from the post-cancel
account (a hedge-mode long/short leg has no net bucket for Flatten to size against, so it is
skipped) — each a reduce-only MARKET order for |position|, lowered through the same apply_intent
site as any order, so mint → RiskGate → client stays structural. It is usable from a Halted core
with no precondition: the gate admits a position-covered reduce (vike_model::is_covered_reduce —
the order must oppose the position and be covered by it), which is exactly the shape Flatten
mints. The gate re-takes that verdict on the lot-rounded size, so the one leg a halt can still
deny is a position sitting off the lot grid, whose flatten rounds up into a flip. So market-exit
and state halted work in either order.
It is best-effort, not atomic. Against a real venue the mass-cancel is fire-and-forget over the
adapter's actor thread; its ACKs land asynchronously, possibly after the flatten market orders are
already on the wire, so a resting order can still fill after a flatten leg. Check positions
afterwards and re-issue the exit if the board is not flat.
Then close the door. state halted sets TradingState::Halted on the engine and the RiskGate
denies everything except a covered reduce — the in-process gate. The cross-process gate is a file:
touch <project>/settings/state/HALT # the very next submit is refused
rm <project>/settings/state/HALT # resumecrates/vike-bridge-core/src/halt.rs resolves the path once per process — VIKE_HALT_FILE if
set, else <project>/settings/state/HALT, else <exe_dir>/HALT as a reported last resort — probes
that it can be created, and logs HALT sentinel path resolved (or HALT KILL SWITCH IS NOT ARMABLE, as an error) at the first mount. With the file present, every venue on the shared
ExecActor refuses an opening submit with a terminal OrderRejected carrying
HALT_REJECT_REASON, admits a reduce_only submit, passes cancels through and blocks modifies. It
needs no IPC and no responsive runtime, which is what it is for. Nothing writes it for you except a
tripped dead-man's switch; state halted does not. Halt carries the full
admit table.
The file boundary trusts the reduce_only flag
ExecActor holds no position book, so at that boundary the caller-asserted flag is the only
evidence in existence (crates/vike-exec/src/halt.rs's halt_admits_submit). A strategy bug that
tags entries reduce_only can open risk through a HALT file on a flat book; every such admission is
logged. The RiskGate in front of it is the verifying gate, and policy.toml's
halt_admit = "verify" tightens the file boundary only where the adapter holds a book — cTrader
alone today; everywhere else it degrades to admit and says so at mount.
The dead-man's switches are the automatic twin, and there are
two of them with opposite defaults. The connection one
(crates/vike-core/src/runtime/link_deadman.rs) trips on a link a venue's own bridge has reported
Disconnected for longer than policy.toml's link_deadman_grace_ms — a key that is armed when
absent, at two minutes — and cancels that venue's resting orders. The silence one
(crates/vike-core/src/runtime/deadman.rs) trips on ingest gone quiet past deadman_timeout_ms
and cancels everything; that key is off unless written, because a market that closes is silence
too and an armed default halted every FX and equities mount at every session close. Under the shared
default action DeadManAction::CancelAllAndHalt either one also sets Halted on every engine and
writes this same sentinel; plain cancel_all leaves the trading state as-is. Either way the latch
re-arms on recovery — Halted and the file do not. vike-tradehub's live mount is the only thing
that constructs either switch; vike-desktop sets neither, so a desktop session has no automatic stop
of any kind.
Armed by default is not armed everywhere
The connection switch arms for a venue only when that venue's bridge discloses a disconnect and
this daemon subscribes the lane that carries it. On a default live mount both hold for binance,
bybit, okx and aster — the quote/trade/book tick pump this daemon subscribes now discloses its own
transport state onto the core tick lane — and for polymarket behind its Cargo feature. Every FX and
equities venue is off because its market has sessions and a close is not an outage, and seven roster
venues are off because their bridges disclose no disconnect at all. So an FX-only or equities-only
mount still has no automatic stop for "the link died and we still have orders out", and no line in
policy.toml changes that. The daemon logs one line per mounted venue at startup saying
armed-or-why-not — read those for the box in front of you, and see
the dead-man's switches for what would move a venue.
Restart: what comes back
Up to two interlocks fire first. A configured journal directory takes an exclusive advisory lock
(crates/vike-core/src/journal_lock.rs, acquired on the CommandJournal::open path and so only
when journaling is on), which makes restarting before the old process has died a loud AddrInUse
naming the directory, never a second writer; a crashed process's lock is released by
the OS, so a leftover LOCK file is never stale. Each armed live venue claims
<state_dir>/LIVE-<venue>.lock (crates/vike-ops/src/live_lock.rs): one live process per venue
account, because two of them see each other's activity as PositionDrift. A paper mount claims
nothing.
What a restarted vike-tradehub recovers:
- Profile mounts — re-created from the profile at every boot.
- Runtime mounts — the topology sidecar (
crates/vike-core/src/mount_topology.rs), replayed byresurrect_runtime_mountsafter the core spawns and before feeds arm. A clean stop deliberately keeps its records: "the operator said stop the daemon" is not "the operator said unmount". Only an explicit unmount forgets one. - Strategy state —
<state root>/strategy-state/<mount_id>.json(crates/vike-core/src/strategy_state.rs; the id isbinance__BTC_USDT__1m-shaped, or the sanitized controller id), loaded throughStrategy::load_stateat mount and saved on a clean shutdown or the periodic save. Fail-open: an absent or corrupt sidecar means no saved state, never a refused boot. See Mounting. - The HALT file — it is a file. A halted node restarts halted until you
rmit.
What it does not recover:
- The order and position book.
crates/vike-core/src/replay.rs'srestore_from_journalcan rebuild the engines, the coid session, the armed conditional and contingency books and the per-mount ledgers from the latestSnap— and no shipped binary calls it (crates/vike-exec/src/account.rs'sseed_seen_fill_idssays so in as many words). The journal itself is opt-in (VIKE_JOURNAL_DIR, or a run profile's[sinks.journal]); what it buys today is offline replay. Engines start empty, the fill-dedup ledger starts empty, and the venue resync lane is floored at spawn (crates/vike-bridge-core/src/exec_actor.rs'sis_pre_spawn), so history from before the process started is never re-folded through it. Adopting that state is reconciliation's job:PositionDriftre-converges position size, and the daemon defaults its policy toquarantine, so a restarted node holds the drift for your confirm instead of rewriting the book at the venue's average price unattended. - Orders you did not cancel. They rest at the venue under a process that no longer exists; the
new one meets them as
UnknownOrderdivergences — quarantined, with nothing to adopt unlessgenerate_missing_ordersis on, and adoption is narrowed to terminal orders. Cancel them by hand, or turncancel_orders_on_shutdownon. - Attribution. Post-restart fills on pre-restart orders are unattributed and land in the
residual row (
CoreConfig::coid_mounts). - The in-process halt.
Haltedlives on the engine, and the engine is new.
Never trade the account by hand while a mount runs
The mount cannot tell your order from a stranger's. Anything at the venue that nothing local
explains — an order row, a position row, a net-qty residual, a cash move — is tagged
DivergenceOrigin::External (crates/vike-exec/src/recon/types.rs), and you and a running mount
are exactly the two processes live_lock exists to keep apart. Under hybrid, PositionDrift
auto-applies: local position size is rewritten onto the venue's number and realized PnL is booked at
the venue's average price with no operator in front of it; a foreign fill inside the lookback
arrives as a coid-less MissingFill. external-quarantine holds both for a claim. If you must act
at the venue, state halted first, and expect reconciliation to raise what you did under whichever
policy the node runs. Stuck orders have their own path:
Triage a stuck order.
Control the daemon from Telegram
A second remote write surface on the headless daemon, off at compile time, behind four gates and a mandatory preview.
Record a live feed
One TOML profile, one daemon, and a tape of the markets you actually watched — plus the two watchdogs that tell you it is really recording.