"""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 _toggle_mover_tab(state_key: str): st.session_state[state_key] = not st.session_state.get(state_key, False) def _inject_styles(compact: bool = False): row_template = ( "minmax(60px, 0.95fr) minmax(0, 2.0fr) minmax(74px, 1fr) minmax(86px, 1fr)" if compact else "minmax(72px, 0.8fr) minmax(0, 2.6fr) minmax(90px, 1fr) minmax(110px, 1.1fr)" ) row_gap = "0.45rem" if compact else "0.85rem" row_padding = "0.12rem 0" if compact else "0.18rem 0" symbol_size = "0.75rem" if compact else "0.875rem" name_size = "0.75rem" if compact else "0.8125rem" price_size = "0.75rem" if compact else "0.8125rem" change_size = "0.75rem" if compact else "0.8125rem" change_meta_size = "10px" if compact else "11px" st.markdown( """ """.format( row_template=row_template, row_gap=row_gap, row_padding=row_padding, symbol_size=symbol_size, name_size=name_size, price_size=price_size, change_size=change_size, change_meta_size=change_meta_size, ), unsafe_allow_html=True, ) @st.cache_data(ttl=180) def _fetch_movers(screen: str, count: int = MAX_MOVERS) -> list[dict]: try: result = yf.screen(screen, count=count) return result.get("quotes", []) except Exception: return [] def _fmt_price(val) -> str: try: return f"${float(val):,.2f}" except Exception: return "—" 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") try: chg_f = float(chg_pct) color = "#4F8C5E" if chg_f >= 0 else "#B5494B" sign = "+" if chg_f >= 0 else "" pct_str = f"{sign}{chg_f:.2f}%" except Exception: color = "#9aa0b0" pct_str = "—" try: 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) if len(quotes) > DEFAULT_VISIBLE_MOVERS: button_label = "Show Less" if expanded else f"Show More ({len(quotes) - DEFAULT_VISIBLE_MOVERS} more)" st.button( button_label, key=f"{state_key}_button", width="stretch", on_click=_toggle_mover_tab, args=(state_key,), ) @st.fragment def render_top_movers(compact: bool = False): _inject_styles(compact=compact) st.markdown("""
Top Movers
""", unsafe_allow_html=True) tab_gainers, tab_losers, tab_active = st.tabs(["Gainers", "Losers", "Most Active"]) screens = { "gainers": "day_gainers", "losers": "day_losers", "active": "most_actives", } with tab_gainers: if compact: quotes = _fetch_movers(screens["gainers"]) if not quotes: st.caption("No data available.") else: rows_html = "".join(_mover_row_html(q) for q in quotes[:DEFAULT_VISIBLE_MOVERS]) st.markdown(f"
{rows_html}
", unsafe_allow_html=True) else: _render_mover_tab(screens["gainers"], "top_movers_gainers_expanded") with tab_losers: if compact: quotes = _fetch_movers(screens["losers"]) if not quotes: st.caption("No data available.") else: rows_html = "".join(_mover_row_html(q) for q in quotes[:DEFAULT_VISIBLE_MOVERS]) st.markdown(f"
{rows_html}
", unsafe_allow_html=True) else: _render_mover_tab(screens["losers"], "top_movers_losers_expanded") with tab_active: if compact: quotes = _fetch_movers(screens["active"]) if not quotes: st.caption("No data available.") else: rows_html = "".join(_mover_row_html(q) for q in quotes[:DEFAULT_VISIBLE_MOVERS]) st.markdown(f"
{rows_html}
", unsafe_allow_html=True) else: _render_mover_tab(screens["active"], "top_movers_active_expanded")