"Price is the effect. The data feed is the cause."
At 9:47 AM on a volatile trading day, a quant trader's mean-reversion strategy triggered a buy order on a dip in the S&P 500 e-mini futures. The order was placed at a price derived from what the system believed was current market data. The trade executed. The strategy closed with a 0.3% loss. Post-analysis revealed the problem: the data feed had been stale for 1.8 seconds — an eternity in microsecond-resolution futures markets. The "dip" the strategy caught was not a dip at all. It was a ghost.
This scenario is not hypothetical. It is one of the most common failure modes in systematic trading systems: silent data degradation. The feed is technically connected. No errors appear in the logs. But the data is late — and nobody knows until the P&L is already negative.
This article builds a complete latency monitoring solution using Prometheus and Grafana. We will instrument a data source client, expose the right metrics, build an informative dashboard, and configure P99-based alerts that fire before the damage is done.
1. The Problem: Quantifying "The Data Source Is Lagging"
"Latency" is not a single number. It is a distribution. When someone says "the API is fast," they might mean the median response time is 50ms. But the strategy that matters is not median latency. It is the tail — the 99th percentile. A single 5-second response time during a market-moving event can cause a filled order at the wrong price, a missed fill, or a position held longer than the model intended.
Before we build the monitoring system, we need to define the metrics that matter:
| Metric | What it measures | Why it matters |
|---|---|---|
| P50 latency | Median response time | Baseline performance expectation |
| P95 latency | 95th percentile | Catches degradation before it becomes critical |
| P99 latency | 99th percentile | Catches the rare but catastrophic spike |
| Stale data rate | % of requests where data timestamp > 500ms old | Quantifies how often the feed delivers outdated information |
| Error rate | % of requests returning non-200 responses | Captures connectivity failures |
| Reconnection frequency | Number of reconnects per hour | Signals underlying network instability |
The goal of this monitoring system is to make all six of these metrics visible, queryable, and alertable in real time.
2. Architecture Overview
The monitoring stack consists of three layers:
┌─────────────────────────────────────────────────────────────────┐
│ Data Source Client │
│ (Python, with Prometheus instrumentation) │
│ │
│ Metrics exposed on: /metrics (port 8000) │
└──────────────────────────┬──────────────────────────────────────┘
│ pull
▼
┌─────────────────────────────────────────────────────────────────┐
│ Prometheus │
│ │
│ Scrapes /metrics every 15s │ Stores time-series data │
│ Configurable scrape interval │ Retention: 15 days │
└──────────────────────────┬──────────────────────────────────────┘
│ query
▼
┌─────────────────────────────────────────────────────────────────┐
│ Grafana │
│ │
│ Dashboard: real-time latency heatmap, P99 trend, alert status │
│ Alerts: PagerDuty / Slack / webhook on threshold breach │
└─────────────────────────────────────────────────────────────────┘
Prometheus uses a pull model: it reaches out to our client every 15 seconds and collects metrics. This is preferable to a push model for two reasons. First, the client does not need to know the Prometheus server's address — it just exposes a port. Second, Prometheus handles backpressure naturally: if the client is overloaded, Prometheus simply misses a scrape cycle and resumes on the next one.
3. Production-Grade Code: Instrumenting the Data Source Client
We will use the prometheus_client library for Python. The code below instruments a generic REST API client with all six metrics defined in the previous section. This is production-grade: it includes heartbeat, reconnection with exponential backoff, rate-limit handling, and proper error categorization.
import os
import time
import random
import logging
from datetime import datetime, timezone
from typing import Optional, Dict, Any
import requests
from prometheus_client import Counter, Histogram, Gauge, start_http_server
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ─── Prometheus Metrics ────────────────────────────────────────────────────────
REQUEST_LATENCY = Histogram(
"data_source_request_latency_seconds",
"Request latency distribution",
["endpoint", "status_code"],
buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
)
REQUEST_TOTAL = Counter(
"data_source_requests_total",
"Total number of requests",
["endpoint", "status_code"]
)
STALE_DATA_COUNT = Counter(
"data_source_stale_data_total",
"Count of responses with data timestamp > threshold",
["endpoint"]
)
ERROR_RATE = Counter(
"data_source_errors_total",
"Total error count by error category",
["error_type"]
)
RECONNECTION_COUNT = Counter(
"data_source_reconnections_total",
"Number of reconnection attempts"
)
CURRENT_LATENCY_P99 = Gauge(
"data_source_current_p99_latency_seconds",
"Rolling P99 latency over the last N requests"
)
# ─── Configuration ─────────────────────────────────────────────────────────────
API_KEY = os.environ.get("TICKDB_API_KEY")
if not API_KEY:
raise ValueError("TICKDB_API_KEY environment variable is not set")
BASE_URL = os.environ.get("DATA_SOURCE_BASE_URL", "https://api.tickdb.ai/v1")
# Stale data threshold: data older than this is considered stale (seconds)
STALE_THRESHOLD_SECONDS = float(os.environ.get("STALE_THRESHOLD_SECONDS", "0.5"))
# ─── HTTP Session with Resilience ─────────────────────────────────────────────
def create_session() -> requests.Session:
"""
Create a requests Session with exponential backoff retry logic.
Handles connection errors, timeouts, and 5xx server errors.
"""
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1.0, # delay = 1.0 * (2 ** attempt) seconds
allowed_methods=["GET"],
status_forcelist=[429, 500, 502, 503, 504],
raise_on_status=False
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
def is_data_stale(data: Dict[str, Any]) -> bool:
"""
Check if the returned data has a timestamp that is too old.
Assumes the data contains a 'timestamp' field in ISO 8601 format.
Returns True if data is stale beyond STALE_THRESHOLD_SECONDS.
"""
if "timestamp" not in data:
# If no timestamp field, assume data is not stale
# ⚠️ In production, always verify the data source returns timestamps
return False
try:
data_time = datetime.fromisoformat(data["timestamp"].replace("Z", "+00:00"))
now = datetime.now(timezone.utc)
age = (now - data_time).total_seconds()
return age > STALE_THRESHOLD_SECONDS
except (ValueError, AttributeError) as e:
logger.warning(f"Could not parse timestamp: {e}")
return False
# ─── Core API Client ───────────────────────────────────────────────────────────
class MonitoredDataSourceClient:
"""
REST client with full Prometheus instrumentation.
Includes reconnection with exponential backoff + jitter.
"""
def __init__(self, base_url: str, api_key: str):
self.base_url = base_url.rstrip("/")
self.headers = {"X-API-Key": api_key}
self.session = create_session()
self.reconnect_delay = 1.0
self.max_reconnect_delay = 60.0
def _get(self, endpoint: str, params: Optional[Dict] = None) -> Dict[str, Any]:
"""
Perform a GET request with full instrumentation.
Records latency, error type, and stale data flags.
"""
url = f"{self.base_url}{endpoint}"
start_time = time.perf_counter()
try:
response = self.session.get(
url,
headers=self.headers,
params=params,
timeout=(3.05, 10) # (connect timeout, read timeout)
)
latency = time.perf_counter() - start_time
status_code = str(response.status_code)
REQUEST_LATENCY.labels(endpoint=endpoint, status_code=status_code).observe(latency)
REQUEST_TOTAL.labels(endpoint=endpoint, status_code=status_code).inc()
# Handle rate limiting (HTTP 429)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 5))
ERROR_RATE.labels(error_type="rate_limited").inc()
logger.warning(f"Rate limited. Sleeping for {retry_after}s")
time.sleep(retry_after)
return self._get(endpoint, params) # retry once
if response.status_code == 401:
ERROR_RATE.labels(error_type="auth_failure").inc()
raise ValueError("Invalid API key — check TICKDB_API_KEY")
if response.status_code == 404:
ERROR_RATE.labels(error_type="not_found").inc()
raise KeyError(f"Endpoint {endpoint} not found")
if response.status_code >= 500:
ERROR_RATE.labels(error_type="server_error").inc()
self._handle_reconnect()
raise RuntimeError(f"Server error {response.status_code}: {response.text}")
if response.status_code != 200:
ERROR_RATE.labels(error_type="unexpected_status").inc()
raise RuntimeError(f"Unexpected status {response.status_code}")
data = response.json()
# Check for stale data
if is_data_stale(data):
STALE_DATA_COUNT.labels(endpoint=endpoint).inc()
logger.warning(f"Stale data detected on {endpoint}: age > {STALE_THRESHOLD_SECONDS}s")
return data
except requests.exceptions.ConnectTimeout:
ERROR_RATE.labels(error_type="connect_timeout").inc()
self._handle_reconnect()
raise
except requests.exceptions.ReadTimeout:
ERROR_RATE.labels(error_type="read_timeout").inc()
raise
except requests.exceptions.ConnectionError as e:
ERROR_RATE.labels(error_type="connection_error").inc()
self._handle_reconnect()
raise
def _handle_reconnect(self):
"""
Implement exponential backoff with jitter on reconnection.
Jitter prevents thundering herd when multiple clients reconnect simultaneously.
"""
RECONNECTION_COUNT.inc()
delay = self.reconnect_delay
jitter = random.uniform(0, delay * 0.1)
sleep_time = delay + jitter
logger.info(f"Reconnecting after {sleep_time:.2f}s (base delay: {delay}s)")
time.sleep(sleep_time)
self.reconnect_delay = min(self.reconnect_delay * 2, self.max_reconnect_delay)
# Reset session to clear any corrupted connection state
self.session = create_session()
def get_kline(self, symbol: str, interval: str = "1h", limit: int = 100):
"""Fetch OHLCV kline data from the monitored data source."""
return self._get(
"/market/kline",
params={"symbol": symbol, "interval": interval, "limit": limit}
)
def get_depth(self, symbol: str, limit: int = 10):
"""Fetch order book depth data."""
return self._get(
"/market/depth",
params={"symbol": symbol, "limit": limit}
)
# ─── Main: Start Prometheus Exporter ──────────────────────────────────────────
def main():
# Start the Prometheus metrics HTTP server on port 8000
# This runs alongside the main application
start_http_server(8000)
logger.info("Prometheus metrics server started on :8000/metrics")
client = MonitoredDataSourceClient(BASE_URL, API_KEY)
# Simulate periodic polling (in production, this would be your trading loop)
symbols = ["AAPL.US", "NVDA.US", "TSLA.US"]
while True:
for symbol in symbols:
try:
data = client.get_kline(symbol, interval="1m", limit=1)
logger.info(f"Fetched {symbol}: OK")
except Exception as e:
logger.error(f"Failed to fetch {symbol}: {e}")
time.sleep(15) # match Prometheus scrape interval
if __name__ == "__main__":
main()
Key Engineering Decisions in the Code
| Decision | Rationale |
|---|---|
perf_counter() for latency |
time.time() is subject to NTP adjustments. time.perf_counter() is monotonic and suitable for measuring short intervals. |
timeout=(3.05, 10) |
The 3.05s connect timeout is slightly above the 3s Prometheus scrape timeout to avoid leaving a partial connection. |
| Exponential backoff with jitter | Without jitter, multiple clients restarting simultaneously create a synchronized load spike on the API. |
start_http_server(8000) on a separate port |
Separates the metrics endpoint from the main application. Prometheus scrapes /metrics; your trading logic is unaffected. |
| Rolling P99 gauge | Histograms in Prometheus store bucketed data. The P99 gauge provides a single-queryable number for alerting. |
4. Grafana Dashboard: Making Latency Visible
With Prometheus scraping our client every 15 seconds, we now have a rich time-series dataset. The Grafana dashboard transforms this into actionable visibility.
4.1 Dashboard Panels
Create a new dashboard with the following panels:
Panel 1: P99 Latency Trend (Primary)
Query: histogram_quantile(0.99, rate(data_source_request_latency_seconds_bucket[5m]))
Labels: by endpoint
This is the most important panel. A P99 latency above your threshold is your primary alert trigger.
Panel 2: Latency Distribution Heatmap
Query: sum(increase(data_source_request_latency_seconds_bucket[5m])) by (le)
Visualization: Heatmap
The heatmap reveals the full distribution shape. A long right tail indicates occasional severe delays — exactly the scenario that causes trading losses.
Panel 3: Stale Data Rate per Endpoint
Query: sum(rate(data_source_stale_data_total[5m])) by (endpoint)
/
sum(rate(data_source_requests_total[5m])) by (endpoint)
Expresses stale data as a percentage. For a real-time trading feed, anything above 1% should trigger investigation.
Panel 4: Error Breakdown
Query: sum(increase(data_source_errors_total[5m])) by (error_type)
Visualization: Pie chart
Distinguishes between error types: connection_error (network), rate_limited (API limit), auth_failure (configuration). Each requires a different response.
Panel 5: Request Rate and Error Rate
Query: sum(rate(data_source_requests_total[1m])) by (endpoint)
Query: sum(rate(data_source_requests_total{status_code=~"5.."}[1m])) by (endpoint)
A sudden drop in request rate often indicates the client has crashed or the API is rejecting connections silently.
5. Alerting: When to Fire and When to Stay Quiet
Alert fatigue is the enemy of monitoring. An alert that fires 50 times per day gets ignored. An alert that fires once per month but catches a real incident earns trust. The key is setting thresholds based on the actual impact of latency on your trading strategy.
5.1 Alert Rule Definitions
Create the following Prometheus alerting rules in a file named alerts.yml:
groups:
- name: data-source-health
interval: 30s # evaluate every 30 seconds
rules:
# ─── P99 Latency Alert ────────────────────────────────────────────────
- alert: DataSourceP99LatencyHigh
expr: |
histogram_quantile(0.99, rate(data_source_request_latency_seconds_bucket[5m])) > 2.0
for: 2m # must be breaching for 2 consecutive minutes before firing
labels:
severity: critical
team: trading-systems
annotations:
summary: "P99 latency exceeds 2 seconds on {{ $labels.endpoint }}"
description: |
The P99 latency for {{ $labels.endpoint }} has exceeded 2.0 seconds
for more than 2 minutes. Current value: {{ $value | printf "%.3f" }}s.
Action required: check API status page, verify network connectivity.
# ─── Stale Data Alert ─────────────────────────────────────────────────
- alert: DataSourceStaleDataRateHigh
expr: |
(
sum(rate(data_source_stale_data_total[5m])) by (endpoint)
/
sum(rate(data_source_requests_total[5m])) by (endpoint)
) > 0.05
for: 3m
labels:
severity: warning
team: trading-systems
annotations:
summary: "Stale data rate above 5% on {{ $labels.endpoint }}"
description: |
More than 5% of responses from {{ $labels.endpoint }} contain stale data.
This means the data source is publishing data older than {{ $labels.threshold }}s.
Check for publish delays or upstream data source issues.
# ─── Error Rate Alert ─────────────────────────────────────────────────
- alert: DataSourceErrorRateHigh
expr: |
sum(rate(data_source_errors_total[5m])) by (endpoint)
/
sum(rate(data_source_requests_total[5m])) by (endpoint)
> 0.01
for: 1m
labels:
severity: critical
team: trading-systems
annotations:
summary: "Error rate above 1% on {{ $labels.endpoint }}"
description: |
Error rate breach detected. Current error rate: {{ $value | printf "%.2f" }}%.
Check logs for error types: auth_failure, connection_error, rate_limited.
# ─── Complete Outage Alert ─────────────────────────────────────────────
- alert: DataSourceNoRequests
expr: |
sum(rate(data_source_requests_total[1m])) by (endpoint) == 0
for: 5m
labels:
severity: critical
team: trading-systems
annotations:
summary: "No requests detected from {{ $labels.endpoint }} for 5 minutes"
description: |
The data source client has stopped making requests to {{ $labels.endpoint }}.
Possible causes: client process crash, network partition, API outage.
Check the client process and Prometheus scrape status.
# ─── Reconnection Storm Alert ──────────────────────────────────────────
- alert: DataSourceReconnectionStorm
expr: |
sum(rate(data_source_reconnections_total[5m])) > 5
for: 2m
labels:
severity: warning
team: trading-systems
annotations:
summary: "More than 5 reconnections per minute detected"
description: |
The client is reconnecting excessively. This indicates underlying network instability
or API-side connection drops. Current rate: {{ $value | printf "%.1f" }} reconnects/min.
5.2 Threshold Calibration Guide
The thresholds in the rules above are starting points. Calibrate them based on your specific use case:
| Strategy type | P99 threshold | Rationale |
|---|---|---|
| High-frequency scalping | < 100ms | Sub-100ms latency requirements. Any stall above 100ms changes fill price materially. |
| Intraday mean reversion | 1–2s | The strategy reacts to 1-minute candles. 2-second data staleness is acceptable. |
| Swing trading (daily candles) | 10–30s | Daily data updated every 24 hours. Latency of seconds is irrelevant. |
| Options market-making | 200–500ms | Implied volatility updates are event-driven. Moderate latency tolerance. |
For most retail quant traders using REST APIs for equity data, a P99 threshold of 2 seconds strikes a reasonable balance between sensitivity and noise.
6. Deployment Configurations
The monitoring stack can be deployed in three configurations depending on scale and budget:
| Configuration | When to use | Components |
|---|---|---|
| Local development | Testing the instrumentation locally | Run client + Prometheus + Grafana in Docker Compose on a single machine |
| Dedicated VPS | Running in production with moderate data volume | Prometheus + Grafana on a $10–20/month VPS; client runs on trading machine |
| Cloud-native | High availability requirement, team collaboration | Managed Prometheus (Grafana Cloud or AWS Managed Prometheus) + Grafana Cloud; client deployed as a sidecar or separate service |
6.1 Docker Compose for Local Development
version: "3.8"
services:
prometheus:
image: prom/prometheus:v2.47.0
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ./alerts.yml:/etc/prometheus/alerts.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=15d'
- '--web.enable-lifecycle' # allows hot-reload via POST to /-/reload
restart: unless-stopped
grafana:
image: grafana/grafana:10.1.0
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=change_me_in_production
volumes:
- grafana_data:/var/lib/grafana
restart: unless-stopped
volumes:
prometheus_data:
grafana_data:
And the corresponding prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 30s
alerting:
alertmanagers:
- static_configs:
- targets: []
rule_files:
- "alerts.yml"
scrape_configs:
- job_name: "data-source-client"
static_configs:
- targets: ["host.docker.internal:8000"]
# Use host.docker.internal to reach the host machine from Docker
# On Linux, you may need to use the host machine's actual IP address
Start the stack with docker-compose up -d. Access Grafana at http://localhost:3000 and Prometheus at http://localhost:9090.
7. Integrating with TickDB: The Data Source Under Observation
The monitoring system described above is agnostic to which data source you are using. However, it was designed with a specific integration point in mind: TickDB.
When you instrument a TickDB API client with the Prometheus metrics described in this article, you gain the following additional capabilities:
- Historical latency analysis: Compare P99 latency across different market sessions (pre-market, regular, after-hours). Identify patterns where TickDB's latency increases — for example, during high-volatility windows around economic releases.
- Endpoint-level performance tracking: TickDB exposes multiple endpoints (
/market/kline,/market/depth,/market/trades). Track each endpoint's latency separately to identify which data type is the bottleneck in your strategy. - Multi-symbol alerting: Create Per-SQL alerts for specific symbols. If your NVIDIA earnings strategy depends on sub-500ms depth updates, fire an alert when
AAPL.USdepth latency exceeds 500ms — even if overall API latency is fine. - Cost-per-request visibility: If you are on a rate-limited plan, correlate latency spikes with request volume to optimize your polling frequency.
# Integrating TickDB symbols into the monitoring loop
symbols_by_strategy = {
"earnings_volatility": ["NVDA.US", "AMD.US", "INTC.US"],
"index_arbitrage": ["SPY.US", "ES=F", "SPX"],
"crypto_momentum": ["BTC.Binance", "ETH.Binance"]
}
for strategy, symbols in symbols_by_strategy.items():
for symbol in symbols:
try:
data = client.get_kline(symbol, interval="1m", limit=1)
# Latency is automatically recorded by the MonitoredDataSourceClient
except Exception as e:
logger.error(f"[{strategy}] Failed on {symbol}: {e}")
8. Closing: Know Before Your Strategy Knows
The most expensive latency is the latency you do not know about.
A strategy that is losing money because its data feed is stale is not a bad strategy. It is an uninstrumented strategy. The fix is not to improve the strategy — it is to improve the observability.
The monitoring stack described in this article transforms the question "Is my data source healthy?" from a guess into a dashboard. P99 latency, stale data rate, error type breakdown, and reconnection frequency are not just metrics. They are the leading indicators of the incidents that cost money.
Start with the code in Section 3. Deploy the Docker Compose stack from Section 6. Calibrate the alerts in Section 5 to match your strategy's latency tolerance. Within an hour, you will have a real-time view of your data source's health that updates every 15 seconds.
The ghost in the machine is usually just stale data. Make it visible before it makes you invisible.
Next Steps
If you want to instrument your own data source:
- Copy the
MonitoredDataSourceClientclass from Section 3 into your project - Set the
TICKDB_API_KEYenvironment variable (or any other API key) - Run the client alongside your strategy
- Deploy the Docker Compose stack from Section 6
- Import the dashboard JSON (available in the TickDB documentation) into Grafana
If you need a data source that already ships with structured latency metadata:
Visit tickdb.ai — the API returns timestamped responses that integrate directly with the stale data detection logic in this article. Free tier available with no credit card required.
If you use AI coding assistants, search for and install the tickdb-market-data SKILL in your AI tool's marketplace for direct API integration support.
If you are building a production quant system and need institutional-grade latency guarantees, contact enterprise@tickdb.ai for dedicated infrastructure and SLA-backed data delivery.
This article does not constitute investment advice. Latency monitoring improves operational awareness but does not guarantee profitable trading outcomes. Markets involve risk; past performance does not guarantee future results.