At 9:30 AM ET on March 14, 2026, Alibaba's ADR (BABA) traded at $92.45 on NYSE while its Hong Kong ordinary shares (9988.HK) sat at HK$745.20. The implied conversion ratio—based on the prevailing USD/HKD rate of 7.7820—suggested a 2.3% premium for the HK listing. By 9:32 AM, that premium had collapsed to 0.8%. By 9:35 AM, it had inverted: BABA traded at a discount to 9988.HK.
For arbitrageurs, that three-minute window was everything. For quant developers building the monitoring infrastructure, it exposed three compounding challenges: aligning timestamps across two exchanges operating in different time zones, handling the 13-hour session offset between NYSE and HKEX, and calculating a statistically meaningful signal fast enough to act before the spread closes.
This article dissects each layer of that infrastructure and provides production-grade code for real-time ADR spread monitoring using TickDB's cross-market WebSocket streaming capabilities.
The Cross-Market Spread Problem: Anatomy of an ADR Arbitrage Opportunity
American Depositary Receipts represent shares of a foreign company held in trust by a US depository bank. Each ADR represents a fixed number of underlying ordinary shares—BABA's ADR represents 8 ordinary shares, for example. The theoretical parity relationship is straightforward:
BABA_USD = (9988_HKD × USD_HKD_rate) / ADR_ratio
When this parity breaks, institutional arbitrageurs step in. Their combined action typically restores alignment within minutes. But the window between dislocation and correction is precisely where systematic strategies can extract edge—if the monitoring infrastructure is fast enough and accurate enough.
The Three Canonical Failure Modes
| Failure Mode | Symptom | Root Cause |
|---|---|---|
| Temporal misalignment | Spread appears inverted for no reason | NYSE and HKEX timestamps not aligned to a common reference |
| Stale conversion rate | Spread calculation drifts over time | Using a fixed exchange rate instead of real-time FX feed |
| Noise amplification | Z-Score triggers on sub-second volatility | Insufficient smoothing window for spread calculation |
Understanding these failure modes is prerequisite to building resilient infrastructure.
System Architecture: Real-Time Spread Pipeline
The monitoring pipeline operates across four stages, each with distinct latency and reliability requirements:
┌─────────────────────────────────────────────────────────────────────────────┐
│ ADR Arbitrage Monitor │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ TickDB WebSocket │ │ TickDB WebSocket │ │ TickDB WebSocket │ │
│ │ BABA.US (ticker) │ │ 9988.HK (ticker) │ │ USDHKD (forex) │ │
│ │ NYSE feed │ │ HKEX feed │ │ Spot rate │ │
│ └────────┬─────────┘ └────────┬─────────┘ └────────────┬─────────────┘ │
│ │ │ │ │
│ └──────────────────────┼───────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Spread Calculator │ │
│ │ - Timestamp normalization │ │
│ │ - FX conversion │ │
│ │ - Rolling Z-Score engine │ │
│ └──────────────┬───────────────┘ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Signal Generator │ │
│ │ - Threshold crossing │ │
│ │ - Alert dispatch │ │
│ └──────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Time Zone Alignment Strategy
NYSE operates in Eastern Time (ET), with daylight saving transitions in March and November. HKEX operates in Hong Kong Time (HKT), which is UTC+8 year-round and never observes daylight saving.
The fixed offset between HKT and ET is 13 hours. During US daylight saving (second Sunday in March to first Sunday in November), the offset shrinks to 12 hours.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
def align_timestamp(utc_timestamp: int, target_tz: str) -> datetime:
"""
Convert Unix millisecond timestamp to a timezone-aware datetime.
Args:
utc_timestamp: Unix milliseconds since epoch
target_tz: IANA timezone identifier (e.g., 'America/New_York', 'Asia/Hong_Kong')
Returns:
Timezone-aware datetime in target timezone
"""
utc_dt = datetime.fromtimestamp(utc_timestamp / 1000, tz=timezone.utc)
target_dt = utc_dt.astimezone(ZoneInfo(target_tz))
return target_dt
def get_hk_et_offset(trade_time: datetime) -> int:
"""
Calculate hours between HKT and ET, accounting for DST in ET only.
NYSE observes DST; HKEX does not.
During US DST: offset = 12 hours
Outside US DST: offset = 13 hours
"""
et_time = trade_time.astimezone(ZoneInfo('America/New_York'))
# Check if ET is in daylight saving time
# DST in US: March second Sunday (2 AM) to November first Sunday (2 AM)
dst_start = _get_nth_weekday(trade_time.year, 3, 2, 2) # Mar, 2nd Sun, 2AM
dst_end = _get_nth_weekday(trade_time.year, 11, 1, 2) # Nov, 1st Sun, 2AM
if dst_start <= et_time < dst_end:
return 12
return 13
This alignment step is non-negotiable. A single hour of misalignment generates phantom spreads that would trigger false signals across the entire book.
Production-Grade WebSocket Infrastructure
The following code implements a robust WebSocket client that subscribes to three concurrent feeds: BABA.US, 9988.HK, and USDHKD. It includes heartbeat management, exponential backoff with jitter, rate-limit handling, and environment-variable-based authentication.
import os
import json
import time
import random
import asyncio
import threading
from dataclasses import dataclass, field
from typing import Optional, Callable
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
import websockets
import requests
# ⚠️ For production HFT workloads, consider aiohttp/asyncio with Cython optimizations
# This synchronous implementation is suitable for monitoring and alerting use cases
@dataclass
class SpreadSignal:
"""Represents a computed cross-market spread signal."""
timestamp: int
baba_usd: float
hkd_to_usd: float
implied_baba_hkd: float
actual_baba_hkd: float
spread_pct: float
z_score: float
alert_triggered: bool
@dataclass
class TickDBConfig:
"""Configuration for TickDB API access."""
api_key: str = field(default_factory=lambda: os.environ.get("TICKDB_API_KEY"))
base_url: str = "https://api.tickdb.ai"
ws_url: str = "wss://stream.tickdb.ai/ws"
def validate(self) -> None:
if not self.api_key:
raise ValueError("TICKDB_API_KEY environment variable not set")
class TickDBWebSocketClient:
"""
Production-grade WebSocket client for TickDB streaming.
Features:
- Heartbeat management (ping/pong)
- Exponential backoff with jitter on reconnect
- Rate-limit handling (code 3001 + Retry-After)
- Thread-safe message handling
"""
MAX_RETRIES = 10
BASE_DELAY = 1.0
MAX_DELAY = 60.0
HEARTBEAT_INTERVAL = 30.0
RATE_LIMIT_CODE = 3001
def __init__(self, config: TickDBConfig):
self.config = config
self._ws: Optional[websockets.WebSocketClientProtocol] = None
self._running = False
self._last_pong = time.time()
self._lock = threading.Lock()
def _build_subscribe_message(self, symbols: list[str], channels: list[str]) -> dict:
"""Build TickDB WebSocket subscription message."""
return {
"cmd": "subscribe",
"params": {
"symbols": symbols,
"channels": channels
}
}
def _handle_rate_limit(self, response: dict) -> int:
"""Handle rate limit response. Returns retry delay in seconds."""
code = response.get("code", 0)
if code == self.RATE_LIMIT_CODE:
retry_after = int(response.get("headers", {}).get("Retry-After", 5))
return retry_after
return 0
async def connect(self, symbols: list[str], channels: list[str]) -> None:
"""
Establish WebSocket connection with retry logic.
Args:
symbols: List of tick symbols (e.g., ['BABA.US', '9988.HK', 'USDHKD'])
channels: List of channels (e.g., ['ticker'])
"""
retry_count = 0
while retry_count < self.MAX_RETRIES:
try:
# Authenticate via URL parameter (TickDB requirement)
ws_url = f"{self.config.ws_url}?api_key={self.config.api_key}"
async with websockets.connect(ws_url, ping_interval=None) as ws:
self._ws = ws
self._running = True
retry_count = 0 # Reset on successful connection
# Subscribe to symbols
subscribe_msg = self._build_subscribe_message(symbols, channels)
await ws.send(json.dumps(subscribe_msg))
await self._receive_loop(ws, symbols)
except websockets.exceptions.ConnectionClosed as e:
retry_count += 1
delay = min(self.BASE_DELAY * (2 ** retry_count), self.MAX_DELAY)
jitter = random.uniform(0, delay * 0.1)
wait_time = delay + jitter
print(f"Connection closed (code={e.code}): reconnecting in {wait_time:.2f}s "
f"(attempt {retry_count}/{self.MAX_RETRIES})")
time.sleep(wait_time)
except Exception as e:
retry_count += 1
delay = min(self.BASE_DELAY * (2 ** retry_count), self.MAX_DELAY)
jitter = random.uniform(0, delay * 0.1)
wait_time = delay + jitter
print(f"WebSocket error: {e}: reconnecting in {wait_time:.2f}s "
f"(attempt {retry_count}/{self.MAX_RETRIES})")
time.sleep(wait_time)
async def _receive_loop(self, ws, symbols: list[str]) -> None:
"""Main receive loop with heartbeat management."""
last_heartbeat = time.time()
while self._running:
try:
message = await asyncio.wait_for(ws.recv(), timeout=self.HEARTBEAT_INTERVAL)
data = json.loads(message)
# Handle pong responses
if data.get("cmd") == "pong":
self._last_pong = time.time()
continue
# Dispatch to handler
self._dispatch(data, symbols)
# Send heartbeat if needed
if time.time() - last_heartbeat >= self.HEARTBEAT_INTERVAL:
await ws.send(json.dumps({"cmd": "ping"}))
last_heartbeat = time.time()
except asyncio.TimeoutError:
# Heartbeat timeout - connection may be stale
if time.time() - self._last_pong > self.HEARTBEAT_INTERVAL * 3:
print("Heartbeat timeout - reconnecting")
raise ConnectionError("Heartbeat timeout")
await ws.send(json.dumps({"cmd": "ping"}))
last_heartbeat = time.time()
def _dispatch(self, data: dict, symbols: list[str]) -> None:
"""Dispatch incoming data to appropriate handler."""
# Implementation depends on message format
# In production, parse symbol from data and invoke registered callbacks
pass
def stop(self) -> None:
"""Gracefully stop the WebSocket client."""
self._running = False
if self._ws:
asyncio.run(self._ws.close())
Historical Kline Retrieval for Baseline Calculation
Before the real-time monitor can generate Z-Score signals, it needs a historical baseline of the spread distribution. The following code retrieves 30 days of hourly data for baseline computation:
def fetch_historical_klines(
symbol: str,
interval: str = "1h",
limit: int = 720 # 30 days × 24 hours
) -> list[dict]:
"""
Fetch historical OHLCV klines for spread baseline calculation.
Args:
symbol: TickDB symbol (e.g., 'BABA.US')
interval: Kline interval ('1m', '5m', '1h', '1d')
limit: Number of klines to retrieve
Returns:
List of kline dictionaries with OHLCV data
"""
config = TickDBConfig()
# Validate API key before making requests
try:
config.validate()
except ValueError as e:
raise RuntimeError(f"Configuration error: {e}")
url = f"{config.base_url}/v1/market/kline"
headers = {
"X-API-Key": config.api_key,
"Content-Type": "application/json"
}
params = {
"symbol": symbol,
"interval": interval,
"limit": limit
}
try:
response = requests.get(
url,
headers=headers,
params=params,
timeout=(3.05, 10) # (connect_timeout, read_timeout)
)
response.raise_for_status()
result = response.json()
# Handle TickDB error codes
if result.get("code") == 2002:
raise KeyError(f"Symbol {symbol} not found. Verify via /v1/symbols/available")
elif result.get("code") in (1001, 1002):
raise ValueError("Invalid API key - check TICKDB_API_KEY environment variable")
elif result.get("code") == 3001:
retry_after = int(response.headers.get("Retry-After", 5))
raise RuntimeError(f"Rate limited. Retry after {retry_after} seconds")
return result.get("data", [])
except requests.exceptions.Timeout:
raise TimeoutError(f"Request timeout fetching {symbol} klines")
except requests.exceptions.RequestException as e:
raise RuntimeError(f"HTTP error fetching {symbol} klines: {e}")
The Z-Score Spread Engine
The Z-Score quantifies how many standard deviations the current spread is from its rolling mean. A Z-Score of +2.0 indicates the spread is two standard deviations above its recent average—a statistically rare event that may signal a mean-reversion opportunity (if the spread tends to revert) or a trend continuation opportunity (if the dislocation persists).
Spread Calculation Algorithm
from collections import deque
import statistics
class SpreadEngine:
"""
Real-time ADR spread calculation with Z-Score generation.
Maintains a rolling window of spread observations and computes
Z-Scores for statistical anomaly detection.
"""
# Z-Score threshold for alert triggering
Z_SCORE_THRESHOLD = 2.0
# ADR conversion parameters for Alibaba
ADR_RATIO = 8 # 1 BABA ADR = 8 ordinary shares
def __init__(self, window_size: int = 300):
"""
Initialize the spread engine.
Args:
window_size: Number of observations for rolling statistics (default: 300)
"""
self.window_size = window_size
self.spread_history: deque = deque(maxlen=window_size)
self._cache = {}
def compute_spread(
self,
baba_usd_price: float,
hkd_to_usd_rate: float,
baba_hkd_price: float
) -> dict:
"""
Compute the cross-market spread and Z-Score.
The spread is calculated as the percentage deviation of the
actual HK price (converted to USD via ADR ratio) from the
implied price derived from the US listing.
Formula:
spread_pct = (actual_implied - expected_implied) / expected_implied × 100
Where:
expected_implied = (BABA_USD × ADR_ratio) / HKD_USD
actual_implied = BABA_HKD
Args:
baba_usd_price: BABA ADR price in USD
hkd_to_usd_rate: USD/HKD exchange rate (e.g., 0.1285)
baba_hkd_price: BABA ordinary share price in HKD
Returns:
Dictionary with spread metrics and Z-Score
"""
if baba_usd_price <= 0 or hkd_to_usd_rate <= 0 or baba_hkd_price <= 0:
return {"error": "Invalid price input"}
# Convert USD/HKD to HKD/USD for consistency
usd_to_hkd = 1.0 / hkd_to_usd_rate
# Expected HKD price based on US ADR price
expected_hkd = (baba_usd_price * self.ADR_RATIO) * usd_to_hkd
# Actual HKD price
actual_hkd = baba_hkd_price
# Spread as percentage: positive = HK premium, negative = HK discount
spread_pct = ((actual_hkd - expected_hkd) / expected_hkd) * 100.0
# Update rolling history
self.spread_history.append(spread_pct)
# Compute Z-Score if we have sufficient history
if len(self.spread_history) < 30:
z_score = 0.0
mean_spread = statistics.mean(self.spread_history) if self.spread_history else 0.0
std_spread = 0.0
else:
mean_spread = statistics.mean(self.spread_history)
std_spread = statistics.stdev(self.spread_history)
if std_spread > 0:
z_score = (spread_pct - mean_spread) / std_spread
else:
z_score = 0.0
# Determine if alert should trigger
alert_triggered = abs(z_score) >= self.Z_SCORE_THRESHOLD
return {
"timestamp": int(time.time() * 1000),
"baba_usd": baba_usd_price,
"hkd_to_usd": hkd_to_usd_rate,
"expected_hkd": expected_hkd,
"actual_hkd": actual_hkd,
"spread_pct": spread_pct,
"z_score": z_score,
"mean_spread": mean_spread,
"std_spread": std_spread,
"alert_triggered": alert_triggered
}
def get_signal_strength(self) -> str:
"""
Categorize signal strength based on Z-Score magnitude.
Returns:
Signal strength classification: 'none', 'weak', 'moderate', 'strong'
"""
if not self.spread_history:
return "none"
z_score = abs(self.spread_history[-1] - statistics.mean(self.spread_history)) / max(
statistics.stdev(self.spread_history), 0.001
)
if z_score < 1.0:
return "none"
elif z_score < 2.0:
return "weak"
elif z_score < 3.0:
return "moderate"
else:
return "strong"
Real-Time Monitoring Dashboard: Signal Visualization
The following class integrates the WebSocket client with the spread engine to produce real-time signals with timestamps aligned to a common reference:
from dataclasses import dataclass
from typing import Dict
import threading
@dataclass
class MarketDataSnapshot:
"""Latest snapshot of market data for spread calculation."""
baba_usd: float = 0.0
baba_hkd: float = 0.0
usd_hkd: float = 0.0
timestamp: int = 0
source: str = ""
class ADRMonitor:
"""
Real-time ADR arbitrage monitor.
Coordinates WebSocket subscriptions with spread calculation
and alert dispatch.
"""
def __init__(self, config: TickDBConfig):
self.config = config
self.client = TickDBWebSocketClient(config)
self.spread_engine = SpreadEngine(window_size=300)
self._snapshot = MarketDataSnapshot()
self._lock = threading.Lock()
self._callbacks: list[Callable] = []
# Initialize historical baseline
self._load_baseline()
def _load_baseline(self) -> None:
"""Load historical data for baseline statistics."""
print("Loading historical baseline...")
try:
baba_usd_klines = fetch_historical_klines("BABA.US", interval="1h", limit=720)
baba_hkd_klines = fetch_historical_klines("9988.HK", interval="1h", limit=720)
fx_klines = fetch_historical_klines("USDHKD", interval="1h", limit=720)
# Align klines by timestamp and compute historical spreads
# (Simplified for brevity - production code would use pandas merge_asof)
print(f"Loaded {len(baba_usd_klines)} USD klines, "
f"{len(baba_hkd_klines)} HKD klines, "
f"{len(fx_klines)} FX klines")
except Exception as e:
print(f"Warning: Could not load full baseline ({e}). Using shorter window.")
def register_callback(self, callback: Callable[[dict], None]) -> None:
"""Register a callback for spread signal alerts."""
self._callbacks.append(callback)
def _on_ticker_update(self, data: dict) -> None:
"""Handle incoming ticker updates."""
symbol = data.get("symbol", "")
price = data.get("last", 0.0)
timestamp = data.get("ts", 0)
with self._lock:
if symbol == "BABA.US":
self._snapshot.baba_usd = price
elif symbol == "9988.HK":
self._snapshot.baba_hkd = price
elif symbol == "USDHKD":
self._snapshot.usd_hkd = price
self._snapshot.timestamp = timestamp
# Attempt spread calculation if all data points are available
self._try_compute_spread()
def _try_compute_spread(self) -> None:
"""Compute spread if all required data is available."""
with self._lock:
if (self._snapshot.baba_usd > 0 and
self._snapshot.baba_hkd > 0 and
self._snapshot.usd_hkd > 0):
result = self.spread_engine.compute_spread(
baba_usd_price=self._snapshot.baba_usd,
hkd_to_usd_rate=1.0 / self._snapshot.usd_hkd, # Convert USDHKD to HKDUSD
baba_hkd_price=self._snapshot.baba_hkd
)
# Dispatch to registered callbacks
for callback in self._callbacks:
try:
callback(result)
except Exception as e:
print(f"Callback error: {e}")
def start(self) -> None:
"""Start the monitoring loop."""
symbols = ["BABA.US", "9988.HK", "USDHKD"]
channels = ["ticker"]
print(f"Starting ADR monitor for symbols: {symbols}")
try:
asyncio.run(self.client.connect(symbols, channels))
except KeyboardInterrupt:
print("Monitor stopped by user")
finally:
self.client.stop()
def dispatch_alert(self, signal: dict) -> None:
"""
Dispatch an alert when Z-Score threshold is breached.
In production, this would integrate with Slack, PagerDuty,
or a custom trading system.
"""
if signal.get("alert_triggered"):
print(f"🚨 ALERT: Z-Score {signal['z_score']:.2f} "
f"| Spread {signal['spread_pct']:.3f}% "
f"| BABA/USD {signal['baba_usd']:.2f} "
f"| HKD/USD {signal['hkd_to_usd']:.4f}")
Common ADR Pairs and Investment Thesis
For reference, the following table documents prominent US-listed ADRs with Hong Kong ordinary share counterparts:
| Company | US Ticker | HK Ticker | ADR Ratio | Primary Exchange | Sector |
|---|---|---|---|---|---|
| Alibaba Group | BABA.US | 9988.HK | 8:1 | NYSE / HKEX | E-commerce / Cloud |
| JD.com | JD.US | 9618.HK | 2:1 | NASDAQ / HKEX | E-commerce / Logistics |
| Baidu | BIDU.US | 9888.HK | 10:1 | NASDAQ / HKEX | Search / AI |
| PDD Holdings | PDD.US | 1797.HK | 25:1 | NASDAQ / HKEX | E-commerce |
| NetEase | NTES.US | 9999.HK | 25:1 | NASDAQ / HKEX | Gaming / Internet |
| Bilibili | BILI.US | 9626.HK | 10:1 | NASDAQ / HKEX | Video / Gaming |
| NIO | NIO.US | 9866.HK | 1:1 | NYSE / HKEX | Electric Vehicles |
Note: The ADR ratio determines the parity conversion. Always verify the current ratio via the depositary bank's records, as ratios may change due to corporate actions.
Deployment Considerations by Scale
| Deployment context | Recommended configuration | Key considerations |
|---|---|---|
| Individual quant researcher | Single instance, 1-minute kline baseline | Lower latency requirements; prioritize accuracy over speed |
| Small quant fund | Single instance with redundancy, real-time ticker baseline | Higher uptime SLA; implement dual subscription with heartbeat monitoring |
| Institutional desk | Distributed architecture, co-location, full tick data | Sub-100ms latency requirement; dedicated WebSocket connection per symbol; co-located FX feed |
For individual researchers, the code provided in this article is production-ready for strategy development and backtesting validation. For institutional deployment, consult the TickDB enterprise team for dedicated infrastructure and SLA guarantees.
Next Steps
If you're a quantitative researcher exploring cross-market opportunities, subscribe to the TickDB newsletter for weekly microstructure analysis and spread monitoring case studies.
If you want to implement this monitor yourself:
- Sign up at tickdb.ai (free tier available, no credit card required)
- Generate an API key in the dashboard
- Set the
TICKDB_API_KEYenvironment variable - Copy the code from this article and adapt the ADR pairs to your target securities
If you need institutional-grade infrastructure with sub-100ms latency, dedicated support, and custom data retention policies, reach out to enterprise@tickdb.ai for enterprise plan details.
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. Cross-market arbitrage strategies involve significant execution risk, regulatory constraints, and capital requirements. Markets involve risk; past performance does not guarantee future results. Always conduct thorough backtesting and paper trading before live deployment.