SPY_500 / app.py
mystycrym's picture
Rename main.py to app.py
d2939e5 verified
Raw History Blame Contribute Delete
7.44 kB
import datetime as dt
from datetime import timezone
import pandas as pd
import streamlit as st
import yfinance as yf
import plotly.graph_objects as go
st.set_page_config(page_title="SPY Dashboard", layout="wide")
TICKER = "SPY"
# ---------------------- Helpers ---------------------- #
@st.cache_data(ttl=60*60) # cache 1 hour to be gentle on API
def fetch_prices(timeframe: str) -> pd.DataFrame:
"""Fetch OHLCV data for SPY for the selected timeframe."""
kwargs = dict(auto_adjust=True, progress=False)
if timeframe == "1D":
return yf.download(TICKER, period="1d", interval="1m", **kwargs)
elif timeframe == "1W":
return yf.download(TICKER, period="7d", interval="15m", **kwargs)
elif timeframe == "1M":
return yf.download(TICKER, period="1mo", interval="1h", **kwargs)
elif timeframe == "YTD":
start = dt.datetime(dt.datetime.now().year, 1, 1, tzinfo=timezone.utc)
return yf.download(TICKER, start=start, interval="1d", **kwargs)
elif timeframe == "1Y":
return yf.download(TICKER, period="1y", interval="1d", **kwargs)
elif timeframe == "5Y":
return yf.download(TICKER, period="5y", interval="1wk", **kwargs)
else: # "ALL"
# Weekly gives a clean long-term view without too many points
return yf.download(TICKER, period="max", interval="1wk", **kwargs)
@st.cache_data(ttl=60*10)
def fast_info():
t = yf.Ticker(TICKER)
return getattr(t, "fast_info", {})
def is_intraday(df: pd.DataFrame) -> bool:
if df.empty:
return False
last_date = df.index[-1].date()
return (df.index.date == last_date).sum() > 1
def last_day_stats(df: pd.DataFrame) -> dict:
"""Return open/high/low/close/volume for the latest trading day represented in df."""
if df.empty:
return {}
# pick the latest calendar day present in the data
last_dt = pd.to_datetime(df.index[-1])
mask = (pd.to_datetime(df.index).date == last_dt.date())
day = df.loc[mask]
if day.empty:
# fallback to last row
row = df.iloc[-1]
return dict(
open=float(row["Open"]),
high=float(row["High"]),
low=float(row["Low"]),
close=float(row["Close"]),
volume=int(row.get("Volume", 0)),
)
# intraday volume is sum across the day; daily interval has one row already
vol = int(day["Volume"].sum() if is_intraday(df) else day["Volume"].iloc[-1])
return dict(
open=float(day["Open"].iloc[0]),
high=float(day["High"].max()),
low=float(day["Low"].min()),
close=float(day["Close"].iloc[-1]),
volume=vol,
)
@st.cache_data(ttl=60*30)
def year_range_52w() -> tuple[float, float]:
df = yf.download(TICKER, period="1y", interval="1d", auto_adjust=True, progress=False)
if df.empty:
return (None, None)
return (float(df["Low"].min()), float(df["High"].max()))
def human_int(n):
# 1,234,567 -> 1.23M style
try:
n = float(n)
except Exception:
return "-"
for unit in ["", "K", "M", "B", "T"]:
if abs(n) < 1000:
return f"{n:.2f}{unit}".rstrip("0").rstrip(".") + unit
n /= 1000
return f"{n:.2f}P"
# ---------------------- UI ---------------------- #
st.title("📈 SPY (S&P 500 ETF)")
col_l, col_r = st.columns([3, 1])
with col_l:
timeframe = st.radio(
"Timeframe",
["1D", "1W", "1M", "YTD", "1Y", "5Y", "ALL"],
horizontal=True,
index=3, # default YTD
)
with col_r:
show_ma = st.toggle("Show Moving Averages (20/50)", value=False)
log_scale = st.toggle("Log Scale", value=False)
# Data
prices = fetch_prices(timeframe)
fi = fast_info()
if prices.empty:
st.warning("No data available right now. Market may be closed or the data provider rate-limited. Try a different timeframe.")
st.stop()
stats = last_day_stats(prices)
prev_close = None
if len(prices) > 1:
if is_intraday(prices):
# previous trading day close
prev_date = pd.to_datetime(prices.index[-1]).date()
# take rows from prior calendar day
prior = prices.loc[prices.index.date < prev_date]
if not prior.empty:
prev_close = float(prior["Close"].iloc[-1])
else:
prev_close = float(prices["Close"].iloc[-2])
# Chart
fig = go.Figure()
fig.add_trace(
go.Candlestick(
x=prices.index,
open=prices["Open"],
high=prices["High"],
low=prices["Low"],
close=prices["Close"],
name="Price",
increasing_line_color="#26a69a",
decreasing_line_color="#ef5350",
)
)
if show_ma and len(prices) >= 50:
ma20 = prices["Close"].rolling(20).mean()
ma50 = prices["Close"].rolling(50).mean()
fig.add_trace(go.Scatter(x=prices.index, y=ma20, name="MA 20", mode="lines"))
fig.add_trace(go.Scatter(x=prices.index, y=ma50, name="MA 50", mode="lines"))
fig.update_layout(
margin=dict(l=10, r=10, t=10, b=10),
height=520,
yaxis_type="log" if log_scale else "linear",
xaxis_rangeslider_visible=False,
)
st.plotly_chart(fig, use_container_width=True)
# Top metrics row
cur = stats.get("close")
chg = (cur - prev_close) if (cur is not None and prev_close) else None
pct = (chg / prev_close * 100) if (chg is not None and prev_close) else None
m1, m2, m3, m4 = st.columns(4)
m1.metric("Last Price", f"${cur:,.2f}" if cur else "-", f"{(chg):+,.2f}" if chg else None)
m2.metric("Change %", f"{(pct):+.2f}%" if pct else "-")
day_range = f"${stats.get('low', 0):,.2f} – ${stats.get('high', 0):,.2f}" if stats else "-"
m3.metric("Day's Range", day_range)
yr_low, yr_high = year_range_52w()
yr_range = f"${yr_low:,.2f} – ${yr_high:,.2f}" if yr_low and yr_high else "-"
m4.metric("52-Week Range", yr_range)
st.write("---")
# Details table (Robinhood-style)
# Pull additional stats from fast_info, with safe fallbacks
open_p = stats.get("open")
high_p = stats.get("high")
low_p = stats.get("low")
vol = stats.get("volume")
avg10d = getattr(fi, "ten_day_average_volume", None) or fi.get("ten_day_average_volume")
avg3m = getattr(fi, "three_month_average_volume", None) or fi.get("three_month_average_volume")
market_cap = getattr(fi, "market_cap", None) or fi.get("market_cap")
prev_close_fi = getattr(fi, "previous_close", None) or fi.get("previous_close")
details = pd.DataFrame(
{
"Field": [
"Previous Close",
"Open",
"Day’s High",
"Day’s Low",
"Volume (Today)",
"Avg Volume (10D)",
"Avg Volume (3M)",
"Market Cap",
"52-Week High",
"52-Week Low",
],
"Value": [
f"${(prev_close or prev_close_fi):,.2f}" if (prev_close or prev_close_fi) else "-",
f"${open_p:,.2f}" if open_p else "-",
f"${high_p:,.2f}" if high_p else "-",
f"${low_p:,.2f}" if low_p else "-",
human_int(vol) if vol else "-",
human_int(avg10d) if avg10d else "-",
human_int(avg3m) if avg3m else "-",
human_int(market_cap) if market_cap else "-",
f"${yr_high:,.2f}" if yr_high else "-",
f"${yr_low:,.2f}" if yr_low else "-",
],
}
)
st.subheader("Details")
st.dataframe(
details,
use_container_width=True,
hide_index=True,
)
st.caption("Data source: Yahoo Finance via yfinance")