"""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.persistence import render_persistence_bridge
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 components.macro import render_macro
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"
render_persistence_bridge()
def open_topbar_search():
query = st.session_state.get("topbar_search_query", "").strip()
if not query:
return
results = search_tickers(query)
if results:
st.session_state["ticker"] = results[0]["symbol"]
else:
st.session_state["ticker"] = query.upper()
ticker = st.session_state["ticker"]
# ── 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)
# ── Exit to landing page ──────────────────────────────────────────────
if st.session_state.get("ticker"):
if st.button("← Watchlist", key="exit_ticker", use_container_width=True):
st.session_state["ticker"] = None
st.rerun()
# ── Workspace nav ─────────────────────────────────────────────────────
st.markdown("Workspace
", unsafe_allow_html=True)
_nav = [
("overview", "◎ Overview"),
("financials", "▦ Financials"),
("valuation", "◈ Valuation"),
("options", "◇ Options"),
("insiders", "○ Insiders"),
("filings", "▤ Filings"),
("news", "◉ News"),
("macro", "⬡ Macro"),
]
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)
# ── Sticky Top Bar + Market Bar ───────────────────────────────────────────────
with st.container(key="prism_topbar_sticky"):
_search_col, _match_col, _open_col = st.columns([3.2, 2.3, 0.7], vertical_alignment="bottom")
with _search_col:
_topbar_query = st.text_input(
"Global ticker search",
placeholder="Search ticker, company, or filing…",
key="topbar_search_query",
on_change=open_topbar_search,
).strip()
_topbar_results = search_tickers(_topbar_query) if _topbar_query else []
_topbar_selected_symbol = _topbar_query.upper() if _topbar_query else None
with _match_col:
if _topbar_results:
_topbar_options = {
r["symbol"] + " — " + r["name"] + " (" + r["exchange"] + ")": r["symbol"]
for r in _topbar_results
}
_topbar_choice = st.selectbox(
"Matches",
options=list(_topbar_options.keys()),
key="topbar_search_match",
label_visibility="collapsed",
)
_topbar_selected_symbol = _topbar_options[_topbar_choice]
else:
st.markdown("", unsafe_allow_html=True)
with _open_col:
if st.button("Open", key="topbar_search_open", use_container_width=True, type="primary"):
if _topbar_selected_symbol:
st.session_state["ticker"] = _topbar_selected_symbol
st.rerun()
ticker = st.session_state["ticker"]
with st.container(key="market_bar_sticky"):
render_market_bar()
st.divider()
# ── ⌘K / Ctrl+K shortcut — focuses top-bar ticker search ─────────────────────
components.html(
"",
height=0,
scrolling=False,
)
# ── Main Content ──────────────────────────────────────────────────────────────
if st.session_state["active_tab"] == "macro":
try:
render_macro()
except Exception as e:
st.error(f"Macro data failed to load: {e}")
st.stop()
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 top search bar.
""", 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}")