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'