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")