aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.streamlit/config.toml8
-rw-r--r--app.py658
-rw-r--r--components/filings.py35
-rw-r--r--components/insiders.py10
-rw-r--r--components/market_bar.py54
-rw-r--r--components/options.py4
-rw-r--r--components/overview.py57
-rw-r--r--components/top_movers.py42
-rw-r--r--components/valuation.py14
9 files changed, 691 insertions, 191 deletions
diff --git a/.streamlit/config.toml b/.streamlit/config.toml
index e3fbcf9..16c0c8e 100644
--- a/.streamlit/config.toml
+++ b/.streamlit/config.toml
@@ -1,9 +1,9 @@
[theme]
base = "dark"
-primaryColor = "#4F8EF7"
-backgroundColor = "#0E1117"
-secondaryBackgroundColor = "#161C27"
-textColor = "#FAFAFA"
+primaryColor = "#C2AA7A"
+backgroundColor = "#0B0E13"
+secondaryBackgroundColor = "#11151C"
+textColor = "#C7C0AE"
font = "sans serif"
[server]
diff --git a/app.py b/app.py
index d86475b..d5e082a 100644
--- a/app.py
+++ b/app.py
@@ -11,50 +11,414 @@ st.set_page_config(
initial_sidebar_state="expanded",
)
-# ── Global CSS ────────────────────────────────────────────────────────────────
+# ── Design system CSS ─────────────────────────────────────────────────────────
st.markdown("""
<style>
- /* Tighten metric cards */
- [data-testid="metric-container"] {
- padding: 0.4rem 0.6rem !important;
- }
- [data-testid="stMetricLabel"] {
- font-size: 0.72rem !important;
- font-weight: 500;
- color: #9aa0b0 !important;
- letter-spacing: 0.03em;
- text-transform: uppercase;
- }
- [data-testid="stMetricValue"] {
- font-size: 1.1rem !important;
- font-weight: 600;
- letter-spacing: -0.01em;
- }
- [data-testid="stMetricDelta"] svg { width: 0.75rem; height: 0.75rem; }
- [data-testid="stMetricDelta"] > div { font-size: 0.78rem !important; }
+@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400;1,500;1,600&family=IBM+Plex+Mono:wght@300;400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap');
- /* Tighter headings */
- h1 { font-size: 1.5rem !important; font-weight: 700 !important; }
- h2 { font-size: 1.25rem !important; font-weight: 600 !important; }
- h3 { font-size: 1.05rem !important; font-weight: 600 !important; }
+/* ── Tokens ─────────────────────────────────────────────────────────────── */
+:root {
+ --ink-0: #0B0E13;
+ --ink-1: #11151C;
+ --ink-2: #181D26;
+ --ink-3: #222934;
+ --ink-4: #2C3340;
+ --line-1: #232934;
+ --line-2: #2E3645;
+ --line-3: #3D4658;
+ --fg-1: #F2ECDC;
+ --fg-2: #C7C0AE;
+ --fg-3: #8E8676;
+ --fg-4: #5E5849;
+ --brass: #C2AA7A;
+ --brass-bright: #DCC79E;
+ --brass-deep: #8F7A50;
+ --brass-ink: #17120A;
+ --positive: #4F8C5E;
+ --positive-bg: #15241A;
+ --negative: #B5494B;
+ --negative-bg: #2A1517;
+ --warning: #C49545;
+ --font-display: 'EB Garamond', Georgia, serif;
+ --font-sans: 'IBM Plex Sans', 'Helvetica Neue', system-ui, sans-serif;
+ --font-mono: 'IBM Plex Mono', 'SF Mono', Menlo, monospace;
+}
- /* Tighter sidebar text */
- [data-testid="stSidebar"] .stCaption p {
- font-size: 0.78rem !important;
- line-height: 1.5 !important;
- }
+/* ── Base ───────────────────────────────────────────────────────────────── */
+html, body, [class*="css"] {
+ font-family: var(--font-sans) !important;
+ -webkit-font-smoothing: antialiased;
+}
- /* Slim dividers */
- hr { margin: 0.5rem 0 !important; }
+.stApp { background: var(--ink-0) !important; }
- /* Tab labels */
- [data-testid="stTab"] p { font-size: 0.85rem !important; font-weight: 500; }
+/* ── Sidebar shell ──────────────────────────────────────────────────────── */
+[data-testid="stSidebar"] {
+ background: var(--ink-1) !important;
+ border-right: 1px solid var(--line-1) !important;
+}
- /* Top padding — enough to clear Streamlit's sticky toolbar */
- .block-container { padding-top: 3.5rem !important; }
+[data-testid="stSidebar"] > div:first-child {
+ padding-top: 0 !important;
+}
+
+[data-testid="stSidebarCollapseButton"] { opacity: 0.4; }
+
+/* ── Sidebar text ───────────────────────────────────────────────────────── */
+[data-testid="stSidebar"] p {
+ font-family: var(--font-sans) !important;
+ font-size: 0.8125rem !important;
+ color: var(--fg-3) !important;
+ line-height: 1.45 !important;
+}
+
+[data-testid="stSidebar"] .stCaption p {
+ font-size: 0.75rem !important;
+ color: var(--fg-3) !important;
+}
+
+[data-testid="stSidebar"] strong { color: var(--fg-1) !important; }
+
+[data-testid="stSidebar"] hr {
+ border: none !important;
+ border-top: 1px solid var(--line-1) !important;
+ margin: 0.375rem 0 !important;
+}
+
+[data-testid="stSidebar"] h3 {
+ font-family: var(--font-display) !important;
+ font-size: 1.125rem !important;
+ color: var(--fg-1) !important;
+ font-weight: 500 !important;
+ letter-spacing: -0.01em !important;
+ margin: 0 !important;
+}
+
+/* ── Sidebar inputs ─────────────────────────────────────────────────────── */
+[data-testid="stSidebar"] .stTextInput input {
+ background: var(--ink-2) !important;
+ border: 1px solid var(--line-2) !important;
+ border-radius: 2px !important;
+ font-family: var(--font-mono) !important;
+ font-size: 0.8125rem !important;
+ color: var(--fg-1) !important;
+}
+
+[data-testid="stSidebar"] .stTextInput input::placeholder { color: var(--fg-4) !important; }
+
+[data-testid="stSidebar"] .stTextInput input:focus {
+ border-color: var(--brass-deep) !important;
+ box-shadow: none !important;
+}
+
+[data-testid="stSidebar"] .stTextInput label {
+ font-family: var(--font-sans) !important;
+ font-size: 10px !important;
+ font-weight: 600 !important;
+ text-transform: uppercase !important;
+ letter-spacing: 0.14em !important;
+ color: var(--fg-4) !important;
+}
+
+/* ── Selectbox ──────────────────────────────────────────────────────────── */
+[data-baseweb="select"] > div {
+ background: var(--ink-2) !important;
+ border: 1px solid var(--line-2) !important;
+ border-radius: 2px !important;
+ font-family: var(--font-mono) !important;
+ font-size: 0.8125rem !important;
+ color: var(--fg-1) !important;
+}
+
+[data-baseweb="select"] [data-baseweb="menu"] {
+ background: var(--ink-2) !important;
+ border: 1px solid var(--line-2) !important;
+ border-radius: 2px !important;
+}
+
+[data-baseweb="select"] li {
+ font-family: var(--font-mono) !important;
+ font-size: 0.8125rem !important;
+ color: var(--fg-2) !important;
+}
+
+/* ── Buttons ────────────────────────────────────────────────────────────── */
+button[kind="primary"],
+[data-testid="stFormSubmitButton"] button {
+ background: var(--brass) !important;
+ color: var(--brass-ink) !important;
+ border: none !important;
+ border-radius: 2px !important;
+ font-family: var(--font-sans) !important;
+ font-size: 0.75rem !important;
+ font-weight: 600 !important;
+ letter-spacing: 0.1em !important;
+ text-transform: uppercase !important;
+ transition: background 0.12s ease !important;
+}
+
+button[kind="primary"]:hover,
+[data-testid="stFormSubmitButton"] button:hover {
+ background: var(--brass-bright) !important;
+ border: none !important;
+}
+
+button[kind="secondary"] {
+ background: var(--ink-3) !important;
+ color: var(--fg-2) !important;
+ border: 1px solid var(--line-2) !important;
+ border-radius: 2px !important;
+ font-family: var(--font-sans) !important;
+ font-size: 0.75rem !important;
+ font-weight: 500 !important;
+ letter-spacing: 0.06em !important;
+ text-transform: uppercase !important;
+}
+
+button[kind="secondary"]:hover {
+ background: var(--ink-4) !important;
+ border-color: var(--line-3) !important;
+ color: var(--fg-1) !important;
+}
+
+/* ── Main content area ──────────────────────────────────────────────────── */
+.block-container {
+ padding-top: 1.25rem !important;
+ padding-bottom: 3rem !important;
+}
+
+/* ── Tabs ───────────────────────────────────────────────────────────────── */
+.stTabs [data-baseweb="tab-list"] {
+ background: var(--ink-2) !important;
+ border: 1px solid var(--line-2) !important;
+ border-radius: 2px !important;
+ padding: 2px !important;
+ gap: 2px !important;
+}
+
+.stTabs [data-baseweb="tab"] {
+ background: transparent !important;
+ border-radius: 2px !important;
+ font-family: var(--font-mono) !important;
+ font-size: 11px !important;
+ color: var(--fg-4) !important;
+ padding: 4px 10px !important;
+ border: none !important;
+ transition: color 0.1s, background 0.1s !important;
+}
+
+.stTabs [data-baseweb="tab"]:hover { color: var(--fg-2) !important; }
+
+.stTabs [aria-selected="true"] {
+ background: var(--ink-4) !important;
+ color: var(--fg-1) !important;
+}
+
+.stTabs [data-baseweb="tab-border"],
+.stTabs [data-baseweb="tab-highlight"] { display: none !important; }
+
+/* ── Metric cards ───────────────────────────────────────────────────────── */
+[data-testid="metric-container"] {
+ background: var(--ink-1) !important;
+ border: 1px solid var(--line-1) !important;
+ border-radius: 2px !important;
+ padding: 12px 16px !important;
+}
+
+[data-testid="stMetricLabel"] p {
+ font-family: var(--font-sans) !important;
+ font-size: 10px !important;
+ font-weight: 600 !important;
+ text-transform: uppercase !important;
+ letter-spacing: 0.14em !important;
+ color: var(--fg-4) !important;
+}
+
+[data-testid="stMetricValue"] {
+ font-family: var(--font-mono) !important;
+ font-size: 1.125rem !important;
+ color: var(--fg-1) !important;
+ font-weight: 500 !important;
+ font-variant-numeric: tabular-nums !important;
+ letter-spacing: -0.01em !important;
+}
+
+[data-testid="stMetricDelta"] {
+ font-family: var(--font-mono) !important;
+ font-size: 0.75rem !important;
+ font-variant-numeric: tabular-nums !important;
+}
+
+[data-testid="stMetricDelta"] svg { width: 0.7rem; height: 0.7rem; }
+
+/* ── Expanders ──────────────────────────────────────────────────────────── */
+[data-testid="stExpander"] {
+ background: var(--ink-1) !important;
+ border: 1px solid var(--line-1) !important;
+ border-radius: 2px !important;
+}
+
+[data-testid="stExpander"] summary {
+ font-family: var(--font-display) !important;
+ font-size: 1.0625rem !important;
+ color: var(--fg-1) !important;
+ font-weight: 500 !important;
+ letter-spacing: -0.01em !important;
+}
+
+[data-testid="stExpander"] summary:hover { color: var(--brass-bright) !important; }
+
+/* ── Headings ───────────────────────────────────────────────────────────── */
+h1, h2, h3 {
+ font-family: var(--font-display) !important;
+ color: var(--fg-1) !important;
+ font-weight: 500 !important;
+ letter-spacing: -0.01em !important;
+}
+
+h1 { font-size: 1.875rem !important; line-height: 1.1 !important; }
+h2 { font-size: 1.5rem !important; line-height: 1.2 !important; }
+h3 { font-size: 1.125rem !important; line-height: 1.2 !important; }
+
+h4 {
+ font-family: var(--font-sans) !important;
+ font-size: 0.9375rem !important;
+ font-weight: 600 !important;
+ color: var(--fg-1) !important;
+ letter-spacing: -0.01em !important;
+}
+
+h5, h6 {
+ font-family: var(--font-sans) !important;
+ font-size: 10px !important;
+ font-weight: 600 !important;
+ text-transform: uppercase !important;
+ letter-spacing: 0.14em !important;
+ color: var(--fg-4) !important;
+}
+
+/* ── Body text ──────────────────────────────────────────────────────────── */
+p, .stMarkdown p {
+ font-family: var(--font-sans) !important;
+ font-size: 0.875rem !important;
+ color: var(--fg-2) !important;
+ line-height: 1.45 !important;
+}
+
+.stCaption p {
+ font-family: var(--font-sans) !important;
+ font-size: 0.75rem !important;
+ color: var(--fg-3) !important;
+}
+
+/* ── Code ───────────────────────────────────────────────────────────────── */
+code {
+ font-family: var(--font-mono) !important;
+ background: var(--ink-3) !important;
+ color: var(--fg-1) !important;
+ padding: 2px 6px !important;
+ border-radius: 2px !important;
+ border: 1px solid var(--line-1) !important;
+ font-size: 0.875em !important;
+}
+
+/* ── Dividers ───────────────────────────────────────────────────────────── */
+hr {
+ border: none !important;
+ border-top: 1px solid var(--line-1) !important;
+ margin: 0.625rem 0 !important;
+}
+
+/* ── DataFrames ─────────────────────────────────────────────────────────── */
+[data-testid="stDataFrame"] {
+ border: 1px solid var(--line-1) !important;
+ border-radius: 2px !important;
+}
+
+/* ── Number inputs ──────────────────────────────────────────────────────── */
+.stNumberInput input {
+ background: var(--ink-2) !important;
+ border: 1px solid var(--line-2) !important;
+ border-radius: 2px !important;
+ font-family: var(--font-mono) !important;
+ font-size: 0.8125rem !important;
+ color: var(--fg-1) !important;
+ font-variant-numeric: tabular-nums !important;
+}
+
+.stNumberInput label {
+ font-family: var(--font-sans) !important;
+ font-size: 10px !important;
+ font-weight: 600 !important;
+ text-transform: uppercase !important;
+ letter-spacing: 0.12em !important;
+ color: var(--fg-4) !important;
+}
+
+/* ── Alerts ─────────────────────────────────────────────────────────────── */
+[data-testid="stAlertContainer"] {
+ border-radius: 2px !important;
+ font-family: var(--font-sans) !important;
+ font-size: 0.875rem !important;
+}
+
+/* ── Spinner ────────────────────────────────────────────────────────────── */
+.stSpinner > div { border-top-color: var(--brass) !important; }
+
+/* ── Scrollbars ─────────────────────────────────────────────────────────── */
+::-webkit-scrollbar { width: 5px; height: 5px; }
+::-webkit-scrollbar-track { background: transparent; }
+::-webkit-scrollbar-thumb { background: var(--ink-3); border-radius: 3px; }
+::-webkit-scrollbar-thumb:hover { background: var(--ink-4); }
</style>
""", unsafe_allow_html=True)
+import plotly.graph_objects as go
+import plotly.io as pio
+
+# ── Plotly theme ──────────────────────────────────────────────────────────────
+_prism_layout = go.Layout(
+ paper_bgcolor="#0B0E13",
+ plot_bgcolor="#0B0E13",
+ font=dict(family="IBM Plex Mono, SF Mono, Menlo, monospace", color="#C7C0AE", size=11),
+ title=dict(
+ font=dict(family="EB Garamond, Georgia, serif", color="#F2ECDC", size=18),
+ x=0,
+ ),
+ colorway=["#C2AA7A", "#4F8C5E", "#4A78B5", "#B5494B", "#C49545", "#8B7FBF"],
+ xaxis=dict(
+ gridcolor="#232934",
+ linecolor="#232934",
+ tickfont=dict(family="IBM Plex Mono, monospace", color="#5E5849", size=10),
+ title=dict(font=dict(color="#8E8676", size=11)),
+ showgrid=True,
+ zeroline=False,
+ ),
+ yaxis=dict(
+ gridcolor="#232934",
+ linecolor="#232934",
+ tickfont=dict(family="IBM Plex Mono, monospace", color="#5E5849", size=10),
+ title=dict(font=dict(color="#8E8676", size=11)),
+ showgrid=True,
+ zeroline=False,
+ ),
+ legend=dict(
+ bgcolor="rgba(17,21,28,0.85)",
+ bordercolor="#232934",
+ borderwidth=1,
+ font=dict(family="IBM Plex Sans, sans-serif", color="#C7C0AE", size=11),
+ ),
+ margin=dict(l=48, r=16, t=32, b=40),
+ hoverlabel=dict(
+ bgcolor="#181D26",
+ bordercolor="#2E3645",
+ font=dict(family="IBM Plex Mono, monospace", color="#F2ECDC", size=11),
+ ),
+)
+_prism_template = go.layout.Template(layout=_prism_layout)
+pio.templates["prism"] = _prism_template
+pio.templates.default = "prism"
+
from components.market_bar import render_market_bar
from components.top_movers import render_top_movers
from components.overview import render_overview
@@ -71,21 +435,49 @@ if "ticker" not in st.session_state:
st.session_state["ticker"] = None
-# ── Sidebar ──────────────────────────────────────────────────────────────────
+# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
- col_logo, col_title = st.columns([1, 2])
- with col_logo:
- st.image("assets/logo.png", width=60)
- with col_title:
- st.markdown("### Prism")
- st.caption("Financial Analysis Dashboard")
- st.divider()
+ # Brand mark
+ st.markdown("""
+ <div style="
+ padding: 12px 16px;
+ border-bottom: 1px solid #232934;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 4px;
+ ">
+ <div style="
+ width: 28px; height: 28px;
+ border-radius: 50%;
+ border: 1px solid #C2AA7A;
+ display: flex; align-items: center; justify-content: center;
+ font-family: 'EB Garamond', Georgia, serif;
+ font-style: italic; font-size: 16px;
+ color: #C2AA7A; letter-spacing: -0.05em;
+ flex-shrink: 0;
+ ">P</div>
+ <div style="display: flex; flex-direction: column; gap: 1px; overflow: hidden;">
+ <span style="
+ font-family: 'EB Garamond', Georgia, serif;
+ font-size: 18px; color: #F2ECDC;
+ font-weight: 500; letter-spacing: -0.01em;
+ line-height: 1; white-space: nowrap;
+ ">Prism</span>
+ <span style="
+ font-family: 'IBM Plex Mono', 'SF Mono', monospace;
+ font-size: 10px; color: #5E5849;
+ letter-spacing: 0.12em; text-transform: uppercase;
+ ">Research Terminal</span>
+ </div>
+ </div>
+ """, unsafe_allow_html=True)
with st.form("ticker_search_form", clear_on_submit=False):
query = st.text_input(
- "Search company or ticker",
- placeholder="e.g. Apple, AAPL, MSFT…",
+ "Ticker or company",
+ placeholder="AAPL, Apple, MSFT…",
key="search_query",
).strip()
@@ -108,50 +500,117 @@ with st.sidebar:
if submitted and selected_symbol:
st.session_state["ticker"] = selected_symbol
- if st.session_state["ticker"]:
- st.caption(f"Currently viewing: **{st.session_state['ticker']}**")
-
ticker = st.session_state["ticker"]
- # Quick company info in sidebar
- st.divider()
+ # Company snapshot
if ticker:
+ st.divider()
info = get_company_info(ticker)
- if ticker and info:
- st.caption(info.get("longName", ticker))
- price = get_latest_price(ticker)
- prev_close = info.get("previousClose") or info.get("regularMarketPreviousClose")
- if price is not None:
- if prev_close and prev_close > 0:
- chg = price - prev_close
- chg_pct = chg / prev_close * 100
- sign = "+" if chg >= 0 else ""
- color = "#2ecc71" if chg >= 0 else "#e74c3c"
- st.markdown(
- f"<span style='font-size:1.3rem;font-weight:700'>${price:,.2f}</span>"
- f"&nbsp;<span style='font-size:0.82rem;color:{color}'>{sign}{chg:+.2f} ({sign}{chg_pct:.2f}%)</span>",
- unsafe_allow_html=True,
- )
- else:
- st.markdown(
- f"<span style='font-size:1.3rem;font-weight:700'>${price:,.2f}</span>",
- unsafe_allow_html=True,
- )
- _EXCHANGE_NAMES = {
- "NYQ": "NYSE", "NMS": "NASDAQ", "NGM": "NASDAQ",
- "NCM": "NASDAQ", "ASE": "AMEX", "PCX": "NYSE Arca",
- "BTS": "BATS", "TSX": "TSX", "LSE": "LSE",
- }
- raw_exchange = info.get("exchange", "")
- exchange = _EXCHANGE_NAMES.get(raw_exchange, raw_exchange) or "—"
- st.caption(f"Exchange: {exchange}")
- st.caption(f"Currency: {info.get('currency', 'USD')}")
- st.caption(f"Sector: {info.get('sector', '—')}")
- employees = info.get("fullTimeEmployees")
- st.caption(f"Employees: {employees:,}" if isinstance(employees, int) else "Employees: —")
- website = info.get("website")
- if website:
- st.markdown(f"[🌐 Website]({website})")
+ if info:
+ co_name = info.get("longName", ticker)
+ price = get_latest_price(ticker)
+ prev_close = info.get("previousClose") or info.get("regularMarketPreviousClose")
+
+ # Ticker + name
+ st.markdown(f"""
+ <div style="padding: 6px 0 4px;">
+ <div style="
+ font-family: 'EB Garamond', Georgia, serif;
+ font-style: italic;
+ font-size: 2rem; color: #F2ECDC;
+ line-height: 0.95; letter-spacing: -0.025em;
+ margin-bottom: 4px;
+ ">{ticker}</div>
+ <div style="
+ font-family: 'IBM Plex Sans', sans-serif;
+ font-size: 11px; color: #8E8676;
+ letter-spacing: 0.01em;
+ ">{co_name}</div>
+ </div>
+ """, unsafe_allow_html=True)
+
+ # Price + change
+ if price is not None:
+ if prev_close and prev_close > 0:
+ chg = price - prev_close
+ chg_pct = chg / prev_close * 100
+ sign = "+" if chg >= 0 else ""
+ px_color = "#4F8C5E" if chg >= 0 else "#B5494B"
+ st.markdown(f"""
+ <div style="padding: 4px 0 8px;">
+ <span style="
+ font-family: 'IBM Plex Mono', monospace;
+ font-size: 1.375rem; color: #F2ECDC;
+ font-weight: 500; font-variant-numeric: tabular-nums;
+ ">${price:,.2f}</span>
+ <span style="
+ font-family: 'IBM Plex Mono', monospace;
+ font-size: 0.6875rem; color: {px_color};
+ margin-left: 6px; font-variant-numeric: tabular-nums;
+ ">{sign}{chg_pct:.2f}%</span>
+ </div>
+ """, unsafe_allow_html=True)
+ else:
+ st.markdown(f"""
+ <div style="padding: 4px 0 8px;">
+ <span style="
+ font-family: 'IBM Plex Mono', monospace;
+ font-size: 1.375rem; color: #F2ECDC;
+ font-weight: 500; font-variant-numeric: tabular-nums;
+ ">${price:,.2f}</span>
+ </div>
+ """, unsafe_allow_html=True)
+
+ # Company meta
+ _EXCHANGE_NAMES = {
+ "NYQ": "NYSE", "NMS": "NASDAQ", "NGM": "NASDAQ",
+ "NCM": "NASDAQ", "ASE": "AMEX", "PCX": "NYSE Arca",
+ "BTS": "BATS", "TSX": "TSX", "LSE": "LSE",
+ }
+ raw_exchange = info.get("exchange", "")
+ exchange = _EXCHANGE_NAMES.get(raw_exchange, raw_exchange) or "—"
+ sector = info.get("sector", "—")
+ currency = info.get("currency", "USD")
+ employees = info.get("fullTimeEmployees")
+ emp_str = f"{employees:,}" if isinstance(employees, int) else "—"
+
+ rows = [
+ ("Exchange", exchange),
+ ("Sector", sector),
+ ("Currency", currency),
+ ("Employees", emp_str),
+ ]
+ rows_html = "".join(f"""
+ <div style="display:flex;justify-content:space-between;align-items:baseline;">
+ <span style="font-family:'IBM Plex Sans',sans-serif;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.12em;color:#5E5849;">{k}</span>
+ <span style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#C7C0AE;text-align:right;max-width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{v}</span>
+ </div>
+ """ for k, v in rows)
+
+ st.markdown(f"""
+ <div style="
+ display:flex;flex-direction:column;gap:6px;
+ padding:8px 0 4px;
+ border-top:1px solid #232934;
+ ">{rows_html}</div>
+ """, unsafe_allow_html=True)
+
+ website = info.get("website", "")
+ if website:
+ st.markdown(f"""
+ <div style="padding:6px 0 0;">
+ <a href="{website}" target="_blank" style="
+ font-family:'IBM Plex Sans',sans-serif;
+ font-size:11px;color:#C2AA7A;
+ text-decoration:none;
+ border-bottom:1px solid #8F7A50;
+ padding-bottom:1px;
+ ">Website ↗</a>
+ </div>
+ """, unsafe_allow_html=True)
+
+ elif ticker:
+ st.caption(f"Viewing: **{ticker}**")
# ── Market Bar ────────────────────────────────────────────────────────────────
@@ -171,17 +630,32 @@ st.divider()
# ── Main Content ──────────────────────────────────────────────────────────────
if not ticker:
- st.info("Search for a company or ticker in the sidebar to get started.")
+ st.markdown("""
+ <div style="padding:48px 0 32px;text-align:center;">
+ <div style="
+ font-family:'EB Garamond',Georgia,serif;
+ font-style:italic;font-size:2.375rem;
+ color:#F2ECDC;font-weight:400;
+ letter-spacing:-0.01em;line-height:1.1;
+ margin-bottom:12px;
+ ">Search for a ticker to begin.</div>
+ <div style="
+ font-family:'IBM Plex Sans',sans-serif;
+ font-size:0.875rem;color:#5E5849;
+ letter-spacing:0.01em;
+ ">Enter a company name or symbol in the sidebar.</div>
+ </div>
+ """, unsafe_allow_html=True)
st.stop()
tab_overview, tab_financials, tab_valuation, tab_options, tab_insiders, tab_filings, tab_news = st.tabs([
- "📈 Overview",
- "📊 Financials",
- "💰 Valuation",
- "🎯 Options",
- "👤 Insiders",
- "📁 Filings",
- "📰 News",
+ "Overview",
+ "Financials",
+ "Valuation",
+ "Options",
+ "Insiders",
+ "Filings",
+ "News",
])
with tab_overview:
diff --git a/components/filings.py b/components/filings.py
index a1e4417..9e3b156 100644
--- a/components/filings.py
+++ b/components/filings.py
@@ -17,9 +17,9 @@ _FORM_DESCRIPTIONS = {
}
_FORM_COLORS = {
- "10-K": "rgba(79,142,247,0.15)",
- "10-Q": "rgba(130,224,170,0.15)",
- "8-K": "rgba(247,162,79,0.15)",
+ "10-K": "rgba(74,120,181,0.15)",
+ "10-Q": "rgba(79,140,94,0.15)",
+ "8-K": "rgba(196,149,69,0.15)",
}
@@ -74,18 +74,35 @@ def render_filings(ticker: str):
left, right = st.columns([5, 1])
with left:
st.markdown(
- f"<div style='background:{color};padding:6px 10px;border-radius:4px;margin-bottom:2px'>"
- f"<strong>{form}</strong> &nbsp;·&nbsp; "
- f"<span style='color:#9aa0b0;font-size:0.82rem'>{date}</span><br>"
- f"<span style='font-size:0.85rem'>{title}</span>"
+ f"<div style='"
+ f"background:{color};"
+ f"border:1px solid rgba(194,170,122,0.12);"
+ f"padding:8px 12px;border-radius:2px;margin-bottom:2px;"
+ f"display:flex;align-items:baseline;gap:10px;"
+ f"'>"
+ f"<span style='"
+ f"font-family:IBM Plex Mono,monospace;"
+ f"font-size:11px;color:#C2AA7A;"
+ f"background:rgba(194,170,122,0.07);"
+ f"border:1px solid rgba(194,170,122,0.25);"
+ f"padding:2px 6px;border-radius:2px;"
+ f"white-space:nowrap;"
+ f"'>{form}</span>"
+ f"<span style='font-family:IBM Plex Sans,sans-serif;font-size:0.8125rem;color:#F2ECDC;'>{title}</span>"
+ f"<span style='font-family:IBM Plex Mono,monospace;font-size:11px;color:#5E5849;margin-left:auto;white-space:nowrap;'>{date}</span>"
f"</div>",
unsafe_allow_html=True,
)
with right:
- # Prefer the actual filing doc over the Yahoo index page
doc_url = exhibits.get(form) or edgar_url
if doc_url:
st.markdown(
- f"<div style='padding-top:8px'><a href='{doc_url}' target='_blank'>🔗 View</a></div>",
+ f"<div style='padding-top:8px;text-align:right;'>"
+ f"<a href='{doc_url}' target='_blank' style='"
+ f"font-family:IBM Plex Sans,sans-serif;"
+ f"font-size:11px;color:#C2AA7A;"
+ f"text-decoration:none;"
+ f"border-bottom:1px solid #8F7A50;"
+ f"'>View ↗</a></div>",
unsafe_allow_html=True,
)
diff --git a/components/insiders.py b/components/insiders.py
index 07bc3e3..bdb1818 100644
--- a/components/insiders.py
+++ b/components/insiders.py
@@ -83,18 +83,16 @@ def render_insiders(ticker: str):
fig = go.Figure()
fig.add_trace(go.Bar(
x=months, y=[monthly[m]["Buy"] / 1e6 for m in months],
- name="Buys", marker_color="#2ecc71",
+ name="Buys", marker_color="#4F8C5E",
))
fig.add_trace(go.Bar(
x=months, y=[-monthly[m]["Sell"] / 1e6 for m in months],
- name="Sells", marker_color="#e74c3c",
+ name="Sells", marker_color="#B5494B",
))
fig.update_layout(
title="Monthly Insider Net Activity ($M)",
barmode="relative",
yaxis_title="Value ($M)",
- plot_bgcolor="rgba(0,0,0,0)",
- paper_bgcolor="rgba(0,0,0,0)",
margin=dict(l=0, r=0, t=40, b=0),
height=280,
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
@@ -133,9 +131,9 @@ def render_insiders(ticker: str):
def _color_type(row):
if row["Type"] == "Buy":
- return [""] * 3 + ["background-color: rgba(46,204,113,0.15)"] + [""] * 2
+ return [""] * 3 + ["background-color: #15241A; color: #4F8C5E"] + [""] * 2
if row["Type"] == "Sell":
- return [""] * 3 + ["background-color: rgba(231,76,60,0.15)"] + [""] * 2
+ return [""] * 3 + ["background-color: #2A1517; color: #B5494B"] + [""] * 2
return [""] * len(row)
st.dataframe(
diff --git a/components/market_bar.py b/components/market_bar.py
index 411b232..e3accc5 100644
--- a/components/market_bar.py
+++ b/components/market_bar.py
@@ -23,46 +23,38 @@ def render_market_bar():
"""
<style>
.prism-market-card {
- background: rgba(22, 28, 39, 0.92);
- border: 1px solid rgba(255,255,255,0.06);
- border-radius: 14px;
- padding: 0.8rem 0.95rem;
- min-height: 96px;
+ background: #11151C;
+ border: 1px solid #232934;
+ border-radius: 2px;
+ padding: 12px 16px;
}
.prism-market-label {
- color: #9aa0b0;
- font-size: 0.78rem;
+ font-family: 'IBM Plex Sans', sans-serif;
+ font-size: 10px;
font-weight: 600;
- letter-spacing: 0.06em;
+ letter-spacing: 0.14em;
text-transform: uppercase;
- margin-bottom: 0.45rem;
+ color: #5E5849;
+ margin-bottom: 6px;
}
.prism-market-value {
- color: #f3f6fb;
- font-size: 1.5rem;
- font-weight: 700;
- line-height: 1.15;
- margin-bottom: 0.45rem;
+ font-family: 'IBM Plex Mono', monospace;
+ font-variant-numeric: tabular-nums;
+ font-size: 1.25rem;
+ font-weight: 500;
+ color: #F2ECDC;
+ line-height: 1.1;
+ margin-bottom: 4px;
}
.prism-market-delta {
- display: inline-block;
- border-radius: 999px;
- font-size: 0.78rem;
- font-weight: 600;
- padding: 0.18rem 0.5rem;
- }
- .prism-market-delta.positive {
- color: #7ce3a1;
- background: rgba(28, 131, 72, 0.18);
- }
- .prism-market-delta.negative {
- color: #ff8a8a;
- background: rgba(180, 47, 47, 0.18);
- }
- .prism-market-delta.neutral {
- color: #c6cfdd;
- background: rgba(198, 207, 221, 0.12);
+ font-family: 'IBM Plex Mono', monospace;
+ font-variant-numeric: tabular-nums;
+ font-size: 11px;
+ font-weight: 500;
}
+ .prism-market-delta.positive { color: #4F8C5E; }
+ .prism-market-delta.negative { color: #B5494B; }
+ .prism-market-delta.neutral { color: #5E5849; }
</style>
""",
unsafe_allow_html=True,
diff --git a/components/options.py b/components/options.py
index e9bf016..0acce31 100644
--- a/components/options.py
+++ b/components/options.py
@@ -104,7 +104,7 @@ def render_options(ticker: str):
y=calls_atm["impliedVolatility"] * 100,
name="Calls IV",
mode="lines+markers",
- line=dict(color="#4F8EF7", width=2),
+ line=dict(color="#C2AA7A", width=2),
marker=dict(size=4),
))
if not puts_atm.empty and "impliedVolatility" in puts_atm.columns:
@@ -113,7 +113,7 @@ def render_options(ticker: str):
y=puts_atm["impliedVolatility"] * 100,
name="Puts IV",
mode="lines+markers",
- line=dict(color="#F7A24F", width=2),
+ line=dict(color="#C49545", width=2),
marker=dict(size=4),
))
if current_price:
diff --git a/components/overview.py b/components/overview.py
index 1bb65c2..9a0d162 100644
--- a/components/overview.py
+++ b/components/overview.py
@@ -118,22 +118,22 @@ def _score_card(info: dict) -> None:
return
color_map = {
- "green": ("rgba(46,204,113,0.15)", "#7ce3a1"),
- "yellow": ("rgba(243,156,18,0.15)", "#f0c040"),
- "red": ("rgba(231,76,60,0.15)", "#ff8a8a"),
- "neutral": ("rgba(255,255,255,0.05)", "#9aa0b0"),
+ "green": ("#15241A", "#4F8C5E"),
+ "yellow": ("#2A1F0F", "#C49545"),
+ "red": ("#2A1517", "#B5494B"),
+ "neutral": ("#181D26", "#5E5849"),
}
cards_html = ""
for label, color, value, desc in signals:
bg, fg = color_map[color]
cards_html += (
- f'<div style="background:{bg};border:1px solid {fg}44;border-radius:10px;'
- f'padding:0.5rem 0.75rem;flex:1;min-width:110px;">'
- f'<div style="font-size:0.68rem;font-weight:600;color:#9aa0b0;text-transform:uppercase;'
- f'letter-spacing:0.05em;margin-bottom:0.15rem;">{label}</div>'
- f'<div style="font-size:0.85rem;font-weight:700;color:{fg};">{value}</div>'
- f'<div style="font-size:0.68rem;color:#9aa0b0;margin-top:0.1rem;">{desc}</div>'
+ f'<div style="background:{bg};border:1px solid {fg}55;border-radius:2px;'
+ f'padding:8px 12px;flex:1;min-width:110px;">'
+ f'<div style="font-family:IBM Plex Sans,sans-serif;font-size:10px;font-weight:600;color:#5E5849;text-transform:uppercase;'
+ f'letter-spacing:0.12em;margin-bottom:3px;">{label}</div>'
+ f'<div style="font-family:IBM Plex Mono,monospace;font-size:0.875rem;font-weight:500;color:{fg};font-variant-numeric:tabular-nums;">{value}</div>'
+ f'<div style="font-family:IBM Plex Sans,sans-serif;font-size:0.75rem;color:#8E8676;margin-top:2px;">{desc}</div>'
f'</div>'
)
@@ -159,23 +159,28 @@ def _render_52w_bar(info: dict) -> None:
st.markdown(
f"""
- <div style="margin:0.4rem 0 0.9rem 0;">
- <div style="display:flex;justify-content:space-between;font-size:0.72rem;color:#9aa0b0;margin-bottom:0.35rem;">
- <span>52W Low: {fmt_currency(low)}</span>
- <span style="color:#c6cfdd;font-weight:600;">
- {fmt_currency(price)} &nbsp;·&nbsp; {pct:.0f}% of range
+ <div style="margin:8px 0 16px 0;">
+ <div style="display:flex;justify-content:space-between;
+ font-family:'IBM Plex Mono',monospace;font-size:11px;
+ font-variant-numeric:tabular-nums;
+ color:#8E8676;margin-bottom:6px;">
+ <span>{fmt_currency(low)}</span>
+ <span style="color:#C2AA7A;font-weight:500;">
+ {fmt_currency(price)} &nbsp;·&nbsp; {pct:.0f}%
</span>
- <span>52W High: {fmt_currency(high)}</span>
+ <span>{fmt_currency(high)}</span>
</div>
- <div style="position:relative;height:7px;background:rgba(255,255,255,0.08);border-radius:4px;overflow:visible;">
+ <div style="position:relative;height:3px;background:#222934;border-radius:999px;overflow:visible;">
<div style="position:absolute;left:0;width:{pct}%;height:100%;
- background:linear-gradient(to right,#e74c3c,#f0b27a,#2ecc71);border-radius:4px;"></div>
- <div style="position:absolute;left:calc({pct}% - 2px);top:-4px;width:4px;height:15px;
- background:#fff;border-radius:2px;box-shadow:0 0 5px rgba(0,0,0,0.5);"></div>
+ background:#C2AA7A;border-radius:999px;"></div>
+ <div style="position:absolute;left:calc({pct}% - 1px);top:-4px;width:2px;height:11px;
+ background:#DCC79E;border-radius:1px;"></div>
</div>
- <div style="display:flex;justify-content:space-between;font-size:0.68rem;color:#9aa0b0;margin-top:0.3rem;">
- <span>+{from_low_pct:.1f}% above low</span>
- <span>{to_high_pct:.1f}% below high</span>
+ <div style="display:flex;justify-content:space-between;
+ font-family:'IBM Plex Mono',monospace;font-size:10px;
+ color:#5E5849;margin-top:5px;">
+ <span>+{from_low_pct:.1f}% from low</span>
+ <span>{to_high_pct:.1f}% to high</span>
</div>
</div>
""",
@@ -286,10 +291,10 @@ def _render_relative_chart(ticker: str, info: dict, period: str):
y=subject_series.values,
mode="lines",
name=ticker.upper(),
- line=dict(color="#4F8EF7", width=2.5),
+ line=dict(color="#C2AA7A", width=2.5),
))
- palette = ["#7ce3a1", "#F7A24F", "#c084fc", "#ff8a8a", "#9ad1ff"]
+ palette = ["#7ce3a1", "#C49545", "#c084fc", "#ff8a8a", "#9ad1ff"]
plotted = 1
for idx, label in enumerate(selected_labels):
symbol = option_map[label]
@@ -424,7 +429,7 @@ def render_overview(ticker: str):
y=hist["Close"],
mode="lines",
name="Close",
- line=dict(color="#4F8EF7", width=2),
+ line=dict(color="#C2AA7A", width=2),
fill="tozeroy",
fillcolor="rgba(79,142,247,0.08)",
))
diff --git a/components/top_movers.py b/components/top_movers.py
index 5589df6..db95592 100644
--- a/components/top_movers.py
+++ b/components/top_movers.py
@@ -28,30 +28,39 @@ def _inject_styles():
padding: 0.18rem 0;
}
.prism-mover-symbol {
- font-size: 1rem;
- font-weight: 700;
+ font-family: 'IBM Plex Sans', sans-serif;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: #F2ECDC;
line-height: 1.1;
}
.prism-mover-name {
- color: #9aa0b0;
- font-size: 0.84rem;
+ font-family: 'IBM Plex Sans', sans-serif;
+ color: #8E8676;
+ font-size: 0.8125rem;
line-height: 1.15;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.prism-mover-price {
- font-size: 0.98rem;
+ font-family: 'IBM Plex Mono', monospace;
+ font-variant-numeric: tabular-nums;
+ font-size: 0.8125rem;
+ color: #C7C0AE;
line-height: 1.1;
}
.prism-mover-change {
- font-size: 0.98rem;
- font-weight: 600;
+ font-family: 'IBM Plex Mono', monospace;
+ font-variant-numeric: tabular-nums;
+ font-size: 0.8125rem;
+ font-weight: 500;
line-height: 1.1;
}
.prism-mover-change-meta {
- font-size: 0.74rem;
- color: #9aa0b0;
+ font-family: 'IBM Plex Mono', monospace;
+ font-size: 11px;
+ color: #5E5849;
margin-left: 0.2rem;
}
@media (max-width: 900px) {
@@ -91,7 +100,7 @@ def _mover_row_html(q: dict) -> str:
try:
chg_f = float(chg_pct)
- color = "#2ecc71" if chg_f >= 0 else "#e74c3c"
+ color = "#4F8C5E" if chg_f >= 0 else "#B5494B"
sign = "+" if chg_f >= 0 else ""
pct_str = f"{sign}{chg_f:.2f}%"
except Exception:
@@ -143,11 +152,16 @@ def _render_mover_tab(screen: str, state_key: str):
@st.fragment
def render_top_movers():
_inject_styles()
- st.markdown("#### 🔥 Top Movers")
+ st.markdown("""
+ <div style="
+ font-family:'IBM Plex Sans',sans-serif;
+ font-size:10px;font-weight:600;
+ text-transform:uppercase;letter-spacing:0.14em;
+ color:#5E5849;margin-bottom:8px;
+ ">Top Movers</div>
+ """, unsafe_allow_html=True)
- tab_gainers, tab_losers, tab_active = st.tabs([
- "📈 Gainers", "📉 Losers", "⚡ Most Active"
- ])
+ tab_gainers, tab_losers, tab_active = st.tabs(["Gainers", "Losers", "Most Active"])
screens = {
"gainers": "day_gainers",
diff --git a/components/valuation.py b/components/valuation.py
index 0095f41..a141846 100644
--- a/components/valuation.py
+++ b/components/valuation.py
@@ -586,7 +586,7 @@ def _render_dcf_model(ctx: dict):
fig = go.Figure(go.Bar(
x=years + ["Terminal Value"],
y=[(v / 1e9) for v in discounted] + [terminal_pv / 1e9],
- marker_color=["#4F8EF7"] * len(years) + ["#F7A24F"],
+ marker_color=["#C2AA7A"] * len(years) + ["#C49545"],
text=[f"${v / 1e9:.2f}B" for v in discounted] + [f"${terminal_pv / 1e9:.2f}B"],
textposition="outside",
))
@@ -1235,7 +1235,7 @@ def _render_analyst_targets(ticker: str):
st.write("")
- colors = ["#2ecc71", "#82e0aa", "#f0b27a", "#e59866", "#e74c3c"]
+ colors = ["#4F8C5E", "#4F8C5E", "#C49545", "#8F7A50", "#B5494B"]
fig = go.Figure(go.Bar(
x=list(counts.keys()),
y=list(counts.values()),
@@ -1314,14 +1314,14 @@ def _render_earnings_history(ticker: str):
y=df_chart["epsActual"],
name="Actual EPS",
mode="lines+markers",
- line=dict(color="#4F8EF7", width=2),
+ line=dict(color="#C2AA7A", width=2),
))
fig.add_trace(go.Scatter(
x=df_chart.index.astype(str),
y=df_chart["epsEstimate"],
name="Estimated EPS",
mode="lines+markers",
- line=dict(color="#F7A24F", width=2, dash="dash"),
+ line=dict(color="#C49545", width=2, dash="dash"),
))
fig.update_layout(
title="EPS: Actual vs. Estimate",
@@ -1351,7 +1351,7 @@ _HIST_RATIO_OPTIONS = {
}
_CHART_COLORS = [
- "#4F8EF7", "#F7A24F", "#2ecc71", "#e74c3c",
+ "#C2AA7A", "#C49545", "#4F8C5E", "#B5494B",
"#9b59b6", "#1abc9c", "#f39c12", "#e67e22",
]
@@ -1534,7 +1534,7 @@ def _render_forward_estimates(ticker: str):
y=hist["epsActual"],
name="EPS Actual",
mode="lines+markers",
- line=dict(color="#4F8EF7", width=2),
+ line=dict(color="#C2AA7A", width=2),
))
if fwd_dates:
@@ -1560,7 +1560,7 @@ def _render_forward_estimates(ticker: str):
y=fwd_eps,
name="EPS Est. (Avg)",
mode="lines+markers",
- line=dict(color="#F7A24F", width=2, dash="dash"),
+ line=dict(color="#C49545", width=2, dash="dash"),
))
fig.update_layout(