"""Company overview — score card, key stats, 52W range, short interest, price chart.""" import streamlit as st import plotly.graph_objects as go from services.data_service import get_company_info, get_price_history from utils.formatters import fmt_large, fmt_currency, fmt_pct, fmt_ratio PERIODS = {"1 Month": "1mo", "3 Months": "3mo", "6 Months": "6mo", "1 Year": "1y", "5 Years": "5y"} SECTOR_ETF_MAP = { "Technology": "XLK", "Communication Services": "XLC", "Consumer Cyclical": "XLY", "Consumer Defensive": "XLP", "Financial Services": "XLF", "Healthcare": "XLV", "Industrials": "XLI", "Energy": "XLE", "Utilities": "XLU", "Real Estate": "XLRE", "Basic Materials": "XLB", } INDUSTRY_PEER_MAP = { "consumer electronics": ["SONY", "DELL", "HPQ"], "software - infrastructure": ["MSFT", "ORCL", "CRM"], "semiconductors": ["NVDA", "AMD", "AVGO"], "internet content & information": ["GOOGL", "META", "RDDT"], "banks - diversified": ["JPM", "BAC", "WFC"], "credit services": ["V", "MA", "AXP"], "insurance - diversified": ["BRK-B", "AIG", "ALL"], "reit - industrial": ["PLD", "PSA", "EXR"], } SECTOR_PEER_MAP = { "Technology": ["AAPL", "MSFT", "NVDA"], "Communication Services": ["GOOGL", "META", "NFLX"], "Consumer Cyclical": ["AMZN", "TSLA", "HD"], "Consumer Defensive": ["WMT", "COST", "PG"], "Financial Services": ["JPM", "BAC", "GS"], "Healthcare": ["LLY", "UNH", "JNJ"], "Industrials": ["GE", "CAT", "RTX"], "Energy": ["XOM", "CVX", "COP"], "Utilities": ["NEE", "DUK", "SO"], "Real Estate": ["PLD", "AMT", "EQIX"], } # ── Score card ─────────────────────────────────────────────────────────────── def _score_card(info: dict) -> None: """Render a row of green/yellow/red signal badges.""" signals: list[tuple[str, str, str, str]] = [] # (label, color, value, description) # Valuation — trailing P/E pe = info.get("trailingPE") if pe and pe > 0: if pe < 15: signals.append(("Valuation", "green", f"P/E {pe:.1f}x", "Attractively valued")) elif pe < 30: signals.append(("Valuation", "yellow", f"P/E {pe:.1f}x", "Fairly valued")) else: signals.append(("Valuation", "red", f"P/E {pe:.1f}x", "Richly valued")) else: signals.append(("Valuation", "neutral", "P/E N/A", "No trailing earnings")) # Revenue growth (TTM YoY) rev_growth = info.get("revenueGrowth") if rev_growth is not None: if rev_growth > 0.10: signals.append(("Growth", "green", f"{rev_growth*100:+.0f}% rev", "Strong growth")) elif rev_growth >= 0: signals.append(("Growth", "yellow", f"{rev_growth*100:+.0f}% rev", "Slow growth")) else: signals.append(("Growth", "red", f"{rev_growth*100:+.0f}% rev", "Declining revenue")) # Profitability — net margin margin = info.get("profitMargins") if margin is not None: if margin > 0.15: signals.append(("Profit", "green", f"{margin*100:.0f}% margin", "High margins")) elif margin > 0.05: signals.append(("Profit", "yellow", f"{margin*100:.0f}% margin", "Moderate margins")) else: signals.append(("Profit", "red", f"{margin*100:.0f}% margin", "Thin/negative margins")) # Leverage — D/E (yfinance returns as %, e.g. 162 = 1.62x) de = info.get("debtToEquity") if de is not None: de_x = de / 100 if de_x < 0.5: signals.append(("Leverage", "green", f"D/E {de_x:.2f}x", "Low leverage")) elif de_x < 2.0: signals.append(("Leverage", "yellow", f"D/E {de_x:.2f}x", "Moderate leverage")) else: signals.append(("Leverage", "red", f"D/E {de_x:.2f}x", "High leverage")) # Momentum — price vs 52W high price = info.get("currentPrice") or info.get("regularMarketPrice") high52 = info.get("fiftyTwoWeekHigh") if price and high52 and high52 > 0: from_high_pct = (price - high52) / high52 * 100 if from_high_pct > -10: signals.append(("Momentum", "green", f"{from_high_pct:.0f}% from 52W↑", "Near highs")) elif from_high_pct > -25: signals.append(("Momentum", "yellow", f"{from_high_pct:.0f}% from 52W↑", "Mid-range")) else: signals.append(("Momentum", "red", f"{from_high_pct:.0f}% from 52W↑", "Far from highs")) # Short interest short_pct = info.get("shortPercentOfFloat") if short_pct is not None: if short_pct < 0.05: signals.append(("Short Int.", "green", f"{short_pct*100:.1f}% float", "Low short interest")) elif short_pct < 0.15: signals.append(("Short Int.", "yellow", f"{short_pct*100:.1f}% float", "Moderate short interest")) else: signals.append(("Short Int.", "red", f"{short_pct*100:.1f}% float", "High short interest")) if not signals: return color_map = { "green": ("rgba(46,204,113,0.15)", "#7ce3a1"), "yellow": ("rgba(243,156,18,0.15)", "#f0c040"), "red": ("rgba(231,76,60,0.15)", "#ff8a8a"), "neutral": ("rgba(255,255,255,0.05)", "#9aa0b0"), } cards_html = "" for label, color, value, desc in signals: bg, fg = color_map[color] cards_html += ( f'
' f'
{label}
' f'
{value}
' f'
{desc}
' f'
' ) st.markdown( f'
{cards_html}
', unsafe_allow_html=True, ) # ── 52-week range bar ──────────────────────────────────────────────────────── def _render_52w_bar(info: dict) -> None: low = info.get("fiftyTwoWeekLow") high = info.get("fiftyTwoWeekHigh") price = info.get("currentPrice") or info.get("regularMarketPrice") if not (low and high and price and high > low): return pct = max(0.0, min(100.0, (price - low) / (high - low) * 100)) from_low_pct = (price - low) / low * 100 to_high_pct = (high - price) / price * 100 st.markdown( f"""
52W Low: {fmt_currency(low)} {fmt_currency(price)}  ·  {pct:.0f}% of range 52W High: {fmt_currency(high)}
+{from_low_pct:.1f}% above low {to_high_pct:.1f}% below high
""", unsafe_allow_html=True, ) # ── Short interest strip ───────────────────────────────────────────────────── def _render_short_interest(info: dict) -> None: short_pct = info.get("shortPercentOfFloat") short_ratio = info.get("shortRatio") shares_short = info.get("sharesShort") shares_short_prior = info.get("sharesShortPriorMonth") if not any([short_pct, short_ratio, shares_short]): return st.markdown("**Short Interest**") cols = st.columns(4) cols[0].metric( "Short % of Float", f"{short_pct * 100:.2f}%" if short_pct is not None else "—", ) cols[1].metric( "Days to Cover", f"{short_ratio:.1f}" if short_ratio is not None else "—", help="Shares short ÷ avg daily volume. Higher = harder to unwind.", ) cols[2].metric( "Shares Short", fmt_large(shares_short) if shares_short else "—", ) if shares_short and shares_short_prior: chg = (shares_short - shares_short_prior) / shares_short_prior * 100 cols[3].metric( "vs Prior Month", fmt_large(shares_short_prior), delta=f"{chg:+.1f}%", ) else: cols[3].metric("Prior Month", fmt_large(shares_short_prior) if shares_short_prior else "—") def _suggest_relative_comparisons(ticker: str, info: dict) -> list[tuple[str, str]]: comparisons: list[tuple[str, str]] = [("S&P 500", "^GSPC")] sector = str(info.get("sector") or "").strip() industry = str(info.get("industry") or "").strip().lower() sector_etf = SECTOR_ETF_MAP.get(sector) if sector_etf: comparisons.append((f"{sector} ETF", sector_etf)) peer_candidates = INDUSTRY_PEER_MAP.get(industry) or SECTOR_PEER_MAP.get(sector) or [] for peer in peer_candidates: peer_up = peer.upper() if peer_up != ticker.upper(): comparisons.append((peer_up, peer_up)) deduped: list[tuple[str, str]] = [] seen = set() for label, symbol in comparisons: if symbol not in seen: deduped.append((label, symbol)) seen.add(symbol) return deduped[:5] def _build_relative_series(symbol: str, period: str): hist = get_price_history(symbol, period=period) if hist.empty or "Close" not in hist.columns: return None closes = hist["Close"].dropna() if closes.empty: return None base = float(closes.iloc[0]) if base <= 0: return None return (closes / base - 1.0) * 100.0 def _render_relative_chart(ticker: str, info: dict, period: str): options = _suggest_relative_comparisons(ticker, info) option_map = {label: symbol for label, symbol in options} default_labels = ["S&P 500"] if "S&P 500" in option_map else [label for label, _ in options[:1]] selected_labels = st.multiselect( "Compare against", options=list(option_map.keys()), default=default_labels, key=f"overview_relative_comparisons_{ticker.upper()}", help="Performance is rebased to 0% at the start of the selected period.", ) fig = go.Figure() subject_series = _build_relative_series(ticker, period) if subject_series is None: st.warning("No price history available.") return fig.add_trace(go.Scatter( x=subject_series.index, y=subject_series.values, mode="lines", name=ticker.upper(), line=dict(color="#4F8EF7", width=2.5), )) palette = ["#7ce3a1", "#F7A24F", "#c084fc", "#ff8a8a", "#9ad1ff"] plotted = 1 for idx, label in enumerate(selected_labels): symbol = option_map[label] series = _build_relative_series(symbol, period) if series is None: continue fig.add_trace(go.Scatter( x=series.index, y=series.values, mode="lines", name=label, line=dict(color=palette[idx % len(palette)], width=1.8), )) plotted += 1 if plotted == 1: st.caption("No comparison series were available for the selected period.") fig.update_layout( margin=dict(l=0, r=0, t=10, b=0), xaxis=dict(showgrid=False, zeroline=False), yaxis=dict( showgrid=True, gridcolor="rgba(255,255,255,0.05)", zeroline=True, zerolinecolor="rgba(255,255,255,0.12)", ticksuffix="%", ), plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)", hovermode="x unified", height=320, legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0), ) st.plotly_chart(fig, use_container_width=True) # ── Main render ────────────────────────────────────────────────────────────── def render_overview(ticker: str): info = get_company_info(ticker) if not info: st.error(f"Could not load data for **{ticker}**. Check the ticker symbol.") return name = info.get("longName") or info.get("shortName", ticker.upper()) price = info.get("currentPrice") or info.get("regularMarketPrice") prev_close = info.get("regularMarketPreviousClose") or info.get("previousClose") price_change = price_change_pct = None if price and prev_close: price_change = price - prev_close price_change_pct = price_change / prev_close # ── Header ────────────────────────────────────────────────────────────── col1, col2 = st.columns([3, 1]) with col1: st.subheader(f"{name} ({ticker.upper()})") sector = info.get("sector", "") industry = info.get("industry", "") if sector: st.caption(f"{sector} · {industry}") with col2: delta_str = None if price_change is not None and price_change_pct is not None: delta_str = f"{price_change:+.2f} ({price_change_pct * 100:+.2f}%)" st.metric( label="Price", value=fmt_currency(price) if price else "—", delta=delta_str, ) # ── Score card ────────────────────────────────────────────────────────── _score_card(info) # ── Key stats strip ───────────────────────────────────────────────────── stats_cols = st.columns(6) stats = [ ("Mkt Cap", fmt_large(info.get("marketCap"))), ("P/E (TTM)", fmt_ratio(info.get("trailingPE"))), ("EPS (TTM)", fmt_currency(info.get("trailingEps"))), ("Volume", fmt_large(info.get("volume"))), ("Avg Volume", fmt_large(info.get("averageVolume"))), ("Beta", fmt_ratio(info.get("beta"))), ] for col, (label, val) in zip(stats_cols, stats): col.metric(label, val) st.write("") # ── 52-week range bar ──────────────────────────────────────────────────── _render_52w_bar(info) # ── Short interest ─────────────────────────────────────────────────────── _render_short_interest(info) st.divider() # ── Price chart ───────────────────────────────────────────────────────── control_col1, control_col2 = st.columns([3, 1.4]) with control_col1: period_label = st.radio( "Period", options=list(PERIODS.keys()), index=3, horizontal=True, label_visibility="collapsed", key=f"overview_period_{ticker.upper()}", ) with control_col2: chart_mode = st.radio( "Chart mode", options=["Price", "Relative"], horizontal=True, label_visibility="collapsed", key=f"overview_chart_mode_{ticker.upper()}", ) period = PERIODS[period_label] if chart_mode == "Relative": _render_relative_chart(ticker, info, period) return hist = get_price_history(ticker, period=period) if hist.empty: st.warning("No price history available.") return fig = go.Figure() fig.add_trace(go.Scatter( x=hist.index, y=hist["Close"], mode="lines", name="Close", line=dict(color="#4F8EF7", width=2), fill="tozeroy", fillcolor="rgba(79,142,247,0.08)", )) fig.update_layout( margin=dict(l=0, r=0, t=10, b=0), xaxis=dict(showgrid=False, zeroline=False), yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.05)", zeroline=False), plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)", hovermode="x unified", height=320, ) st.plotly_chart(fig, use_container_width=True)