mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-22 07:08:04 +00:00
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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
56a9561f71
commit
b2acda81ee
@@ -0,0 +1,221 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user