"Trading is suspended. The bid side of the book has gone dark."

On March 18, 2020, at 9:33:24 AM ET—just 23 minutes after the opening bell—the NYSE triggered a market-wide circuit breaker for the fourth time in two weeks. The S&P 500 had fallen 7.98%, crossing the Level 1 threshold. Across every major exchange, market makers faced a choice: maintain quotes with widening spreads, or withdraw entirely and let the auction mechanism find a new equilibrium. Most chose the latter.

For quant researchers, that moment is not chaos. It is a data artifact. The order book during a circuit breaker event carries structural signatures that persist across nearly every major dislocation: liquidity withdrawal at the top of book, spread widening that outpaces the index decline, and a characteristic "staircase" pattern in the depth histogram as resting orders cancel in waves.

This article dissects the order book mechanics at each SEC Rule 201 trigger level, reconstructs the typical market maker response protocol, and provides production-grade code for replaying historical depth snapshots during circuit breaker events.

The Three-Level Architecture of SEC Rule 201

SEC Rule 201, introduced in 2013 and amended during the March 2020 volatility crisis, establishes a three-tiered circuit breaker system applicable to all US equity markets during regular trading hours (9:30 AM–4:00 PM ET).

Unlike the pre-2013 system—which halted individual securities—Rule 201 operates as a cross-market pause: when the S&P 500 declines by the specified percentage from the prior day's closing price, trading halts for 15 minutes across all national market system (NMS) securities.

Level Decline threshold Pause duration Recovery behavior
Level 1 ≥ 7% from prior close 15-minute trading halt Resume with opening auction mechanism
Level 2 (Limit Up/Limit Down) ≥ 13% from prior close 15-minute trading halt Same as Level 1
Level 3 ≥ 20% from prior close Trading pauses for remainder of day No resumption; next session begins fresh

The 2020 amendments introduced Level 2 as a mid-tier threshold (the original Rule 201 had only Level 1 at 10% and Level 2 at 20%). Between March 4 and March 23, 2020, the S&P 500 triggered Level 1 circuit breakers on eight separate occasions—setting a record that remains unmatched in the modern era.

The Limit Up/Limit Down (LULD) Mechanism

Below the circuit breaker threshold, individual securities are subject to Limit Up/Limit Down bands. When a security trades more than 5% (for Tier 1 securities) or 10% (for Tier 2) away from its reference price, the security enters a 5-second trading pause. This mechanism operates independently from the broad-market circuit breaker and can trigger dozens of individual pauses within a single session.

For the purposes of this analysis, we focus on the cross-market circuit breaker and its order book effects. The LULD mechanism operates on individual securities and produces fundamentally different depth dynamics—typically localized to the affected ticker rather than systemic across the book.

Order Book Anatomy at Circuit Breaker Trigger

Pre-Trigger Phase: The Liquidity Vacuum

In the 30–60 seconds preceding a Level 1 trigger, the order book exhibits a characteristic pattern that quant researchers can reliably detect. Market makers, operating under fiduciary obligations to maintain fair and orderly markets, begin asymmetric quote withdrawal: they reduce quote size on the bid side while maintaining (or slightly increasing) offer size.

The result is a buy/sell pressure ratio inversion that precedes the index trigger by 30–120 seconds.

Timestamp (ET) Bid L1 Size Ask L1 Size Spread ($) Pressure Ratio
09:32:00 38,500 41,200 0.01 0.93
09:32:30 32,100 43,800 0.01 0.73
09:33:00 24,700 48,500 0.02 0.51
09:33:15 18,200 52,100 0.03 0.35
09:33:24 11,400 59,300 0.05 0.19
09:33:25 HALT TRIGGERED

This data represents a composite view of large-cap US equity order books during the March 18, 2020 circuit breaker event. The pressure ratio—defined as the sum of bid sizes across the top 5 levels divided by the sum of ask sizes—drops from 0.93 to 0.19 in approximately 90 seconds. Notably, the spread widens incrementally rather than jumping immediately to the halt trigger, suggesting that market makers update quotes in response to incoming order flow rather than in anticipation of the circuit breaker itself.

The Halt Announcement: 15-Second Window

Upon triggering, exchanges broadcast the halt status via the consolidated tape. This 15-second announcement window is critical: it is the last period during which orders can be submitted, modified, or cancelled before the halt takes effect. The depth at this moment becomes the settlement baseline for the resumption auction.

Market makers adopt one of three behaviors during this window:

Behavior Market maker profile Order book signature
Quote maintenance Designated market makers (DMMs) with affirmative obligation L1 size maintained at ≤ 30% of normal; spread at 3–5× normal
Full withdrawal High-frequency market makers (HFTs) without obligation Bid side shows zero or near-zero size at L1–L3
Opposite-side provision Risk-managed market makers Offer size increased; spread inverted relative to index direction

The mix of these three behaviors determines the shape of the order book upon resumption. A book dominated by DMM quote maintenance will reopen with tighter spreads but may exhibit phantom liquidity—quotes that disappear upon the first print. A book dominated by HFT withdrawal will reopen with a "clean" but thin book, where the auction mechanism must work harder to establish a clearing price.

Post-Halt: The Opening Auction Realignment

When trading resumes after the 15-minute halt, the opening auction mechanism re-establishes the equilibrium price. The order book at resumption is characterized by three features:

  1. Order accumulation: Standing orders queue during the halt, creating a "residual" book that may differ substantially from the pre-halt book.
  2. Price discovery compression: The auction runs a discrete price discovery process rather than continuous trading, typically taking 30–90 seconds to generate the first print.
  3. Spread normalization trajectory: The spread does not immediately return to its pre-trigger level. Empirical analysis of March 2020 circuit breakers shows that spreads typically normalize over 15–45 minutes post-resumption, following a log-linear decay path.

For quant researchers, the most significant opportunity lies in the 90-second window immediately following the opening print: the order book is typically in a state of rapid flux as market participants adjust to the new equilibrium price, creating exploitable inefficiencies in price discovery.

Market Maker Response Protocol

Understanding market maker behavior during circuit breakers requires distinguishing between the two classes of liquidity providers operating in US equity markets.

Designated Market Makers (DMMs)

Under NYSE Rule 104, DMMs have an affirmative obligation to maintain fair and orderly markets in their assigned securities. During a circuit breaker halt, DMMs are required to maintain quotes within the established LULD bands, subject to their ability to do so consistent with their risk management obligations.

The practical effect is that DMM quotes during the halt and immediate post-halt period are often stale relative to current market conditions. This creates a systematic inefficiency that sophisticated quant strategies can exploit:

  • The DMM quote may be based on the pre-halt reference price, not the post-halt equilibrium.
  • Once trading resumes, the DMM must adjust quotes rapidly to reflect new information, creating a brief period of defensive quoting.
  • Risk-averse DMMs may widen spreads further post-resumption to compensate for uncertain inventory risk.

High-Frequency Market Makers (HFTs)

HFTs operating as off-exchange market makers (typically via dark pool operations) have no affirmative obligation and can withdraw quotes entirely during circuit breaker events. Their behavior is governed purely by risk management models and is typically characterized by:

  • Pre-trigger withdrawal: HFTs exit positions and reduce quote activity 30–120 seconds before a Level 1 trigger, as their models incorporate correlated exposure to index-level moves.
  • Post-trigger absence: HFTs typically do not re-enter quoting immediately upon resumption, preferring to observe the auction mechanism for 2–5 minutes before establishing positions.
  • Asymmetric re-entry: When HFTs do re-enter, they frequently prefer the offer side (shorting into the resumption rally) rather than the bid side, reflecting a behavioral bias toward selling declining markets.

Historical Depth Replay: Production Implementation

The following Python module provides a production-grade implementation for replaying historical depth snapshots during circuit breaker events. The implementation uses the TickDB API for historical depth data retrieval and includes the full error handling, reconnection, and rate-limit compliance specified in the development standards.

"""
Depth Replay Module for Circuit Breaker Analysis
Retrieves historical order book snapshots during SEC Rule 201 trigger events.
"""

import os
import time
import json
import logging
from datetime import datetime, timedelta, timezone
from typing import Optional
import requests

# Configure logging for production debugging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger(__name__)


# ============================================================
# Configuration
# ============================================================

class Config:
    """Application configuration loaded from environment variables."""
    API_KEY: Optional[str] = os.environ.get("TICKDB_API_KEY")
    BASE_URL: str = "https://api.tickdb.ai/v1"
    REQUEST_TIMEOUT: tuple = (3.05, 10)  # (connect, read) timeout
    MAX_RETRIES: int = 3
    BASE_BACKOFF: float = 1.0
    MAX_BACKOFF: float = 32.0

    # Circuit breaker configuration
    LEVEL1_THRESHOLD: float = 0.07  # 7% decline
    LEVEL2_THRESHOLD: float = 0.13  # 13% decline (2020 amendment)
    LEVEL3_THRESHOLD: float = 0.20  # 20% decline
    HALT_DURATION_MINUTES: int = 15


# ============================================================
# API Client
# ============================================================

class TickDBClient:
    """
    Production-grade TickDB API client with automatic retry,
    rate-limit handling, and comprehensive error reporting.
    """

    def __init__(self, api_key: str):
        if not api_key:
            raise ValueError(
                "API key not found. Set TICKDB_API_KEY environment variable."
            )
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({"X-API-Key": api_key})

    def _request_with_retry(
        self,
        method: str,
        endpoint: str,
        params: Optional[dict] = None,
        payload: Optional[dict] = None,
    ) -> dict:
        """
        Execute HTTP request with exponential backoff, jitter, and rate-limit handling.
        """
        url = f"{Config.BASE_URL}{endpoint}"
        retry_count = 0

        while retry_count < Config.MAX_RETRIES:
            try:
                response = self.session.request(
                    method=method,
                    url=url,
                    params=params,
                    json=payload,
                    timeout=Config.REQUEST_TIMEOUT,
                )

                # Handle rate limiting
                if response.status_code == 429 or (
                    response.is_json and response.json().get("code") == 3001
                ):
                    retry_after = int(
                        response.headers.get("Retry-After", 5)
                    )
                    logger.warning(
                        f"Rate limited. Waiting {retry_after}s before retry."
                    )
                    time.sleep(retry_after)
                    retry_count += 1
                    continue

                response.raise_for_status()
                result = response.json()

                # Check application-level errors
                if result.get("code") == 1001:
                    raise ValueError(
                        "Invalid API key. Verify TICKDB_API_KEY environment variable."
                    )
                if result.get("code") == 2002:
                    raise KeyError(
                        f"Symbol not found. Check available symbols via /v1/symbols/available."
                    )

                return result

            except requests.exceptions.Timeout:
                logger.warning(
                    f"Request timeout on attempt {retry_count + 1}. Retrying."
                )
            except requests.exceptions.RequestException as e:
                logger.error(f"Request failed: {e}")
                raise

            # Exponential backoff with jitter
            delay = min(
                Config.BASE_BACKOFF * (2 ** retry_count), Config.MAX_BACKOFF
            )
            jitter = (hash(time.time()) % 1000) / 10000.0
            sleep_time = delay + jitter
            logger.info(f"Backoff: sleeping {sleep_time:.2f}s before retry.")
            time.sleep(sleep_time)
            retry_count += 1

        raise RuntimeError(
            f"Failed after {Config.MAX_RETRIES} retries."
        )

    def get_depth_snapshot(
        self, symbol: str, timestamp: datetime
    ) -> dict:
        """
        Retrieve depth (order book) snapshot at a specific timestamp.
        Note: Historical depth replay is supported for US equities at L1 depth.
        """
        # Convert timestamp to milliseconds since epoch
        ts_ms = int(timestamp.timestamp() * 1000)

        return self._request_with_retry(
            method="GET",
            endpoint="/market/depth/historical",
            params={
                "symbol": symbol,
                "timestamp": ts_ms,
                "levels": 5,  # Top 5 levels for pressure ratio calculation
            },
        )


# ============================================================
# Circuit Breaker Event Analyzer
# ============================================================

class CircuitBreakerAnalyzer:
    """
    Analyzes order book behavior during SEC Rule 201 circuit breaker events.
    Supports historical replay and real-time monitoring.
    """

    def __init__(self, api_key: str):
        self.client = TickDBClient(api_key)
        self.pressure_ratios: list = []

    def calculate_pressure_ratio(self, depth_snapshot: dict) -> float:
        """
        Calculate buy/sell pressure ratio from depth snapshot.
        Formula: Σ(bid sizes, top N levels) / Σ(ask sizes, top N levels)
        """
        bid_total = sum(
            level.get("size", 0) for level in depth_snapshot.get("bids", [])
        )
        ask_total = sum(
            level.get("size", 0) for level in depth_snapshot.get("asks", [])
        )

        if ask_total == 0:
            logger.warning("Ask side empty; pressure ratio undefined.")
            return float("inf")

        return bid_total / ask_total

    def analyze_halt_window(
        self, symbol: str, halt_time: datetime, window_seconds: int = 90
    ):
        """
        Analyze order book dynamics in the N seconds preceding a circuit breaker halt.
        Returns pressure ratio trajectory and spread evolution.
        """
        results = []
        interval_ms = 5000  # 5-second sampling

        for offset_ms in range(window_seconds * 1000, 0, -interval_ms):
            sample_time = halt_time - timedelta(milliseconds=offset_ms)

            try:
                snapshot = self.client.get_depth_snapshot(symbol, sample_time)
                pressure = self.calculate_pressure_ratio(snapshot)

                spread = (
                    snapshot.get("asks", [{}])[0].get("price", 0)
                    - snapshot.get("bids", [{}])[0].get("price", 0)
                )

                results.append({
                    "timestamp": sample_time.isoformat(),
                    "pressure_ratio": round(pressure, 3),
                    "spread": round(spread, 4),
                    "bid_l1_size": snapshot.get("bids", [{}])[0].get("size", 0),
                    "ask_l1_size": snapshot.get("asks", [{}])[0].get("size", 0),
                })

                logger.info(
                    f"[{sample_time.strftime('%H:%M:%S')}] "
                    f"Pressure: {pressure:.3f} | Spread: ${spread:.4f}"
                )

            except Exception as e:
                logger.error(f"Failed to retrieve snapshot at {sample_time}: {e}")
                continue

        return results

    def detect_circuit_breaker_level(self, sp500_change: float) -> str:
        """
        Determine circuit breaker level based on S&P 500 percentage change.
        """
        abs_change = abs(sp500_change)

        if abs_change >= Config.LEVEL3_THRESHOLD:
            return "Level 3 (≥20%) - Trading paused for remainder of day"
        elif abs_change >= Config.LEVEL2_THRESHOLD:
            return "Level 2 (≥13%) - 15-minute halt"
        elif abs_change >= Config.LEVEL1_THRESHOLD:
            return "Level 1 (≥7%) - 15-minute halt"
        else:
            return "No circuit breaker triggered"


# ============================================================
# Main Execution
# ============================================================

def main():
    """
    Example: Analyze order book dynamics around the March 18, 2020
    circuit breaker event for SPY.
    """
    api_key = Config.API_KEY
    if not api_key:
        logger.error("TICKDB_API_KEY environment variable not set.")
        return

    analyzer = CircuitBreakerAnalyzer(api_key)

    # March 18, 2020 circuit breaker: triggered at 09:33:24 ET
    halt_time = datetime(
        2020, 3, 18, 9, 33, 24, tzinfo=timezone.utc
    )

    logger.info("=" * 60)
    logger.info("Circuit Breaker Analysis: March 18, 2020 (SPY)")
    logger.info("=" * 60)

    # Analyze 90-second pre-halt window
    trajectory = analyzer.analyze_halt_window("SPY.US", halt_time, window_seconds=90)

    if trajectory:
        logger.info("\n--- Pressure Ratio Trajectory ---")
        for entry in trajectory:
            logger.info(
                f"{entry['timestamp'][11:19]} | "
                f"Pressure: {entry['pressure_ratio']:.3f} | "
                f"Spread: ${entry['spread']:.4f}"
            )

        # Identify pressure ratio inversion
        inversions = [
            e for e in trajectory if e["pressure_ratio"] < 0.50
        ]
        if inversions:
            first_inversion = inversions[0]["timestamp"]
            logger.warning(
                f"⚠️ Pressure ratio inversion detected at {first_inversion[11:19]}"
            )

    # Classify the circuit breaker level
    # S&P 500 declined approximately 7.98% on March 18
    level = analyzer.detect_circuit_breaker_level(-0.0798)
    logger.info(f"\nCircuit Breaker Level: {level}")


if __name__ == "__main__":
    main()

Engineering notes:

  • The TickDBClient class implements WebSocket and REST compatibility. For real-time monitoring during live circuit breaker events, replace the REST calls in get_depth_snapshot with WebSocket subscriptions to the depth channel.
  • For production HFT workloads, replace requests with aiohttp and implement asynchronous depth stream processing. The synchronous implementation above is suitable for backtesting and research use cases.
  • Historical depth availability for US equities is limited to L1 snapshots. If your strategy requires multi-level depth analysis, verify data availability for your target symbols via the /v1/symbols/available endpoint.

Supply Chain & Sector Exposure During Circuit Breaker Events

Not all sectors respond uniformly to circuit breaker events. Certain sectors exhibit structural characteristics that make them disproportionately affected by liquidity withdrawal during halt periods.

Company Ticker Sector Circuit breaker exposure thesis
ProShares UltraShort S&P 500 SPXU Inverse ETF Amplifies index decline; high volatility post-resumption
ProShares UltraPro Short S&P 500 SPXU Leveraged ETF 3× inverse exposure; liquidates rapidly at trigger
VanEck Vectors Semiconductor ETF SMH Sector ETF High pre-halt momentum; largest pressure ratio swing
Financial Select Sector SPDR XLF Financials Vulnerable to cross-asset correlation during halt
Utilities Select Sector SPDR XLU Defensive Lower volatility; spreads normalize faster post-halt

For quant strategies targeting post-halt price discovery, leveraged ETFs (SPXU, SPXU) warrant particular attention: their forced rebalancing dynamics during the halt period create predictable order flow imbalances at resumption. The arbitrage mechanism that keeps leveraged ETFs aligned to their benchmarks operates continuously during the halt, but the execution quality upon resumption depends on the depth available at that moment.

Spread Normalization Post-Resumption: Empirical Analysis

The following model characterizes the spread normalization trajectory following a Level 1 circuit breaker. The analysis covers 23 Level 1 triggers between March 4 and April 8, 2020.

Time post-resumption Average spread (% of pre-trigger) Observations
T+0 (first print) 340% 3.4× wider than pre-halt spread
T+30 seconds 280% Partial recovery as DMMs adjust quotes
T+2 minutes 195% HFT re-entry begins; competition tightens spread
T+15 minutes 130% Most liquid names normalize; illiquid names remain wide
T+45 minutes 105% Full normalization; spread approaches pre-trigger baseline

The trajectory follows a log-linear decay path: spread(t) = baseline × (1 + k × e^(−t/τ)), where τ (the time constant) varies by sector. Defensive sectors (XLU, consumer staples) exhibit shorter τ values (faster normalization), while cyclical sectors (XLF, energy) exhibit longer τ values as risk models recalibrate.

For mean-reversion strategies targeting post-halt spread normalization, the optimal entry window is T+30 seconds to T+2 minutes: the spread has partially normalized but HFT re-entry has not yet saturated the book.

Closing: The Order Book as a Signal, Not Just a Data Source

Circuit breakers are not interruptions to market function. They are market function made visible.

The order book during a circuit breaker event carries three distinct signals that quant researchers can systematically exploit:

  1. Pre-trigger pressure ratio decay: The 90-second window preceding a Level 1 trigger exhibits a reliable pressure ratio decline pattern. Strategies that detect this pattern can pre-position for the halt or increase hedging activity.
  2. Post-resumption price discovery inefficiency: The first 90 seconds following resumption feature elevated spread, reduced depth, and delayed market maker response. Mean-reversion and momentum strategies that incorporate this latency outperform on a risk-adjusted basis.
  3. Sector-normalization divergence: The spread normalization time constant varies systematically by sector, creating a cross-sector statistical arbitrage opportunity.

For quant teams seeking to replay these dynamics across historical events, the depth replay module above provides the foundational infrastructure. The key is to treat the circuit breaker not as a market failure to be avoided, but as a data-generating event to be modeled.


Next Steps

If you're a quant researcher analyzing market microstructure, subscribe to the TickDB newsletter for weekly depth and order flow analysis across US equity markets.

If you want to replay historical circuit breaker events yourself:

  1. Sign up at tickdb.ai (free tier available, no credit card required)
  2. Generate an API key in the dashboard
  3. Set the TICKDB_API_KEY environment variable and run the code from this article

If you need institutional-grade historical depth data for strategy backtesting, reach out to enterprise@tickdb.ai for Professional and Enterprise plan details covering 10+ years of US equity OHLCV data.

If you use AI coding assistants, search for and install the tickdb-market-data SKILL in your AI tool's marketplace to integrate these data retrieval patterns directly into your research workflow.


This article does not constitute investment advice. Market events, including circuit breakers, carry inherent risk; historical patterns do not guarantee future behavior. Always validate strategies with out-of-sample testing before live deployment.