Developer 10 min read

Caching Financial Data: Redis Patterns for Trading Applications

Learn smart caching strategies for financial data using Redis. Reduce API costs, improve latency, and maintain data freshness with TTL-based patterns.

7Sections
5Code samples
10Min read
DeveloperCategory

The Caching Dilemma

Financial data caching is a balancing act. Cache too aggressively and your bot trades on stale data. Cache too little and you burn through API rate limits. The key insight: different data types have different staleness tolerances.

120s

Live quote TTL

900s

H1 indicator TTL

2700s

H4/D1 indicator TTL

Redis Setup

Python Redis Setup
import redis
import json
from typing import Optional

r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)

def cache_get(key: str) -> Optional[dict]:
    """Get cached data, returns None if expired or missing."""
    data = r.get(key)
    if data:
        return json.loads(data)
    return None

def cache_set(key: str, data: dict, ttl_seconds: int):
    """Store data with a TTL."""
    r.setex(key, ttl_seconds, json.dumps(data))

TTL Strategy by Data Type

The instinct is to tie the TTL to the candle length — cache an H1 value for an hour. That is the wrong clock. What actually decides how long a cached value can still be correct is how often the data is republished. A cached value only goes stale when a newer one exists upstream, so the TTL should track the publish interval, then add a margin so the entry survives until the next push lands.

That is how TickAtlas sets its own numbers. The price feeder publishes M1 and M5 every 60 seconds, M15/M30/H1 every 600 seconds, and H4 and above every 1800 seconds. Each server-side TTL is roughly 1.5x its publish interval: 180s for M1/M5, 900s for M15/M30/H1, 2700s for H4/D1, and 7200s for W1/MN1. Live quotes use 120s (twice the 60s base TTL). When a snapshot is detected as stale, it is re-cached for only 30s instead of the full TTL, so bad data cannot be masked for the whole window.

Mirror those numbers client-side and your cache expires at roughly the moment new data becomes available — maximum hit rate, no extra staleness. Pick anything longer and you are trading on data you could already have replaced.

Python TTL Strategy by Data Type
# TTL configuration based on data type and timeframe
# TTL tracks the PUBLISH interval of the timeframe, not the candle length.
# These are the server-side values, so a client cache that mirrors them
# expires at about the moment fresher data becomes available.
TTL_CONFIG = {
    # Indicators and OHLCV share the same clock: both come from the same push.
    "indicator": {
        "M1": 180,      # published every 60s
        "M5": 180,      # published every 60s
        "M15": 900,     # published every 600s
        "M30": 900,     # published every 600s
        "H1": 900,      # published every 600s
        "H4": 2700,     # published every 1800s
        "D1": 2700,     # published every 1800s
        "W1": 7200,
        "MN1": 7200,
    },
    # Live quotes tick continuously but are cached at twice the 60s base TTL.
    "quote": 120,
    # Anything the API does not publish on a fixed clock is your own call.
    "symbols": 86400,    # 24 hours -- the instrument list rarely changes
}

# When you detect a stale upstream snapshot, re-cache it briefly instead of
# for the full TTL, so one bad read cannot be masked for the whole window.
STALE_FALLBACK_TTL = 30

def get_ttl(data_type: str, timeframe: str = None) -> int:
    ttl = TTL_CONFIG.get(data_type)
    if isinstance(ttl, dict):
        return ttl.get(timeframe, 120)  # Default to the base quote TTL
    return ttl or 120

Cached API Client

Python Cached API Client
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://tickatlas.com/v1"

class CachedClient:
    """API client with Redis caching layer."""

    def __init__(self, api_key: str):
        self.headers = {"X-API-Key": api_key}

    def get_indicator(self, symbol: str, indicator: str, timeframe: str) -> dict:
        cache_key = f"ind:{symbol}:{indicator}:{timeframe}"

        cached = cache_get(cache_key)
        if cached:
            return cached

        resp = requests.get(
            f"{BASE_URL}/indicator",
            headers=self.headers,
            params={"symbol": symbol, "indicator": indicator, "timeframe": timeframe},
        )
        resp.raise_for_status()
        data = resp.json()["data"]

        ttl = get_ttl("indicator", timeframe)
        cache_set(cache_key, data, ttl)
        return data

    def get_ohlcv(self, symbol: str, timeframe: str, limit: int = 50) -> list:
        cache_key = f"ohlcv:{symbol}:{timeframe}:{limit}"

        cached = cache_get(cache_key)
        if cached:
            return cached

        resp = requests.get(
            f"{BASE_URL}/ohlc",
            headers=self.headers,
            params={"symbol": symbol, "timeframe": timeframe, "limit": limit},
        )
        data = resp.json()["data"]["candles"]

        ttl = get_ttl("indicator", timeframe)
        cache_set(cache_key, data, ttl)
        return data

    def get_spread(self, symbol: str) -> dict:
        cache_key = f"spread:{symbol}"

        cached = cache_get(cache_key)
        if cached:
            return cached

        resp = requests.get(
            f"{BASE_URL}/spread",
            headers=self.headers,
            params={"symbol": symbol},
        )
        data = resp.json()["data"]

        cache_set(cache_key, data, get_ttl("quote"))
        return data

# Usage -- automatic caching
client = CachedClient(API_KEY)
rsi = client.get_indicator("EURUSD", "RSI_14", "H4")  # API call
rsi = client.get_indicator("EURUSD", "RSI_14", "H4")  # Cache hit

Cache Warming

For critical symbols, pre-fetch data before your strategy loop runs:

Python Cache Warming
WATCHLIST = ["EURUSD", "GBPUSD", "USDJPY", "XAUUSD", "BTCUSD"]
INDICATORS = ["RSI_14", "MACD_hist", "EMA_50", "ATR_14"]

def warm_cache(client: CachedClient, timeframe: str):
    """Pre-fetch all indicators for the watchlist."""
    for symbol in WATCHLIST:
        for indicator in INDICATORS:
            try:
                client.get_indicator(symbol, indicator, timeframe)
            except Exception as e:
                print(f"Failed to warm {symbol}/{indicator}: {e}")

# Warm cache before the strategy loop starts
warm_cache(client, "H4")

Monitoring Cache Performance

Python Monitoring Cache Performance
class CacheStats:
    def __init__(self):
        self.hits = 0
        self.misses = 0

    def record_hit(self):
        self.hits += 1

    def record_miss(self):
        self.misses += 1

    @property
    def hit_rate(self) -> float:
        total = self.hits + self.misses
        return self.hits / total if total > 0 else 0.0

    def report(self):
        print(f"Cache hits: {self.hits}, misses: {self.misses}")
        print(f"Hit rate: {self.hit_rate:.1%}")
        print(f"API calls saved: {self.hits}")

Related Reading

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.

Prefer the API to the article

Build it instead of reading about it.

Create a key, run the first request, then extend one layer at a time. Every account starts pay-as-you-go with $2.50 of credit — no card required.