"""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() # ── Sidebar ─────────────────────────────────────────────────────────────────── with st.sidebar: # Brand mark st.markdown("""
P
Prism v 1.2
""", 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"] # ── 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) # ── Market Bar ──────────────────────────────────────────────────────────────── with st.container(): render_market_bar() st.divider() # ── 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 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}")