Financial Data Is Different
Building on a financial data API is not the same as calling a weather API. Stale data can cost real money. A missed rate limit can leave your bot blind during a volatile move. An unhandled error can turn a small loss into a large one. These patterns are not optional.
1. Retry with Exponential Backoff
Network blips happen. The difference between a production-grade system and a weekend project is retry logic.
import requests
import time
import logging
logger = logging.getLogger(__name__)
def api_call_with_retry(
url: str,
headers: dict,
params: dict,
max_retries: int = 3,
base_delay: float = 1.0,
) -> dict:
"""Make an API call with exponential backoff retry."""
for attempt in range(max_retries + 1):
try:
resp = requests.get(url, headers=headers, params=params, timeout=10)
if resp.status_code == 429: # Rate limited
retry_after = int(resp.headers.get("Retry-After", base_delay * 2))
logger.warning(f"Rate limited. Waiting {retry_after}s")
time.sleep(retry_after)
continue
if resp.status_code >= 500: # Server error -- retry
raise requests.exceptions.HTTPError(f"Server error: {resp.status_code}")
resp.raise_for_status()
return resp.json()
except (requests.exceptions.ConnectionError,
requests.exceptions.Timeout,
requests.exceptions.HTTPError) as e:
if attempt == max_retries:
logger.error(f"Failed after {max_retries} retries: {e}")
raise
delay = base_delay * (2 ** attempt)
logger.warning(f"Attempt {attempt + 1} failed: {e}. Retrying in {delay}s")
time.sleep(delay)
# Usage
data = api_call_with_retry(
"https://tickatlas.com/v1/indicator",
headers={"X-API-Key": "YOUR_KEY"},
params={"symbol": "EURUSD", "indicator": "RSI_14", "timeframe": "H4"},
) 2. Respect Rate Limits
import threading
import time
class RateLimiter:
"""Token bucket rate limiter for API calls."""
def __init__(self, calls_per_minute: int):
self.calls_per_minute = calls_per_minute
self.interval = 60.0 / calls_per_minute
self.last_call = 0.0
self.lock = threading.Lock()
def wait(self):
with self.lock:
now = time.monotonic()
elapsed = now - self.last_call
if elapsed < self.interval:
time.sleep(self.interval - elapsed)
self.last_call = time.monotonic()
# Create a limiter matching your plan
limiter = RateLimiter(calls_per_minute=120) # Starter plan
def safe_api_call(url: str, headers: dict, params: dict) -> dict:
limiter.wait() # Block until we can make a call
return api_call_with_retry(url, headers, params) 3. Validate Response Data
Never trust API responses blindly. Validate that the data makes sense before acting on it.
from datetime import datetime, timezone, timedelta
def validate_indicator_response(data: dict, symbol: str) -> bool:
"""Validate that indicator data is fresh and reasonable."""
if not data.get("success"):
return False
inner = data.get("data", {})
# Check symbol matches
if inner.get("symbol") != symbol:
logger.error(f"Symbol mismatch: expected {symbol}, got {inner.get('symbol')}")
return False
# Check freshness (server_time is the time of the data, ISO-8601 UTC)
ts = inner.get("server_time")
if ts:
data_time = datetime.fromisoformat(ts.replace("Z", "+00:00"))
age = datetime.now(timezone.utc) - data_time
if age > timedelta(hours=2):
logger.warning(f"Stale data: {age} old for {symbol}")
return False
# Check the value exists and is numeric
value = inner.get("value")
if not isinstance(value, (int, float)):
logger.error(f"Non-numeric value for {inner.get('indicator')}: {value}")
return False
return True 4. Build a Resilient Client Class
class TickAtlasClient:
"""Production-grade API client with retry, rate limiting, and validation."""
def __init__(self, api_key: str, rate_limit: int = 60):
self.base_url = "https://tickatlas.com/v1"
self.headers = {"X-API-Key": api_key}
self.limiter = RateLimiter(rate_limit)
def get_indicator(self, symbol: str, indicator: str, timeframe: str) -> dict:
self.limiter.wait()
data = api_call_with_retry(
f"{self.base_url}/indicator",
self.headers,
{"symbol": symbol, "indicator": indicator, "timeframe": timeframe},
)
if not validate_indicator_response(data, symbol):
raise ValueError(f"Invalid response for {symbol}/{indicator}")
return data["data"]
def get_ohlcv(self, symbol: str, timeframe: str, limit: int = 50) -> list:
self.limiter.wait()
data = api_call_with_retry(
f"{self.base_url}/ohlcv",
self.headers,
{"symbol": symbol, "timeframe": timeframe, "limit": limit},
)
return data["data"]["candles"]
def get_spread(self, symbol: str) -> dict:
self.limiter.wait()
data = api_call_with_retry(
f"{self.base_url}/spread",
self.headers,
{"symbol": symbol},
)
return data["data"]
# Usage
client = TickAtlasClient("YOUR_API_KEY", rate_limit=60)
rsi = client.get_indicator("EURUSD", "RSI_14", "H4")
print(f"RSI: {rsi['value']}") 5. Log Everything
Log every API call and its latency
When something goes wrong at 2 AM, logs are the only way to reconstruct what happened. Include response times so you can spot degradation.
Log rate limit headers
Track X-RateLimit-Remaining to proactively slow down before hitting limits rather than reacting to 429 errors.
Use structured logging
JSON-formatted logs are searchable. Include symbol, indicator, timeframe, response time, and status code in every log entry.
Run this against live data.
Every account starts pay-as-you-go with $2.50 of credit and no card. Paste the key into the samples above and the requests work unchanged.