"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:
- Data Ingestion Layer: WebSocket subscriptions to US equity price feeds and HKD/USD exchange rates.
- Computation Layer: Real-time spread calculation, normalization, and Z-Score signal generation.
- 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:
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
depthchannel is not supported for HK stocks, but thetickerendpoint provides real-time HK prices — enabling true dual-leg arbitrage rather than one-sided monitoring.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.
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.