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
OrderRejectedis synthesized through the normal event path with the exact reasonHALT_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.
Contingent orders
One-triggers-other arming and one-cancels-other cancellation are decided by a single resolver that every fill lane calls, so the paper book and the backtest broker can no longer disagree about what a bracket does.
Margin calls
A pure margin-call model over the live account computes liquidation intents that the core submits through the normal risk gate, on one liquidation law whose pool is the parameter, priced by the same resolver the operator sees.