Risk

RiskConfig + RiskEngine + 7 defensive layers

RiskConfig

Top-level container passed to hz.run(risk=...).

python
from horizon.risk import RiskConfig

@dataclass
class RiskConfig:
    per_order: OrderRisk
    stop_loss: Any = None
    per_asset_class: dict[AssetClass, AssetRisk] = field(default_factory=dict)
    drawdown: list[DrawdownGuard] = field(default_factory=list)
    circuit_breakers: list[Any] = field(default_factory=list)
    margin: MarginGuard | None = None
    scenarios: ScenarioGuard | None = None
    kill_switch: KillSwitch | None = None
    extra_checks: list[Any] = field(default_factory=list)
    max_gross_notional_usd: float = 0.0
    max_net_notional_usd: float = 0.0
    max_gross_leverage: float = 2.0
    max_notional_per_market_usd: float = 0.0
    recent_reject_streak_threshold: int = 0

Method: .override(**kwargs) -> RiskConfig: return a copy with selected fields replaced (useful with presets).

RiskProfile

Presets:

python
RiskProfile.conservative()  # 5% stops, 3/7/15% DD, 20% kill switch, 1.0x leverage
RiskProfile.moderate()      # 10% stops, 5/10/20% DD, 25% kill switch, 2.0x leverage
RiskProfile.aggressive()    # 20% stops, 10/20/35% DD, 40% kill switch, 4.0x leverage

OrderRisk

Per-order check configuration:

python
@dataclass
class OrderRisk:
    max_order_notional_usd: float = 50_000.0
    max_size_pct_of_adv: float = 0.01
    price_min: float = 0.01
    price_max: float = 100_000.0
    rate_limit_per_sec: float = 10.0
    dedup_window_ms: int = 500
    allow_market_orders: bool = True
    max_orders_per_tick: int = 20
    shrink_to_fit: bool = False

AssetRisk

Per-asset-class bundle:

python
@dataclass
class AssetRisk:
    stop_loss: Any = None
    time_stop: Any = None
    max_gross_notional_usd: float | None = None
    max_position_pct_of_adv: float | None = None
    max_position_notional_usd: float | None = None
    max_sector_exposure: float | None = None
    exclude_earnings_within_days: int | None = None
    # Options
    max_portfolio_delta: float | None = None
    max_portfolio_gamma: float | None = None
    max_portfolio_vega: float | None = None
    max_theta_burn_per_day_usd: float | None = None
    # Perps
    max_leverage: float | None = None
    liquidation_buffer_pct: float | None = None
    max_funding_exposure_per_day_usd: float | None = None
    # Prediction markets
    max_per_event_usd: float | None = None
    min_days_to_resolution: int | None = None

Stop loss types

StopLoss

python
@dataclass
class StopLoss:
    per_position_pct: float | None = None
    per_position_usd: float | None = None
    trailing_pct: float | None = None
    cooldown: timedelta = timedelta(0)
    applies_to: list[AssetClass] = field(default_factory=list)
    exclude_markets: list[str] = field(default_factory=list)

Set one or more trigger conditions. Whichever hits first fires the stop.

TrailingStop

python
@dataclass
class TrailingStop:
    trailing_pct: float = 0.05
    cooldown: timedelta = timedelta(0)

GreekStop (options)

python
@dataclass
class GreekStop:
    close_if_delta_over: float | None = None
    close_if_gamma_over: float | None = None
    close_if_vega_over: float | None = None
    close_if_dte_under: int | None = None

TimeStop

python
@dataclass
class TimeStop:
    max_hold_time: timedelta | None = None
    close_before_earnings: bool = False
    close_before_resolution: timedelta | None = None

DrawdownGuard

python
@dataclass
class DrawdownGuard:
    daily_pct: float | None = None
    weekly_pct: float | None = None
    monthly_pct: float | None = None
    total_pct: float | None = None
    from_peak: bool = True
    action: DrawdownAction = DrawdownAction.HaltNew
    recovery_threshold: float = 0.02
    cooldown: timedelta = timedelta(hours=24)

Validation: exactly one of daily_pct, weekly_pct, monthly_pct, total_pct must be set.

DrawdownAction

python
class DrawdownAction(str, Enum):
    HaltNew    = "halt_new"
    ReduceHalf = "reduce_half"
    Flatten    = "flatten"

Circuit breakers

python
@dataclass
class VolatilityBreaker:
    metric: str = "realized_vol_1h"
    threshold: float = 3.0
    lookback: str = "30d"
    action: DrawdownAction = DrawdownAction.HaltNew

@dataclass
class CorrelationShockBreaker:
    threshold: float = 0.9
    min_positions: int = 5
    action: DrawdownAction = DrawdownAction.ReduceHalf

@dataclass
class FeedStalenessBreaker:
    max_age_sec: float = 30.0
    critical_feeds: list[str] | None = None
    action: DrawdownAction = DrawdownAction.HaltNew

@dataclass
class OrderRejectStreakBreaker:
    consecutive_rejects: int = 10
    action: DrawdownAction = DrawdownAction.HaltNew

@dataclass
class SlippageBreaker:
    threshold_bps: float = 20.0
    window: int = 100
    action: DrawdownAction = DrawdownAction.HaltNew

MarginGuard

python
@dataclass
class MarginGuard:
    warn_at_utilization: float = 0.80
    reduce_at_utilization: float = 0.90
    emergency_at_utilization: float = 0.98
    reduce_method: MarginReduceMethod = MarginReduceMethod.LargestLoser
    per_venue: bool = True

KillSwitch

python
@dataclass
class KillSwitch:
    auto_trigger_on: list = field(default_factory=list)
    require_manual_reset: bool = True
    alert_channels: list[str] = field(default_factory=lambda: ["slack", "email"])
    reason_required: bool = True

Auto-trigger events

python
@dataclass
class PortfolioDrawdownEvent:
    threshold: float = 0.25

@dataclass
class MarginCallEvent: ...

@dataclass
class DataFeedCorruptionEvent: ...

@dataclass
class CompliancePauseEvent: ...

RiskEngine

The enforcement side. Used by the run loop.

python
from horizon.risk import RiskEngine, RiskEngineState

engine = RiskEngine(config)

# Per-order check
decision = engine.check_order(action, state)
if decision.passed:
    venue.submit(action)

# Watchdog (per-tick)
events = engine.watchdog(state)

RiskEngineState

python
@dataclass
class RiskEngineState:
    now: datetime
    equity: float
    peak_equity: float
    cash_by_venue: dict[str, float]
    buying_power_by_venue: dict[str, float]
    used_buying_power_by_venue: dict[str, float]
    ledger: PositionLedger | None = None
    kill_switch_triggered: bool = False
    orders_this_tick: int = 0
    drawdown_halt_active: str | None = None
    recent_reject_streak: int = 0

    @property
    def drawdown_pct: float   # max(0, (peak - equity) / peak)

Methods

  • check_order(action, state) -> Decision: run pre-trade layers
  • watchdog(state) -> list[LifecycleEvent]: run per-tick layers (stops, drawdown, breakers, margin, kill switch)
  • active_drawdown_action() -> str | None: the currently active halt mode

Tests

22 tests in tests/test_risk_engine.py covering all 7 layers and both entry points.

Next