Deep dive
How it works
In: an outgoing order, then execution reports · Out: pass (with reservation) or reject with a reason · State: per account and symbol: position, open buy/sell quantity, open notional, rate limiter, kill flag · Cost: O(1), order of 100 ns in software
Per account (and per account × symbol) keep: position, open_buy_qty, open_sell_qty, open_notional, a rate limiter and a kill flag. For each new order:
- Stateless sanity: kill switch, restricted symbol,
0 < qty ≤ max_qty,px > 0,px × qty ≤ max_notional. - Price collar:
|px − ref| × 10_000 ≤ collar_bps × ref, whererefis last trade, mid or a static reference. No reference → reject (or a tighter fallback), never pass. - Worst-case position: buy:
position + open_buy + qty ≤ max_pos; sell:position − open_sell − qty ≥ −max_pos. - Credit:
open_notional + px × qty ≤ credit_limit. - Throttle: orders per second per account/session (token bucket).
- If all pass, reserve:
open_buy += qty,open_notional += px × qty, consume a token, then send.
Execution reports release the reservation: fill q → open_side −= q, position ± q, open_notional −= limit_px × q; cancel/expire/reject of leaves → open_side −= leaves, open_notional −= limit_px × leaves. Cancel/replace: treat as release of the old order plus reservation of the new one, checking the new values before sending.
Code
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Side { Buy, Sell }
pub struct Order { pub symbol: u32, pub side: Side, pub px: i64, pub qty: i64 } // px in integer ticks
pub struct Limits {
pub max_order_qty: i64,
pub max_order_notional: i128,
pub collar_bps: i64,
pub max_position: i64, // absolute, per symbol
pub credit_limit: i128, // gross notional of all open orders
pub orders_per_sec: u64,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Reject { Killed, Restricted, Qty, Price, Notional, NoReference, Collar, Position, Credit, Throttle }
#[derive(Default, Clone, Copy)]
pub struct Exposure { pub position: i64, pub open_buy: i64, pub open_sell: i64 }
pub struct Account {
pub limits: Limits,
pub killed: bool,
pub restricted: Vec<bool>, // by dense symbol index
pub exp: Vec<Exposure>, // by dense symbol index
pub open_notional: i128,
bucket: u64, // tokens × 1e9 (integer token bucket)
last_ns: u64,
}
const NS: u64 = 1_000_000_000;
impl Account {
pub fn new(limits: Limits, symbols: usize) -> Self {
let bucket = limits.orders_per_sec * NS;
Account { limits, killed: false, restricted: vec![false; symbols], exp: vec![Exposure::default(); symbols],
open_notional: 0, bucket, last_ns: 0 }
}
/// Returns Ok after reserving exposure; the caller must then send the order.
pub fn check_and_reserve(&mut self, o: &Order, ref_px: i64, now_ns: u64) -> Result<(), Reject> {
let l = &self.limits;
if self.killed { return Err(Reject::Killed); }
let s = o.symbol as usize;
if self.restricted[s] { return Err(Reject::Restricted); }
if o.qty <= 0 || o.qty > l.max_order_qty { return Err(Reject::Qty); }
if o.px <= 0 { return Err(Reject::Price); }
let notional = o.px as i128 * o.qty as i128; // i128: no overflow
if notional > l.max_order_notional { return Err(Reject::Notional); }
if ref_px <= 0 { return Err(Reject::NoReference); }
let dev = (o.px as i128 - ref_px as i128).abs() * 10_000;
if dev > l.collar_bps as i128 * ref_px as i128 { return Err(Reject::Collar); }
let e = self.exp[s];
let ok = match o.side {
Side::Buy => e.position + e.open_buy + o.qty <= l.max_position,
Side::Sell => e.position - e.open_sell - o.qty >= -l.max_position,
};
if !ok { return Err(Reject::Position); }
if self.open_notional + notional > l.credit_limit { return Err(Reject::Credit); }
// Token bucket, burst = one second of rate; refill in integer token·ns units.
let rate = l.orders_per_sec;
self.bucket = (self.bucket + now_ns.saturating_sub(self.last_ns).saturating_mul(rate)).min(rate * NS);
self.last_ns = now_ns;
if self.bucket < NS { return Err(Reject::Throttle); }
// Everything passed: reserve.
self.bucket -= NS;
self.open_notional += notional;
let e = &mut self.exp[s];
match o.side { Side::Buy => e.open_buy += o.qty, Side::Sell => e.open_sell += o.qty }
Ok(())
}
/// Fill of `q` on an order with limit price `limit_px` (release at the reserved price, not the fill price).
pub fn on_fill(&mut self, symbol: u32, side: Side, limit_px: i64, q: i64) {
let e = &mut self.exp[symbol as usize];
match side {
Side::Buy => { e.open_buy -= q; e.position += q; }
Side::Sell => { e.open_sell -= q; e.position -= q; }
}
self.open_notional -= limit_px as i128 * q as i128;
}
/// Cancel, expiry or reject of the remaining `leaves` quantity.
pub fn on_done(&mut self, symbol: u32, side: Side, limit_px: i64, leaves: i64) {
let e = &mut self.exp[symbol as usize];
match side { Side::Buy => e.open_buy -= leaves, Side::Sell => e.open_sell -= leaves }
self.open_notional -= limit_px as i128 * leaves as i128;
}
}
Complexity and constants
- O(1): about 10 integer compares, two i128 multiplies, one cache line of account state and one of symbol exposure. Order of 100 ns in software including cache misses; FPGA/NIC-resident risk gateways do it in well under 1 µs wire-to-wire.
- Memory: 24 B per account × symbol exposure; dense symbol indices let a
Vecreplace a hash map.
Efficiency
Cost: the bound is latency of a short compare chain plus cache misses for account and symbol state. About 10 integer compares, two i128 multiplies, one cache line of account state and one of symbol exposure: order of 100 ns in software; 24 B per account × symbol exposure. FPGA or smart-NIC gateways do it in well under 1 µs wire-to-wire.
Levers
- Running counters, reserve on send and release on fill, cancel, reject — O(1) instead of O(open orders) — always.
- Dense
Vecby account and symbol index — hot set stays in L1/L2 — instead of a hash map. - Single writer per account — no atomics or locks on counters.
- Stateless rejects first, reserve last — a reject writes nothing.
- Collar bounds precomputed per symbol on each reference refresh — not per order.
Naive → standard → state of the art
- Naive: scan open orders on every new order · O(open orders)
- Standard: counters in software on the order path · order of 100 ns
- State of the art: FPGA or smart-NIC risk gateway · well under 1 µs wire-to-wire Coarser second line: exchange-side per-member limits. Do not move the check off the order path to save latency.
Implementation notes
- Order of checks: cheap stateless checks first, reservation last and only after all checks pass, so a reject never leaves partial state.
- Single writer: the risk state for an account belongs to one thread, the same one that sends orders for it; execution reports are routed to that thread. Shared risk across gateways needs either one gateway per account or a central sequencer.
- Units: prices as integer ticks or minor units (money representation); notional in i128; convert contract multipliers and FX into the limit currency with integer rates.
- Reference price: refresh from market data; mark it stale after a timeout and fall back to rejecting or a static band. Opening auctions and halts need explicit handling.
- Duplicate-order check: reject the same symbol/side/qty/px within a short window, a common fat-finger guard (double click, retry loop).
- Kill switch: a flag checked first; flipping it also sends mass cancels (FIX OrderMassCancelRequest or exchange-specific cancel-on-disconnect).
- Exchange-side limits: many venues offer their own per-member limits; use them as a second line, not as the only one.
Pitfalls
- Counting exposure only from fills: an algo can send 1,000 orders before the first fill arrives.
- Releasing reservations at the fill price instead of the reserved limit price:
open_notionaldrifts negative or positive over the day. - Losing execution reports (disconnect, gap) leaves reservations stuck; reconcile open orders with the venue on reconnect.
- Using floats for collars:
0.1 %of a price becomes a rounding-dependent pass/fail. - A missing reference price treated as "no collar".
- Knight Capital, 2012: a faulty deployment sent millions of unintended orders in about 45 minutes; no effective aggregate exposure limit or kill switch stopped it.
Variants and alternatives
In production
Broker market-access gateways (15c3-5), exchange member risk controls, and every algorithmic trading system's order manager.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| NautilusTrader | Rust core, Python API | nautechsystems/nautilus_trader | pip install nautilus_trader | — | — | — | — | built-in risk engine in front of order submission |
Sources
- SEC, Risk Management Controls for Brokers or Dealers with Market Access, Rule 15c3-5, 17 CFR 240.15c3-5, 2010. rule
- Commission Delegated Regulation (EU) 2017/589 (RTS 6), Articles 12 (kill functionality) and 15 (pre-trade controls). text
- SEC, In the Matter of Knight Capital Americas LLC, SEC order, 2013.