diff options
Diffstat (limited to 'components/top_movers.py')
| -rw-r--r-- | components/top_movers.py | 140 |
1 files changed, 98 insertions, 42 deletions
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( + """ + <style> + .prism-mover-list { + display: grid; + gap: 0.12rem; + } + .prism-mover-row { + display: grid; + grid-template-columns: minmax(72px, 0.8fr) minmax(0, 2.6fr) minmax(90px, 1fr) minmax(110px, 1.1fr); + gap: 0.85rem; + align-items: center; + padding: 0.18rem 0; + } + .prism-mover-symbol { + font-size: 1rem; + font-weight: 700; + line-height: 1.1; + } + .prism-mover-name { + color: #9aa0b0; + font-size: 0.84rem; + line-height: 1.15; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .prism-mover-price { + font-size: 0.98rem; + line-height: 1.1; + } + .prism-mover-change { + font-size: 0.98rem; + font-weight: 600; + line-height: 1.1; + } + .prism-mover-change-meta { + font-size: 0.74rem; + color: #9aa0b0; + margin-left: 0.2rem; + } + @media (max-width: 900px) { + .prism-mover-row { + grid-template-columns: minmax(68px, 0.9fr) minmax(0, 2.2fr) minmax(82px, 1fr) minmax(96px, 1fr); + gap: 0.55rem; + } + } + </style> + """, + 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 ( + "<div class='prism-mover-row'>" + f"<div class='prism-mover-symbol'>{symbol}</div>" + f"<div class='prism-mover-name'>{name}</div>" + f"<div class='prism-mover-price'>{_fmt_price(price)}</div>" + "<div>" + f"<span class='prism-mover-change' style='color:{color}'>{pct_str}</span>" + f"<span class='prism-mover-change-meta'>{abs_str}</span>" + "</div>" + "</div>" + ) + + +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"<div class='prism-mover-list'>{rows_html}</div>", 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"<span style='color:{color};font-weight:600'>{pct_str}</span>" - f"<span style='font-size:0.75rem;color:#9aa0b0'> {abs_str}</span>", - 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") |
