aboutsummaryrefslogtreecommitdiff
path: root/services
diff options
context:
space:
mode:
Diffstat (limited to 'services')
-rw-r--r--services/data_service.py147
-rw-r--r--services/fmp_service.py62
2 files changed, 168 insertions, 41 deletions
diff --git a/services/data_service.py b/services/data_service.py
index acc935f..73374df 100644
--- a/services/data_service.py
+++ b/services/data_service.py
@@ -159,6 +159,153 @@ def get_insider_transactions(ticker: str) -> pd.DataFrame:
@st.cache_data(ttl=3600)
+def compute_ttm_ratios(ticker: str) -> dict:
+ """Compute all key financial ratios from raw yfinance quarterly statements.
+
+ Returns a dict with FMP-compatible key names so existing rendering code
+ doesn't need changes. Income items use TTM (sum of last 4 quarters).
+ Balance-sheet items use the most recent quarter. Market data (price,
+ market cap, EV) comes from yfinance's info dict.
+
+ This replaces both FMP's /ratios-ttm and /key-metrics-ttm endpoints,
+ saving ~2 API calls per ticker.
+ """
+ try:
+ t = yf.Ticker(ticker.upper())
+ info = t.info or {}
+ inc_q = t.quarterly_income_stmt
+ bal_q = t.quarterly_balance_sheet
+ cf_q = t.quarterly_cashflow
+
+ if inc_q is None or inc_q.empty:
+ return {}
+
+ ratios: dict = {}
+
+ def ttm(label):
+ """Sum last 4 quarters from quarterly income statement."""
+ if label in inc_q.index:
+ vals = inc_q.loc[label].iloc[:4].dropna()
+ if len(vals) == 4:
+ return float(vals.sum())
+ return None
+
+ def ttm_cf(label):
+ """Sum last 4 quarters from quarterly cash flow."""
+ if cf_q is not None and not cf_q.empty and label in cf_q.index:
+ vals = cf_q.loc[label].iloc[:4].dropna()
+ if len(vals) == 4:
+ return float(vals.sum())
+ return None
+
+ def bs(label):
+ """Most recent quarterly balance-sheet value."""
+ if bal_q is not None and not bal_q.empty and label in bal_q.index:
+ val = bal_q.loc[label].iloc[0]
+ if pd.notna(val):
+ return float(val)
+ return None
+
+ # ── TTM income items ──────────────────────────────────────────────────
+ revenue = ttm("Total Revenue")
+ gross_profit = ttm("Gross Profit")
+ operating_income = ttm("Operating Income")
+ net_income = ttm("Net Income")
+ ebit = ttm("EBIT")
+ ebitda = ttm("EBITDA") or ttm("Normalized EBITDA")
+ interest_expense = ttm("Interest Expense")
+ tax_provision = ttm("Tax Provision")
+ pretax_income = ttm("Pretax Income")
+
+ # ── Balance-sheet items (most recent quarter) ────────────────────────
+ equity = bs("Stockholders Equity") or bs("Common Stock Equity")
+ total_assets = bs("Total Assets")
+ total_debt = bs("Total Debt")
+ current_assets = bs("Current Assets")
+ current_liabilities = bs("Current Liabilities")
+ inventory = bs("Inventory")
+ cash = (
+ bs("Cash And Cash Equivalents")
+ or bs("Cash Cash Equivalents And Short Term Investments")
+ or 0.0
+ )
+
+ # ── Market data (live) ────────────────────────────────────────────────
+ market_cap = info.get("marketCap")
+ ev = info.get("enterpriseValue")
+
+ # ── Profitability ─────────────────────────────────────────────────────
+ if revenue and revenue > 0:
+ if gross_profit is not None:
+ ratios["grossProfitMarginTTM"] = gross_profit / revenue
+ if operating_income is not None:
+ ratios["operatingProfitMarginTTM"] = operating_income / revenue
+ if net_income is not None:
+ ratios["netProfitMarginTTM"] = net_income / revenue
+
+ if equity and equity > 0 and net_income is not None:
+ ratios["returnOnEquityTTM"] = net_income / equity
+
+ if total_assets and total_assets > 0 and net_income is not None:
+ ratios["returnOnAssetsTTM"] = net_income / total_assets
+
+ # ROIC = NOPAT / Invested Capital
+ if ebit is not None and pretax_income and pretax_income != 0:
+ effective_tax_rate = max(0.0, (tax_provision or 0) / pretax_income)
+ nopat = ebit * (1 - effective_tax_rate)
+ invested_capital = (equity or 0) + (total_debt or 0) - cash
+ if invested_capital > 0:
+ ratios["returnOnInvestedCapitalTTM"] = nopat / invested_capital
+
+ # ── Valuation multiples ───────────────────────────────────────────────
+ if market_cap and market_cap > 0:
+ if net_income and net_income > 0:
+ ratios["peRatioTTM"] = market_cap / net_income
+ if revenue and revenue > 0:
+ ratios["priceToSalesRatioTTM"] = market_cap / revenue
+ if equity and equity > 0:
+ ratios["priceToBookRatioTTM"] = market_cap / equity
+
+ if ev and ev > 0:
+ if revenue and revenue > 0:
+ ratios["evToSalesTTM"] = ev / revenue
+ if ebitda and ebitda > 0:
+ ratios["enterpriseValueMultipleTTM"] = ev / ebitda
+
+ # ── Leverage & Liquidity ──────────────────────────────────────────────
+ if equity and equity > 0 and total_debt is not None:
+ ratios["debtToEquityRatioTTM"] = total_debt / equity
+
+ if current_liabilities and current_liabilities > 0 and current_assets is not None:
+ ratios["currentRatioTTM"] = current_assets / current_liabilities
+ inv = inventory if inventory is not None else 0.0
+ ratios["quickRatioTTM"] = (current_assets - inv) / current_liabilities
+
+ if ebit is not None and interest_expense:
+ ie = abs(interest_expense)
+ if ie > 0:
+ ratios["interestCoverageRatioTTM"] = ebit / ie
+
+ # ── Dividends (from cash flow statement) ─────────────────────────────
+ dividends_paid = None
+ for label in ("Cash Dividends Paid", "Common Stock Dividend Paid"):
+ val = ttm_cf(label)
+ if val is not None:
+ dividends_paid = abs(val)
+ break
+
+ if dividends_paid and dividends_paid > 0:
+ if market_cap and market_cap > 0:
+ ratios["dividendYieldTTM"] = dividends_paid / market_cap
+ if net_income and net_income > 0:
+ ratios["dividendPayoutRatioTTM"] = dividends_paid / net_income
+
+ return ratios
+ except Exception:
+ return {}
+
+
+@st.cache_data(ttl=3600)
def get_ebitda_from_income_stmt(ticker: str) -> float | None:
"""Return the most recent annual EBITDA from the income statement.
diff --git a/services/fmp_service.py b/services/fmp_service.py
index 3bfa5c1..6d0ecd0 100644
--- a/services/fmp_service.py
+++ b/services/fmp_service.py
@@ -3,7 +3,7 @@ import os
import requests
import streamlit as st
from dotenv import load_dotenv
-from services.data_service import get_company_info, get_historical_ratios_yfinance
+from services.data_service import get_company_info, get_historical_ratios_yfinance, compute_ttm_ratios
load_dotenv()
@@ -27,51 +27,31 @@ def _get(base_url: str, endpoint: str, params: dict | None = None) -> dict | lis
return None
-def _apply_yfinance_ratio_fallbacks(ticker: str, merged: dict) -> dict:
- info = get_company_info(ticker)
- if not info:
- return merged
-
- fallback_map = {
- "peRatioTTM": info.get("trailingPE"),
- "priceToSalesRatioTTM": info.get("priceToSalesTrailing12Months"),
- "priceToBookRatioTTM": info.get("priceToBook"),
- "enterpriseValueMultipleTTM": info.get("enterpriseToEbitda"),
- "evToEBITDATTM": info.get("enterpriseToEbitda"),
- "evToSalesTTM": info.get("enterpriseToRevenue"),
- "grossProfitMarginTTM": info.get("grossMargins"),
- "operatingProfitMarginTTM": info.get("operatingMargins"),
- "netProfitMarginTTM": info.get("profitMargins"),
- "returnOnEquityTTM": info.get("returnOnEquity"),
- "returnOnAssetsTTM": info.get("returnOnAssets"),
- "debtToEquityRatioTTM": info.get("debtToEquity"),
- "currentRatioTTM": info.get("currentRatio"),
- "quickRatioTTM": info.get("quickRatio"),
- "dividendYieldTTM": info.get("dividendYield"),
- "dividendPayoutRatioTTM": info.get("payoutRatio"),
- }
-
- for key, value in fallback_map.items():
- if merged.get(key) is None and value is not None:
- merged[key] = value
-
- return merged
-
-
@st.cache_data(ttl=3600)
def get_key_ratios(ticker: str) -> dict:
- """Return merged stable TTM ratios + key metrics for a ticker, with yfinance fallbacks."""
- ticker = ticker.upper()
- ratios = _get(STABLE_BASE, "/ratios-ttm", params={"symbol": ticker})
- metrics = _get(STABLE_BASE, "/key-metrics-ttm", params={"symbol": ticker})
+ """Return TTM ratios for a ticker, computed from raw financial statements.
+ All ratios are self-computed via compute_ttm_ratios() — no FMP calls.
+ Forward P/E and dividend fallbacks come from yfinance's info dict.
+ """
+ ticker = ticker.upper()
merged = {"symbol": ticker}
- if isinstance(ratios, list) and ratios:
- merged.update(ratios[0])
- if isinstance(metrics, list) and metrics:
- merged.update(metrics[0])
- merged = _apply_yfinance_ratio_fallbacks(ticker, merged)
+ computed = compute_ttm_ratios(ticker)
+ if computed:
+ merged.update(computed)
+
+ # Forward P/E requires analyst estimates — can't compute from statements
+ info = get_company_info(ticker)
+ if info:
+ if info.get("forwardPE") is not None:
+ merged["forwardPE"] = info["forwardPE"]
+ # Fallback: dividends from info dict when cash-flow data is missing
+ if merged.get("dividendYieldTTM") is None and info.get("dividendYield") is not None:
+ merged["dividendYieldTTM"] = info["dividendYield"]
+ if merged.get("dividendPayoutRatioTTM") is None and info.get("payoutRatio") is not None:
+ merged["dividendPayoutRatioTTM"] = info["payoutRatio"]
+
return merged if len(merged) > 1 else {}