The first version of our data pipeline required managing seven separate WebSocket connections, four vendor SDKs, and two polling loops. At 9:30 AM ET on a typical trading day, we were burning CPU on connection management while missing half-second windows of opportunity.
The maintenance overhead was staggering. Every time a vendor changed their heartbeat interval, updated their authentication headers, or—worse—deprecated an endpoint, we spent sprint cycles patching adapters. The problem was not data acquisition. The problem was the infrastructure tax of connecting to heterogeneous markets.
This article dissects the architectural approach TickDB uses to consolidate multiple market feeds into a single WebSocket connection. We examine the protocol adaptation layer, the unified data model that normalizes disparate exchange formats, and the timezone standardization logic that makes cross-market analysis tractable.
The Fragmentation Problem
Each major market operates on fundamentally different protocols and data conventions.
| Market | Primary Protocol | Auth Method | Heartbeat Interval | Timestamp Convention |
|---|---|---|---|---|
| US Equities | Proprietary / WebSocket | API key header | 30s | US Eastern Time |
| HK Equities | HKEX gateway protocol | Session token | 15s | Hong Kong Time |
| Crypto | Binance/Kraken WebSocket | API key URL param | 3 min | UTC |
| Futures | CQG/Rithmic | Certificate + user | 20s | Exchange local time |
Building a unified gateway is not a matter of "translating JSON." It requires handling protocol handshakes, subscription models, authentication flows, and timestamp normalization across all of these environments—while maintaining sub-second latency.
Architecture Overview: Three-Layer Design
TickDB's unified gateway operates as a three-layer architecture:
┌─────────────────────────────────────────────────────────────┐
│ Unified WebSocket Gateway │
│ (Single connection to client) │
└────────────────────────────┬────────────────────────────────┘
│
┌────────────────────────────▼────────────────────────────────┐
│ Protocol Adaptation Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ US Feed │ │ HK Feed │ │ Crypto │ │ Futures │ │
│ │ Adapter │ │ Adapter │ │ Adapter │ │ Adapter │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└────────────────────────────┬────────────────────────────────┘
│
┌────────────────────────────▼────────────────────────────────┐
│ Exchange Connectors │
│ NYSE │ HKEX │ Binance │ CME │ ... │
└─────────────────────────────────────────────────────────────┘
Layer 1: Exchange Connectors
Each exchange connector maintains a dedicated, optimized connection to its source. These connectors are built on exchange-specific SDKs where necessary, or raw WebSocket clients where the protocol is open.
Connectors are responsible for:
- Raw message ingestion
- Initial parsing and validation
- Timestamp extraction (before normalization)
- Error detection and reconnection to the source exchange
Layer 2: Protocol Adaptation Layer
This is the core innovation. Each adapter translates between the exchange-specific protocol and TickDB's internal message format. The adaptation layer handles:
- Subscription model normalization: Exchanges use different subscription semantics (subscribe/unsubscribe frames, batch subscription formats, symbol formats like
AAPL.USvs00700.HKvsBTC-USD) - Authentication translation: Different auth methods are normalized to a single internal session model
- Heartbeat protocol mapping: Ping/pong intervals are normalized to a unified keepalive schedule
- Error code translation: Exchange-specific error codes map to TickDB's internal error taxonomy
Layer 3: Unified Gateway
The gateway exposes a single WebSocket interface to clients. It routes messages based on symbol routing rules, manages client sessions, and applies the unified data model to all outbound messages.
Protocol Adaptation: How Symbol Translation Works
The most visible challenge in multi-market data is symbol format divergence. Consider the same asset across three markets:
| Asset | US Symbol Format | HK Symbol Format | Crypto Symbol Format |
|---|---|---|---|
| Apple | AAPL.US |
— | — |
| Tencent | — | 0700.HK |
— |
| Bitcoin | — | — | BTC-USD or BTCUSDT |
TickDB's symbol registry normalizes all incoming symbols to a canonical format before they reach the gateway. The routing table below shows how subscriptions are resolved:
| Canonical Symbol | US Resolution | HK Resolution | Crypto Resolution |
|---|---|---|---|
AAPL.US |
AAPL on NYSE feed |
Not applicable | Not applicable |
0700.HK |
Not applicable | 0700 on HKEX feed |
Not applicable |
BTC-USD |
Not applicable | Not applicable | BTCUSDT on Binance / BTC-USD on Kraken |
When a client subscribes to AAPL.US, the gateway routes that subscription to the US equity adapter. When a client subscribes to BTC-USD, the gateway routes to the crypto adapter—and the adapter handles the translation to the specific exchange's expected symbol format.
This routing is transparent to the client. The client sends one subscription frame; the gateway handles distribution internally.
Unified Data Model
The unified data model is the contract between the gateway and clients. Regardless of source exchange, all order book, trade, and OHLCV data follows the same schema.
Kline (OHLCV) Normalization
For candle data, the unified model follows a consistent structure:
{
"channel": "kline",
"symbol": "AAPL.US",
"interval": "1m",
"data": {
"open": 182.45,
"high": 183.20,
"low": 182.10,
"close": 183.05,
"volume": 45230000,
"timestamp": 1747844400000
}
}
The timestamp field is always epoch milliseconds in UTC. This is non-negotiable—the gateway never exposes exchange-native timestamps to the client.
Order Book Normalization
The depth (order book) channel follows a similar normalization:
{
"channel": "depth",
"symbol": "0700.HK",
"data": {
"bids": [[435.20, 120000], [435.00, 85000]],
"asks": [[435.40, 95000], [435.60, 110000]],
"timestamp": 1747844400500
}
}
Each level is an [price, size] tuple. The gateway normalizes price precision and lot sizes across exchanges—US equities use two decimal places, while HK equities may use different tick sizes depending on price range.
Timezone Standardization: Why UTC Is the Only Viable Choice
Cross-market analysis is impossible without a unified time reference. Each market operates in its local timezone:
| Market | Exchange Timezone | DST Behavior |
|---|---|---|
| US Equities | America/New_York | Observed |
| HK Equities | Asia/Hong_Kong | Not observed |
| Crypto | UTC | Not applicable |
TickDB standardizes all timestamps to UTC epoch milliseconds at the adapter layer, before data reaches the unified gateway. This has three practical benefits:
- Cross-market alignment: A 15-minute candle for
AAPL.USending at1747844400000aligns precisely with a candle for0700.HKending at the same timestamp. - No DST edge cases: UTC has no daylight saving transitions. A "9:30 AM ET" on March 10, 2025 is a different wall-clock time than on November 3, 2025 due to DST. The UTC timestamp
1747844400000refers to exactly one instant, unambiguously. - Simplified backtesting: When replaying historical data across multiple markets, UTC timestamps eliminate the need to track which markets were in DST on any given date.
The one caveat: for display purposes, clients are responsible for converting UTC to their target timezone. The gateway provides the raw epoch; display is the client's concern.
Implementation: Unified WebSocket Subscription
The following code demonstrates how a client connects to the unified gateway and subscribes to cross-market data. Note the production-grade implementation with heartbeat, reconnection, and rate-limit handling.
import os
import json
import time
import random
import threading
import websocket
from datetime import datetime, timezone
class UnifiedMarketDataClient:
"""
Unified WebSocket client for TickDB multi-market data.
Connects once, subscribes across US equities, HK equities, and crypto.
"""
def __init__(self, api_key: str):
self.api_key = api_key
self.ws = None
self.base_url = "wss://api.tickdb.ai/ws"
self.running = False
self.reconnect_delay = 1.0
self.max_reconnect_delay = 32.0
self.heartbeat_interval = 30
# Subscription state
self.subscribed_symbols = {
"us": [],
"hk": [],
"crypto": []
}
def connect(self):
"""Establish WebSocket connection with API key in URL parameter."""
# ⚠️ TickDB WebSocket auth: API key goes in URL parameter, not header
url = f"{self.base_url}?api_key={self.api_key}"
self.ws = websocket.WebSocketApp(
url,
on_open=self._on_open,
on_message=self._on_message,
on_error=self._on_error,
on_close=self._on_close
)
self.running = True
# Run in daemon thread; production use should use threading.Event for shutdown
thread = threading.Thread(target=self.ws.run_forever)
thread.daemon = True
thread.start()
print(f"[{self._timestamp()}] Connected to unified gateway")
def subscribe(self, symbols: list[str]):
"""
Subscribe to any combination of US, HK, and crypto symbols.
Symbol format: AAPL.US, 0700.HK, BTC-USD, ETH-USDT
"""
for symbol in symbols:
subscribe_frame = {
"cmd": "subscribe",
"channel": self._detect_channel(symbol),
"symbol": symbol
}
self.ws.send(json.dumps(subscribe_frame))
market = self._market_from_symbol(symbol)
self.subscribed_symbols[market].append(symbol)
print(f"[{self._timestamp()}] Subscribed to {symbol} ({market})")
def _detect_channel(self, symbol: str) -> str:
"""Determine the appropriate channel for a symbol."""
if symbol.endswith(".US"):
return "kline"
elif symbol.endswith(".HK"):
return "depth"
elif "-" in symbol or symbol.endswith("USDT"):
return "kline"
return "kline"
def _market_from_symbol(self, symbol: str) -> str:
"""Map symbol to market bucket."""
if symbol.endswith(".US"):
return "us"
elif symbol.endswith(".HK"):
return "hk"
return "crypto"
def _on_open(self, ws):
"""Called when WebSocket connection is established."""
print(f"[{self._timestamp()}] WebSocket opened; starting heartbeat")
self._start_heartbeat()
# Resubscribe to previously subscribed symbols after reconnect
for market, symbols in self.subscribed_symbols.items():
for symbol in symbols:
subscribe_frame = {
"cmd": "subscribe",
"channel": self._detect_channel(symbol),
"symbol": symbol
}
ws.send(json.dumps(subscribe_frame))
def _start_heartbeat(self):
"""Send ping frames at regular intervals to maintain connection."""
def heartbeat_loop():
while self.running and self.ws:
time.sleep(self.heartbeat_interval)
if self.running and self.ws:
try:
# TickDB uses JSON-based ping/pong; check docs for your version
self.ws.send(json.dumps({"cmd": "ping"}))
print(f"[{self._timestamp()}] Heartbeat sent")
except Exception as e:
print(f"[{self._timestamp()}] Heartbeat error: {e}")
break
thread = threading.Thread(target=heartbeat_loop, daemon=True)
thread.start()
def _on_message(self, ws, message: str):
"""Handle incoming market data messages."""
try:
data = json.loads(message)
# Handle pong responses
if data.get("cmd") == "pong":
return
# Handle error responses (including rate limits)
if "code" in data:
code = data["code"]
if code == 0:
# Success, no action needed
return
elif code == 3001:
# Rate limit exceeded — extract Retry-After
retry_after = int(data.get("headers", {}).get("Retry-After", 5))
print(f"[{self._timestamp()}] Rate limited; waiting {retry_after}s")
time.sleep(retry_after)
return
else:
print(f"[{self._timestamp()}] Gateway error {code}: {data.get('message')}")
return
# Handle market data messages
channel = data.get("channel")
symbol = data.get("symbol")
if channel == "kline":
self._process_kline(data)
elif channel == "depth":
self._process_depth(data)
elif channel == "trades":
self._process_trade(data)
else:
print(f"[{self._timestamp()}] Unknown channel: {channel}")
except json.JSONDecodeError as e:
print(f"[{self._timestamp()}] JSON decode error: {e}")
except Exception as e:
print(f"[{self._timestamp()}] Message handling error: {e}")
def _process_kline(self, data: dict):
"""Process OHLCV candle data."""
kline = data.get("data", {})
ts = datetime.fromtimestamp(kline.get("timestamp", 0) / 1000, tz=timezone.utc)
print(
f"[{ts.isoformat()}] {data.get('symbol')} | "
f"O:{kline['open']} H:{kline['high']} L:{kline['low']} "
f"C:{kline['close']} V:{kline['volume']}"
)
def _process_depth(self, data: dict):
"""Process order book depth snapshot."""
depth = data.get("data", {})
ts = datetime.fromtimestamp(depth.get("timestamp", 0) / 1000, tz=timezone.utc)
best_bid = depth["bids"][0] if depth.get("bids") else None
best_ask = depth["asks"][0] if depth.get("asks") else None
if best_bid and best_ask:
spread = best_ask[0] - best_bid[0]
print(
f"[{ts.isoformat()}] {data.get('symbol')} | "
f"Bid:{best_bid[0]}({best_bid[1]}) Ask:{best_ask[0]}({best_ask[1]}) "
f"Spread:{spread:.4f}"
)
def _process_trade(self, data: dict):
"""Process trade tick data."""
trade = data.get("data", {})
ts = datetime.fromtimestamp(trade.get("timestamp", 0) / 1000, tz=timezone.utc)
print(
f"[{ts.isoformat()}] TRADE {data.get('symbol')} | "
f"Price:{trade['price']} Size:{trade['size']} Side:{trade.get('side', 'N/A')}"
)
def _on_error(self, ws, error):
"""Handle WebSocket errors and trigger reconnection."""
print(f"[{self._timestamp()}] WebSocket error: {error}")
self._schedule_reconnect()
def _on_close(self, ws, close_status_code, close_msg):
"""Handle connection close and schedule reconnection."""
print(f"[{self._timestamp()}] Connection closed ({close_status_code}): {close_msg}")
self.running = False
self._schedule_reconnect()
def _schedule_reconnect(self):
"""Reconnect with exponential backoff and jitter."""
self.running = False
# Exponential backoff
delay = min(self.reconnect_delay * 2, self.max_reconnect_delay)
# Add jitter to prevent thundering herd
jitter = random.uniform(0, delay * 0.1)
total_delay = delay + jitter
print(f"[{self._timestamp()}] Reconnecting in {total_delay:.2f}s")
time.sleep(total_delay)
self.reconnect_delay = delay
self.connect()
@staticmethod
def _timestamp() -> str:
"""Return current UTC timestamp for logging."""
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
# Usage example
if __name__ == "__main__":
# ⚠️ Load API key from environment variable — never hardcode credentials
api_key = os.environ.get("TICKDB_API_KEY")
if not api_key:
raise ValueError("TICKDB_API_KEY environment variable not set")
client = UnifiedMarketDataClient(api_key)
client.connect()
# Subscribe across markets with a single connection
symbols = [
"AAPL.US", # US equity
"0700.HK", # HK equity
"BTC-USD" # Crypto
]
client.subscribe(symbols)
# Keep main thread alive; production use should implement graceful shutdown
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Shutting down...")
Key architectural points in this implementation:
- Single connection, multiple markets: The client establishes one WebSocket connection. The gateway handles routing to individual exchange adapters internally.
- Symbol-based routing: The
_market_from_symbol()method routes subscriptions to the correct adapter based on symbol suffix (.US,.HK, or crypto format). - Heartbeat management: The client maintains its own heartbeat loop, which is independent of exchange-specific heartbeat intervals. This decouples client-side keepalive from the underlying adapters.
- Exponential backoff with jitter: Reconnection uses
delay = min(base * (2 ** retry), max_delay)plus random jitter, preventing thundering herd on gateway restart. - Rate-limit handling: The
_on_message()method checks forcode: 3001and respects theRetry-Afterheader.
Data Coverage by Market
The unified gateway exposes different channels depending on market capability. The table below summarizes supported channels:
| Channel | US Equities | HK Equities | Crypto |
|---|---|---|---|
kline (OHLCV) |
✅ 10+ years | ✅ | ✅ |
depth (order book) |
✅ L1 | ✅ L1–L10 | ✅ L1–L10 |
trades (tick data) |
❌ Not supported | ✅ | ✅ |
Important: TickDB's trades endpoint does not support US equities or A-shares. For US equity tick data, you must use a specialized tick data provider. For cross-market OHLCV backtesting, the kline endpoint is fully supported across all three markets.
Unified Gateway vs. Multi-Connection Approach
For teams evaluating whether to build their own multi-vendor integration or adopt a unified gateway, the comparison below quantifies the tradeoffs:
| Dimension | Build Your Own | TickDB Unified Gateway |
|---|---|---|
| Connections managed | N (one per vendor) | 1 (to TickDB) |
| Protocol maintenance | N integrations | 0 (managed by TickDB) |
| Auth management | N credential sets | 1 API key |
| Timezone normalization | DIY | Built-in (UTC) |
| Symbol format translation | DIY | Automatic |
| Latency overhead | None (direct to exchange) | ~50–100 ms added |
| Historical data | Requires separate backfill | Unified via /v1/market/kline |
| Cost model | Per-vendor pricing | Single TickDB plan |
| Multi-market candle alignment | DIY | Automatic |
The latency overhead is the primary tradeoff. Direct exchange connections offer the lowest possible latency, but at the cost of operational complexity. The unified gateway adds approximately 50–100 ms of latency—acceptable for most quantitative strategies, but potentially disqualifying for latency-sensitive high-frequency strategies.
Deployment Recommendations
The unified gateway is suitable for the following scenarios:
| Use Case | Recommendation |
|---|---|
| Cross-market strategy backtesting | Use /v1/market/kline REST endpoint for historical OHLCV; single API key, unified timestamps |
| Multi-monitor dashboards | Use the WebSocket gateway; subscribe to 20+ symbols across markets on one connection |
| Pairs trading (US + HK) | Use WebSocket with depth channel for real-time order book; kline for signal generation |
| Crypto-only strategies | Valid but consider direct exchange WebSocket if latency is critical |
| HFT / market making | Not recommended; direct exchange connectivity required for sub-millisecond latency |
Conclusion
The unified gateway solves a real operational problem: the infrastructure tax of managing heterogeneous market data sources. By centralizing protocol adaptation, symbol translation, and timezone normalization into a single layer, TickDB allows quantitative teams to focus on strategy development rather than connector maintenance.
The architecture is straightforward in concept—three-layer design with an adapter per market—but the implementation details matter. Heartbeat protocol mapping, error code translation, reconnection logic, and UTC normalization are the details that determine whether the unified gateway actually works in production.
For cross-market strategies, the single WebSocket connection approach reduces connection management overhead significantly. For latency-critical applications, the tradeoff is explicit: simplicity versus speed.
Next Steps
If you're building a cross-market quantitative strategy, start with the free TickDB API tier to test multi-market OHLCV data alignment across US equities, HK equities, and crypto.
If you need historical data for backtesting:
- Sign up at tickdb.ai (free, no credit card required)
- Generate an API key in the dashboard
- Set the
TICKDB_API_KEYenvironment variable - Use the
/v1/market/klineendpoint with cross-market symbol parameters
If you're evaluating enterprise data volumes, contact enterprise@tickdb.ai for plans covering extended historical depth, real-time depth for US equities, and dedicated support.
If you use AI coding assistants, search for the tickdb-market-data SKILL in your AI tool's marketplace for assisted integration.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. Data accuracy depends on exchange-provided feeds; TickDB serves as a data aggregation layer and does not guarantee the completeness or accuracy of exchange-sourced information.