diff options
| -rw-r--r-- | app.py | 44 | ||||
| -rw-r--r-- | components/market_bar.py | 84 | ||||
| -rw-r--r-- | components/valuation.py | 33 |
3 files changed, 134 insertions, 27 deletions
@@ -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 |
