"""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"} # ── 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 "—") # ── 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 ───────────────────────────────────────────────────────── period_label = st.radio( "Period", options=list(PERIODS.keys()), index=3, horizontal=True, label_visibility="collapsed", ) period = PERIODS[period_label] 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)