aboutsummaryrefslogtreecommitdiff
path: root/components/overview.py
diff options
context:
space:
mode:
Diffstat (limited to 'components/overview.py')
-rw-r--r--components/overview.py170
1 files changed, 163 insertions, 7 deletions
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.")