vike
Concepts

The control boundary

The live core has exactly one external control surface — commands and order intents in, snapshot queries out — and every front end, in-process or remote, is a translator over it.

crates/vike-core/src/control.rs names the external control boundary of the live core in one place, and its contract fits in a sentence: Command and OrderIntent go in through CoreHandle::try_command (non-blocking, the desktop app's path) or CoreHandle::send_command (lossless, for session control and tests), and CoreSnapshot queries come out through the snapshot's accessors — orders_for, order, open_order, position, last_mark, equity, trading_state. The desktop app, the CLI and the MCP server are translators over that seam. Nothing bypasses it.

Why one seam matters: the risk gate is structural

Every order-scoped verb is an OrderIntent, and every intent from every origin lowers through the core's single apply_intent site: mint a client-order-id, route, run the RiskGate, hand the order to the venue client. crates/vike-core/src/runtime/apply.rs lists the origins it serves — the external command path, a mounted strategy's drain, a conditional order firing, a margin-call liquidation — and notes that a source-scan test in the control-boundary suite enforces that no engine submit call lives outside that file. The consequence for a reader is that there is no path by which a front end can place an order that the risk gate did not see. It is a property of the code's shape, not a convention each caller has to remember.

Fire-and-forget

Commands have no synchronous reply. The core is a single-writer fold, and a synchronous order handle was considered and rejected. A caller that must know its client-order-id pre-mints it: a non-empty client_order_id on the request is respected, and an empty one asks the core to mint. Outcomes are observed through the snapshot — rejected_commands counts queue rejections, and a RiskGate veto surfaces as an OrderDenied event in recent_events. Everything about an order's later life arrives as venue events and is read from the published snapshot, which is how the order lifecycle is meant to be watched.

The two entry points differ only in what happens when the ingest lane is full. CoreHandle::try_command is a try_send: it never blocks, a full queue returns CommandRejected::Busy and increments the same rejected counter the snapshot surfaces, and a closed lane — the core has exited — returns CommandRejected::Gone. CoreHandle::send_command is a blocking_send into the same lane: every command queued ahead of it still folds first, and a closed lane is ignored, matching shutdown_and_join.

Why the GUI path may drop and the session path may not

crates/vike-core/src/runtime/mod.rs pins the ingest design: one dedicated thread owns all mutable trading state and reaches it through one totally-ordered queue. Exec events from venues are lossless and back-pressure the venue task — never the GUI. Commands from the GUI use try_send so the GUI thread never blocks, and a full-queue rejection is returned to the caller and counted in the snapshot, so it cannot be silent. Market data rides a latest-wins conflation slot with at most one marker in flight. The two command paths are the same lane with two policies for the full case.

The out-of-process write half

CoreHandle::command_sink hands out a CommandSink: a narrow, cloneable capability holding only the ingest lane and the shared rejected counter. It is what the headless daemon's control server holds, and it lowers a remote Scope::Control peer's order command into the same apply_intent seam — carrying no snapshot read, no market slot and no join handle, so a connection can neither read state nor shut the core down. Its try_command is, by its own doc, the exact body of CoreHandle::try_command, so mint → RiskGate → client stays structural on the network path too.

The sink also carries send_blocking, the lossless twin, for the operator's own stdio loop — a human at a terminal may wait for their typed order. The remote server path deliberately keeps try_command: a network peer must never be able to back-pressure a per-connection thread. How a remote peer reaches the sink at all — keys, framing, the handshake — is the node protocol.

Building another front end

The module's own guidance for a new adapter is short. In-process — a headless mode or a REPL inside the running core binary — holds the CoreHandle and the snapshot cell directly; no transport is needed. Command is already serde, so the recommended first cut for anything else is newline-JSON over stdio. Out-of-process additionally needs a transport and Serialize on the snapshot view types; that layer was parked until a real consumer existed, and the write half of it now does, as the CommandSink above. The CLI's trade verb and the agent tools are both translators in exactly this sense: they compose commands, submit them, and read the snapshot.

On this page