from __future__ import annotations from typing import Any import streamlit as st import yfinance as yf @st.cache_data(show_spinner=False, ttl=3600) def lookup_symbol_candidates(query: str, max_results: int = 10) -> list[dict[str, str]]: cleaned = query.strip() if len(cleaned) < 2: return [] try: search = yf.Search(cleaned, max_results=max_results) quotes = getattr(search, "quotes", []) or [] except Exception: return [] seen_symbols: set[str] = set() candidates: list[dict[str, str]] = [] for quote in quotes: symbol = str(quote.get("symbol", "")).strip().upper() if not symbol or symbol in seen_symbols: continue seen_symbols.add(symbol) candidates.append( { "symbol": symbol, "name": str(quote.get("shortname") or quote.get("longname") or "").strip(), "exchange": str(quote.get("exchDisp") or quote.get("exchange") or "").strip(), "type": str(quote.get("typeDisp") or quote.get("quoteType") or "").strip(), } ) return candidates @st.cache_data(show_spinner=False, ttl=3600) def resolve_symbol_identity(symbol: str) -> dict[str, str]: normalized_symbol = symbol.strip().upper() if not normalized_symbol: return {"symbol": "", "name": "", "exchange": ""} def _from_quote(quote: dict[str, Any]) -> dict[str, str]: return { "symbol": normalized_symbol, "name": str(quote.get("shortname") or quote.get("longname") or "").strip(), "exchange": str(quote.get("exchDisp") or quote.get("exchange") or "").strip(), } try: search = yf.Search(normalized_symbol, max_results=8) quotes = getattr(search, "quotes", []) or [] for quote in quotes: candidate_symbol = str(quote.get("symbol", "")).strip().upper() if candidate_symbol == normalized_symbol: return _from_quote(quote) if quotes: return _from_quote(quotes[0]) except Exception: pass try: info = yf.Ticker(normalized_symbol).info return { "symbol": normalized_symbol, "name": str(info.get("shortName") or info.get("longName") or "").strip(), "exchange": str(info.get("exchange") or "").strip(), } except Exception: return {"symbol": normalized_symbol, "name": "", "exchange": ""}