"""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.watchlist import render_watchlist
from components.quotetable import render_quotetable
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
import streamlit.components.v1 as components
if "ticker" not in st.session_state:
st.session_state["ticker"] = None
if "watchlist" not in st.session_state:
st.session_state["watchlist"] = []
if "active_tab" not in st.session_state:
st.session_state["active_tab"] = "overview"
# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
# Brand mark
st.markdown("""
""", unsafe_allow_html=True)
_clock_html = (
""
""
" "
" NYSE · Closed"
" --:--:-- ET"
"
"
""
)
components.html(_clock_html, height=32, scrolling=False)
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"]
# ── Workspace nav ─────────────────────────────────────────────────────
st.markdown("Workspace
", unsafe_allow_html=True)
_nav = [
("overview", "◎ Overview"),
("financials", "▦ Financials"),
("valuation", "◈ Valuation"),
("options", "◇ Options"),
("insiders", "○ Insiders"),
("filings", "▤ Filings"),
("news", "◉ News"),
]
for _tab_id, _tab_label in _nav:
_is_active = st.session_state["active_tab"] == _tab_id
if st.button(
_tab_label,
key=f"nav_{_tab_id}",
use_container_width=True,
type="primary" if _is_active else "secondary",
):
st.session_state["active_tab"] = _tab_id
st.rerun()
# ── Save / Remove watchlist toggle ────────────────────────────────────
if ticker:
in_watch = ticker in st.session_state["watchlist"]
label = "— Remove from watchlist" if in_watch else "+ Save to watchlist"
if st.button(label, key="watch_toggle", use_container_width=True):
if in_watch:
st.session_state["watchlist"].remove(ticker)
else:
if len(st.session_state["watchlist"]) >= 10:
st.toast("Watchlist full — 10 tickers maximum")
else:
st.session_state["watchlist"].append(ticker)
st.rerun()
# ── Watchlist ──────────────────────────────────────────────────────────
render_watchlist()
st.divider()
render_top_movers(True)
# ── Market Bar ────────────────────────────────────────────────────────────────
with st.container():
render_market_bar()
st.divider()
# ── Main Content ──────────────────────────────────────────────────────────────
if not ticker:
_watchlist = st.session_state.get("watchlist", [])
if _watchlist:
render_quotetable(_watchlist)
else:
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_plain(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 "—"
_sign = "+" if (_chg_pct or 0) >= 0 else "-"
_chg_s = (
f"{_chg_arrow} {abs(_chg_abs):,.2f} ({_sign}{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"), 1)
_raw_eps = _hdr_info.get("trailingEps")
_eps = ("$" + _fmt_plain(_raw_eps, 2)) if _raw_eps else "—"
_div = _fmt_pct(_hdr_info.get("dividendYield"))
_beta = _fmt_plain(_hdr_info.get("beta"), 2)
_short = _fmt_pct(_hdr_info.get("shortPercentOfFloat"))
_fwd_eps = _hdr_info.get("forwardEps")
_eps_sub = ("fwd $" + _fmt_plain(_fwd_eps, 2)) if _fwd_eps else ""
_short_rat = _hdr_info.get("shortRatio")
_short_sub = (f"{float(_short_rat):.1f} d to cover") if _short_rat else ""
st.markdown(
""
"
"
"
"
"
"
+ ("" + _sector_e + "" if _sector_e else "")
+ "" + _name_e + ""
"
"
"
" + _sym_e + ""
"
"
"
"
"
52-wk"
"
" + _lo_s + ""
"
"
"
" + _hi_s + ""
"
"
"
"
"" + _price_s + ""
"" + _chg_s + ""
"
"
"
"
"
"
"
Market Cap" + _mktcap + "
"
"
P / E" + _pe + "
"
"
EPS · LTM" + _eps + ""
+ ("" + _eps_sub + "" if _eps_sub else "")
+ "
"
"
Div Yield" + _div + "
"
"
Beta" + _beta + "
"
"
Short Int." + _short + ""
+ ("" + _short_sub + "" if _short_sub else "")
+ "
"
"
"
"
",
unsafe_allow_html=True,
)
_active = st.session_state["active_tab"]
if _active == "overview":
try:
render_overview(ticker)
except Exception as e:
st.error(f"Overview failed to load: {e}")
elif _active == "financials":
try:
render_financials(ticker)
except Exception as e:
st.error(f"Financials failed to load: {e}")
elif _active == "valuation":
try:
render_valuation(ticker)
except Exception as e:
st.error(f"Valuation failed to load: {e}")
elif _active == "options":
try:
render_options(ticker)
except Exception as e:
st.error(f"Options data failed to load: {e}")
elif _active == "insiders":
try:
render_insiders(ticker)
except Exception as e:
st.error(f"Insider data failed to load: {e}")
elif _active == "filings":
try:
render_filings(ticker)
except Exception as e:
st.error(f"Filings failed to load: {e}")
elif _active == "news":
try:
render_news(ticker)
except Exception as e:
st.error(f"News failed to load: {e}")