Skip to main content
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:
#CheckWhat it doesConfig
1Kill switchBlocks all orders when activeengine.activate_kill_switch(reason)
2Price validationRejects price outside price_min..price_maxprice_min, price_max
3Size validationRejects size ≤ 0-
4Max order sizeCaps individual order sizemax_order_size
5Position limitCaps position per marketmax_position
6Notional limitCaps total portfolio notionalmax_notional
7Drawdown checkActivates kill switch on excessive drawdownmax_drawdown_pct
8Rate limitToken bucket (sustained + burst)rate_limit, rate_burst
9DedupRejects duplicate orders within windowdedup_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)
)
ParameterDefaultDescription
max_position_per_market100.0Maximum position size per market
max_portfolio_notional1000.0Maximum total portfolio notional value
max_daily_drawdown_pct5.0Kill switch trigger (% of daily baseline)
max_order_size50.0Maximum size for a single order
rate_limit_sustained50Sustained orders per second
rate_limit_burst300Burst capacity for the token bucket
dedup_window_ms1000Window for duplicate order detection (ms)
max_position_per_eventNoneMaximum total position across all outcomes in a registered event. None = disabled.
price_min0.01Minimum valid limit-order price
price_max0.99Maximum 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:
  1. On startup, the daily baseline is set to the current total P&L
  2. Each cycle, update_daily_pnl() is called with the latest total P&L
  3. 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.