vike
ConceptsOrders

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.

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".

PrefixRaised when
ORDER_KIND_UNSUPPORTEDorder_type is a known non-trigger kind the venue adapter does not wire
TRIGGER_UNSUPPORTEDorder_type is a trigger kind the venue adapter does not wire
TIF_UNSUPPORTEDa limit order's time_in_force is outside the venue's accepted_tifs
MARGIN_MODE_UNSUPPORTEDan 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.

On this page