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.
This commit is contained in:
+342
@@ -0,0 +1,342 @@
|
||||
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
|
||||
Reference in New Issue
Block a user