vike
Tools

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

ParameterTypeRequiredNotes
statestringyesactive, reducing or halted — lower case. Anything else is a tool error naming the three
reasonstringnoThe rationale recorded in the node's audit trail
confirmbooleannoMust be the JSON boolean true and be accompanied by preview_token — either one missing returns a preview
preview_tokenstringnoThe 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 reason reduce-only.
  • halted — "no new orders (kill switch)". Denied with halted, 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.

On this page