"""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
from utils.security import escape_html, validate_outbound_url
# ── 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", width="stretch", 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_html = escape_html(ticker)
co_name_html = escape_html(co_name)
# Ticker + name
st.markdown(f"""
{ticker_html}
{co_name_html}
""", 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", escape_html(exchange)),
("Sector", escape_html(sector)),
("Currency", escape_html(currency)),
("Employees", escape_html(emp_str)),
]
rows_html = "".join(f"""
{k}
{v}
""" for k, v in rows)
st.markdown(f"""
{rows_html}
""", unsafe_allow_html=True)
website = validate_outbound_url(info.get("website", ""))
if website:
st.markdown(f"""
""", 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}")