"Price is the effect. The order book is the cause."

At 9:30 AM ET on a Tuesday, BABA opens on the NYSE at $85.20. Meanwhile, 13 hours earlier in Hong Kong, Alibaba had closed at HK$658.80. Converted to USD at the prevailing exchange rate of 7.78, the HK close implies a NYSE-equivalent price of $84.68. The spread: 52 cents, or 0.61%. For a quant running an ADR arbitrage book, this is where the question begins—not where it ends.

The real challenge is not spotting a $0.52 spread. The challenge is maintaining a synchronized view of both legs as the NYSE trades actively while the HKEX is dark, recalculating implied prices in real time as exchange rates fluctuate, and generating a Z-Score signal that accounts for the historical distribution of that specific pair's spread. This article walks through the complete architecture: data ingestion, time-zone alignment, currency conversion, spread normalization, and the production-grade WebSocket subscription layer that makes this run reliably.


The ADR Arbitrage Problem: Three Core Frictions

ADR (American Depositary Receipt) arbitrage exploits price discrepancies between a US-listed depositary receipt and its underlying Hong Kong ordinary share. The theoretical relationship is defined by the depositary ratio—typically 8 HK shares per ADR for Alibaba. When this relationship breaks down beyond transaction costs and execution risk, a mean-reversion opportunity exists.

The frictions that make this non-trivial fall into three categories.

Time-Zone Misalignment

The NYSE operates from 9:30 AM to 4:00 PM ET. The HKEX closes at 4:00 PM HKT, which is 3:00 AM ET during standard time. During the NYSE session, the Hong Kong leg is stale. The arbitrageur must choose between two modes:

  • Static-hedge mode: Hold a static HK position from the previous close, monitor the US leg intraday, and hedge when the spread exceeds a threshold.
  • Live-hedge mode: Use the HK close price plus real-time exchange rate movement as an implied reference, updating as FX fluctuates.

The choice depends on the acceptable tracking error and the cost of maintaining HKEX margin.

Currency Conversion Risk

The ADR is denominated in USD; the Hong Kong ordinary is in HKD. The conversion uses the USD/HKD rate, which is pegged within a narrow band (typically 7.75–7.85), but the peg is not perfectly fixed intraday. For pairs with significant overnight HKD moves, ignoring FX drift introduces a systematic bias.

Pair ADR Ratio Typical Spread (bps) FX Sensitivity (bps/% move)
BABA 8:1 40–120 0.8
JD 2:1 30–80 0.4
BIDU 10:1 50–150 1.2
PDD 4:1 60–200 0.9

Spread Normalization and Signal Generation

A raw spread in dollars is not comparable across pairs. BABA's $0.52 move means something different than BIDU's $0.52 move. Normalization requires computing the spread as a percentage of the US price, then converting to a Z-Score using the historical mean and standard deviation of that pair's spread.

This is where most retail implementations fail: they use a fixed threshold instead of an adaptive one. A static $0.50 threshold for BABA generates false signals when the pair's natural spread routinely moves ±$0.40. The Z-Score approach adapts to regime changes—high-volatility periods expand the threshold automatically.


Architecture Overview: Three-Layer Monitoring System

The system consists of three layers:

  1. Data Ingestion Layer: WebSocket subscriptions to US equity price feeds and HKD/USD exchange rates.
  2. Computation Layer: Real-time spread calculation, normalization, and Z-Score signal generation.
  3. Alert/Execution Layer: Threshold detection and webhook/Slack notification (extensible to order execution).
┌─────────────────────────────────────────────────────────┐
│                  ADR Arbitrage Monitor                  │
│                                                         │
│  ┌──────────────┐   ┌──────────────┐   ┌────────────┐ │
│  │  NYSE US Leg │   │  HKD/USD FX   │   │  HK Ref*   │ │
│  │  (TickDB)    │   │  (TickDB)     │   │  (Static)  │ │
│  └──────┬───────┘   └──────┬───────┘   └─────┬──────┘ │
│         │                  │                  │        │
│         ▼                  ▼                  ▼        │
│  ┌──────────────────────────────────────────────────┐ │
│  │           Spread Computation Engine              │ │
│  │  Implied HK Price = US_Price / ADR_Ratio         │ │
│  │  FX_Adjusted = HKD_Rate * (1 + HKD_Drift)       │ │
│  │  Spread = US_Price - Implied_HK_USD              │ │
│  │  Z_Score = (Spread - Mean) / StdDev               │ │
│  └──────────────────────────────────────────────────┘ │
│                          │                             │
│                          ▼                             │
│  ┌──────────────────────────────────────────────────┐ │
│  │           Signal Layer: Z-Score Threshold        │ │
│  │  |Z| > 2.0 → Alert (webhook / Slack)             │ │
│  │  |Z| > 3.0 → High-conviction signal               │ │
│  └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
* HK Reference price is the previous close during NYSE hours.

Production-Grade WebSocket Implementation

The following code implements a resilient ADR arbitrage monitor. It subscribes to a US equity price stream (via TickDB's WebSocket endpoint), retrieves the HK reference price from a stored configuration or a prior snapshot, and monitors the USD/HKD exchange rate for intraday drift.

The implementation includes every production-resilience element mandated by TickDB's code standards: heartbeat, exponential backoff with jitter, rate-limit handling, timeout enforcement, and environment-variable-based authentication.

import os
import json
import time
import asyncio
import logging
import statistics
from datetime import datetime, timezone
from collections import deque
from typing import Optional
from dataclasses import dataclass, field

import websockets
import requests

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s"
)
logger = logging.getLogger("adr_monitor")


# ─── Configuration ────────────────────────────────────────────────────────────

@dataclass
class ADRPair:
    """ADR pair configuration with depositary ratio and HK reference data."""
    adr_symbol: str          # US ticker, e.g. "BABA"
    hk_symbol: str           # HK ticker, e.g. "9988.HK"
    ratio: float             # HK shares per ADR
    hk_close_hkd: float     # Previous HK close price in HKD
    hk_close_timestamp: str # ISO timestamp of HK close
    fx_rate: float           # USD/HKD rate at HK close
    spread_history: deque = field(default_factory=lambda: deque(maxlen=60))


@dataclass
class MonitorConfig:
    """Global monitor configuration."""
    tickdb_api_key: str
    tickdb_ws_url: str = "wss://api.tickdb.ai/ws/market"
    tickdb_rest_url: str = "https://api.tickdb.ai/v1"
    zscore_window: int = 60  # Number of spread observations for Z-Score
    zscore_threshold: float = 2.0
    high_conviction_threshold: float = 3.0
    webhook_url: Optional[str] = None


# ─── ADR Pair Definitions ─────────────────────────────────────────────────────

ADR_PAIRS = {
    "BABA": ADRPair(
        adr_symbol="BABA.US",
        hk_symbol="9988.HK",
        ratio=8.0,
        hk_close_hkd=658.80,
        hk_close_timestamp="2026-04-14T16:00:00+08:00",
        fx_rate=7.7820
    ),
    "JD": ADRPair(
        adr_symbol="JD.US",
        hk_symbol="9618.HK",
        ratio=2.0,
        hk_close_hkd=162.50,
        hk_close_timestamp="2026-04-14T16:00:00+08:00",
        fx_rate=7.7820
    ),
    "BIDU": ADRPair(
        adr_symbol="BIDU.US",
        hk_symbol="9888.HK",
        ratio=10.0,
        hk_close_hkd=108.20,
        hk_close_timestamp="2026-04-14T16:00:00+08:00",
        fx_rate=7.7820
    ),
    "PDD": ADRPair(
        adr_symbol="PDD.US",
        hk_symbol="TEMU.HK",
        ratio=4.0,
        hk_close_hkd=145.60,
        hk_close_timestamp="2026-04-14T16:00:00+08:00",
        fx_rate=7.7820
    ),
}

# ─── Error Handling Utilities ─────────────────────────────────────────────────

def handle_api_error(response: requests.Response, context: str) -> dict:
    """
    Standard TickDB REST error handler.
    Raises ValueError for auth failures, KeyError for symbol issues,
    and RuntimeError for unexpected errors.
    """
    try:
        body = response.json()
    except ValueError:
        raise RuntimeError(f"{context}: Non-JSON response ({response.status_code})")

    code = body.get("code", 0)
    message = body.get("message", "Unknown error")

    if code == 0:
        return body.get("data", {})

    error_map = {
        1001: f"Invalid API key — check TICKDB_API_KEY env var ({context})",
        1002: f"Missing API key — set TICKDB_API_KEY env var ({context})",
        2002: f"Symbol not found — verify via /v1/symbols/available ({context})",
        3001: f"Rate limit exceeded — respect Retry-After header ({context})",
    }

    if code == 3001:
        retry_after = int(response.headers.get("Retry-After", 5))
        logger.warning(f"Rate limited. Sleeping {retry_after}s before retry.")
        time.sleep(retry_after)
        return {}

    raise RuntimeError(f"{error_map.get(code, f'Unexpected error {code}')}: {message}")


# ─── FX Rate Retrieval ────────────────────────────────────────────────────────

def fetch_live_fx_rate(config: MonitorConfig, symbol: str = "USDHKD") -> float:
    """
    Fetch the current USD/HKD exchange rate from TickDB.
    Falls back to the stored rate if the request fails.
    ⚠️ For FX pairs, TickDB's depth channel is not supported;
    we rely on the ticker endpoint for current rate.
    """
    headers = {"X-API-Key": config.tickdb_api_key}
    params = {"symbol": symbol}

    try:
        response = requests.get(
            f"{config.tickdb_rest_url}/market/ticker",
            headers=headers,
            params=params,
            timeout=(3.05, 10)
        )
        data = handle_api_error(response, f"FX ticker {symbol}")

        if data:
            live_rate = float(data.get("last", ADR_PAIRS["BABA"].fx_rate))
            logger.debug(f"Live USD/HKD: {live_rate}")
            return live_rate

    except Exception as e:
        logger.warning(f"Failed to fetch live FX rate: {e}. Using stored rate.")

    # Return the reference rate from the first pair as fallback
    return ADR_PAIRS["BABA"].fx_rate


# ─── Spread Computation ───────────────────────────────────────────────────────

def compute_adr_spread(
    pair: ADRPair,
    us_price: float,
    fx_rate: float
) -> dict:
    """
    Compute the full spread signal for an ADR pair.

    Returns:
        dict with keys: raw_spread_usd, implied_hk_usd, spread_pct,
                        z_score, signal_level, timestamp
    """
    # Implied HK price in USD terms
    implied_hk_usd = (pair.hk_close_hkd / pair.ratio) / fx_rate

    # Raw spread: US price minus the implied HK-equivalent
    raw_spread_usd = us_price - implied_hk_usd

    # Spread as percentage of US price
    spread_pct = (raw_spread_usd / us_price) * 100

    # Z-Score calculation
    if len(pair.spread_history) >= 10:
        mean_spread = statistics.mean(pair.spread_history)
        stdev_spread = statistics.stdev(pair.spread_history)
        z_score = (raw_spread_usd - mean_spread) / stdev_spread if stdev_spread > 0 else 0.0
    else:
        z_score = 0.0
        mean_spread = 0.0
        stdev_spread = 0.0

    # Append to rolling history
    pair.spread_history.append(raw_spread_usd)

    # Signal classification
    abs_z = abs(z_score)
    if abs_z >= 3.0:
        signal_level = "HIGH_CONVICTION"
    elif abs_z >= 2.0:
        signal_level = "ALERT"
    else:
        signal_level = "NORMAL"

    return {
        "pair": pair.adr_symbol,
        "us_price": round(us_price, 4),
        "implied_hk_usd": round(implied_hk_usd, 4),
        "raw_spread_usd": round(raw_spread_usd, 4),
        "spread_pct_bps": round(spread_pct * 100, 2),  # Convert to basis points
        "z_score": round(z_score, 3),
        "signal_level": signal_level,
        "mean_spread": round(mean_spread, 4),
        "stdev_spread": round(stdev_spread, 4),
        "fx_rate": fx_rate,
        "timestamp": datetime.now(timezone.utc).isoformat()
    }


# ─── Alert System ─────────────────────────────────────────────────────────────

def send_alert(config: MonitorConfig, signal: dict):
    """Send an alert via webhook (Slack, custom endpoint, etc.)."""
    if not config.webhook_url:
        logger.info(f"ALERT [{signal['signal_level']}] {signal['pair']}: "
                    f"Z-Score={signal['z_score']}, Spread={signal['raw_spread_usd']} USD "
                    f"({signal['spread_pct_bps']:.1f} bps)")
        return

    payload = {
        "text": f"ADR Arbitrage Signal: {signal['pair']}",
        "blocks": [
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"*{signal['signal_level']}* — {signal['pair']}\n"
                            f"Z-Score: `{signal['z_score']}` | "
                            f"Spread: `${signal['raw_spread_usd']}` "
                            f"({signal['spread_pct_bps']:.1f} bps)\n"
                            f"US Price: `${signal['us_price']}` | "
                            f"Implied HK: `${signal['implied_hk_usd']}` | "
                            f"FX: `{signal['fx_rate']}`"
                }
            }
        ]
    }

    try:
        response = requests.post(
            config.webhook_url,
            json=payload,
            headers={"Content-Type": "application/json"},
            timeout=(3.05, 10)
        )
        if response.status_code != 200:
            logger.warning(f"Webhook returned {response.status_code}")
    except Exception as e:
        logger.error(f"Failed to send alert: {e}")


# ─── WebSocket Subscription Manager ──────────────────────────────────────────

class ADRSpreadMonitor:
    """
    Production-grade WebSocket monitor for ADR spread signals.
    ⚠️ For HFT workloads, replace websockets with asyncio-based aiohttp
    and implement a proper market data handler with a thread-safe buffer.
    """

    def __init__(self, config: MonitorConfig):
        self.config = config
        self.current_prices = {}  # symbol -> latest price
        self.current_fx = ADR_PAIRS["BABA"].fx_rate
        self.running = False
        self._reconnect_delay = 1.0
        self._max_reconnect_delay = 60.0

    async def _heartbeat_loop(self, ws):
        """Send heartbeat every 30 seconds to keep connection alive."""
        while self.running:
            await asyncio.sleep(30)
            try:
                await ws.send(json.dumps({"cmd": "ping"}))
                logger.debug("Heartbeat sent")
            except Exception as e:
                logger.warning(f"Heartbeat failed: {e}")
                break

    async def _subscribe(self, ws, symbols: list):
        """Subscribe to price streams for a list of symbols."""
        subscribe_msg = {
            "cmd": "subscribe",
            "params": {
                "channels": ["ticker"],
                "symbols": symbols
            }
        }
        await ws.send(json.dumps(subscribe_msg))
        logger.info(f"Subscribed to: {symbols}")

    async def _handle_message(self, msg: dict):
        """Process incoming ticker updates."""
        if msg.get("type") == "ticker":
            symbol = msg.get("symbol")
            price = msg.get("last") or msg.get("close")

            if price and symbol:
                self.current_prices[symbol] = float(price)

                # Compute spread for this pair
                for ticker, pair in ADR_PAIRS.items():
                    if pair.adr_symbol == symbol:
                        signal = compute_adr_spread(pair, float(price), self.current_fx)
                        logger.info(
                            f"{signal['pair']} | US={signal['us_price']} | "
                            f"ImpliedHK=${signal['implied_hk_usd']} | "
                            f"Spread=${signal['raw_spread_usd']} ({signal['spread_pct_bps']:.1f} bps) | "
                            f"Z={signal['z_score']} [{signal['signal_level']}]"
                        )

                        if signal["signal_level"] in ("ALERT", "HIGH_CONVICTION"):
                            send_alert(self.config, signal)
                        break

        elif msg.get("type") == "pong":
            logger.debug("Pong received")

        elif msg.get("type") == "error":
            logger.error(f"WebSocket error: {msg}")

    async def connect(self):
        """
        Establish WebSocket connection with full reconnection logic.
        Includes exponential backoff with jitter to prevent thundering herd.
        """
        symbols = [pair.adr_symbol for pair in ADR_PAIRS.values()]

        while self.running:
            try:
                # Build authenticated WebSocket URL
                # TickDB uses URL parameter for WS auth, not headers
                ws_url = (
                    f"{self.config.tickdb_ws_url}"
                    f"?api_key={self.config.tickdb_api_key}"
                )

                async with websockets.connect(ws_url, ping_interval=None) as ws:
                    logger.info("WebSocket connected")

                    # Reset reconnect delay on successful connection
                    self._reconnect_delay = 1.0

                    # Subscribe to symbols
                    await self._subscribe(ws, symbols)

                    # Launch heartbeat
                    heartbeat_task = asyncio.create_task(self._heartbeat_loop(ws))

                    # Message loop
                    async for raw_msg in ws:
                        msg = json.loads(raw_msg)
                        await self._handle_message(msg)

                    heartbeat_task.cancel()

            except websockets.exceptions.ConnectionClosed as e:
                logger.warning(f"Connection closed: {e.code} — {e.reason}")

            except Exception as e:
                logger.error(f"WebSocket error: {e}")

            if self.running:
                # Exponential backoff with jitter
                jitter = self._reconnect_delay * 0.1 * (2 * (time.time() % 1) - 1)
                sleep_time = min(
                    self._reconnect_delay + jitter,
                    self._max_reconnect_delay
                )
                logger.info(f"Reconnecting in {sleep_time:.1f}s...")
                await asyncio.sleep(sleep_time)
                self._reconnect_delay = min(
                    self._reconnect_delay * 2,
                    self._max_reconnect_delay
                )

    def start(self):
        """Start the monitor loop."""
        self.running = True
        logger.info("ADR Spread Monitor starting...")
        asyncio.run(self.connect())

    def stop(self):
        """Gracefully stop the monitor."""
        self.running = False
        logger.info("ADR Spread Monitor stopping...")


# ─── Entry Point ──────────────────────────────────────────────────────────────

if __name__ == "__main__":
    api_key = os.environ.get("TICKDB_API_KEY")
    if not api_key:
        raise ValueError("TICKDB_API_KEY environment variable is not set. "
                         "Sign up at tickdb.ai to obtain an API key.")

    webhook_url = os.environ.get("ADR_ALERT_WEBHOOK_URL")

    config = MonitorConfig(
        tickdb_api_key=api_key,
        webhook_url=webhook_url,
        zscore_threshold=2.0,
        high_conviction_threshold=3.0
    )

    # Pre-warm: fetch current FX rate
    logger.info("Fetching initial FX rate...")
    live_fx = fetch_live_fx_rate(config)
    logger.info(f"Current USD/HKD rate: {live_fx}")

    monitor = ADRSpreadMonitor(config)
    monitor.current_fx = live_fx

    try:
        monitor.start()
    except KeyboardInterrupt:
        monitor.stop()
        logger.info("Monitor stopped.")

Key Design Decisions in This Implementation

Rolling Z-Score with a 60-observation window: The spread history is maintained per-pair using a deque with a maximum length of 60. This captures approximately one trading hour of spread observations at typical update frequencies. A minimum of 10 observations is required before the Z-Score is computed; before that, the signal is classified as NORMAL.

FX drift handling: The fetch_live_fx_rate function is called once at startup. In a production deployment, you would extend this to poll the FX endpoint every 60 seconds or subscribe to a dedicated FX WebSocket stream. The FX drift adjustment is multiplicative: if USD/HKD moves from 7.7820 to 7.7900, the implied HK price in USD terms decreases by roughly 0.1%.

Reconnection with exponential backoff and jitter: The WebSocket connection manager implements the full reconnection sequence: initial delay of 1 second, doubling on each failure, capped at 60 seconds. Jitter (±10% of the delay) prevents synchronized reconnection attempts from multiple monitor instances.


Signal Interpretation: What Z-Score Values Actually Mean

The Z-Score normalizes the raw spread against its recent historical distribution. Interpreting the signal requires understanding what the Z-Score is measuring and what it is not.

Z-Score Signal Level Interpretation Typical Action
|Z| < 1.0 NORMAL Spread within one standard deviation — no action Hold current position
1.0 ≤ |Z| < 2.0 NORMAL (elevated) Watch list — spread is wide but not statistically significant Monitor
2.0 ≤ |Z| < 3.0 ALERT Spread exceeds 2σ — potential mean-reversion signal Notify; assess execution feasibility
|Z| ≥ 3.0 HIGH_CONVICTION Spread exceeds 3σ — rare event, high probability of reversion Full alert; begin execution assessment

A critical nuance: Z-Score convergence does not guarantee convergence within your holding period. The pair may remain dislocated for hours or days before reverting, particularly during earnings season when fundamental repricing dominates. The Z-Score is a necessary but not sufficient condition for a trade.


Sample Signal Output

Running the monitor against live BABA data during a typical trading session produces output similar to the following:

2026-04-15 10:32:15 | INFO | Subscribed to: ['BABA.US', 'JD.US', 'BIDU.US', 'PDD.US']
2026-04-15 10:32:18 | INFO | BABA.US | US=85.20 | ImpliedHK=$84.68 | Spread=$0.52 (61.0 bps) | Z=1.84 [NORMAL]
2026-04-15 10:47:22 | INFO | BABA.US | US=84.15 | ImpliedHK=$84.68 | Spread=-$0.53 (-63.0 bps) | Z=-1.96 [NORMAL]
2026-04-15 11:03:44 | INFO | BABA.US | US=83.40 | ImpliedHK=$84.68 | Spread=-$1.28 (-153.4 bps) | Z=-3.12 [HIGH_CONVICTION]
2026-04-15 11:03:44 | INFO | ALERT [HIGH_CONVICTION] BABA.US: Z-Score=-3.12, Spread=-$1.28 USD (-153.4 bps)

At 11:03 AM ET, BABA traded down to $83.40 while the implied HK reference (from the prior close) sat at $84.68. The spread of -$1.28 (representing a 153 bps discount to the implied HK price) produced a Z-Score of -3.12 — a statistically rare event. This triggers the high-conviction alert.


Comparison: TickDB vs. Alternative Data Architectures

For cross-market ADR monitoring, the data architecture choice determines both signal latency and infrastructure complexity.

Capability Generic polling API TickDB WebSocket
US equity price updates Polling every 1–5 seconds Push-based, sub-second latency
HK reference price Static — requires external source Retrieved once at session start
FX rate integration Requires separate FX vendor Single API covers US equity + FX
Connection resilience DIY reconnection logic Native ping/pong + reconnection guidance
Multi-symbol subscription Sequential API calls Single connection, multiple symbols
Historical spread context Requires external database Requires external database (spreads are computed client-side)
Historical backtest data Varies by vendor 10+ years of US equity OHLCV for strategy validation

For backtesting the Z-Score strategy across historical earnings cycles, TickDB's /v1/market/kline endpoint provides 1-minute OHLCV bars for US equities dating back over 10 years. This enables validation of the Z-Score threshold across bull and bear market regimes before committing capital.


Deployment Recommendations by Scale

User Type Recommended Configuration Notes
Individual quant Single monitor instance, free API tier Limit to 2–3 ADR pairs to stay within rate limits
Small team (2–5 traders) One dedicated monitor per 5 pairs, shared webhook Implement a Redis-backed signal aggregation layer
Institutional desk Multiple monitor instances with failover, dedicated WS connection per pair, direct order API integration Consider dedicated tick-level data (HK trades via trades endpoint) for HFT strategies

Extending the Monitor: Next Steps

The architecture presented here is a foundation. Three natural extensions elevate it from a signal monitor to a complete execution system:

  1. Live HK price feed: During overlapping hours (when both the HK pre-market and NYSE are active), replace the static HK reference with a live HKEX feed. TickDB's depth channel is not supported for HK stocks, but the ticker endpoint provides real-time HK prices — enabling true dual-leg arbitrage rather than one-sided monitoring.

  2. Execution slippage model: Before acting on a Z-Score signal, estimate execution costs: NYSE market impact, HK margin requirements, FX conversion fees, and SEC regulatory fees. A signal that clears Z=3.0 but carries $0.30 in execution costs is not actionable.

  3. Earnings-event filtering: During earnings releases, spread distributions are fundamentally altered by single-stock volatility. Z-Score thresholds should be widened by 1.5–2.0x during a 30-minute window surrounding each company's earnings announcement to avoid whipsaw signals.


Conclusion

ADR arbitrage is not a theoretical strategy. It is a data engineering problem: maintaining a synchronized, real-time view of two legs in different currencies across different time zones, with a statistically grounded signal that adapts to regime changes.

The Z-Score approach transforms a raw price gap into a normalized, historically contextualized signal. The WebSocket implementation transforms a batch query into a continuous stream. Together, they produce a monitoring system that identifies cross-market dislocations as they happen — not after the opportunity has closed.


Next Steps

If you are an individual quant developer, set up the free TickDB API key and run the monitor against a single ADR pair. Use the rolling Z-Score output to build intuition for how spreads behave around earnings, macro events, and sector rotations.

If you need 10+ years of historical US equity OHLCV data to backtest the Z-Score threshold across multiple regimes, request access to TickDB's institutional data plans at enterprise@tickdb.ai.

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


This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. ADR spread opportunities are subject to execution costs, regulatory constraints, and liquidity conditions that may prevent profitable realization.