diff options
Diffstat (limited to 'app.py')
| -rw-r--r-- | app.py | 658 |
1 files changed, 566 insertions, 92 deletions
@@ -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" <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: |
