diff options
| author | Tyler <tyler@tylerhoang.xyz> | 2026-03-30 19:09:45 -0700 |
|---|---|---|
| committer | Tyler <tyler@tylerhoang.xyz> | 2026-03-30 19:09:45 -0700 |
| commit | 92b7eae36866c3424f44b4b6a653833a65df91a9 (patch) | |
| tree | 1ab8d86c74b741d5e1b2e8df2caa6de0b0bc9464 /services/data_service.py | |
| parent | 8f6199a97c592e68f812d952b41942603723e2ed (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.py | 147 |
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. |
