vike
ConceptsEvents

Events

The tagged union every part of Vike Trader speaks, and the three rules — emitter split, no silent vanish, exactly one terminal — that every venue adapter is machine-checked against.

Everything that happens to an order, a position or an account balance arrives as one value from one enum. vike_model::Event is, in its own words, "the tagged union the core loop consumes (and fixtures replay)" — 20 variants, serialized with #[serde(tag = "type")], spoken identically by a venue socket, the command journal and a frozen parity fixture. There is no per-venue dialect: a bridge's job is to translate its venue into this enum and stop there.

A mounted strategy is the one consumer that does not see the enum. Strategy::on_order_event takes an OrderLifecycle, "an INTERNAL, strategy-facing view the runtime derives from the venue events it already folds" — not part of the wire union, and not variant-for-variant with it: OrderLifecycle::from_event returns None for every fill event, because fills flow the richer Strategy::on_fill path (crates/vike-model/src/strategy/mod.rs).

Every payload is a plain value struct — Clone, no interior references — "so events are copy-safe across the channel boundary", which is what lets the single-writer core own them without sharing anything back (crates/vike-model/src/events.rs).

The three rules

The venue-adapter contract is not prose that adapters are trusted to follow. It is a scenario table run against every covered venue's real event_mapper and the shared exec plumbing — crates/vike-bridge-core/tests/bridge_conformance.rs, whose header states all three:

Emitter split. Rust emits OrderSubmitted synchronously at submit; the venue emits OrderAccepted / Rejected / Canceled / (Partially)Filled. The local side owns two events the venue never sees, both of them a RiskGate veto raised before the venue:

  • a vetoed submit publishes OrderDenied — ExecutionEngine::submit_order "calls the gate and publishes OrderDenied on veto or client.submit on ok";
  • a vetoed modify publishes OrderModifyRejected with a risk: reason instead, and "nothing reaches the venue" — ExecutionEngine::modify_order returns without calling client.modify.

The engine publishes no other event (crates/vike-exec/src/execution_engine/mod.rs).

No silent vanish. A dead venue path must synthesize a terminal OrderRejected. The conformance suite's TransportDeath scenario kills the path through the exact shared seam each venue really uses — ExecActor for command venues, resolve_ambiguous_submit for REST-poll venues — and requires a terminal out the other side. An order that was submitted and then simply stopped existing is the failure this rule exists to make impossible.

Exactly one terminal. The oracle is the real state machine, not a mock: ManagedOrder::apply is "the ONLY mutator of order state" (crates/vike-exec/src/order.rs), and there are five terminal statuses — Filled, Canceled, Rejected, Denied, Expired (crates/vike-exec/src/order.rs's OrderStatus::is_terminal). Liquidated is deliberately excluded from that set: it is a live perp force-close state, not a closed one. Illegal or out-of-order events are dropped idempotently rather than corrupting the single terminal — a late venue OrderCanceled replay and a late OrderCancelRejected advisory arriving after the order already closed are both dropped by one row of that four-row table, CancelAfterClose.

Two variants are advisories that end nothing. OrderCancelRejected is a venue or transport failure of a cancel request and is non-terminal — the order stays live; OrderModifyRejected is the same shape for a modify, and the order keeps its terms (crates/vike-model/src/events.rs's Event). Both exist because a failed cancel that is swallowed is indistinguishable from one that worked.

OrderModifyRejected is not always the venue's word, though: the local modify veto above arrives on it too, with risk: in front of the reason.

Which venues are held to this is itself gated: the covered and deferred lists are checked exhaustive against the canonical roster vike_model::VENUES, so a new bridge crate cannot join the venue roster and quietly escape the harness. Four are deferred with a written reason rather than faked — polymarket, dukascopy, ibkr and ctrader, each for a shape that does not fit the stateless decode seam.

How an event travels

Producers hold fire-and-forget senders from crates/vike-exec/src/lanes.rs — "the producer-side ingest lanes… the consumer (the single-writer core thread) lives in vike-core", which is why bridges depend on vike-exec and never on vike-core. The event lane is lossless: EventSender::send is an "async lossless send (venue WS tasks) — back-pressures the task, never drops", with blocking_send as the twin for non-async producers. A producer that outruns the core waits; it does not discard. See ingest lanes and the execution client seam.

Two wire tags are not their variant names

Event::Fill serializes as "FillEvent" and Event::Funding as "FundingEvent" — the tags predate the Rust spellings and are pinned by fixtures and the execution journal, so only the variant names were shortened. A consumer matches on the tag, not on the Rust identifier (crates/vike-model/src/events.rs's Event).

trade_id is a type, and it cannot be empty

venue and symbol are interned (Ustr) because their distinct-value sets are bounded over a process lifetime; trade_id is deliberately not, because it is minted per fill and unbounded. It is also TradeId, a newtype that implements no Default — so unwrap_or_default() cannot name it. An empty id did not dedup badly, it skipped dedup entirely: the measured consequence on a live daemon was re-applied fills double-booking commission and realized PnL, and on the reconcile side an empty id can never match, manufacturing a MissingFill divergence (crates/vike-model/src/events.rs's TradeId). See reconciliation.

The pages in this section

One page per variant, generated from the enum itself: who emits it, what it carries, and whether it ends an order's life. For how the variants compose into a working order's life, read the order lifecycle; for what the fill lane does to positions and realized PnL, the account; for how a whole session of events becomes replayable, the journal.

On this page