From 85160a558b2d37965ee2b8799fdd177ec986eadd Mon Sep 17 00:00:00 2001 From: Tyler Date: Sun, 17 May 2026 02:08:26 -0700 Subject: Fix valuation review: dead code, wrong peer bands, height scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove dead _build_dcf_canvas_html (~525 lines, superseded by _build_dcf_canvas_only_html) - Remove unused ev_ebit assignment - Fix FCF yield KPI strip and mini row using dividendYieldTTM as peer band proxy (pass None — no equivalent peer field for FCF yield) - Expand st.spinner in _render_historical_ratios to cover get_peers() and get_ratios_for_tickers() calls - Collapse _render_all_multiples / _render_multiples_model indirection into a single function - Replace Comps fixed 2600px height with max(1900, 1500 + n_peers*80), saving 400-700px whitespace for typical 5-8 peer sets Co-Authored-By: Claude Sonnet 4.6 --- components/valuation.py | 553 +----------------------------------------------- 1 file changed, 7 insertions(+), 546 deletions(-) (limited to 'components') diff --git a/components/valuation.py b/components/valuation.py index 407538e..f0fbdb9 100644 --- a/components/valuation.py +++ b/components/valuation.py @@ -465,7 +465,6 @@ def _render_ratios(ticker: str): peg = _r("pegRatioTTM") or (info.get("pegRatio") if info else None) ev_ebt = _r("enterpriseValueMultipleTTM") ev_rev = _r("evToSalesTTM") - ev_ebit = _r("evToOperatingCashFlowTTM") # best proxy if direct unavailable pb = _r("priceToBookRatioTTM") ps = _r("priceToSalesRatioTTM") fcf_yield_v = (fcf_ttm / market_cap) if fcf_ttm and market_cap and market_cap > 0 else None @@ -679,7 +678,7 @@ def _render_ratios(ticker: str): + _kpi_spark("EV / Revenue", ev_rev, "x", "evToSalesTTM", None, invert=True) + _kpi_spark("P / Book", pb, "x", "priceToBookRatioTTM", sparks.get("pb"), invert=True) + _kpi_spark("P / FCF", p_fcf, "x", "peRatioTTM", None, invert=True) - + _kpi_spark("FCF Yield", fcf_yield_v, "%", "dividendYieldTTM", None, invert=False) + + _kpi_spark("FCF Yield", fcf_yield_v, "%", None, None, invert=False) ) # ── Assemble val rows ─────────────────────────────────────────────────── @@ -727,7 +726,7 @@ def _render_ratios(ticker: str): cash_rows_html = ( '
MetricSubjectPeersTrend
' - + _mini_row("FCF yield", fcf_yield_v, "%", _pm("dividendYieldTTM"), None) + + _mini_row("FCF yield", fcf_yield_v, "%", None, None) + _mini_row("Dividend yield", div_y, "%", _pm("dividendYieldTTM"), None) + _mini_row("Payout ratio", payout, "%", _pm("dividendPayoutRatioTTM"), None, good_low=True) + _mini_row("Buyback yield", buyback_yield, "%", None, None) @@ -1213,531 +1212,6 @@ def _fmt_b(v_dollars: float) -> str: return f"${b:.2f}B" -def _build_dcf_canvas_html( - ctx: dict, - result: dict, - wacc_pct: float, - tg_pct: float, - yrs: int, - g_pct: float, - ev_ebitda_price: float | None, - ev_rev_price: float | None, - pb_price: float | None, -) -> str: - iv = result["intrinsic_value_per_share"] - market = float(ctx["current_price"] or 0) - has_market = market > 0 - - upside_pct = (iv - market) / market * 100 if has_market else 0.0 - is_pos = upside_pct >= 0 - gap = iv - market - - # Bridge - ev_b = _fmt_b(result["enterprise_value"]) - net_debt_b = _fmt_b(abs(result["net_debt"])) - other_claims_b = _fmt_b(ctx["preferred_equity"] + ctx["minority_interest"]) - equity_b = _fmt_b(result["equity_value"]) - total_debt_b = _fmt_b(ctx["total_debt"]) - cash_b = _fmt_b(ctx["cash_and_equivalents"]) - other_b_val = ctx["preferred_equity"] + ctx["minority_interest"] - - shares_b = ctx["shares"] / 1e9 - source_date = ctx["bridge_items"].get("source_date", "") - - # Forecast sequences (capped at yrs) - discounted = result["discounted_fcfs"][:yrs] - projected = result["projected_fcfs"][:yrs] - tv_pv = result["terminal_value_pv"] - terminal_fcf = projected[-1] * (1 + tg_pct / 100) if projected else 0.0 - disc_factors = [1.0 / (1 + wacc_pct / 100) ** (i + 1) for i in range(len(discounted))] - disc_tv_factor = 1.0 / (1 + wacc_pct / 100) ** yrs - - # Plotly chart data - bar_x = [f"Year {i + 1}" for i in range(len(discounted))] + ["Terminal"] - bar_y = [v / 1e9 for v in discounted] + [tv_pv / 1e9] - bar_colors = ["#243E5A"] * len(discounted) + ["#C2AA7A"] - bar_line_colors = ["#1F3B5E"] * len(discounted) + ["#DCC79E"] - bar_text = [_fmt_b(v) for v in discounted] + [_fmt_b(tv_pv)] - - plotly_data_json = json_for_script([{ - "type": "bar", - "x": bar_x, - "y": bar_y, - "marker": {"color": bar_colors, "line": {"color": bar_line_colors, "width": 1}}, - "text": bar_text, - "textposition": "outside", - "textfont": {"family": "IBM Plex Mono", "size": 10, "color": "#C7C0AE"}, - "hovertemplate": "%{x}: %{text}", - "cliponaxis": False, - }]) - plotly_layout_json = json_for_script({ - "paper_bgcolor": "#11151C", - "plot_bgcolor": "#11151C", - "margin": {"l": 48, "r": 8, "t": 28, "b": 36}, - "xaxis": { - "gridcolor": "rgba(0,0,0,0)", - "linecolor": "#232934", - "tickfont": {"family": "IBM Plex Sans", "size": 11, "color": "#8E8676"}, - "fixedrange": True, - }, - "yaxis": { - "gridcolor": "#232934", - "linecolor": "rgba(0,0,0,0)", - "tickfont": {"family": "IBM Plex Mono", "size": 10, "color": "#8E8676"}, - "tickprefix": "$", - "ticksuffix": "B", - "fixedrange": True, - "zeroline": False, - }, - "bargap": 0.35, - "showlegend": False, - "uniformtext": {"mode": "hide", "minsize": 8}, - }) - - data_json = json_for_script({ - "baseFcf": result["base_fcf"], - "netDebt": result["net_debt"], - "otherClaims": ctx["preferred_equity"] + ctx["minority_interest"], - "shares": ctx["shares"], - "market": float(ctx["current_price"] or 0), - }) - - # Verdict - verdict_gradient = ( - "linear-gradient(110deg,transparent 35%,rgba(79,140,94,.07) 100%)" - if is_pos else - "linear-gradient(110deg,transparent 35%,rgba(181,73,75,.07) 100%)" - ) - pill_cls = "pos" if is_pos else "neg" - pill_arrow = "▲" if is_pos else "▼" - pill_sign = "+" if is_pos else "−" - pill_text = f"{pill_arrow} {pill_sign}{abs(upside_pct):.1f}% {'upside' if is_pos else 'downside'}" - reading = "Constructive" if is_pos else "Cautious" - gap_dir = "above" if gap >= 0 else "below" - - iv_str = f"${iv:,.2f}" - market_str = f"${market:,.2f}" if has_market else "—" - gap_str = f"${abs(gap):,.2f}" - - # Cash-flow table - n = len(discounted) - hdr_cells = "".join(f"Yr {i + 1}" for i in range(n)) + "Terminal" - fcf_cells = "".join(f"{_fmt_b(v)}" for v in projected) - fcf_cells += f'{_fmt_b(terminal_fcf)}' - df_cells = "".join(f"{disc_factors[i]:.3f}" for i in range(n)) - df_cells += f"{disc_tv_factor:.3f}" - pv_cells = "".join(f"{_fmt_b(v)}" for v in discounted) - pv_cells += f'{_fmt_b(tv_pv)}' - - # Cross-check cells - def cx_cell(cls, lbl, val_str, delta_pct, meta): - if delta_pct is not None and has_market: - dcls = "pos" if delta_pct >= 0 else "neg" - dsign = "+" if delta_pct >= 0 else "" - dhtml = f'{dsign}{delta_pct:.1f}% vs market' - else: - dhtml = '' - return ( - f'
' - f'{lbl}' - f'{val_str}' - f"{dhtml}" - f'{meta}' - f"
" - ) - - dcf_delta = upside_pct if has_market else None - if dcf_delta is not None and has_market: - dcf_dcls = "pos" if dcf_delta >= 0 else "neg" - dcf_dsign = "+" if dcf_delta >= 0 else "" - dcf_dhtml = f'{dcf_dsign}{dcf_delta:.1f}% vs market' - else: - dcf_dhtml = '' - cx_dcf = ( - f'
' - f'DCF · THIS MODEL' - f'{iv_str}' - f"{dcf_dhtml}" - f'Firm-value DCF · {yrs}-yr explicit · WACC {wacc_pct:.1f}%' - f"
" - ) - - def _cx_multiple_cell(label, implied, market_multiple, mult_label): - if implied is not None and has_market: - delta = (implied - market) / market * 100 - val = f"${implied:,.2f}" - meta = f"Market multiple {market_multiple:.1f}× · {mult_label}" if market_multiple else mult_label - else: - delta = None - val = "—" - meta = "Unavailable for this company" - return cx_cell("va-cx-cell", label, val, delta, meta) - - cx_ev = _cx_multiple_cell( - "EV / EBITDA", ev_ebitda_price, - ctx.get("ev_ebitda_current") or 0, "based on current market multiple", - ) - cx_rev = _cx_multiple_cell( - "EV / REVENUE", ev_rev_price, - ctx.get("ev_revenue_current") or 0, "based on current market multiple", - ) - cx_pb = _cx_multiple_cell( - "P / BOOK", pb_price, - ctx.get("pb_current") or 0, "based on current market multiple", - ) - - # Recon gap cell color - gap_color = "var(--positive)" if gap >= 0 else "var(--negative)" - gap_sign = "+" if gap >= 0 else "" - gap_display = f"{gap_sign}${gap:,.2f}" if has_market else "—" - gap_pct_str = f"{upside_pct:.1f}% vs market" if has_market else "—" - - # Rail filing strings (static, Python-formatted) - net_debt_raw = ctx["total_debt"] - ctx["cash_and_equivalents"] - base_fcf_str = _fmt_b(result["base_fcf"]) - hist_growth_str = f"{result['growth_rate_used']*100:+.1f}%" - net_debt_str = _fmt_b(net_debt_raw) - shares_str = f"{ctx['shares']/1e9:.2f} B" - net_debt_label = f"Net debt{(' · ' + source_date) if source_date else ''}" - - html = f""" - - - - - - - - - - -
- - - -
- -
-
-
-
- DCF Intrinsic Value - {iv_str} - per share · firm value method · {yrs}-yr horizon -
- vs -
- Market Price - {market_str} - {pill_text} -
-
-
- Reading · DCF implies {gap_str} {gap_dir} the current market. - {reading} -
-
- -
-
-

Enterprise value build — present value of FCFs + terminal

- USD · billions · discounted at WACC {wacc_pct:.1f}% -
-
- - {hdr_cells} - - {fcf_cells} - {df_cells} - {pv_cells} - -
Forecast FCF
Discount factor
Present value
-
- -
-
-

From enterprise to equity

- Balance-sheet bridge{(' · ' + source_date) if source_date else ''} -
-
-
Enterprise value{ev_b}
-
Net debt
-
Net debt{net_debt_b}
-
Other claims
-
Other claims{other_claims_b}
-
=
-
Equity value{equity_b}
-
-
- Total debt {total_debt_b} - · - Cash & equiv. {cash_b} - · - Preferred + minority {_fmt_b(other_b_val)} -
-
- -
-
- Intrinsic · Per Share - {iv_str} - Equity value ÷ shares -
-
- Market · Last - {market_str} -   -
-
- Gap - {gap_display} - {gap_pct_str} -
-
- Shares Outstanding - {shares_b:.2f} B - diluted -
-
- -
-
-

Cross-check against the multiples

- Same business, different lenses · implied per-share -
-
- {cx_dcf} - {cx_ev} - {cx_rev} - {cx_pb} -
-
- -
- Firm-value DCF · enterprise value bridged to equity using debt & cash from the most recent balance sheet. Negative-FCF years are excluded from the base; terminal value uses Gordon Growth Model. - Methodology & sources ↗ -
- -
-
- - -""" - - return html - - def _build_multiples_canvas_html(ctx: dict) -> str: market = float(ctx["current_price"] or 0) shares = float(ctx["shares"] or 0) @@ -2700,22 +2174,11 @@ def _render_dcf_model(ctx: dict): components.html(canvas_html, height=1500, scrolling=False) -def _render_all_multiples(ctx: dict): - """Render all three multiples methods side-by-side in a single HTML canvas. - - Three lenses (EV/EBITDA, EV/Revenue, P/Book) are shown in a math-flow - comparison grid. All computation and slider interactivity happens client-side - in JS. No Streamlit sliders or rail column — one full-width components.html() - call only. - """ +def _render_multiples_model(ctx: dict): doc = _build_multiples_canvas_html(ctx) components.html(doc, height=1900, scrolling=False) -def _render_multiples_model(ctx: dict): - _render_all_multiples(ctx) - - def _render_ev_ebitda_model(ctx: dict): st.markdown("**EV/EBITDA Valuation**") st.caption( @@ -3375,7 +2838,7 @@ def _render_comps(ticker: str): "nPeers": n_peers, }) - total_height = 2600 + max(0, n_peers - 10) * 54 + total_height = max(1900, 1500 + n_peers * 80) ctx_html = ( '
' @@ -4434,6 +3897,9 @@ def _render_historical_ratios(ticker: str): info = get_company_info(ticker) with st.spinner("Loading historical ratios…"): hist_rows = get_historical_ratios(ticker, limit=10) + peers_raw = get_peers(ticker) + peers = [p for p in (peers_raw or []) if p.upper() != ticker.upper()][:6] + peer_ratios_list = get_ratios_for_tickers(peers) if peers else [] if not hist_rows: st.info("Historical ratio data unavailable.") return @@ -4443,11 +3909,6 @@ def _render_historical_ratios(ticker: str): y = str(r.get("date", ""))[:4] periods.append("FY" + y[2:] if len(y) == 4 else y) - # ── Sector median data from TTM peer ratios ─────────────────────────────── - peers_raw = get_peers(ticker) - peers = [p for p in (peers_raw or []) if p.upper() != ticker.upper()][:6] - peer_ratios_list = get_ratios_for_tickers(peers) if peers else [] - def _peer_median(field_ttm): vals = [] for pr in peer_ratios_list: -- cgit v1.3-2-g0d8e