The ExecutionClient seam
The one trait every venue bridge implements — only submit and cancel are required, every state change comes back as an event, and one method exists precisely so a venue never overrides it.
Every venue bridge implements one trait: vike_exec::ExecutionClient, declared in
crates/vike-exec/src/execution_engine/client.rs and isolated from the engine so the contract
reads on its own. The engine holds it type-erased — Box<dyn ExecutionClient + Send> — and never
knows which venue is behind it. That is the whole point: the core, the risk gate and the order
state machine are written once, and a venue's job is to translate. Each page under
venues is one implementation of what this page describes.
Two methods are required
fn submit(&mut self, request: &OrderRequest);
fn cancel(&mut self, client_order_id: &str);They are the only order-mutating calls that leave the core, and both are fire-and-forget. Neither returns a result, because the venue's answer does not arrive on this call — it arrives later, as an event, through the ingest lane the adapter was handed at construction. That is true of every method on the trait: every state change comes back as a venue event, never as a return value.
Everything else has a default
| method | default behaviour | who overrides it, and why |
|---|---|---|
cancel_with_intent | drops the CancelIntent and calls cancel | a venue that meters its cancels. The engine calls this, never cancel, so an override sees every cancel the core issues |
modify | no-op — the order keeps its terms | a venue with an in-place amend. Takes the whole resting OrderRequest, not just its id, because some venues need side, quantity and price together |
submit_batch / cancel_batch | fan out to the per-order call | a venue with a native batch endpoint |
cancel_batch_with_intent | drops the intent and calls cancel_batch | a venue that wants the whole batch, with its intent, as a single unit; Polymarket is the only declaration in the tree, for the latency of an emergency flatten |
confirm | no-op | a venue with an order-status re-query. The stuck-order watchdog is its caller; on a client that cannot re-query, the watchdog's last-resort reject still backstops it |
detach | no-op | adapters with something to release |
poll_events | None | in-process test clients only — a real adapter's events arrive over the ingest channel |
on_bar | no-op | the paper exchange, which fills its resting book on each closed bar before the strategy runs |
amend_semantics | None — "ask the venue table" | the paper exchange only, see below |
Two contracts hide in that table. confirm must run its re-query off the core fold thread, on
the adapter's own thread — it is the watchdog's way of prodding a wedged adapter into answering
rather than only waiting on it, and a blocking re-query here would stall the fold it exists to
protect. And the boxed impl delegates every method explicitly, because trait-object defaults would
otherwise shadow a concrete client's overrides: a boxed intent-aware client that fell through to a
default would silently report every cancel as unclassified.
Why a cancel carries an intent
CancelIntent is the one cross-venue classification of why a cancel is being issued: Routine
(maker ladder churn), RiskOff (a declared flatten, market exit, dead-man trip or safe-state
sweep), or Unspecified. It exists because a venue-side cancel budget is a real liveness hazard —
a maker that spends its whole cancel bucket on requote churn is rate-limit-locked at the moment it
needs to pull a book — and holding a reserve back is only possible if the shedding decision can
tell the two apart.
Unspecified is the flatten-safe default, not a don't-care
A caller that cannot classify its cancel gets the treatment an emergency gets — fire it, never
hold it back — because the failure modes are not symmetric: shedding a routine cancel costs one
stale quote for one refill window, while shedding an emergency one leaves a live order resting in
the situation the operator was trying to get out of. CancelIntent::may_be_shed is the one place
that says only Routine may ever be held back, so a venue never re-derives the rule from a
match and is never one _ => arm from inverting it. A venue that overrides cancel_with_intent
must write cancel in terms of it, or a direct cancel call silently skips the override.
Where the adapter runs
The trait is synchronous and the venue is not, so an adapter owns a thread. The shared shape is
crates/vike-bridge-core/src/exec_actor.rs's ExecActor: an mpsc channel plus a dedicated OS
thread draining submit, cancel and shutdown, with a Drop/detach that sends shutdown and joins
the thread — deterministic teardown, because a stranded feed thread is a crash on process exit.
Each venue supplies only its command loop, and for the REST-driven crypto venues even that loop is
shared. Venue replies are pushed back to the core with EventSender::blocking_send. Dukascopy is
the one adapter that owns a child process instead — there is no REST execution API for that venue,
so a Java sidecar drives execution over JSON lines on stdio. In both cases the core thread never
waits on the wire.
The actor is also where the operator halt bites first: submit and
modify consult the halt file sentinel before touching the command channel. While it exists, a
submit the caller has not flagged reduce_only is refused with a synthesized terminal
OrderRejected, and a reduce_only submit is admitted — and logged, because this boundary holds
no position book and can only trust the flag. modify is blocked either way, since it can add size
or chase price and reduce-only cannot be inferred from the request alone; nothing is synthesized
there, the resting order simply keeps its terms. A cancel is deliberately never gated, and a cancel
alone would not discharge the rule: it removes a resting order, whereas leaving a position
means sending one, so a halt must let an operator reduce or exit, never trap a position. It works
even when the runtime is wedged.
The emitter split
Who emits which event is fixed, not a per-venue choice. The Rust side emits OrderSubmitted
synchronously at submit. The venue side emits OrderAccepted, OrderRejected, OrderCanceled,
OrderFilled and OrderPartiallyFilled. A dead venue path — a transport that died mid-order, a
sidecar that never answered — must synthesize a terminal OrderRejected rather than let the order
vanish. The order lifecycle page carries the three rules and
the harness that machine-checks them against every venue's real decoder.
The one method a venue adapter should never override
amend_semantics answers "what does my modify's quantity mean" — the new total, or the new
remaining size — which is the fact ExecutionEngine::modify_order needs before it may net a
partially filled order's executed lots out of the pre-trade projection. None means "consult
vike_model::amend_semantics for the engine's venue string", which is the right answer for every
real venue, because that table is where the per-venue fact is declared and tested.
It exists because the engine's venue string is a label, not a client. vike_mount::make_engine
builds the engine with the real venue id even when the arming ceiling or the absent-credentials
gate fell back to vike_paper::PaperExecutionClient, so a paper mount on a venue whose amend is
in-place-total looks up that row while the paper book's modify assigns the amend quantity to the
order's remaining size — netting there would let the gate assume q − filled while the book
executes q. Declaring the convention on the client means it travels with the object that
implements it and no mount can forget to pass it. The
paper exchange is therefore the only override in the tree, and
the method's own documentation says so.
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.
Order lifecycle
Every order folds through one state machine with one mutator, must reach exactly one terminal state, and may never silently vanish — and a shared harness proves that against every venue's real decoder.