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 Fires when realized vol exceeds N× its recent baseline.
FeedStalenessBreaker Fires when critical feeds stop updating.
CorrelationShockBreaker Fires when pairwise correlations collapse to near-1 (risk-off shock).
OrderRejectStreakBreaker Fires after N consecutive order rejections (venue issue).
SlippageBreaker Fires when realized slippage vastly exceeds expected.

VolatilityBreaker

python
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

python
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

python
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

python
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

python
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

python
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_streak counter (✅ 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.

Pitfalls

Next