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:
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:
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:
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
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:
LifecycleEvent(kind=StopLoss, market_id=...)is emitted- The run loop converts it to a
OrderAction.place(..., order_type="market", urgency=Immediate) - The market order passes through pre-order checks (which always allow reduces during drawdown halts)
- Paper venue fills it at the current tick price + slippage
- The market enters cooldown: new orders on it are rejected until cooldown expires
Greek stops (options)
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:
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:
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:
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:
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:
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:
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:
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:
- All per-order checks immediately reject new orders
- All strategies get halted
- Alerts fire on all configured channels
- Requires an explicit reset with a reason
Assembling the full config
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:
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.