Risk Management

Configuring all seven defensive layers

This page walks through each of the seven risk layers with concrete configuration examples.

Starting from a preset

Start with a preset and override what you need:

python
from horizon.risk import RiskProfile, StopLoss

risk = RiskProfile.moderate().override(
    stop_loss=StopLoss(per_position_pct=0.08, trailing_pct=0.04),
    max_gross_leverage=1.5,
)

Presets are conservative(), moderate(), aggressive(). They set all 7 layers to sensible defaults.

Layer 1. Per-order checks

Every order goes through these before submission:

python
from horizon.risk import OrderRisk

per_order = OrderRisk(
    max_order_notional_usd=25_000,    # reject orders > $25k
    max_size_pct_of_adv=0.01,         # max 1% of avg daily volume
    price_min=0.01,
    price_max=100_000,
    rate_limit_per_sec=10,            # max 10 orders per second
    dedup_window_ms=500,              # reject duplicate within 500ms
    allow_market_orders=True,
    max_orders_per_tick=20,

    shrink_to_fit=False,              # if True, resize rather than reject
)

Shrink-to-fit

Normally, an order that exceeds max_order_notional_usd gets rejected. With shrink_to_fit=True, the engine reduces the quantity to exactly fit the cap and lets the order through:

python
per_order = OrderRisk(max_order_notional_usd=500, shrink_to_fit=True)
# An order for 10 @ 100 = 1000 notional → resized to 5 @ 100 = 500

Layer 2. Stop losses

python
from datetime import timedelta
from horizon.risk import StopLoss

stop = StopLoss(
    per_position_pct=0.10,              # close at 10% unrealized loss
    per_position_usd=5_000,              # ...or $5,000, whichever first
    trailing_pct=0.05,                   # trail 5% below unrealized peak
    cooldown=timedelta(hours=1),         # no re-entry for 1 hour after stop
)

Stop events fire as market orders

When the watchdog detects a breach:

  1. LifecycleEvent(kind=StopLoss, market_id=...) is emitted
  2. The run loop converts it to a OrderAction.place(..., order_type="market", urgency=Immediate)
  3. The market order passes through pre-order checks (which always allow reduces during drawdown halts)
  4. Paper venue fills it at the current tick price + slippage
  5. The market enters cooldown: new orders on it are rejected until cooldown expires

Greek stops (options)

python
from horizon.risk import GreekStop

greek_stop = GreekStop(
    close_if_delta_over=0.70,   # short call too ITM
    close_if_gamma_over=0.08,
    close_if_vega_over=500,
    close_if_dte_under=2,       # close within 2 days of expiry
)

Attach to AssetRisk for the options asset class:

python
from horizon.asset_classes import Option
from horizon.risk import AssetRisk, RiskConfig

risk = RiskConfig(
    per_asset_class={
        Option: AssetRisk(stop_loss=greek_stop, max_portfolio_vega=500),
    },
)

Layer 3. Drawdown guards

Portfolio-wide thresholds on rolling drawdown:

python
from horizon.risk import DrawdownGuard
from horizon.risk.drawdown import DrawdownAction

drawdown = [
    DrawdownGuard(daily_pct=0.05, action=DrawdownAction.HaltNew),
    DrawdownGuard(weekly_pct=0.10, action=DrawdownAction.ReduceHalf),
    DrawdownGuard(monthly_pct=0.20, action=DrawdownAction.Flatten),
    DrawdownGuard(total_pct=0.30, action=DrawdownAction.Flatten),
]

Stacked guards compose: a 5% daily loss halts new entries, a 10% weekly loss reduces the book, a 20% monthly loss flattens.

Recovery

Once a guard fires, new trading resumes only after equity recovers by recovery_threshold from the trough:

python
DrawdownGuard(
    daily_pct=0.05,
    action=DrawdownAction.HaltNew,
    recovery_threshold=0.02,   # resume when equity climbs 2% from trough
    cooldown=timedelta(hours=24),
)

Layer 4. Circuit breakers

Market-wide anomaly detection:

python
from horizon.risk import (
    VolatilityBreaker,
    FeedStalenessBreaker,
    CorrelationShockBreaker,
    OrderRejectStreakBreaker,
)

circuit_breakers = [
    VolatilityBreaker(threshold=3.0, lookback="30d"),
    CorrelationShockBreaker(threshold=0.9, min_positions=5),
    FeedStalenessBreaker(max_age_sec=30),
    OrderRejectStreakBreaker(consecutive_rejects=10),
]

Circuit breakers usually fire HaltNew rather than Flatten: when the market is anomalous, you don’t want to dump into it.

Layer 5. Margin watchdog

Per-venue buying-power monitoring:

python
from horizon.risk import MarginGuard

margin = MarginGuard(
    warn_at_utilization=0.80,       # alert at 80% used
    reduce_at_utilization=0.90,     # force reduce at 90%
    emergency_at_utilization=0.98,  # aggressive flatten at 98%
    reduce_method="largest_loser",  # which positions to close first
    per_venue=True,                 # track per venue vs aggregate
)

Three progressive actions as utilization climbs toward forced liquidation.

Layer 6. Scenario gates

Forward-looking tail risk:

python
from horizon.risk import Scenario, ScenarioGuard

scenarios = ScenarioGuard(
    enabled_scenarios=[
        Scenario.MarketDrop(pct=-0.10),
        Scenario.VolSpike(vix_to=40),
        Scenario.CryptoCrash(pct=-0.30),
        Scenario.PredictionMarketBinary(asset="ELECTION_YES", outcome="NO"),
    ],
    max_scenario_drawdown_pct=0.15,
    check_frequency="daily",
    block_orders_if_worsens=True,
    alert_on_breach=True,
)

Scenarios are declarative only in the current release: the runner that revalues the portfolio under each scenario is a future release. The config is stable.

Layer 7. Kill switch

Emergency stop:

python
from horizon.risk import (
    KillSwitch,
    PortfolioDrawdownEvent,
    MarginCallEvent,
)

kill_switch = KillSwitch(
    auto_trigger_on=[
        PortfolioDrawdownEvent(threshold=0.25),   # 25% drawdown
        MarginCallEvent(),                         # any margin call
    ],
    require_manual_reset=True,
    alert_channels=["slack", "email", "sms"],
    reason_required=True,
)

When triggered:

  1. All per-order checks immediately reject new orders
  2. All strategies get halted
  3. Alerts fire on all configured channels
  4. Requires an explicit reset with a reason

Assembling the full config

python
from horizon.risk import (
    RiskConfig,
    OrderRisk,
    StopLoss,
    DrawdownGuard,
    MarginGuard,
    KillSwitch,
    PortfolioDrawdownEvent,
    VolatilityBreaker,
    FeedStalenessBreaker,
)
from horizon.risk.drawdown import DrawdownAction

risk = RiskConfig(
    per_order=OrderRisk(
        max_order_notional_usd=25_000,
        rate_limit_per_sec=10,
        shrink_to_fit=True,
    ),
    stop_loss=StopLoss(per_position_pct=0.10, trailing_pct=0.05),
    drawdown=[
        DrawdownGuard(daily_pct=0.05, action=DrawdownAction.HaltNew),
        DrawdownGuard(weekly_pct=0.10, action=DrawdownAction.ReduceHalf),
        DrawdownGuard(monthly_pct=0.20, action=DrawdownAction.Flatten),
    ],
    circuit_breakers=[
        VolatilityBreaker(threshold=3.0),
        FeedStalenessBreaker(max_age_sec=30),
    ],
    margin=MarginGuard(
        warn_at_utilization=0.80,
        reduce_at_utilization=0.90,
    ),
    kill_switch=KillSwitch(
        auto_trigger_on=[PortfolioDrawdownEvent(0.25)],
    ),
    max_gross_leverage=2.0,
    max_notional_per_market_usd=50_000,
)

Pass to hz.run(risk=risk, ...).

Observing risk in action

The test suite verifies every layer fires when its condition is met:

bash
PYTHONPATH=. python3 -m pytest tests/test_risk_engine.py -v

22 tests covering every path: NaN rejects, notional caps, rate limiting, stops firing, cooldowns blocking re-entry, drawdown guards firing once and not double-firing, margin warn/reduce levels, kill-switch auto-trigger.

Next