diff options
| author | Tyler <tyler@tylerhoang.xyz> | 2026-04-02 18:11:18 -0700 |
|---|---|---|
| committer | Tyler <tyler@tylerhoang.xyz> | 2026-04-02 18:11:18 -0700 |
| commit | 0c146fec412fb99153fbb32fa90f0211aa0c8b32 (patch) | |
| tree | e87425e7e1e7cace303488ab0be4a5b50db4ea77 /components | |
| parent | 7a267bc3c28bc7a77e84eaa400667a7b4c0d5adf (diff) | |
Improve valuation model clarity
Diffstat (limited to 'components')
| -rw-r--r-- | components/top_movers.py | 13 | ||||
| -rw-r--r-- | components/valuation.py | 372 |
2 files changed, 382 insertions, 3 deletions
diff --git a/components/top_movers.py b/components/top_movers.py index ea72fc6..5589df6 100644 --- a/components/top_movers.py +++ b/components/top_movers.py @@ -8,6 +8,10 @@ DEFAULT_VISIBLE_MOVERS = 3 MAX_MOVERS = 8 +def _toggle_mover_tab(state_key: str): + st.session_state[state_key] = not st.session_state.get(state_key, False) + + def _inject_styles(): st.markdown( """ @@ -127,8 +131,13 @@ def _render_mover_tab(screen: str, state_key: str): if len(quotes) > DEFAULT_VISIBLE_MOVERS: button_label = "Show Less" if expanded else f"Show More ({len(quotes) - DEFAULT_VISIBLE_MOVERS} more)" - if st.button(button_label, key=f"{state_key}_button", use_container_width=True): - st.session_state[state_key] = not expanded + st.button( + button_label, + key=f"{state_key}_button", + use_container_width=True, + on_click=_toggle_mover_tab, + args=(state_key,), + ) @st.fragment 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**") |
