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:
+11
@@ -0,0 +1,11 @@
|
||||
[project]
|
||||
name = "dashboard-stock-peers-snowflake"
|
||||
version = "1.0.0"
|
||||
description = "Stock peer analysis dashboard with Snowflake connection"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"altair>=5.5.0",
|
||||
"pandas>=2.2.3",
|
||||
"snowflake-connector-python>=3.3.0",
|
||||
"streamlit[snowflake]>=1.54.0",
|
||||
]
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
definition_version: 2
|
||||
entities:
|
||||
DASHBOARD_STOCK_PEERS_SNOWFLAKE:
|
||||
type: streamlit
|
||||
identifier:
|
||||
name: DASHBOARD_STOCK_PEERS_SNOWFLAKE
|
||||
database: <FROM_CONNECTION> # Use: snow connection list
|
||||
schema: <FROM_CONNECTION>
|
||||
query_warehouse: <FROM_CONNECTION>
|
||||
runtime_name: SYSTEM$ST_CONTAINER_RUNTIME_PY3_11
|
||||
# List available: SHOW EXTERNAL ACCESS INTEGRATIONS;
|
||||
# Common: PYPI_ACCESS_INTEGRATION (verify exists)
|
||||
external_access_integrations:
|
||||
- <YOUR_PYPI_INTEGRATION>
|
||||
main_file: streamlit_app.py
|
||||
artifacts:
|
||||
- streamlit_app.py
|
||||
- pyproject.toml
|
||||
+384
@@ -0,0 +1,384 @@
|
||||
"""
|
||||
Stock Peer Analysis Dashboard (Snowflake Edition)
|
||||
|
||||
A stock comparison dashboard demonstrating:
|
||||
- Snowflake connection via st.connection("snowflake")
|
||||
- Generating synthetic stock data in Snowflake
|
||||
- Normalized price comparison charts
|
||||
- Individual stock vs peer average analysis
|
||||
|
||||
This template uses synthetic stock data generated in Snowflake.
|
||||
Replace the synthetic query with your actual stock data table.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
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.
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Snowflake Connection
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def get_snowflake_connection():
|
||||
"""Get Snowflake connection via st.connection.
|
||||
|
||||
Displays an error and stops the app if the connection fails.
|
||||
"""
|
||||
try:
|
||||
return st.connection("snowflake")
|
||||
except Exception as e:
|
||||
st.error(f"Failed to connect to Snowflake: {e}")
|
||||
st.info(
|
||||
"Make sure you have configured your Snowflake connection in "
|
||||
"`.streamlit/secrets.toml` or via environment variables."
|
||||
)
|
||||
st.stop()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Constants and Configuration
|
||||
# =============================================================================
|
||||
|
||||
STOCKS = [
|
||||
"AAPL", "ABBV", "ACN", "ADBE", "ADP", "AMD", "AMGN", "AMT", "AMZN", "APD",
|
||||
"AVGO", "AXP", "BA", "BK", "BKNG", "BMY", "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",
|
||||
]
|
||||
|
||||
# Base prices for synthetic data (approximate real values for realism)
|
||||
STOCK_BASE_PRICES = {
|
||||
"AAPL": 175, "MSFT": 380, "GOOGL": 140, "AMZN": 180, "NVDA": 500,
|
||||
"META": 350, "TSLA": 250, "JPM": 170, "V": 280, "UNH": 520,
|
||||
"HD": 350, "PG": 160, "MA": 450, "COST": 580, "ABBV": 170,
|
||||
"MRK": 120, "AVGO": 900, "PEP": 180, "KO": 60, "TMO": 550,
|
||||
"ADBE": 550, "CRM": 280, "CSCO": 50, "ACN": 340, "NKE": 100,
|
||||
}
|
||||
|
||||
DEFAULT_STOCKS = ["AAPL", "MSFT", "GOOGL", "NVDA", "AMZN", "TSLA", "META"]
|
||||
|
||||
# Time horizon mapping
|
||||
HORIZON_MAP = {
|
||||
"1 Month": 30,
|
||||
"3 Months": 90,
|
||||
"6 Months": 180,
|
||||
"1 Year": 365,
|
||||
"2 Years": 730,
|
||||
}
|
||||
|
||||
|
||||
def stocks_to_str(stocks):
|
||||
return ",".join(stocks)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Data Loading
|
||||
# =============================================================================
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# PRODUCTION PATTERN: Use parameterized queries for real stock data
|
||||
# -----------------------------------------------------------------------------
|
||||
# For production use with actual stock tables, use parameterized queries:
|
||||
#
|
||||
# STOCK_QUERY = """
|
||||
# SELECT trade_date AS date, ticker, close_price
|
||||
# FROM stock_prices
|
||||
# WHERE ticker = ANY(:tickers)
|
||||
# AND trade_date >= DATEADD(day, -:days, CURRENT_DATE())
|
||||
# ORDER BY trade_date, ticker
|
||||
# """
|
||||
#
|
||||
# def load_stock_data(tickers: list[str], days: int) -> pd.DataFrame:
|
||||
# conn = get_snowflake_connection()
|
||||
# df = conn.query(
|
||||
# STOCK_QUERY,
|
||||
# params={"tickers": tickers, "days": days}
|
||||
# )
|
||||
# return df
|
||||
#
|
||||
# The synthetic data generation below uses f-strings for the VALUES clause
|
||||
# which cannot be parameterized. This is acceptable for demo/synthetic data
|
||||
# but should NOT be used with user input in production.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def generate_stock_data_query(tickers: list[str], days: int) -> str:
|
||||
"""Generate SQL query that creates synthetic stock price data.
|
||||
|
||||
NOTE: This uses f-strings for VALUES clause construction which is acceptable
|
||||
for synthetic data generation with controlled inputs. For production apps
|
||||
with real tables, always use parameterized queries as shown above.
|
||||
"""
|
||||
# Build ticker values and base prices (controlled data, not user input)
|
||||
ticker_values = []
|
||||
for ticker in tickers:
|
||||
base_price = STOCK_BASE_PRICES.get(ticker, 100 + hash(ticker) % 400)
|
||||
growth_rate = 0.0003 + (hash(ticker) % 10) * 0.00005
|
||||
volatility = 0.02 + (hash(ticker) % 5) * 0.005
|
||||
ticker_values.append(f"('{ticker}', {base_price}, {growth_rate}, {volatility})")
|
||||
|
||||
tickers_cte = ", ".join(ticker_values)
|
||||
|
||||
return f"""
|
||||
WITH tickers AS (
|
||||
SELECT column1 AS ticker, column2 AS base_price, column3 AS growth_rate, column4 AS volatility
|
||||
FROM VALUES {tickers_cte}
|
||||
),
|
||||
date_series AS (
|
||||
SELECT DATEADD(day, -seq4(), CURRENT_DATE() - 1) AS trade_date
|
||||
FROM TABLE(GENERATOR(ROWCOUNT => {days}))
|
||||
),
|
||||
raw_prices AS (
|
||||
SELECT
|
||||
d.trade_date,
|
||||
t.ticker,
|
||||
t.base_price * POWER(1 + t.growth_rate, DATEDIFF(day, DATEADD(day, -{days}, CURRENT_DATE()), d.trade_date))
|
||||
* (1 + (RANDOM() / 10000000000000000000.0 - 0.5) * t.volatility * 2) AS close_price
|
||||
FROM date_series d
|
||||
CROSS JOIN tickers t
|
||||
WHERE DAYOFWEEK(d.trade_date) NOT IN (0, 6) -- Exclude weekends
|
||||
)
|
||||
SELECT
|
||||
trade_date AS date,
|
||||
ticker,
|
||||
ROUND(close_price, 2) AS close_price
|
||||
FROM raw_prices
|
||||
ORDER BY trade_date, ticker
|
||||
"""
|
||||
|
||||
|
||||
@st.cache_data(ttl=3600, show_spinner="Loading stock data from Snowflake...")
|
||||
def load_stock_data(tickers: list[str], days: int) -> pd.DataFrame:
|
||||
"""Load stock price data from Snowflake."""
|
||||
conn = get_snowflake_connection()
|
||||
query = generate_stock_data_query(tickers, days)
|
||||
df = conn.query(query)
|
||||
df.columns = df.columns.str.lower()
|
||||
|
||||
# Pivot to get tickers as columns
|
||||
pivoted = df.pivot(index="date", columns="ticker", values="close_price")
|
||||
pivoted.index = pd.to_datetime(pivoted.index)
|
||||
return pivoted
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Session State and Query Params
|
||||
# =============================================================================
|
||||
|
||||
if "tickers_input" not in st.session_state:
|
||||
st.session_state.tickers_input = st.query_params.get(
|
||||
"stocks", stocks_to_str(DEFAULT_STOCKS)
|
||||
).split(",")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Page Layout
|
||||
# =============================================================================
|
||||
|
||||
# Check Snowflake connection
|
||||
get_snowflake_connection()
|
||||
|
||||
cols = st.columns([1, 3])
|
||||
|
||||
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 = 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:
|
||||
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"
|
||||
)
|
||||
|
||||
# Load the data from Snowflake
|
||||
try:
|
||||
data = load_stock_data(tickers, HORIZON_MAP[horizon])
|
||||
except Exception as e:
|
||||
st.error(f"Error loading stock data: {e}")
|
||||
st.stop()
|
||||
|
||||
# Check for missing data
|
||||
missing_tickers = [t for t in tickers if t not in data.columns]
|
||||
if missing_tickers:
|
||||
st.warning(f"No data available for: {', '.join(missing_tickers)}")
|
||||
# Filter to available tickers
|
||||
tickers = [t for t in tickers if t in data.columns]
|
||||
if not tickers:
|
||||
st.stop()
|
||||
|
||||
# Normalize prices (start at 1)
|
||||
normalized = data[tickers].div(data[tickers].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:
|
||||
metric_cols = st.columns(2)
|
||||
metric_cols[0].metric(
|
||||
"Best stock",
|
||||
max_norm_value[1],
|
||||
delta=f"{round((max_norm_value[0] - 1) * 100)}%",
|
||||
width="content",
|
||||
)
|
||||
metric_cols[1].metric(
|
||||
"Worst stock",
|
||||
min_norm_value[1],
|
||||
delta=f"{round((min_norm_value[0] - 1) * 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", title="Date"),
|
||||
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
|
||||
chart_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 = chart_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 = chart_cols[(i * 2 + 1) % NUM_COLS].container(border=True)
|
||||
cell.write("")
|
||||
cell.altair_chart(chart)
|
||||
|
||||
""
|
||||
""
|
||||
|
||||
"""
|
||||
## Raw data
|
||||
"""
|
||||
|
||||
st.caption(":material/cloud: Data loaded from Snowflake (synthetic)")
|
||||
data[tickers]
|
||||
Reference in New Issue
Block a user