aboutsummaryrefslogtreecommitdiff
path: root/services/data_service.py
diff options
context:
space:
mode:
authorTyler <tyler@tylerhoang.xyz>2026-03-30 19:09:45 -0700
committerTyler <tyler@tylerhoang.xyz>2026-03-30 19:09:45 -0700
commit92b7eae36866c3424f44b4b6a653833a65df91a9 (patch)
tree1ab8d86c74b741d5e1b2e8df2caa6de0b0bc9464 /services/data_service.py
parent8f6199a97c592e68f812d952b41942603723e2ed (diff)
Compute all Key Ratios from raw statements, eliminating FMP ratio calls
Added compute_ttm_ratios() which derives all 16 TTM ratios directly from yfinance quarterly income statements, balance sheets, and cash flow: Valuation: P/E, P/S, P/B, EV/EBITDA, EV/Revenue Profitability: Gross/Operating/Net Margin, ROE, ROA, ROIC Leverage: D/E, Current Ratio, Quick Ratio, Interest Coverage Dividends: Yield, Payout Ratio get_key_ratios() no longer calls FMP's /ratios-ttm or /key-metrics-ttm endpoints, saving ~2 FMP API calls per ticker load (including each Comps peer). Forward P/E still comes from yfinance info dict (analyst estimate). This also fixes EV/EBITDA for all tickers (DDOG was 4998x from FMP/yfinance pre-computed values, now correctly 194x from income statement EBITDA). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'services/data_service.py')
-rw-r--r--services/data_service.py147
1 files changed, 147 insertions, 0 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.