When to Use
Use this skill when a router or execution algorithm sends live orders to more than one venue (NASDAQ, NYSE, Cboe BATS/EDGX, IEX, or crypto equivalents) and must keep working when one of them stops working. Exchange outages, FIX gateway drops, and matching-engine stalls happen without warning and without the venue withdrawing its quote first.
The failure this prevents is subtler than "the venue is down." A dead venue usually keeps displaying its last quote, and that quote is often the best one on the book — because it is stale. A naive best-price router therefore routes preferentially into the outage, and keeps doing so until enough orders have been lost to notice. This engine excludes a venue on evidence (open breaker, stale quote, invalid quote, no liquidity), ranks what is left, and records everything it bypassed.
SmartOrderRouterFailoverEngine in
scripts/smart_order_router_failover_on_venue_outage.py is the reference
implementation: thread-safe, monotonic-clocked, with HEALTHY / DEGRADED /
CIRCUIT_BROKEN_OUTAGE / RECOVERY_PROBE per venue.
When NOT to Use
- As an in-flight order recovery mechanism. This engine does not know what
you already sent to the failed venue and never re-routes residual quantity on
its own. It reports
unrouted_quantity; you must reconcile fills against the venue's drop copy before re-routing anything. Seeorder-placement-idempotencyandwebsocket-reconnection-with-state-recovery. - As your market data plane. It stores whatever quote you last handed it. It
cannot tell a stale quote from a fresh one unless you stamp
quote_monotonic_ts— useupdate_quote(), which stamps it for you. - For sizing or sweeping a parent order across venues. One call selects one
venue. Splitting a parent across several venues at the NBBO, with maker-taker
fee arithmetic, is
smart-order-routing-across-venues; scheduling the parent over time isexecution-algo-twap-vwap-slicing. - For a halted instrument. A trading halt is not a venue outage; the venue is
healthy and deliberately not trading. Routing around it to a venue that has not
yet processed the halt is a different and worse error — see
execution-algo-behavior-under-halted-instrument. - For failover between gateways or regions of the same venue. Two sessions
into one exchange is
exchange-gateway-redundancy-and-failover-testing; region-level connectivity failover ismulti-region-failover-for-broker-connectivity. - As a kill switch. This keeps trading by moving it elsewhere. Deciding to
stop trading is a separate control that must not share this code path —
kill-switch-and-drawdown-circuit-breakers. - On a sub-millisecond hot path in CPython. The per-route lock and linear scan are fine for tens of venues at human or algo-order rates, not for a latency-arbitrage stack.
Prerequisites
- A live quote feed you push into the engine, via
update_quote(venue_id, bid, ask, qty). Quotes carry atime.monotonic()stamp; the engine's staleness check is inert without one. - A venue health signal: FIX session-level rejects, gateway timeouts,
transport disconnects, HTTP 5xx →
report_venue_error(); fills, acks, heartbeats →report_venue_success(). Both raiseKeyErroron an unknown venue id rather than silently doing nothing. - Error classification. Only transport/gateway faults may reach the breaker. An order rejected for buying power, a bad symbol, or a failed pre-trade risk check says nothing about venue health and must not trip it.
- Calibrated thresholds. The defaults (3 errors, 1 s quote age, 60 s
cooldown) are engineering starting points, not regulatory figures — see
references/standards.md. - Python 3.10+. Standard library only.
Workflow
-
Register venues and keep both inputs current.
add_venue()rejects a duplicate id rather than replacing it — a silent replace would discard the existing venue's breaker state and re-enable a venue that is currently tripped. Then push quotes and health continuously. -
Classify the error before reporting it.
- Decision point — a rejected order is not a broken venue. Feeding business rejects into the breaker trips healthy venues and routes you away from your best liquidity because of a bug in your own order construction.
-
Self-diagnose before declaring venues dead.
- Decision point — if half your venues fail at once, suspect yourself.
diagnose_suspected_local_fault()implements the check the Reg NMS adopting release attaches to the Rule 611(b)(1) self-help exception: an electing trading center "must also assess ... whether the cause of a problem lies with its own systems." Simultaneous multi-venue outages are rare; a dead NIC, an expired credential, or clock skew breaking FIX sequencing is not. The flag is surfaced on every result and never stops routing by itself.
- Decision point — if half your venues fail at once, suspect yourself.
-
Filter on evidence, and record every exclusion. A venue is dropped only as
CIRCUIT_BROKEN_OUTAGE,INVALID_QUOTE,NO_LIQUIDITY,STALE_QUOTE,QUOTE_TIMESTAMP_MISSING, orQUOTE_TIMESTAMP_IN_FUTURE, each recorded inexcluded_venues.- Decision point — a price of
0.0is not a cheap venue. It is the dataclass default, i.e. a venue that has never quoted or whose book was wiped on reconnect. Unfiltered, it winsmin(ask)on every buy and the order routes at $0.00. - Decision point — exclusion must never be silent. An exclusion you cannot see is indistinguishable from a routing bug.
- Decision point — a price of
-
Rank: probe-last, then price, then health, then latency.
- Decision point — a recovering venue never wins on price. A venue that
just failed is the one most likely to be quoting a price it can no longer
honour, so
RECOVERY_PROBEvenues rank below everything regardless of how good their quote looks. - Decision point — but a
DEGRADEDvenue at a better price still wins. Health only breaks ties. Skipping a better price to avoid a venue with a single timeout is a trade-through taken for noise. preferred_venue_idoverrides ranking when eligible; the price given up is returned inprice_improvement_forgoneand logged at WARNING as a best-execution exception.
- Decision point — a recovering venue never wins on price. A venue that
just failed is the one most likely to be quoting a price it can no longer
honour, so
-
Recover through a cooldown and a single probe.
- Decision point — a success on an open breaker must not close it. The ack
for an order sent before the outage arrives after the trip; honouring it
resurrects a dead venue on evidence that proves nothing about its current
state. Only a success while in
RECOVERY_PROBEcloses the circuit; each re-trip multiplies the cooldown up tomax_cooldown_seconds. - Decision point — the probe is a real order. Prefer a session heartbeat or test-request round trip where the venue offers one.
- Decision point — a success on an open breaker must not close it. The ack
for an order sent before the outage arrives after the trip; honouring it
resurrects a dead venue on evidence that proves nothing about its current
state. Only a success while in
-
Reconcile in-flight orders yourself, then handle the residual.
unrouted_quantityis reported, never auto-routed. Confirm the state of everything already sent to the failed venue before placing more. -
Persist the whole result.
fallback_venues_usedis the set of venues you actually traded through; withaudit_notesit is the record that supports a self-help election and the FINRA Rule 5310 .09 regular and rigorous review.
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Routing preferentially into the outage. The core trap. A dead venue keeps showing its last quote, staleness makes that quote look best, and a pure best-price router therefore increases the share of flow it sends to the broken venue. Price alone cannot detect this; only quote age and health can.
- Treating an unquoted venue as free.
ask_pricedefaults to0.0. Without a positivity check that venue wins every buy and the router reports a fill price of $0.00. - Letting a stray success clear the breaker. Resetting to
HEALTHYon any success means one late acknowledgement from before the outage re-enables the dead venue, and the next order goes straight back into it. - Tripping every venue on a local fault. Three timeouts against every venue at once is your NIC, not three simultaneous exchange outages. A router that concludes "all venues are down" and raises, with a live parent order outstanding, has converted a local cable fault into an unmanaged position.
- Silently dropping residual quantity.
min(quantity, available_qty)looks like prudent sizing and is actually a silent partial route: the caller believes the whole order went out. Report the residual explicitly. - Re-routing residual before reconciling. Orders in flight to a venue that dropped are in an unknown state. Re-sending on the assumption they died is how one parent order becomes two positions.
- Defaulting an unrecognised side to SELL.
if side == "BUY": ... else: ...turns a typo'd"SEL"into a live short at the bid. Validate and raise. - Feeding business rejects into the breaker. Insufficient buying power, a bad symbol, or a locked/crossed limit are your errors, not the venue's. They trip healthy venues and push flow to worse prices.
- Wall-clock arithmetic for cooldowns. An NTP step backwards during a
recovery window either extends the cooldown indefinitely or expires it
instantly. Use
time.monotonic()for every elapsed-time decision. - Assuming Rule 611 applies to you. The Order Protection Rule binds trading
centers (17 CFR 242.600(b)(106)). A broker that only routes orders away is
generally not one; its obligation is best execution under FINRA Rule 5310. And
note Rule 611 is under an active SEC rescission proposal (Rel. 34-105680, June
2026) that has not been adopted — see
references/standards.md. - Citing a failover latency SLA. No regulator publishes one. The one-second figure in the Reg NMS adopting release is a standard for judging the away venue's response time, not a deadline for your own failover code.
Verification
- Baseline routing: asks NASDAQ 100.05 / BATS 100.08 / NYSE 100.10 ⟹ a BUY
routes to NASDAQ at 100.05,
is_failover_triggeredFalse,excluded_venuesempty. Bids BATS 100.02 / NASDAQ 100.00 / NYSE 99.95 ⟹ a SELL routes to BATS at 100.02, so a side bug cannot pass both directional tests. - Invalid quotes: a venue with the default
ask_price=0.0and realavailable_qtymust be excluded asINVALID_QUOTEand must not win the buy; aNaNprice and a zeroavailable_qtymust likewise be excluded. - Staleness: with
max_quote_age_seconds=0.5, a quote stamped 5 s ago is excluded asSTALE_QUOTEand appears infallback_venues_usedif it was quoting better. A venue with no timestamp appears instale_quote_check_skipped, and is excluded outright underrequire_quote_timestamp=True. A quote stamped from the wrong clock (atime.time()value, giving a negative age) is excluded asQUOTE_TIMESTAMP_IN_FUTURErather than read as permanently fresh. - Breaker: 3 errors ⟹
CIRCUIT_BROKEN_OUTAGE.report_venue_success()while open must leave it open. After the cooldown,refresh_venue_states()⟹RECOVERY_PROBE; a success then ⟹HEALTHYwithconsecutive_tripsreset; a single error instead ⟹ re-trip with the cooldown doubled, capped atmax_cooldown_seconds. - Probe demotion: a
RECOVERY_PROBEvenue showing the best ask must not be selected. - Failover audit: tripping NASDAQ and routing with no
preferred_venue_idmust still reportis_failover_triggeredTrue andfallback_venues_used == ["NASDAQ"]. A dead venue that was already worse than the fill must appear inexcluded_venuesbut not infallback_venues_used— unless it was the venue the caller explicitly asked for, which is always recorded as a bypass. - Residual: BUY 2,500 against 1,000 displayed ⟹
routed_quantity1,000 andunrouted_quantity1,500. - Local fault: 2 of 3 venues tripped ⟹
diagnose_suspected_local_fault()True andsuspected_local_faultset on the result; a single-venue engine ⟹ always False. - Validation:
sideof"SEL",""or"SHORT"⟹ValueError;quantityof 0, negative,NaN,infor"500"⟹ValueError; an unknown venue id onreport_venue_error,report_venue_successorpreferred_venue_id⟹KeyError; a duplicateadd_venue⟹ValueError. - Exhaustion: all venues tripped ⟹
NoEligibleVenueError(aRuntimeErrorsubclass) carrying the per-venue reason map and the local-fault flag. - Run
python -m unittest discover -s skills/smart-order-router-failover-on-venue-outage/scriptsand confirm 43/43 pass.
Related Skills
smart-order-routing-across-venuesexecution-algorithm-kill-switch-integrationexchange-gateway-redundancy-and-failover-testingmulti-region-failover-for-broker-connectivitycircuit-breaker-for-downstream-service-callsorder-placement-idempotencyexecution-algo-behavior-under-halted-instrumentfix-protocol-session-management-across-venues