aboutsummaryrefslogtreecommitdiff
path: root/services/data_service.py
diff options
context:
space:
mode:
Diffstat (limited to 'services/data_service.py')
-rw-r--r--services/data_service.py114
1 files changed, 114 insertions, 0 deletions
diff --git a/services/data_service.py b/services/data_service.py
index 5ac8573..f0f981b 100644
--- a/services/data_service.py
+++ b/services/data_service.py
@@ -171,6 +171,120 @@ def get_sec_filings(ticker: str) -> list[dict]:
return []
+@st.cache_data(ttl=86400)
+def get_historical_ratios_yfinance(ticker: str) -> list[dict]:
+ """Compute annual historical ratios from yfinance financial statements.
+
+ Returns dicts with the same field names used by FMP's /ratios and /key-metrics
+ endpoints so callers can use either source interchangeably.
+ Covers: margins, ROE, ROA, D/E, P/E, P/B, P/S (price-based ratios are
+ approximate — they use average price near each fiscal year-end date).
+ """
+ try:
+ t = yf.Ticker(ticker.upper())
+ income = t.income_stmt # rows=metrics, cols=fiscal-year dates
+ balance = t.balance_sheet
+ info = t.info or {}
+
+ if income is None or income.empty:
+ return []
+
+ # One year of monthly price history per fiscal year going back 10 years
+ hist = t.history(period="10y", interval="1mo")
+ shares = info.get("sharesOutstanding") or info.get("impliedSharesOutstanding")
+
+ rows: list[dict] = []
+ for date in income.columns:
+ row: dict = {"date": str(date)[:10]}
+
+ # Pull income-statement items (may be NaN)
+ def _inc(label):
+ try:
+ v = income.loc[label, date]
+ return float(v) if pd.notna(v) else None
+ except KeyError:
+ return None
+
+ total_rev = _inc("Total Revenue")
+ gross_profit = _inc("Gross Profit")
+ operating_income = _inc("Operating Income")
+ net_income = _inc("Net Income")
+ ebitda_raw = _inc("EBITDA") or _inc("Normalized EBITDA")
+
+ # Margins
+ if total_rev and total_rev > 0:
+ if gross_profit is not None:
+ row["grossProfitMargin"] = gross_profit / total_rev
+ if operating_income is not None:
+ row["operatingProfitMargin"] = operating_income / total_rev
+ if net_income is not None:
+ row["netProfitMargin"] = net_income / total_rev
+
+ # Balance-sheet items
+ equity = None
+ total_assets = None
+ total_debt = None
+ if balance is not None and not balance.empty and date in balance.columns:
+ def _bal(label):
+ try:
+ v = balance.loc[label, date]
+ return float(v) if pd.notna(v) else None
+ except KeyError:
+ return None
+
+ equity = _bal("Stockholders Equity") or _bal("Common Stock Equity")
+ total_assets = _bal("Total Assets")
+ total_debt = _bal("Total Debt") or _bal("Long Term Debt And Capital Lease Obligation")
+ total_cash = _bal("Cash And Cash Equivalents") or _bal("Cash Cash Equivalents And Short Term Investments") or 0.0
+
+ if equity and equity > 0:
+ if net_income is not None:
+ row["returnOnEquity"] = net_income / equity
+ if total_debt is not None:
+ row["debtEquityRatio"] = total_debt / equity
+
+ if total_assets and total_assets > 0 and net_income is not None:
+ row["returnOnAssets"] = net_income / total_assets
+
+ # Price-based ratios — average closing price in ±45-day window around year-end
+ if shares and not hist.empty:
+ try:
+ date_ts = pd.Timestamp(date)
+ # Normalize timezones: yfinance history index may be tz-aware
+ hist_idx = hist.index
+ if hist_idx.tz is not None:
+ date_ts = date_ts.tz_localize(hist_idx.tz)
+ mask = (
+ (hist_idx >= date_ts - pd.DateOffset(days=45)) &
+ (hist_idx <= date_ts + pd.DateOffset(days=45))
+ )
+ window = hist.loc[mask, "Close"]
+ if not window.empty:
+ price = float(window.mean())
+ market_cap = price * shares
+
+ if net_income and net_income > 0:
+ row["peRatio"] = market_cap / net_income
+ if equity and equity > 0:
+ row["priceToBookRatio"] = market_cap / equity
+ if total_rev and total_rev > 0:
+ row["priceToSalesRatio"] = market_cap / total_rev
+
+ # EV/EBITDA — approximate
+ if ebitda_raw and ebitda_raw > 0 and total_debt is not None:
+ ev = market_cap + (total_debt or 0) - (total_cash or 0)
+ row["enterpriseValueMultiple"] = ev / ebitda_raw
+ except Exception:
+ pass
+
+ if len(row) > 1:
+ rows.append(row)
+
+ return rows
+ except Exception:
+ return []
+
+
@st.cache_data(ttl=3600)
def get_free_cash_flow_series(ticker: str) -> pd.Series:
"""Return annual Free Cash Flow series (most recent first)."""