"""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")