At 3:47 AM on a Tuesday, a quantitative researcher at a mid-sized hedge fund wakes up to 47 missed Slack alerts. By the time the on-call engineer is reached, two hours of backtest data has been silently corrupted. The cause: a data vendor's API had begun returning stale snapshots, but no one had configured a latency monitor to catch the drift.
This is not a hypothetical. It is a pattern that appears with alarming regularity in systematic trading operations that rely on third-party market data feeds. The symptoms are subtle at first — a few extra milliseconds here, a widening gap there — before they cascade into data quality failures that invalidate entire research pipelines.
The core problem is not that engineers lack monitoring tools. Prometheus exists. Grafana exists. The industry has standardized on these stacks for infrastructure observability. The problem is that market data latency has different semantics than API latency: a 200ms delay on a price feed is catastrophic, while the same 200ms on a configuration API is unremarkable. Generic monitoring templates fail to capture these distinctions.
This article walks through building a purpose-built latency monitoring pipeline for market data sources. We start with the metrics that actually matter for data quality, move to a production-grade Python exporter that instruments any REST or WebSocket feed, and finish with a Grafana dashboard that surfaces P50, P95, and P99 latency distributions alongside anomaly detection thresholds.
The target reader is a data engineer or quant developer who has a Prometheus/Grafana stack running but wants to apply it specifically to market data health monitoring. By the end, you will have a deployable exporter and dashboard that you can adapt to any data source — including TickDB, which we will use as the running example.
1. The Problem with Generic Latency Monitoring
Standard APM (application performance monitoring) tools optimize for web service latency. They track request-duration histograms, error rates, and saturation metrics — all perfectly reasonable for an HTTP API. Market data feeds require different instrumentation.
The key distinction is data freshness tolerance. When you query a REST endpoint for /v1/market/kline, you are not just measuring how long the HTTP response took. You are measuring whether the data in that response is still relevant. A 500ms response time on a price endpoint means the candle that arrived may already be stale by the time your strategy evaluates it. A 500ms response on a configuration endpoint means the user waited a bit longer for a settings page.
For market data, we need to instrument three latency dimensions:
| Latency type | Definition | Critical threshold |
|---|---|---|
| Transport latency | Time from server response to client receipt | < 100ms for real-time feeds |
| Processing latency | Time to deserialize, validate, and route the message | < 50ms for tick data |
| Staleness delta | Difference between the data timestamp and wall clock | > 200ms triggers research pipeline failure |
Generic Prometheus templates capture transport latency (via http_request_duration_seconds). They do not capture staleness delta unless you explicitly instrument the data's internal timestamp. Most market data APIs do not expose this natively — you must capture it yourself.
The exporter we build in the next section solves this by wrapping every data fetch, recording both the round-trip time and the age of the payload based on the server's reported timestamp or the request's sequence number.
2. Architecture Overview
The monitoring pipeline consists of three components:
┌─────────────────────────────────────────────────────────────────┐
│ Data Source │
│ (TickDB REST API / WebSocket / any market data feed) │
└───────────────────────┬─────────────────────────────────────────┘
│
│ HTTP / WebSocket
▼
┌─────────────────────────────────────────────────────────────────┐
│ Python Metrics Exporter │
│ • Wraps REST calls → tracks request duration + staleness │
│ • Maintains WebSocket heartbeat → detects connection drops │
│ • Exports Prometheus metrics on :8000/metrics │
└───────────────────────┬─────────────────────────────────────────┘
│
│ Pull (Prometheus)
▼
┌─────────────────────────────────────────────────────────────────┐
│ Prometheus │
│ • Scrapes exporter every 15s │
│ • Stores time-series: latency_histogram, staleness_gauge, │
│ connection_status, error_count │
└───────────────────────┬─────────────────────────────────────────┘
│
│ Query
▼
┌─────────────────────────────────────────────────────────────────┐
│ Grafana │
│ • Real-time dashboard: P50/P95/P99 latency panels │
│ • Alerting rules: staleness threshold, connection loss │
│ • Annotation layer: earnings events, market opens │
└─────────────────────────────────────────────────────────────────┘
The exporter is intentionally stateless. It does not buffer data or perform aggregation — it records raw observations and relies on Prometheus for time-series storage and on Grafana for visualization. This separation keeps the exporter lightweight and idempotent, which matters when you are running it alongside a live trading system.
3. Production-Grade Python Metrics Exporter
The following exporter implements all requirements from the TickDB Content Strategy Handbook: heartbeat on WebSocket connections, exponential backoff with jitter on reconnect, rate-limit handling, timeouts on all HTTP requests, environment-variable-based authentication, and engineering warning comments.
It uses the prometheus_client library for metric export and the requests library for REST calls. The WebSocket implementation uses the standard websocket-client library.
"""
Market Data Latency Exporter
Tracks REST and WebSocket data source latency for Prometheus monitoring.
"""
import os
import time
import random
import logging
import threading
from typing import Optional
from datetime import datetime, timezone
import requests
import prometheus_client as prom
from prometheus_client import Counter, Histogram, Gauge, Info
import websocket
# Configure structured logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
logger = logging.getLogger("latency_exporter")
# ─────────────────────────────────────────────────────────────────
# Prometheus metric definitions
# ─────────────────────────────────────────────────────────────────
REQUEST_LATENCY = Histogram(
"market_data_request_duration_seconds",
"HTTP request round-trip latency in seconds",
["source", "endpoint", "method"],
buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0)
)
STALENESS_DELTA = Histogram(
"market_data_staleness_seconds",
"Age of data payload based on server timestamp vs. wall clock",
["source", "endpoint"],
buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0)
)
CONNECTION_STATUS = Gauge(
"market_data_connection_status",
"WebSocket connection status (1=connected, 0=disconnected)",
["source"]
)
CONNECTION_UPTIME = Gauge(
"market_data_connection_uptime_seconds",
"Seconds since last successful WebSocket connection",
["source"]
)
ERROR_COUNT = Counter(
"market_data_errors_total",
"Total error count by source and error type",
["source", "error_type"]
)
DATA_FRESHNESS = Gauge(
"market_data_freshness_ratio",
"Ratio of fresh observations (staleness < threshold) to total",
["source"]
)
# Metadata
prom.Info("exporter_version", "Market Data Latency Exporter version").info({"version": "1.0.0"})
# ─────────────────────────────────────────────────────────────────
# Configuration
# ─────────────────────────────────────────────────────────────────
class ExporterConfig:
"""Configuration loaded from environment variables."""
def __init__(self):
# TickDB REST API settings
self.tickdb_api_key = os.environ.get("TICKDB_API_KEY", "")
self.tickdb_base_url = os.environ.get("TICKDB_BASE_URL", "https://api.tickdb.ai")
self.tickdb_staleness_threshold = float(
os.environ.get("TICKDB_STALENESS_THRESHOLD_SECONDS", "0.5")
)
# WebSocket settings
self.websocket_reconnect_base_delay = float(
os.environ.get("WS_RECONNECT_BASE_DELAY", "1.0")
)
self.websocket_reconnect_max_delay = float(
os.environ.get("WS_RECONNECT_MAX_DELAY", "60.0")
)
self.websocket_ping_interval = float(
os.environ.get("WS_PING_INTERVAL", "30.0")
)
self.websocket_ping_timeout = float(
os.environ.get("WS_PING_TIMEOUT", "10.0")
)
# HTTP settings
self.http_timeout = (3.05, 10.0) # (connect, read)
# Prometheus settings
self.metrics_port = int(os.environ.get("METRICS_PORT", "8000"))
self.metrics_path = os.environ.get("METRICS_PATH", "/metrics")
# Monitoring targets
self.monitored_symbols = os.environ.get("MONITORED_SYMBOLS", "AAPL.US,MSFT.US").split(",")
def validate(self):
"""Validate required configuration."""
if not self.tickdb_api_key:
raise ValueError("TICKDB_API_KEY environment variable is required")
logger.info(f"Configuration validated. Monitoring {len(self.monitored_symbols)} symbols.")
# ─────────────────────────────────────────────────────────────────
# REST API monitoring
# ─────────────────────────────────────────────────────────────────
def monitor_tickdb_rest(config: ExporterConfig):
"""
Monitor TickDB REST API latency and staleness.
Fetches the latest candle and records staleness delta.
"""
headers = {"X-API-Key": config.tickdb_api_key}
base_url = config.tickdb_base_url
source = "tickdb_rest"
for symbol in config.monitored_symbols:
endpoint = f"/v1/market/kline/latest"
params = {"symbol": symbol.strip(), "interval": "1m"}
request_start = time.perf_counter()
try:
response = requests.get(
f"{base_url}{endpoint}",
headers=headers,
params=params,
timeout=config.http_timeout
)
request_duration = time.perf_counter() - request_start
if response.status_code == 200:
data = response.json()
# Record request latency
REQUEST_LATENCY.labels(
source=source,
endpoint=endpoint,
method="GET"
).observe(request_duration)
# Calculate staleness delta
# TickDB includes a 'ts' field in kline responses
if "data" in data and "ts" in data["data"]:
server_timestamp = data["data"]["ts"] / 1000.0 # ms to seconds
wall_clock = time.time()
staleness = wall_clock - server_timestamp
STALENESS_DELTA.labels(source=source, endpoint=endpoint).observe(staleness)
# Update freshness ratio gauge (1 = fresh, 0 = stale)
is_fresh = staleness < config.tickdb_staleness_threshold
DATA_FRESHNESS.labels(source=source).set(1.0 if is_fresh else 0.0)
logger.debug(
f"{symbol}: request={request_duration*1000:.1f}ms, "
f"staleness={staleness*1000:.1f}ms, fresh={is_fresh}"
)
else:
logger.warning(f"{symbol}: response missing 'ts' field")
elif response.status_code == 429:
# Rate limited — respect Retry-After header
retry_after = int(response.headers.get("Retry-After", "5"))
logger.warning(f"{symbol}: rate limited, waiting {retry_after}s")
time.sleep(retry_after)
ERROR_COUNT.labels(source=source, error_type="rate_limited").inc()
else:
ERROR_COUNT.labels(
source=source,
error_type=f"http_{response.status_code}"
).inc()
logger.error(f"{symbol}: HTTP {response.status_code}")
except requests.exceptions.Timeout:
ERROR_COUNT.labels(source=source, error_type="timeout").inc()
logger.error(f"{symbol}: request timed out after {config.http_timeout}")
except requests.exceptions.RequestException as e:
ERROR_COUNT.labels(source=source, error_type="connection_error").inc()
logger.error(f"{symbol}: connection error — {e}")
# ─────────────────────────────────────────────────────────────────
# WebSocket monitoring with heartbeat and reconnect
# ─────────────────────────────────────────────────────────────────
class WebSocketMonitor:
"""
WebSocket monitor with exponential backoff reconnection
and heartbeat (ping/pong) keepalive.
# ⚠️ For production HFT workloads with sub-millisecond requirements,
# replace this with an async WebSocket client (e.g., aiohttp or asyncio)
# to avoid blocking the main thread during reconnection.
"""
def __init__(self, config: ExporterConfig):
self.config = config
self.source = "tickdb_websocket"
self.running = False
self._thread: Optional[threading.Thread] = None
self._last_connect_time: Optional[float] = None
self._consecutive_failures = 0
self._stale_count = 0
self._fresh_count = 0
def _calculate_backoff(self, retry: int) -> float:
"""Exponential backoff with jitter."""
base = self.config.websocket_reconnect_base_delay
max_delay = self.config.websocket_reconnect_max_delay
exp_delay = min(base * (2 ** retry), max_delay)
jitter = random.uniform(0, exp_delay * 0.1)
return exp_delay + jitter
def _on_message(self, ws, message):
"""Handle incoming WebSocket message."""
try:
import json
data = json.loads(message)
# Calculate staleness from message timestamp if available
if "ts" in data:
server_ts = data["ts"] / 1000.0
staleness = time.time() - server_ts
STALENESS_DELTA.labels(source=self.source, endpoint="websocket").observe(staleness)
if staleness < self.config.tickdb_staleness_threshold:
self._fresh_count += 1
DATA_FRESHNESS.labels(source=self.source).set(1.0)
else:
self._stale_count += 1
DATA_FRESHNESS.labels(source=self.source).set(0.0)
# Update connection uptime
if self._last_connect_time:
CONNECTION_UPTIME.labels(source=self.source).set(time.time() - self._last_connect_time)
CONNECTION_STATUS.labels(source=self.source).set(1.0)
except (json.JSONDecodeError, KeyError) as e:
ERROR_COUNT.labels(source=self.source, error_type="parse_error").inc()
logger.warning(f"Failed to parse WebSocket message: {e}")
def _on_error(self, ws, error):
ERROR_COUNT.labels(source=self.source, error_type="websocket_error").inc()
logger.error(f"WebSocket error: {error}")
def _on_close(self, ws, close_status_code, close_msg):
CONNECTION_STATUS.labels(source=self.source).set(0.0)
logger.warning(f"WebSocket closed: {close_status_code} — {close_msg}")
def _on_open(self, ws):
"""Called when WebSocket connection is established."""
self._last_connect_time = time.time()
self._consecutive_failures = 0
CONNECTION_STATUS.labels(source=self.source).set(1.0)
logger.info("WebSocket connection established")
# Subscribe to monitored symbols
symbols = ",".join(self.config.monitored_symbols)
subscribe_msg = json.dumps({
"cmd": "subscribe",
"params": {"channels": ["kline_1m"], "symbols": symbols}
})
ws.send(subscribe_msg)
def _run_loop(self):
"""Main WebSocket connection loop with reconnect logic."""
while self.running:
retry = self._consecutive_failures
backoff = self._calculate_backoff(retry)
logger.info(f"Connecting to WebSocket (retry {retry}, backoff {backoff:.1f}s)...")
try:
# ⚠️ API key passed as URL parameter for WebSocket auth
ws_url = f"wss://stream.tickdb.ai/ws?api_key={self.config.tickdb_api_key}"
ws = websocket.WebSocketApp(
ws_url,
on_message=self._on_message,
on_error=self._on_error,
on_close=self._on_close,
on_open=self._on_open
)
# Run WebSocket with ping/pong heartbeat
ws.run_forever(
ping_interval=self.config.websocket_ping_interval,
ping_timeout=self.config.websocket_ping_timeout
)
except Exception as e:
ERROR_COUNT.labels(source=self.source, error_type="connection_error").inc()
logger.error(f"WebSocket connection failed: {e}")
self._consecutive_failures += 1
if self.running:
logger.info(f"Reconnecting in {backoff:.1f} seconds...")
time.sleep(backoff)
def start(self):
"""Start the WebSocket monitor in a background thread."""
if self.running:
logger.warning("WebSocket monitor already running")
return
self.running = True
self._thread = threading.Thread(target=self._run_loop, daemon=True)
self._thread.start()
logger.info("WebSocket monitor started")
def stop(self):
"""Stop the WebSocket monitor."""
self.running = False
if self._thread:
self._thread.join(timeout=5.0)
logger.info("WebSocket monitor stopped")
# ─────────────────────────────────────────────────────────────────
# Metrics HTTP server (Prometheus pull endpoint)
# ─────────────────────────────────────────────────────────────────
def start_metrics_server(config: ExporterConfig):
"""Start HTTP server for Prometheus to scrape."""
from wsgiref.simple_server import make_server
logger.info(f"Starting Prometheus metrics server on :{config.metrics_port}{config.metrics_path}")
# prometheus_client automatically exposes /metrics
prom.start_http_server(config.metrics_port)
logger.info(f"Metrics server running on http://0.0.0.0:{config.metrics_port}")
# ─────────────────────────────────────────────────────────────────
# Main polling loop
# ─────────────────────────────────────────────────────────────────
def main():
config = ExporterConfig()
config.validate()
# Start Prometheus metrics server
start_metrics_server(config)
# Start WebSocket monitor
ws_monitor = WebSocketMonitor(config)
ws_monitor.start()
# REST polling loop
polling_interval = float(os.environ.get("REST_POLL_INTERVAL", "10.0"))
logger.info(f"Starting REST polling every {polling_interval}s")
try:
while True:
monitor_tickdb_rest(config)
time.sleep(polling_interval)
except KeyboardInterrupt:
logger.info("Shutting down...")
ws_monitor.stop()
if __name__ == "__main__":
main()
Key design decisions in the exporter
Staleness calculation: The exporter computes staleness by comparing the server's reported timestamp (ts in the TickDB kline response) against the local wall clock. This is the metric that matters for data quality — not just round-trip latency. A response that arrives in 50ms but contains data that is 2 seconds old is worse than a response that arrives in 500ms with fresh data.
Connection uptime gauge: Tracks how long the WebSocket has been continuously connected. A declining uptime gauge signals frequent reconnections, which may indicate network instability or server-side throttling.
Freshness ratio: A derived gauge that records 1.0 when the last observation was fresh and 0.0 when it was stale. Over time, this produces a time-series that Grafana can use to calculate uptime percentage.
Rate-limit handling: When TickDB returns HTTP 429, the exporter reads the Retry-After header and sleeps accordingly. This prevents the exporter from hammering the API during planned rate-limit windows.
Exponential backoff with jitter: Reconnection attempts use the formula min(base × 2^retry, max_delay) + uniform(0, 0.1 × delay). The jitter prevents thundering-herd scenarios where multiple exporters reconnect simultaneously after a shared outage.
4. Prometheus Alerting Rules
With the exporter running, Prometheus now has the following time-series to query:
market_data_request_duration_seconds_bucket— latency histogrammarket_data_staleness_seconds_bucket— staleness histogrammarket_data_connection_status— connection gaugemarket_data_freshness_ratio— freshness gaugemarket_data_errors_total— error counter by type
The alerting rules below define thresholds that distinguish between "acceptable variance" and "data source is degraded."
# alerts_latency.yaml
groups:
- name: market_data_latency
rules:
# P99 latency alert — triggered when 99th percentile exceeds 500ms
- alert: DataSourceHighLatencyP99
expr: |
histogram_quantile(0.99,
rate(market_data_request_duration_seconds_bucket{source="tickdb_rest"}[5m])
) > 0.5
for: 2m
labels:
severity: warning
team: data-engineering
annotations:
summary: "P99 latency exceeds 500ms on {{ $labels.source }}"
description: "P99 latency is {{ $value | printf \"%.3f\" }}s (threshold: 0.5s) for the last 5 minutes."
# Staleness alert — triggered when staleness exceeds threshold
- alert: DataSourceStaleData
expr: |
histogram_quantile(0.95,
rate(market_data_staleness_seconds_bucket{source="tickdb_rest"}[5m])
) > 0.5
for: 1m
labels:
severity: critical
team: data-engineering
annotations:
summary: "Data staleness exceeds 500ms on {{ $labels.source }}"
description: "P95 staleness is {{ $value | printf \"%.3f\" }}s. Data may be unusable for real-time strategies."
# Connection loss alert
- alert: DataSourceConnectionLost
expr: |
market_data_connection_status{source="tickdb_websocket"} == 0
for: 30s
labels:
severity: critical
team: data-engineering
annotations:
summary: "WebSocket connection lost on {{ $labels.source }}"
description: "Connection has been down for more than 30 seconds. Reconnection attempts may be failing."
# Freshness ratio alert — triggered when freshness drops below 95%
- alert: DataSourceLowFreshness
expr: |
avg_over_time(market_data_freshness_ratio{source="tickdb_rest"}[10m]) < 0.95
for: 5m
labels:
severity: warning
team: quant-research
annotations:
summary: "Freshness ratio below 95% on {{ $labels.source }}"
description: "Less than 95% of observations have been fresh over the last 10 minutes. Strategy signals may be degraded."
# Error rate spike alert
- alert: DataSourceErrorRateSpike
expr: |
rate(market_data_errors_total{source="tickdb_rest"}[5m])
/ ignoring(error_type) group_left
rate(market_data_request_duration_seconds_count{source="tickdb_rest"}[5m])
> 0.05
for: 3m
labels:
severity: critical
team: data-engineering
annotations:
summary: "Error rate exceeds 5% on {{ $labels.source }}"
description: "Error rate is {{ $value | printf \"%.2f\" }}% of total requests."
The for: 2m clause in Prometheus alerting is intentional. It prevents flapping alerts triggered by transient spikes — a single slow response should not page an on-call engineer. Two consecutive minutes of elevated latency is a real degradation.
5. Grafana Dashboard Configuration
The Grafana dashboard is organized into four rows, each addressing a distinct monitoring question.
Row 1: Latency Distribution
Three panels showing P50, P95, and P99 latency over time:
# P50 — median latency
histogram_quantile(0.50,
rate(market_data_request_duration_seconds_bucket{source="tickdb_rest"}[5m])
)
# P95 — 95th percentile latency
histogram_quantile(0.95,
rate(market_data_request_duration_seconds_bucket{source="tickdb_rest"}[5m])
)
# P99 — 99th percentile latency
histogram_quantile(0.99,
rate(market_data_request_duration_seconds_bucket{source="tickdb_rest"}[5m])
)
Use a line chart with three series. Set the P99 line to a brighter color (e.g., orange) and add horizontal threshold lines at 0.1s, 0.25s, and 0.5s so operators can see which threshold is being breached at a glance.
Row 2: Staleness Heatmap
A histogram panel showing the distribution of staleness deltas:
# Staleness histogram (rendered as heatmap)
sum by (le) (
rate(market_data_staleness_seconds_bucket{source="tickdb_rest"}[5m])
)
Use a green-to-red color scheme where green represents fresh data (< 200ms) and red represents stale data (> 500ms). This visual pattern makes it immediately obvious whether data quality is degrading over time.
Row 3: Connection Health
Two panels:
Connection status timeline:
market_data_connection_status{source="tickdb_websocket"}
Rendered as a stat panel with green (1.0) and red (0.0) states.
Connection uptime:
market_data_connection_uptime_seconds{source="tickdb_websocket"}
Rendered as a time-series with a gradient fill. A declining line signals instability.
Row 4: Freshness Ratio and Error Breakdown
Freshness ratio:
avg_over_time(market_data_freshness_ratio{source="tickdb_rest"}[$__interval])
Rendered as a gauge panel with thresholds at 0.95 (green), 0.90 (yellow), and 0.85 (red).
Error breakdown by type:
sum by (error_type) (
rate(market_data_errors_total{source="tickdb_rest"}[5m])
)
Rendered as a pie chart showing the proportion of each error type (timeout, rate_limited, connection_error, http_4xx).
6. Integrating with TickDB: Why the Exporter Works for This Data Source
TickDB's REST API and WebSocket feed are well-suited to this monitoring approach for three reasons.
First, every kline response includes a server-side ts timestamp in milliseconds. This gives you an authoritative staleness reference without requiring you to guess whether the data is fresh. Other market data vendors may not expose this field, forcing you to estimate staleness from round-trip time alone — which is a proxy, not a measurement.
Second, TickDB's WebSocket supports native ping/pong heartbeat frames. The Python exporter uses ping_interval and ping_timeout to detect dead connections proactively, before the TCP keepalive timeout triggers. This reduces the window during which stale data might silently accumulate in a strategy's state.
Third, TickDB's rate-limit response includes a Retry-After header, which the exporter reads and respects. This is essential for backtest data fetches where you might be requesting thousands of historical candles in a loop — a scenario where you are likely to hit rate limits intentionally.
For users who are building systematic strategies with TickDB, the combination of the /v1/market/kline endpoint for historical data and the WebSocket stream for real-time ingestion, this exporter provides the observability layer that catches data quality issues before they invalidate a backtest or corrupt live signals.
7. Deployment Guide
Local Development
# Set environment variables
export TICKDB_API_KEY="your_api_key_here"
export TICKDB_BASE_URL="https://api.tickdb.ai"
export TICKDB_STALENESS_THRESHOLD_SECONDS="0.5"
export MONITORED_SYMBOLS="AAPL.US,MSFT.US,NVDA.US"
# Install dependencies
pip install prometheus_client requests websocket-client
# Run the exporter
python latency_exporter.py
Prometheus will scrape http://localhost:8000/metrics every 15 seconds by default.
Docker Deployment
FROM python:3.11-slim
WORKDIR /app
RUN pip install prometheus_client requests websocket-client
COPY latency_exporter.py /app/
ENV TICKDB_API_KEY=""
ENV TICKDB_STALENESS_THRESHOLD_SECONDS="0.5"
ENV MONITORED_SYMBOLS="AAPL.US,MSFT.US,NVDA.US"
ENV REST_POLL_INTERVAL="10.0"
ENV METRICS_PORT="8000"
CMD ["python", "latency_exporter.py"]
# docker-compose.yml
version: "3.8"
services:
latency-exporter:
build: .
environment:
TICKDB_API_KEY: "${TICKDB_API_KEY}"
TICKDB_STALENESS_THRESHOLD_SECONDS: "0.5"
MONITORED_SYMBOLS: "AAPL.US,MSFT.US,NVDA.US"
ports:
- "8000:8000"
restart: unless-stopped
prometheus:
image: prom/prometheus:v2.47.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ./alerts_latency.yaml:/etc/prometheus/alerts_latency.yaml
ports:
- "9090:9090"
restart: unless-stopped
grafana:
image: grafana/grafana:10.1.0
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_PASSWORD}"
volumes:
- ./grafana/dashboards:/var/lib/grafana/dashboards
restart: unless-stopped
# prometheus.yml
global:
scrape_interval: 15s
rule_files:
- "alerts_latency.yaml"
scrape_configs:
- job_name: "latency-exporter"
static_configs:
- targets: ["latency-exporter:8000"]
Recommended Alert Routing
| Alert | Severity | Routing |
|---|---|---|
| DataSourceConnectionLost | Critical | PagerDuty → on-call engineer |
| DataSourceStaleData | Critical | PagerDuty → on-call engineer + quant team Slack |
| DataSourceErrorRateSpike | Critical | PagerDuty → on-call engineer |
| DataSourceHighLatencyP99 | Warning | Slack → data-engineering channel |
| DataSourceLowFreshness | Warning | Slack → quant-research channel |
8. Closing
A monitoring dashboard that nobody watches is a liability, not an asset. The framework built here is designed to produce signals that are actionable: P99 thresholds calibrated to your strategy's latency tolerance, staleness alerts that fire before data quality degrades far enough to corrupt a backtest, and freshness ratios that give quant researchers a single number to check before trusting a dataset.
The exporter itself is intentionally generic. The Prometheus metrics, Grafana panels, and alerting rules described here apply equally to any REST or WebSocket market data source. Swap the TickDB endpoints for another vendor's API, update the source label in the metric definitions, and the dashboard adapts without structural changes.
If you are running systematic strategies on TickDB and want visibility into your data pipeline's health — or if you are evaluating whether TickDB's latency characteristics are suitable for your strategy's requirements — the combination of this exporter with the Grafana dashboard gives you the instrumentation to answer those questions with data rather than guesswork.
Next Steps
If you want to monitor your TickDB data pipeline right now:
- Sign up at tickdb.ai and generate an API key (free tier available, no credit card required)
- Clone the exporter code from this article
- Set
TICKDB_API_KEYand runpython latency_exporter.py - Import the Grafana dashboard JSON (available in the TickDB documentation)
If you need historical OHLCV data for backtesting alongside your live monitoring setup, TickDB provides 10+ years of cleaned US equity kline data via the /v1/market/kline endpoint. The same exporter monitors your live feed; your backtest pipeline uses the historical endpoint — both are instrumented with the same staleness tracking.
If you use AI coding assistants for quant strategy development, search for the tickdb-market-data SKILL in your AI tool's marketplace to get direct integration with TickDB endpoints inside your development environment.
If you are building a team observability stack and need enterprise-grade data retention or SLA-backed uptime guarantees for your market data feed, reach out to enterprise@tickdb.ai for institutional plans.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. The monitoring framework described here is an engineering tool for data quality observability and does not generate trading signals.