aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--components/valuation.py19
-rw-r--r--services/data_service.py22
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.