111 lines
3.2 KiB
Python
111 lines
3.2 KiB
Python
# 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}")
|