aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app.py44
-rw-r--r--components/market_bar.py84
-rw-r--r--components/valuation.py33
3 files changed, 134 insertions, 27 deletions
diff --git a/app.py b/app.py
index 78b503f..6db7f0a 100644
--- a/app.py
+++ b/app.py
@@ -63,6 +63,10 @@ from components.news import render_news
from services.data_service import get_company_info, search_tickers
+if "ticker" not in st.session_state:
+ st.session_state["ticker"] = "AAPL"
+
+
# ── Sidebar ──────────────────────────────────────────────────────────────────
with st.sidebar:
@@ -70,34 +74,40 @@ with st.sidebar:
st.caption("Financial Analysis Dashboard")
st.divider()
- # Search input
- query = st.text_input(
- "Search company or ticker",
- placeholder="e.g. Apple, AAPL, MSFT…",
- key="search_query",
- ).strip()
+ 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
- # Autocomplete: show results if query looks like a search term (not a bare ticker)
- selected_symbol = None
- if query:
- results = search_tickers(query)
if results:
options = {f"{r['symbol']} — {r['name']} ({r['exchange']})": r["symbol"] for r in results}
choice = st.selectbox(
- "Select",
+ "Matches",
options=list(options.keys()),
label_visibility="collapsed",
)
selected_symbol = options[choice]
- else:
- # Treat the raw input as a direct ticker
+ elif query:
selected_symbol = query.upper()
- if st.button("Analyze", use_container_width=True, type="primary"):
- if selected_symbol:
- st.session_state["ticker"] = selected_symbol
+ submitted = st.form_submit_button("Open", use_container_width=True, type="primary")
+
+ if submitted and selected_symbol:
+ st.session_state["ticker"] = selected_symbol
+
+ st.caption(f"Currently viewing: **{st.session_state['ticker']}**")
+
+ quick_cols = st.columns(4)
+ for col, symbol in zip(quick_cols, ["AAPL", "MSFT", "NVDA", "JPM"]):
+ if col.button(symbol, use_container_width=True):
+ st.session_state["ticker"] = symbol
- ticker = st.session_state.get("ticker", "AAPL")
+ ticker = st.session_state["ticker"]
# Quick company info in sidebar
st.divider()
diff --git a/components/market_bar.py b/components/market_bar.py
index cb813e5..411b232 100644
--- a/components/market_bar.py
+++ b/components/market_bar.py
@@ -1,25 +1,89 @@
"""Market bar — displays major indices at the top of the app."""
import streamlit as st
from services.data_service import get_market_indices
-from utils.formatters import fmt_number
+
+
+def _delta_class(change_pct: float | None) -> str:
+ if change_pct is None:
+ return "neutral"
+ return "positive" if change_pct >= 0 else "negative"
+
+
+def _delta_text(change_pct: float | None) -> str:
+ if change_pct is None:
+ return "—"
+ arrow = "▲" if change_pct >= 0 else "▼"
+ return f"{arrow} {change_pct * 100:+.2f}%"
def render_market_bar():
indices = get_market_indices()
+ st.markdown(
+ """
+ <style>
+ .prism-market-card {
+ background: rgba(22, 28, 39, 0.92);
+ border: 1px solid rgba(255,255,255,0.06);
+ border-radius: 14px;
+ padding: 0.8rem 0.95rem;
+ min-height: 96px;
+ }
+ .prism-market-label {
+ color: #9aa0b0;
+ font-size: 0.78rem;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ margin-bottom: 0.45rem;
+ }
+ .prism-market-value {
+ color: #f3f6fb;
+ font-size: 1.5rem;
+ font-weight: 700;
+ line-height: 1.15;
+ margin-bottom: 0.45rem;
+ }
+ .prism-market-delta {
+ display: inline-block;
+ border-radius: 999px;
+ font-size: 0.78rem;
+ font-weight: 600;
+ padding: 0.18rem 0.5rem;
+ }
+ .prism-market-delta.positive {
+ color: #7ce3a1;
+ background: rgba(28, 131, 72, 0.18);
+ }
+ .prism-market-delta.negative {
+ color: #ff8a8a;
+ background: rgba(180, 47, 47, 0.18);
+ }
+ .prism-market-delta.neutral {
+ color: #c6cfdd;
+ background: rgba(198, 207, 221, 0.12);
+ }
+ </style>
+ """,
+ unsafe_allow_html=True,
+ )
+
cols = st.columns(len(indices))
for col, (name, data) in zip(cols, indices.items()):
price = data.get("price")
change_pct = data.get("change_pct")
- if price is None:
- col.metric(label=name, value="—")
- continue
+ value = f"{price:,.2f}" if price is not None else "—"
+ delta_class = _delta_class(change_pct)
+ delta_text = _delta_text(change_pct)
- price_str = f"{price:,.2f}"
- delta_str = f"{change_pct * 100:+.2f}%" if change_pct is not None else None
- col.metric(
- label=name,
- value=price_str,
- delta=delta_str,
+ col.markdown(
+ f"""
+ <div class="prism-market-card">
+ <div class="prism-market-label">{name}</div>
+ <div class="prism-market-value">{value}</div>
+ <span class="prism-market-delta {delta_class}">{delta_text}</span>
+ </div>
+ """,
+ unsafe_allow_html=True,
)
diff --git a/components/valuation.py b/components/valuation.py
index 82e0f0d..d7a84d4 100644
--- a/components/valuation.py
+++ b/components/valuation.py
@@ -15,6 +15,27 @@ from services.valuation_service import run_dcf, run_ev_ebitda, compute_historica
from utils.formatters import fmt_ratio, fmt_pct, fmt_large, fmt_currency
+FINANCIAL_SECTORS = {"Financial Services"}
+FINANCIAL_INDUSTRY_KEYWORDS = (
+ "bank",
+ "insurance",
+ "asset management",
+ "capital markets",
+ "financial data",
+ "credit services",
+ "mortgage",
+ "reit",
+)
+
+
+def _is_financial_company(info: dict) -> bool:
+ sector = str(info.get("sector") or "").strip()
+ industry = str(info.get("industry") or "").strip().lower()
+ if sector in FINANCIAL_SECTORS:
+ return True
+ return any(keyword in industry for keyword in FINANCIAL_INDUSTRY_KEYWORDS)
+
+
def render_valuation(ticker: str):
tab_ratios, tab_dcf, tab_comps, tab_analyst, tab_earnings = st.tabs([
"Key Ratios", "DCF Model", "Comps", "Analyst Targets", "Earnings History"
@@ -97,6 +118,18 @@ def _render_ratios(ticker: str):
def _render_dcf(ticker: str):
info = get_company_info(ticker)
+
+ if _is_financial_company(info):
+ st.warning(
+ "DCF is disabled for financial companies in Prism. Free-cash-flow and capital-structure "
+ "assumptions are not directly comparable for banks, insurers, and similar businesses."
+ )
+ st.caption(
+ "Use ratios, comps, earnings history, and analyst targets instead. A bank-specific valuation "
+ "framework can be added later."
+ )
+ return
+
shares = info.get("sharesOutstanding") or info.get("floatShares")
current_price = info.get("currentPrice") or info.get("regularMarketPrice")
total_debt = info.get("totalDebt") or 0.0