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.
NoneorLastarms evaluate on trade and bar prices — today's law, byte-identical.check_barruns against every closed bar of the mount's series, before the strategy step, which is the oracle's firing order. WithCoreConfig::conditionals_on_ticks(defaultfalse, 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.Markarms evaluate only on the mark lane.check_markis fed from the conflated mark drain and is always on when a mark arm rests — deliberately not behindconditionals_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).Indexmatches 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.
The price board
One cell per (venue, symbol) holds a price from every source that has spoken, and a side-aware read walks them in a fixed order so a position is valued at the price it could actually be exited at.
The dead-man's switches
Two of them, with opposite defaults because they read opposite evidence — the connection switch is on unless you turn it off and trips on a link the venue's own bridge reports dead; the silence switch is off unless you write the key and trips on a feed that has gone quiet, which is also what a closed market looks like.