From b321614ef1aabf3ce001fea471e45bebc35ccb86 Mon Sep 17 00:00:00 2001 From: Tyler Date: Sat, 16 May 2026 01:30:34 -0700 Subject: Add session-scoped personal watchlist to sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a save/remove toggle below the company snapshot, a watchlist section (capped at 10 tickers) that renders sym · price · Δ% rows above Top Movers, and an empty-state placeholder. Clicking a watchlist row loads that ticker. Co-Authored-By: Claude Sonnet 4.6 --- components/watchlist.py | 126 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 components/watchlist.py (limited to 'components/watchlist.py') diff --git a/components/watchlist.py b/components/watchlist.py new file mode 100644 index 0000000..0b50d3b --- /dev/null +++ b/components/watchlist.py @@ -0,0 +1,126 @@ +"""Watchlist sidebar component — session-scoped personal watchlist.""" +import streamlit as st +from services.data_service import get_latest_price, get_company_info +from utils.security import escape_html + +_WATCHLIST_CSS_INJECTED = False + + +def _inject_css(): + global _WATCHLIST_CSS_INJECTED + if _WATCHLIST_CSS_INJECTED: + return + _WATCHLIST_CSS_INJECTED = True + st.markdown(""" + +""", unsafe_allow_html=True) + + +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. + """ + _inject_css() + + watchlist: list[str] = st.session_state.get("watchlist", []) + + st.markdown('
Watchlist
', unsafe_allow_html=True) + + if not watchlist: + st.markdown( + '
No saved tickers
', + unsafe_allow_html=True, + ) + return + + for sym in watchlist: + try: + price = get_latest_price(sym) + info = get_company_info(sym) or {} + prev = info.get("regularMarketPreviousClose") or info.get("previousClose") + + if price is not None and prev and prev > 0: + chg_pct = (price - prev) / prev * 100 + sign = "+" if chg_pct >= 0 else "" + chg_color = "#4F8C5E" if chg_pct >= 0 else "#B5494B" + row_label = ( + escape_html(sym) + + " $" + f"{price:,.2f}" + + " " + sign + f"{chg_pct:.2f}%" + ) + elif price is not None: + chg_color = "#8E8676" + row_label = escape_html(sym) + " $" + f"{price:,.2f}" + else: + chg_color = "#8E8676" + row_label = escape_html(sym) + " —" + + # Render a styled container div then the Streamlit button inside it + st.markdown('
', unsafe_allow_html=True) + if st.button(row_label, key="watch_row_" + sym, use_container_width=True): + st.session_state["ticker"] = sym + st.rerun() + st.markdown("
", unsafe_allow_html=True) + + except Exception: + # Never let one broken ticker blow up the whole watchlist + sym_safe = escape_html(sym) + st.markdown( + '
', + unsafe_allow_html=True, + ) + if st.button(sym_safe + " (error)", key="watch_row_" + sym, use_container_width=True): + st.session_state["ticker"] = sym + st.rerun() + st.markdown("
", unsafe_allow_html=True) -- cgit v1.3-2-g0d8e