"Volume went to zero. The bid side just evaporated."
That was the only explanation a market maker gave on a public forum after his firm's risk system failed to capture a $47 move in 4.3 seconds. The stock was reporting earnings. The move was predictable in direction — but not in magnitude.
Earnings releases are the most violent short-duration events in equity markets. The price does not drift toward a new equilibrium. It jumps — and in the 3 to 8 seconds immediately following the release, the order book enters a state that quantitative models trained on calm-market data simply cannot handle. Liquidity providers withdraw. Spread widens. The book thins to its first and second levels, and for a brief window, a single order of 5,000 shares can move the price more than 50 basis points.
For quant developers building event-driven systems, that window is simultaneously the most dangerous and the most opportunity-rich moment in the trading day. This article dissects what happens to the order book during those five seconds and provides production-grade code to capture it in real time using the TickDB depth channel.
The Anatomy of an Earnings-Induced Liquidity Collapse
What the Order Book Looks Like Before the Release
Under normal conditions, a heavily traded equity like Apple (AAPL) or NVIDIA (NVDA) maintains a deep, layered order book. At 50 milliseconds of latency, you might observe 10 to 15 levels on each side, with size distributed relatively evenly across those levels. The bid-ask spread sits tight — typically $0.01 to $0.02 for large-cap names — and the buy/sell pressure ratio hovers between 0.8 and 1.2.
This is equilibrium. Market makers are present, algos are posting, and any large order gets absorbed by the cumulative depth.
The Moment of Release: Structural Breakdown
The earnings release breaks this equilibrium in a characteristic sequence:
Phase 1 — Freeze (T-2 sec to T+0.5 sec):
Market makers pull their resting orders the instant they detect unusual quote traffic. This is not panic — it is rational risk management. The cost of being on the wrong side of a 200% implied move in 3 seconds exceeds any market-making spread. The visible book thins dramatically. L2 through L10 levels disappear. Only L1 remains, often with severely reduced size.
Phase 2 — Gap and Spread Explosion (T+0.5 sec to T+2 sec):
If the reported EPS or revenue surprises to the upside, the ask side compresses. If to the downside, the bid side compresses. Simultaneously, the surviving limit orders on the compressed side get lifted or hit immediately. The spread explodes from $0.02 to $0.10, $0.20, or wider. This is the "liquidity vacuum" — the moment where price discovery is most chaotic and the book is most fragile.
Phase 3 — Re-accumulation (T+2 sec to T+8 sec):
New liquidity arrives. But it arrives at the new price level, not the old one. The book re-forms around a new equilibrium, but the depth is shallower and the spread remains elevated for 30 to 90 seconds before normalizing.
Quantified Order Book Metrics During Earnings
The following table represents idealized observations from real earnings events on high-profile US equities (anonymized, reconstructed from public tick data research and industry reports). Actual values vary by stock, volatility regime, and time of day.
| Timestamp (relative) | Bid L1 Size | Ask L1 Size | Spread ($) | Pressure Ratio | Book Depth (L1–L5) |
|---|---|---|---|---|---|
| T-5 sec | 18,200 | 17,500 | 0.02 | 1.04 | 124,000 |
| T-1 sec | 9,800 | 11,400 | 0.02 | 0.86 | 67,000 |
| T+0.2 sec (beat) | 3,200 | 21,500 | 0.05 | 0.15 | 31,000 |
| T+1 sec | 1,100 | 35,000 | 0.12 | 0.03 | 18,000 |
| T+3 sec | 8,400 | 14,200 | 0.08 | 0.59 | 52,000 |
| T+10 sec | 22,000 | 19,800 | 0.03 | 1.11 | 98,000 |
Notice the pressure ratio collapse: from 1.04 (balanced) to 0.03 (extreme bid-side withdrawal) in under two seconds. This is the signal. A system that can detect this ratio inversion in real time — before price has fully adjusted — has a structural edge in understanding order flow asymmetry.
The Depth Channel: Real-Time Visibility into Book Structure
Capturing this sequence requires more than price bars. OHLCV data is too coarse. Level 1 NBBO is insufficient. What you need is the structure of the book — how size is distributed across levels, how the ratio between bid-side depth and ask-side depth is shifting, and how rapidly the spread is widening.
The TickDB depth channel delivers this at WebSocket push frequency. For US equities, it provides L1 depth snapshots — the single best bid and best ask with their respective sizes — at sub-100ms latency.
This is not a polling endpoint. It is a streaming subscription. Your code receives a push every time the top of the book changes. During the earnings window, that frequency can exceed 50 updates per second on high-volume names.
Why the Depth Channel Beats Polling for Event-Driven Work
A polling architecture — querying /depth every 500ms — will miss the T+0.2 sec to T+1 sec window entirely. By the time your next poll executes, the book has already collapsed, recovered partially, and begun re-accumulating. You are looking at a photograph of a moving car.
A WebSocket subscription to the depth channel gives you a continuous video. You see the collapse in real time. You can compute the pressure ratio update-to-update. You can detect the spread explosion within 50ms of its occurrence.
Production-Grade Code: Real-Time Depth Monitor with Pressure Ratio Engine
The following Python implementation provides a complete depth monitoring system. It includes WebSocket subscription with heartbeat and reconnection logic, a pressure ratio calculation engine with configurable windows, a spread explosion detector, and a webhook alerting mechanism for downstream consumption.
This code is production-grade: it handles reconnection with exponential backoff and jitter, respects rate limits, sets timeouts on all HTTP operations, and loads all credentials from environment variables. Engineering warnings are included in comments for operators who may need to extend this for HFT workloads.
"""
TickDB Depth Monitor — Earnings Event Stream
Captures order book depth changes in real time, computes buy/sell pressure ratio,
and triggers alerts when spread explodes or pressure ratio inverts.
⚠️ Engineering note: For HFT workloads (< 5ms latency requirements),
replace websocket-client with asyncio-based aiohttp or websockets.
This implementation is optimized for < 100ms alert latency, suitable for
mid-frequency event-driven strategies.
"""
import os
import json
import time
import random
import logging
import threading
from datetime import datetime
from collections import deque
from typing import Optional, Callable
import websocket # pip install websocket-client
# ─── Configuration ───────────────────────────────────────────────────────────
TICKDB_API_KEY = os.environ.get("TICKDB_API_KEY")
if not TICKDB_API_KEY:
raise ValueError("TICKDB_API_KEY environment variable is not set")
TICKDB_WS_HOST = "wss://api.tickdb.ai/ws/depth"
SYMBOLS = ["AAPL.US", "NVDA.US", "MSFT.US", "GOOGL.US"] # Configurable list
# Pressure ratio window: number of snapshots to average
PRESSURE_WINDOW = 10
# Thresholds for alerting
SPREAD_EXPLOSION_THRESHOLD = 0.05 # dollars
PRESSURE_INVERSION_THRESHOLD = 0.20 # ratio < this = alert
ALERT_COOLDOWN_SECONDS = 5 # Prevent alert spam
# Webhook for alerts (Slack, Discord, or custom endpoint)
WEBHOOK_URL = os.environ.get("ALERT_WEBHOOK_URL")
# ─── Logging Setup ───────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger("depth_monitor")
# ─── Alert Manager ───────────────────────────────────────────────────────────
class AlertManager:
"""Manages alert dispatch with cooldown to prevent flooding."""
def __init__(self, webhook_url: Optional[str] = None):
self.webhook_url = webhook_url
self.last_alert_time = {} # symbol -> timestamp
def should_alert(self, symbol: str) -> bool:
now = time.time()
last = self.last_alert_time.get(symbol, 0)
if now - last < ALERT_COOLDOWN_SECONDS:
return False
self.last_alert_time[symbol] = now
return True
def send(self, symbol: str, alert_type: str, message: str):
if not self.should_alert(symbol):
logger.debug(f"Alert suppressed for {symbol} (cooldown active)")
return
payload = {
"symbol": symbol,
"alert_type": alert_type,
"message": message,
"timestamp": datetime.utcnow().isoformat()
}
logger.warning(f"ALERT [{symbol}] {alert_type}: {message}")
if self.webhook_url:
self._dispatch_webhook(payload)
def _dispatch_webhook(self, payload: dict):
"""Sends alert to webhook endpoint with timeout protection."""
try:
import requests
response = requests.post(
self.webhook_url,
json=payload,
timeout=(3.05, 10)
)
response.raise_for_status()
logger.info(f"Webhook dispatched successfully: {response.status_code}")
except Exception as e:
logger.error(f"Webhook dispatch failed: {e}")
# ─── Pressure Ratio Engine ────────────────────────────────────────────────────
class PressureRatioEngine:
"""
Computes rolling buy/sell pressure ratio from depth snapshots.
Buy pressure = cumulative bid size at top N levels
Sell pressure = cumulative ask size at top N levels
Pressure ratio = buy_pressure / sell_pressure
Ratio > 1.0: buying pressure dominant
Ratio < 1.0: selling pressure dominant
Ratio < 0.20: extreme selling pressure (liquidity vacuum)
"""
def __init__(self, window_size: int = PRESSURE_WINDOW):
self.window_size = window_size
self.bid_sizes = deque(maxlen=window_size)
self.ask_sizes = deque(maxlen=window_size)
def update(self, bid_size: int, ask_size: int):
self.bid_sizes.append(bid_size)
self.ask_sizes.append(ask_size)
@property
def pressure_ratio(self) -> Optional[float]:
if not self.bid_sizes or not self.ask_sizes:
return None
avg_bid = sum(self.bid_sizes) / len(self.bid_sizes)
avg_ask = sum(self.ask_sizes) / len(self.ask_sizes)
if avg_ask == 0:
return float('inf')
return avg_bid / avg_ask
def reset(self):
self.bid_sizes.clear()
self.ask_sizes.clear()
# ─── Depth Monitor ────────────────────────────────────────────────────────────
class DepthMonitor:
"""
Manages WebSocket connection to TickDB depth channel with:
- Heartbeat (ping/pong)
- Exponential backoff + jitter reconnection
- Rate-limit handling
- Per-symbol pressure ratio tracking
- Alert dispatching
"""
RECONNECT_BASE_DELAY = 1.0
RECONNECT_MAX_DELAY = 32.0
HEARTBEAT_INTERVAL = 20.0
MAX_RETRY_ATTEMPTS = 10
def __init__(
self,
symbols: list[str],
api_key: str,
alert_manager: AlertManager
):
self.symbols = symbols
self.api_key = api_key
self.alert_manager = alert_manager
self.pressure_engines = {sym: PressureRatioEngine() for sym in symbols}
self.running = False
self.ws = None
self.retry_count = 0
self.last_heartbeat = 0
self.heartbeat_thread = None
def _build_ws_url(self) -> str:
"""WebSocket auth uses query parameter, not header."""
return f"{TICKDB_WS_HOST}?api_key={self.api_key}"
def _subscribe(self):
"""Send subscription message for depth channel on all symbols."""
subscribe_msg = {
"cmd": "subscribe",
"channel": "depth",
"symbols": self.symbols
}
self.ws.send(json.dumps(subscribe_msg))
logger.info(f"Subscribed to depth channel: {self.symbols}")
def _handle_message(self, raw_message: str):
"""Parse and process incoming depth snapshot."""
try:
msg = json.loads(raw_message)
except json.JSONDecodeError:
logger.warning(f"Non-JSON message received: {raw_message[:100]}")
return
# Handle pong response (heartbeat acknowledgment)
if msg.get("cmd") == "pong":
logger.debug("Heartbeat acknowledged")
return
# Handle rate limit response
code = msg.get("code")
if code == 3001:
retry_after = int(msg.get("retry_after", 5))
logger.warning(f"Rate limited. Retrying after {retry_after} seconds")
time.sleep(retry_after)
return
# Parse depth snapshot
data = msg.get("data", {})
symbol = data.get("symbol")
if symbol not in self.pressure_engines:
return
bid_price = float(data.get("bid_price", 0))
bid_size = int(data.get("bid_size", 0))
ask_price = float(data.get("ask_price", 0))
ask_size = int(data.get("ask_size", 0))
spread = round(ask_price - bid_price, 4)
timestamp = data.get("ts", "N/A")
# Update pressure ratio engine
engine = self.pressure_engines[symbol]
engine.update(bid_size, ask_size)
pressure_ratio = engine.pressure_ratio
# Log current state
logger.info(
f"[{symbol}] bid={bid_size:>6} ask={ask_size:>6} "
f"spread=${spread:.4f} ratio={pressure_ratio:.3f}" if pressure_ratio else f"[{symbol}] "
f"bid={bid_size:>6} ask={ask_size:>6} spread=${spread:.4f}"
)
# Detect anomalies
if spread >= SPREAD_EXPLOSION_THRESHOLD:
self.alert_manager.send(
symbol,
"SPREAD_EXPLOSION",
f"Spread widened to ${spread:.4f} (threshold: ${SPREAD_EXPLOSION_THRESHOLD:.4f})"
)
if pressure_ratio is not None and pressure_ratio <= PRESSURE_INVERSION_THRESHOLD:
self.alert_manager.send(
symbol,
"PRESSURE_INVERSION",
f"Buy/sell pressure ratio collapsed to {pressure_ratio:.3f} "
f"(threshold: {PRESSURE_INVERSION_THRESHOLD:.3f}) — liquidity vacuum detected"
)
def _heartbeat_loop(self):
"""Send ping every HEARTBEAT_INTERVAL seconds."""
while self.running:
time.sleep(self.HEARTBEAT_INTERVAL)
if self.ws and self.ws.connected:
try:
self.ws.send(json.dumps({"cmd": "ping"}))
logger.debug("Heartbeat ping sent")
except Exception as e:
logger.error(f"Heartbeat failed: {e}")
def _reconnect_with_backoff(self):
"""Reconnect with exponential backoff and jitter."""
delay = min(
self.RECONNECT_BASE_DELAY * (2 ** self.retry_count),
self.RECONNECT_MAX_DELAY
)
jitter = random.uniform(0, delay * 0.1)
wait_time = delay + jitter
logger.info(f"Reconnecting in {wait_time:.2f} seconds (attempt {self.retry_count + 1})")
time.sleep(wait_time)
self.retry_count += 1
def _on_message(self, ws, message):
self._handle_message(message)
def _on_error(self, ws, error):
logger.error(f"WebSocket error: {error}")
def _on_close(self, ws, close_status_code, close_msg):
logger.warning(f"Connection closed: {close_status_code} — {close_msg}")
if self.running:
self._reconnect_with_backoff()
def _on_open(self, ws):
logger.info("WebSocket connection established")
self.retry_count = 0
self._subscribe()
def start(self):
"""Start the depth monitor. Blocking call."""
self.running = True
# Start heartbeat thread
self.heartbeat_thread = threading.Thread(target=self._heartbeat_loop, daemon=True)
self.heartbeat_thread.start()
while self.retry_count < self.MAX_RETRY_ATTEMPTS:
try:
ws_url = self._build_ws_url()
self.ws = websocket.WebSocketApp(
ws_url,
on_message=self._on_message,
on_error=self._on_error,
on_close=self._on_close,
on_open=self._on_open
)
self.ws.run_forever(ping_interval=30, ping_timeout=10)
except Exception as e:
logger.error(f"Connection exception: {e}")
if self.running:
self._reconnect_with_backoff()
logger.error("Max retry attempts reached. Monitor stopped.")
def stop(self):
self.running = False
if self.ws:
self.ws.close()
logger.info("Depth monitor stopped.")
# ─── Entry Point ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
logger.info("Starting TickDB Depth Monitor for earnings event tracking")
logger.info(f"Monitoring symbols: {SYMBOLS}")
logger.info(f"Spread explosion threshold: ${SPREAD_EXPLOSION_THRESHOLD:.4f}")
logger.info(f"Pressure inversion threshold: {PRESSURE_INVERSION_THRESHOLD:.3f}")
alert_manager = AlertManager(webhook_url=WEBHOOK_URL)
monitor = DepthMonitor(
symbols=SYMBOLS,
api_key=TICKDB_API_KEY,
alert_manager=alert_manager
)
try:
monitor.start()
except KeyboardInterrupt:
logger.info("Keyboard interrupt received")
monitor.stop()
How to Use This System
- Install dependencies:
pip install websocket-client requests - Set environment variables:
export TICKDB_API_KEY="your_api_key_here" export ALERT_WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL" - Configure symbols: Edit the
SYMBOLSlist to match your target names. - Run during earnings season: Start the monitor 15 minutes before the first earnings release you want to track.
- Interpret alerts:
SPREAD_EXPLOSION: The bid-ask spread has widened beyond your threshold — liquidity is deteriorating.PRESSURE_INVERSION: The pressure ratio has dropped below your threshold — buy-side depth has collapsed relative to sell-side, indicating a liquidity vacuum on the bid side.
Deploying the Depth Monitor: Architecture Overview
For a complete event-driven earnings strategy, the depth monitor is one component of a three-layer architecture:
┌─────────────────────────────────────────────────────────────┐
│ Layer 1: Data Ingestion │
│ TickDB Depth Monitor (WebSocket) │
│ → Subscribes to depth channel for target symbols │
│ → Computes pressure ratio per snapshot │
│ → Dispatches alerts on threshold breach │
└────────────────────────┬────────────────────────────────────┘
│ Webhook / Queue
┌────────────────────────▼────────────────────────────────────┐
│ Layer 2: Event Processor │
│ Alert Handler Service │
│ → Receives SPREAD_EXPLOSION / PRESSURE_INVERSION alerts │
│ → Checks earnings calendar (pre-configured release times) │
│ → Routes to strategy engine only during event windows │
└────────────────────────┬────────────────────────────────────┘
│ Order signal
┌────────────────────────▼────────────────────────────────────┐
│ Layer 3: Strategy Engine │
│ Position Manager │
│ → Evaluates directional bias based on alert type + symbol │
│ → Computes position size (risk-managed) │
│ → Submits orders via brokerage API │
│ → Manages exit rules (timeout, P&L threshold) │
└─────────────────────────────────────────────────────────────┘
Deployment recommendations by user segment:
| User segment | Recommended deployment | Rationale |
|---|---|---|
| Individual quant | Single-process Python on a VPS in NY4 | Lowest cost; sufficient for monitoring 5–10 symbols |
| Small team (2–5) | Containerized monitor + Redis queue + separate strategy service | Decouples ingestion from execution; easier to scale |
| Institutional | Kubernetes deployment with autoscaling, dedicated OMS, and latency-optimized network routing | Supports 50+ symbols simultaneously; compliance-grade logging |
Complementary Data: Adding kline Context for Pre-Event Baseline
The depth channel tells you what is happening during the event. But a complete event-driven system needs to establish a baseline before the event occurs. This is where the TickDB /v1/market/kline endpoint provides essential context.
Two queries that complement the depth monitor:
1. Historical volatility baseline — Establish the typical spread and pressure ratio for the symbol over the past 20 trading days. This gives you a calibrated threshold rather than a fixed one.
2. Implied volatility surface — Options data (if accessible) tells you where the market pricing uncertainty ahead of the event. A high IV rank suggests a wider spread explosion is likely, and your alert thresholds should be tightened.
import requests
import os
TICKDB_API_KEY = os.environ.get("TICKDB_API_KEY")
HEADERS = {"X-API-Key": TICKDB_API_KEY}
def fetch_baseline_klines(symbol: str, interval: str = "5m", limit: int = 500):
"""
Fetch historical klines to compute baseline spread and depth metrics.
Use interval='5m' for intraday analysis; '1h' for multi-day baseline.
"""
response = requests.get(
"https://api.tickdb.ai/v1/market/kline",
headers=HEADERS,
params={
"symbol": symbol,
"interval": interval,
"limit": limit
},
timeout=(3.05, 10)
)
response.raise_for_status()
data = response.json()
if data.get("code") != 0:
raise RuntimeError(f"API error {data.get('code')}: {data.get('message')}")
klines = data["data"]
# Compute average high-low spread as percentage
avg_range = sum((k["high"] - k["low"]) / k["close"] for k in klines) / len(klines)
avg_volume = sum(k["volume"] for k in klines) / len(klines)
return {
"symbol": symbol,
"avg_range_pct": round(avg_range * 100, 4),
"avg_volume": round(avg_volume, 2),
"sample_period": f"{len(klines)} {interval} bars"
}
# Example: Fetch AAPL baseline before earnings season
if __name__ == "__main__":
baseline = fetch_baseline_klines("AAPL.US", interval="5m", limit=500)
print(f"AAPL baseline: {baseline['avg_range_pct']}% avg range, "
f"{baseline['avg_volume']:,.0f} avg volume over {baseline['sample_period']}")
Supply Chain & Event Calendar: Earnings Season Targets
For quant systems, the "supply chain" is the earnings calendar. The following table represents a sample of high-impact earnings releases that historically generate the most significant liquidity dislocations. Systems should be initialized and monitoring at least 15 minutes before each event.
| Company | Ticker | Sector | Typical Post-Release Volatility | Liquidity Vacuum Duration |
|---|---|---|---|---|
| NVIDIA | NVDA.US | Semiconductors | Extremely high | 5–10 seconds |
| Apple | AAPL.US | Consumer tech | High | 3–6 seconds |
| Microsoft | MSFT.US | Enterprise software | High | 3–5 seconds |
| Amazon | AMZN.US | E-commerce / cloud | Very high | 4–8 seconds |
| Tesla | TSLA.US | EV / energy | Extreme | 8–15 seconds |
| Meta | META.US | Social media | Very high | 4–7 seconds |
| Alphabet | GOOGL.US | Search / cloud | Moderate–high | 3–5 seconds |
Note: Volatility and vacuum duration estimates are based on historical observations across multiple earnings cycles. Actual values vary by quarter, market regime, and macro conditions.
Key Takeaways
The five seconds following an earnings release are not random. They follow a characteristic sequence: market maker withdrawal, spread explosion, pressure ratio collapse, and gradual re-accumulation. This sequence is reproducible and detectable with the right data infrastructure.
What you need to capture it:
- WebSocket-based depth streaming (not polling) to avoid missing the T+0.2 to T+1 sec window
- A rolling pressure ratio engine to detect bid-side withdrawal before price fully adjusts
- Calibrated spread thresholds derived from historical baseline data
- A webhook alerting system that can trigger downstream strategy logic within 100ms of detection
What this article provided:
- A quantified model of order book behavior during earnings events
- Production-grade Python code with heartbeat, reconnection with backoff and jitter, rate-limit handling, and environment-variable-based authentication
- A deployment architecture for individual, team, and institutional use cases
- Complementary data queries using the TickDB
/v1/market/klineendpoint to establish pre-event baselines
Next Steps
If you want to run this monitoring system yourself:
- Sign up at tickdb.ai and generate an API key (free tier available, no credit card required)
- Set the
TICKDB_API_KEYenvironment variable - Copy the depth monitor code from this article and run it 15 minutes before your next earnings trade
If you need 10+ years of historical OHLCV data to backtest your event-driven strategy across multiple earnings cycles:
Visit tickdb.ai for Professional and Enterprise plans with full historical data access.
If you use AI coding assistants:
Search for and install the tickdb-market-data SKILL in your AI tool's marketplace for direct API integration within your development workflow.
If you are building a full strategy stack:
Reach out to enterprise@tickdb.ai for custom data feeds, dedicated support, and latency-optimized infrastructure for institutional-grade deployment.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. Any strategy involving real-time market data and automated execution carries substantial risk of financial loss. Backtest results presented in this article are based on reconstructed historical observations and do not represent live trading performance. Always conduct thorough out-of-sample validation before deploying any strategy with real capital.