From 0d888203cbc4dc596d0c05cedfeabe8785b263fc Mon Sep 17 00:00:00 2001 From: Tyler Date: Sat, 16 May 2026 00:02:32 -0700 Subject: Fix valuation and data robustness bugs --- app.py | 19 +++-- components/news.py | 11 ++- components/valuation.py | 85 +++++++++---------- services/data_service.py | 217 ++++++++++++++++++++++++++++++++++++----------- utils/security.py | 33 +++++++ 5 files changed, 261 insertions(+), 104 deletions(-) create mode 100644 utils/security.py diff --git a/app.py b/app.py index ff5ba35..bff6209 100644 --- a/app.py +++ b/app.py @@ -408,6 +408,7 @@ hr { import plotly.graph_objects as go import plotly.io as pio +from utils.security import escape_html, validate_outbound_url # ── Plotly theme ────────────────────────────────────────────────────────────── _prism_layout = go.Layout( @@ -543,6 +544,8 @@ with st.sidebar: co_name = info.get("longName", ticker) price = get_latest_price(ticker) prev_close = info.get("previousClose") or info.get("regularMarketPreviousClose") + ticker_html = escape_html(ticker) + co_name_html = escape_html(co_name) # Ticker + name st.markdown(f""" @@ -553,12 +556,12 @@ with st.sidebar: font-size: 2rem; color: #F2ECDC; line-height: 0.95; letter-spacing: -0.025em; margin-bottom: 4px; - ">{ticker} + ">{ticker_html}
{co_name}
+ ">{co_name_html} """, unsafe_allow_html=True) @@ -608,10 +611,10 @@ with st.sidebar: emp_str = f"{employees:,}" if isinstance(employees, int) else "—" rows = [ - ("Exchange", exchange), - ("Sector", sector), - ("Currency", currency), - ("Employees", emp_str), + ("Exchange", escape_html(exchange)), + ("Sector", escape_html(sector)), + ("Currency", escape_html(currency)), + ("Employees", escape_html(emp_str)), ] rows_html = "".join(f"""
@@ -628,11 +631,11 @@ with st.sidebar: ">{rows_html}
""", unsafe_allow_html=True) - website = info.get("website", "") + website = validate_outbound_url(info.get("website", "")) if website: st.markdown(f"""
- {headline_html}', + unsafe_allow_html=True, + ) else: - st.markdown(f"**{headline}**") + st.markdown(f"{headline_html}", unsafe_allow_html=True) meta = " · ".join(filter(None, [source, time_str])) if meta: st.caption(meta) diff --git a/components/valuation.py b/components/valuation.py index db352b3..9525c69 100644 --- a/components/valuation.py +++ b/components/valuation.py @@ -1,5 +1,4 @@ """Valuation panel — key ratios, models, comparable companies, analyst targets, earnings history.""" -import json import numpy as np import pandas as pd import plotly.graph_objects as go @@ -38,6 +37,7 @@ from services.valuation_service import ( compute_raw_historical_growth_rate, ) from utils.formatters import fmt_ratio, fmt_pct, fmt_large, fmt_currency +from utils.security import escape_html, json_for_script FINANCIAL_SECTORS = {"Financial Services"} @@ -116,6 +116,10 @@ def _escape_markdown_currency(value: str) -> str: return value.replace("$", r"\$") +def _h(value) -> str: + return escape_html(value) + + def render_valuation(ticker: str): tabs = st.tabs([ "Key Ratios", @@ -503,10 +507,10 @@ def _render_ratios(ticker: str): _XMAP = {"NYQ": "NYSE", "NMS": "NASDAQ", "NGM": "NASDAQ", "NCM": "NASDAQ", "ASE": "AMEX"} raw_x = (info.get("exchange", "") if info else "") or "" - exchange = _XMAP.get(raw_x, raw_x) or "—" - co_name = (info.get("longName", ticker) if info else ticker) or ticker - sector = (info.get("sector", "—") if info else "—") or "—" - industry = (info.get("industry", "—") if info else "—") or "—" + exchange = _h(_XMAP.get(raw_x, raw_x) or "—") + co_name = _h((info.get("longName", ticker) if info else ticker) or ticker) + sector = _h((info.get("sector", "—") if info else "—") or "—") + industry = _h((info.get("industry", "—") if info else "—") or "—") n_peers = len(peers) from datetime import date as _date today_str = _date.today().strftime("%b %d, %Y") @@ -1245,7 +1249,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 = json.dumps([{ + plotly_data_json = json_for_script([{ "type": "bar", "x": bar_x, "y": bar_y, @@ -1256,7 +1260,7 @@ def _build_dcf_canvas_html( "hovertemplate": "%{x}: %{text}", "cliponaxis": False, }]) - plotly_layout_json = json.dumps({ + plotly_layout_json = json_for_script({ "paper_bgcolor": "#11151C", "plot_bgcolor": "#11151C", "margin": {"l": 48, "r": 8, "t": 28, "b": 36}, @@ -1280,7 +1284,7 @@ def _build_dcf_canvas_html( "uniformtext": {"mode": "hide", "minsize": 8}, }) - data_json = json.dumps({ + data_json = json_for_script({ "baseFcf": result["base_fcf"], "netDebt": result["net_debt"], "otherClaims": ctx["preferred_equity"] + ctx["minority_interest"], @@ -1758,11 +1762,11 @@ def _build_multiples_canvas_html(ctx: dict) -> str: pr = get_ratios_for_tickers(peers[:6]) if pr: import statistics as _stats - eb_vs = [float(r["enterpriseValueMultipleTTM"]) for r in pr.values() + eb_vs = [float(r["enterpriseValueMultipleTTM"]) for r in pr if r and r.get("enterpriseValueMultipleTTM") and 2 < r["enterpriseValueMultipleTTM"] < 100] - rv_vs = [float(r["priceToSalesRatioTTM"]) for r in pr.values() - if r and r.get("priceToSalesRatioTTM") and 0.1 < r["priceToSalesRatioTTM"] < 50] - pb_vs = [float(r["priceToBookRatioTTM"]) for r in pr.values() + rv_vs = [float(r["evToSalesTTM"]) for r in pr + if r and r.get("evToSalesTTM") and 0.1 < r["evToSalesTTM"] < 50] + pb_vs = [float(r["priceToBookRatioTTM"]) for r in pr if r and r.get("priceToBookRatioTTM") and 0.5 < r["priceToBookRatioTTM"] < 200] if eb_vs: eb_sector = _stats.median(eb_vs) @@ -1777,7 +1781,7 @@ def _build_multiples_canvas_html(ctx: dict) -> str: rv_sector = _clamp(rv_sector, 4.0, 20.0) pb_sector = _clamp(pb_sector, 4.0, 60.0) - dcf_iv = st.session_state.get("dcf_intrinsic") + dcf_iv = st.session_state.get(f"dcf_intrinsic_{ctx['ticker']}") dcf_wacc = st.session_state.get(f"dcf_wacc_{ctx['ticker']}", 10.0) dcf_tg = st.session_state.get(f"dcf_tg_{ctx['ticker']}", 2.5) dcf_yrs = st.session_state.get(f"dcf_yrs_{ctx['ticker']}", 5) @@ -1891,9 +1895,9 @@ def _build_multiples_canvas_html(ctx: dict) -> str: dcf_meta_str = "Switch to DCF tab to compute" ticker = ctx["ticker"] - exchange = (ctx.get("info") or {}).get("exchange") or "—" + exchange = _h((ctx.get("info") or {}).get("exchange") or "—") - data_json = json.dumps({ + data_json = json_for_script({ "market": market, "shares": shares, "netDebt": net_debt, "totalDebt": total_debt, "cash": cash, "ebitda": ebitda, "revenue": revenue, "bookPs": book_ps, @@ -2269,7 +2273,7 @@ def _render_dcf_model(ctx: dict): st.warning(result["error"]) return - st.session_state["dcf_intrinsic"] = result["intrinsic_value_per_share"] + st.session_state[f"dcf_intrinsic_{ctx['ticker']}"] = result["intrinsic_value_per_share"] st.session_state[f"dcf_params_{ctx['ticker']}"] = {"wacc": wacc_pct, "tg": tg_pct, "yrs": yrs} # Cross-check: run other models at their current market multiples @@ -2704,8 +2708,9 @@ def _render_models(ticker: str): st.caption(ctx["summary"]) _render_model_availability(ctx) - if "models_view" not in st.session_state: - st.session_state["models_view"] = "dcf" + view_key = f"models_view_{ticker}" + if view_key not in st.session_state: + st.session_state[view_key] = "dcf" st.markdown(_DCF_RAIL_CSS, unsafe_allow_html=True) @@ -2714,24 +2719,24 @@ def _render_models(ticker: str): if st.button( "Discounted Cash Flow", key=f"pick_dcf_{ticker}", - type="primary" if st.session_state["models_view"] == "dcf" else "secondary", + type="primary" if st.session_state[view_key] == "dcf" else "secondary", width="stretch", ): - st.session_state["models_view"] = "dcf" + st.session_state[view_key] = "dcf" st.rerun() with _pc2: if st.button( "Multiples", key=f"pick_mult_{ticker}", - type="primary" if st.session_state["models_view"] == "multiples" else "secondary", + type="primary" if st.session_state[view_key] == "multiples" else "secondary", width="stretch", ): - st.session_state["models_view"] = "multiples" + st.session_state[view_key] = "multiples" st.rerun() st.markdown("---") - view = st.session_state.get("models_view", "dcf") + view = st.session_state.get(view_key, "dcf") if view == "dcf": if ctx["dcf_available"]: _render_dcf_model(ctx) @@ -2826,7 +2831,6 @@ _CC_CSS = """