import os from datetime import datetime, timezone import pandas as pd import plotly.graph_objects as go import streamlit as st from dotenv import load_dotenv from streamlit_autorefresh import st_autorefresh import aws import persistance load_dotenv() st.set_page_config(page_title="Spot Crash Board", page_icon="📉", layout="wide") st.markdown( """ """, unsafe_allow_html=True, ) max_pay_raw = os.getenv("MAX_PAY") st.caption(f"i will not pay more than ${max_pay_raw} per hour") if "simulate_mega_crash" not in st.session_state: st.session_state.simulate_mega_crash = False with st.sidebar: st.header("Display") poll_seconds = st.slider("Poll interval (seconds)", min_value=2, max_value=30, value=5) history_limit = st.slider("Points on chart", min_value=100, max_value=5000, value=1000) chart_expanded = st.toggle("Chart expanded", value=True) show_markers = st.toggle("Show point markers", value=False) st.divider() st.caption("Test mode") if st.button("Trigger MEGA CRASH", type="primary", use_container_width=True): st.session_state.simulate_mega_crash = True if st.button("Clear test crash", use_container_width=True): st.session_state.simulate_mega_crash = False if not max_pay_raw: st.error("Missing MAX_PAY environment variable. Add MAX_PAY to your .env file.") st.stop() try: max_pay = float(max_pay_raw) except ValueError: st.error(f"Invalid MAX_PAY value: {max_pay_raw}. Use a numeric value like 0.03") st.stop() persistance.init_storage() st_autorefresh(interval=poll_seconds * 1000, key="spot-pricing-poller") @st.cache_data(ttl=1, show_spinner=False) def fetch_spot_payload(): return aws.get_asg_spot_pricing() payload = fetch_spot_payload() if payload.get("error"): st.error(payload["error"]) st.stop() latest = payload.get("latest") or {} instance = payload.get("instance") or {} history = payload.get("history") or [] if not history: if latest.get("spotPrice") is None: st.warning("No spot price returned from API right now.") st.stop() history = [latest] imported_points = 0 for item in history: timestamp = item.get("timestamp") spot_price = item.get("spotPrice") if not timestamp or spot_price is None: continue point = { "polled_at": str(timestamp), "source_timestamp": str(timestamp), "asg_name": payload.get("asg_name"), "instance_id": instance.get("id"), "instance_type": instance.get("type"), "az": item.get("availabilityZone") or instance.get("az"), "spot_price": float(spot_price), } if persistance.save_spot_datapoint(point): imported_points += 1 rows = persistance.load_spot_datapoints(limit=history_limit) if not rows: st.info("Waiting for first saved data point...") st.stop() df = pd.DataFrame(rows) df["polled_at"] = pd.to_datetime(df["polled_at"], errors="coerce", utc=True) df["source_timestamp"] = pd.to_datetime(df["source_timestamp"], errors="coerce", utc=True) df = df.dropna(subset=["polled_at", "spot_price"]).sort_values("polled_at") if df.empty: st.info("No valid points available yet.") st.stop() current_price = float(df.iloc[-1]["spot_price"]) prev_price = float(df.iloc[-2]["spot_price"]) if len(df) > 1 else current_price visible_low = float(df["spot_price"].min()) visible_high = float(df["spot_price"].max()) price_delta = current_price - prev_price multiplier = current_price / visible_low if visible_low > 0 else 1.0 on_demand_price = latest.get("onDemandPrice") if on_demand_price is None: for item in reversed(history): if item.get("onDemandPrice") is not None: on_demand_price = item.get("onDemandPrice") break if on_demand_price is not None: on_demand_price = float(on_demand_price) savings_per_hour = on_demand_price - current_price savings_percent = (savings_per_hour / on_demand_price) * 100 if on_demand_price > 0 else None else: savings_per_hour = None savings_percent = None peak_all_time = persistance.get_peak_spot_price() first_breach = persistance.get_first_breach(max_pay) historical_mega_crash = first_breach is not None mega_crash = historical_mega_crash or st.session_state.simulate_mega_crash if mega_crash: if historical_mega_crash: breach_time = first_breach.get("polled_at", "unknown") breach_price = float(first_breach.get("spot_price", 0.0)) message = ( 'MEGA CRASH: MAX_PAY has been exceeded in your saved history. ' f'First breach at {breach_time} ' f'with ${breach_price:.5f}/hr ' f'(MAX_PAY = ${max_pay:.5f}).' ) else: message = ( 'MEGA CRASH (TEST): Simulated crash from sidebar button. ' f'MAX_PAY is ${max_pay:.5f}.' ) st.markdown(f'
{message}
', unsafe_allow_html=True) trend_icon = "â–²" if price_delta >= 0 else "â–¼" trend_color = "#f59e0b" if price_delta >= 0 else "#22c55e" if mega_crash: trend_color = "#ef4444" if savings_percent is None or savings_per_hour is None: savings_text = "Savings vs on-demand: n/a" elif savings_per_hour >= 0: savings_text = f"Savings vs on-demand: {savings_percent:.2f}% (${savings_per_hour:.5f}/hr)" else: savings_text = f"Spot above on-demand by {abs(savings_percent):.2f}% (${abs(savings_per_hour):.5f}/hr)" st.markdown( f"""
Crash Multiplier
{multiplier:.2f}x {trend_icon}
Current: ${current_price:.5f}/hr   •   On-demand: {f'${on_demand_price:.5f}/hr' if on_demand_price is not None else 'n/a'}   •   {savings_text}   •   MAX_PAY: ${max_pay:.5f}
""", unsafe_allow_html=True, ) c1, c2, c3, c4, c5, c6, c7 = st.columns(7) c1.metric("Instance", instance.get("type", "unknown")) c2.metric("AZ", instance.get("az", "unknown")) c3.metric("Spot ($/hr)", f"{current_price:.5f}", f"{price_delta:+.5f}") c4.metric("On-demand ($/hr)", f"{on_demand_price:.5f}" if on_demand_price is not None else "n/a") if savings_percent is not None and savings_per_hour is not None: c5.metric("Savings (%)", f"{savings_percent:.2f}%", f"${savings_per_hour:+.5f}/hr") else: c5.metric("Savings (%)", "n/a") c6.metric("24h estimate ($)", f"{current_price * 24:.2f}") c7.metric("All-time peak ($/hr)", f"{(peak_all_time or current_price):.5f}") with st.expander("Ticker chart", expanded=chart_expanded): line_color = "#ef4444" if mega_crash else ("#f59e0b" if price_delta >= 0 else "#22c55e") marker_mode = "lines+markers" if show_markers else "lines" fig = go.Figure() fig.add_trace( go.Scatter( x=df["polled_at"], y=df["spot_price"], mode=marker_mode, line={"width": 3, "color": line_color, "shape": "spline"}, marker={"size": 5, "color": line_color}, fill="tozeroy", fillcolor="rgba(245, 158, 11, 0.13)" if not mega_crash else "rgba(239, 68, 68, 0.18)", name="Spot price", ) ) fig.add_hline( y=max_pay, line_dash="dash", line_color="#ef4444", annotation_text=f"MAX_PAY ${max_pay:.5f}", annotation_position="top right", annotation_font_color="#ef4444", ) fig.update_layout( template="plotly_dark", margin={"l": 10, "r": 10, "t": 10, "b": 10}, hovermode="x unified", height=430, xaxis={ "title": "Time (UTC)", "tickformat": "%H:%M:%S", "hoverformat": "%Y-%m-%d %H:%M:%S", "showgrid": False, "rangeslider": {"visible": False}, }, yaxis={ "title": "Spot price ($/hr)", "tickprefix": "$", "tickformat": ".5f", "gridcolor": "#2c3448", }, showlegend=False, ) st.plotly_chart(fig, width="stretch") status = f"Imported {imported_points} new historical points" if imported_points else "No new historical points" st.caption(f"{status} • Last refresh {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") with st.expander("Recent datapoints", expanded=False): st.dataframe(df.tail(150).sort_values("polled_at", ascending=False), width="stretch")