From 49368331324f201307fd260d288e5c5f65ea3fcc Mon Sep 17 00:00:00 2001 From: Tyler Date: Sat, 16 May 2026 00:43:47 -0700 Subject: Rewrite overview tab as client-side HTML surface --- components/overview.py | 788 +++++++++++++++++++++++++++---------------------- 1 file changed, 436 insertions(+), 352 deletions(-) diff --git a/components/overview.py b/components/overview.py index 433f6e5..27c4166 100644 --- a/components/overview.py +++ b/components/overview.py @@ -1,11 +1,14 @@ -"""Company overview — score card, key stats, 52W range, short interest, price chart.""" +"""Company overview tab rendered as a client-side HTML surface.""" +from datetime import datetime +from html import escape as _esc + +import pandas as pd 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 +import streamlit.components.v1 as components +from services.data_service import get_company_info, get_latest_price, get_price_history -PERIODS = {"1 Month": "1mo", "3 Months": "3mo", "6 Months": "6mo", "1 Year": "1y", "5 Years": "5y"} +PERIODS = {"1mo": "1M", "3mo": "3M", "6mo": "6M", "1y": "1Y", "5y": "5Y"} SECTOR_ETF_MAP = { "Technology": "XLK", "Communication Services": "XLC", @@ -41,405 +44,486 @@ SECTOR_PEER_MAP = { "Utilities": ["NEE", "DUK", "SO"], "Real Estate": ["PLD", "AMT", "EQIX"], } +_XMAP = {"NYQ": "NYSE", "NMS": "NASDAQ", "NGM": "NASDAQ", "NCM": "NASDAQ", "ASE": "AMEX"} + + +def _to_float(val): + try: + n = float(val) + except (TypeError, ValueError): + return None + if pd.isna(n): + return None + return n -# ── Score card ─────────────────────────────────────────────────────────────── +def _to_int(val): + try: + n = float(val) + except (TypeError, ValueError): + return None + if pd.isna(n): + return None + return int(round(n)) -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: +def _series_points(symbol: str, period: str) -> list[dict]: + hist = get_price_history(symbol, period=period) + if hist is None or hist.empty or "Close" not in hist.columns: + return [] + closes = pd.to_numeric(hist["Close"], errors="coerce") + out = [] + for idx, v in closes.dropna().items(): + ts = idx.strftime("%Y-%m-%d") + out.append({"d": ts, "c": float(v)}) + return out + + +def _build_signals(info: dict) -> list[dict]: + signals = [] + + pe = _to_float(info.get("trailingPE")) + if pe is not None and pe > 0: if pe < 15: - signals.append(("Valuation", "green", f"P/E {pe:.1f}x", "Attractively valued")) + signals.append({"k": "Valuation", "state": "pos", "v": "P/E " + "{:.1f}".format(pe) + "x", "d": "Attractive multiple"}) elif pe < 30: - signals.append(("Valuation", "yellow", f"P/E {pe:.1f}x", "Fairly valued")) + signals.append({"k": "Valuation", "state": "warn", "v": "P/E " + "{:.1f}".format(pe) + "x", "d": "Middle of range"}) else: - signals.append(("Valuation", "red", f"P/E {pe:.1f}x", "Richly valued")) + signals.append({"k": "Valuation", "state": "neg", "v": "P/E " + "{:.1f}".format(pe) + "x", "d": "Premium multiple"}) else: - signals.append(("Valuation", "neutral", "P/E N/A", "No trailing earnings")) + signals.append({"k": "Valuation", "state": "neu", "v": "P/E unavailable", "d": "No trailing earnings"}) - # Revenue growth (TTM YoY) - rev_growth = info.get("revenueGrowth") + rev_growth = _to_float(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")) + state = "pos" + desc = "Strong top-line growth" elif rev_growth >= 0: - signals.append(("Growth", "yellow", f"{rev_growth*100:+.0f}% rev", "Slow growth")) + state = "warn" + desc = "Low but positive growth" else: - signals.append(("Growth", "red", f"{rev_growth*100:+.0f}% rev", "Declining revenue")) + state = "neg" + desc = "Contracting revenue" + signals.append({"k": "Growth", "state": state, "v": "{:+.0f}%".format(rev_growth * 100.0), "d": desc}) - # Profitability — net margin - margin = info.get("profitMargins") + margin = _to_float(info.get("profitMargins")) if margin is not None: if margin > 0.15: - signals.append(("Profit", "green", f"{margin*100:.0f}% margin", "High margins")) + state = "pos" + desc = "High net margin" elif margin > 0.05: - signals.append(("Profit", "yellow", f"{margin*100:.0f}% margin", "Moderate margins")) + state = "warn" + desc = "Moderate net margin" else: - signals.append(("Profit", "red", f"{margin*100:.0f}% margin", "Thin/negative margins")) + state = "neg" + desc = "Thin or negative margin" + signals.append({"k": "Profit", "state": state, "v": "{:.0f}%".format(margin * 100.0), "d": desc}) - # Leverage — D/E (yfinance returns as %, e.g. 162 = 1.62x) - de = info.get("debtToEquity") + de = _to_float(info.get("debtToEquity")) if de is not None: - de_x = de / 100 + de_x = de / 100.0 if de_x < 0.5: - signals.append(("Leverage", "green", f"D/E {de_x:.2f}x", "Low leverage")) + state = "pos" + desc = "Low leverage" elif de_x < 2.0: - signals.append(("Leverage", "yellow", f"D/E {de_x:.2f}x", "Moderate leverage")) + state = "warn" + desc = "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")) + state = "neg" + desc = "High leverage" + signals.append({"k": "Leverage", "state": state, "v": "D/E " + "{:.2f}".format(de_x) + "x", "d": desc}) + + price = _to_float(info.get("currentPrice") or info.get("regularMarketPrice")) + high52 = _to_float(info.get("fiftyTwoWeekHigh")) + if price is not None and high52 is not None and high52 > 0: + from_high = (price - high52) / high52 * 100.0 + if from_high > -10: + state = "pos" + desc = "Near 52-week highs" + elif from_high > -25: + state = "warn" + desc = "Mid-range momentum" else: - signals.append(("Momentum", "red", f"{from_high_pct:.0f}% from 52W↑", "Far from highs")) + state = "neg" + desc = "Far below highs" + signals.append({"k": "Momentum", "state": state, "v": "{:+.0f}%".format(from_high), "d": desc}) - # Short interest - short_pct = info.get("shortPercentOfFloat") + short_pct = _to_float(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")) + state = "pos" + desc = "Low crowding" elif short_pct < 0.15: - signals.append(("Short Int.", "yellow", f"{short_pct*100:.1f}% float", "Moderate short interest")) + state = "warn" + desc = "Moderate crowding" else: - signals.append(("Short Int.", "red", f"{short_pct*100:.1f}% float", "High short interest")) - - if not signals: - return - - color_map = { - "green": ("#15241A", "#4F8C5E"), - "yellow": ("#2A1F0F", "#C49545"), - "red": ("#2A1517", "#B5494B"), - "neutral": ("#181D26", "#5E5849"), - } - - 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""" -
-
- {fmt_currency(low)} - - {fmt_currency(price)}  ·  {pct:.0f}% - - {fmt_currency(high)} -
-
-
-
-
-
- +{from_low_pct:.1f}% from low - {to_high_pct:.1f}% to 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) + state = "neg" + desc = "Elevated crowding" + signals.append({"k": "Short Int.", "state": state, "v": "{:.1f}%".format(short_pct * 100.0), "d": desc}) - 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 "—") + return signals -def _suggest_relative_comparisons(ticker: str, info: dict) -> list[tuple[str, str]]: - comparisons: list[tuple[str, str]] = [("S&P 500", "^GSPC")] - +def _comparison_list(ticker: str, info: dict) -> list[dict]: + out = [{"label": "S&P 500", "symbol": "^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)) + etf = SECTOR_ETF_MAP.get(sector) + if etf: + out.append({"label": sector + " ETF", "symbol": 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)) + peers = INDUSTRY_PEER_MAP.get(industry) or SECTOR_PEER_MAP.get(sector) or [] + for p in peers: + pp = str(p).upper() + if pp != ticker.upper(): + out.append({"label": pp, "symbol": pp}) - deduped: list[tuple[str, str]] = [] + deduped = [] seen = set() - for label, symbol in comparisons: - if symbol not in seen: - deduped.append((label, symbol)) - seen.add(symbol) - return deduped[:5] - + for row in out: + sym = row["symbol"] + if sym not in seen: + deduped.append(row) + seen.add(sym) + return deduped[:6] -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 +def render_overview(ticker: str): + import json as _json - base = float(closes.iloc[0]) - if base <= 0: - return None + info = get_company_info(ticker) or {} + if not info: + st.error("Could not load data for this ticker.") + return - return (closes / base - 1.0) * 100.0 + tkr = str(ticker or "").upper() + name = str(info.get("longName") or info.get("shortName") or tkr) + sector = str(info.get("sector") or "") or None + industry = str(info.get("industry") or "") or None + exchange_raw = str(info.get("exchange") or "") + exchange = _XMAP.get(exchange_raw, exchange_raw) or None + + price = _to_float(info.get("currentPrice") or info.get("regularMarketPrice") or get_latest_price(ticker)) + prev_close = _to_float(info.get("regularMarketPreviousClose") or info.get("previousClose")) + + comparisons = _comparison_list(tkr, info) + symbols = [tkr] + for c in comparisons: + sym = c.get("symbol") + if sym and sym not in symbols: + symbols.append(sym) + + series = {} + for period in PERIODS.keys(): + bucket = {} + for sym in symbols: + pts = _series_points(sym, period) + if pts: + bucket[sym] = pts + series[period] = bucket + + short_pct = _to_float(info.get("shortPercentOfFloat")) + short_ratio = _to_float(info.get("shortRatio")) + shares_short = _to_int(info.get("sharesShort")) + shares_short_prior = _to_int(info.get("sharesShortPriorMonth")) + short_delta_pct = None + if shares_short is not None and shares_short_prior is not None and shares_short_prior > 0: + short_delta_pct = (float(shares_short) - float(shares_short_prior)) / float(shares_short_prior) + + payload = { + "ticker": tkr, + "name": name, + "sector": sector, + "industry": industry, + "exchange": exchange, + "price": price, + "prev_close": prev_close, + "signals": _build_signals(info), + "stats": { + "marketCap": _to_float(info.get("marketCap")), + "trailingPE": _to_float(info.get("trailingPE")), + "trailingEps": _to_float(info.get("trailingEps")), + "volume": _to_float(info.get("volume")), + "averageVolume": _to_float(info.get("averageVolume")), + "beta": _to_float(info.get("beta")), + }, + "range_52w": { + "low": _to_float(info.get("fiftyTwoWeekLow")), + "high": _to_float(info.get("fiftyTwoWeekHigh")), + "price": price, + }, + "short_interest": { + "shortPercentOfFloat": short_pct, + "shortRatio": short_ratio, + "sharesShort": shares_short, + "sharesShortPriorMonth": shares_short_prior, + "sharesShortDeltaPct": short_delta_pct, + }, + "comparisons": comparisons, + "series": series, + } + meta = { + "updated_label": datetime.now().strftime("%Y-%m-%d %H:%M"), + "default_period": "1y", + "default_mode": "price", + "default_comparisons": ["^GSPC"], + } -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]] + payload_js = "const OVERVIEW_DATA=" + _json.dumps(payload) + ";" + meta_js = "const OVERVIEW_META=" + _json.dumps(meta) + ";" - 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.", + if price is not None and prev_close is not None and prev_close > 0: + chg_pct = (price - prev_close) / prev_close * 100.0 + chg_sign = "+" if chg_pct >= 0 else "" + chg_arrow = "▲" if chg_pct >= 0 else "▼" + chg_str = chg_arrow + " " + chg_sign + "{:.2f}%".format(chg_pct) + chg_cls = "chg-pos" if chg_pct >= 0 else "chg-neg" + else: + chg_str = "—" + chg_cls = "" + + price_str = "${:,.2f}".format(price) if price is not None else "—" + co_name = _esc(name) + + plotly_cdn = "" + _ROOT = ( + "" ) - 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="#C2AA7A", width=2.5), - )) - - palette = ["#7ce3a1", "#C49545", "#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), + fonts_link = ( + "" + "" ) - st.plotly_chart(fig, width="stretch") + _OV_CSS = """""" + + ctx_html = ( + '
' + + '' + + _esc(tkr) + + "" + + '' + + co_name + + "" + + 'Overview' + + '
' + + "" + + _esc(exchange or "—") + + "" + + '' + + price_str + + "" + + '' + + chg_str + + "" + + "
" + ) -# ── Main render ────────────────────────────────────────────────────────────── + lede_html = ( + '
' + + '
' + + 'Snapshot' + + '
Business quality, valuation, and positioning
' + + '

A fast company snapshot with valuation and risk signals, core fundamentals, 52-week positioning, short-interest context, and price versus relative performance across multiple windows.

' + + "
" + + '
' + + '
Sourceyfinance
' + + '
Windowmarket + fundamentals
' + + '
Updated' + + _esc(meta["updated_label"]) + + "
" + + "
" + ) -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 + controls_html = ( + '
' + + '
' + + 'Period' + + '' + + '' + + '' + + '' + + '' + + "
" + + '
' + + 'Mode' + + '' + + '' + + "
" + + '' + + "
" + ) - 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 + foot_html = ( + '
' + + "Overview data provided by Yahoo Finance via yfinance · Relative performance is rebased to 0% at period start · Some fields may be unavailable for certain tickers" + + "
" + ) - hist = get_price_history(ticker, period=period) - if hist.empty: - st.warning("No price history available.") - return + js = ( + "" + ) - fig = go.Figure() - fig.add_trace(go.Scatter( - x=hist.index, - y=hist["Close"], - mode="lines", - name="Close", - line=dict(color="#C2AA7A", 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, + height = 1480 + + doc = ( + "" + + "" + + fonts_link + + plotly_cdn + + _ROOT + + _OV_CSS + + "" + + '
' + + ctx_html + + '
' + + lede_html + + '
' + + '
' + + '
' + + '
52-Week Positioning
' + + '
Short Interest
' + + "
" + + controls_html + + '
' + + '
' + + foot_html + + "
" + + js + + "" ) - st.plotly_chart(fig, width="stretch") + + components.html(doc, height=height, scrolling=False) -- cgit v1.3-2-g0d8e