set_trading_state
The account kill switch as an MCP tool — three states, applied to every engine the node runs, gated like every other write on confirm plus the single-use preview_token, and unable to arm a venue.
set_trading_state sets the account trading state on a running vike-tradehub node. It is one of
the seven order-write tools vike-cli mcp exposes, annotated destructiveHint in tools/list, and
it needs --node <host:port> plus VIKE_TRADEHUB_CONTROL_KEY — taken from the process environment
first, then from the credential store the daemon itself reads
(crates/vike-cli/src/cmd/mcp.rs's module header; crates/vike-cli/src/cmd/nodekeys.rs's
CONTROL_KEY_ENV).
It is the kill switch, and it is not a venue arming control
The tool's own description says both halves, and it says them because an agent reached for this
tool to put a venue live. It moves an already-mounted account between active, reducing and
halted; it cannot move a venue off the paper simulator, and neither can any other tool on this
server. Which venues may go live is policy.venues.<venue> under [venues] in
<project>/settings/policy.toml, read above the credential read by
crates/vike-mount/src/lib.rs's make_engine and only ever able to refuse
(crates/vike-config/src/venue_mode.rs's VenueMode::cap is a minimum, never a maximum). It is a
file a human edits on the box, and nothing reachable from here can write it. The remedy an agent
invents when it does not know that — "set it in the node's environment and restart" — arms nothing
and leaves the operator on paper with no error. Arm one venue and
policy.toml are the pages that do answer it.
Input schema
| Parameter | Type | Required | Notes |
|---|---|---|---|
state | string | yes | active, reducing or halted — lower case. Anything else is a tool error naming the three |
reason | string | no | The rationale recorded in the node's audit trail |
confirm | boolean | no | Must be the JSON boolean true and be accompanied by preview_token — either one missing returns a preview |
preview_token | string | no | The token this tool's preview returned. Fires once, expires after 60s, bound to the command it previewed |
The three spellings are the whole vocabulary: crates/vike-cli/src/cmd/verbs.rs's
verb_from_tool_args matches "active" / "reducing" / "halted" and returns
unknown state {other:?} (active | reducing | halted) for everything else.
What each state means
The states are a strict ladder — Halted admits less than Reducing, which admits less than
Active — enforced in the risk gate, not in this tool.
crates/vike-exec/src/risk.rs's TradingState is the definition and RiskGate's check is where
each one bites:
active— normal trading.reducing— "only position-reducing orders allowed". An order that does not reduce is denied with the reasonreduce-only.halted— "no new orders (kill switch)". Denied withhalted, with one deliberate exception: a position-covered reduce (vike_model::is_covered_reduce— side opposite the position, size within it) is still admitted, so a halt stops you opening risk without trapping you in it.
A call, and what comes back
The transport is newline-delimited JSON-RPC 2.0 on stdin/stdout (crates/vike-cli/src/cmd/mcp.rs
hand-rolls it — no SDK). One line in, one line out:
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"set_trading_state","arguments":{"state":"halted","reason":"feed gap on the primary venue"}}}With no confirm, nothing is sent. Server::call_tool's write arm returns preview_of(...),
whose structuredContent looks like this — the values below are from a server with no node
configured, which is why the verdict is absent:
{
"will_execute": false,
"tool": "set_trading_state",
"wire_command": { "SetTradingState": "Halted" },
"guardrail": { "within_limits": true, "note": "no order size to check for this verb" },
"node_verdict": null,
"verified_by_node": false,
"preview_token": "pv-1",
"reason": "feed gap on the primary venue",
"note": "PREVIEW ONLY — nothing was sent. ..."
}wire_command is pinned by set_trading_state_preview_maps_the_wire_command in the same file. The
guardrail cell is Guardrail::NoSize: verbs::guardrail_check only sizes a Submit, so every
other verb reports trivially within limits. node_verdict is the node's own dry-run and is null
here only because there was no node address or control key to ask with; the note spells out the
confirm rule, names the node's own gates as the enforcing ones, and — on this unverified path —
says in words that the guardrail is a client-side estimate rather than an approval.
To execute, send the same arguments again with both "confirm": true and that
"preview_token". Either field alone returns a fresh preview and moves nothing, which is not an
error and is easy to mistake for success — read will_execute. The token fires once, expires after
60 seconds, and is bound to this exact command, so a preview of halted cannot be used to confirm
active; Live node is the full rule
set.
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"set_trading_state","arguments":{"state":"halted","reason":"feed gap on the primary venue","confirm":true,"preview_token":"pv-1"}}}Server::execute awaits the ticket that send returned
and answers for that command — outcome: "accepted" with an empty client_order_id (this is an
account-wide verb), isError on a refusal, or outcome: "unknown" after the two-second ACK_WAIT
deadline, in which case the command may still execute and
node_snapshot is what settles it.
Scope: every engine, not one venue
crates/vike-core/src/runtime/mod.rs's Command::SetTradingState arm writes the primary engine's
trading_state and then every extra engine's. One call moves the whole node, which is why the
accepted response carries no client order id.
It is also never size-capped. crates/vike-tradehub/src/server.rs's ControlLimits::vet consumes a
rate token on every command, but its notional ceiling applies only to the order-increasing verbs
that carry a price; cancel, flatten, mass-cancel, market-exit and set-trading-state are
risk-reducing and exempt by construction.
You do not need `active` to escape a halt
market_exit and its mass-cancel are usable from a halted core with no precondition — the covered
reduce a flatten mints is exactly the shape the gate admits under Halted
(crates/vike-exec/src/lanes.rs's MarketExit), with one residual: the gate re-takes that
verdict against the lot-rounded size, so a position sitting off the lot grid rounds its own leg
up past itself and that leg alone comes back denied with the reason halted. This is the reverse
of the rule that held for the verb's whole life: the gate used to deny every order under Halted,
so an operator had to un-halt first, re-arming the strategy that caused the halt in order to escape
it. Reach for market_exit, not for set_trading_state active.
`active` does not clear the HALT file
The file sentinel (crates/vike-bridge-core/src/halt.rs) is a separate, cross-process kill switch
checked at the ExecActor submit boundary, and it exists precisely to work when the runtime is
wedged. Setting the trading state back to active does not remove it. Either dead-man's switch
under the default cancel_all_and_halt action engages both at once — the silence one
(crates/vike-core/src/runtime/deadman.rs) and the connection one
(crates/vike-core/src/runtime/link_deadman.rs), which is armed unless policy.toml writes
link_deadman_grace_ms = 0 — so clearing one leaves the other armed, and on a mount where the
connection switch arms this file can appear with no operator having touched anything.
Re-arming a latched drawdown
The optional drawdown latch sets every engine to Reducing and never un-latches itself. A manual
set_trading_state active re-arms it against the same running high-water mark, so a still
underwater account latches again on the next closed-bar sweep —
CoreThread::sweep_drawdown_latch in crates/vike-core/src/runtime/watchdog.rs states the rule.
What is recorded, and how to see the result
An accepted command is recorded under the kind set_trading_state with an empty coid
(crates/vike-tradehub/src/audit.rs's record). Any reason is trimmed client-side, sanitized
server-side by sanitize_reason — control characters stripped, capped at MAX_REASON_CHARS — and
never reaches the order, the core fold, the journal or a venue.
The result is visible in the node's snapshot: trading_state at the top level and
venues[].trading_state per venue. Concepts: Halt,
Risk and arming.
Halt trading on the node — reason: the primary feed has gone quiet and I want to look before anything else goes out.
mass_cancel
Cancel every live order at once, optionally scoped to a venue and symbol — behind the confirm-plus-preview_token gate, and with one scoping combination that is deliberately ignored rather than guessed at.
node_snapshot
The one read tool against a running vike-tradehub node — orders, positions, per-venue equity, recent events — and the tool that settles an ambiguous write.