# engine/data.py from datetime import datetime, timezone from pathlib import Path import os import threading import pandas as pd import yfinance as yf ENGINE_ROOT = Path(__file__).resolve().parents[1] HISTORY_DIR = ENGINE_ROOT / "storage" / "history" ALLOW_PRICE_CACHE = os.getenv("ALLOW_PRICE_CACHE", "0").strip().lower() in {"1", "true", "yes"} _LAST_PRICE: dict[str, dict[str, object]] = {} _LAST_PRICE_LOCK = threading.Lock() def _set_last_price(ticker: str, price: float, source: str): now = datetime.now(timezone.utc) with _LAST_PRICE_LOCK: _LAST_PRICE[ticker] = {"price": float(price), "source": source, "ts": now} def get_price_snapshot(ticker: str) -> dict[str, object] | None: with _LAST_PRICE_LOCK: data = _LAST_PRICE.get(ticker) if not data: return None return dict(data) def _get_last_live_price(ticker: str) -> float | None: with _LAST_PRICE_LOCK: data = _LAST_PRICE.get(ticker) if not data: return None if data.get("source") == "live": return float(data.get("price", 0)) return None def _cached_last_close(ticker: str) -> float | None: file = HISTORY_DIR / f"{ticker}.csv" if not file.exists(): return None df = pd.read_csv(file) if df.empty or "Close" not in df.columns: return None return float(df["Close"].iloc[-1]) def fetch_live_price(ticker, allow_cache: bool | None = None): if allow_cache is None: allow_cache = ALLOW_PRICE_CACHE try: df = yf.download( ticker, period="1d", interval="1m", auto_adjust=True, progress=False, timeout=5, ) if df is not None and not df.empty: price = float(df["Close"].iloc[-1]) _set_last_price(ticker, price, "live") return price except Exception: pass if allow_cache: last_live = _get_last_live_price(ticker) if last_live is not None: return last_live cached = _cached_last_close(ticker) if cached is not None: _set_last_price(ticker, cached, "cache") return cached raise RuntimeError(f"No live data for {ticker}")