"""Prism — Financial Analysis Dashboard""" import streamlit as st from dotenv import load_dotenv load_dotenv() st.set_page_config( page_title="Prism", page_icon="assets/logo.png", layout="wide", initial_sidebar_state="expanded", ) # ── Design system CSS ───────────────────────────────────────────────────────── st.markdown(""" """, 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 from components.financials import render_financials from components.valuation import render_valuation from components.insiders import render_insiders from components.filings import render_filings from components.news import render_news from components.options import render_options from services.data_service import get_company_info, search_tickers, get_latest_price if "ticker" not in st.session_state: st.session_state["ticker"] = None # ── Sidebar ─────────────────────────────────────────────────────────────────── with st.sidebar: # Brand mark st.markdown("""
P
Prism Research Terminal
""", unsafe_allow_html=True) with st.form("ticker_search_form", clear_on_submit=False): query = st.text_input( "Ticker or company", placeholder="AAPL, Apple, MSFT…", key="search_query", ).strip() results = search_tickers(query) if query else [] selected_symbol = None if results: options = {f"{r['symbol']} — {r['name']} ({r['exchange']})": r["symbol"] for r in results} choice = st.selectbox( "Matches", options=list(options.keys()), label_visibility="collapsed", ) selected_symbol = options[choice] elif query: selected_symbol = query.upper() submitted = st.form_submit_button("Open", use_container_width=True, type="primary") if submitted and selected_symbol: st.session_state["ticker"] = selected_symbol ticker = st.session_state["ticker"] # Company snapshot if ticker: st.divider() info = get_company_info(ticker) 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"""
{ticker}
{co_name}
""", 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"""
${price:,.2f} {sign}{chg_pct:.2f}%
""", unsafe_allow_html=True) else: st.markdown(f"""
${price:,.2f}
""", 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"""
{k} {v}
""" for k, v in rows) st.markdown(f"""
{rows_html}
""", unsafe_allow_html=True) website = info.get("website", "") if website: st.markdown(f"""
Website ↗
""", unsafe_allow_html=True) elif ticker: st.caption(f"Viewing: **{ticker}**") # ── Market Bar ──────────────────────────────────────────────────────────────── with st.container(): render_market_bar() st.divider() # ── Top Movers ──────────────────────────────────────────────────────────────── with st.container(): render_top_movers() st.divider() # ── Main Content ────────────────────────────────────────────────────────────── if not ticker: st.markdown("""
Search for a ticker to begin.
Enter a company name or symbol in the sidebar.
""", 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", ]) with tab_overview: try: render_overview(ticker) except Exception as e: st.error(f"Overview failed to load: {e}") with tab_financials: try: render_financials(ticker) except Exception as e: st.error(f"Financials failed to load: {e}") with tab_valuation: try: render_valuation(ticker) except Exception as e: st.error(f"Valuation failed to load: {e}") with tab_options: try: render_options(ticker) except Exception as e: st.error(f"Options data failed to load: {e}") with tab_insiders: try: render_insiders(ticker) except Exception as e: st.error(f"Insider data failed to load: {e}") with tab_filings: try: render_filings(ticker) except Exception as e: st.error(f"Filings failed to load: {e}") with tab_news: try: render_news(ticker) except Exception as e: st.error(f"News failed to load: {e}")