From 96b27f1d00ae8110273de973053c3d6bfc4f3662 Mon Sep 17 00:00:00 2001 From: Tyler Date: Tue, 31 Mar 2026 23:01:05 -0700 Subject: Add relative performance chart and refine top movers --- components/overview.py | 170 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 163 insertions(+), 7 deletions(-) (limited to 'components/overview.py') diff --git a/components/overview.py b/components/overview.py index 53b8554..1bb65c2 100644 --- a/components/overview.py +++ b/components/overview.py @@ -6,6 +6,41 @@ 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 ─────────────────────────────────────────────────────────────── @@ -186,6 +221,112 @@ def _render_short_interest(info: dict) -> None: 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): @@ -248,15 +389,30 @@ def render_overview(ticker: str): st.divider() # ── Price chart ───────────────────────────────────────────────────────── - period_label = st.radio( - "Period", - options=list(PERIODS.keys()), - index=3, - horizontal=True, - label_visibility="collapsed", - ) + 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.") -- cgit v1.3-2-g0d8e