"""Yahoo Finance fundamental data provider (via yfinance).""" from __future__ import annotations import asyncio import logging from datetime import datetime, timezone from shared.fundamentals.base import FundamentalsProvider, FundamentalsSnapshot logger = logging.getLogger(__name__) def _safe_float(val: object) -> float | None: """Convert a value to float, returning None for missing/invalid values.""" if val is None: return None try: return float(val) except (ValueError, TypeError): return None def _safe_float_div100(val: object) -> float | None: """Convert a value to float and divide by 100 (e.g. D/E ratio).""" f = _safe_float(val) return f / 100.0 if f is not None else None def _fetch_info(ticker: str) -> dict: """Synchronous helper — runs in a thread pool to avoid blocking the loop.""" import yfinance # noqa: local import to avoid hard dependency return yfinance.Ticker(ticker).info class YahooFinanceProvider(FundamentalsProvider): """Fetches fundamental data from Yahoo Finance via the yfinance library.""" async def fetch(self, ticker: str) -> FundamentalsSnapshot | None: """Fetch ticker info from Yahoo Finance in a thread pool.""" try: loop = asyncio.get_running_loop() info = await loop.run_in_executor(None, _fetch_info, ticker) if not info: logger.warning("Yahoo Finance returned empty info for %s", ticker) return None return FundamentalsSnapshot( ticker=ticker, eps_ttm=_safe_float(info.get("trailingEps")), pe_ratio=_safe_float(info.get("trailingPE")), peg_ratio=_safe_float(info.get("pegRatio")), revenue_growth_yoy=_safe_float(info.get("revenueGrowth")), profit_margin=_safe_float(info.get("profitMargins")), debt_to_equity=_safe_float_div100(info.get("debtToEquity")), market_cap=_safe_float(info.get("marketCap")), fetched_at=datetime.now(timezone.utc), ) except Exception: logger.exception("Yahoo Finance fetch failed for %s", ticker) return None