Per-Order Checks
The 8-stage pre-trade pipeline that runs before every order
Every OrderAction the executor emits goes through the per-order check pipeline before the venue sees it. The pipeline catches NaN / Inf, price range violations, notional cap breaches, rate limit overruns, and drawdown-halt violations.
The 8 stages
Kill switch check
Drawdown halt check
action="halt_new", new-opening orders are rejected. Reducing orders are allowed.NaN / Inf sanity
Price range
per_order.price_min ≤ price ≤ per_order.price_max. Default is (0.01, 100_000). Catches fat-finger errors.Notional cap
quantity × price ≤ per_order.max_order_notional_usd. Optional shrink-to-fit behavior when shrink_to_fit=True.Rate limit
per_order.rate_limit_per_sec, reject.Max orders per tick
per_order.max_orders_per_tick orders have already been accepted this tick, reject.Stop-loss cooldown
Configuration
from horizon.risk import OrderRisk
per_order = OrderRisk(
max_order_notional_usd=25_000,
max_size_pct_of_adv=0.01,
price_min=0.01,
price_max=100_000.0,
rate_limit_per_sec=10.0,
dedup_window_ms=500,
allow_market_orders=True,
max_orders_per_tick=20,
shrink_to_fit=False,
)
max_order_notional_usdfloatmax_size_pct_of_advfloatprice_minfloatprice_maxfloatrate_limit_per_secfloatdedup_window_msintallow_market_ordersboolmax_orders_per_tickintshrink_to_fitboolDecision types
from horizon.types import Decision, DecisionKind
class DecisionKind(str, Enum):
Pass = "pass"
Reject = "reject"
Resize = "resize"
The per-order check returns one of:
Decision.pass_() # approved, submit as-is
Decision.reject("reason string") # blocked
Decision.resize(new_quantity, reason) # approved at reduced size
Example: shrink-to-fit
from horizon.risk import OrderRisk
# Reject the order entirely if notional > $500
cfg1 = OrderRisk(max_order_notional_usd=500, shrink_to_fit=False)
# Shrink to 500 notional if notional > $500
cfg2 = OrderRisk(max_order_notional_usd=500, shrink_to_fit=True)
With cfg1:
# Order: 10 shares @ $100 = $1,000 notional
# Cap: $500
# Result: rejected
With cfg2:
# Order: 10 shares @ $100 = $1,000 notional
# Cap: $500
# Result: resized to 5 shares @ $100 = $500 notional
Shrink-to-fit is useful when you trust the strategy’s direction but want to bound single-order risk.
Rate limiting
cfg = OrderRisk(rate_limit_per_sec=3)
Within any 1-second window, max 3 orders. The 4th is rejected:
# t=0.0: place order 1 → pass
# t=0.1: place order 2 → pass
# t=0.2: place order 3 → pass
# t=0.3: place order 4 → reject "rate limit exceeded"
# t=1.1: place order 5 → pass (old orders expired from window)
The sliding window is precise. orders are added to a deque with their timestamps, and _prune_rate_window removes expired entries before each check.
Drawdown halt interaction
When a drawdown guard is active, the pre-order check reads state.drawdown_halt_active and:
- If
"halt_new": reject opening orders, allow reductions - If
"flatten": reject everything (only forced-close events from the watchdog pass)
Reduction detection:
def _is_opening(self, action: OrderAction, state: RiskEngineState) -> bool:
if state.ledger is None:
return True
pos = state.ledger.position(action.market_id)
if pos is None:
return True
if action.side == "buy":
return pos.quantity >= 0 # buying while long or flat = opening
return pos.quantity <= 0 # selling while short or flat = opening
So “buy AAPL” while you have 10 long AAPL is an opening order (adding to the position), while “sell AAPL” would be a reducing order.
Stop-loss cooldown
After a stop loss fires on a market, that market enters a cooldown window. New orders on it are rejected:
cooldown_until = self._stop_loss_cooldown.get(action.market_id)
if cooldown_until is not None and state.now < cooldown_until:
return Decision.reject(f"market in stop-loss cooldown until {cooldown_until}")
The cooldown duration comes from StopLoss.cooldown. Default is no cooldown (zero duration), so stops fire without re-entry protection. Set to timedelta(hours=1) or similar for production.
Tests
# tests/test_risk_engine.py::TestPreOrder
def test_valid_order_passes(self) -> None:
engine = RiskEngine(RiskConfig())
d = engine.check_order(_action(), _state())
assert d.passed
def test_nan_quantity_rejected(self) -> None:
engine = RiskEngine(RiskConfig())
a = OrderAction.place(market_id="A", side="buy", quantity=float("nan"), price=100)
d = engine.check_order(a, _state())
assert not d.passed
def test_rate_limit(self) -> None:
cfg = RiskConfig(per_order=OrderRisk(rate_limit_per_sec=3))
engine = RiskEngine(cfg)
state = _state()
for _ in range(3):
assert engine.check_order(_action(), state).passed
# 4th order rejected
assert not engine.check_order(_action(), state).passed
11 tests across per-order behavior.