from __future__ import annotations from typing import List, Dict import pandas as pd import yfinance as yf DEFAULT_UNIVERSE = [ "AAPL", "MSFT", "NVDA", "AMZN", "GOOGL", "META", "JPM", "XOM", "AVGO", "LLY", "AMD", "COST" ] def sanitize_tickers(tickers: List[str]) -> List[str]: cleaned = [] for t in tickers: t = (t or "").strip().upper().replace('"', "").replace("'", "") if t and t.isascii() and t.replace("-", "").replace(".", "").isalnum(): cleaned.append(t) return list(dict.fromkeys(cleaned))[:20] def fetch_price_history(tickers: List[str], lookback_days: int = 365) -> pd.DataFrame: tickers = sanitize_tickers(tickers) if len(tickers) < 2: raise ValueError("Need at least 2 valid tickers") period = "2y" if lookback_days > 365 else "1y" data = yf.download( tickers=tickers, period=period, interval="1d", auto_adjust=True, progress=False, threads=True, group_by="ticker", ) rows = [] if isinstance(data.columns, pd.MultiIndex): for ticker in tickers: try: block = data[ticker].copy() except Exception: continue if block.empty or "Close" not in block.columns: continue block = block.reset_index() date_col = "Date" if "Date" in block.columns else block.columns[0] for _, row in block.iterrows(): close_val = row.get("Close") if pd.isna(close_val): continue rows.append({ "date": pd.Timestamp(row[date_col]).normalize(), "ticker": ticker, "close": float(close_val), "volume": float(row.get("Volume", 0.0) or 0.0), }) else: if len(tickers) != 1: raise ValueError("Unexpected Yahoo Finance schema for multiple tickers") ticker = tickers[0] block = data.copy() if block.empty or "Close" not in block.columns: raise ValueError("No usable market data returned") block = block.reset_index() date_col = "Date" if "Date" in block.columns else block.columns[0] for _, row in block.iterrows(): close_val = row.get("Close") if pd.isna(close_val): continue rows.append({ "date": pd.Timestamp(row[date_col]).normalize(), "ticker": ticker, "close": float(close_val), "volume": float(row.get("Volume", 0.0) or 0.0), }) out = pd.DataFrame(rows) if out.empty: raise ValueError("No market data returned") if "ticker" not in out.columns: raise ValueError(f"Ticker column missing from market data. Columns: {list(out.columns)}") out = out.sort_values(["ticker", "date"]).reset_index(drop=True) return out def fetch_quotes(tickers: List[str]) -> List[Dict[str, float]]: tickers = sanitize_tickers(tickers) result = [] for ticker in tickers: try: hist = yf.Ticker(ticker).history(period="5d", interval="1d", auto_adjust=True) if hist.empty: continue last = float(hist["Close"].iloc[-1]) prev = float(hist["Close"].iloc[-2]) if len(hist) > 1 else last chg = 0.0 if prev == 0 else (last / prev - 1.0) result.append({ "ticker": ticker, "price": last, "day_change_pct": chg, }) except Exception: continue return result def infer_sectors(tickers: List[str]) -> Dict[str, str]: return {ticker: "Unknown" for ticker in sanitize_tickers(tickers)}