aboutsummaryrefslogtreecommitdiff
path: root/app.py
diff options
context:
space:
mode:
Diffstat (limited to 'app.py')
-rw-r--r--app.py146
1 files changed, 116 insertions, 30 deletions
diff --git a/app.py b/app.py
index b5f2735..b0a9453 100644
--- a/app.py
+++ b/app.py
@@ -404,6 +404,31 @@ hr {
::-webkit-scrollbar-thumb { background: var(--ink-3); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--ink-4); }
+/* ── Watch toggle button ────────────────────────────────────────────────── */
+[data-testid="stSidebar"] .psm-watch-toggle button {
+ background: var(--ink-2) !important;
+ color: var(--fg-3) !important;
+ border: 1px solid var(--line-2) !important;
+ border-radius: 2px !important;
+ font-family: var(--font-sans) !important;
+ font-size: 0.6875rem !important;
+ font-weight: 500 !important;
+ letter-spacing: 0.06em !important;
+ text-transform: uppercase !important;
+ padding: 3px 8px !important;
+ margin-top: 6px !important;
+}
+
+[data-testid="stSidebar"] .psm-watch-toggle button:hover {
+ background: var(--ink-3) !important;
+ border-color: var(--line-3) !important;
+ color: var(--fg-1) !important;
+}
+
+[data-testid="stSidebar"] .psm-watch-toggle button p {
+ color: inherit !important;
+}
+
/* ── Ticker Header Band ──────────────────────────────────────────────────── */
.psm-header-band {
margin-bottom: 1rem;
@@ -411,27 +436,37 @@ hr {
padding-bottom: 0.75rem;
}
.psm-ticker-head {
- display: flex; align-items: center; gap: 1.5rem;
- flex-wrap: wrap; margin-bottom: 0.5rem;
+ display: flex; align-items: flex-end; gap: 2rem;
+ flex-wrap: nowrap; margin-bottom: 0.5rem;
+}
+.psm-th-left {
+ display: flex; flex-direction: column; gap: 0; flex-shrink: 0;
+}
+.psm-th-meta {
+ display: flex; align-items: baseline; gap: 0.6rem; margin-bottom: 0.15rem;
}
.psm-ticker-head .sector {
- font-family: var(--font-sans); font-size: 11px;
+ font-family: var(--font-sans); font-size: 10px;
color: var(--brass); text-transform: uppercase;
- letter-spacing: 0.1em;
+ letter-spacing: 0.12em;
}
-.psm-ticker-head .sym {
+.psm-ticker-head .name {
font-family: var(--font-display); font-style: italic;
- font-size: 2rem; color: var(--fg-1); line-height: 0.95;
- letter-spacing: -0.025em;
+ font-size: 1rem; color: var(--fg-2); line-height: 1.2;
}
-.psm-ticker-head .name {
- font-family: var(--font-sans); font-size: 13px;
- color: var(--fg-3);
+.psm-ticker-head .sym {
+ font-family: var(--font-display); font-style: normal; font-weight: 400;
+ font-size: 3.75rem; color: var(--fg-1); line-height: 0.88;
+ letter-spacing: -0.02em;
}
.psm-range {
display: flex; align-items: center; gap: 0.5rem;
font-family: var(--font-mono); font-size: 11px; color: var(--fg-3);
- flex: 1; min-width: 180px; max-width: 280px;
+ flex: 1; min-width: 200px; max-width: 320px;
+ align-self: center;
+}
+.psm-range .lbl {
+ color: var(--fg-4); white-space: nowrap;
}
.psm-range .rail {
flex: 1; height: 3px; background: var(--line-2);
@@ -441,10 +476,11 @@ hr {
position: absolute; top: -3px; width: 2px; height: 9px;
background: var(--brass); border-radius: 1px;
}
-.px-block { margin-left: auto; text-align: right; }
+.px-block { margin-left: auto; text-align: right; flex-shrink: 0; align-self: flex-start; }
.px-block .px {
- font-family: var(--font-mono); font-size: 1.5rem;
- color: var(--fg-1); display: block;
+ font-family: var(--font-display); font-size: 2.75rem;
+ color: var(--fg-1); display: block; line-height: 1.0;
+ letter-spacing: -0.02em;
}
.px-block .chg { font-family: var(--font-mono); font-size: 13px; }
.px-block .chg.pos { color: var(--positive); }
@@ -455,9 +491,10 @@ hr {
gap: 0;
}
.psm-kpi {
- padding: 0.35rem 0.75rem 0.35rem 0;
+ padding: 0.4rem 0.75rem;
border-right: 1px solid var(--line-1);
}
+.psm-kpi:first-child { padding-left: 0; }
.psm-kpi:last-child { border-right: none; }
.psm-kpi .k {
font-family: var(--font-sans); font-size: 10px;
@@ -465,8 +502,12 @@ hr {
letter-spacing: 0.08em; display: block;
}
.psm-kpi .v {
- font-family: var(--font-mono); font-size: 13px;
- color: var(--fg-1); display: block; margin-top: 2px;
+ font-family: var(--font-mono); font-size: 1.15rem;
+ color: var(--fg-1); display: block; margin-top: 3px;
+}
+.psm-kpi .sub {
+ font-family: var(--font-sans); font-size: 10px;
+ color: var(--fg-4); display: block; margin-top: 2px;
}
</style>
""", unsafe_allow_html=True)
@@ -520,6 +561,7 @@ pio.templates.default = "prism"
from components.market_bar import render_market_bar
from components.top_movers import render_top_movers
+from components.watchlist import render_watchlist
from components.overview import render_overview
from components.financials import render_financials
from components.valuation import render_valuation
@@ -533,6 +575,9 @@ from services.data_service import get_company_info, search_tickers, get_latest_p
if "ticker" not in st.session_state:
st.session_state["ticker"] = None
+if "watchlist" not in st.session_state:
+ st.session_state["watchlist"] = []
+
# ── Sidebar ───────────────────────────────────────────────────────────────────
@@ -713,6 +758,26 @@ with st.sidebar:
elif ticker:
st.caption(f"Viewing: **{ticker}**")
+ # ── Save / Remove watchlist toggle ────────────────────────────────────
+ if ticker:
+ in_watch = ticker in st.session_state["watchlist"]
+ label = "— Remove from watchlist" if in_watch else "+ Save to watchlist"
+ st.markdown('<div class="psm-watch-toggle">', unsafe_allow_html=True)
+ if st.button(label, key="watch_toggle", use_container_width=True):
+ if in_watch:
+ st.session_state["watchlist"].remove(ticker)
+ else:
+ if (
+ ticker not in st.session_state["watchlist"]
+ and len(st.session_state["watchlist"]) < 10
+ ):
+ st.session_state["watchlist"].append(ticker)
+ st.rerun()
+ st.markdown("</div>", unsafe_allow_html=True)
+
+ # ── Watchlist ──────────────────────────────────────────────────────────
+ render_watchlist()
+
st.divider()
render_top_movers(compact=True)
@@ -781,6 +846,12 @@ def _fmt_ratio(v, d=2):
except (TypeError, ValueError):
return "—"
+def _fmt_plain(v, d=2):
+ try:
+ return f"{float(v):.{d}f}"
+ except (TypeError, ValueError):
+ return "—"
+
def _fmt_pct(v):
try:
return f"{float(v) * 100:.2f}%"
@@ -790,29 +861,40 @@ def _fmt_pct(v):
_sym_e = escape_html(ticker)
_name_e = escape_html(_hdr_info.get("longName") or _hdr_info.get("shortName") or ticker)
_sector_e = escape_html(_hdr_info.get("sector") or "")
-_price_s = f"${_hdr_price:,.2f}" if _hdr_price else "—"
-_lo_s = f"${_lo52:,.2f}" if _lo52 else "—"
-_hi_s = f"${_hi52:,.2f}" if _hi52 else "—"
+_price_s = f"{_hdr_price:,.2f}" if _hdr_price else "—"
+_lo_s = f"{_lo52:,.2f}" if _lo52 else "—"
+_hi_s = f"{_hi52:,.2f}" if _hi52 else "—"
+_sign = "+" if (_chg_pct or 0) >= 0 else "-"
_chg_s = (
- f"{_chg_arrow} ${abs(_chg_abs):,.2f} ({abs(_chg_pct):.2f}%)"
+ f"{_chg_arrow} {abs(_chg_abs):,.2f} ({_sign}{abs(_chg_pct):.2f}%)"
if (_chg_abs is not None and _chg_pct is not None) else "—"
)
+
_mktcap = _fmt_large(_hdr_info.get("marketCap"))
-_pe = _fmt_ratio(_hdr_info.get("trailingPE"))
-_eps = _fmt_ratio(_hdr_info.get("trailingEps"))
+_pe = _fmt_ratio(_hdr_info.get("trailingPE"), 1)
+_raw_eps = _hdr_info.get("trailingEps")
+_eps = ("$" + _fmt_plain(_raw_eps, 2)) if _raw_eps else "—"
_div = _fmt_pct(_hdr_info.get("dividendYield"))
-_beta = _fmt_ratio(_hdr_info.get("beta"))
+_beta = _fmt_plain(_hdr_info.get("beta"), 2)
_short = _fmt_pct(_hdr_info.get("shortPercentOfFloat"))
+_fwd_eps = _hdr_info.get("forwardEps")
+_eps_sub = ("fwd $" + _fmt_plain(_fwd_eps, 2)) if _fwd_eps else ""
+_short_rat = _hdr_info.get("shortRatio")
+_short_sub = (f"{float(_short_rat):.1f} d to cover") if _short_rat else ""
+
st.markdown(
"<div class='psm-header-band'>"
"<div class='psm-ticker-head'>"
- "<div style='display:flex;align-items:baseline;gap:0.75rem;flex-wrap:wrap;'>"
+ "<div class='psm-th-left'>"
+ "<div class='psm-th-meta'>"
+ ("<span class='sector'>" + _sector_e + "</span>" if _sector_e else "")
- + "<span class='sym'>" + _sym_e + "</span>"
- "<span class='name'>" + _name_e + "</span>"
+ + "<span class='name'>" + _name_e + "</span>"
+ "</div>"
+ "<span class='sym'>" + _sym_e + "</span>"
"</div>"
"<div class='psm-range'>"
+ "<span class='lbl'>52-wk</span>"
"<span class='edge'>" + _lo_s + "</span>"
"<div class='rail'><div class='pin' style='left:" + str(round(_range_pct, 1)) + "%'></div></div>"
"<span class='edge'>" + _hi_s + "</span>"
@@ -824,11 +906,15 @@ st.markdown(
"</div>"
"<div class='psm-kpis'>"
"<div class='psm-kpi'><span class='k'>Market Cap</span><span class='v'>" + _mktcap + "</span></div>"
- "<div class='psm-kpi'><span class='k'>P/E (TTM)</span><span class='v'>" + _pe + "</span></div>"
- "<div class='psm-kpi'><span class='k'>EPS (TTM)</span><span class='v'>" + _eps + "</span></div>"
+ "<div class='psm-kpi'><span class='k'>P / E</span><span class='v'>" + _pe + "</span></div>"
+ "<div class='psm-kpi'><span class='k'>EPS · LTM</span><span class='v'>" + _eps + "</span>"
+ + ("<span class='sub'>" + _eps_sub + "</span>" if _eps_sub else "")
+ + "</div>"
"<div class='psm-kpi'><span class='k'>Div Yield</span><span class='v'>" + _div + "</span></div>"
"<div class='psm-kpi'><span class='k'>Beta</span><span class='v'>" + _beta + "</span></div>"
- "<div class='psm-kpi'><span class='k'>Short %</span><span class='v'>" + _short + "</span></div>"
+ "<div class='psm-kpi'><span class='k'>Short Int.</span><span class='v'>" + _short + "</span>"
+ + ("<span class='sub'>" + _short_sub + "</span>" if _short_sub else "")
+ + "</div>"
"</div>"
"</div>",
unsafe_allow_html=True,