"""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}**")
st.divider()
render_top_movers(compact=True)
# ── Market Bar ────────────────────────────────────────────────────────────────
with st.container():
render_market_bar()
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()
# ── Ticker Header + KPI Strip ─────────────────────────────────────────────────
_hdr_info = get_company_info(ticker) or {}
_hdr_price = get_latest_price(ticker)
_hdr_prev = _hdr_info.get("regularMarketPreviousClose") or _hdr_info.get("previousClose")
_lo52 = _hdr_info.get("fiftyTwoWeekLow")
_hi52 = _hdr_info.get("fiftyTwoWeekHigh")
_range_pct = 50
if _lo52 and _hi52 and _hi52 > _lo52 and _hdr_price:
_range_pct = max(0, min(100, (_hdr_price - _lo52) / (_hi52 - _lo52) * 100))
_chg_abs = (_hdr_price - _hdr_prev) if (_hdr_price and _hdr_prev) else None
_chg_pct = (_chg_abs / _hdr_prev * 100) if (_chg_abs is not None and _hdr_prev) else None
_chg_cls = "pos" if (_chg_pct or 0) >= 0 else "neg"
_chg_arrow = "▲" if (_chg_pct or 0) >= 0 else "▼"
def _fmt_large(v):
try:
n = float(v)
except (TypeError, ValueError):
return "—"
if abs(n) >= 1e12:
return f"${n / 1e12:.2f}T"
if abs(n) >= 1e9:
return f"${n / 1e9:.2f}B"
if abs(n) >= 1e6:
return f"${n / 1e6:.2f}M"
return f"${n:,.0f}"
def _fmt_ratio(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}%"
except (TypeError, ValueError):
return "—"
_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 "—"
_chg_s = (
f"{_chg_arrow} ${abs(_chg_abs):,.2f} ({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"))
_div = _fmt_pct(_hdr_info.get("dividendYield"))
_beta = _fmt_ratio(_hdr_info.get("beta"))
_short = _fmt_pct(_hdr_info.get("shortPercentOfFloat"))
st.markdown(
""
"
"
"
"
+ ("" + _sector_e + "" if _sector_e else "")
+ "" + _sym_e + ""
"" + _name_e + ""
"
"
"
"
"
" + _lo_s + ""
"
"
"
" + _hi_s + ""
"
"
"
"
"" + _price_s + ""
"" + _chg_s + ""
"
"
"
"
"
"
"
Market Cap" + _mktcap + "
"
"
P/E (TTM)" + _pe + "
"
"
EPS (TTM)" + _eps + "
"
"
Div Yield" + _div + "
"
"
Beta" + _beta + "
"
"
Short %" + _short + "
"
"
"
"
",
unsafe_allow_html=True,
)
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}")