# 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 _history_cache_file(ticker: str, provider: str = "yfinance") -> Path: safe_ticker = (ticker or "").replace(":", "_").replace("/", "_") return HISTORY_DIR / f"{safe_ticker}.csv" def _set_last_price( ticker: str, price: float, source: str, *, provider: str | None = None, instrument_token: int | None = None, ): now = datetime.now(timezone.utc) with _LAST_PRICE_LOCK: payload = {"price": float(price), "source": source, "ts": now} if provider: payload["provider"] = provider if instrument_token is not None: payload["instrument_token"] = int(instrument_token) _LAST_PRICE[ticker] = payload 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, provider: str | None = None) -> float | None: with _LAST_PRICE_LOCK: data = _LAST_PRICE.get(ticker) if not data: return None if data.get("source") == "live": if provider and data.get("provider") not in {None, provider}: return None return float(data.get("price", 0)) return None def _cached_last_close(ticker: str, provider: str = "yfinance") -> float | None: file = _history_cache_file(ticker, provider=provider) 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, *, provider: str = "yfinance", user_id: str | None = None, run_id: str | 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: close_value = df["Close"].iloc[-1] if hasattr(close_value, "iloc"): close_value = close_value.iloc[-1] price = float(close_value) _set_last_price(ticker, price, "live", provider="yfinance") return price except Exception: pass if allow_cache: last_live = _get_last_live_price(ticker, provider="yfinance") if last_live is not None: return last_live cached = _cached_last_close(ticker, provider="yfinance") if cached is not None: _set_last_price(ticker, cached, "cache", provider="yfinance") return cached raise RuntimeError(f"No live data for {ticker}")