vike
Concepts

Halt

An engaged halt stops orders that open risk and admits orders that close it; the two mechanisms that enforce that rule agree on policy and differ only in how much they can verify.

A halt in Vike Trader is a file. While the sentinel exists on disk, every submit that would open or add to a position is refused at the venue adapter's submit boundary. An operator engages it with touch <HALT file> over ssh and the very next submit is refused — no IPC, no running event loop, no responsive window, only a filesystem exists() check at the submit seam. The sentinel is owned by crates/vike-bridge-core/src/halt.rs; the decision about what a halt lets out is crates/vike-exec/src/halt.rs's halt_admits_submit; the policy is crates/vike-model/src/halt_admit.rs. This page is written from those three.

Where the file is

The path resolves in three rungs: VIKE_HALT_FILE when set to a non-blank value; otherwise <project>/settings/state/HALT, the one directory this workspace writes program-owned state into, with which project taken from the composition root's own boot walk so a deployment that names its settings directory moves the sentinel with it; and, only when no project resolves at all, <exe_dir>/HALT. The resolution is probed once per process at mount time by halt_path_arming_error — error when an operator could not create the file, info when they could — because a kill switch that is not armed looks exactly like one that is, right up until it is needed. Rung 3 is reported as a warn carrying EXE_DIR_FALLBACK_ADVISORY: in a container the binary lives in the image while <project> is a bind mount, so a sentinel beside the binary is armable and an operator touching the project's file on the host arms one this process never reads.

What an engaged halt lets out

  • A submit that opens or adds is blocked. A terminal OrderRejected is synthesized through the normal event path with the exact reason HALT_REJECT_REASON = halted: HALT file present, so the intent never silently vanishes.
  • A submit that reduces is admitted. Under the default policy the whole rule is request.reduce_only.
  • Cancel is always allowed.
  • Modify is blocked on every client: a modify can add size or chase price, and reduce-only cannot be inferred from the request alone. The exit path under halt is a cancel or a reducing submit, so blocking modify traps nobody.

The reducing arm exists because a cancel is not an exit. A cancel removes a resting order; leaving a position requires sending one. OrderIntent::Flatten mints a reduce_only market order for |position|, and until the exemption existed market-exit under a HALT file ran its mass-cancel and then had every flatten leg rejected, leaving the operator halted with the position still open. The promise the module doc states first is halting cannot trap you, and it outranks halting is airtight.

halt_admits_submit is one shared function rather than an if in each client because several independent implementations enforce the sentinel — the shared ExecActor, hyperliquid's bespoke client and vike_paper::PaperExecutionClient — and the original pair had already drifted once (both blocked submit, only one blocked submit_batch). It lives in vike-exec because the paper client must refuse under a halt for the same reason a live one does — an operator rehearses the switch on a paper daemon — and vike-paper may not grow a transport dependency to reach it.

Two mechanisms, one policy, different verification

The in-process twin is vike_exec::RiskGate under TradingState::Halted. It holds the position book, so it admits only a position-verified reduce — crates/vike-model/src/scalar.rs's is_covered_reduce: the order must oppose a non-zero position and be covered by it (|position| >= |qty|), or carry the flag and satisfy both direction and coverage. A flat-book reduce_only order is an opening order there. The adapter boundary holds no book — a command channel, an event lane and a sentinel path — so the caller-asserted flag is the only evidence in existence, and halt_admits_submit takes it.

The honest residual

A strategy bug that tags its entries reduce_only can open risk through a HALT file on a flat book, and nothing anywhere catches that. Three things bound it: every submit reaching the boundary was minted by the core and has already passed the RiskGate; the gate's own Reducing state trusts the same bare flag; and real perp venues reject a reduce_only order that would increase a position — but only where a position exists to check against. The admission is logged on every occurrence for exactly that reason.

The halt_admit policy

policy.toml's halt_admit key (vike_config::Policy::halt_admit, on the policy settings page) selects a vike_model::HaltAdmit mode. admit, the default, is the flag-trusting rule above, byte-identical on every venue with evidence ignored. verify is strictly tighter: still reduce_only, and additionally refused when the venue's own position book proves the submit opens risk. halt_admits_submit_under weighs a three-state PositionEvidence — Unknown with its reason, Flat, or Opposing(qty) — and refuses only Flat. Every absence admits: never fetched, fetch failed, an answer that could not be read in full or was for another account, socket dropped since, poisoned lock, unresolvable symbol, and a book holding no position in the symbol at all. Opposing admits at any magnitude, even smaller than the order, because the adapter caps an oversized reduce and refusing it would deny a genuine partial exit. That verify admits only a subset of what admit admits is pinned by verify_is_a_subset_of_admit_over_every_input.

vike_model::halt_verify_support says where verify is real. One venue answers Real today — cTrader, the only adapter holding a position book at its halt boundary — and every other roster venue degrades to admit with a written reason, reported once at mount by vike_mount::make_engine; a cTrader mount that fell back to paper says so too, since the paper client holds no book either. cTrader extends the predicate as closes || admits: a book is evidence for a reduce and never against one, because it holds nothing until the venue answers a reconcile, and a restarted daemon can have heard nothing about a position the account holds.

There is deliberately no third mode

A refuse mode with no exemption was the behaviour until 2026-08-06 and was removed because it trapped operators: halting denied reduce_only too, so the panic button was disarmed in exactly the situations that reach a halt on their own — a fold panic, either dead-man's switch. verify degrades toward admit on every unknown, never toward refusal, and an unknown spelling is rejected at load by name.

What else engages it

The file is the manual switch. Its automatic twins are the two dead-man's switches — the connection one, armed unless policy.toml writes link_deadman_grace_ms = 0, and the silence one, off unless that file arms it. Either sets trading_state = Halted on every engine under the default cancel_all_and_halt action and writes that same HALT file, so this sentinel can appear with nobody having touched anything; what a trip is scoped to differs, but the halt half is process-wide in both. The connection switch arms only for a venue whose bridge discloses a dead link on a lane this daemon subscribes, which is narrower than its default suggests — its own page carries the per-venue reading. The watchdog's safe-state entry halts in-process after a fold panic. OrderIntent::MarketExit — the panic button — is usable from a halted core, because a flatten leg is exactly the position-covered reduce the gate admits; the one leg it still refuses is a position sitting off the lot grid, whose |position| rounds up on the wire and would flip it. The check runs only at the submit and modify boundary, never per message, so the latency gate is untouched.

On this page