vike
Guides

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   # resume

crates/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 by resurrect_runtime_mounts after 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 is binance__BTC_USDT__1m-shaped, or the sanitized controller id), loaded through Strategy::load_state at 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 rm it.

What it does not recover:

  • The order and position book. crates/vike-core/src/replay.rs's restore_from_journal can rebuild the engines, the coid session, the armed conditional and contingency books and the per-mount ledgers from the latest Snap — and no shipped binary calls it (crates/vike-exec/src/account.rs's seed_seen_fill_ids says 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's is_pre_spawn), so history from before the process started is never re-folded through it. Adopting that state is reconciliation's job: PositionDrift re-converges position size, and the daemon defaults its policy to quarantine, 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 UnknownOrder divergences — quarantined, with nothing to adopt unless generate_missing_orders is on, and adoption is narrowed to terminal orders. Cancel them by hand, or turn cancel_orders_on_shutdown on.
  • Attribution. Post-restart fills on pre-restart orders are unattributed and land in the residual row (CoreConfig::coid_mounts).
  • The in-process halt. Halted lives 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.

On this page