Files
All-in-one-Financial-Analysis/atlas-terminal/server/db/cache.py
T
shawnkim1997andClaude Opus 4.6 b2acda81ee feat: add Atlas Terminal — Next.js 14 + FastAPI full-stack migration
Complete migration from Streamlit to Next.js 14 App Router + FastAPI backend.

Frontend (Next.js 14):
- 10 pages: Overview, Research, Valuation, Technical, Markets, Earnings, News, Portfolio, Filings, Settings
- Terminal Noir dark theme with custom Tailwind config
- TradingView Lightweight Charts for candlestick/volume
- Valuation: DCF, Sensitivity Matrix, Monte Carlo, Tornado, Reverse DCF
- Financial Statements table with YoY growth badges and margin rows
- SEC EDGAR inline filing viewer with section tabs
- News split-view with iframe article embedding
- Technical Analysis with RSI, MACD, Bollinger, Fibonacci, Moving Averages
- Earnings beat/miss visualization
- AI Copilot chat panel with Gemini integration

Backend (FastAPI):
- 13 routers: market_data, financials, valuation, technical, earnings, insider, edgar, news, portfolio, analysis, chat, estimates, fx
- Services: DCF engine, Monte Carlo simulation, sensitivity analysis, risk metrics, SEC parser, technical indicators
- yfinance + yahooquery data sources with fallback pattern
- SQLite caching layer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 02:10:10 +00:00

222 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Two-tier caching system for ATLAS Terminal.
Tier 1 ``MemoryCache``: fast in-process dict with per-key TTL.
Tier 2 ``DBCache``: durable SQLite-backed cache with per-key TTL.
``CacheManager`` orchestrates both tiers (memory-first, DB-second).
"""
import json
import time
from typing import Any, Dict, Optional
import aiosqlite
from server.db.database import get_db
# ---------------------------------------------------------------------------
# Tier 1: In-memory cache
# ---------------------------------------------------------------------------
_DEFAULT_MEMORY_TTL: int = 300 # 5 minutes
class MemoryCache:
"""Thread-*unsafe* in-memory cache backed by a plain dict.
Each entry stores ``(value, expiry_timestamp)``. Expired entries are
lazily evicted on ``get()``.
"""
def __init__(self) -> None:
self._store: Dict[str, tuple[Any, float]] = {}
def get(self, key: str) -> Optional[Any]:
"""Return the cached value for *key*, or ``None`` if missing/expired.
Args:
key: Cache key.
Returns:
The stored value, or ``None``.
"""
entry = self._store.get(key)
if entry is None:
return None
value, expires_at = entry
if time.time() > expires_at:
del self._store[key]
return None
return value
def set(self, key: str, value: Any, ttl: int = _DEFAULT_MEMORY_TTL) -> None:
"""Store *value* under *key* with the given TTL in seconds.
Args:
key: Cache key.
value: Arbitrary Python object to cache.
ttl: Time-to-live in seconds (default 300).
"""
self._store[key] = (value, time.time() + ttl)
def delete(self, key: str) -> None:
"""Remove *key* from the cache (no-op if absent).
Args:
key: Cache key.
"""
self._store.pop(key, None)
def clear(self) -> None:
"""Remove all entries from the cache."""
self._store.clear()
# ---------------------------------------------------------------------------
# Tier 2: SQLite-backed cache
# ---------------------------------------------------------------------------
_DEFAULT_DB_TTL: int = 86400 # 1 day
class DBCache:
"""Durable cache that persists entries in the ``cache`` SQLite table.
Values are stored as JSON-encoded text so that structured data survives
a round-trip.
"""
async def get(self, key: str) -> Optional[str]:
"""Return the cached value for *key*, or ``None`` if missing/expired.
Args:
key: Cache key.
Returns:
The stored value string, or ``None``.
"""
db: aiosqlite.Connection = await get_db()
cursor = await db.execute(
"SELECT value, expires_at FROM cache WHERE key = ?",
(key,),
)
row = await cursor.fetchone()
if row is None:
return None
value: str = row[0]
expires_at: float = row[1]
if time.time() > expires_at:
await db.execute("DELETE FROM cache WHERE key = ?", (key,))
await db.commit()
return None
return value
async def set(self, key: str, value: str, ttl: int = _DEFAULT_DB_TTL) -> None:
"""Store *value* under *key* with the given TTL in seconds.
Uses ``INSERT OR REPLACE`` so existing entries are overwritten.
Args:
key: Cache key.
value: String value to persist.
ttl: Time-to-live in seconds (default 86400).
"""
db: aiosqlite.Connection = await get_db()
expires_at = time.time() + ttl
await db.execute(
"INSERT OR REPLACE INTO cache (key, value, expires_at) VALUES (?, ?, ?)",
(key, value, expires_at),
)
await db.commit()
async def cleanup(self) -> None:
"""Delete all expired entries from the cache table."""
db: aiosqlite.Connection = await get_db()
await db.execute(
"DELETE FROM cache WHERE expires_at < ?",
(time.time(),),
)
await db.commit()
# ---------------------------------------------------------------------------
# Orchestrator
# ---------------------------------------------------------------------------
class CacheManager:
"""Two-tier cache that checks memory first, then SQLite.
Usage::
value = await cache_manager.get("key")
await cache_manager.set("key", payload, memory_ttl=60, db_ttl=3600)
"""
def __init__(self) -> None:
self.memory = MemoryCache()
self.db = DBCache()
async def get(self, key: str) -> Optional[Any]:
"""Look up *key* in memory, then in the DB cache.
If the value is found only in the DB tier it is promoted back into
memory with the default memory TTL.
Args:
key: Cache key.
Returns:
The cached value (deserialized from JSON when coming from DB),
or ``None``.
"""
# Tier 1
mem_value = self.memory.get(key)
if mem_value is not None:
return mem_value
# Tier 2
db_value = await self.db.get(key)
if db_value is not None:
try:
deserialized = json.loads(db_value)
except (json.JSONDecodeError, TypeError):
deserialized = db_value
# Promote to memory for faster subsequent access
self.memory.set(key, deserialized)
return deserialized
return None
async def set(
self,
key: str,
value: Any,
memory_ttl: int = _DEFAULT_MEMORY_TTL,
db_ttl: int = _DEFAULT_DB_TTL,
) -> None:
"""Write *value* to both cache tiers.
The value is JSON-serialized before writing to the DB tier.
Args:
key: Cache key.
value: Arbitrary Python object to cache.
memory_ttl: TTL for the in-memory tier (default 300s).
db_ttl: TTL for the SQLite tier (default 86400s).
"""
self.memory.set(key, value, ttl=memory_ttl)
serialized = json.dumps(value, default=str)
await self.db.set(key, serialized, ttl=db_ttl)
# Module-level singleton
cache_manager: CacheManager = CacheManager()