diff options
| author | Tyler <tyler@tylerhoang.xyz> | 2026-05-14 00:08:12 -0700 |
|---|---|---|
| committer | Tyler <tyler@tylerhoang.xyz> | 2026-05-14 00:08:12 -0700 |
| commit | 679baae2773dc7eaf419648ac7a59c4734fca735 (patch) | |
| tree | c800dd3f209567bffceef4c62ee0a87b693edc94 /components/valuation.py | |
| parent | 1072357a6997ab273deb0cb383aa081aab448fe0 (diff) | |
Move DCF sliders into left rail of canvas iframe
The sliders live inside the canvas iframe (keeping live JS behavior)
but are now in a narrow left rail column — the same visual layout as
before the live-DCF refactor. The canvas uses a 272px + 1fr grid:
left aside has the header, four range inputs, and the filings panel;
right div has all output sections unchanged.
Removes the horizontal va-controls bar added in the previous commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'components/valuation.py')
| -rw-r--r-- | components/valuation.py | 320 |
1 files changed, 169 insertions, 151 deletions
diff --git a/components/valuation.py b/components/valuation.py index 8d52ae0..6fc0171 100644 --- a/components/valuation.py +++ b/components/valuation.py @@ -493,6 +493,7 @@ _DCF_CANVAS_CSS = """ --oxford:#1F3B5E;--oxford-light:#243E5A; --positive:#4F8C5E;--positive-bg:#15241A; --negative:#B5494B;--negative-bg:#2A1517; + --warning:#C49545;--warning-bg:#2A1F0A; --font-display:'EB Garamond',Georgia,serif; --font-sans:'IBM Plex Sans',system-ui,sans-serif; --font-mono:'IBM Plex Mono',monospace; @@ -888,6 +889,13 @@ def _build_dcf_canvas_html( 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" + html = f"""<!DOCTYPE html> <html> <head> @@ -897,144 +905,183 @@ def _build_dcf_canvas_html( <link href="https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;1,400;1,500&family=IBM+Plex+Mono:wght@300;400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <script src="https://cdn.plot.ly/plotly-2.35.2.min.js" charset="utf-8"></script> <style>{_DCF_CANVAS_CSS} -.va-controls{{background:var(--ink-1);border:1px solid var(--line-1);border-radius:6px;padding:16px 24px}} -.ctrl-grid{{display:grid;grid-template-columns:repeat(4,1fr);gap:20px}} -.ctrl-item{{display:flex;flex-direction:column;gap:6px}} -.ctrl-head{{display:flex;justify-content:space-between;align-items:baseline}} -.ctrl-lbl{{font-family:var(--font-sans);font-size:11px;text-transform:uppercase;letter-spacing:.12em;color:var(--fg-3)}} -.ctrl-val{{font-family:var(--font-mono);font-size:14px;color:var(--brass-bright);font-variant-numeric:tabular-nums}} -.ctrl-warn{{font-family:var(--font-sans);font-size:12px;color:var(--warning);margin-top:8px;padding:6px 10px;background:var(--warning-bg);border-radius:4px}} -.va-controls input[type=range]{{width:100%;-webkit-appearance:none;appearance:none;background:var(--ink-3);height:4px;border-radius:999px;cursor:pointer;outline:none;margin-top:2px}} -.va-controls input[type=range]::-webkit-slider-thumb{{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--brass);border:2px solid #0B0E13;box-shadow:0 0 0 1px var(--brass-deep);cursor:pointer}} -.va-controls input[type=range]::-moz-range-thumb{{width:14px;height:14px;border-radius:50%;background:var(--brass);border:2px solid #0B0E13;cursor:pointer;border:none}} +/* 2-col inspector layout */ +.dcf-inspector{{display:grid;grid-template-columns:272px 1fr;min-height:100%;background:var(--ink-0)}} +.dcf-rail{{padding:20px 16px 32px;border-right:1px solid var(--line-1);display:flex;flex-direction:column;gap:0;background:var(--ink-0)}} +.dcf-canvas-inner{{display:flex;flex-direction:column;gap:24px;padding:24px 24px 48px}} +/* Rail type */ +.dcf-eyebrow{{font-family:var(--font-sans);font-size:11px;text-transform:uppercase;letter-spacing:.18em;color:var(--fg-3);font-weight:600;line-height:1}} +.dcf-title{{font-family:'EB Garamond',Georgia,serif;font-size:20px;font-weight:500;letter-spacing:-.01em;color:var(--fg-1);margin:6px 0 0;line-height:1.2}} +.dcf-sub{{font-family:var(--font-sans);font-size:12px;color:var(--fg-3);margin-top:6px;line-height:1.5}} +.dcf-divider{{border:none;border-top:1px solid var(--line-1);margin:14px 0}} +.dcf-filings-eyebrow{{font-family:var(--font-sans);font-size:11px;text-transform:uppercase;letter-spacing:.18em;color:var(--fg-3);font-weight:600;margin-bottom:10px}} +.dcf-filing-row{{display:flex;justify-content:space-between;align-items:baseline;font-family:var(--font-mono);font-size:12px;color:var(--fg-2);margin-bottom:6px}} +.dcf-filing-val{{color:var(--fg-1);font-variant-numeric:tabular-nums}} +/* Rail sliders */ +.rail-sliders{{display:flex;flex-direction:column;gap:14px;margin-top:14px}} +.rail-sl-item{{display:flex;flex-direction:column;gap:5px}} +.rail-sl-head{{display:flex;justify-content:space-between;align-items:baseline}} +.rail-sl-lbl{{font-family:var(--font-sans);font-size:12px;color:var(--fg-2)}} +.rail-sl-val{{font-family:var(--font-mono);font-size:12px;color:var(--brass-bright);font-variant-numeric:tabular-nums}} +.rail-warn{{font-family:var(--font-sans);font-size:11px;color:var(--warning);padding:6px 8px;background:var(--warning-bg);border-radius:4px;margin-top:4px}} +.dcf-rail input[type=range]{{width:100%;-webkit-appearance:none;appearance:none;background:var(--ink-3);height:4px;border-radius:999px;cursor:pointer;outline:none}} +.dcf-rail input[type=range]::-webkit-slider-thumb{{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--brass);border:2px solid #0B0E13;box-shadow:0 0 0 1px var(--brass-deep);cursor:pointer}} +.dcf-rail input[type=range]::-moz-range-thumb{{width:14px;height:14px;border-radius:50%;background:var(--brass);border:2px solid #0B0E13;cursor:pointer;border:none}} </style> </head> <body> -<div class="va-canvas"> +<div class="dcf-inspector"> + + <aside class="dcf-rail"> + <span class="dcf-eyebrow">Assumptions</span> + <div class="dcf-title">3-stage DCF</div> + <div class="dcf-sub">Firm-value DCF — projects free cash flow, discounts to today, bridges to equity per share.</div> - <section class="va-controls"> - <div class="ctrl-grid"> - <div class="ctrl-item"> - <div class="ctrl-head"><span class="ctrl-lbl">WACC</span><span class="ctrl-val" id="wacc-disp">{wacc_pct:.2f}%</span></div> + <div class="rail-sliders"> + <div class="rail-sl-item"> + <div class="rail-sl-head"> + <span class="rail-sl-lbl">WACC (%)</span> + <span class="rail-sl-val" id="wacc-disp">{wacc_pct:.2f}%</span> + </div> <input type="range" id="sl-wacc" min="4" max="15" step="0.25" value="{wacc_pct}"> </div> - <div class="ctrl-item"> - <div class="ctrl-head"><span class="ctrl-lbl">Terminal growth</span><span class="ctrl-val" id="tg-disp">{tg_pct:.1f}%</span></div> + <div class="rail-sl-item"> + <div class="rail-sl-head"> + <span class="rail-sl-lbl">Terminal growth (%)</span> + <span class="rail-sl-val" id="tg-disp">{tg_pct:.1f}%</span> + </div> <input type="range" id="sl-tg" min="0" max="5" step="0.1" value="{tg_pct}"> </div> - <div class="ctrl-item"> - <div class="ctrl-head"><span class="ctrl-lbl">Forecast horizon</span><span class="ctrl-val" id="yrs-disp">{yrs} yr</span></div> + <div class="rail-sl-item"> + <div class="rail-sl-head"> + <span class="rail-sl-lbl">Forecast horizon (yr)</span> + <span class="rail-sl-val" id="yrs-disp">{yrs} yr</span> + </div> <input type="range" id="sl-yrs" min="3" max="10" step="1" value="{yrs}"> </div> - <div class="ctrl-item"> - <div class="ctrl-head"><span class="ctrl-lbl">FCF growth</span><span class="ctrl-val" id="g-disp">{g_pct:.1f}%</span></div> + <div class="rail-sl-item"> + <div class="rail-sl-head"> + <span class="rail-sl-lbl">FCF growth (%)</span> + <span class="rail-sl-val" id="g-disp">{g_pct:.1f}%</span> + </div> <input type="range" id="sl-g" min="-15" max="20" step="0.1" value="{g_pct}"> </div> </div> - <div class="ctrl-warn" id="wacc-tg-warn" style="display:none">WACC must exceed terminal growth — adjust the sliders</div> - </section> + <div class="rail-warn" id="wacc-tg-warn" style="display:none">WACC must exceed terminal growth</div> + + <hr class="dcf-divider"> - <section class="va-verdict" style="--verdict-gradient:{verdict_gradient}"> - <div id="verdict-grad" style="position:absolute;inset:0;background:{verdict_gradient};pointer-events:none;z-index:0"></div> - <div class="top"> - <div class="col"> - <span class="lbl">DCF Intrinsic Value</span> - <span class="big num" id="iv-big">{iv_str}</span> - <span class="sub">per share · firm value method · {yrs}-yr horizon</span> + <div class="dcf-filings-eyebrow">From the filings</div> + <div class="dcf-filing-row"><span>Base FCF (TTM)</span><span class="dcf-filing-val">{base_fcf_str}</span></div> + <div class="dcf-filing-row"><span>FCF · 5-yr median</span><span class="dcf-filing-val">{hist_growth_str}</span></div> + <div class="dcf-filing-row"><span>Net debt</span><span class="dcf-filing-val">{net_debt_str}</span></div> + <div class="dcf-filing-row"><span>Shares outstanding</span><span class="dcf-filing-val">{shares_str}</span></div> + </aside> + + <div class="dcf-canvas-inner"> + + <section class="va-verdict" style="--verdict-gradient:{verdict_gradient}"> + <div id="verdict-grad" style="position:absolute;inset:0;background:{verdict_gradient};pointer-events:none;z-index:0"></div> + <div class="top"> + <div class="col"> + <span class="lbl">DCF Intrinsic Value</span> + <span class="big num" id="iv-big">{iv_str}</span> + <span class="sub">per share · firm value method · {yrs}-yr horizon</span> + </div> + <span class="arrow">vs</span> + <div class="col" style="align-items:flex-end"> + <span class="lbl">Market Price</span> + <span class="big market num">{market_str}</span> + <span class="pill {pill_cls}" id="upside-pill">{pill_text}</span> + </div> </div> - <span class="arrow">vs</span> - <div class="col" style="align-items:flex-end"> - <span class="lbl">Market Price</span> - <span class="big market num">{market_str}</span> - <span class="pill {pill_cls}" id="upside-pill">{pill_text}</span> + <div class="band"> + <span>Reading · DCF implies <span class="mono" id="gap-str">{gap_str}</span> <span id="gap-dir">{gap_dir}</span> the current market.</span> + <span class="reading" id="reading-str">{reading}</span> </div> - </div> - <div class="band"> - <span>Reading · DCF implies <span class="mono" id="gap-str">{gap_str}</span> <span id="gap-dir">{gap_dir}</span> the current market.</span> - <span class="reading" id="reading-str">{reading}</span> - </div> - </section> + </section> - <section class="va-projection"> - <div class="head"> - <h3>Enterprise value build — present value of FCFs + terminal</h3> - <span class="units" id="chart-units">USD · billions · discounted at WACC {wacc_pct:.1f}%</span> - </div> - <div id="dcf-chart" style="width:100%;height:260px"></div> - <table class="va-cf-table"> - <thead><tr id="cf-thead"><th></th>{hdr_cells}</tr></thead> - <tbody> - <tr id="cf-fcf"><td>Forecast FCF</td>{fcf_cells}</tr> - <tr id="cf-df"><td>Discount factor</td>{df_cells}</tr> - <tr class="total" id="cf-pv"><td>Present value</td>{pv_cells}</tr> - </tbody> - </table> - </section> + <section class="va-projection"> + <div class="head"> + <h3>Enterprise value build — present value of FCFs + terminal</h3> + <span class="units" id="chart-units">USD · billions · discounted at WACC {wacc_pct:.1f}%</span> + </div> + <div id="dcf-chart" style="width:100%;height:260px"></div> + <table class="va-cf-table"> + <thead><tr id="cf-thead"><th></th>{hdr_cells}</tr></thead> + <tbody> + <tr id="cf-fcf"><td>Forecast FCF</td>{fcf_cells}</tr> + <tr id="cf-df"><td>Discount factor</td>{df_cells}</tr> + <tr class="total" id="cf-pv"><td>Present value</td>{pv_cells}</tr> + </tbody> + </table> + </section> - <section class="va-bridge"> - <div class="bhead"> - <h3>From enterprise to equity</h3> - <span class="bdate">Balance-sheet bridge{(' · ' + source_date) if source_date else ''}</span> - </div> - <div class="flow"> - <div class="node start"><span class="lbl">Enterprise value</span><span class="v num" id="ev-node-val">{ev_b}</span></div> - <div class="op">−<span class="sub">Net debt</span></div> - <div class="node"><span class="lbl">Net debt</span><span class="v num">{net_debt_b}</span></div> - <div class="op">−<span class="sub">Other claims</span></div> - <div class="node"><span class="lbl">Other claims</span><span class="v num">{other_claims_b}</span></div> - <div class="op">=</div> - <div class="node result"><span class="lbl">Equity value</span><span class="v num" id="equity-node-val">{equity_b}</span></div> - </div> - <div class="bfoot"> - <span>Total debt {total_debt_b}</span> - <span>·</span> - <span>Cash & equiv. {cash_b}</span> - <span>·</span> - <span>Preferred + minority {_fmt_b(other_b_val)}</span> - </div> - </section> + <section class="va-bridge"> + <div class="bhead"> + <h3>From enterprise to equity</h3> + <span class="bdate">Balance-sheet bridge{(' · ' + source_date) if source_date else ''}</span> + </div> + <div class="flow"> + <div class="node start"><span class="lbl">Enterprise value</span><span class="v num" id="ev-node-val">{ev_b}</span></div> + <div class="op">−<span class="sub">Net debt</span></div> + <div class="node"><span class="lbl">Net debt</span><span class="v num">{net_debt_b}</span></div> + <div class="op">−<span class="sub">Other claims</span></div> + <div class="node"><span class="lbl">Other claims</span><span class="v num">{other_claims_b}</span></div> + <div class="op">=</div> + <div class="node result"><span class="lbl">Equity value</span><span class="v num" id="equity-node-val">{equity_b}</span></div> + </div> + <div class="bfoot"> + <span>Total debt {total_debt_b}</span> + <span>·</span> + <span>Cash & equiv. {cash_b}</span> + <span>·</span> + <span>Preferred + minority {_fmt_b(other_b_val)}</span> + </div> + </section> - <section class="va-recon"> - <div class="cell intrinsic"> - <span class="lbl">Intrinsic · Per Share</span> - <span class="v num" id="recon-iv">{iv_str}</span> - <span class="sub">Equity value ÷ shares</span> - </div> - <div class="cell"> - <span class="lbl">Market · Last</span> - <span class="v num">{market_str}</span> - <span class="sub"> </span> - </div> - <div class="cell"> - <span class="lbl">Gap</span> - <span class="v num" id="recon-gap" style="color:{gap_color}">{gap_display}</span> - <span class="sub" id="recon-gap-pct">{gap_pct_str}</span> - </div> - <div class="cell"> - <span class="lbl">Shares Outstanding</span> - <span class="v num">{shares_b:.2f} B</span> - <span class="sub">diluted</span> - </div> - </section> + <section class="va-recon"> + <div class="cell intrinsic"> + <span class="lbl">Intrinsic · Per Share</span> + <span class="v num" id="recon-iv">{iv_str}</span> + <span class="sub">Equity value ÷ shares</span> + </div> + <div class="cell"> + <span class="lbl">Market · Last</span> + <span class="v num">{market_str}</span> + <span class="sub"> </span> + </div> + <div class="cell"> + <span class="lbl">Gap</span> + <span class="v num" id="recon-gap" style="color:{gap_color}">{gap_display}</span> + <span class="sub" id="recon-gap-pct">{gap_pct_str}</span> + </div> + <div class="cell"> + <span class="lbl">Shares Outstanding</span> + <span class="v num">{shares_b:.2f} B</span> + <span class="sub">diluted</span> + </div> + </section> - <section class="va-cx"> - <div class="va-cx-head"> - <h3>Cross-check against the multiples</h3> - <span class="hint">Same business, different lenses · implied per-share</span> - </div> - <div class="va-cx-grid"> - {cx_dcf} - {cx_ev} - {cx_rev} - {cx_pb} + <section class="va-cx"> + <div class="va-cx-head"> + <h3>Cross-check against the multiples</h3> + <span class="hint">Same business, different lenses · implied per-share</span> + </div> + <div class="va-cx-grid"> + {cx_dcf} + {cx_ev} + {cx_rev} + {cx_pb} + </div> + </section> + + <div class="va-foot"> + <span>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.</span> + <a href="#">Methodology & sources ↗</a> </div> - </section> - <div class="va-foot"> - <span>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.</span> - <a href="#">Methodology & sources ↗</a> </div> - </div> <script> var D = {data_json}; @@ -1679,34 +1726,10 @@ def _render_dcf_model(ctx: dict): st.markdown(_DCF_RAIL_CSS, unsafe_allow_html=True) - rail_col, canvas_col = st.columns([1, 3], gap="medium") - - with rail_col: - st.markdown( - '<span class="dcf-eyebrow">Assumptions</span>' - '<div class="dcf-title">3-stage DCF</div>' - '<div class="dcf-sub">Firm-value DCF — projects free cash flow, discounts to today, bridges to equity per share.</div>', - unsafe_allow_html=True, - ) - - st.markdown('<hr class="dcf-divider">', unsafe_allow_html=True) - - net_debt_raw = ctx["total_debt"] - ctx["cash_and_equivalents"] - st.markdown( - '<div class="dcf-filings-eyebrow">From the filings</div>' - f'<div class="dcf-filing-row"><span>Base FCF (TTM)</span><span class="dcf-filing-val">{_fmt_b(ctx["base_fcf"])}</span></div>' - f'<div class="dcf-filing-row"><span>FCF · 5-yr median</span><span class="dcf-filing-val">{hist_growth_raw_pct:+.1f}%</span></div>' - f'<div class="dcf-filing-row"><span>Net debt</span><span class="dcf-filing-val">{_fmt_b(net_debt_raw)}</span></div>' - f'<div class="dcf-filing-row"><span>Shares outstanding</span><span class="dcf-filing-val">{ctx["shares"] / 1e9:.2f} B</span></div>', - unsafe_allow_html=True, - ) - - st.markdown('<hr class="dcf-divider">', unsafe_allow_html=True) - - if st.button("Recompute", key=f"dcf_recompute_{ctx['ticker']}", type="primary", use_container_width=True): - get_free_cash_flow_ttm.clear() - get_balance_sheet_bridge_items.clear() - st.rerun() + if st.button("Recompute", key=f"dcf_recompute_{ctx['ticker']}", type="primary"): + get_free_cash_flow_ttm.clear() + get_balance_sheet_bridge_items.clear() + st.rerun() # Read assumptions from session state (set by in-canvas JS sliders, defaulting on first load) wacc_pct = float(st.session_state.get(f"dcf_wacc_{ctx['ticker']}", 10.0)) @@ -1729,12 +1752,10 @@ def _render_dcf_model(ctx: dict): ) if not result: - with canvas_col: - st.warning("Insufficient data to run DCF model.") + st.warning("Insufficient data to run DCF model.") return if result.get("error"): - with canvas_col: - st.warning(result["error"]) + st.warning(result["error"]) return st.session_state["dcf_intrinsic"] = result["intrinsic_value_per_share"] @@ -1780,10 +1801,7 @@ def _render_dcf_model(ctx: dict): ev_ebitda_price, ev_rev_price, pb_price, ) - with canvas_col: - # Height: base sections + per-year table width is constant in rows - canvas_height = 1620 - components.html(canvas_html, height=canvas_height, scrolling=False) + components.html(canvas_html, height=1620, scrolling=False) def _render_multiples_model(ctx: dict): |
