From 1072357a6997ab273deb0cb383aa081aab448fe0 Mon Sep 17 00:00:00 2001 From: Tyler Date: Thu, 14 May 2026 00:00:45 -0700 Subject: Make DCF sliders live — no page reruns on drag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move all four DCF assumptions (WACC, TG, horizon, FCF growth) from st.slider widgets into the canvas iframe as native range inputs. A JavaScript runDCF() engine recomputes the full projection in the browser on every drag event, updating the verdict, bar chart (Plotly.react), cash-flow table, bridge, recon strip, and cross-check cell in place without a Streamlit round-trip. Python still runs run_dcf() once on page load (using session-state defaults) to populate dcf_intrinsic for the Multiples cross-check. The Recompute button in the rail clears API caches and reruns when fresh filing data is needed. Co-Authored-By: Claude Sonnet 4.6 --- components/valuation.py | 265 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 199 insertions(+), 66 deletions(-) diff --git a/components/valuation.py b/components/valuation.py index 53be09c..8d52ae0 100644 --- a/components/valuation.py +++ b/components/valuation.py @@ -755,7 +755,7 @@ def _build_dcf_canvas_html( bar_line_colors = ["#1F3B5E"] * len(discounted) + ["#DCC79E"] bar_text = [_fmt_b(v) for v in discounted] + [_fmt_b(tv_pv)] - plotly_data = json.dumps([{ + plotly_data_json = json.dumps([{ "type": "bar", "x": bar_x, "y": bar_y, @@ -766,7 +766,7 @@ def _build_dcf_canvas_html( "hovertemplate": "%{x}: %{text}", "cliponaxis": False, }]) - plotly_layout = json.dumps({ + plotly_layout_json = json.dumps({ "paper_bgcolor": "#11151C", "plot_bgcolor": "#11151C", "margin": {"l": 48, "r": 8, "t": 28, "b": 36}, @@ -790,6 +790,14 @@ def _build_dcf_canvas_html( "uniformtext": {"mode": "hide", "minsize": 8}, }) + data_json = json.dumps({ + "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%)" @@ -835,9 +843,19 @@ def _build_dcf_canvas_html( ) dcf_delta = upside_pct if has_market else None - cx_dcf = cx_cell( - "va-cx-cell dcf", "DCF · THIS MODEL", iv_str, dcf_delta, - f"Firm-value DCF · {yrs}-yr explicit · WACC {wacc_pct:.1f}%", + 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): @@ -878,44 +896,77 @@ def _build_dcf_canvas_html( - +
+
+
+
+
WACC{wacc_pct:.2f}%
+ +
+
+
Terminal growth{tg_pct:.1f}%
+ +
+
+
Forecast horizon{yrs} yr
+ +
+
+
FCF growth{g_pct:.1f}%
+ +
+
+ +
+
-
+
DCF Intrinsic Value - {iv_str} + {iv_str} per share · firm value method · {yrs}-yr horizon
vs
Market Price {market_str} - {pill_text} + {pill_text}
- Reading · DCF implies {gap_str} {gap_dir} the current market. - {reading} + 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}% + USD · billions · discounted at WACC {wacc_pct:.1f}%
- {hdr_cells} + {hdr_cells} - {fcf_cells} - {df_cells} - {pv_cells} + {fcf_cells} + {df_cells} + {pv_cells}
Forecast FCF
Discount factor
Present value
Forecast FCF
Discount factor
Present value
@@ -926,13 +977,13 @@ def _build_dcf_canvas_html( Balance-sheet bridge{(' · ' + source_date) if source_date else ''}
-
Enterprise value{ev_b}
+
Enterprise value{ev_b}
Net debt
Net debt{net_debt_b}
Other claims
Other claims{other_claims_b}
=
-
Equity value{equity_b}
+
Equity value{equity_b}
Total debt {total_debt_b} @@ -946,7 +997,7 @@ def _build_dcf_canvas_html(
Intrinsic · Per Share - {iv_str} + {iv_str} Equity value ÷ shares
@@ -956,8 +1007,8 @@ def _build_dcf_canvas_html(
Gap - {gap_display} - {gap_pct_str} + {gap_display} + {gap_pct_str}
Shares Outstanding @@ -986,9 +1037,123 @@ def _build_dcf_canvas_html(
""" @@ -1524,27 +1689,6 @@ def _render_dcf_model(ctx: dict): unsafe_allow_html=True, ) - wacc_pct = st.slider( - "WACC (%)", - min_value=4.0, max_value=15.0, value=10.0, step=0.25, - key=f"dcf_wacc_{ctx['ticker']}", - ) - tg_pct = st.slider( - "Terminal growth (%)", - min_value=0.0, max_value=5.0, value=2.5, step=0.1, - key=f"dcf_tg_{ctx['ticker']}", - ) - yrs = st.slider( - "Forecast horizon (yr)", - min_value=3, max_value=10, value=5, step=1, - key=f"dcf_yrs_{ctx['ticker']}", - ) - g_pct = st.slider( - "FCF growth (%)", - min_value=-15.0, max_value=20.0, value=round(slider_default, 1), step=0.1, - key=f"dcf_g_{ctx['ticker']}", - ) - st.markdown('
', unsafe_allow_html=True) net_debt_raw = ctx["total_debt"] - ctx["cash_and_equivalents"] @@ -1559,27 +1703,16 @@ def _render_dcf_model(ctx: dict): st.markdown('
', unsafe_allow_html=True) - btn_reset, btn_save, btn_recompute = st.columns(3) - with btn_reset: - if st.button("Reset", key=f"dcf_reset_{ctx['ticker']}", use_container_width=True): - st.session_state[f"dcf_wacc_{ctx['ticker']}"] = 10.0 - st.session_state[f"dcf_tg_{ctx['ticker']}"] = 2.5 - st.session_state[f"dcf_yrs_{ctx['ticker']}"] = 5 - st.session_state[f"dcf_g_{ctx['ticker']}"] = round(slider_default, 1) - st.rerun() - with btn_save: - st.button("Save scenario", key=f"dcf_save_{ctx['ticker']}", disabled=True, use_container_width=True) - with btn_recompute: - 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() - - # Guard: WACC must exceed terminal growth - if wacc_pct <= tg_pct: - with canvas_col: - st.warning(f"WACC ({wacc_pct:.2f}%) must be greater than terminal growth ({tg_pct:.2f}%). Adjust the sliders.") - return + 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() + + # 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)) + tg_pct = float(st.session_state.get(f"dcf_tg_{ctx['ticker']}", 2.5)) + yrs = int(st.session_state.get(f"dcf_yrs_{ctx['ticker']}", 5)) + g_pct = round(float(st.session_state.get(f"dcf_g_{ctx['ticker']}", round(slider_default, 1))), 1) result = run_dcf( fcf_series=ctx["fcf_series"], -- cgit v1.3-2-g0d8e