mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-21 22:58:04 +00:00
189 lines
8.3 KiB
Python
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)
|