From b1e129bff08076fcd7dfe3ef9c3a98c8f1712a26 Mon Sep 17 00:00:00 2001 From: Openclaw Date: Sun, 29 Mar 2026 01:33:07 -0700 Subject: Improve UX and disable DCF for financials --- app.py | 46 +++++++++++++++----------- components/market_bar.py | 84 ++++++++++++++++++++++++++++++++++++++++++------ components/valuation.py | 33 +++++++++++++++++++ 3 files changed, 135 insertions(+), 28 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() - - # Autocomplete: show results if query looks like a search term (not a bare ticker) - selected_symbol = None - if query: - results = search_tickers(query) + 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( - "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( + """ + + """, + 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""" +
+
{name}
+
{value}
+ {delta_text} +
+ """, + 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 -- cgit v1.3-2-g0d8e