1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
|
"""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",
)
# ── Global CSS ────────────────────────────────────────────────────────────────
st.markdown("""
<style>
/* Tighten metric cards */
[data-testid="metric-container"] {
padding: 0.4rem 0.6rem !important;
}
[data-testid="stMetricLabel"] {
font-size: 0.72rem !important;
font-weight: 500;
color: #9aa0b0 !important;
letter-spacing: 0.03em;
text-transform: uppercase;
}
[data-testid="stMetricValue"] {
font-size: 1.1rem !important;
font-weight: 600;
letter-spacing: -0.01em;
}
[data-testid="stMetricDelta"] svg { width: 0.75rem; height: 0.75rem; }
[data-testid="stMetricDelta"] > div { font-size: 0.78rem !important; }
/* Tighter headings */
h1 { font-size: 1.5rem !important; font-weight: 700 !important; }
h2 { font-size: 1.25rem !important; font-weight: 600 !important; }
h3 { font-size: 1.05rem !important; font-weight: 600 !important; }
/* Tighter sidebar text */
[data-testid="stSidebar"] .stCaption p {
font-size: 0.78rem !important;
line-height: 1.5 !important;
}
/* Slim dividers */
hr { margin: 0.5rem 0 !important; }
/* Tab labels */
[data-testid="stTab"] p { font-size: 0.85rem !important; font-weight: 500; }
/* Top padding — enough to clear Streamlit's sticky toolbar */
.block-container { padding-top: 3.5rem !important; }
</style>
""", unsafe_allow_html=True)
from components.market_bar import render_market_bar
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:
col_logo, col_title = st.columns([1, 2])
with col_logo:
st.image("assets/logo.png", width=60)
with col_title:
st.markdown("### Prism")
st.caption("Financial Analysis Dashboard")
st.divider()
with st.form("ticker_search_form", clear_on_submit=False):
query = st.text_input(
"Search company or ticker",
placeholder="e.g. Apple, AAPL, 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", use_container_width=True, type="primary")
if submitted and selected_symbol:
st.session_state["ticker"] = selected_symbol
if st.session_state["ticker"]:
st.caption(f"Currently viewing: **{st.session_state['ticker']}**")
ticker = st.session_state["ticker"]
# Quick company info in sidebar
st.divider()
if ticker:
info = get_company_info(ticker)
if ticker and info:
st.caption(info.get("longName", ticker))
price = get_latest_price(ticker)
prev_close = info.get("previousClose") or info.get("regularMarketPreviousClose")
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 ""
color = "#2ecc71" if chg >= 0 else "#e74c3c"
st.markdown(
f"<span style='font-size:1.3rem;font-weight:700'>${price:,.2f}</span>"
f" <span style='font-size:0.82rem;color:{color}'>{sign}{chg:+.2f} ({sign}{chg_pct:.2f}%)</span>",
unsafe_allow_html=True,
)
else:
st.markdown(
f"<span style='font-size:1.3rem;font-weight:700'>${price:,.2f}</span>",
unsafe_allow_html=True,
)
st.caption(f"Exchange: {info.get('exchange', '—')}")
st.caption(f"Currency: {info.get('currency', 'USD')}")
st.caption(f"Sector: {info.get('sector', '—')}")
employees = info.get("fullTimeEmployees")
st.caption(f"Employees: {employees:,}" if isinstance(employees, int) else "Employees: —")
website = info.get("website")
if website:
st.markdown(f"[🌐 Website]({website})")
# ── Market Bar ────────────────────────────────────────────────────────────────
with st.container():
render_market_bar()
st.divider()
# ── Main Content ──────────────────────────────────────────────────────────────
if not ticker:
st.info("Search for a company or ticker in the sidebar to get started.")
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}")
|