Skip to main content

added redis caching - cut market data latency by 60%

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:

  1. hit polygon API over HTTPS
  2. parse JSON response
  3. extract OHLCV data
  4. calculate indicators
  5. 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

Related

rebuilt backtesting pipeline - 10x faster parameter optimization
spent last 3 days rebuilding backtest optimization pipeline. went from 6 hours to 35 minutes for full parameter sweep. the problem # old approach: sequential parameter testing.
using python async for real-time market data
rewrote my market data pipeline to use async. 3x faster, way cleaner code. the problem # old synchronous code:
how i organize my trading code on github
got asked on r/algotrading how i organize my trading repos. here’s my setup after 4 months of refactoring. repo structure # i have 4 main repos:
A. came over - gave her the server rack tour
A. came over sunday afternoon. gave her full tour of my trading setup. she fucking loved it. the setup tour # server rack:
building volatility regime detection
need to stop trading when volatility spikes. building detection system. the problem # this week VIX spiked 18% in 2 days. my strategies got stopped out twice.
checking my backtests for overfitting
worried my strategies are overfit to historical data. spent today testing for it. been reading NexusFi backtesting threads about this exact problem. the problem # my backtests look great: