Orders
The five canonical order kinds, the trigger subset, and the capability preflight that refuses an unsupported combination at the core edge with a machine-readable reason.
An order leaves a strategy as an OrderRequest — the single serde schema authority for order
intents, crates/vike-model/src/order.rs's OrderRequest — whose order_type is a plain String.
The canonical vocabulary that string is drawn from has five words, and
crates/vike-model/src/venue_caps.rs's ORDER_KINDS is the authority for it: "every canonical
OrderRequest.order_type string the platform produces, lowercase — the vocabulary
preflight_order classifies". The field's own comment in order.rs names four of them —
stop_limit, which alpaca and hyperliquid both declare, is the one it omits.
- Market —
market - Limit —
limit - Stop —
stop - Stop-limit —
stop_limit - Take-profit —
take_profit
These names are Vike Trader's, not any exchange's wire strings; a bridge translates them at the
edge. A string outside the list is deliberately not refused — the ORDER_KINDS doc says "a string
OUTSIDE this list is deliberately NOT preflight-refused (no declared row can know it) — the
venue-side path keeps owning it".
Triggers are a subset, and an empty row is a refusal
TRIGGER_KINDS is declared as "the trigger subset of ORDER_KINDS": stop, stop_limit,
take_profit. The venue holds the trigger, and crates/vike-model/src/venue_caps.rs's
TriggerType has exactly two variants — StopLoss (order_type: "stop", trigger_price armed
venue-side, firing "as market, or as limit where the adapter also wires a stop_limit kind") and
TakeProfit (whose doc names hyperliquid's tpsl:"tp" as "the one adapter that wires it natively
today"). An EMPTY trigger_types slice means trigger orders are unsupported on that adapter: the
preflight refuses them "rather than letting a 'stop' silently coerce into an immediate market/limit
order".
The two axes cannot drift apart: expanded_axes_invariants_hold asserts, for every roster venue,
that a non-empty trigger_types holds exactly when supported_order_kinds contains one of the
three trigger strings.
Which price series a trigger watches is a separate field. OrderRequest::trigger_by is an
Option<TriggerBy> over Last, Mark and Index; None means the venue's default series, which
that enum's doc records per adapter — bybit hardcodes triggerBy: "LastPrice", binance perp and okx
send no field and inherit a last-price default, hyperliquid "no field exists — triggers evaluate
against MARK by venue law". Some(source) asks the adapter to honor that source, and one that
cannot express it "must synthesize a terminal OrderRejected (deny loudly — never silently
substitute a different trigger series)".
Preflight: refused before the wire, with a reason a machine can read
crates/vike-model/src/venue_caps.rs's preflight_order is the core-edge capability check: it
validates an outgoing request against its venue's declared VenueCaps row before the request
reaches the ExecutionClient seam. An Err means the
caller "must synthesize a loud TERMINAL refusal ... never send the order, and never let it silently
vanish".
PreflightDeny carries one variant per checked axis, and its Display is machine-readable —
CATEGORY_CONDITION: key=value ..., e.g. TIF_UNSUPPORTED: tif=Fok venue=ig — "so an operator
surface or a log grep can dispatch on the prefix without parsing prose".
| Prefix | Raised when |
|---|---|
ORDER_KIND_UNSUPPORTED | order_type is a known non-trigger kind the venue adapter does not wire |
TRIGGER_UNSUPPORTED | order_type is a trigger kind the venue adapter does not wire |
TIF_UNSUPPORTED | a limit order's time_in_force is outside the venue's accepted_tifs |
MARGIN_MODE_UNSUPPORTED | an explicit margin_mode request the venue adapter cannot honor |
ORDER_KIND_UNSUPPORTED is declared but unreachable today. preflight_refuses_the_silent_lie_class
asserts that every roster venue wires both market and limit, and an undeclared venue is skipped
outright, so no request can raise it. The variant is kept so that a future row dropping one of those
two kinds re-arms the class.
The function is built around a compat law its own doc states: "it refuses NOTHING a venue adapter
accepts-and-honors today". An unknown venue string passes untouched; a combo request passes (the
OrderIntent::Combo lowering already resolved supports_combo); an order_type outside
ORDER_KINDS passes; the TIF check runs on the limit path only, since "no venue reads a request TIF
on its market/trigger paths"; coerced-but-live TIFs sit in accepted_tifs and so pass; a
margin_mode of None always passes, meaning "the venue's default behavior". What is refused is
"the silent-lie class": kinds an adapter would coerce into different semantics, TIFs it would
silently ignore, margin modes no adapter honors.
The refusal is a full lifecycle, not an error return.
crates/vike-core/src/runtime/apply.rs's synthesize_capability_reject registers the order so the
FSM has an entry to advance, then publishes OrderSubmitted followed by OrderRejected carrying
that reason string — see order lifecycle and the per-event pages
under events. It runs after the journal write on purpose: "a refused
order is exactly the audit trail worth keeping".
The row is a fact about an exchange, not about a route
preflight_order_at takes the capability venue explicitly, because OrderRequest::venue "does two
jobs at once" — it is what the core routes on and what selects the caps row — while routing is a
per-account question and a caps row is a per-exchange fact. Should a routing key ever name something
outside the canonical roster, the unknown-venue affordance answers Ok(()), which is "EVERY check
below skipped, silently, for one account of one exchange". The live core therefore resolves the
engine first and passes ExecutionEngine::venue.
Post-only is declared but unreachable
VenueCaps::supports_post_only is false for every venue on the roster, and the field's doc gives
the reason: OrderRequest has no post-only field, "so no adapter can wire one (hyperliquid's Alo
and bybit's PostOnly exist at the venues but are unreachable); the axis is declared now so the
field's arrival flips rows, not schemas". The matrix pin asserts it venue by venue —
"post-only is inexpressible via OrderRequest".
A capability row states what the adapter wires, not what the exchange offers
The module's honesty rule: "every field reflects what the ADAPTER wires TODAY, not what the exchange
could theoretically do", and an unverifiable capability is declared conservatively false. Note too
what a bridge's own caps_test proves — it asserts the same constant against itself through
caps_for, so it "does not execute the adapter" and "cannot catch a row that was wrong when
written, nor one that went stale when the adapter changed underneath it". The per-venue rows are
rendered on the venue pages.