Circuit Breakers
Market-wide anomaly detection: vol spikes, feed staleness, reject streaks
Circuit breakers are the market-wide risk layer. They fire on environmental conditions rather than portfolio P&L. things that indicate “the market is behaving strangely, be careful.”
Types
VolatilityBreaker
from horizon.risk import VolatilityBreaker
from horizon.risk.drawdown import DrawdownAction
breaker = VolatilityBreaker(
metric="realized_vol_1h",
threshold=3.0, # 3× baseline
lookback="30d",
action=DrawdownAction.HaltNew,
)
Fires when the chosen vol metric exceeds threshold × rolling_baseline. 3.0 means vol is 3× its recent average, a significant regime shift.
FeedStalenessBreaker
from horizon.risk import FeedStalenessBreaker
breaker = FeedStalenessBreaker(
max_age_sec=30.0,
critical_feeds=["binance:BTC-USD", "alpaca:SPY"],
action=DrawdownAction.HaltNew,
)
Halts trading when any critical feed hasn’t updated in max_age_sec seconds. Essential for intraday strategies: stale data produces false signals.
CorrelationShockBreaker
from horizon.risk import CorrelationShockBreaker
breaker = CorrelationShockBreaker(
threshold=0.9, # average pairwise correlation
min_positions=5,
action=DrawdownAction.ReduceHalf,
)
When the average pairwise correlation across your positions crosses 0.9, it’s a sign of a risk-off event. Normally-diversified positions start moving together. Reducing by half cuts the effective exposure before the portfolio gets hit.
Requires at least min_positions open positions to have enough pairs to compute correlation.
OrderRejectStreakBreaker
from horizon.risk import OrderRejectStreakBreaker
breaker = OrderRejectStreakBreaker(
consecutive_rejects=10,
action=DrawdownAction.HaltNew,
)
Most venues reject occasionally due to timing or liquidity. Ten consecutive rejects means something is wrong: bad credentials, rate limit, venue outage, or a bug.
SlippageBreaker
from horizon.risk import SlippageBreaker
breaker = SlippageBreaker(
threshold_bps=20.0,
window=100,
action=DrawdownAction.HaltNew,
)
Tracks the last window fills. When average realized slippage exceeds threshold_bps, fire. A sudden spike in slippage usually indicates:
- Market impact from your own order size
- Regime change (volatility up, liquidity down)
- Venue issue (partial outage, odd behavior)
Configuration
from horizon.risk import (
RiskConfig,
VolatilityBreaker,
FeedStalenessBreaker,
CorrelationShockBreaker,
OrderRejectStreakBreaker,
SlippageBreaker,
)
from horizon.risk.drawdown import DrawdownAction
risk = RiskConfig(
circuit_breakers=[
VolatilityBreaker(threshold=3.0, action=DrawdownAction.HaltNew),
FeedStalenessBreaker(max_age_sec=30, action=DrawdownAction.HaltNew),
CorrelationShockBreaker(threshold=0.9, action=DrawdownAction.ReduceHalf),
OrderRejectStreakBreaker(consecutive_rejects=10, action=DrawdownAction.HaltNew),
],
)
Status
Circuit breakers are declarative config. Horizon’s RiskEngine.watchdog() currently implements:
- OrderRejectStreakBreaker via
state.recent_reject_streakcounter (✅ live) - FeedStalenessBreaker, VolatilityBreaker, CorrelationShockBreaker, SlippageBreaker: config exists but the watchdog handlers are a future release
The interfaces are stable; fill-in is incremental.
When they fire
All circuit breakers fire LifecycleEvent(kind=CircuitBreakerFire, ...) events from the watchdog. The run loop converts them to drawdown halt mode transitions. setting state.drawdown_halt_active to the appropriate action.