Files
shelly-ui/.agents/skills/developing-with-streamlit/templates/apps/dashboard-stock-peers/streamlit_app.py
T
jonas 71803418e5 Initial commit: Shelly Manager with Textual CLI, Streamlit UI, and comprehensive .gitignore
Shelly device management app with mDNS/subnet discovery, inventory,
configuration, and mass operations for Gen1/Gen2+ devices.

Includes .gitignore excluding runtime data (device DB, user config),
AI conversation history, build artifacts, and common Python/OS patterns.
2026-03-23 21:51:59 +01:00

343 lines
7.0 KiB
Python

import streamlit as st
import yfinance as yf
import pandas as pd
import altair as alt
st.set_page_config(
page_title="Stock peer analysis dashboard",
page_icon=":chart_with_upwards_trend:",
layout="wide",
)
"""
# :material/query_stats: Stock peer analysis
Easily compare stocks against others in their peer group.
"""
"" # Add some space.
cols = st.columns([1, 3])
# Will declare right cell later to avoid showing it when no data.
STOCKS = [
"AAPL",
"ABBV",
"ACN",
"ADBE",
"ADP",
"AMD",
"AMGN",
"AMT",
"AMZN",
"APD",
"AVGO",
"AXP",
"BA",
"BK",
"BKNG",
"BMY",
"BRK.B",
"BSX",
"C",
"CAT",
"CI",
"CL",
"CMCSA",
"COST",
"CRM",
"CSCO",
"CVX",
"DE",
"DHR",
"DIS",
"DUK",
"ELV",
"EOG",
"EQR",
"FDX",
"GD",
"GE",
"GILD",
"GOOG",
"GOOGL",
"HD",
"HON",
"HUM",
"IBM",
"ICE",
"INTC",
"ISRG",
"JNJ",
"JPM",
"KO",
"LIN",
"LLY",
"LMT",
"LOW",
"MA",
"MCD",
"MDLZ",
"META",
"MMC",
"MO",
"MRK",
"MSFT",
"NEE",
"NFLX",
"NKE",
"NOW",
"NVDA",
"ORCL",
"PEP",
"PFE",
"PG",
"PLD",
"PM",
"PSA",
"REGN",
"RTX",
"SBUX",
"SCHW",
"SLB",
"SO",
"SPGI",
"T",
"TJX",
"TMO",
"TSLA",
"TXN",
"UNH",
"UNP",
"UPS",
"V",
"VZ",
"WFC",
"WM",
"WMT",
"XOM",
]
DEFAULT_STOCKS = ["AAPL", "MSFT", "GOOGL", "NVDA", "AMZN", "TSLA", "META"]
def stocks_to_str(stocks):
return ",".join(stocks)
if "tickers_input" not in st.session_state:
st.session_state.tickers_input = st.query_params.get(
"stocks", stocks_to_str(DEFAULT_STOCKS)
).split(",")
# Callback to update query param when input changes
def update_query_param():
if st.session_state.tickers_input:
st.query_params["stocks"] = stocks_to_str(st.session_state.tickers_input)
else:
st.query_params.pop("stocks", None)
top_left_cell = cols[0].container(
border=True, height="stretch", vertical_alignment="center"
)
with top_left_cell:
# Selectbox for stock tickers
tickers = st.multiselect(
"Stock tickers",
options=sorted(set(STOCKS) | set(st.session_state.tickers_input)),
default=st.session_state.tickers_input,
placeholder="Choose stocks to compare. Example: NVDA",
accept_new_options=True,
)
# Time horizon selector
horizon_map = {
"1 Month": "1mo",
"3 Months": "3mo",
"6 Months": "6mo",
"1 Year": "1y",
"5 Years": "5y",
"10 Years": "10y",
"20 Years": "20y",
}
with top_left_cell:
# Buttons for picking time horizon
horizon = st.pills(
"Time horizon",
options=list(horizon_map.keys()),
default="6 Months",
)
tickers = [t.upper() for t in tickers]
# Update query param when text input changes
if tickers:
st.query_params["stocks"] = stocks_to_str(tickers)
else:
# Clear the param if input is empty
st.query_params.pop("stocks", None)
if not tickers:
top_left_cell.info("Pick some stocks to compare", icon=":material/info:")
st.stop()
right_cell = cols[1].container(
border=True, height="stretch", vertical_alignment="center"
)
@st.cache_resource(show_spinner=False, ttl="6h")
def load_data(tickers, period):
tickers_obj = yf.Tickers(tickers)
data = tickers_obj.history(period=period)
if data is None:
raise RuntimeError("YFinance returned no data.")
return data["Close"]
# Load the data
try:
data = load_data(tickers, horizon_map[horizon])
except yf.exceptions.YFRateLimitError as e:
st.warning("YFinance is rate-limiting us :(\nTry again later.")
load_data.clear() # Remove the bad cache entry.
st.stop()
empty_columns = data.columns[data.isna().all()].tolist()
if empty_columns:
st.error(f"Error loading data for the tickers: {', '.join(empty_columns)}.")
st.stop()
# Normalize prices (start at 1)
normalized = data.div(data.iloc[0])
latest_norm_values = {normalized[ticker].iat[-1]: ticker for ticker in tickers}
max_norm_value = max(latest_norm_values.items())
min_norm_value = min(latest_norm_values.items())
bottom_left_cell = cols[0].container(
border=True, height="stretch", vertical_alignment="center"
)
with bottom_left_cell:
cols = st.columns(2)
cols[0].metric(
"Best stock",
max_norm_value[1],
delta=f"{round(max_norm_value[0] * 100)}%",
width="content",
)
cols[1].metric(
"Worst stock",
min_norm_value[1],
delta=f"{round(min_norm_value[0] * 100)}%",
width="content",
)
# Plot normalized prices
with right_cell:
st.altair_chart(
alt.Chart(
normalized.reset_index().melt(
id_vars=["Date"], var_name="Stock", value_name="Normalized price"
)
)
.mark_line()
.encode(
alt.X("Date:T"),
alt.Y("Normalized price:Q").scale(zero=False),
alt.Color("Stock:N"),
)
.properties(height=400)
)
""
""
# Plot individual stock vs peer average
"""
## Individual stocks vs peer average
For the analysis below, the "peer average" when analyzing stock X always
excludes X itself.
"""
if len(tickers) <= 1:
st.warning("Pick 2 or more tickers to compare them")
st.stop()
NUM_COLS = 4
cols = st.columns(NUM_COLS)
for i, ticker in enumerate(tickers):
# Calculate peer average (excluding current stock)
peers = normalized.drop(columns=[ticker])
peer_avg = peers.mean(axis=1)
# Create DataFrame with peer average.
plot_data = pd.DataFrame(
{
"Date": normalized.index,
ticker: normalized[ticker],
"Peer average": peer_avg,
}
).melt(id_vars=["Date"], var_name="Series", value_name="Price")
chart = (
alt.Chart(plot_data)
.mark_line()
.encode(
alt.X("Date:T"),
alt.Y("Price:Q").scale(zero=False),
alt.Color(
"Series:N",
scale=alt.Scale(domain=[ticker, "Peer average"], range=["red", "gray"]),
legend=alt.Legend(orient="bottom"),
),
alt.Tooltip(["Date", "Series", "Price"]),
)
.properties(title=f"{ticker} vs peer average", height=300)
)
cell = cols[(i * 2) % NUM_COLS].container(border=True)
cell.write("")
cell.altair_chart(chart)
# Create Delta chart
plot_data = pd.DataFrame(
{
"Date": normalized.index,
"Delta": normalized[ticker] - peer_avg,
}
)
chart = (
alt.Chart(plot_data)
.mark_area()
.encode(
alt.X("Date:T"),
alt.Y("Delta:Q").scale(zero=False),
)
.properties(title=f"{ticker} minus peer average", height=300)
)
cell = cols[(i * 2 + 1) % NUM_COLS].container(border=True)
cell.write("")
cell.altair_chart(chart)
""
""
"""
## Raw data
"""
data