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:
- 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.
- 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.
- 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 millisecondsbids: 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-clientlibrarypython-dotenvfor 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
depthchannel delivers real-time order book data. Historical depth snapshots for backtesting are not supported. For backtesting event-driven strategies, use TickDB's/v1/market/klineendpoint (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:
- Sign up at tickdb.ai — the free tier requires no credit card
- Generate an API key in the dashboard
- Set the
TICKDB_API_KEYenvironment variable - 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.