diff options
Diffstat (limited to 'components/valuation.py')
| -rw-r--r-- | components/valuation.py | 426 |
1 files changed, 153 insertions, 273 deletions
diff --git a/components/valuation.py b/components/valuation.py index 89e7d49..24405d6 100644 --- a/components/valuation.py +++ b/components/valuation.py @@ -8,6 +8,7 @@ from services.data_service import ( get_shares_outstanding, get_market_cap_computed, get_free_cash_flow_series, + get_free_cash_flow_ttm, get_revenue_ttm, get_balance_sheet_bridge_items, get_analyst_price_targets, @@ -334,7 +335,7 @@ def _build_model_context(ticker: str) -> dict: except Exception: fcf_series = pd.Series(dtype=float) - base_fcf = float(fcf_series.iloc[-1]) if not fcf_series.empty else None + base_fcf = _coerce_float(get_free_cash_flow_ttm(ticker)) hist_growth = compute_historical_growth_rate(fcf_series) if len(fcf_series) >= 2 else None ebitda = _coerce_float(ratios_data.get("ebitdaTTM")) revenue_ttm = _coerce_float(get_revenue_ttm(ticker)) @@ -391,12 +392,13 @@ def _build_model_context(ticker: str) -> dict: ev_value = None ev_ebitda_current = None ev_revenue_current = None + other_claims = preferred_equity + minority_interest if market_cap and market_cap > 0 and ebitda and ebitda > 0: - ev_value = float(market_cap) + total_debt - cash_and_equivalents + ev_value = float(market_cap) + total_debt - cash_and_equivalents + other_claims if ev_value > 0: ev_ebitda_current = ev_value / ebitda elif market_cap and market_cap > 0: - ev_value = float(market_cap) + total_debt - cash_and_equivalents + ev_value = float(market_cap) + total_debt - cash_and_equivalents + other_claims if ev_value and ev_value > 0 and revenue_ttm and revenue_ttm > 0: ev_revenue_current = ev_value / revenue_ttm @@ -530,6 +532,7 @@ def _render_dcf_model(ctx: dict): cash_and_equivalents=ctx["cash_and_equivalents"], preferred_equity=ctx["preferred_equity"], minority_interest=ctx["minority_interest"], + base_fcf_override=ctx["base_fcf"], ) if not result: @@ -557,27 +560,6 @@ def _render_dcf_model(ctx: dict): "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)) - if current_price: - upside = (iv - current_price) / current_price - m2.metric("Current Price", fmt_currency(current_price)) - 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" - implied_value = _escape_markdown_currency(fmt_currency(iv)) - gap_value = _escape_markdown_currency(fmt_currency(abs(valuation_gap))) - current_value = _escape_markdown_currency(fmt_currency(current_price)) - st.markdown( - f"The DCF implies **{implied_value} per share**, which is **{gap_value} " - f"{market_message}** the current market price of **{current_value}**." - ) - 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"])) @@ -592,103 +574,59 @@ def _render_dcf_model(ctx: dict): 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 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"])) - bridge_c.metric("Preferred Equity", fmt_large(ctx["preferred_equity"])) - bridge_d.metric("Minority Interest", fmt_large(ctx["minority_interest"])) - - bridge1, bridge2, bridge3, bridge4 = st.columns(4) - bridge1.metric("Enterprise Value", fmt_large(result["enterprise_value"])) - 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"])) - - 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"])) + years = [f"Year {y}" for y in result["years"]] + discounted = result["discounted_fcfs"] + terminal_pv = result["terminal_value_pv"] - 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", + fig = go.Figure(go.Bar( + x=years + ["Terminal Value"], + y=[(v / 1e9) for v in discounted] + [terminal_pv / 1e9], + marker_color=["#4F8EF7"] * len(years) + ["#F7A24F"], + text=[f"${v / 1e9:.2f}B" for v in discounted] + [f"${terminal_pv / 1e9:.2f}B"], + textposition="outside", + )) + fig.update_layout( + title="Enterprise Value Build: PV of Forecast FCFs + Terminal Value (Billions)", + yaxis_title="USD (Billions)", plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)", - margin=dict(l=0, r=0, t=44, b=0), + margin=dict(l=0, r=0, t=40, b=0), height=360, ) - st.plotly_chart(bridge_fig, use_container_width=True) + st.plotly_chart(fig, use_container_width=True) + + st.markdown("**Enterprise Value To Equity Value Bridge**") + st.caption("Enterprise value is adjusted for net debt or net cash and other claims to arrive at equity value.") + + bridge_a, bridge_b, bridge_c, bridge_d = st.columns(4) + bridge_a.metric("Enterprise Value", fmt_large(result["enterprise_value"])) + bridge_b.metric(_net_debt_label(result["net_debt"]), fmt_large(abs(result["net_debt"]))) + bridge_c.metric("Other Claims", fmt_large(ctx["preferred_equity"] + ctx["minority_interest"])) + bridge_d.metric("Equity Value", fmt_large(result["equity_value"])) + + detail_a, detail_b, detail_c = st.columns(3) + detail_a.metric("Total Debt", fmt_large(ctx["total_debt"])) + detail_b.metric("Cash & Equivalents", fmt_large(ctx["cash_and_equivalents"])) + detail_c.metric("Preferred + Minority", fmt_large(ctx["preferred_equity"] + ctx["minority_interest"])) 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)) + st.markdown("**Market Comparison**") + compare_a, compare_b = st.columns(2) if market_enterprise_value and market_enterprise_value > 0: ev_delta = (result["enterprise_value"] - market_enterprise_value) / market_enterprise_value - compare_b.metric( + compare_a.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") + compare_b.metric("Market Cap", fmt_large(market_cap), delta=f"{equity_delta * 100:+.1f}%") 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.", + "What it means": "Trailing-twelve-month free cash flow used as the starting point.", }, { "Step": "2. Project and discount forecast cash flows", @@ -720,26 +658,46 @@ def _render_dcf_model(ctx: dict): st.write("") - years = [f"Year {y}" for y in result["years"]] - discounted = result["discounted_fcfs"] - terminal_pv = result["terminal_value_pv"] + st.markdown("**DCF Conclusion**") + conclusion_a, conclusion_b, conclusion_c, conclusion_d = st.columns(4) + conclusion_a.metric("Equity Value / Share", fmt_currency(iv)) + if current_price: + upside = (iv - current_price) / current_price + conclusion_b.metric("Current Price", fmt_currency(current_price)) + conclusion_c.metric("Upside / Downside", f"{upside * 100:+.1f}%", delta=f"{upside * 100:+.1f}%") + else: + conclusion_b.metric("FCF Growth Used", f"{result['growth_rate_used'] * 100:.1f}%") + conclusion_d.metric("Shares Outstanding", f"{ctx['shares'] / 1e6:,.1f}M") - fig = go.Figure(go.Bar( - x=years + ["Terminal Value"], - y=[(v / 1e9) for v in discounted] + [terminal_pv / 1e9], - marker_color=["#4F8EF7"] * len(years) + ["#F7A24F"], - text=[f"${v / 1e9:.2f}B" for v in discounted] + [f"${terminal_pv / 1e9:.2f}B"], - textposition="outside", - )) - fig.update_layout( - title="Enterprise Value Build: PV of Forecast FCFs + Terminal Value (Billions)", - yaxis_title="USD (Billions)", - plot_bgcolor="rgba(0,0,0,0)", - paper_bgcolor="rgba(0,0,0,0)", - margin=dict(l=0, r=0, t=40, b=0), - height=360, - ) - st.plotly_chart(fig, use_container_width=True) + if current_price: + assumption_a, assumption_b = st.columns(2) + assumption_a.metric("FCF Growth Used", f"{result['growth_rate_used'] * 100:.1f}%") + assumption_b.metric("Equity Value", fmt_large(result["equity_value"])) + + 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" + implied_value = _escape_markdown_currency(fmt_currency(iv)) + gap_value = _escape_markdown_currency(fmt_currency(abs(valuation_gap))) + current_value = _escape_markdown_currency(fmt_currency(current_price)) + st.markdown( + f"The DCF implies **{implied_value} per share**, which is **{gap_value} " + f"{market_message}** the current market price of **{current_value}**." + ) + + 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 + preferred equity + minority interest.\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 quarterly balance sheet for debt, cash, preferred equity, and minority interest.\n" + "- **Base FCF:** computed as trailing-twelve-month free cash flow from the last four quarterly cash flow statements.\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." + ) def _render_ev_ebitda_model(ctx: dict): st.markdown("**EV/EBITDA Valuation**") @@ -768,6 +726,8 @@ def _render_ev_ebitda_model(ctx: dict): ebitda=float(ctx["ebitda"]), total_debt=ctx["total_debt"], total_cash=ctx["cash_and_equivalents"], + preferred_equity=ctx["preferred_equity"], + minority_interest=ctx["minority_interest"], shares_outstanding=float(ctx["shares"]), target_multiple=target_multiple, ) @@ -785,38 +745,14 @@ def _render_ev_ebitda_model(ctx: dict): float(market_cap) + float(ctx["total_debt"]) - float(ctx["cash_and_equivalents"]) + + float(ctx["preferred_equity"]) + + float(ctx["minority_interest"]) ) 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: - ev_upside = (imp_price - current_price) / current_price - ev_m2.metric("Current Price", fmt_currency(current_price)) - ev_m3.metric( - "Upside / Downside", - f"{ev_upside * 100:+.1f}%", - 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" - implied_value = _escape_markdown_currency(fmt_currency(imp_price)) - gap_value = _escape_markdown_currency(fmt_currency(abs(valuation_gap))) - current_value = _escape_markdown_currency(fmt_currency(current_price)) - st.markdown( - f"At **{target_multiple:.1f}x EBITDA**, the model implies **{implied_value} per share**, " - f"which is **{gap_value} {market_message}** the current market price of " - f"**{current_value}**." - ) - 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") @@ -826,69 +762,25 @@ def _render_ev_ebitda_model(ctx: dict): st.caption( f"EBITDA: {fmt_large(ctx['ebitda'])} · " f"{_net_debt_label(ev_result['net_debt'])}: {fmt_large(abs(ev_result['net_debt']))} · " + f"Other claims: {fmt_large(ev_result['other_claims'])} · " f"Equity Value: {fmt_large(ev_result['equity_value'])}" ) source_date = ctx["bridge_items"].get("source_date") 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)) + st.markdown("**Market Comparison**") + compare_a, compare_b = st.columns(2) if market_enterprise_value and market_enterprise_value > 0: ev_delta = (ev_result["implied_ev"] - market_enterprise_value) / market_enterprise_value - compare_b.metric( + compare_a.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") + compare_b.metric("Market Cap", fmt_large(market_cap), delta=f"{equity_delta * 100:+.1f}%") summary_rows = [ { @@ -909,7 +801,7 @@ def _render_ev_ebitda_model(ctx: dict): { "Step": "4. Bridge to equity value", "Value": fmt_large(ev_result["equity_value"]), - "What it means": "Enterprise value less net debt.", + "What it means": "Enterprise value less net debt and other claims.", }, { "Step": "5. Convert to value per share", @@ -919,6 +811,33 @@ def _render_ev_ebitda_model(ctx: dict): ] st.dataframe(pd.DataFrame(summary_rows), use_container_width=True, hide_index=True) + st.markdown("**EV/EBITDA Conclusion**") + ev_m1, ev_m2, ev_m3, ev_m4 = st.columns(4) + ev_m1.metric("Implied Price / Share", fmt_currency(imp_price)) + if current_price: + ev_upside = (imp_price - current_price) / current_price + ev_m2.metric("Current Price", fmt_currency(current_price)) + ev_m3.metric( + "Upside / Downside", + f"{ev_upside * 100:+.1f}%", + 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" + implied_value = _escape_markdown_currency(fmt_currency(imp_price)) + gap_value = _escape_markdown_currency(fmt_currency(abs(valuation_gap))) + current_value = _escape_markdown_currency(fmt_currency(current_price)) + st.markdown( + f"At **{target_multiple:.1f}x EBITDA**, the model implies **{implied_value} per share**, " + f"which is **{gap_value} {market_message}** the current market price of " + f"**{current_value}**." + ) + def _render_ev_revenue_model(ctx: dict): st.markdown("**EV/Revenue Valuation**") @@ -947,6 +866,8 @@ def _render_ev_revenue_model(ctx: dict): revenue=float(ctx["revenue_ttm"]), total_debt=ctx["total_debt"], total_cash=ctx["cash_and_equivalents"], + preferred_equity=ctx["preferred_equity"], + minority_interest=ctx["minority_interest"], shares_outstanding=float(ctx["shares"]), target_multiple=target_multiple, ) @@ -964,38 +885,14 @@ def _render_ev_revenue_model(ctx: dict): float(market_cap) + float(ctx["total_debt"]) - float(ctx["cash_and_equivalents"]) + + float(ctx["preferred_equity"]) + + float(ctx["minority_interest"]) ) 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: - evr_upside = (implied_price - current_price) / current_price - evr_m2.metric("Current Price", fmt_currency(current_price)) - evr_m3.metric( - "Upside / Downside", - f"{evr_upside * 100:+.1f}%", - 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" - implied_value = _escape_markdown_currency(fmt_currency(implied_price)) - gap_value = _escape_markdown_currency(fmt_currency(abs(valuation_gap))) - current_value = _escape_markdown_currency(fmt_currency(current_price)) - st.markdown( - f"At **{target_multiple:.1f}x revenue**, the model implies **{implied_value} per share**, " - f"which is **{gap_value} {market_message}** the current market price of " - f"**{current_value}**." - ) - 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") @@ -1005,69 +902,25 @@ def _render_ev_revenue_model(ctx: dict): 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']))} · " + f"Other claims: {fmt_large(ev_revenue_result['other_claims'])} · " f"Equity Value: {fmt_large(ev_revenue_result['equity_value'])}" ) source_date = ctx["bridge_items"].get("source_date") 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)) + st.markdown("**Market Comparison**") + compare_a, compare_b = st.columns(2) 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( + compare_a.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") + compare_b.metric("Market Cap", fmt_large(market_cap), delta=f"{equity_delta * 100:+.1f}%") summary_rows = [ { @@ -1088,7 +941,7 @@ def _render_ev_revenue_model(ctx: dict): { "Step": "4. Bridge to equity value", "Value": fmt_large(ev_revenue_result["equity_value"]), - "What it means": "Enterprise value less net debt.", + "What it means": "Enterprise value less net debt and other claims.", }, { "Step": "5. Convert to value per share", @@ -1098,6 +951,33 @@ def _render_ev_revenue_model(ctx: dict): ] st.dataframe(pd.DataFrame(summary_rows), use_container_width=True, hide_index=True) + st.markdown("**EV/Revenue Conclusion**") + evr_m1, evr_m2, evr_m3, evr_m4 = st.columns(4) + evr_m1.metric("Implied Price / Share", fmt_currency(implied_price)) + if current_price: + evr_upside = (implied_price - current_price) / current_price + evr_m2.metric("Current Price", fmt_currency(current_price)) + evr_m3.metric( + "Upside / Downside", + f"{evr_upside * 100:+.1f}%", + 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" + implied_value = _escape_markdown_currency(fmt_currency(implied_price)) + gap_value = _escape_markdown_currency(fmt_currency(abs(valuation_gap))) + current_value = _escape_markdown_currency(fmt_currency(current_price)) + st.markdown( + f"At **{target_multiple:.1f}x revenue**, the model implies **{implied_value} per share**, " + f"which is **{gap_value} {market_message}** the current market price of " + f"**{current_value}**." + ) + def _render_price_to_book_model(ctx: dict): st.markdown("**Price / Book Valuation**") |
