aboutsummaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/valuation.py320
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 &middot; firm value method &middot; {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 &middot; firm value method &middot; {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 &middot; 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 &middot; 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 &mdash; present value of FCFs + terminal</h3>
- <span class="units" id="chart-units">USD &middot; billions &middot; 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 &mdash; present value of FCFs + terminal</h3>
+ <span class="units" id="chart-units">USD &middot; billions &middot; 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{(' &middot; ' + 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">&minus;<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">&minus;<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>&middot;</span>
- <span>Cash &amp; equiv. {cash_b}</span>
- <span>&middot;</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{(' &middot; ' + 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">&minus;<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">&minus;<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>&middot;</span>
+ <span>Cash &amp; equiv. {cash_b}</span>
+ <span>&middot;</span>
+ <span>Preferred + minority {_fmt_b(other_b_val)}</span>
+ </div>
+ </section>
- <section class="va-recon">
- <div class="cell intrinsic">
- <span class="lbl">Intrinsic &middot; Per Share</span>
- <span class="v num" id="recon-iv">{iv_str}</span>
- <span class="sub">Equity value &divide; shares</span>
- </div>
- <div class="cell">
- <span class="lbl">Market &middot; Last</span>
- <span class="v num">{market_str}</span>
- <span class="sub">&nbsp;</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 &middot; Per Share</span>
+ <span class="v num" id="recon-iv">{iv_str}</span>
+ <span class="sub">Equity value &divide; shares</span>
+ </div>
+ <div class="cell">
+ <span class="lbl">Market &middot; Last</span>
+ <span class="v num">{market_str}</span>
+ <span class="sub">&nbsp;</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 &middot; 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 &middot; 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 &middot; enterprise value bridged to equity using debt &amp; 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 &amp; sources &nearr;</a>
</div>
- </section>
- <div class="va-foot">
- <span>Firm-value DCF &middot; enterprise value bridged to equity using debt &amp; 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 &amp; sources &nearr;</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):