aboutsummaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
authorTyler <tyler@tylerhoang.xyz>2026-05-16 00:43:47 -0700
committerTyler <tyler@tylerhoang.xyz>2026-05-16 00:43:47 -0700
commit49368331324f201307fd260d288e5c5f65ea3fcc (patch)
treeec1a91e105e506d85f0b7e371220320777e16eeb /components
parent05b7005a414465f7abc55d149b2272854325875d (diff)
Rewrite overview tab as client-side HTML surface
Diffstat (limited to 'components')
-rw-r--r--components/overview.py770
1 files changed, 427 insertions, 343 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"}
-# ── Score card ───────────────────────────────────────────────────────────────
+def _to_float(val):
+ try:
+ n = float(val)
+ except (TypeError, ValueError):
+ return None
+ if pd.isna(n):
+ return None
+ return 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 _to_int(val):
+ try:
+ n = float(val)
+ except (TypeError, ValueError):
+ return None
+ if pd.isna(n):
+ return None
+ return int(round(n))
+
+
+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"))
+ state = "neg"
+ desc = "High leverage"
+ signals.append({"k": "Leverage", "state": state, "v": "D/E " + "{:.2f}".format(de_x) + "x", "d": desc})
- # 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"))
+ 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'<div style="background:{bg};border:1px solid {fg}55;border-radius:2px;'
- f'padding:8px 12px;flex:1;min-width:110px;">'
- f'<div style="font-family:IBM Plex Sans,sans-serif;font-size:10px;font-weight:600;color:#5E5849;text-transform:uppercase;'
- f'letter-spacing:0.12em;margin-bottom:3px;">{label}</div>'
- f'<div style="font-family:IBM Plex Mono,monospace;font-size:0.875rem;font-weight:500;color:{fg};font-variant-numeric:tabular-nums;">{value}</div>'
- f'<div style="font-family:IBM Plex Sans,sans-serif;font-size:0.75rem;color:#8E8676;margin-top:2px;">{desc}</div>'
- f'</div>'
- )
-
- st.markdown(
- f'<div style="display:flex;gap:0.5rem;margin-bottom:0.75rem;flex-wrap:wrap;">{cards_html}</div>',
- 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"""
- <div style="margin:8px 0 16px 0;">
- <div style="display:flex;justify-content:space-between;
- font-family:'IBM Plex Mono',monospace;font-size:11px;
- font-variant-numeric:tabular-nums;
- color:#8E8676;margin-bottom:6px;">
- <span>{fmt_currency(low)}</span>
- <span style="color:#C2AA7A;font-weight:500;">
- {fmt_currency(price)} &nbsp;·&nbsp; {pct:.0f}%
- </span>
- <span>{fmt_currency(high)}</span>
- </div>
- <div style="position:relative;height:3px;background:#222934;border-radius:999px;overflow:visible;">
- <div style="position:absolute;left:0;width:{pct}%;height:100%;
- background:#C2AA7A;border-radius:999px;"></div>
- <div style="position:absolute;left:calc({pct}% - 1px);top:-4px;width:2px;height:11px;
- background:#DCC79E;border-radius:1px;"></div>
- </div>
- <div style="display:flex;justify-content:space-between;
- font-family:'IBM Plex Mono',monospace;font-size:10px;
- color:#5E5849;margin-top:5px;">
- <span>+{from_low_pct:.1f}% from low</span>
- <span>{to_high_pct:.1f}% to high</span>
- </div>
- </div>
- """,
- 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 "—")
+ state = "neg"
+ desc = "Elevated crowding"
+ signals.append({"k": "Short Int.", "state": state, "v": "{:.1f}%".format(short_pct * 100.0), "d": desc})
+ 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
-
- 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.",
- )
+def render_overview(ticker: str):
+ import json as _json
- fig = go.Figure()
- subject_series = _build_relative_series(ticker, period)
- if subject_series is None:
- st.warning("No price history available.")
+ info = get_company_info(ticker) or {}
+ if not info:
+ st.error("Could not load data for this ticker.")
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),
- ))
+ 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
- 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
+ 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"))
- if plotted == 1:
- st.caption("No comparison series were available for the selected period.")
+ 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)
- 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, width="stretch")
+ 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)
-# ── Main render ──────────────────────────────────────────────────────────────
+ 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,
+ }
-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
+ meta = {
+ "updated_label": datetime.now().strftime("%Y-%m-%d %H:%M"),
+ "default_period": "1y",
+ "default_mode": "price",
+ "default_comparisons": ["^GSPC"],
+ }
- 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")
+ payload_js = "const OVERVIEW_DATA=" + _json.dumps(payload) + ";"
+ meta_js = "const OVERVIEW_META=" + _json.dumps(meta) + ";"
- price_change = price_change_pct = None
- if price and prev_close:
- price_change = price - prev_close
- price_change_pct = price_change / prev_close
+ 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 = ""
- # ── 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,
- )
+ price_str = "${:,.2f}".format(price) if price is not None else "—"
+ co_name = _esc(name)
- # ── Score card ──────────────────────────────────────────────────────────
- _score_card(info)
+ plotly_cdn = "<script src='https://cdn.plot.ly/plotly-2.27.0.min.js'></script>"
+ _ROOT = (
+ "<style>*,*::before,*::after{box-sizing:border-box}"
+ ":root{"
+ "--ink-0:#0B0E13;--ink-1:#11151C;--ink-2:#181D26;--ink-3:#222934;--ink-4:#2C3340;"
+ "--line-1:#232934;--line-2:#2E3645;--line-3:#3D4658;"
+ "--fg-1:#F2ECDC;--fg-2:#C7C0AE;--fg-3:#8E8676;--fg-4:#5E5849;"
+ "--brass:#C2AA7A;--brass-bright:#DCC79E;--brass-deep:#8F7A50;--brass-ink:#17120A;"
+ "--oxford:#1F3D5C;--oxford-light:#2E5A87;"
+ "--positive:#4F8C5E;--positive-bg:#15241A;--negative:#B5494B;--negative-bg:#2A1517;"
+ "--warning:#C49545;--warning-bg:#2A1F0F;--info:#4A78B5;--info-bg:#11202E;"
+ "--font-display:'EB Garamond',Georgia,serif;"
+ "--font-sans:'IBM Plex Sans','Helvetica Neue',system-ui,sans-serif;"
+ "--font-mono:'IBM Plex Mono','SF Mono',Menlo,monospace;"
+ "--fs-12:0.75rem;--fs-13:0.8125rem;--fs-14:0.875rem;--fs-16:1rem;--fs-18:1.125rem;"
+ "--fs-20:1.25rem;--fs-24:1.5rem;--fs-30:1.875rem;--fs-38:2.375rem;"
+ "--tr-wider:0.12em;--tr-wide:0.04em;--tr-tight:-0.02em;"
+ "--sp-1:4px;--sp-2:8px;--sp-3:12px;--sp-4:16px;--sp-5:24px;--sp-6:32px;--sp-7:48px;"
+ "--r-1:2px;--r-2:4px;--r-3:6px;--r-full:999px;"
+ "--shadow-1:0 1px 3px rgba(0,0,0,0.4);"
+ "}"
+ "html,body{margin:0;padding:0;background:var(--ink-0);color:var(--fg-2);"
+ "font-family:var(--font-sans);font-size:14px;-webkit-font-smoothing:antialiased}"
+ "</style>"
+ )
- # ── 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)
+ fonts_link = (
+ "<link rel='preconnect' href='https://fonts.googleapis.com'>"
+ "<link href='https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;1,400;1,500"
+ "&family=IBM+Plex+Mono:wght@300;400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap'"
+ " rel='stylesheet'>"
+ )
- st.write("")
+ _OV_CSS = """<style>
+.ov-wrap{background:var(--ink-0);min-height:100vh}
+.num{font-family:var(--font-mono);font-variant-numeric:tabular-nums}
+.eyebrow-lbl{font-family:var(--font-sans);font-size:var(--fs-12);text-transform:uppercase;letter-spacing:var(--tr-wider);color:var(--fg-3);font-weight:600}
+.val-ctx{display:flex;align-items:center;gap:var(--sp-4);padding:var(--sp-3) var(--sp-5);border-bottom:1px solid var(--line-1);background:var(--ink-1)}
+.val-ctx .sym{font-family:var(--font-display);font-size:var(--fs-24);font-weight:500;letter-spacing:var(--tr-tight)}
+.val-ctx .name{font-family:var(--font-display);font-style:italic;font-size:var(--fs-16);color:var(--fg-2);margin-left:calc(-1 * var(--sp-1));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:48ch}
+.val-ctx .eyebrow-ctx{font-family:var(--font-sans);font-size:var(--fs-12);text-transform:uppercase;letter-spacing:var(--tr-wider);color:var(--fg-3);font-weight:600;white-space:nowrap}
+.val-ctx .meta{display:flex;gap:var(--sp-4);margin-left:auto;font-family:var(--font-mono);font-size:var(--fs-12);color:var(--fg-3)}
+.val-ctx .meta span{white-space:nowrap}
+.val-ctx .meta .px{color:var(--fg-1);font-size:var(--fs-14)}
+.val-ctx .meta .chg-pos{color:var(--positive)}
+.val-ctx .meta .chg-neg{color:var(--negative)}
+.ov-body{padding:var(--sp-5);display:flex;flex-direction:column;gap:var(--sp-4)}
+.ov-lede{display:grid;grid-template-columns:1.6fr 1fr;gap:var(--sp-5);align-items:stretch;background:var(--ink-1);border:1px solid var(--line-1);border-radius:var(--r-3);padding:var(--sp-5)}
+.ov-lede .left{display:flex;flex-direction:column;gap:var(--sp-2)}
+.ov-lede .ttl{font-family:var(--font-display);font-size:var(--fs-30);font-weight:500;letter-spacing:var(--tr-tight);line-height:1.1;color:var(--fg-1);margin:var(--sp-1) 0 0}
+.ov-lede .sub{font-family:var(--font-sans);font-size:var(--fs-13);color:var(--fg-2);line-height:1.55;max-width:68ch;margin:0}
+.ov-lede .right{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-3);align-content:end}
+.kr-source{display:flex;flex-direction:column;gap:2px;padding:var(--sp-3) var(--sp-4);background:var(--ink-2);border:1px solid var(--line-1);border-radius:var(--r-2)}
+.kr-source .lbl{font-family:var(--font-sans);font-size:10px;text-transform:uppercase;letter-spacing:var(--tr-wider);color:var(--fg-3);font-weight:600}
+.kr-source .v{font-family:var(--font-mono);font-size:var(--fs-14);font-variant-numeric:tabular-nums;color:var(--fg-1);font-weight:500}
+.ov-signals{display:grid;grid-template-columns:repeat(6,1fr);gap:var(--sp-3)}
+.ov-sig{background:var(--ink-1);border:1px solid var(--line-1);border-radius:var(--r-3);padding:var(--sp-3) var(--sp-4);display:flex;flex-direction:column;gap:var(--sp-1)}
+.ov-sig .k{font-family:var(--font-sans);font-size:10px;text-transform:uppercase;letter-spacing:var(--tr-wider);color:var(--fg-4);font-weight:600}
+.ov-sig .v{font-family:var(--font-mono);font-size:var(--fs-14);font-variant-numeric:tabular-nums;color:var(--fg-1)}
+.ov-sig .d{font-family:var(--font-sans);font-size:var(--fs-12);color:var(--fg-3)}
+.ov-sig.pos{border-color:var(--positive)}
+.ov-sig.pos .v{color:var(--positive)}
+.ov-sig.warn{border-color:var(--warning)}
+.ov-sig.warn .v{color:var(--warning)}
+.ov-sig.neg{border-color:var(--negative)}
+.ov-sig.neg .v{color:var(--negative)}
+.ov-sig.neu .v{color:var(--fg-3)}
+.ov-kpis{display:grid;grid-template-columns:repeat(6,1fr);gap:var(--sp-3)}
+.ov-kpi{background:var(--ink-1);border:1px solid var(--line-1);border-radius:var(--r-3);padding:var(--sp-3) var(--sp-4);display:flex;flex-direction:column;gap:var(--sp-1)}
+.ov-kpi .lbl{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:var(--tr-wider);color:var(--fg-4)}
+.ov-kpi .v{font-family:var(--font-mono);font-size:var(--fs-24);font-variant-numeric:tabular-nums;color:var(--fg-1);line-height:1.1}
+.ov-range-short{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-4)}
+.ov-card{background:var(--ink-1);border:1px solid var(--line-1);border-radius:var(--r-3);padding:var(--sp-4)}
+.ov-card .hd{font-family:var(--font-sans);font-size:var(--fs-12);font-weight:600;letter-spacing:var(--tr-wider);text-transform:uppercase;color:var(--fg-3);margin-bottom:var(--sp-3)}
+.range-grid{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:var(--sp-2);margin-bottom:var(--sp-2)}
+.range-grid .edge{font-family:var(--font-mono);font-size:var(--fs-12);color:var(--fg-3)}
+.range-grid .mid{font-family:var(--font-mono);font-size:var(--fs-14);color:var(--brass-bright)}
+.range-rail{position:relative;height:4px;border-radius:var(--r-full);background:var(--line-2)}
+.range-fill{position:absolute;left:0;top:0;height:100%;border-radius:var(--r-full);background:var(--brass)}
+.range-pin{position:absolute;top:-4px;width:2px;height:12px;border-radius:var(--r-1);background:var(--brass-bright)}
+.range-meta{display:flex;justify-content:space-between;font-family:var(--font-mono);font-size:var(--fs-12);color:var(--fg-3);margin-top:var(--sp-2)}
+.short-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--sp-3)}
+.short-cell{padding:var(--sp-2) var(--sp-3);border:1px solid var(--line-1);border-radius:var(--r-2);background:var(--ink-2)}
+.short-cell .lbl{font-family:var(--font-sans);font-size:10px;letter-spacing:var(--tr-wider);text-transform:uppercase;color:var(--fg-4)}
+.short-cell .v{font-family:var(--font-mono);font-size:var(--fs-14);color:var(--fg-1)}
+.short-cell .delta-pos{color:var(--positive)}
+.short-cell .delta-neg{color:var(--negative)}
+.ov-controls{display:grid;grid-template-columns:1fr 1fr 1.2fr;gap:var(--sp-3);padding:var(--sp-3) var(--sp-4);background:var(--ink-1);border:1px solid var(--line-1);border-radius:var(--r-3)}
+.ctl-group{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap}
+.ctl-lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:var(--tr-wider);text-transform:uppercase;color:var(--fg-4);margin-right:var(--sp-2)}
+.ctl-btn{font-family:var(--font-sans);font-size:var(--fs-12);font-weight:500;padding:5px 14px;border-radius:var(--r-2);border:1px solid var(--line-2);background:var(--ink-2);color:var(--fg-3);cursor:pointer;letter-spacing:var(--tr-wide)}
+.ctl-btn:hover{background:var(--ink-3);color:var(--fg-2)}
+.ctl-btn.active{background:var(--ink-3);color:var(--brass);border-color:var(--brass-deep)}
+.comp-wrap{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap}
+.comp-btn{font-family:var(--font-sans);font-size:var(--fs-12);padding:4px 10px;border-radius:var(--r-full);border:1px solid var(--line-2);background:var(--ink-2);color:var(--fg-3);cursor:pointer}
+.comp-btn.active{color:var(--brass);border-color:var(--brass-deep);background:var(--ink-3)}
+.comp-wrap.hidden{display:none}
+.ov-chart-card{background:var(--ink-1);border:1px solid var(--line-1);border-radius:var(--r-3);padding:var(--sp-2)}
+#ov-chart{height:320px}
+.ov-readout{background:var(--ink-1);border:1px solid var(--line-1);border-radius:var(--r-2);padding:var(--sp-3) var(--sp-4);font-family:var(--font-sans);font-size:var(--fs-13);color:var(--fg-2)}
+.va-foot{font-family:var(--font-sans);font-size:var(--fs-12);color:var(--fg-3);line-height:1.6;padding:var(--sp-3) var(--sp-5);border:1px solid var(--line-1);border-radius:var(--r-2);background:var(--ink-1)}
+@media (max-width:1100px){
+ .ov-lede,.ov-range-short{grid-template-columns:1fr}
+ .ov-lede .right{grid-template-columns:1fr}
+ .ov-signals{grid-template-columns:1fr 1fr 1fr}
+ .ov-kpis{grid-template-columns:1fr 1fr 1fr}
+ .ov-controls{grid-template-columns:1fr}
+}
+</style>"""
- # ── 52-week range bar ────────────────────────────────────────────────────
- _render_52w_bar(info)
+ ctx_html = (
+ '<div class="val-ctx">'
+ + '<span class="sym">'
+ + _esc(tkr)
+ + "</span>"
+ + '<span class="name">'
+ + co_name
+ + "</span>"
+ + '<span class="eyebrow-ctx" style="margin-left:12px">Overview</span>'
+ + '<div class="meta">'
+ + "<span>"
+ + _esc(exchange or "—")
+ + "</span>"
+ + '<span class="px num">'
+ + price_str
+ + "</span>"
+ + '<span class="'
+ + chg_cls
+ + ' num">'
+ + chg_str
+ + "</span>"
+ + "</div></div>"
+ )
- # ── Short interest ───────────────────────────────────────────────────────
- _render_short_interest(info)
+ lede_html = (
+ '<section class="ov-lede">'
+ + '<div class="left">'
+ + '<span class="eyebrow-lbl">Snapshot</span>'
+ + '<div class="ttl">Business quality, valuation, and positioning</div>'
+ + '<p class="sub">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.</p>'
+ + "</div>"
+ + '<div class="right">'
+ + '<div class="kr-source"><span class="lbl">Source</span><span class="v">yfinance</span></div>'
+ + '<div class="kr-source"><span class="lbl">Window</span><span class="v">market + fundamentals</span></div>'
+ + '<div class="kr-source"><span class="lbl">Updated</span><span class="v num">'
+ + _esc(meta["updated_label"])
+ + "</span></div>"
+ + "</div></section>"
+ )
- st.divider()
+ controls_html = (
+ '<section class="ov-controls">'
+ + '<div class="ctl-group">'
+ + '<span class="ctl-lbl">Period</span>'
+ + '<button class="ctl-btn" id="btn-p-1mo" onclick="setPeriod(\'1mo\',this)">1M</button>'
+ + '<button class="ctl-btn" id="btn-p-3mo" onclick="setPeriod(\'3mo\',this)">3M</button>'
+ + '<button class="ctl-btn" id="btn-p-6mo" onclick="setPeriod(\'6mo\',this)">6M</button>'
+ + '<button class="ctl-btn active" id="btn-p-1y" onclick="setPeriod(\'1y\',this)">1Y</button>'
+ + '<button class="ctl-btn" id="btn-p-5y" onclick="setPeriod(\'5y\',this)">5Y</button>'
+ + "</div>"
+ + '<div class="ctl-group">'
+ + '<span class="ctl-lbl">Mode</span>'
+ + '<button class="ctl-btn active" id="btn-m-price" onclick="setMode(\'price\',this)">Price</button>'
+ + '<button class="ctl-btn" id="btn-m-relative" onclick="setMode(\'relative\',this)">Relative</button>'
+ + "</div>"
+ + '<div class="comp-wrap hidden" id="ov-comps"></div>'
+ + "</section>"
+ )
- # ── 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]
+ foot_html = (
+ '<div class="va-foot">'
+ + "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"
+ + "</div>"
+ )
- if chart_mode == "Relative":
- _render_relative_chart(ticker, info, period)
- return
+ js = (
+ "<script>"
+ + payload_js
+ + meta_js
+ + "var activePeriod='1y';var activeMode='price';var activeComparisons=['^GSPC'];"
+ + "function cssVar(n){return getComputedStyle(document.documentElement).getPropertyValue(n).trim();}"
+ + "function withAlpha(hex,a){if(!hex||hex.charAt(0)!=='#'||hex.length!==7)return 'rgba(0,0,0,'+a+')';var r=parseInt(hex.substring(1,3),16),g=parseInt(hex.substring(3,5),16),b=parseInt(hex.substring(5,7),16);return 'rgba('+r+','+g+','+b+','+a+')';}"
+ + "var C_FG1=cssVar('--fg-1');var C_FG2=cssVar('--fg-2');var C_FG3=cssVar('--fg-3');var C_LINE=cssVar('--line-1');var C_BRASS=cssVar('--brass');var C_BRASS_B=cssVar('--brass-bright');var C_POS=cssVar('--positive');var C_NEG=cssVar('--negative');var C_WARN=cssVar('--warning');var C_INFO=cssVar('--info');"
+ + "var REL_COLORS=[C_BRASS,C_POS,C_INFO,C_WARN,C_NEG,cssVar('--oxford-light')];"
+ + "function asNum(v){if(v===null||v===undefined||v==='')return null;var n=Number(v);return isFinite(n)?n:null;}"
+ + "function esc(s){if(s===null||s===undefined)return '';return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}"
+ + "function fmtInt(v){var n=asNum(v);if(n===null)return '—';return Math.round(n).toLocaleString();}"
+ + "function fmtRatio(v,d){var n=asNum(v);if(n===null)return '—';return n.toFixed(d);};"
+ + "function fmtCurrency(v,d){var n=asNum(v);if(n===null)return '—';return '$'+n.toLocaleString(undefined,{minimumFractionDigits:d,maximumFractionDigits:d});}"
+ + "function fmtLarge(v){var n=asNum(v);if(n===null)return '—';var a=Math.abs(n);if(a>=1e12)return (n<0?'-':'')+(a/1e12).toFixed(2)+'T';if(a>=1e9)return (n<0?'-':'')+(a/1e9).toFixed(2)+'B';if(a>=1e6)return (n<0?'-':'')+(a/1e6).toFixed(1)+'M';if(a>=1e3)return (n<0?'-':'')+(a/1e3).toFixed(1)+'K';return String(Math.round(n));}"
+ + "function periodLabel(p){return ({'1mo':'1M','3mo':'3M','6mo':'6M','1y':'1Y','5y':'5Y'})[p]||p;}"
+ + "function getSeries(sym){var b=((OVERVIEW_DATA.series||{})[activePeriod]||{});return b[sym]||[];}"
+ + "function toTraceData(list){var x=[],y=[];list.forEach(function(r){if(r&&r.d&&r.c!==null&&r.c!==undefined){x.push(r.d);y.push(Number(r.c));}});return {x:x,y:y};}"
+ + "function rebased(list){var clean=[];list.forEach(function(r){if(r&&r.d&&r.c!==null&&r.c!==undefined){clean.push({d:r.d,c:Number(r.c)});}});if(!clean.length)return {x:[],y:[]};var base=clean[0].c;if(!isFinite(base)||base<=0)return {x:[],y:[]};var x=[],y=[];clean.forEach(function(r){x.push(r.d);y.push(((r.c/base)-1)*100);});return {x:x,y:y};}"
+ + "function renderSignals(){var arr=OVERVIEW_DATA.signals||[];var html='';arr.forEach(function(s){var st=s.state||'neu';html+='<div class=\"ov-sig '+st+'\"><div class=\"k\">'+esc(s.k||'')+'</div><div class=\"v num\">'+esc(s.v||'—')+'</div><div class=\"d\">'+esc(s.d||'')+'</div></div>';});if(!html){html='<div class=\"ov-sig neu\"><div class=\"k\">Signals</div><div class=\"v num\">—</div><div class=\"d\">No fundamentals available.</div></div>';}document.getElementById('ov-signals').innerHTML=html;}"
+ + "function renderKpis(){var s=OVERVIEW_DATA.stats||{};var rows=[['Market Cap',fmtLarge(s.marketCap)],['P/E (TTM)',fmtRatio(s.trailingPE,2)],['EPS (TTM)',fmtCurrency(s.trailingEps,2)],['Volume',fmtLarge(s.volume)],['Avg Volume',fmtLarge(s.averageVolume)],['Beta',fmtRatio(s.beta,2)]];var html='';rows.forEach(function(r){html+='<div class=\"ov-kpi\"><div class=\"lbl\">'+r[0]+'</div><div class=\"v num\">'+r[1]+'</div></div>';});document.getElementById('ov-kpis').innerHTML=html;}"
+ + "function renderRangeCard(){var rg=OVERVIEW_DATA.range_52w||{};var lo=asNum(rg.low),hi=asNum(rg.high),px=asNum(rg.price);if(lo===null||hi===null||px===null||!(hi>lo)){document.getElementById('ov-range').innerHTML='<div class=\"short-cell\"><div class=\"lbl\">52W Range</div><div class=\"v num\">Unavailable</div></div>';return;}var pct=Math.max(0,Math.min(100,((px-lo)/(hi-lo))*100));var fromLow=((px-lo)/lo)*100;var toHigh=((hi-px)/px)*100;var html='';html+='<div class=\"range-grid\"><div class=\"edge num\">'+fmtCurrency(lo,2)+'</div><div class=\"mid num\">'+fmtCurrency(px,2)+'</div><div class=\"edge num\" style=\"text-align:right\">'+fmtCurrency(hi,2)+'</div></div>';html+='<div class=\"range-rail\"><div class=\"range-fill\" style=\"width:'+pct+'%\"></div><div class=\"range-pin\" style=\"left:calc('+pct+'% - 1px)\"></div></div>';html+='<div class=\"range-meta\"><span class=\"num\">'+fromLow.toFixed(1)+'% from low</span><span class=\"num\">'+toHigh.toFixed(1)+'% to high</span></div>';document.getElementById('ov-range').innerHTML=html;}"
+ + "function renderShortCard(){var s=OVERVIEW_DATA.short_interest||{};var d=s.sharesShortDeltaPct;var dCls='';var dTxt='—';if(asNum(d)!==null){dCls=d>=0?'delta-pos':'delta-neg';dTxt=(d>=0?'+':'')+(d*100).toFixed(1)+'%';}var rows=[['Short % Float',asNum(s.shortPercentOfFloat)===null?'—':(s.shortPercentOfFloat*100).toFixed(2)+'%'],['Days to Cover',asNum(s.shortRatio)===null?'—':Number(s.shortRatio).toFixed(2)],['Shares Short',fmtInt(s.sharesShort)],['Vs Prior Month',dTxt,dCls]];var html='';rows.forEach(function(r){html+='<div class=\"short-cell\"><div class=\"lbl\">'+r[0]+'</div><div class=\"v num '+(r[2]||'')+'\">'+r[1]+'</div></div>';});document.getElementById('ov-short').innerHTML=html;}"
+ + "function renderCompButtons(){var root=document.getElementById('ov-comps');var arr=OVERVIEW_DATA.comparisons||[];var html='<span class=\"ctl-lbl\">Compare</span>';arr.forEach(function(c){var sym=c.symbol||'';var act=activeComparisons.indexOf(sym)>=0;html+='<button class=\"comp-btn '+(act?'active':'')+'\" onclick=\"toggleComparison(\\\''+sym+'\\\',this)\">'+esc(c.label||sym)+'</button>';});root.innerHTML=html;root.classList.toggle('hidden',activeMode!=='relative'||arr.length===0);}"
+ + "function setPeriod(period,btn){activePeriod=period;document.querySelectorAll('[id^=btn-p-]').forEach(function(b){b.classList.remove('active');});if(btn)btn.classList.add('active');refreshAll();}"
+ + "function setMode(mode,btn){activeMode=mode;document.querySelectorAll('[id^=btn-m-]').forEach(function(b){b.classList.remove('active');});if(btn)btn.classList.add('active');renderCompButtons();renderChart();renderReadout();}"
+ + "function toggleComparison(sym,btn){var i=activeComparisons.indexOf(sym);if(i>=0){activeComparisons.splice(i,1);}else{activeComparisons.push(sym);}if(!activeComparisons.length){activeComparisons=['^GSPC'];}renderCompButtons();renderChart();renderReadout();}"
+ + "function renderChart(){if(typeof Plotly==='undefined'){return;}var tkr=OVERVIEW_DATA.ticker;var base=getSeries(tkr);var traces=[];if(activeMode==='price'){var p=toTraceData(base);traces.push({x:p.x,y:p.y,type:'scatter',mode:'lines',name:tkr,line:{color:C_BRASS,width:2.2},fill:'tozeroy',fillcolor:withAlpha(C_BRASS,0.10)});}else{var p2=rebased(base);traces.push({x:p2.x,y:p2.y,type:'scatter',mode:'lines',name:tkr,line:{color:C_BRASS,width:2.4}});var idx=1;activeComparisons.forEach(function(sym){if(sym===tkr)return;var rr=rebased(getSeries(sym));if(!rr.x.length)return;var label=sym;var comps=OVERVIEW_DATA.comparisons||[];for(var i=0;i<comps.length;i+=1){if(comps[i].symbol===sym){label=comps[i].label||sym;break;}}traces.push({x:rr.x,y:rr.y,type:'scatter',mode:'lines',name:label,line:{color:REL_COLORS[idx%REL_COLORS.length],width:1.8}});idx+=1;});}var layout={height:320,margin:{l:52,r:14,t:14,b:34},paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',hovermode:'x unified',font:{family:'IBM Plex Mono',color:C_FG3},xaxis:{showgrid:false,zeroline:false,tickfont:{family:'IBM Plex Mono'}},yaxis:{showgrid:true,gridcolor:C_LINE,zeroline:(activeMode==='relative'),zerolinecolor:C_LINE,tickprefix:(activeMode==='price'?'$':''),ticksuffix:(activeMode==='relative'?'%':''),tickformat:(activeMode==='relative'?'.1f':',.2f')},legend:{orientation:'h',x:0,y:1.12,bgcolor:'rgba(0,0,0,0)',font:{family:'IBM Plex Mono',color:C_FG2,size:11}}};Plotly.react('ov-chart',traces,layout,{displayModeBar:false,responsive:true});}"
+ + "function renderReadout(){var rg=OVERVIEW_DATA.range_52w||{};var lo=asNum(rg.low),hi=asNum(rg.high),px=asNum(rg.price);var txt='';if(lo!==null&&hi!==null&&px!==null&&hi>lo&&px>0){var above=((px-lo)/lo)*100;var below=((hi-px)/px)*100;txt='Stock is '+above.toFixed(1)+'% above 52-week low and '+below.toFixed(1)+'% below 52-week high.';}if(activeMode==='relative'){var t=rebased(getSeries(OVERVIEW_DATA.ticker));var tLast=t.y.length?t.y[t.y.length-1]:null;var sp=rebased(getSeries('^GSPC'));var spLast=sp.y.length?sp.y[sp.y.length-1]:null;if(tLast!==null&&spLast!==null){txt='Relative over '+periodLabel(activePeriod)+': '+OVERVIEW_DATA.ticker+' '+(tLast>=0?'+':'')+tLast.toFixed(1)+'% vs S&P 500 '+(spLast>=0?'+':'')+spLast.toFixed(1)+'%.';}}if(!txt){txt='Data is limited for this ticker and selected period, but chart controls remain available.';}document.getElementById('ov-readout').textContent=txt;}"
+ + "function refreshAll(){renderSignals();renderKpis();renderRangeCard();renderShortCard();renderCompButtons();renderChart();renderReadout();}"
+ + "function bootOverview(){refreshAll();if(typeof Plotly==='undefined'){setTimeout(refreshAll,350);setTimeout(refreshAll,900);}}"
+ + "if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',bootOverview);}else{bootOverview();}"
+ + "</script>"
+ )
- hist = get_price_history(ticker, period=period)
- if hist.empty:
- st.warning("No price history available.")
- return
+ height = 1480
- 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,
+ doc = (
+ "<!doctype html><html><head><meta charset='utf-8'>"
+ + "<meta name='viewport' content='width=device-width, initial-scale=1'>"
+ + fonts_link
+ + plotly_cdn
+ + _ROOT
+ + _OV_CSS
+ + "</head><body>"
+ + '<div class="ov-wrap">'
+ + ctx_html
+ + '<div class="ov-body">'
+ + lede_html
+ + '<section class="ov-signals" id="ov-signals"></section>'
+ + '<section class="ov-kpis" id="ov-kpis"></section>'
+ + '<section class="ov-range-short">'
+ + '<div class="ov-card"><div class="hd">52-Week Positioning</div><div id="ov-range"></div></div>'
+ + '<div class="ov-card"><div class="hd">Short Interest</div><div class="short-grid" id="ov-short"></div></div>'
+ + "</section>"
+ + controls_html
+ + '<section class="ov-chart-card"><div id="ov-chart"></div></section>'
+ + '<section class="ov-readout" id="ov-readout"></section>'
+ + foot_html
+ + "</div></div>"
+ + js
+ + "</body></html>"
)
- st.plotly_chart(fig, width="stretch")
+
+ components.html(doc, height=height, scrolling=False)