been noticing market data latency creeping up.
average fetch time: 180ms from polygon API.
slowing down entry execution.
the problem #
every time algo needs current price:
- hit polygon API over HTTPS
- parse JSON response
- extract OHLCV data
- calculate indicators
- return to strategy
180ms average. sometimes 400ms during market open.
unacceptable for intraday strategies.
the solution: redis caching #
redis = in-memory key-value store.
sub-millisecond reads. perfect for real-time data caching.
implementation #
import redis
import json
from datetime import datetime, timedelta
import aioredis
from typing import Optional, Dict, Any
class MarketDataCache:
"""Redis-backed market data caching layer"""
def __init__(self, redis_url: str = "redis://localhost:6379"):
self.redis = aioredis.from_url(
redis_url,
encoding="utf-8",
decode_responses=True
)
self.ttl_seconds = 1 # Cache for 1 second (real-time data)
async def get_quote(self, symbol: str) -> Optional[Dict[str, Any]]:
"""Get cached quote or None if expired/missing"""
cache_key = f"quote:{symbol}"
cached = await self.redis.get(cache_key)
if cached:
data = json.loads(cached)
# Check if cache is stale
cached_time = datetime.fromisoformat(data['timestamp'])
age = (datetime.now() - cached_time).total_seconds()
if age < self.ttl_seconds:
return data
return None
async def set_quote(self, symbol: str, quote_data: Dict[str, Any]):
"""Cache quote data with TTL"""
cache_key = f"quote:{symbol}"
# Add timestamp
quote_data['timestamp'] = datetime.now().isoformat()
# Store with expiration
await self.redis.setex(
cache_key,
self.ttl_seconds,
json.dumps(quote_data)
)
async def get_or_fetch(
self,
symbol: str,
fetch_func: callable
) -> Dict[str, Any]:
"""Get from cache or fetch from API"""
# Try cache first
cached = await self.get_quote(symbol)
if cached:
return cached
# Cache miss - fetch from API
fresh_data = await fetch_func(symbol)
# Cache for next time
await self.set_quote(symbol, fresh_data)
return fresh_data
class PolygonAPI:
"""Market data API with caching"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.polygon.io"
self.cache = MarketDataCache()
self.session = aiohttp.ClientSession()
async def get_last_quote(self, symbol: str) -> Dict[str, Any]:
"""Get last quote with caching"""
return await self.cache.get_or_fetch(
symbol,
self._fetch_quote_from_api
)
async def _fetch_quote_from_api(self, symbol: str) -> Dict[str, Any]:
"""Fetch directly from Polygon API"""
url = f"{self.base_url}/v2/last/trade/{symbol}"
params = {"apiKey": self.api_key}
async with self.session.get(url, params=params) as resp:
data = await resp.json()
return {
'symbol': symbol,
'price': data['results']['p'],
'size': data['results']['s'],
'timestamp': data['results']['t']
}
# Usage in strategy
class MeanReversionStrategy:
def __init__(self):
self.api = PolygonAPI(api_key=os.getenv("POLYGON_API_KEY"))
async def check_entry_signal(self, symbol: str) -> bool:
# This now hits cache 99% of the time
quote = await self.api.get_last_quote(symbol)
current_price = quote['price']
# ... rest of strategy logic
the architecture #
before:
Strategy → Polygon API (180ms) → Parse → Return
after:
Strategy → Redis Cache (0.3ms) → Return [cache hit]
Strategy → Redis Cache → Polygon API (180ms) → Cache → Return [cache miss]
performance improvement #
before redis:
- average latency: 180ms
- worst case: 400ms during market open
- requests per strategy check: 3-5 API calls
after redis:
- average latency: 0.3ms (cache hit)
- worst case: 180ms (cache miss on first request)
- cache hit rate: 99.2%
- requests per strategy check: 0.05 API calls (rest from cache)
net improvement: 60% reduction in average data latency.
cache strategy #
TTL = 1 second for real-time quotes.
why 1 second?
- market data updates ~10 times per second
- 1 second staleness is acceptable for my strategies
- balances freshness vs cache hit rate
for different data types:
- real-time quotes: 1 second TTL
- daily bars: 60 seconds TTL
- historical data: 1 hour TTL (rarely changes)
infrastructure details #
redis deployment:
- running on same dell server as algos
- 1GB memory allocated
- persistence disabled (cache-only, data is ephemeral)
- single instance (no cluster needed yet)
monitoring:
- grafana dashboard tracks cache hit rate
- alert if hit rate drops below 95%
- tracks redis memory usage
cost savings #
polygon API pricing:
- $199/month for unlimited plan
- but rate limited to 5 requests/second
before:
- hitting rate limit during market open
- had to throttle strategy checks
- slower execution
after:
- 99.2% cache hit rate
- API calls dropped from ~1,200/min to ~10/min
- zero rate limit issues
- faster execution
real trading impact #
tested monday morning (market open 6:30am PST).
entry speed improvement:
- before: 180-400ms data fetch + 50ms decision logic = 230-450ms total
- after: 0.3ms data fetch + 50ms decision logic = 50ms total
got filled 200ms faster on average.
in fast-moving markets, 200ms matters.
redis installation #
stupid simple on debian:
apt-get install redis-server
systemctl enable redis-server
systemctl start redis-server
python client:
pip install aioredis
monitoring cache performance #
class CacheMonitor:
"""Track cache performance metrics"""
def __init__(self, cache: MarketDataCache):
self.cache = cache
self.hits = 0
self.misses = 0
async def get_stats(self) -> Dict[str, Any]:
"""Get cache statistics"""
total = self.hits + self.misses
hit_rate = (self.hits / total * 100) if total > 0 else 0
# Get redis memory stats
info = await self.cache.redis.info('memory')
return {
'hit_rate': f"{hit_rate:.2f}%",
'total_requests': total,
'cache_hits': self.hits,
'cache_misses': self.misses,
'memory_used': info['used_memory_human'],
'keys_count': await self.cache.redis.dbsize()
}
added this to grafana dashboard. monitoring cache performance in real-time.
next optimizations #
1. multi-level caching:
- L1: in-process python dict (0.01ms)
- L2: redis (0.3ms)
- L3: API call (180ms)
2. cache warming:
- pre-fetch common symbols at market open
- avoid cold start cache misses
3. predictive caching:
- if algo checks SPX, probably will check QQQ next
- pre-fetch correlated symbols
lessons #
1. measure first:
profiled code before optimizing.
found: 80% time in API calls.
optimized the bottleneck.
2. caching is free speed:
redis added 10 lines of code.
60% latency reduction.
massive ROI.
3. monitor everything:
cache only works if you measure it.
added metrics to track hit rate, staleness, memory.
impact on trading #
already seeing faster execution.
entries filled 200ms sooner on average.
in volatile markets, this matters.
better fills = higher profits.
3:18am tuesday. added redis caching. cut market data latency 60%. getting filled faster now.
-AK