vike
Concepts

The execution core

One dedicated thread owns all mutable trading state and is its only mutator; everything reaches it through one totally-ordered queue, so determinism is by construction rather than by locking.

vike-core is the live core. One dedicated OS thread — vt-core — owns the ExecutionEngine (account, order registry, risk gate, venue client) and is the only mutator of any of it. Everything else in the process reaches that state through one ingest queue, totally ordered, and reads it back through an immutable snapshot. The authority for this page is crates/vike-core/src/runtime/mod.rs, whose own header calls this the pinned architecture.

Three lanes into one queue

Three kinds of message arrive, and each has a different contract with its producer:

  • Exec events — fills, order transitions, account and funding updates, liquidations — are lossless. A venue adapter pushes them with blocking_send, so a slow core back-pressures the venue task and never the GUI.
  • Commands from the GUI or a control surface use try_send into the same queue. The GUI thread never blocks. A full-queue rejection comes back to the caller as CommandRejected::Busy and is counted in the snapshot as rejected_commands, so it cannot be silent; CommandRejected::Gone means the core thread has exited.
  • Market data goes through a latest-wins conflation slot plus a marker message. The producer overwrites the slot (a drop counter is surfaced) and at most one marker is ever in flight, so a slow core sees the freshest price without unbounded queueing.

On the core side the thread sits in blocking_recv() — there is no tokio runtime on the hot thread — then drains opportunistically with a try_recv batch bounded by CoreConfig::batch_max before publishing, which keeps latency bounded. The producer-side types for all three lanes live in crates/vike-exec/src/lanes.rs, not in vike-core, so that a venue bridge depends on vike-exec and never on the core above it; the ingest lanes page is theirs.

The snapshot is a lossy observer

The core publishes a CoreSnapshot into an arc_swap cell that the GUI reads on repaint. Publish is dirty-flagged and coalesced: at least every CoreConfig::snapshot_interval while busy (about 16 ms by default — the one number on this page that is a live constant, held to the code by crates/vike-ops/tests/docs_constants_gate.rs), and immediately when the queue goes idle. The build never runs per-event while busy.

On a quiet venue, publish is per-event work

The idle-path publish is ungated by the interval. On a venue whose events arrive sporadically the core is idle between them, so it publishes after every one. Anything CoreSnapshot::build does is therefore per-event work, not per-interval work — which is why deferring a render "to publish time" once turned out to be a pessimization rather than a saving: it relocated the work instead of avoiding it (p99 2039 ns deferred, against 606 ns rendered at push and a 481 ns baseline).

The fold is latency-gated

The drain loop is the thread the core-hop tail gate measures — crates/vike-core/tests/runtime_latency.rs, run in CI on a runner whose cores are reserved for it. Every ceiling in that file is a derived constant with its data on it, and only three of its harnesses gate (baseline, journal, journal-snap); the rest measure, asserting no timing beyond the snapshot build's pre-existing 1 ms sanity check, because calibrating one ceiling there cost 98 CI attempts and an uncalibrated ceiling is a flaky gate, which teaches people to re-run rather than read. The latency gate page is where a number is read.

Two consequences follow for anyone touching the core. Nothing on the fold path may log per-message — instrument per-order boundaries and fault transitions only. And everything that can run off the fold belongs in vike-ops, which exists to consume the published snapshot, event and journal streams without ever sitting in this loop.

What happens when a handler panics

Every dispatch runs under catch_unwind. On a panic the core does not die and does not keep trading. It enters safe-state: trading_state becomes Halted, so the risk gate denies every new order; a best-effort cancel is issued for every non-terminal working order; and the fault is surfaced in the snapshot. A possibly-inconsistent engine is never traded on again — only read for display — until the process restarts, which is what makes wrapping it in AssertUnwindSafe sound.

The drain-loop boundary

Cold-path work does not ride the per-message dispatch. It is armed on a DeadlineTimerWheel and runs at the boundary of a drain, and runtime/mod.rs's TimerKind is the list of what may fire there: the stuck-order watchdog sweep, the equity sampler, the periodic strategy-state save, the two dead-man's-switch sweeps — one per switch, each on its own window — managed good-till-date expiry, the periodic portfolio journal record, and the fast in-flight confirm — the reject-free early rung of the stuck-order ladder, which only re-queries and never terminalizes. Every one is opt-in through CoreConfig, and when none is configured the wheel stays empty and the boundary advance is skipped. The timers, watchdog and dead-man's switches pages carry each.

Two more pieces sit around the loop. JournalConfig is the opt-in write-ahead command journal: None — the default — opens nothing and leaves the fold path byte-identical, while Some journals every exec-lane message before it folds, with the segment, flush and snapshot cadence the [sinks.journal] profile section uses. And the live strategy mount is the path through which a Strategy drives a LiveBroker — the same mount whether the venue behind it is paper or live. Each has its own pages; this one is only the thread they all hand their work to, and the portfolio and snapshot page is what comes out the other side.

On this page