trading/shared/fundamentals/yahoo.py

63 lines
2.2 KiB
Python

"""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