Files
All-in-one-Financial-Analysis/atlas-terminal/server/core/providers/yfinance.py
T

189 lines
8.3 KiB
Python

"""yfinance provider for the ATLAS Data Gateway."""
from __future__ import annotations
from datetime import date
from typing import Any
from server.core.data_gateway import Fundamentals, HoldersData, OHLCV, OHLCVBar, Profile, Quote
from server.core.providers.base import BaseProvider, ProviderError
from server.utils.peer_universe import peer_symbols_for_profile
class YFinanceProvider(BaseProvider):
name = "yfinance"
def _ticker(self, symbol: str) -> Any:
try:
import yfinance as yf
except Exception as exc: # pragma: no cover - dependency is expected in app runtime
raise ProviderError(f"yfinance import failed: {exc}") from exc
return yf.Ticker(symbol.strip().upper())
async def quote(self, symbol: str) -> Quote:
def fetch() -> Quote:
ticker = self._ticker(symbol)
info = ticker.info or {}
price = info.get("regularMarketPrice") or info.get("currentPrice") or info.get("previousClose")
prev = info.get("regularMarketPreviousClose") or info.get("previousClose")
change = (price - prev) if isinstance(price, (int, float)) and isinstance(prev, (int, float)) else None
change_pct = (change / prev * 100) if change is not None and prev else None
return Quote(
symbol=symbol.upper(),
price=float(price) if isinstance(price, (int, float)) else None,
currency=info.get("currency"),
change=change,
change_pct=change_pct,
source=self.name,
raw=info,
)
return await self._to_thread(fetch)
async def profile(self, symbol: str) -> Profile:
def fetch() -> Profile:
info = self._ticker(symbol).info or {}
return Profile(
symbol=symbol.upper(),
name=info.get("longName") or info.get("shortName"),
description=info.get("longBusinessSummary"),
sector=info.get("sector"),
industry=info.get("industry"),
country=info.get("country"),
exchange=info.get("exchange"),
currency=info.get("currency"),
website=info.get("website"),
employees=info.get("fullTimeEmployees"),
market_cap=info.get("marketCap"),
source=self.name,
raw=info,
)
return await self._to_thread(fetch)
async def fundamentals(self, symbol: str, period: str = "annual") -> Fundamentals:
def fetch() -> Fundamentals:
ticker = self._ticker(symbol)
info = ticker.info or {}
return Fundamentals(
symbol=symbol.upper(),
period=period,
name=info.get("shortName") or info.get("longName"),
market_cap=info.get("marketCap"),
revenue=info.get("totalRevenue"),
revenue_growth=info.get("revenueGrowth"),
gross_profit=info.get("grossProfits"),
gross_margin=info.get("grossMargins"),
operating_income=info.get("operatingMargins"),
net_income=info.get("netIncomeToCommon"),
ebitda=info.get("ebitda"),
free_cash_flow=info.get("freeCashflow"),
total_debt=info.get("totalDebt"),
cash=info.get("totalCash"),
shares=info.get("sharesOutstanding"),
pe=info.get("trailingPE") or info.get("forwardPE"),
pb=info.get("priceToBook"),
ps=info.get("priceToSalesTrailing12Months"),
ev_ebitda=info.get("enterpriseToEbitda"),
roic=info.get("returnOnInvestedCapital") or info.get("returnOnCapital"),
source=self.name,
raw=info,
)
return await self._to_thread(fetch)
async def peers(self, symbol: str) -> list[str]:
def fetch() -> list[str]:
normalized = symbol.strip().upper()
info = self._ticker(normalized).info or {}
syms = peer_symbols_for_profile(
normalized,
str(info.get("sector") or ""),
str(info.get("industry") or ""),
cap=6,
)
return [peer for peer in syms if peer != normalized]
return await self._to_thread(fetch)
async def financials(self, symbol: str, statement: str = "income", period: str = "annual") -> dict[str, Any]:
def fetch() -> dict[str, Any]:
import pandas as pd
normalized = symbol.strip().upper()
ticker = self._ticker(normalized)
statement_key = statement.strip().lower()
quarterly = period.strip().lower().startswith("q")
if statement_key == "income":
df = ticker.quarterly_income_stmt if quarterly else ticker.income_stmt
elif statement_key == "balance":
df = ticker.quarterly_balance_sheet if quarterly else ticker.balance_sheet
elif statement_key in {"cashflow", "cash_flow"}:
df = ticker.quarterly_cashflow if quarterly else ticker.cashflow
else:
raise ProviderError(f"unsupported statement: {statement}")
if df is None or not isinstance(df, pd.DataFrame) or df.empty:
raise ProviderError("missing financial statement dataframe")
sliced = df.iloc[:, :5]
periods = [str(col)[:10] for col in sliced.columns]
line_items: dict[str, list[float | None]] = {}
for idx, row in sliced.iterrows():
values: list[float | None] = []
for value in row.tolist():
try:
if pd.isna(value):
values.append(None)
else:
values.append(float(value))
except (TypeError, ValueError):
values.append(None)
line_items[str(idx).replace(" ", "")] = values
return {
"ticker": normalized,
"statement": statement_key,
"period": "quarter" if quarterly else "annual",
"source": self.name,
"periods": periods,
"line_items": line_items,
}
return await self._to_thread(fetch)
async def holders(self, symbol: str) -> HoldersData:
def fetch() -> HoldersData:
import pandas as pd
normalized = symbol.strip().upper()
ticker = self._ticker(normalized)
institutions: list[dict[str, Any]] = []
insiders: list[dict[str, Any]] = []
inst_df = getattr(ticker, "institutional_holders", None)
if isinstance(inst_df, pd.DataFrame) and not inst_df.empty:
institutions = inst_df.where(inst_df.notna(), None).to_dict(orient="records")
insider_df = getattr(ticker, "insider_roster_holders", None)
if isinstance(insider_df, pd.DataFrame) and not insider_df.empty:
insiders = insider_df.where(insider_df.notna(), None).to_dict(orient="records")
return HoldersData(symbol=normalized, institutions=institutions[:25], insiders=insiders[:25], source=self.name)
return await self._to_thread(fetch)
async def history(self, symbol: str, range: str = "1y") -> OHLCV:
def fetch() -> OHLCV:
hist = self._ticker(symbol).history(period=range)
bars: list[OHLCVBar] = []
for idx, row in hist.iterrows():
idx_date = idx.date() if hasattr(idx, "date") else date.fromisoformat(str(idx)[:10])
bars.append(
OHLCVBar(
date=idx_date,
open=float(row["Open"]) if "Open" in row else None,
high=float(row["High"]) if "High" in row else None,
low=float(row["Low"]) if "Low" in row else None,
close=float(row["Close"]) if "Close" in row else None,
volume=float(row["Volume"]) if "Volume" in row else None,
)
)
return OHLCV(symbol=symbol.upper(), range=range, bars=bars, source=self.name)
return await self._to_thread(fetch)