Every order passes through an 8-point risk pipeline in Rust before reaching the exchange. The pipeline is designed to prevent catastrophic losses and enforce trading limits.
Risk Pipeline
The checks run in order. If any check fails, the order is rejected immediately:
| # | Check | What it does | Config |
|---|
| 1 | Kill switch | Blocks all orders when active | engine.activate_kill_switch(reason) |
| 2 | Price validation | Rejects price outside price_min..price_max | price_min, price_max |
| 3 | Size validation | Rejects size ≤ 0 | - |
| 4 | Max order size | Caps individual order size | max_order_size |
| 5 | Position limit | Caps position per market | max_position |
| 6 | Notional limit | Caps total portfolio notional | max_notional |
| 7 | Drawdown check | Activates kill switch on excessive drawdown | max_drawdown_pct |
| 8 | Rate limit | Token bucket (sustained + burst) | rate_limit, rate_burst |
| 9 | Dedup | Rejects duplicate orders within window | dedup_window_ms |
When the drawdown check triggers, it automatically activates the kill switch and cancels all open orders across all exchanges. This is a hard stop that requires manual intervention.
Configuration
Risk Builder
The Risk class provides a clean builder API:
hz.run(
risk=hz.Risk(
max_position=100, # Max contracts per market (default: 100)
max_notional=1000, # Max total portfolio value (default: 1000)
max_drawdown_pct=5, # Kill switch at 5% drawdown (default: 5)
max_order_size=50, # Max single order size (default: 50)
rate_limit=50, # Sustained orders/sec (default: 50)
rate_burst=300, # Burst capacity (default: 300)
price_min=0.01, # Min valid price (default: 0.01)
price_max=0.99, # Max valid price (default: 0.99)
),
...
)
hz.Risk() does not support max_position_per_event. For event-level position limits, use RiskConfig directly (see below).
Equity Risk Preset
For equity strategies, use Risk.equity() which sets appropriate defaults:
hz.run(
risk=hz.Risk.equity(
max_position=1000, # shares per symbol
max_notional=100_000, # total portfolio value
max_drawdown_pct=5,
max_order_size=500,
),
...
)
Risk.equity() sets price_min=0.01 and price_max=100000 (vs 0.01-0.99 for prediction markets).
Use default hz.Risk() for prediction markets (price: 0.01-0.99). Use Risk.equity() for equities/options (price: 0.01-100,000). For crypto, customize price_min/price_max to match the asset’s range.
RiskConfig (Direct)
For full control, use the Rust RiskConfig directly:
from horizon import RiskConfig
config = RiskConfig(
max_position_per_market=100.0,
max_portfolio_notional=1000.0,
max_daily_drawdown_pct=5.0,
max_order_size=50.0,
rate_limit_sustained=50,
rate_limit_burst=300,
dedup_window_ms=1000,
max_position_per_event=200.0, # Event-level limit (None = disabled)
)
| Parameter | Default | Description |
|---|
max_position_per_market | 100.0 | Maximum position size per market |
max_portfolio_notional | 1000.0 | Maximum total portfolio notional value |
max_daily_drawdown_pct | 5.0 | Kill switch trigger (% of daily baseline) |
max_order_size | 50.0 | Maximum size for a single order |
rate_limit_sustained | 50 | Sustained orders per second |
rate_limit_burst | 300 | Burst capacity for the token bucket |
dedup_window_ms | 1000 | Window for duplicate order detection (ms) |
max_position_per_event | None | Maximum total position across all outcomes in a registered event. None = disabled. |
price_min | 0.01 | Minimum valid limit-order price |
price_max | 0.99 | Maximum valid limit-order price. Set to 100000 for equities. |
Kill Switch
The kill switch is a global emergency stop:
# Activate: blocks all orders + cancels existing
engine.activate_kill_switch("manual halt")
# Deactivate: resume trading
engine.deactivate_kill_switch()
# Check status
status = engine.status()
if status.kill_switch_active:
print(f"Kill switch reason: {status.kill_switch_reason}")
The kill switch is automatically activated when:
- Daily drawdown exceeds
max_drawdown_pct
- You can also trigger it manually or from a pipeline function
In the TUI dashboard, press k to toggle the kill switch.
Drawdown Tracking
The strategy loop automatically tracks drawdown:
- On startup, the daily baseline is set to the current total P&L
- Each cycle,
update_daily_pnl() is called with the latest total P&L
- If P&L drops below
baseline * (1 - max_drawdown_pct / 100), the kill switch triggers
# Manually set the daily baseline
engine.set_daily_baseline(1000.0)
# Update P&L (done automatically in the main loop)
engine.update_daily_pnl(current_pnl)
Rate Limiting
The rate limiter uses a token bucket algorithm:
- Sustained rate: refill rate in orders per second
- Burst capacity: maximum tokens available for bursts
This allows short bursts of rapid order submission while enforcing a sustainable average rate.
Dedup Window
The dedup check prevents submitting identical orders within a configurable time window. Two orders are considered duplicates if they have the same:
- Market ID
- Side (Yes/No/Long)
- Order side (Buy/Sell)
- Size
- Price
Default window: 1000ms.
Event Risk Limits
When trading multi-outcome events, you can set max_position_per_event to cap total exposure across all outcomes in an event:
config = RiskConfig(
max_position_per_market=100.0,
max_position_per_event=200.0, # Caps total across all outcomes
)
This check only applies to markets registered in an event via engine.register_event() (or via hz.run(events=...)). Markets not in any event are unaffected.
When max_position_per_event is None (the default), event-level risk checks are skipped entirely.
Netting and Risk
When netting pairs are configured, the notional limit check accounts for hedged positions. For each netting pair (market_a, market_b), the hedged portion is subtracted from the total portfolio notional:
adjusted_notional = raw_notional - sum(min(exposure_a, exposure_b) * 0.5)
This allows larger positions when they’re hedged across exchanges.