aboutsummaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
authorOpenclaw <openclaw@mail.tylerhoang.xyz>2026-03-30 19:36:42 -0700
committerOpenclaw <openclaw@mail.tylerhoang.xyz>2026-03-30 19:36:42 -0700
commit2de6ae37b902e3632ea62b904164552538501ec3 (patch)
tree1fef3aa3df32dc27278e2be7d5b8fd88cb9bae06 /components
parent7a169d4f2bfeb79735823c1eb39f9162329b240e (diff)
Unify valuation calculations across Prism
- compute EV consistently as market cap + debt - cash - derive DCF/EV bridge inputs from balance-sheet rows - centralize latest price, shares outstanding, and computed market cap helpers - relabel negative net debt as net cash in valuation UI - self-compute historical ratios/key metrics instead of relying on vendor ratios - guard against nonsensical historical EV/EBITDA values - add methodology/source notes in DCF tab
Diffstat (limited to 'components')
-rw-r--r--components/valuation.py69
1 files changed, 49 insertions, 20 deletions
diff --git a/components/valuation.py b/components/valuation.py
index 88ea889..863193b 100644
--- a/components/valuation.py
+++ b/components/valuation.py
@@ -4,7 +4,11 @@ import plotly.graph_objects as go
import streamlit as st
from services.data_service import (
get_company_info,
+ get_latest_price,
+ get_shares_outstanding,
+ get_market_cap_computed,
get_free_cash_flow_series,
+ get_balance_sheet_bridge_items,
get_analyst_price_targets,
get_recommendations_summary,
get_earnings_history,
@@ -140,7 +144,7 @@ def _render_ratios(ticker: str):
info = get_company_info(ticker)
if not ratios and not info:
- st.info("Ratio data unavailable. Check your FMP API key.")
+ st.info("Ratio data unavailable.")
return
def r(key, fmt=fmt_ratio):
@@ -184,6 +188,10 @@ def _render_ratios(ticker: str):
# ── DCF Model ────────────────────────────────────────────────────────────────
+def _net_debt_label(value: float) -> str:
+ return "Net Cash" if value < 0 else "Net Debt"
+
+
def _render_dcf(ticker: str):
info = get_company_info(ticker)
@@ -198,17 +206,13 @@ def _render_dcf(ticker: str):
)
return
- shares = info.get("sharesOutstanding") or info.get("floatShares")
- current_price = info.get("currentPrice") or info.get("regularMarketPrice")
- total_debt = info.get("totalDebt") or 0.0
- cash_and_equivalents = (
- info.get("totalCash")
- or info.get("cash")
- or info.get("cashAndCashEquivalents")
- or 0.0
- )
- preferred_equity = info.get("preferredStock") or 0.0
- minority_interest = info.get("minorityInterest") or 0.0
+ shares = get_shares_outstanding(ticker)
+ current_price = get_latest_price(ticker)
+ bridge_items = get_balance_sheet_bridge_items(ticker)
+ total_debt = bridge_items["total_debt"]
+ cash_and_equivalents = bridge_items["cash_and_equivalents"]
+ preferred_equity = bridge_items["preferred_equity"]
+ minority_interest = bridge_items["minority_interest"]
if not shares:
st.info("Shares outstanding not available — DCF cannot be computed.")
@@ -276,14 +280,34 @@ def _render_dcf(ticker: str):
m3.metric("Upside / Downside", f"{upside * 100:+.1f}%", delta=f"{upside * 100:+.1f}%")
m4.metric("FCF Growth Used", f"{result['growth_rate_used'] * 100:.1f}%")
+ source_date = bridge_items.get("source_date")
st.caption(
"DCF is modeled on firm-level free cash flow, so enterprise value is bridged to equity value "
- "using cash and debt before calculating per-share value."
+ "using debt and cash from the most recent balance sheet before calculating per-share value."
)
+ if source_date:
+ st.caption(f"Balance-sheet bridge source date: **{source_date}**")
+
+ with st.expander("Methodology & sources", expanded=False):
+ st.markdown(
+ "- **TTM ratios:** computed from raw quarterly financial statements where possible.\n"
+ "- **Enterprise Value:** computed as market cap + total debt - cash & equivalents.\n"
+ "- **Market cap:** computed as latest price × shares outstanding when available.\n"
+ "- **Shares outstanding:** pulled from yfinance shares fields.\n"
+ "- **DCF bridge:** uses the most recent annual balance sheet for debt, cash, preferred equity, and minority interest.\n"
+ "- **Historical ratios:** computed from annual statements plus price history, with guards against nonsensical EV/EBITDA values.\n"
+ "- **Forward metrics:** analyst-driven items such as Forward P/E and estimates still depend on vendor data."
+ )
+
+ bridge_a, bridge_b, bridge_c, bridge_d = st.columns(4)
+ bridge_a.metric("Total Debt", fmt_large(total_debt))
+ bridge_b.metric("Cash & Equivalents", fmt_large(cash_and_equivalents))
+ bridge_c.metric("Preferred Equity", fmt_large(preferred_equity))
+ bridge_d.metric("Minority Interest", fmt_large(minority_interest))
bridge1, bridge2, bridge3, bridge4 = st.columns(4)
bridge1.metric("Enterprise Value", fmt_large(result["enterprise_value"]))
- bridge2.metric("Net Debt", fmt_large(result["net_debt"]))
+ bridge2.metric(_net_debt_label(result["net_debt"]), fmt_large(abs(result["net_debt"])))
bridge3.metric("Equity Value", fmt_large(result["equity_value"]))
bridge4.metric("Terminal Value PV", fmt_large(result["terminal_value_pv"]))
@@ -316,11 +340,14 @@ def _render_dcf(ticker: str):
# 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_bridge_items = get_balance_sheet_bridge_items(ticker)
+ total_debt = ev_bridge_items["total_debt"]
+ total_cash = ev_bridge_items["cash_and_equivalents"]
- # Compute current EV/EBITDA from our own data, not the bad info dict value
- ev_val = info.get("enterpriseValue")
+ market_cap = get_market_cap_computed(ticker)
+ ev_val = None
+ if market_cap and ebitda and ebitda > 0:
+ ev_val = float(market_cap) + float(total_debt or 0.0) - float(total_cash or 0.0)
ev_ebitda_current = (ev_val / ebitda) if (ev_val and ebitda and ebitda > 0) else None
if not ebitda or ebitda <= 0:
@@ -367,9 +394,11 @@ def _render_dcf(ticker: str):
ev_m4.metric("Implied EV", fmt_large(ev_result["implied_ev"]))
st.caption(
f"EBITDA: {fmt_large(ebitda)} · "
- f"Net Debt: {fmt_large(ev_result['net_debt'])} · "
+ f"{_net_debt_label(ev_result['net_debt'])}: {fmt_large(abs(ev_result['net_debt']))} · "
f"Equity Value: {fmt_large(ev_result['equity_value'])}"
)
+ if ev_bridge_items.get("source_date"):
+ st.caption(f"EV/EBITDA bridge source date: **{ev_bridge_items['source_date']}**")
else:
st.warning("Could not compute EV/EBITDA valuation.")
@@ -741,7 +770,7 @@ def _render_forward_estimates(ticker: str):
return
info = get_company_info(ticker)
- current_price = info.get("currentPrice") or info.get("regularMarketPrice")
+ current_price = get_latest_price(ticker)
tab_ann, tab_qtr = st.tabs(["Annual", "Quarterly"])