1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
|
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(
"""
<style>
.top-card {
border: 1px solid #2a3142;
border-radius: 14px;
padding: 1rem 1.15rem;
margin-bottom: 0.8rem;
background: linear-gradient(145deg, #0d1320 0%, #121a2b 100%);
}
.top-label {
color: #8fa6cc;
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.top-value {
font-size: 2.2rem;
font-weight: 800;
margin-top: 0.15rem;
line-height: 1;
}
.top-sub {
color: #adbacf;
margin-top: 0.45rem;
font-size: 0.95rem;
}
.mega-crash {
border: 1px solid #7f1d1d;
border-radius: 14px;
background: linear-gradient(145deg, #2b0b0b 0%, #430f0f 100%);
color: #fee2e2;
padding: 1rem 1.15rem;
margin-bottom: 0.8rem;
animation: pulse 1.25s infinite;
}
.mega-crash strong {
color: #fecaca;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.55); }
70% { box-shadow: 0 0 0 12px rgba(220, 38, 38, 0); }
100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0); }
}
</style>
""",
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 = (
'<strong>MEGA CRASH:</strong> MAX_PAY has been exceeded in your saved history. '
f'First breach at <strong>{breach_time}</strong> '
f'with <strong>${breach_price:.5f}/hr</strong> '
f'(MAX_PAY = <strong>${max_pay:.5f}</strong>).'
)
else:
message = (
'<strong>MEGA CRASH (TEST):</strong> Simulated crash from sidebar button. '
f'MAX_PAY is <strong>${max_pay:.5f}</strong>.'
)
st.markdown(f'<div class="mega-crash">{message}</div>', 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"""
<div class="top-card">
<div class="top-label">Crash Multiplier</div>
<div class="top-value" style="color: {trend_color};">{multiplier:.2f}x {trend_icon}</div>
<div class="top-sub">
Current: <strong>${current_price:.5f}/hr</strong> •
On-demand: <strong>{f'${on_demand_price:.5f}/hr' if on_demand_price is not None else 'n/a'}</strong> •
{savings_text} •
MAX_PAY: <strong>${max_pay:.5f}</strong>
</div>
</div>
""",
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")
|