aboutsummaryrefslogtreecommitdiff
path: root/components/watchlist.py
blob: 0b50d3b9b4be826d21e734c4c34550e184671dde (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
"""Watchlist sidebar component — session-scoped personal watchlist."""
import streamlit as st
from services.data_service import get_latest_price, get_company_info
from utils.security import escape_html

_WATCHLIST_CSS_INJECTED = False


def _inject_css():
    global _WATCHLIST_CSS_INJECTED
    if _WATCHLIST_CSS_INJECTED:
        return
    _WATCHLIST_CSS_INJECTED = True
    st.markdown("""
<style>
/* ── Watchlist section label ───────────────────────────────────────────── */
.psm-watch-label {
  font-family: var(--font-sans);
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--fg-4);
  margin: 0.5rem 0 0.25rem;
}

/* ── Watchlist row button overrides ────────────────────────────────────── */
[data-testid="stSidebar"] .psm-watch-btn button {
  background: transparent !important;
  border: none !important;
  border-bottom: 1px solid var(--line-1) !important;
  border-radius: 0 !important;
  padding: 0.3rem 0 !important;
  width: 100% !important;
  text-align: left !important;
  cursor: pointer !important;
  display: flex !important;
  align-items: center !important;
}

[data-testid="stSidebar"] .psm-watch-btn button:hover {
  background: var(--ink-2) !important;
}

[data-testid="stSidebar"] .psm-watch-btn button p {
  font-family: var(--font-mono) !important;
  font-size: 12px !important;
  color: var(--fg-1) !important;
  margin: 0 !important;
  line-height: 1.3 !important;
}

/* ── Watchlist empty state ──────────────────────────────────────────────── */
.psm-watch-empty {
  font-family: var(--font-sans);
  font-size: 11px;
  color: var(--fg-4);
  padding: 0.25rem 0 0.5rem;
  font-style: italic;
}
</style>
""", unsafe_allow_html=True)


def render_watchlist():
    """Render the personal watchlist section in the sidebar.

    Displays a section label, then one clickable row per saved ticker showing
    symbol · price · Δ%. Clicking a row sets st.session_state["ticker"] and
    triggers a rerun. Shows an empty-state message when the list is empty.

    Must be called from within a `with st.sidebar:` block.
    """
    _inject_css()

    watchlist: list[str] = st.session_state.get("watchlist", [])

    st.markdown('<div class="psm-watch-label">Watchlist</div>', unsafe_allow_html=True)

    if not watchlist:
        st.markdown(
            '<div class="psm-watch-empty">No saved tickers</div>',
            unsafe_allow_html=True,
        )
        return

    for sym in watchlist:
        try:
            price = get_latest_price(sym)
            info = get_company_info(sym) or {}
            prev = info.get("regularMarketPreviousClose") or info.get("previousClose")

            if price is not None and prev and prev > 0:
                chg_pct = (price - prev) / prev * 100
                sign = "+" if chg_pct >= 0 else ""
                chg_color = "#4F8C5E" if chg_pct >= 0 else "#B5494B"
                row_label = (
                    escape_html(sym)
                    + "  $" + f"{price:,.2f}"
                    + "  " + sign + f"{chg_pct:.2f}%"
                )
            elif price is not None:
                chg_color = "#8E8676"
                row_label = escape_html(sym) + "  $" + f"{price:,.2f}"
            else:
                chg_color = "#8E8676"
                row_label = escape_html(sym) + "  —"

            # Render a styled container div then the Streamlit button inside it
            st.markdown('<div class="psm-watch-btn">', unsafe_allow_html=True)
            if st.button(row_label, key="watch_row_" + sym, use_container_width=True):
                st.session_state["ticker"] = sym
                st.rerun()
            st.markdown("</div>", unsafe_allow_html=True)

        except Exception:
            # Never let one broken ticker blow up the whole watchlist
            sym_safe = escape_html(sym)
            st.markdown(
                '<div class="psm-watch-btn">',
                unsafe_allow_html=True,
            )
            if st.button(sym_safe + "  (error)", key="watch_row_" + sym, use_container_width=True):
                st.session_state["ticker"] = sym
                st.rerun()
            st.markdown("</div>", unsafe_allow_html=True)