aboutsummaryrefslogtreecommitdiff
path: root/components/top_movers.py
diff options
context:
space:
mode:
Diffstat (limited to 'components/top_movers.py')
-rw-r--r--components/top_movers.py140
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")