aboutsummaryrefslogtreecommitdiff
path: root/components/valuation.py
diff options
context:
space:
mode:
Diffstat (limited to 'components/valuation.py')
-rw-r--r--components/valuation.py372
1 files changed, 371 insertions, 1 deletions
diff --git a/components/valuation.py b/components/valuation.py
index 37a964d..f64b0b4 100644
--- a/components/valuation.py
+++ b/components/valuation.py
@@ -537,6 +537,21 @@ def _render_dcf_model(ctx: dict):
iv = result["intrinsic_value_per_share"]
current_price = ctx["current_price"]
+ market_cap = ctx["market_cap"]
+ market_enterprise_value = None
+ if market_cap and market_cap > 0:
+ market_enterprise_value = (
+ float(market_cap)
+ + float(ctx["total_debt"])
+ - float(ctx["cash_and_equivalents"])
+ + float(ctx["preferred_equity"])
+ + float(ctx["minority_interest"])
+ )
+
+ st.caption(
+ "This model projects free cash flow, discounts those cash flows back to today, "
+ "adds a terminal value, and then bridges from enterprise value to equity value per share."
+ )
m1, m2, m3, m4 = st.columns(4)
m1.metric("Equity Value / Share", fmt_currency(iv))
@@ -546,6 +561,22 @@ def _render_dcf_model(ctx: dict):
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}%")
+ if current_price and current_price > 0:
+ valuation_gap = iv - current_price
+ market_message = "above" if valuation_gap > 0 else "below"
+ if abs(valuation_gap) < 0.005:
+ market_message = "roughly in line with"
+ st.markdown(
+ f"The DCF implies **{fmt_currency(iv)} per share**, which is **{fmt_currency(abs(valuation_gap))} "
+ f"{market_message}** the current market price of **{fmt_currency(current_price)}**."
+ )
+
+ calc_a, calc_b, calc_c, calc_d = st.columns(4)
+ calc_a.metric("Base FCF", fmt_large(result["base_fcf"]))
+ calc_b.metric("Forecast FCF PV", fmt_large(result["fcf_pv_sum"]))
+ calc_c.metric("Terminal Value PV", fmt_large(result["terminal_value_pv"]))
+ calc_d.metric("Implied Enterprise Value", fmt_large(result["enterprise_value"]))
+
source_date = ctx["bridge_items"].get("source_date")
st.caption(
"DCF is modeled on firm-level free cash flow, so enterprise value is bridged to equity value "
@@ -560,11 +591,12 @@ def _render_dcf_model(ctx: dict):
"- **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"
+ "- **DCF bridge:** uses the most recent quarterly 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."
)
+ st.markdown("**Enterprise Value To Equity Value Bridge**")
bridge_a, bridge_b, bridge_c, bridge_d = st.columns(4)
bridge_a.metric("Total Debt", fmt_large(ctx["total_debt"]))
bridge_b.metric("Cash & Equivalents", fmt_large(ctx["cash_and_equivalents"]))
@@ -577,6 +609,108 @@ def _render_dcf_model(ctx: dict):
bridge3.metric("Equity Value", fmt_large(result["equity_value"]))
bridge4.metric("Terminal Value PV", fmt_large(result["terminal_value_pv"]))
+ bridge_labels = ["Enterprise Value"]
+ bridge_measures = ["absolute"]
+ bridge_values = [float(result["enterprise_value"])]
+ bridge_text = [fmt_large(result["enterprise_value"])]
+
+ if ctx["total_debt"]:
+ bridge_labels.append("Add Debt")
+ bridge_measures.append("relative")
+ bridge_values.append(float(ctx["total_debt"]))
+ bridge_text.append(fmt_large(ctx["total_debt"]))
+ if ctx["cash_and_equivalents"]:
+ bridge_labels.append("Less Cash")
+ bridge_measures.append("relative")
+ bridge_values.append(-float(ctx["cash_and_equivalents"]))
+ bridge_text.append(fmt_large(-float(ctx["cash_and_equivalents"])))
+ if ctx["preferred_equity"]:
+ bridge_labels.append("Less Preferred")
+ bridge_measures.append("relative")
+ bridge_values.append(-float(ctx["preferred_equity"]))
+ bridge_text.append(fmt_large(-float(ctx["preferred_equity"])))
+ if ctx["minority_interest"]:
+ bridge_labels.append("Less Minority")
+ bridge_measures.append("relative")
+ bridge_values.append(-float(ctx["minority_interest"]))
+ bridge_text.append(fmt_large(-float(ctx["minority_interest"])))
+
+ bridge_labels.append("Equity Value")
+ bridge_measures.append("total")
+ bridge_values.append(float(result["equity_value"]))
+ bridge_text.append(fmt_large(result["equity_value"]))
+
+ bridge_fig = go.Figure(
+ go.Waterfall(
+ x=bridge_labels,
+ measure=bridge_measures,
+ y=bridge_values,
+ text=bridge_text,
+ textposition="outside",
+ connector={"line": {"color": "#6f7785"}},
+ increasing={"marker": {"color": "#4F8EF7"}},
+ decreasing={"marker": {"color": "#F76E6E"}},
+ totals={"marker": {"color": "#2ecc71"}},
+ )
+ )
+ bridge_fig.update_layout(
+ title="Enterprise Value Bridge To Equity Value",
+ yaxis_title="USD",
+ plot_bgcolor="rgba(0,0,0,0)",
+ paper_bgcolor="rgba(0,0,0,0)",
+ margin=dict(l=0, r=0, t=44, b=0),
+ height=360,
+ )
+ st.plotly_chart(bridge_fig, use_container_width=True)
+
+ if market_cap and market_cap > 0:
+ compare_a, compare_b, compare_c, compare_d = st.columns(4)
+ compare_a.metric("Market Cap", fmt_large(market_cap))
+ if market_enterprise_value and market_enterprise_value > 0:
+ ev_delta = (result["enterprise_value"] - market_enterprise_value) / market_enterprise_value
+ compare_b.metric(
+ "Market Enterprise Value",
+ fmt_large(market_enterprise_value),
+ delta=f"{ev_delta * 100:+.1f}%",
+ )
+ equity_delta = (result["equity_value"] - market_cap) / market_cap
+ compare_c.metric("DCF Equity Value", fmt_large(result["equity_value"]), delta=f"{equity_delta * 100:+.1f}%")
+ compare_d.metric("Shares Outstanding", f"{ctx['shares'] / 1e6:,.1f}M")
+
+ summary_rows = [
+ {
+ "Step": "1. Start with base free cash flow",
+ "Value": fmt_large(result["base_fcf"]),
+ "What it means": "Most recent annual free cash flow used as the starting point.",
+ },
+ {
+ "Step": "2. Project and discount forecast cash flows",
+ "Value": fmt_large(result["fcf_pv_sum"]),
+ "What it means": f"Present value of {projection_years} years of projected FCF.",
+ },
+ {
+ "Step": "3. Add discounted terminal value",
+ "Value": fmt_large(result["terminal_value_pv"]),
+ "What it means": "Present value of cash flows beyond the explicit forecast period.",
+ },
+ {
+ "Step": "4. Arrive at enterprise value",
+ "Value": fmt_large(result["enterprise_value"]),
+ "What it means": "Value of the operations before debt, cash, and other claims.",
+ },
+ {
+ "Step": "5. Bridge to equity value",
+ "Value": fmt_large(result["equity_value"]),
+ "What it means": "Enterprise value less net debt, preferred equity, and minority interest.",
+ },
+ {
+ "Step": "6. Convert to value per share",
+ "Value": fmt_currency(iv),
+ "What it means": "Equity value divided by shares outstanding.",
+ },
+ ]
+ st.dataframe(pd.DataFrame(summary_rows), use_container_width=True, hide_index=True)
+
st.write("")
years = [f"Year {y}" for y in result["years"]]
@@ -637,6 +771,19 @@ def _render_ev_ebitda_model(ctx: dict):
imp_price = ev_result["implied_price_per_share"]
current_price = ctx["current_price"]
+ market_cap = ctx["market_cap"]
+ market_enterprise_value = None
+ if market_cap and market_cap > 0:
+ market_enterprise_value = (
+ float(market_cap)
+ + float(ctx["total_debt"])
+ - float(ctx["cash_and_equivalents"])
+ )
+
+ st.caption(
+ "This model applies a target EV/EBITDA multiple to current EBITDA, then bridges from enterprise value to equity value per share."
+ )
+
ev_m1, ev_m2, ev_m3, ev_m4 = st.columns(4)
ev_m1.metric("Implied Price / Share", fmt_currency(imp_price))
if current_price:
@@ -648,6 +795,24 @@ def _render_ev_ebitda_model(ctx: dict):
delta=f"{ev_upside * 100:+.1f}%",
)
ev_m4.metric("Implied EV", fmt_large(ev_result["implied_ev"]))
+
+ if current_price and current_price > 0:
+ valuation_gap = imp_price - current_price
+ market_message = "above" if valuation_gap > 0 else "below"
+ if abs(valuation_gap) < 0.005:
+ market_message = "roughly in line with"
+ st.markdown(
+ f"At **{target_multiple:.1f}x EBITDA**, the model implies **{fmt_currency(imp_price)} per share**, "
+ f"which is **{fmt_currency(abs(valuation_gap))} {market_message}** the current market price of "
+ f"**{fmt_currency(current_price)}**."
+ )
+
+ calc_a, calc_b, calc_c, calc_d = st.columns(4)
+ calc_a.metric("EBITDA Used", fmt_large(ctx["ebitda"]))
+ calc_b.metric("Target Multiple", f"{target_multiple:.1f}x")
+ calc_c.metric("Implied Enterprise Value", fmt_large(ev_result["implied_ev"]))
+ calc_d.metric("Implied Equity Value", fmt_large(ev_result["equity_value"]))
+
st.caption(
f"EBITDA: {fmt_large(ctx['ebitda'])} · "
f"{_net_debt_label(ev_result['net_debt'])}: {fmt_large(abs(ev_result['net_debt']))} · "
@@ -657,6 +822,93 @@ def _render_ev_ebitda_model(ctx: dict):
if source_date:
st.caption(f"EV/EBITDA bridge source date: **{source_date}**")
+ bridge_labels = ["Implied EV"]
+ bridge_measures = ["absolute"]
+ bridge_values = [float(ev_result["implied_ev"])]
+ bridge_text = [fmt_large(ev_result["implied_ev"])]
+
+ if ctx["total_debt"]:
+ bridge_labels.append("Add Debt")
+ bridge_measures.append("relative")
+ bridge_values.append(float(ctx["total_debt"]))
+ bridge_text.append(fmt_large(ctx["total_debt"]))
+ if ctx["cash_and_equivalents"]:
+ bridge_labels.append("Less Cash")
+ bridge_measures.append("relative")
+ bridge_values.append(-float(ctx["cash_and_equivalents"]))
+ bridge_text.append(fmt_large(-float(ctx["cash_and_equivalents"])))
+
+ bridge_labels.append("Implied Equity")
+ bridge_measures.append("total")
+ bridge_values.append(float(ev_result["equity_value"]))
+ bridge_text.append(fmt_large(ev_result["equity_value"]))
+
+ bridge_fig = go.Figure(
+ go.Waterfall(
+ x=bridge_labels,
+ measure=bridge_measures,
+ y=bridge_values,
+ text=bridge_text,
+ textposition="outside",
+ connector={"line": {"color": "#6f7785"}},
+ increasing={"marker": {"color": "#4F8EF7"}},
+ decreasing={"marker": {"color": "#F76E6E"}},
+ totals={"marker": {"color": "#2ecc71"}},
+ )
+ )
+ bridge_fig.update_layout(
+ title="EV/EBITDA Bridge To Equity Value",
+ yaxis_title="USD",
+ plot_bgcolor="rgba(0,0,0,0)",
+ paper_bgcolor="rgba(0,0,0,0)",
+ margin=dict(l=0, r=0, t=44, b=0),
+ height=320,
+ )
+ st.plotly_chart(bridge_fig, use_container_width=True)
+
+ if market_cap and market_cap > 0:
+ compare_a, compare_b, compare_c, compare_d = st.columns(4)
+ compare_a.metric("Market Cap", fmt_large(market_cap))
+ if market_enterprise_value and market_enterprise_value > 0:
+ ev_delta = (ev_result["implied_ev"] - market_enterprise_value) / market_enterprise_value
+ compare_b.metric(
+ "Market Enterprise Value",
+ fmt_large(market_enterprise_value),
+ delta=f"{ev_delta * 100:+.1f}%",
+ )
+ equity_delta = (ev_result["equity_value"] - market_cap) / market_cap
+ compare_c.metric("Model Equity Value", fmt_large(ev_result["equity_value"]), delta=f"{equity_delta * 100:+.1f}%")
+ compare_d.metric("Shares Outstanding", f"{ctx['shares'] / 1e6:,.1f}M")
+
+ summary_rows = [
+ {
+ "Step": "1. Start with EBITDA",
+ "Value": fmt_large(ctx["ebitda"]),
+ "What it means": "Current EBITDA used as the operating earnings base.",
+ },
+ {
+ "Step": "2. Apply target multiple",
+ "Value": f"{target_multiple:.1f}x",
+ "What it means": "Chosen EV/EBITDA multiple applied to EBITDA.",
+ },
+ {
+ "Step": "3. Arrive at enterprise value",
+ "Value": fmt_large(ev_result["implied_ev"]),
+ "What it means": "Implied value of the operating business before capital structure.",
+ },
+ {
+ "Step": "4. Bridge to equity value",
+ "Value": fmt_large(ev_result["equity_value"]),
+ "What it means": "Enterprise value less net debt.",
+ },
+ {
+ "Step": "5. Convert to value per share",
+ "Value": fmt_currency(imp_price),
+ "What it means": "Equity value divided by shares outstanding.",
+ },
+ ]
+ st.dataframe(pd.DataFrame(summary_rows), use_container_width=True, hide_index=True)
+
def _render_ev_revenue_model(ctx: dict):
st.markdown("**EV/Revenue Valuation**")
@@ -695,6 +947,19 @@ def _render_ev_revenue_model(ctx: dict):
implied_price = ev_revenue_result["implied_price_per_share"]
current_price = ctx["current_price"]
+ market_cap = ctx["market_cap"]
+ market_enterprise_value = None
+ if market_cap and market_cap > 0:
+ market_enterprise_value = (
+ float(market_cap)
+ + float(ctx["total_debt"])
+ - float(ctx["cash_and_equivalents"])
+ )
+
+ st.caption(
+ "This model applies a target EV/Revenue multiple to TTM revenue, then bridges from enterprise value to equity value per share."
+ )
+
evr_m1, evr_m2, evr_m3, evr_m4 = st.columns(4)
evr_m1.metric("Implied Price / Share", fmt_currency(implied_price))
if current_price:
@@ -706,6 +971,24 @@ def _render_ev_revenue_model(ctx: dict):
delta=f"{evr_upside * 100:+.1f}%",
)
evr_m4.metric("Implied EV", fmt_large(ev_revenue_result["implied_ev"]))
+
+ if current_price and current_price > 0:
+ valuation_gap = implied_price - current_price
+ market_message = "above" if valuation_gap > 0 else "below"
+ if abs(valuation_gap) < 0.005:
+ market_message = "roughly in line with"
+ st.markdown(
+ f"At **{target_multiple:.1f}x revenue**, the model implies **{fmt_currency(implied_price)} per share**, "
+ f"which is **{fmt_currency(abs(valuation_gap))} {market_message}** the current market price of "
+ f"**{fmt_currency(current_price)}**."
+ )
+
+ calc_a, calc_b, calc_c, calc_d = st.columns(4)
+ calc_a.metric("Revenue Used", fmt_large(ctx["revenue_ttm"]))
+ calc_b.metric("Target Multiple", f"{target_multiple:.1f}x")
+ calc_c.metric("Implied Enterprise Value", fmt_large(ev_revenue_result["implied_ev"]))
+ calc_d.metric("Implied Equity Value", fmt_large(ev_revenue_result["equity_value"]))
+
st.caption(
f"Revenue: {fmt_large(ctx['revenue_ttm'])} · "
f"{_net_debt_label(ev_revenue_result['net_debt'])}: {fmt_large(abs(ev_revenue_result['net_debt']))} · "
@@ -715,6 +998,93 @@ def _render_ev_revenue_model(ctx: dict):
if source_date:
st.caption(f"EV/Revenue bridge source date: **{source_date}**")
+ bridge_labels = ["Implied EV"]
+ bridge_measures = ["absolute"]
+ bridge_values = [float(ev_revenue_result["implied_ev"])]
+ bridge_text = [fmt_large(ev_revenue_result["implied_ev"])]
+
+ if ctx["total_debt"]:
+ bridge_labels.append("Add Debt")
+ bridge_measures.append("relative")
+ bridge_values.append(float(ctx["total_debt"]))
+ bridge_text.append(fmt_large(ctx["total_debt"]))
+ if ctx["cash_and_equivalents"]:
+ bridge_labels.append("Less Cash")
+ bridge_measures.append("relative")
+ bridge_values.append(-float(ctx["cash_and_equivalents"]))
+ bridge_text.append(fmt_large(-float(ctx["cash_and_equivalents"])))
+
+ bridge_labels.append("Implied Equity")
+ bridge_measures.append("total")
+ bridge_values.append(float(ev_revenue_result["equity_value"]))
+ bridge_text.append(fmt_large(ev_revenue_result["equity_value"]))
+
+ bridge_fig = go.Figure(
+ go.Waterfall(
+ x=bridge_labels,
+ measure=bridge_measures,
+ y=bridge_values,
+ text=bridge_text,
+ textposition="outside",
+ connector={"line": {"color": "#6f7785"}},
+ increasing={"marker": {"color": "#4F8EF7"}},
+ decreasing={"marker": {"color": "#F76E6E"}},
+ totals={"marker": {"color": "#2ecc71"}},
+ )
+ )
+ bridge_fig.update_layout(
+ title="EV/Revenue Bridge To Equity Value",
+ yaxis_title="USD",
+ plot_bgcolor="rgba(0,0,0,0)",
+ paper_bgcolor="rgba(0,0,0,0)",
+ margin=dict(l=0, r=0, t=44, b=0),
+ height=320,
+ )
+ st.plotly_chart(bridge_fig, use_container_width=True)
+
+ if market_cap and market_cap > 0:
+ compare_a, compare_b, compare_c, compare_d = st.columns(4)
+ compare_a.metric("Market Cap", fmt_large(market_cap))
+ if market_enterprise_value and market_enterprise_value > 0:
+ ev_delta = (ev_revenue_result["implied_ev"] - market_enterprise_value) / market_enterprise_value
+ compare_b.metric(
+ "Market Enterprise Value",
+ fmt_large(market_enterprise_value),
+ delta=f"{ev_delta * 100:+.1f}%",
+ )
+ equity_delta = (ev_revenue_result["equity_value"] - market_cap) / market_cap
+ compare_c.metric("Model Equity Value", fmt_large(ev_revenue_result["equity_value"]), delta=f"{equity_delta * 100:+.1f}%")
+ compare_d.metric("Shares Outstanding", f"{ctx['shares'] / 1e6:,.1f}M")
+
+ summary_rows = [
+ {
+ "Step": "1. Start with TTM revenue",
+ "Value": fmt_large(ctx["revenue_ttm"]),
+ "What it means": "Trailing twelve-month revenue used as the operating base.",
+ },
+ {
+ "Step": "2. Apply target multiple",
+ "Value": f"{target_multiple:.1f}x",
+ "What it means": "Chosen EV/Revenue multiple applied to TTM revenue.",
+ },
+ {
+ "Step": "3. Arrive at enterprise value",
+ "Value": fmt_large(ev_revenue_result["implied_ev"]),
+ "What it means": "Implied value of the operating business before capital structure.",
+ },
+ {
+ "Step": "4. Bridge to equity value",
+ "Value": fmt_large(ev_revenue_result["equity_value"]),
+ "What it means": "Enterprise value less net debt.",
+ },
+ {
+ "Step": "5. Convert to value per share",
+ "Value": fmt_currency(implied_price),
+ "What it means": "Equity value divided by shares outstanding.",
+ },
+ ]
+ st.dataframe(pd.DataFrame(summary_rows), use_container_width=True, hide_index=True)
+
def _render_price_to_book_model(ctx: dict):
st.markdown("**Price / Book Valuation**")