---
title: "Horizon Risk Pipeline"
description: "The 8-point risk pipeline that guards every Horizon order before it reaches the exchange."
icon: "shield-halved"
---

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 ≤ 0 or > 1 | - |
| 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` |

<Warning>
  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.
</Warning>

## Configuration

### Risk Builder

The `Risk` class provides a clean builder API:

```python
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)
    ),
    ...
)
```

<Note>
  `hz.Risk()` does not support `max_position_per_event`. For event-level position limits, use `RiskConfig` directly (see below).
</Note>

### RiskConfig (Direct)

For full control, use the Rust `RiskConfig` directly:

```python
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. |

## Kill Switch

The kill switch is a global emergency stop:

```python
# 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

<Tip>
  In the TUI dashboard, press `k` to toggle the kill switch.
</Tip>

## 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

```python
# 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)
- Order side (Buy/Sell)
- Size
- Price

Default window: 1000ms.

## Event Risk Limits

When trading [multi-outcome events](/multi-outcome), you can set `max_position_per_event` to cap total exposure across all outcomes in an event:

```python
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](/multi-exchange) 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.
