diff options
| author | Tyler <tyler@tylerhoang.xyz> | 2026-03-30 18:54:56 -0700 |
|---|---|---|
| committer | Tyler <tyler@tylerhoang.xyz> | 2026-03-30 18:54:56 -0700 |
| commit | 712bbf675cfcb32535d8c494505e566efa347feb (patch) | |
| tree | 539a874eab3c785e2a8ba9209939bdb3fb4ee978 | |
| parent | aa3b1a27118ef0efac056ad135a81181bfb15c8e (diff) | |
Fix EV/EBITDA using income statement EBITDA instead of info dict
yfinance's info["ebitda"] is a miscalculated TTM value for many tickers
(e.g. DDOG shows $7.5M when the correct TTM EBITDA is $193.8M). Added
get_ebitda_from_income_stmt() which reads directly from t.income_stmt,
matching the annual and quarterly figures. Key Ratios and DCF EV/EBITDA
valuation now both use this source, with FMP as the preferred override.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | components/valuation.py | 19 | ||||
| -rw-r--r-- | services/data_service.py | 22 |
2 files changed, 39 insertions, 2 deletions
diff --git a/components/valuation.py b/components/valuation.py index b6e9d46..a168580 100644 --- a/components/valuation.py +++ b/components/valuation.py @@ -9,6 +9,7 @@ from services.data_service import ( get_recommendations_summary, get_earnings_history, get_next_earnings_date, + get_ebitda_from_income_stmt, ) from services.fmp_service import ( get_key_ratios, @@ -148,13 +149,26 @@ def _render_ratios(ticker: str): val = info.get(yf_key) return fmt(val) if val is not None else "—" + # Compute EV/EBITDA from income statement EBITDA — yfinance's info["ebitda"] + # is a known bad value for many tickers (miscalculated TTM aggregation). + def _ev_ebitda() -> str: + # Prefer FMP if available + fmp_val = (ratios or {}).get("enterpriseValueMultipleTTM") or (ratios or {}).get("evToEBITDATTM") + if fmp_val is not None: + return fmt_ratio(fmp_val) + ev = info.get("enterpriseValue") + ebitda = get_ebitda_from_income_stmt(ticker) + if ev and ebitda and ebitda > 0: + return fmt_ratio(ev / ebitda) + return "—" + rows = [ ("Valuation", [ ("P/E (TTM)", r("peRatioTTM", "trailingPE")), ("Forward P/E", fmt_ratio(info.get("forwardPE")) if info.get("forwardPE") is not None else "—"), ("P/S (TTM)", r("priceToSalesRatioTTM", "priceToSalesTrailing12Months")), ("P/B", r("priceToBookRatioTTM", "priceToBook")), - ("EV/EBITDA", r("enterpriseValueMultipleTTM", "enterpriseToEbitda") if ratios.get("enterpriseValueMultipleTTM") is not None else r("evToEBITDATTM", "enterpriseToEbitda")), + ("EV/EBITDA", _ev_ebitda()), ("EV/Revenue", r("evToSalesTTM", "enterpriseToRevenue")), ]), ("Profitability", [ @@ -315,7 +329,8 @@ def _render_dcf(ticker: str): st.divider() st.markdown("**EV/EBITDA Valuation**") - ebitda = info.get("ebitda") + # Use income statement EBITDA — info["ebitda"] is unreliable in yfinance + ebitda = get_ebitda_from_income_stmt(ticker) or info.get("ebitda") total_debt = info.get("totalDebt") or 0.0 total_cash = info.get("totalCash") or 0.0 ev_ebitda_current = info.get("enterpriseToEbitda") diff --git a/services/data_service.py b/services/data_service.py index 3de2484..acc935f 100644 --- a/services/data_service.py +++ b/services/data_service.py @@ -158,6 +158,28 @@ def get_insider_transactions(ticker: str) -> pd.DataFrame: return pd.DataFrame() +@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. + + yfinance's info['ebitda'] can be badly wrong for companies with large + stock-based compensation (e.g. it may deduct SBC, leaving near-zero EBITDA + even when the income statement EBITDA line is hundreds of millions). + The income statement 'EBITDA' row is the standard EBIT + D&A figure. + """ + try: + t = yf.Ticker(ticker.upper()) + inc = t.income_stmt + for label in ("EBITDA", "Normalized EBITDA"): + if label in inc.index: + val = inc.loc[label].iloc[0] + if val is not None and pd.notna(val): + return float(val) + return None + except Exception: + return None + + @st.cache_data(ttl=900) def get_options_chain(ticker: str) -> dict: """Return options chain data for the nearest available expirations. |
