""" TradeSense AI — In-Memory Market Data Cache TTL-enforced caching with rich audit metadata. API credentials are NEVER stored. """ import time from datetime import date, datetime, timezone from typing import Dict, Optional, Tuple, Any import pandas as pd from backend.config.settings import settings from backend.app.schemas.contracts import DatasetMetadata class CacheEntryMetadata: """ Audit metadata stored alongside each cached dataset. SECURITY: Must never contain API keys, auth headers, or secrets. """ __slots__ = ( "ticker", "start_date", "end_date", "cutoff_session", "dataset_version", "quality_status", "provider", "lineage_hash", "created_at", "expires_at", ) def __init__( self, ticker: str, start_date: date, end_date: date, cutoff_session: date, dataset_version: str, quality_status: str, provider: str, lineage_hash: str, ttl_seconds: int, ): self.ticker = ticker self.start_date = start_date self.end_date = end_date self.cutoff_session = cutoff_session self.dataset_version = dataset_version self.quality_status = quality_status self.provider = provider self.lineage_hash = lineage_hash self.created_at: datetime = datetime.now(timezone.utc) self.expires_at: float = time.time() + ttl_seconds def is_expired(self) -> bool: return time.time() > self.expires_at def safe_dict(self) -> Dict[str, Any]: """Returns a safe, credential-free audit snapshot of this entry.""" return { "ticker": self.ticker, "start_date": str(self.start_date), "end_date": str(self.end_date), "cutoff_session": str(self.cutoff_session), "dataset_version": self.dataset_version, "quality_status": self.quality_status, "provider": self.provider, "lineage_hash": self.lineage_hash, "created_at": self.created_at.isoformat(), "expires_at": datetime.fromtimestamp(self.expires_at, tz=timezone.utc).isoformat(), } class CacheEntry: """Stores a frozen DataFrame + DatasetMetadata pair with TTL enforcement.""" def __init__( self, df: pd.DataFrame, metadata: DatasetMetadata, ttl_seconds: int, ): self.df = df.copy() self.metadata = metadata self.cache_meta = CacheEntryMetadata( ticker=metadata.ticker, start_date=metadata.start_session, end_date=metadata.cutoff_session, cutoff_session=metadata.cutoff_session, dataset_version=metadata.dataset_version, quality_status=( metadata.quality_report.quality_status.value if metadata.quality_report else "UNKNOWN" ), provider=metadata.source_primary, lineage_hash=metadata.sha256_hash, ttl_seconds=ttl_seconds, ) def is_expired(self) -> bool: return self.cache_meta.is_expired() class MarketDataCache: """ Thread-safe in-memory cache for canonical OHLCV datasets. Enforces: - TTL expiry - Cutoff T validation on retrieval (cached data cannot bypass temporal isolation) - No credential storage """ def __init__(self): self._cache: Dict[str, CacheEntry] = {} # ───────────────────────────────────────────────────────────────────── # Internal helpers # ───────────────────────────────────────────────────────────────────── @staticmethod def _build_key(ticker: str, start_date: Any, end_date: Any, cutoff_session: Any) -> str: return f"{ticker.upper()}|{start_date}|{end_date}|{cutoff_session}" # ───────────────────────────────────────────────────────────────────── # Public API # ───────────────────────────────────────────────────────────────────── def get( self, ticker: str, start_date: Any, end_date: Any, cutoff_session: Any, ) -> Optional[Tuple[pd.DataFrame, DatasetMetadata]]: """ Returns a cached (DataFrame, DatasetMetadata) pair if: - The cache is enabled - An unexpired entry exists for this key - The cached dataset's cutoff_session matches the requested cutoff (temporal safety) """ if not settings.market_data.cache_enabled: return None key = self._build_key(ticker, start_date, end_date, cutoff_session) entry = self._cache.get(key) if entry is None: return None if entry.is_expired(): del self._cache[key] return None # CRITICAL: Validate that the cached cutoff matches the requested cutoff. # This prevents stale cache entries from violating temporal isolation. if str(entry.cache_meta.cutoff_session) != str(cutoff_session): return None return entry.df.copy(), entry.metadata def set( self, ticker: str, start_date: Any, end_date: Any, cutoff_session: Any, df: pd.DataFrame, metadata: DatasetMetadata, is_historical: bool = True, ) -> None: """Stores a canonical dataset under the given key with appropriate TTL.""" if not settings.market_data.cache_enabled: return key = self._build_key(ticker, start_date, end_date, cutoff_session) ttl = ( settings.market_data.historical_cache_ttl_seconds if is_historical else settings.market_data.latest_data_cache_ttl_seconds ) self._cache[key] = CacheEntry(df, metadata, ttl) def get_entry_metadata( self, ticker: str, start_date: Any, end_date: Any, cutoff_session: Any, ) -> Optional[Dict[str, Any]]: """Returns safe, credential-free metadata for a cached entry.""" key = self._build_key(ticker, start_date, end_date, cutoff_session) entry = self._cache.get(key) if entry is None or entry.is_expired(): return None return entry.cache_meta.safe_dict() def invalidate(self, ticker: str) -> int: """Evicts all entries for a ticker. Returns count evicted.""" keys_to_drop = [k for k in self._cache if k.startswith(f"{ticker.upper()}|")] for k in keys_to_drop: del self._cache[k] return len(keys_to_drop) def clear(self) -> None: self._cache.clear() def stats(self) -> Dict[str, Any]: """Returns aggregate cache statistics (no credentials).""" live = sum(1 for e in self._cache.values() if not e.is_expired()) return { "total_entries": len(self._cache), "live_entries": live, "expired_entries": len(self._cache) - live, } # Global Singleton Market Data Cache data_cache = MarketDataCache()