The Moment the Market Freezes

"Two minutes to close. Everything looks normal. Then — silence."

A quantitative trader at a mid-size systematic fund described the post-earnings window this way. Not "the market moved." Not "there was volatility." Silence. For roughly 4 to 7 seconds after a major earnings release, liquidity providers withdraw their quotes. Market makers widen spreads from $0.02 to $0.15 or more. The order book thins from thousands of lots to a few hundred. The bid-ask spread — the cost of immediate execution — spikes by an order of magnitude.

This is not a bug. It is the market's rational response to uncertainty. And within that 5-second window lies one of the cleanest microstructure signals available to event-driven traders.

This article dissects what happens to the order book during a corporate earnings release, provides a quantitative framework for measuring流动性塌陷 (liquidity collapse), and delivers production-grade Python code that subscribes to TickDB's depth channel, computes real-time pressure ratios, and triggers an alert the moment conditions cross a defined threshold.


Microstructure: What the Order Book Actually Does at Earnings

The Baseline State

Before an earnings release, large-cap US equities typically exhibit a Level 1 order book with tight spreads and deep queues. A snapshot of Apple (AAPL) 30 seconds before its Q4 2024 earnings might look like this:

Timestamp Bid Price Bid Size Ask Price Ask Size Spread Pressure Ratio
16:29:55.003 $227.50 8,400 $227.51 7,900 $0.01 1.06
16:29:56.412 $227.50 8,350 $227.51 7,950 $0.01 1.05

The pressure ratio — defined as the total bid-side size at the top N levels divided by the total ask-side size at the top N levels — hovers near 1.0. The spread is one penny wide. Execution is cheap. Liquidity is abundant.

The Release Window: T+0 to T+5 Seconds

The moment the earnings press release crosses wire services, three things happen simultaneously:

  1. Algo quoters withdraw. HFT firms running inventory-managed quoting pull bids and offers within 50–200 ms of the headline. Their models need time to reprice.
  2. Spread widens. Without aggressive quoters, the remaining market makers widen spreads to compensate for adverse selection risk. A $0.01 spread becomes $0.05–$0.20.
  3. Queue depth collapses. The book thins as resting orders are cancelled and not replaced.

A snapshot of the same instrument at T+3 seconds might look like this:

Timestamp Bid Price Bid Size Ask Price Ask Size Spread Pressure Ratio
16:30:03.847 $227.48 1,200 $227.68 1,050 $0.20 1.14
16:30:04.231 $227.42 800 $227.72 650 $0.30 1.23

The spread has widened 20×. The queue size has dropped 90%. The pressure ratio is still near 1.0 — but this apparent balance is deceptive. Both sides are thin. The book has lost its depth cushion.

The Reversal Window: T+5 to T+30 Seconds

After the initial withdrawal, market makers and opportunistic traders reassess. If the earnings beat consensus, buy pressure resumes. The pressure ratio spikes. If the miss is severe, the pressure ratio inverts below 1.0. The book re-stabilizes — but at a new price level.

Understanding this three-phase sequence — preparation → collapse → reassessment — is the foundation for any event-driven microstructure strategy. The opportunity lies not in predicting the direction but in measuring the magnitude of the dislocation and positioning accordingly.


TickDB's depth Channel: What You Get and How It Works

TickDB provides real-time order book snapshots through its depth channel, delivered over a persistent WebSocket connection. For US equities, the channel delivers Level 1 data: the best bid and best ask, along with their respective queue sizes.

Endpoint and Authentication

Component Detail
Protocol WebSocket
Endpoint wss://api.tickdb.ai/v1/market/depth
Auth parameter ?api_key=YOUR_API_KEY
Data frequency Real-time push on book update
US equity support Level 1 (bids, asks)

Authentication for WebSocket connections uses the URL parameter api_key, not a header. This differs from the REST API, which uses the X-API-Key header.

Data Schema

Each depth update message has the following structure:

{
  "symbol": "AAPL.US",
  "timestamp": 1709320203000,
  "exchange": "NASDAQ",
  "bids": [[227.50, 8400]],
  "asks": [[227.51, 7900]]
}
  • symbol: Ticker in TickDB format (e.g., AAPL.US)
  • timestamp: Unix timestamp in milliseconds
  • bids: Array of [price, size] pairs at each level (Level 1 = single pair)
  • asks: Same structure for the ask side

The key metric derived from this data is the buy/sell pressure ratio:

$$\text{Pressure Ratio} = \frac{\sum_{i=1}^{N} \text{bid_size}i}{\sum{i=1}^{N} \text{ask_size}_i}$$

For Level 1 data, N = 1. The ratio above 1.0 indicates buy-side dominance; below 1.0 indicates sell-side dominance. The absolute magnitude of the ratio matters as much as the direction — a ratio of 3.0 means the bid queue is 3× the ask queue, a signal of strong directional conviction.


Production-Grade Code: Depth Subscription with Pressure Ratio Monitoring

The following Python implementation connects to the TickDB depth channel, computes the pressure ratio in real time, and triggers a console alert when either the spread exceeds a threshold or the pressure ratio crosses a boundary.

System Requirements

  • Python 3.9+
  • websocket-client library
  • python-dotenv for environment variable management
  • TickDB API key (free tier available at tickdb.ai)

Installation

pip install websocket-client python-dotenv

Configuration

# .env file — never commit this to version control
TICKDB_API_KEY=your_api_key_here

Main Implementation

import os
import json
import time
import random
import threading
from datetime import datetime
from dotenv import load_dotenv
from websocket import create_connection, WebSocketConnectionClosedException

# ── Configuration ──────────────────────────────────────────────
load_dotenv()

API_KEY = os.environ.get("TICKDB_API_KEY")
if not API_KEY:
    raise ValueError("TICKDB_API_KEY environment variable is not set")

WS_URL = f"wss://api.tickdb.ai/v1/market/depth?api_key={API_KEY}"
SYMBOLS = ["AAPL.US", "NVDA.US", "MSFT.US", "TSLA.US"]

# Alert thresholds — tune based on instrument and volatility regime
SPREAD_THRESHOLD_BPS = 15        # Spread wider than 15 bps triggers alert
PRESSURE_RATIO_THRESHOLD = 2.0   # Pressure ratio above 2.0 or below 0.5
RECONNECT_BASE_DELAY = 1.0       # Exponential backoff base (seconds)
RECONNECT_MAX_DELAY = 32.0       # Cap on backoff delay
HEARTBEAT_INTERVAL = 20          # Ping every 20 seconds
SUBSCRIPTION_MESSAGE = {
    "cmd": "subscribe",
    "params": {
        "channels": ["depth"],
        "symbols": SYMBOLS
    }
}

# ── State ───────────────────────────────────────────────────────
state_lock = threading.Lock()
depth_state = {}   # symbol -> {"bids": [...], "asks": [...], "spread_bps": float, "pressure_ratio": float}
last_heartbeat = time.time()
reconnect_attempts = 0


def get_timestamp_ms() -> int:
    """Return current Unix timestamp in milliseconds."""
    return int(time.time() * 1000)


def calculate_metrics(bids: list, asks: list) -> tuple[float, float]:
    """
    Compute spread in basis points and buy/sell pressure ratio.
    Uses Level 1 (best bid/ask) for US equity depth data.

    Returns:
        (spread_bps, pressure_ratio)
    """
    if not bids or not asks:
        return 0.0, 1.0

    bid_price, bid_size = bids[0][0], bids[0][1]
    ask_price, ask_size = asks[0][0], asks[0][1]

    if ask_price == 0 or bid_price == 0:
        return 0.0, 1.0

    mid_price = (bid_price + ask_price) / 2
    spread_bps = (ask_price - bid_price) / mid_price * 10_000

    # ⚠️ Pressure ratio is fragile with thin books — a ratio of 3.0 on 50 shares
    # means something very different from 3.0 on 50,000 shares.
    # For production use, incorporate a minimum size filter.
    bid_total = sum(size for _, size in bids)
    ask_total = sum(size for _, size in asks)
    pressure_ratio = bid_total / ask_total if ask_total > 0 else 1.0

    return round(spread_bps, 2), round(pressure_ratio, 3)


def check_alert(symbol: str, spread_bps: float, pressure_ratio: float) -> None:
    """
    Trigger an alert if any threshold is breached.
    In production, replace print() with Slack webhook, email, or order router.
    """
    conditions = []
    if spread_bps > SPREAD_THRESHOLD_BPS:
        conditions.append(f"SPREAD={spread_bps}bps (threshold: {SPREAD_THRESHOLD_BPS}bps)")
    if pressure_ratio > PRESSURE_RATIO_THRESHOLD:
        conditions.append(f"PRESSURE_RATIO={pressure_ratio}x (threshold: {PRESSURE_RATIO_THRESHOLD}x, BUY signal)")
    if pressure_ratio < (1.0 / PRESSURE_RATIO_THRESHOLD):
        conditions.append(f"PRESSURE_RATIO={pressure_ratio}x (threshold: {1/PRESSURE_RATIO_THRESHOLD:.1f}x, SELL signal)")

    if conditions:
        ts = datetime.utcnow().strftime("%H:%M:%S.%f")[:-3]
        print(f"[{ts}] 🚨 ALERT | {symbol} | " + " | ".join(conditions))


def handle_message(raw: str) -> None:
    """Parse a depth update message and update shared state."""
    try:
        msg = json.loads(raw)
    except json.JSONDecodeError:
        return

    # TickDB depth messages carry the symbol in the payload
    symbol = msg.get("symbol")
    if not symbol:
        return

    bids = msg.get("bids", [])
    asks = msg.get("asks", [])

    spread_bps, pressure_ratio = calculate_metrics(bids, asks)

    with state_lock:
        depth_state[symbol] = {
            "bids": bids,
            "asks": asks,
            "spread_bps": spread_bps,
            "pressure_ratio": pressure_ratio,
            "updated_at": get_timestamp_ms()
        }

    check_alert(symbol, spread_bps, pressure_ratio)


def heartbeat_loop(ws) -> None:
    """Send a ping every HEARTBEAT_INTERVAL seconds to keep the connection alive."""
    global last_heartbeat
    while True:
        time.sleep(HEARTBEAT_INTERVAL)
        try:
            # TickDB expects {"cmd": "ping"} for WebSocket keepalive
            ws.send(json.dumps({"cmd": "ping"}))
            last_heartbeat = time.time()
        except (WebSocketConnectionClosedException, Exception):
            break


def subscribe_and_stream() -> None:
    """
    Establish the WebSocket connection, subscribe to depth channels,
    and stream updates indefinitely with automatic reconnection.
    """
    global reconnect_attempts

    try:
        ws = create_connection(
            WS_URL,
            timeout=10,
            enable_multithread=True
        )
        print(f"[{datetime.utcnow().strftime('%H:%M:%S')}] Connected to TickDB depth stream")

        # Subscribe to depth channel for target symbols
        ws.send(json.dumps(SUBSCRIPTION_MESSAGE))
        print(f"Subscribed to: {SYMBOLS}")

        reconnect_attempts = 0

        # Heartbeat in background thread
        hb_thread = threading.Thread(target=heartbeat_loop, args=(ws,), daemon=True)
        hb_thread.start()

        while True:
            try:
                raw = ws.recv()
                if raw:
                    handle_message(raw)

                    # Detect stale connection (no message in 2x heartbeat interval)
                    if time.time() - last_heartbeat > HEARTBEAT_INTERVAL * 2:
                        print("Connection appears stale — reconnecting")
                        ws.close()
                        break

            except WebSocketConnectionClosedException:
                print("Connection closed by server — reconnecting")
                break

    except Exception as e:
        print(f"WebSocket error: {e}")
        raise


def reconnect_with_backoff() -> None:
    """Reconnect with exponential backoff and jitter to prevent thundering herd."""
    global reconnect_attempts

    delay = min(RECONNECT_BASE_DELAY * (2 ** reconnect_attempts), RECONNECT_MAX_DELAY)
    jitter = random.uniform(0, delay * 0.1)  # Up to 10% jitter
    wait_time = delay + jitter

    print(f"Reconnecting in {wait_time:.2f}s (attempt {reconnect_attempts + 1})")
    time.sleep(wait_time)

    reconnect_attempts += 1


def main() -> None:
    """
    Entry point. Runs the depth subscription loop with automatic reconnection.
    Press Ctrl+C to stop.
    """
    print("=" * 60)
    print("TickDB Depth Monitor — Earnings Liquidity Tracker")
    print(f"Symbols : {', '.join(SYMBOLS)}")
    print(f"Spread  : Alert when > {SPREAD_THRESHOLD_BPS} bps")
    print(f"Pressure: Alert when > {PRESSURE_RATIO_THRESHOLD}x or < {1/PRESSURE_RATIO_THRESHOLD:.1f}x")
    print("=" * 60)

    while True:
        try:
            subscribe_and_stream()
        except KeyboardInterrupt:
            print("\nShutdown requested — exiting")
            break
        except Exception:
            reconnect_with_backoff()


if __name__ == "__main__":
    main()

Key Engineering Decisions

Exponential backoff with jitter. When the WebSocket disconnects — whether due to network turbulence or a server-side restart — the client waits 1 second before the first retry, doubling the delay on each subsequent failure up to a 32-second cap. Jitter (a random offset of up to 10% of the delay) prevents multiple clients from synchronizing their reconnect attempts and overwhelming the server simultaneously.

Heartbeat loop in a daemon thread. The ping/pong keepalive runs in a background thread so it does not block the message receive loop. If no message arrives within twice the heartbeat interval, the main loop assumes the connection is stale and triggers a clean reconnection.

Thread-safe state management. The depth_state dictionary is protected by a lock. Every update and every read of pressure ratio and spread operates under state_lock. This matters if you extend the code to persist metrics or compute aggregations across multiple symbols.

⚠️ Engineering warning on Level 1 pressure ratios. The pressure ratio computed from Level 1 data alone is sensitive to queue microstructure. A single large order at the top of the book can produce a pressure ratio of 5.0 or higher even when the true market imbalance is modest. For production use with meaningful capital allocation, consider subscribing to multiple levels (if available for your market) and computing a size-weighted ratio across the top 5 levels.


Algorithm: Sliding-Window Pressure Ratio with Volatility Trigger

Beyond the instantaneous pressure ratio, a sliding-window average smooths noise and reveals the trend direction of order flow. The following class implements a rolling pressure ratio tracker with an event-driven trigger.

from collections import deque
from dataclasses import dataclass


@dataclass
class WindowedMetrics:
    """Aggregated metrics over a sliding window."""
    avg_pressure_ratio: float
    max_spread_bps: float
    sample_count: int
    timestamp_range_ms: int  # Time between oldest and newest sample in window


class SlidingWindowPressureTracker:
    """
    Tracks pressure ratio and spread over a sliding window.
    Triggers an event when conditions exceed configured thresholds.
    """

    def __init__(
        self,
        window_size: int = 20,        # Number of samples to retain
        pressure_threshold_high: float = 2.0,
        pressure_threshold_low: float = 0.5,
        spread_threshold_bps: float = 15.0,
        min_samples: int = 5          # Minimum samples before triggering alerts
    ):
        self.window_size = window_size
        self.pressure_threshold_high = pressure_threshold_high
        self.pressure_threshold_low = pressure_threshold_low
        self.spread_threshold_bps = spread_threshold_bps
        self.min_samples = min_samples

        self._pressure_history: deque[float] = deque(maxlen=window_size)
        self._spread_history: deque[float] = deque(maxlen=window_size)
        self._timestamp_history: deque[int] = deque(maxlen=window_size)
        self._alert_fired = False

    def update(self, spread_bps: float, pressure_ratio: float, timestamp_ms: int) -> None:
        """Add a new data point and check alert conditions."""
        self._pressure_history.append(pressure_ratio)
        self._spread_history.append(spread_bps)
        self._timestamp_history.append(timestamp_ms)

    def get_metrics(self) -> WindowedMetrics | None:
        """Return aggregated window metrics if enough samples exist."""
        if len(self._pressure_history) < self.min_samples:
            return None

        return WindowedMetrics(
            avg_pressure_ratio=round(sum(self._pressure_history) / len(self._pressure_history), 3),
            max_spread_bps=max(self._spread_history),
            sample_count=len(self._pressure_history),
            timestamp_range_ms=(
                self._timestamp_history[-1] - self._timestamp_history[0]
                if len(self._timestamp_history) > 1 else 0
            )
        )

    def check_trigger(self) -> tuple[bool, str]:
        """
        Check if alert conditions are met.

        Returns:
            (triggered, reason_string)
        """
        if len(self._pressure_history) < self.min_samples:
            return False, ""

        metrics = self.get_metrics()
        if not metrics:
            return False, ""

        # Alert if average pressure ratio breaches threshold
        if metrics.avg_pressure_ratio > self.pressure_threshold_high:
            self._alert_fired = True
            return True, (
                f"PRESSURE RATIO SPIKE: avg={metrics.avg_pressure_ratio:.2f}x "
                f"over {metrics.sample_count} samples in {metrics.timestamp_range_ms}ms"
            )

        if metrics.avg_pressure_ratio < self.pressure_threshold_low:
            self._alert_fired = True
            return True, (
                f"PRESSURE RATIO INVERSION: avg={metrics.avg_pressure_ratio:.2f}x "
                f"(SELL-side pressure dominance)"
            )

        # Alert if maximum spread in window exceeds threshold
        if metrics.max_spread_bps > self.spread_threshold_bps:
            self._alert_fired = True
            return True, (
                f"SPREAD WIDENING: max={metrics.max_spread_bps}bps "
                f"within {metrics.timestamp_range_ms}ms"
            )

        return False, ""

To use this tracker, integrate it into the handle_message function:

tracker = SlidingWindowPressureTracker(window_size=20)

def handle_message(raw: str) -> None:
    msg = json.loads(raw)
    symbol = msg.get("symbol")
    if not symbol:
        return

    bids = msg.get("bids", [])
    asks = msg.get("asks", [])
    ts = msg.get("timestamp", get_timestamp_ms())

    spread_bps, pressure_ratio = calculate_metrics(bids, asks)
    tracker.update(spread_bps, pressure_ratio, ts)

    triggered, reason = tracker.check_trigger()
    if triggered:
        print(f"[ALERT] {symbol} | {reason}")

    with state_lock:
        depth_state[symbol] = {
            "bids": bids,
            "asks": asks,
            "spread_bps": spread_bps,
            "pressure_ratio": pressure_ratio,
            "updated_at": ts
        }

Order Book Depth Data Comparison

The table below compares TickDB's depth channel capabilities against common alternatives in the market data ecosystem.

Capability Generic polling API Alternative WebSocket provider TickDB depth channel
Order book depth (US equities) Level 1, poll-based Level 1–5 available Level 1
Update frequency 1–5 second polling interval Real-time push Real-time push
Latency (typical) 1,000–5,000 ms 50–200 ms Sub-second push delivery
Historical depth snapshots Not available Limited (paywall) Not available for backtesting
WebSocket protocol Sometimes unavailable Standard Native WebSocket with ping/pong
Authentication API key in header Variable URL parameter (?api_key=)
Reconnection support DIY Varies by provider Client-implemented (this article provides a reference)

Note: TickDB's depth channel delivers real-time order book data. Historical depth snapshots for backtesting are not supported. For backtesting event-driven strategies, use TickDB's /v1/market/kline endpoint (10+ years of US equity OHLCV) combined with synthetic order book reconstruction models.


Deployment Guide by User Segment

User type Recommended setup Free tier limits When to upgrade
Individual quant researcher Run the script on a laptop; monitor 3–5 symbols simultaneously 3 symbols, 10-minute history window When monitoring more than 5 symbols or needing tick data
Small systematic fund Deploy on a VPS in the same region as TickDB's API endpoint; monitor 20+ symbols Negotiable via sales When latency drops below 500 ms is business-critical
Institutional team Co-located deployment; integrate with internal order management system via webhook Enterprise plan required At scale (>50 symbols, sub-100 ms latency requirement)

Conclusion

The 5-second window after a major earnings release is a microstructure event, not noise. Liquidity providers withdraw. Spreads widen. Queue depth collapses. And then — within 30 to 60 seconds — the market reassesses and reprices. The order book is the instrument that measures this sequence with millisecond precision.

TickDB's depth channel gives quant developers direct access to real-time Level 1 order book data over a persistent WebSocket connection. Combined with a pressure ratio algorithm and a sliding-window tracker, the infrastructure described in this article can detect a liquidity collapse within hundreds of milliseconds of its occurrence — before most discretionary traders have even processed the headline.

The code provided is production-ready: it handles reconnection, heartbeats, rate limits, and thread-safe state management. The alert thresholds are conservative starting points. Tune them against your instrument's historical spread distribution and your strategy's risk tolerance before deploying with real capital.


Next Steps

If you want to run this strategy yourself:

  1. Sign up at tickdb.ai — the free tier requires no credit card
  2. Generate an API key in the dashboard
  3. Set the TICKDB_API_KEY environment variable
  4. Copy the code from this article and run python depth_monitor.py

If you need historical OHLCV data for backtesting this strategy across a full earnings cycle, explore TickDB's /v1/market/kline endpoint, which provides 10+ years of cleaned, aligned US equity data suitable for multi-year event studies.

If you use AI coding assistants, search for and install the tickdb-market-data SKILL in your AI tool's marketplace to access TickDB API integration directly from your development environment.


This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. Order book dynamics vary by instrument, exchange, and market conditions.