"""Watchlist sidebar component — session-scoped personal watchlist.""" import streamlit as st import streamlit.components.v1 as components from services.data_service import get_latest_price, get_company_info from utils.security import escape_html def render_watchlist(): """Render the personal watchlist section in the sidebar. Displays a section label, then one clickable row per saved ticker showing symbol · price · Δ%. Clicking a row sets st.session_state["ticker"] and triggers a rerun. Shows an empty-state message when the list is empty. Must be called from within a `with st.sidebar:` block. """ watchlist = st.session_state.get("watchlist", []) # Process any pending click from the JS click handler clicked = st.session_state.get("_wl_click", "") if clicked and clicked in watchlist: st.session_state["ticker"] = clicked st.session_state["_wl_click"] = "" st.rerun() # Hidden click receiver — JS will set this input's value on row click st.text_input("wl_click_receiver", key="_wl_click", label_visibility="collapsed") # Build rows HTML (string concatenation, NOT f-strings — JS {} would break) rows_html = "" for sym in watchlist: try: price = get_latest_price(sym) info = get_company_info(sym) or {} prev = info.get("regularMarketPreviousClose") or info.get("previousClose") sym_e = escape_html(sym) if price is not None and prev and prev > 0: chg_pct = (price - prev) / prev * 100 sign = "+" if chg_pct >= 0 else "−" chg_cls = "pos" if chg_pct >= 0 else "neg" chg_str = sign + f"{abs(chg_pct):.2f}%" px_str = "$" + f"{price:,.2f}" elif price is not None: chg_cls = "flat" chg_str = "—" px_str = "$" + f"{price:,.2f}" else: chg_cls = "flat" chg_str = "—" px_str = "—" rows_html += ( "