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 publishesOrderDeniedon veto orclient.submiton ok"; - a vetoed modify publishes
OrderModifyRejectedwith arisk:reason instead, and "nothing reaches the venue" —ExecutionEngine::modify_orderreturns without callingclient.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.
Locking
The duplicate-instance interlock — one exclusive advisory lock on a sentinel file per journal directory, taken before any segment is mapped, released by the operating system on any exit, and never taken by a reader.
Fill
The Fill event: what it carries, which side emits it, and whether it ends an order's life.