vike
Concepts

Emulated orders

The core keeps its own book of stop and trailing-stop arms for venues and strategies without native conditional orders; arming bypasses the risk gate, and only the fire crosses it, as a plain market order.

crates/vike-core/src/emulator.rs's ConditionalBook is the core-side emulation of conditional orders — a fixed stop or a trailing stop — for venues, or strategies, without native support. It is a port of the Python platform's conditional book, and its trigger predicate is vike_model::order_fill_price, the same parity-pinned oracle the backtest engine fires on, so an emulated stop triggers by exactly the law the backtest applies. This page is written from that module, the OrderIntent arms in crates/vike-exec/src/lanes.rs, and the firing sites in crates/vike-core/src/runtime/strategy_drive.rs.

Arm, fire, disarm

Registration bypasses the gate; the fire goes through the one live path. OrderIntent:: ArmConditional carries a ConditionalIntent — venue, symbol, side, quantity, and either price for a fixed stop trigger or trail for a trailing distance, whose extreme seeds from the current mark at apply time and is refused into the recent-events ring when no mark exists. The runtime mints an arm_id and inserts the arm; the book is keyed by that id, so a duplicate is refused with the book unchanged and the resting arm is never silently replaced. Live ids come from a monotone counter and cannot collide; the keying is the structural backstop.

When a check fires, each FiredConditional — arm id, side, quantity and the oracle's trigger price, diagnostic only since the live fill is the venue's — is released by CoreThread::submit_fired as a plain market request through apply_intent, so mint, RiskGate and client stay structural on the release; a veto surfaces as OrderDenied. OrderIntent::DisarmConditional { arm_id } drops exactly that arm; an unknown or stale id is a loud no-op surfaced to recent-events, never a panic and never silent. Like arming, a disarm bypasses the gate — removing a resting emulated arm is not an order reaching a venue. OrderIntent::MassCancel with no venue and no symbol clears every conditional book along with every resting order.

The trigger law

A fixed stop fires when a bar crosses its price, under the gap-open adverse fill law: a bar that gaps down through a sell-stop fills at the worse open, not the trigger. A trailing stop's extreme ratchets in place on a no-fire bar, exactly as in the Python book and the backtest, and the check runs against the prior extreme's trigger before ratcheting — so a new-high bar cannot stop itself out. Fired arms are removed and returned; survivors keep their ratcheted extremes. Walk and fire order is the book's insertion order, and a disarm in the middle does not reorder the survivors (shift_remove, never swap_remove).

Which price series fires it

Each arm carries the vike_model::TriggerBy it was requested with, and every check names the PriceLane its price came from; the two meet in ArmedConditional::evaluates_on. The type exists because an unmodelled trigger source was a live-versus-backtest divergence: with last at 99 and mark at 101, a stop at 100 fires on a mark-triggering venue and on no last-triggering one, and no emulator could reproduce the timing.

  • None or Last arms evaluate on trade and bar prices — today's law, byte-identical. check_bar runs against every closed bar of the mount's series, before the strategy step, which is the oracle's firing order. With CoreConfig::conditionals_on_ticks (default false, oracle-faithful bar-close only) the book is also checked against each tick of the engine's series through a degenerate one-price bar — the intra-bar triggering the Python book explicitly lacks.
  • Mark arms evaluate only on the mark lane. check_mark is fed from the conflated mark drain and is always on when a mark arm rests — deliberately not behind conditionals_on_ticks, because the mark lane has no bar-close equivalent and gating it would mean a mark arm could never fire. This is what lets the emulator reproduce a mark-triggering venue's timing. The no-mark-arm path costs one integer compare per conflated tick (has_mark_arms).
  • Index matches no lane: the core has no index lane, so the runtime refuses such an arm at apply time, and one smuggled in anyway — a hand-edited journal — rests inert rather than firing off the wrong series.

Arms on the other lane are untouched by a check: not fired, not ratcheted — their price series simply did not tick.

The fire is journaled, or replay cannot reproduce it

A fire is a runtime reaction to a bar or tick, and market data is never journaled — so before the write-ahead ConditionalFire record existed, replay re-folded a fired session without its release and the determinism fence caught it as a hash mismatch. submit_fired now writes the decision, not the tick behind it, before apply_intent, with the request's client order id still empty; replay re-applies the recorded request through the same path and never re-evaluates a trigger, so a fire can neither double nor vanish. Arm and disarm are journaled the same way, and a restart re-arms the book exactly as it rested — a trailing arm's ratcheted extreme included — from the journal's Snap. See replay.

Whose account fires

conditional_books is keyed by (venue, symbol) — an exchange fact — so two accounts of one venue arm into one book and a fired conditional names no account. The runtime records which engine armed each id and routes the release to that engine by index; lowering by the payload's venue string once resolved the venue's default account, so a labelled mount's protective stop sold into the default account's book and opened a naked position while the one it was armed to close stayed open. A book seeded from a restored Snap carries no route key and keeps the payload route — the declared residual on that field. Multi-account mounts are the accounts page's subject.

What it is not

A venue that supports conditional orders natively receives the trigger_price and TriggerBy on the order request itself, and an adapter that cannot express the requested source on its wire must synthesize a terminal OrderRejected rather than silently substitute a different series. Bracket exits held off the venue until their entry fills are a different mechanism — the contingency page's OTO/OCO resolver.

On this page