From 96b27f1d00ae8110273de973053c3d6bfc4f3662 Mon Sep 17 00:00:00 2001 From: Tyler Date: Tue, 31 Mar 2026 23:01:05 -0700 Subject: Add relative performance chart and refine top movers --- components/top_movers.py | 140 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 98 insertions(+), 42 deletions(-) (limited to 'components/top_movers.py') diff --git a/components/top_movers.py b/components/top_movers.py index ac76504..ea72fc6 100644 --- a/components/top_movers.py +++ b/components/top_movers.py @@ -1,10 +1,69 @@ """Top Movers component — day gainers, losers, most active.""" +from html import escape + import streamlit as st import yfinance as yf +DEFAULT_VISIBLE_MOVERS = 3 +MAX_MOVERS = 8 + + +def _inject_styles(): + st.markdown( + """ + + """, + unsafe_allow_html=True, + ) + @st.cache_data(ttl=180) -def _fetch_movers(screen: str, count: int = 8) -> list[dict]: +def _fetch_movers(screen: str, count: int = MAX_MOVERS) -> list[dict]: try: result = yf.screen(screen, count=count) return result.get("quotes", []) @@ -12,13 +71,6 @@ def _fetch_movers(screen: str, count: int = 8) -> list[dict]: return [] -def _fmt_pct(val) -> str: - try: - return f"{float(val):+.2f}%" - except Exception: - return "—" - - def _fmt_price(val) -> str: try: return f"${float(val):,.2f}" @@ -26,9 +78,9 @@ def _fmt_price(val) -> str: return "—" -def _mover_row(q: dict): - symbol = q.get("symbol", "") - name = q.get("shortName") or q.get("longName") or symbol +def _mover_row_html(q: dict) -> str: + symbol = escape(str(q.get("symbol", ""))) + name = escape(str(q.get("shortName") or q.get("longName") or symbol)) price = q.get("regularMarketPrice") chg_pct = q.get("regularMarketChangePercent") chg_abs = q.get("regularMarketChange") @@ -46,23 +98,42 @@ def _mover_row(q: dict): abs_str = f"({'+' if float(chg_abs) >= 0 else ''}{float(chg_abs):.2f})" except Exception: abs_str = "" + abs_str = escape(abs_str) + + return ( + "
" + f"
{symbol}
" + f"
{name}
" + f"
{_fmt_price(price)}
" + "
" + f"{pct_str}" + f"{abs_str}" + "
" + "
" + ) + + +def _render_mover_tab(screen: str, state_key: str): + quotes = _fetch_movers(screen) + if not quotes: + st.caption("No data available.") + return + + expanded = st.session_state.get(state_key, False) + visible_count = len(quotes) if expanded else min(DEFAULT_VISIBLE_MOVERS, len(quotes)) + + rows_html = "".join(_mover_row_html(q) for q in quotes[:visible_count]) + st.markdown(f"
{rows_html}
", unsafe_allow_html=True) - col_sym, col_name, col_price, col_chg = st.columns([1, 3, 1.5, 1.5]) - with col_sym: - st.markdown(f"**{symbol}**") - with col_name: - st.caption(name[:40]) - with col_price: - st.markdown(_fmt_price(price)) - with col_chg: - st.markdown( - f"{pct_str}" - f" {abs_str}", - unsafe_allow_html=True, - ) + if len(quotes) > DEFAULT_VISIBLE_MOVERS: + button_label = "Show Less" if expanded else f"Show More ({len(quotes) - DEFAULT_VISIBLE_MOVERS} more)" + if st.button(button_label, key=f"{state_key}_button", use_container_width=True): + st.session_state[state_key] = not expanded +@st.fragment def render_top_movers(): + _inject_styles() st.markdown("#### 🔥 Top Movers") tab_gainers, tab_losers, tab_active = st.tabs([ @@ -76,25 +147,10 @@ def render_top_movers(): } with tab_gainers: - quotes = _fetch_movers(screens["gainers"]) - if quotes: - for q in quotes: - _mover_row(q) - else: - st.caption("No data available.") + _render_mover_tab(screens["gainers"], "top_movers_gainers_expanded") with tab_losers: - quotes = _fetch_movers(screens["losers"]) - if quotes: - for q in quotes: - _mover_row(q) - else: - st.caption("No data available.") + _render_mover_tab(screens["losers"], "top_movers_losers_expanded") with tab_active: - quotes = _fetch_movers(screens["active"]) - if quotes: - for q in quotes: - _mover_row(q) - else: - st.caption("No data available.") + _render_mover_tab(screens["active"], "top_movers_active_expanded") -- cgit v1.3-2-g0d8e