Spaces:
Sleeping
Sleeping
| 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)} | |