When to Use
Use this skill when implementing client-side conditional order logic (If-Touched, bracket, One-Cancels-the-Other, or cross-asset triggers) in an OMS/EMS. Conditional orders remain dormant in local memory until incoming market data evaluates a nested Boolean condition tree to TRUE, upon which child orders are released for pre-trade risk checking and venue routing.
Client-side simulation is the right answer when the trigger the strategy needs does not exist at the venue or the broker: a condition spanning two instruments, a condition mixing price with volume and wall-clock time, or a trigger on an exchange that does not accept the order type at all. NYSE and NYSE MKT stopped accepting stop orders and GTC orders on 26 February 2016 and cancelled the resting ones, so equity stop logic on those books lives in a broker's or a client's simulator, not on the exchange.
When NOT to Use
- A native or broker-resident conditional order does the same job. IBKR's conditional orders support price, time, margin, execution, volume and percent-change conditions combined with AND/OR, and IBKR states that active orders remain active after exiting Trader Workstation — you can be filled without being logged in. CME Globex triggers stop orders natively against the last trade price. A broker- or exchange-resident trigger survives your process crashing, your feed dropping and your host rebooting. This engine survives none of those: if the process is down when the market touches the level, nothing fires and nothing tells you so. Prefer native; simulate only what native cannot express.
- Latency-sensitive triggering against fast markets. A client-side trigger fires one network round trip after the price prints, then the child order still has to travel to the venue. For stop logic on a fast-moving book, an exchange-resident stop (with protection points, where the venue offers them) fills closer to the level.
- As a risk control. A kill switch or exposure limit must not depend on a condition tree that goes quiet when the data feed dies. Keep circuit breakers in a path that fails closed — see
execution-algorithm-kill-switch-integration. - On an instrument that may be halted or in an auction. A dormant trigger evaluating a stale pre-open or halted book will fire on the first print of a re-opening auction. Gate the tree with an explicit session/state condition; see
execution-algo-behavior-under-halted-instrument.
Prerequisites
- Streaming market data for the target instrument and every referenced benchmark, each quote carrying a timestamp (epoch seconds, UTC) if staleness enforcement is wanted.
- A decision on the trigger price type per condition — last trade, bid, ask, or mid. FIX
TriggerPriceType(1107)enumerates exactly this choice (Best Offer, Last Trade, Best Bid, Best Bid or Last Trade, Best Offer or Last Trade, Best Mid), and it changes when the trigger fires. - A unique client order identifier and a fully specified child order (
symbol,side,quantity,order_type,pricefor limits). - A downstream pre-trade risk check: a fired trigger emits an intent, not an authorised order.
Workflow
- Condition Tree Construction: Build atomic nodes —
PriceCondition,VolumeCondition,TimeCondition,CrossAssetCondition— and compose them withAndCondition/OrCondition/NotCondition. Choose the price field deliberately (lastvsbidvsask): a>=trigger read off the bid and one read off the last trade fire at different moments on the same tape. Empty composites are rejected at construction, because an empty AND gate is vacuously true and would fire on the first tick. - Staleness Policy: Pass
max_quote_age_secondsto any condition whose input can go stale — always to cross-asset legs, whose benchmark feed can disconnect while the primary keeps ticking. With it set, a quote whosetimestampis missing or older than the limit evaluates to UNKNOWN, not to a value. - Conditional Order Registration: Wrap the tree and a validated
ChildOrderPayloadin aConditionalOrderTrigger, thenConditionalOrderEngine.register(trigger, oco_group=...). Triggers sharing anoco_groupare one-cancels-the-other; a bracket registers its target and its stop in one group so both legs cannot reach the venue. - Tick Processing: Call
engine.process_tick(market_state, now=...)on each update. The engine pins one evaluation clock for the whole tick, so a time condition cannot read a different instant than its siblings in the same tree. - Trigger Firing and Dispatch: The engine returns the child orders released by this tick. Firing is a single atomic DORMANT → TRIGGERED transition under a lock — evaluation and state change happen together, so two feed-handler threads delivering the same tick cannot both release the order. On fire, dormant OCO siblings are cancelled.
- Undecided is not false: The tree evaluates in three-valued (Kleene) logic. A missing or stale input yields UNKNOWN; only a definite TRUE fires. Route UNKNOWN to monitoring — a trigger that has been undecided for minutes means a feed is down, and it will not fire when the level trades.
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Firing duplicate orders: Checking
status == DORMANTand settingTRIGGEREDas two separate steps. Two threads can interleave between the check and the set and both release the child order. Evaluate and transition under one lock. - Treating missing data as
False: Under two-valued logic a missing quote reads as FALSE, soNotCondition(missing)reads as TRUE and releases a live order on data you never received. Negation is where a fail-safe FALSE stops being safe. - A dropped benchmark silently rewriting the trigger:
AAPL >= 150 AND SPY >= 500with the SPY feed dead is not "wait"; under two-valued logic it is a permanent FALSE that never fires, and if the composition isORit becomes a bareAAPL >= 150outright trigger. Setmax_quote_age_secondson cross-asset legs and alert on UNKNOWN. - Exact float equality as a trigger:
last == 150.00against a decimal price feed effectively never matches. Use a band with an explicit tolerance, or an ordering operator. - Silently ignoring an unknown operator: Returning
Falsefor a typo'd'=>'produces a trigger that can never fire and reports no error — discovered when the order never arrives. Validate operators at construction. - Naive datetimes in time conditions: A trigger meant for 15:50 New York fires at 15:50 in whatever zone the host happens to be in. Require timezone-aware targets.
- Confusing the trigger price type: Simulated triggers only fire on the field you feed them. IBKR's trigger methods exist for the same reason (double bid/ask, last, double last, bid/ask, last-or-bid/ask, mid-point) and apply only to simulated orders — a natively handled stop ignores them entirely.
- Blocking the hot path: Heavy parsing, disk I/O or network calls inside the market-data callback that evaluates the tree. Keep evaluation to arithmetic on already-parsed values.
- Treating a fired trigger as a fill: The trigger produces an order intent. It still has to pass pre-trade risk, reach the venue, and be accepted — and it can be rejected.
Verification
- Build
(AAPL.last >= 150.00) AND (SPY.last >= 500.00)on aConditionalOrderEngine. FeedAAPL150.50 /SPY499.00: the trigger must stayDORMANTandprocess_tickmust return[]. FeedSPY500.50: it must transition toTRIGGEREDand return exactly one child order payload; a third tick must return nothing. - Register a bracket as one
oco_groupand drive the take-profit level: the stop-loss leg must beCANCELLEDand must stay silent when its own level trades afterwards. - Drive one satisfying tick from 16 threads concurrently and assert exactly one payload is released.
- Feed a cross-asset tree with the benchmark quote removed and confirm
evaluate_tristatereturnsNone(UNKNOWN), notFalse, and thatNotConditionover it does not fire. - Set
max_quote_age_seconds=5.0and feed a quote timestamped 30 seconds ago: no fire, and a stale-quote warning logged. - Run
python -m unittest discover -s skills/conditional-order-logic-for-execution-triggers/scriptsand confirm a 100% pass rate.