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 layerswatchdog(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.