"""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"}
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 ───────────────────────────────────────────────────────────────
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 "—")
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):
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 ─────────────────────────────────────────────────────────
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.")
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)