Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,397 +1,28 @@
|
|
1 |
import streamlit as st
|
2 |
-
import yfinance as yf
|
3 |
import streamlit.components.v1 as components
|
4 |
-
|
5 |
-
# Set the page layout
|
6 |
-
st.set_page_config(layout="wide")
|
7 |
-
|
8 |
-
import matplotlib.pyplot as plt
|
9 |
-
import numpy as np
|
10 |
-
import base64
|
11 |
import pandas as pd
|
12 |
-
import
|
|
|
13 |
from keras.models import load_model
|
14 |
from sklearn.preprocessing import MinMaxScaler
|
|
|
|
|
15 |
|
|
|
|
|
16 |
|
17 |
-
|
18 |
-
|
19 |
-
# Initialize state
|
20 |
-
if "show_modal" not in st.session_state:
|
21 |
-
st.session_state.show_modal = False
|
22 |
-
if "show_overlay" not in st.session_state:
|
23 |
-
st.session_state.show_overlay = False
|
24 |
-
if "model" not in st.session_state:
|
25 |
-
st.session_state.model = "best_bilstm_model.h5"
|
26 |
-
|
27 |
-
|
28 |
-
# Loading model
|
29 |
-
@st.cache_resource
|
30 |
-
def load_lstm_model(path):
|
31 |
-
return load_model(path)
|
32 |
-
|
33 |
-
|
34 |
-
@st.cache_resource
|
35 |
-
def load_data():
|
36 |
-
data = yf.download("AMZN", period="4y", multi_level_index=False)
|
37 |
-
data.reset_index(inplace=True)
|
38 |
-
return data
|
39 |
-
|
40 |
-
|
41 |
-
#################################################################################################
|
42 |
-
|
43 |
-
|
44 |
-
def predict_future_prices(
|
45 |
-
df: pd.DataFrame, n_future_days: int, model_path: str = st.session_state.model
|
46 |
-
) -> tuple[plt.Figure, pd.DataFrame]:
|
47 |
-
# Ensure DataFrame is sorted and clean
|
48 |
-
df = df.sort_values("Date").dropna(subset=["Close"])
|
49 |
-
df = df.reset_index(drop=True)
|
50 |
-
|
51 |
-
# Scale data
|
52 |
-
scaler = MinMaxScaler()
|
53 |
-
prices = df["Close"].values.reshape(-1, 1)
|
54 |
-
scaled_prices = scaler.fit_transform(prices)
|
55 |
-
|
56 |
-
# Load model and get timesteps
|
57 |
-
model = load_lstm_model(model_path)
|
58 |
-
n_steps = model.input_shape[1]
|
59 |
-
|
60 |
-
# --- Calculate validation error (historical residuals) ---
|
61 |
-
X_hist, y_hist = [], []
|
62 |
-
for i in range(n_steps, len(scaled_prices)):
|
63 |
-
X_hist.append(scaled_prices[i - n_steps : i])
|
64 |
-
y_hist.append(scaled_prices[i])
|
65 |
-
X_hist = np.array(X_hist)
|
66 |
-
y_hist = np.array(y_hist)
|
67 |
-
|
68 |
-
# Predict historical values
|
69 |
-
hist_predictions = model.predict(X_hist, verbose=0)
|
70 |
-
|
71 |
-
# Calculate residuals (error)
|
72 |
-
hist_prices_rescaled = scaler.inverse_transform(y_hist.reshape(-1, 1)).flatten()
|
73 |
-
hist_preds_rescaled = scaler.inverse_transform(
|
74 |
-
hist_predictions.reshape(-1, 1)
|
75 |
-
).flatten()
|
76 |
-
residuals = hist_prices_rescaled - hist_preds_rescaled
|
77 |
-
error_std = np.std(residuals) # Key metric for confidence interval
|
78 |
-
|
79 |
-
# --- Predict future values ---
|
80 |
-
last_sequence = scaled_prices[-n_steps:]
|
81 |
-
predicted = []
|
82 |
-
current_sequence = last_sequence.copy()
|
83 |
-
|
84 |
-
for _ in range(n_future_days):
|
85 |
-
pred = model.predict(current_sequence.reshape(1, n_steps, 1), verbose=0)
|
86 |
-
predicted.append(pred[0, 0])
|
87 |
-
current_sequence = np.append(current_sequence[1:], [[pred[0, 0]]], axis=0)
|
88 |
-
|
89 |
-
# Rescale predictions
|
90 |
-
predicted_prices = scaler.inverse_transform(
|
91 |
-
np.array(predicted).reshape(-1, 1)
|
92 |
-
).flatten()
|
93 |
-
last_date = df["Date"].max()
|
94 |
-
today = pd.Timestamp.today().normalize()
|
95 |
-
|
96 |
-
if last_date >= today:
|
97 |
-
start_date = last_date + pd.Timedelta(days=1)
|
98 |
-
else:
|
99 |
-
start_date = today # fallback to today if data is old
|
100 |
-
|
101 |
-
future_dates = pd.date_range(start=start_date, periods=n_future_days)
|
102 |
-
|
103 |
-
prediction_df = pd.DataFrame(
|
104 |
-
{"Date": future_dates, "Predicted Price": predicted_prices}
|
105 |
-
)
|
106 |
-
|
107 |
-
# --- Plotting with confidence interval ---
|
108 |
-
plt.rcParams["font.family"] = "Times New Roman "
|
109 |
-
|
110 |
-
fig, ax = plt.subplots(figsize=(10, 6), facecolor="none")
|
111 |
-
ax.patch.set_alpha(0)
|
112 |
-
fig.patch.set_alpha(0)
|
113 |
-
|
114 |
-
# Historical data
|
115 |
-
ax.plot(df["Date"], df["Close"], label="Historical", color="cyan", linewidth=2)
|
116 |
-
|
117 |
-
# Confidence interval (expanding uncertainty)
|
118 |
-
days = np.arange(1, n_future_days + 1)
|
119 |
-
expanding_std = error_std * np.sqrt(days)
|
120 |
-
upper = predicted_prices + 1.96 * expanding_std # 95% CI
|
121 |
-
lower = predicted_prices - 1.96 * expanding_std
|
122 |
-
|
123 |
-
ax.fill_between(
|
124 |
-
prediction_df["Date"],
|
125 |
-
lower,
|
126 |
-
upper,
|
127 |
-
color="lightblue",
|
128 |
-
alpha=0.3,
|
129 |
-
label="95% Confidence Interval",
|
130 |
-
)
|
131 |
-
|
132 |
-
# Predicted points (magenta dots)
|
133 |
-
ax.plot(
|
134 |
-
prediction_df["Date"],
|
135 |
-
prediction_df["Predicted Price"],
|
136 |
-
label=f"Next {n_future_days-1} Days Forecast",
|
137 |
-
color="magenta",
|
138 |
-
linestyle="None",
|
139 |
-
marker=".",
|
140 |
-
markersize=5,
|
141 |
-
)
|
142 |
-
|
143 |
-
# ---- NEW: Trend line spanning historical + forecasted data ----
|
144 |
-
# Combine historical and predicted dates/prices
|
145 |
-
all_dates = np.concatenate([df["Date"].values, prediction_df["Date"].values])
|
146 |
-
all_prices = np.concatenate(
|
147 |
-
[df["Close"].values, prediction_df["Predicted Price"].values]
|
148 |
-
)
|
149 |
-
|
150 |
-
window_size = 30
|
151 |
-
trend_line = pd.Series(all_prices).rolling(window=window_size, min_periods=1).mean()
|
152 |
-
|
153 |
-
# Plotting the trend line (blue dashed)
|
154 |
-
ax.plot(
|
155 |
-
all_dates,
|
156 |
-
trend_line,
|
157 |
-
color="blue",
|
158 |
-
linestyle="--",
|
159 |
-
linewidth=1.5,
|
160 |
-
label="Long-Term Trend",
|
161 |
-
)
|
162 |
-
|
163 |
-
# Style
|
164 |
-
ax.set_title(
|
165 |
-
f"📈 Stock Price Forecast ({st.session_state.model})",
|
166 |
-
fontsize=14,
|
167 |
-
fontweight="bold",
|
168 |
-
)
|
169 |
-
ax.set_xlabel("Date", fontsize=12)
|
170 |
-
ax.set_ylabel("Price", fontsize=12)
|
171 |
-
ax.legend(loc="upper left")
|
172 |
-
ax.grid(True, linestyle="--", alpha=0.6)
|
173 |
-
|
174 |
-
return fig, prediction_df
|
175 |
-
|
176 |
-
|
177 |
-
#####################################################################################################
|
178 |
-
|
179 |
-
# Function to load data
|
180 |
-
|
181 |
-
|
182 |
-
# Load the data
|
183 |
-
# data = load_data()
|
184 |
-
# import matplotlib.pyplot as plt
|
185 |
-
# Path to your logo image
|
186 |
-
encoded_logo = "tensorflow.png"
|
187 |
-
main_bg_ext = "png"
|
188 |
-
main_bg = "Picture3.png"
|
189 |
-
|
190 |
-
|
191 |
-
if st.session_state.framework == "lstm":
|
192 |
-
bg_color = "#FF5733" # For example, a warm red/orange
|
193 |
-
bg_color_iv = "orange" # For example, a warm red/orange
|
194 |
-
text_h1 = "BI-DIRECTIONAL"
|
195 |
-
text_i = "Long short term memory"
|
196 |
-
model = "TENSORFLOW"
|
197 |
-
st.session_state.model = "best_bilstm_model.h5"
|
198 |
-
if st.session_state.framework == "gru":
|
199 |
-
bg_color = "#FF5733" # For example, a warm red/orange
|
200 |
-
bg_color_iv = "orange" # For example, a warm red/orange
|
201 |
-
text_h1 = "GATED RECURRENT UNIT"
|
202 |
-
text_i = "Recurrent Neural Network"
|
203 |
-
model = "TENSORFLOW"
|
204 |
-
st.session_state.model = "best_gru_model.h5"
|
205 |
-
if st.session_state.framework == "gen":
|
206 |
-
bg_color = "#FF5733" # For example, a warm red/orange
|
207 |
-
bg_color_iv = "orange" # For example, a warm red/orange
|
208 |
-
text_h1 = "Amazon Stock Predictor"
|
209 |
-
text_i = "21 Days Ahead of the Market"
|
210 |
-
model = "TENSORFLOW"
|
211 |
-
st.markdown(
|
212 |
-
f"""
|
213 |
<style>
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
gap: 25px;
|
223 |
-
width: 70%;
|
224 |
-
z-index:1000;
|
225 |
-
}}
|
226 |
-
|
227 |
-
/* Logo styling */
|
228 |
-
.logo-text-container img {{
|
229 |
-
width: 50px; /* Adjust logo size */
|
230 |
-
border-radius: 10px; /* Optional: round edges */
|
231 |
-
margin-left:-5px;
|
232 |
-
margin-top: -15px;
|
233 |
-
|
234 |
-
}}
|
235 |
-
|
236 |
-
/* Bold text styling */
|
237 |
-
.logo-text-container h1 {{
|
238 |
-
font-family: Nunito;
|
239 |
-
color: #0175C2;
|
240 |
-
font-size: 25px;
|
241 |
-
font-weight: bold;
|
242 |
-
margin-right :100px;
|
243 |
-
padding:0px;
|
244 |
-
top:0;
|
245 |
-
margin-top: -12px;
|
246 |
-
}}
|
247 |
-
.logo-text-container i{{
|
248 |
-
font-family: Nunito;
|
249 |
-
color: orange;
|
250 |
-
font-size: 15px;
|
251 |
-
margin-right :10px;
|
252 |
-
padding:0px;
|
253 |
-
margin-left:-18.5%;
|
254 |
-
margin-top:1%;
|
255 |
-
}}
|
256 |
-
|
257 |
-
/* Sidebar styling */
|
258 |
-
section[data-testid="stSidebar"][aria-expanded="true"] {{
|
259 |
-
margin-top: 100px !important; /* Space for the logo */
|
260 |
-
border-radius: 0 60px 0px 60px !important; /* Top-left and bottom-right corners */
|
261 |
-
width: 200px !important; /* Sidebar width */
|
262 |
-
background: none; /* No background */
|
263 |
-
color: white !important;
|
264 |
-
}}
|
265 |
-
|
266 |
-
header[data-testid="stHeader"] {{
|
267 |
-
background: transparent !important;
|
268 |
-
margin-right: 100px !important;
|
269 |
-
margin-top: 1px !important;
|
270 |
-
z-index: 1 !important;
|
271 |
-
|
272 |
-
color: blue; /* White text */
|
273 |
-
font-family: "Times New Roman " !important; /* Font */
|
274 |
-
font-size: 18px !important; /* Font size */
|
275 |
-
font-weight: bold !important; /* Bold text */
|
276 |
-
padding: 10px 20px; /* Padding for buttons */
|
277 |
-
border: none; /* Remove border */
|
278 |
-
border-radius: 35px; /* Rounded corners */
|
279 |
-
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2); /* Shadow effect */
|
280 |
-
transition: all 0.3s ease-in-out; /* Smooth transition */
|
281 |
-
display: flex;
|
282 |
-
align-items: center;
|
283 |
-
justify-content: center;
|
284 |
-
margin: 10px 0;
|
285 |
-
width:90%;
|
286 |
-
left:5.5%;
|
287 |
-
height:60px;
|
288 |
-
margin-top:70px;
|
289 |
-
backdrop-filter: blur(10px);
|
290 |
-
border: 2px solid rgba(255, 255, 255, 0.4); /* Light border */
|
291 |
-
|
292 |
-
}}
|
293 |
-
|
294 |
-
div[data-testid="stDecoration"] {{
|
295 |
-
background-image: none;
|
296 |
-
}}
|
297 |
-
|
298 |
-
div[data-testid="stApp"] {{
|
299 |
-
background: url(data:image/{main_bg_ext};base64,{base64.b64encode(open(main_bg, "rb").read()).decode()});
|
300 |
-
background-size: cover; /* Ensure the image covers the full page */
|
301 |
-
background-position: center;
|
302 |
-
background-repeat:no-repeat;
|
303 |
-
height: 98vh;
|
304 |
-
width: 99.3%;
|
305 |
-
border-radius: 20px !important;
|
306 |
-
margin-left: 5px;
|
307 |
-
margin-right: 20px;
|
308 |
-
margin-top: 10px;
|
309 |
-
overflow: hidden;
|
310 |
-
backdrop-filter: blur(10px); /* Glass effect */
|
311 |
-
-webkit-backdrop-filter: blur(10px);
|
312 |
-
border: 1px solid rgba(255, 255, 255, 0.2); /* Light border */
|
313 |
-
|
314 |
-
}}
|
315 |
-
|
316 |
-
div[data-testid="stSidebarNav"] {{
|
317 |
-
display: none;
|
318 |
-
}}
|
319 |
-
|
320 |
-
div[data-testid="stSlider"] {{
|
321 |
-
margin-top:35px;
|
322 |
-
}}
|
323 |
-
label[data-testid="stWidgetLabel"]{{
|
324 |
-
margin-left:20px !important;
|
325 |
-
}}
|
326 |
-
|
327 |
-
div[data-baseweb="slider"] {{
|
328 |
-
border-radius: 30px;
|
329 |
-
padding-right:40px;
|
330 |
-
z-index: 1;
|
331 |
-
/* Glass effect background */
|
332 |
-
backdrop-filter: blur(2px);
|
333 |
-
-webkit-backdrop-filter: blur(12px);
|
334 |
-
/* Shiny blue borders (left & right) */
|
335 |
-
border-top: 2px solid rgba(255, 255, 155, 0.4); /* Light border */
|
336 |
-
margin-left:13px;
|
337 |
-
border-bottom: 2px solid rgba(255, 255, 155, 0.4); /* Light border */
|
338 |
-
|
339 |
-
|
340 |
-
}}
|
341 |
-
div[data-baseweb="slider"] > :nth-child(1)> div {{
|
342 |
-
margin-left:20px !important;
|
343 |
-
margin-top:10px;
|
344 |
-
}}
|
345 |
-
div[data-testid="stSliderTickBarMin"]{{
|
346 |
-
background:none !important;
|
347 |
-
margin-left:20px !important;
|
348 |
-
font-size:12px;
|
349 |
-
margin-bottom:5px;
|
350 |
-
font-family: "Times New Roman " !important; /* Font */
|
351 |
-
}}
|
352 |
-
div[data-testid="stSliderTickBarMax"]{{
|
353 |
-
background:none !important;
|
354 |
-
font-size:12px;
|
355 |
-
margin-bottom:5px;
|
356 |
-
|
357 |
-
font-family: "Times New Roman " !important; /* Font */
|
358 |
-
}}
|
359 |
-
div[data-testid="stSliderThumbValue"]{{
|
360 |
-
font-size:12px;
|
361 |
-
font-family: "Times New Roman " !important; /* Font */
|
362 |
-
|
363 |
-
}}
|
364 |
-
div[data-testid="stProgress"]{{
|
365 |
-
margin-right:25px;
|
366 |
-
margin-top:-70px;
|
367 |
-
height:10px !important;
|
368 |
-
|
369 |
-
}}
|
370 |
-
[class*="st-key-content-container-3"] {{
|
371 |
-
margin-top: 80px; /* Adjust top margin */
|
372 |
-
marging-left:50px !important;
|
373 |
-
color:orange;
|
374 |
-
|
375 |
-
}}
|
376 |
-
|
377 |
-
/* Button row styling */
|
378 |
-
.button-row {{
|
379 |
-
display: flex;
|
380 |
-
justify-content: flex-start;
|
381 |
-
gap: 20px;
|
382 |
-
margin-bottom: 20px;
|
383 |
-
}}
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
.custom-button:hover {{
|
388 |
-
background-color: #0056b3;
|
389 |
-
}}
|
390 |
-
div.stButton > button p{{
|
391 |
-
color: orange !important;
|
392 |
-
font-weight:bold;
|
393 |
-
}}
|
394 |
-
div.stButton > button {{
|
395 |
background: rgba(255, 255, 255, 0.2);
|
396 |
color: orange !important; /* White text */
|
397 |
font-family: "Times New Roman " !important; /* Font */
|
@@ -406,358 +37,809 @@ st.markdown(
|
|
406 |
align-items: center;
|
407 |
justify-content: center;
|
408 |
margin: 10px 0;
|
409 |
-
width:
|
410 |
height:50px;
|
411 |
margin-top:5px;
|
412 |
-
|
413 |
-
|
414 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
415 |
/* Hover effect */
|
416 |
-
div.stButton > button:hover {
|
417 |
background: rgba(255, 255, 255, 0.2);
|
418 |
box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.4); /* Enhanced shadow on hover */
|
419 |
transform: scale(1.05); /* Slightly enlarge button */
|
420 |
transform: scale(1.1); /* Slight zoom on hover */
|
421 |
box-shadow: 0px 4px 12px rgba(255, 255, 255, 0.4); /* Glow effect */
|
422 |
-
}
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
464 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
465 |
|
|
|
466 |
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
)
|
472 |
-
|
473 |
-
|
474 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
475 |
<style>
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
484 |
-
z-index: 10;
|
485 |
-
width:80vw;
|
486 |
-
height:620px;
|
487 |
-
}}
|
488 |
-
.logo-text-containers img {{
|
489 |
-
height: 40px;
|
490 |
-
right:0;
|
491 |
}}
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
}}
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
font-size: 14px;
|
501 |
-
color: #333;
|
502 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
503 |
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
516 |
}}
|
517 |
-
[class*="st-key-divider-col"] {{
|
518 |
-
border-left: 3px solid rgba(255, 255, 155, 0.5); /* Light border */
|
519 |
-
border-radius: 35px; /* Rounded corners */
|
520 |
-
margin-top:-18px;
|
521 |
-
margin-left:3px;
|
522 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
523 |
}}
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
529 |
|
|
|
|
|
530 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
531 |
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
background-color: tansparent;
|
542 |
-
padding: 15px;
|
543 |
-
border-radius: 30px;
|
544 |
-
padding-right:40px;
|
545 |
-
z-index: 1;
|
546 |
-
width:88vw;
|
547 |
-
height:615px;
|
548 |
-
/* Glass effect background */
|
549 |
-
background: rgba(255, 255, 255, 0.15);
|
550 |
-
backdrop-filter: blur(12px);
|
551 |
-
-webkit-backdrop-filter: blur(12px);
|
552 |
-
/* Shiny blue borders (left & right) */
|
553 |
-
border-left: 3px solid rgba(255, 255, 255, 0.9); /* Light border */
|
554 |
-
border-right: 3px solid rgba(255, 255, 255, 0.9); /* Light border */
|
555 |
|
|
|
|
|
|
|
556 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
557 |
}}
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
562 |
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
|
572 |
-
border-radius: 10px; /* Optional: round edges */
|
573 |
-
margin-left:15px;
|
574 |
-
margin-top: -35px;
|
575 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
576 |
}}
|
577 |
-
|
578 |
-
}}
|
579 |
-
</style>
|
580 |
-
""",
|
581 |
-
unsafe_allow_html=True,
|
582 |
-
)
|
583 |
-
|
584 |
-
if st.session_state.show_overlay:
|
585 |
-
|
586 |
-
with st.container(key="logo-text-containers"):
|
587 |
-
if st.button("✕", key="close-btn"):
|
588 |
-
st.session_state.show_overlay = False
|
589 |
-
st.session_state.framework = "gen"
|
590 |
-
st.rerun()
|
591 |
-
with st.spinner("Downloading and processing the Data..."):
|
592 |
-
progress_bar = st.progress(0)
|
593 |
-
for i in range(1, 11):
|
594 |
-
time.sleep(0.6)
|
595 |
-
progress_bar.progress(i * 10)
|
596 |
-
with st.container(key="content"):
|
597 |
-
days = st.slider(
|
598 |
-
"Amazon Stock Insight: Predictive Analytics Over 21 Days",
|
599 |
-
1,
|
600 |
-
21,
|
601 |
-
7,
|
602 |
-
key="days_slider",
|
603 |
-
)
|
604 |
|
605 |
-
|
606 |
-
|
607 |
-
|
608 |
-
|
609 |
-
|
610 |
-
|
611 |
-
|
612 |
-
|
613 |
-
|
614 |
-
|
615 |
-
|
616 |
-
|
617 |
-
|
618 |
-
[
|
619 |
-
{
|
620 |
-
"selector": "th",
|
621 |
-
"props": [
|
622 |
-
("padding", "12px"),
|
623 |
-
("color", "#000"),
|
624 |
-
(
|
625 |
-
"background-color",
|
626 |
-
"rgba(255, 255, 255, 0.15)",
|
627 |
-
),
|
628 |
-
],
|
629 |
-
},
|
630 |
-
{
|
631 |
-
"selector": "td",
|
632 |
-
"props": [
|
633 |
-
("padding", "10px"),
|
634 |
-
("color", "#000"),
|
635 |
-
("border-bottom", "1px solid rgba(0,0,0,0.1)"),
|
636 |
-
],
|
637 |
-
},
|
638 |
-
{
|
639 |
-
"selector": "table",
|
640 |
-
"props": [
|
641 |
-
("width", "100%"),
|
642 |
-
("border-collapse", "collapse"),
|
643 |
-
],
|
644 |
-
},
|
645 |
-
]
|
646 |
-
)
|
647 |
-
.to_html()
|
648 |
-
)
|
649 |
-
|
650 |
-
|
651 |
-
# Glassmorphism CSS + vertical scroll + black text
|
652 |
-
glass_css = """
|
653 |
-
<style>
|
654 |
-
/* Outer shell for glass effect & border radius */
|
655 |
-
.outer-glass-wrapper {
|
656 |
-
backdrop-filter: blur(10px);
|
657 |
-
-webkit-backdrop-filter: blur(10px);
|
658 |
-
background: rgba(255, 255, 255, 0.15);
|
659 |
-
border-radius: 20px;
|
660 |
-
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.2);
|
661 |
-
max-height: 600px;
|
662 |
-
max-width: 800px;
|
663 |
-
overflow: hidden;
|
664 |
-
margin-right: 15px;
|
665 |
-
margin-left:3px;
|
666 |
-
font-family: "Times New Roman " !important; /* Font */
|
667 |
-
font-size: 14px;
|
668 |
-
border: 1px solid rgba(255, 255, 255, 0.2);
|
669 |
-
margin-bottom:30px;
|
670 |
-
}
|
671 |
-
|
672 |
-
/* Inner scrolling container */
|
673 |
-
.glass-container {
|
674 |
-
max-height: 400px;
|
675 |
-
overflow-y: auto;
|
676 |
-
padding: 16px 24px 16px 16px; /* right padding gives room for scrollbar */
|
677 |
-
}
|
678 |
-
|
679 |
-
/* Scrollbar styles */
|
680 |
-
.glass-container::-webkit-scrollbar {
|
681 |
-
width: 4px;
|
682 |
-
}
|
683 |
-
.glass-container::-webkit-scrollbar-track {
|
684 |
-
background: transparent;
|
685 |
-
}
|
686 |
-
.glass-container::-webkit-scrollbar-thumb {
|
687 |
-
background-color: rgba(0, 0, 0, 0.3);
|
688 |
-
border-radius: 10px;
|
689 |
-
}
|
690 |
-
.glass-container {
|
691 |
-
scrollbar-width: thin;
|
692 |
-
scrollbar-color: rgba(0, 0, 0, 0.3) transparent;
|
693 |
-
}
|
694 |
-
|
695 |
-
/* Table styling */
|
696 |
-
.glass-table {
|
697 |
-
width: 100%;
|
698 |
-
}
|
699 |
-
.glass-table th, .glass-table td {
|
700 |
-
text-align: left;
|
701 |
-
white-space: nowrap;
|
702 |
-
color: #000;
|
703 |
-
}
|
704 |
-
</style>
|
705 |
-
"""
|
706 |
-
|
707 |
-
st.markdown(glass_css, unsafe_allow_html=True)
|
708 |
-
st.markdown(
|
709 |
-
f""" <div class="outer-glass-wrapper">
|
710 |
-
<div class="glass-container">
|
711 |
-
{styled_html}</div> </div>
|
712 |
-
""",
|
713 |
-
unsafe_allow_html=True,
|
714 |
-
)
|
715 |
-
|
716 |
-
with col2:
|
717 |
-
with st.container(key="divider-col"):
|
718 |
-
st.pyplot(fig)
|
719 |
|
720 |
-
|
721 |
-
|
722 |
-
|
723 |
-
|
724 |
-
|
725 |
-
|
726 |
-
|
727 |
-
|
728 |
-
|
729 |
-
|
730 |
-
|
731 |
-
|
732 |
-
|
733 |
-
|
734 |
-
|
735 |
-
|
736 |
-
|
737 |
-
|
738 |
-
|
739 |
-
|
740 |
-
|
741 |
-
|
742 |
-
|
743 |
-
|
744 |
-
|
745 |
-
|
746 |
-
|
747 |
-
|
748 |
-
|
749 |
-
)
|
750 |
-
|
751 |
-
|
752 |
-
|
753 |
-
|
754 |
-
|
755 |
-
|
756 |
-
|
757 |
-
|
758 |
-
|
759 |
-
|
760 |
-
|
761 |
-
|
762 |
-
|
763 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import streamlit as st
|
|
|
2 |
import streamlit.components.v1 as components
|
3 |
+
import yfinance as yf
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
import pandas as pd
|
5 |
+
import numpy as np
|
6 |
+
from datetime import datetime, timedelta
|
7 |
from keras.models import load_model
|
8 |
from sklearn.preprocessing import MinMaxScaler
|
9 |
+
import time
|
10 |
+
import os
|
11 |
|
12 |
+
# --- Page Configuration ---
|
13 |
+
st.set_page_config(layout="wide")
|
14 |
|
15 |
+
# --- Custom CSS ---
|
16 |
+
st.markdown("""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
<style>
|
18 |
+
/* Hide Streamlit's default header, footer, and hamburger menu */
|
19 |
+
#MainMenu, header, footer { visibility: hidden; }
|
20 |
+
|
21 |
+
/* Remove padding from the main block container for a full-width feel */
|
22 |
+
.block-container {
|
23 |
+
padding: 0 !important;
|
24 |
+
}
|
25 |
+
div.stButton > button {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
background: rgba(255, 255, 255, 0.2);
|
27 |
color: orange !important; /* White text */
|
28 |
font-family: "Times New Roman " !important; /* Font */
|
|
|
37 |
align-items: center;
|
38 |
justify-content: center;
|
39 |
margin: 10px 0;
|
40 |
+
width:190px;
|
41 |
height:50px;
|
42 |
margin-top:5px;
|
43 |
+
}
|
44 |
+
div[data-testid="stSelectbox"]
|
45 |
+
{
|
46 |
+
background-color: white !important;
|
47 |
+
position: relative;
|
48 |
+
border-bottom:1px solid #ccc;
|
49 |
+
border-radius:0px;
|
50 |
+
|
51 |
+
|
52 |
+
|
53 |
+
}
|
54 |
+
div[data-testid="stTextInput"]{
|
55 |
+
|
56 |
+
}
|
57 |
+
div[data-testid="stTextInput"] > div >div {
|
58 |
+
background-color: rgba(255, 158, 87, 0.12) !important;
|
59 |
+
|
60 |
+
}
|
61 |
+
div[data-testid="stTextInputRootElement"]{
|
62 |
+
border: 1px solid white !important;
|
63 |
+
}
|
64 |
/* Hover effect */
|
65 |
+
div.stButton > button:hover {
|
66 |
background: rgba(255, 255, 255, 0.2);
|
67 |
box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.4); /* Enhanced shadow on hover */
|
68 |
transform: scale(1.05); /* Slightly enlarge button */
|
69 |
transform: scale(1.1); /* Slight zoom on hover */
|
70 |
box-shadow: 0px 4px 12px rgba(255, 255, 255, 0.4); /* Glow effect */
|
71 |
+
}
|
72 |
+
/* Style the sidebar to have a modern, dark look */
|
73 |
+
section[data-testid="stSidebar"] {
|
74 |
+
backdrop-filter: blur(10px);
|
75 |
+
background: rgba(255, 255, 255, 0.15);
|
76 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
77 |
+
|
78 |
+
[data-testid="stSidebar"] h2 {
|
79 |
+
color: #FFFFFF; /* White headers in the sidebar */
|
80 |
+
font-family:time new roman !important;
|
81 |
+
|
82 |
+
}
|
83 |
+
[data-testid="stSidebar"] .st-emotion-cache-1629p8f a {
|
84 |
+
color: #94A3B8; /* Lighter text color for links */
|
85 |
+
font-family:time new roman !important;
|
86 |
+
}
|
87 |
+
[data-testid="stImageContainer"]>img{
|
88 |
+
max-width:70% !important;
|
89 |
+
margin-top:-70px;
|
90 |
+
}
|
91 |
+
div[data-testid="stMarkdownContainer"] >p{
|
92 |
+
font-family:time new roman !important;
|
93 |
+
|
94 |
+
}
|
95 |
+
|
96 |
+
</style>
|
97 |
+
""", unsafe_allow_html=True)
|
98 |
+
|
99 |
+
# --- Python Backend Functions ---
|
100 |
+
|
101 |
+
@st.cache_resource(ttl=3600)
|
102 |
+
def load_pytorch_model(path, model_type='Bi-Directional LSTM', input_dim=1, hidden_dim=100, num_layers=2, output_dim=1, dropout_prob=0.2):
|
103 |
+
import torch.nn as nn
|
104 |
+
import torch
|
105 |
+
|
106 |
+
class GRUModel(nn.Module):
|
107 |
+
def __init__(self):
|
108 |
+
super(GRUModel, self).__init__()
|
109 |
+
self.gru = nn.GRU(input_dim, hidden_dim, num_layers, batch_first=True, dropout=dropout_prob)
|
110 |
+
self.fc = nn.Linear(hidden_dim, output_dim)
|
111 |
+
|
112 |
+
def forward(self, x):
|
113 |
+
h0 = torch.zeros(num_layers, x.size(0), hidden_dim).to(x.device)
|
114 |
+
out, _ = self.gru(x, h0)
|
115 |
+
return self.fc(out[:, -1, :])
|
116 |
+
|
117 |
+
class BiLSTMModel(nn.Module):
|
118 |
+
def __init__(self):
|
119 |
+
super(BiLSTMModel, self).__init__()
|
120 |
+
self.lstm = nn.LSTM(
|
121 |
+
input_size=1,
|
122 |
+
hidden_size=100,
|
123 |
+
num_layers=1, # <- match saved model
|
124 |
+
batch_first=True,
|
125 |
+
dropout=0.2,
|
126 |
+
bidirectional=True
|
127 |
+
)
|
128 |
+
self.fc = nn.Linear(200, 1) # 2 * hidden_size because of bidirectional
|
129 |
|
130 |
+
def forward(self, x):
|
131 |
+
h0 = torch.zeros(2 * 1, x.size(0), 100)
|
132 |
+
c0 = torch.zeros(2 * 1, x.size(0), 100)
|
133 |
+
out, _ = self.lstm(x, (h0, c0))
|
134 |
+
return self.fc(out[:, -1, :])
|
135 |
+
model_class = BiLSTMModel if model_type == 'Bi-Directional LSTM' else GRUModel
|
136 |
+
model = model_class()
|
137 |
|
138 |
+
checkpoint = torch.load(path, map_location=torch.device('cpu'))
|
139 |
|
140 |
+
# If full checkpoint was saved with keys like 'model_state_dict'
|
141 |
+
if 'model_state_dict' in checkpoint:
|
142 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
143 |
+
else:
|
144 |
+
model.load_state_dict(checkpoint) # Just raw state_dict
|
145 |
+
|
146 |
+
model.eval()
|
147 |
+
return model
|
148 |
+
|
149 |
+
@st.cache_data(ttl=900) # Cache data for 15 minutes
|
150 |
+
def get_stock_data(ticker):
|
151 |
+
"""Fetches historical stock data from Yahoo Finance for the last 4 years."""
|
152 |
+
end_date = datetime.now()
|
153 |
+
start_date = end_date - timedelta(days=4 * 365)
|
154 |
+
print(f"Fetching data for ticker: {ticker} from {start_date.date()} to {end_date.date()}")
|
155 |
+
data = yf.download(ticker, period="4y", multi_level_index=False)
|
156 |
+
if data.empty:
|
157 |
+
print(f"No data found for ticker: {ticker}")
|
158 |
+
return None
|
159 |
+
data.reset_index(inplace=True)
|
160 |
+
print(f"Successfully fetched {len(data)} rows for {ticker}")
|
161 |
+
return data
|
162 |
+
|
163 |
+
def predict_with_model(data: pd.DataFrame, n_days: int, model_path: str, model_type: str) -> pd.DataFrame:
|
164 |
+
import torch
|
165 |
+
|
166 |
+
try:
|
167 |
+
model = load_pytorch_model(model_path, model_type=model_type)
|
168 |
+
except FileNotFoundError as e:
|
169 |
+
raise e
|
170 |
+
print("model:",model)
|
171 |
+
close_prices = data['Close'].values.reshape(-1, 1)
|
172 |
+
scaler = MinMaxScaler(feature_range=(0, 1))
|
173 |
+
scaled_prices = scaler.fit_transform(close_prices)
|
174 |
+
|
175 |
+
sequence_length = 90
|
176 |
+
if len(scaled_prices) < sequence_length:
|
177 |
+
raise ValueError(f"Not enough historical data ({len(scaled_prices)} points) to create a sequence of {sequence_length} for prediction.")
|
178 |
+
|
179 |
+
last_sequence = scaled_prices[-sequence_length:]
|
180 |
+
current_seq = torch.tensor(last_sequence.reshape(1, sequence_length, 1), dtype=torch.float32)
|
181 |
+
|
182 |
+
predictions_scaled = []
|
183 |
+
with torch.no_grad():
|
184 |
+
for _ in range(n_days):
|
185 |
+
pred = model(current_seq)
|
186 |
+
predictions_scaled.append(pred.item())
|
187 |
+
next_input = pred.view(1, 1, 1)
|
188 |
+
current_seq = torch.cat((current_seq[:, 1:, :], next_input), dim=1)
|
189 |
+
|
190 |
+
predictions = scaler.inverse_transform(np.array(predictions_scaled).reshape(-1, 1)).flatten()
|
191 |
+
print("predictions",predictions)
|
192 |
+
last_date = pd.to_datetime(data['Date'].iloc[-1])
|
193 |
+
future_dates = [last_date + timedelta(days=i) for i in range(1, n_days + 1)]
|
194 |
+
|
195 |
+
prediction_df = pd.DataFrame({'Date': future_dates, 'Predicted Price': predictions})
|
196 |
+
|
197 |
+
historical_returns = data['Close'].pct_change().dropna()
|
198 |
+
volatility = historical_returns.std() if not historical_returns.empty else 0.01
|
199 |
+
|
200 |
+
error_std_growth = volatility * np.sqrt(np.arange(1, n_days + 1))
|
201 |
+
prediction_df['Upper CI'] = predictions * (1 + 1.96 * error_std_growth)
|
202 |
+
prediction_df['Lower CI'] = predictions * (1 - 1.96 * error_std_growth)
|
203 |
+
|
204 |
+
return prediction_df
|
205 |
+
|
206 |
+
|
207 |
+
# --- Streamlit Session State Initialization ---
|
208 |
+
if 'run_button_clicked' not in st.session_state:
|
209 |
+
st.session_state.run_button_clicked = False
|
210 |
+
if 'loading' not in st.session_state:
|
211 |
+
st.session_state.loading = False
|
212 |
+
if 'data' not in st.session_state:
|
213 |
+
st.session_state.data = None
|
214 |
+
if 'predictions' not in st.session_state:
|
215 |
+
st.session_state.predictions = None
|
216 |
+
if 'error' not in st.session_state:
|
217 |
+
st.session_state.error = None
|
218 |
+
|
219 |
+
# --- Streamlit Sidebar Controls ---
|
220 |
+
with st.sidebar:
|
221 |
+
st.image("logo2.png", use_container_width=True)
|
222 |
+
st.markdown("Dashboard Controls")
|
223 |
+
ticker = st.text_input("Stock Ticker", st.session_state.get('last_ticker', "AMZN")).upper()
|
224 |
+
model_type = st.selectbox(
|
225 |
+
"Prediction Model",
|
226 |
+
("Bi-Directional LSTM", "Gated Recurrent Unit (GRU)"),
|
227 |
+
key="model_choice",
|
228 |
+
help="Select the neural network architecture for prediction."
|
229 |
+
)
|
230 |
+
|
231 |
+
prediction_days = st.slider("Prediction Horizon (Days)", 7, 30, st.session_state.get('last_prediction_days', 7))
|
232 |
+
|
233 |
+
if st.button("Generate Dashboard", use_container_width=True):
|
234 |
+
st.session_state.run_button_clicked = True
|
235 |
+
st.session_state.loading = True
|
236 |
+
st.session_state.last_ticker = ticker
|
237 |
+
st.session_state.last_prediction_days = prediction_days
|
238 |
+
st.session_state.error = None
|
239 |
+
print("Generate Dashboard button clicked. Loading state set to True.")
|
240 |
+
st.rerun()
|
241 |
+
# Check if model or prediction days have changed
|
242 |
+
if (
|
243 |
+
ticker != st.session_state.get('last_ticker', '') or
|
244 |
+
model_type != st.session_state.get('last_model_type', '') or
|
245 |
+
prediction_days != st.session_state.get('last_prediction_days', 7)
|
246 |
+
):
|
247 |
+
st.session_state.run_button_clicked = True
|
248 |
+
st.session_state.loading = True
|
249 |
+
st.session_state.last_ticker = ticker
|
250 |
+
st.session_state.last_model_type = model_type
|
251 |
+
st.session_state.last_prediction_days = prediction_days
|
252 |
+
st.rerun()
|
253 |
+
# --- Main Application Logic ---
|
254 |
+
if st.session_state.run_button_clicked:
|
255 |
+
print(f"Inside main logic block. Current loading state: {st.session_state.loading}")
|
256 |
+
try:
|
257 |
+
st.session_state.data = get_stock_data(ticker)
|
258 |
+
|
259 |
+
if st.session_state.data is None:
|
260 |
+
st.session_state.error = f"Could not fetch data for ticker '{ticker}'. It may be an invalid symbol or network issue."
|
261 |
+
else:
|
262 |
+
model_path = "best_bilstm_model.pth" if model_type == "Bi-Directional LSTM" else "best_gru_model.pth"
|
263 |
+
st.session_state.predictions = predict_with_model(st.session_state.data, prediction_days, model_path,model_type)
|
264 |
+
st.session_state.error = None
|
265 |
+
|
266 |
+
except FileNotFoundError as e:
|
267 |
+
st.session_state.error = str(e)
|
268 |
+
print(f"Caught FileNotFoundError: {e}")
|
269 |
+
except ValueError as e:
|
270 |
+
st.session_state.error = str(e)
|
271 |
+
print(f"Caught ValueError: {e}")
|
272 |
+
except Exception as e:
|
273 |
+
st.session_state.error = f"An unexpected error occurred: {str(e)}"
|
274 |
+
print(f"Caught general Exception: {e}")
|
275 |
+
|
276 |
+
st.session_state.loading = False
|
277 |
+
st.session_state.run_button_clicked = False
|
278 |
+
print(f"Processing complete. Loading state set to False. Error: {st.session_state.error}")
|
279 |
+
st.rerun()
|
280 |
+
|
281 |
+
# --- Data Preparation for Front-End ---
|
282 |
+
historical_data_json = 'null'
|
283 |
+
prediction_data_json = 'null'
|
284 |
+
is_loading_js = str(st.session_state.get('loading', False)).lower()
|
285 |
+
error_message_js = 'null'
|
286 |
+
|
287 |
+
if st.session_state.get('error'):
|
288 |
+
error_message_js = f"'{st.session_state.error}'" # Pass error to JS
|
289 |
+
|
290 |
+
if st.session_state.data is not None and st.session_state.get('error') is None:
|
291 |
+
historical_data_json = st.session_state.data.to_json(orient='split', date_format='iso')
|
292 |
+
prediction_data_json = st.session_state.predictions.to_json(orient='split', date_format='iso')
|
293 |
+
|
294 |
+
# --- HTML Front-End ---
|
295 |
+
html_code = f"""
|
296 |
+
<!DOCTYPE html>
|
297 |
+
<html lang="en">
|
298 |
+
<head>
|
299 |
+
<meta charset="UTF-8">
|
300 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
301 |
+
<title>Stock Intelligence Dashboard</title>
|
302 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
303 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.js"></script>
|
304 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
305 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
306 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
307 |
<style>
|
308 |
+
body {{ font-family: 'time new roman'; background-color: #f1f5f9;scrollbar-width: 2px !important; scrollbar-color: rgba(100, 100, 100, 0.4) transparent;}}
|
309 |
+
.metric-card, .info-card {{ background-color: #ffffff; border-radius: 1rem; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); transition: all 0.3s ease-in-out; border: 1px solid #e2e8f0; }}
|
310 |
+
.metric-card:hover {{ transform: translateY(-5px); box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); }}
|
311 |
+
.positive {{ color: #10B981; }}
|
312 |
+
.negative {{ color: #EF4444; }}
|
313 |
+
.neutral {{ color: #64748b; }}
|
314 |
+
::-webkit-scrollbar {{
|
315 |
+
width: 6px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
316 |
}}
|
317 |
+
|
318 |
+
::-webkit-scrollbar-thumb {{
|
319 |
+
background-color: rgba(100, 100, 100, 0.4);
|
320 |
+
border-radius: 3px;
|
321 |
}}
|
322 |
+
|
323 |
+
::-webkit-scrollbar-track {{
|
324 |
+
background: transparent;
|
|
|
|
|
325 |
}}
|
326 |
+
#predictionTable table {{ width: 100%; border-collapse: collapse; }}
|
327 |
+
#predictionTable th, #predictionTable td {{ padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #e2e8f0; }}
|
328 |
+
#predictionTable th {{ background-color: #f8fafc; font-weight: 600; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; }}
|
329 |
+
#loading-overlay {{ position: fixed; inset: 0; background-color: rgba(255, 255, 255, 0.8); z-index: 100; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(4px); transition: opacity 0.3s ease; }}
|
330 |
+
.spinner {{ width: 56px; height: 56px; border: 5px solid #3b82f6; border-bottom-color: transparent; border-radius: 50%; display: inline-block; box-sizing: border-box; animation: spin 1s linear infinite; }}
|
331 |
+
@keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }}
|
332 |
+
.hidden {{ display: none !important; }}
|
333 |
+
.error-message {{ color: #EF4444; font-weight: 600; text-align: center; margin-top: 20px; padding: 15px; background-color: #fee2e2; border-radius: 0.5rem; border: 1px solid #ef4444; }}
|
334 |
+
</style>
|
335 |
+
</head>
|
336 |
+
<body class="antialiased text-slate-800">
|
337 |
+
|
338 |
+
|
339 |
+
<main id="content-wrapper">
|
340 |
+
<header class="bg-white/80 backdrop-blur-lg sticky top-0 z-50 border-b border-slate-200">
|
341 |
+
<div class="max-w-8xl mx-auto px-4 sm:px-6 lg:px-8">
|
342 |
+
<div class="flex items-center justify-between h-16">
|
343 |
+
<div class="flex items-center">
|
344 |
+
<i class="fas fa-chart-line text-2xl text-orange-400"></i>
|
345 |
+
<h1 id="dashboard-title" class="text-xl font-bold text-slate-900 ml-3">{ticker} Intelligence Dashboard</h1>
|
346 |
+
</div>
|
347 |
+
<div class="text-sm text-slate-500 flex items-center">
|
348 |
+
<div id="status-message" class="text-center text-sm text-slate-500 mt-4 hidden">Loading updated data...</div>
|
349 |
+
<i class="fas fa-rocket mr-2 text-orange-400"></i> Powered by a <span class="font-semibold text-yellow-600 ml-1">{model_type}</span>  model
|
350 |
+
</div>
|
351 |
+
</div>
|
352 |
+
</div>
|
353 |
+
</header>
|
354 |
+
|
355 |
+
<div class="p-4 sm:p-6 lg:p-8">
|
356 |
+
<div class="max-w-8xl mx-auto">
|
357 |
+
<div id="dashboard-error-message" class="hidden error-message"></div>
|
358 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8" id="metrics-grid"></div>
|
359 |
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
360 |
+
<div class="lg:col-span-2 space-y-8">
|
361 |
+
<div class="info-card p-4 sm:p-6">
|
362 |
+
<canvas id="priceChart" style="height: 350px;"></canvas>
|
363 |
+
</div>
|
364 |
+
<div class="info-card p-4 sm:p-6">
|
365 |
+
<canvas id="volumeChart" style="height: 200px;"></canvas>
|
366 |
+
</div>
|
367 |
+
<div id="predictionDetailsContainer" class="info-card p-4 sm:p-6 hidden">
|
368 |
+
<h3 class="text-lg font-semibold mb-4 text-slate-800">AI Prediction Details</h3>
|
369 |
+
<div class="overflow-x-auto" id="predictionTable"></div>
|
370 |
+
</div>
|
371 |
+
</div>
|
372 |
+
<div class="lg:col-span-1 space-y-8">
|
373 |
+
<div class="info-card p-6">
|
374 |
+
<h3 class="text-lg font-semibold mb-4 text-slate-800 flex items-center"><i class="fas fa-robot mr-3 text-orange-400"></i> AI Prediction Summary</h3>
|
375 |
+
<div id="predictionResult" class="mt-4 text-center"></div>
|
376 |
+
</div>
|
377 |
+
<div class="info-card p-6">
|
378 |
+
<h3 class="text-lg font-semibold mb-4 text-slate-800">Technical Summary</h3>
|
379 |
+
<div class="space-y-3" id="tech-summary"></div>
|
380 |
+
</div>
|
381 |
+
</div>
|
382 |
+
</div>
|
383 |
+
</div>
|
384 |
+
</div>
|
385 |
+
</main>
|
386 |
+
|
387 |
+
<script>
|
388 |
+
document.addEventListener('DOMContentLoaded', function () {{
|
389 |
+
const {{
|
390 |
+
LineController,
|
391 |
+
LineElement,
|
392 |
+
PointElement,
|
393 |
+
LinearScale,
|
394 |
+
TimeScale,
|
395 |
+
Legend,
|
396 |
+
Tooltip,
|
397 |
+
BarController,
|
398 |
+
BarElement,
|
399 |
+
CategoryScale // Although you use TimeScale for X, CategoryScale might be needed for other internal reasons or for completeness for Bar charts
|
400 |
+
}} = Chart;
|
401 |
+
|
402 |
+
Chart.register(
|
403 |
+
LineController,
|
404 |
+
LineElement,
|
405 |
+
PointElement,
|
406 |
+
LinearScale,
|
407 |
+
TimeScale,
|
408 |
+
Legend,
|
409 |
+
Tooltip,
|
410 |
+
BarController,
|
411 |
+
BarElement,
|
412 |
+
CategoryScale
|
413 |
+
);
|
414 |
+
console.log("JS: Chart.js components registered.");
|
415 |
+
|
416 |
+
const historicalDataJson = {historical_data_json};
|
417 |
+
const predictionDataJson = {prediction_data_json};
|
418 |
+
const isLoading = {is_loading_js};
|
419 |
+
const errorMessage = {error_message_js}; // Now receiving Python error
|
420 |
|
421 |
+
console.log("JS: DOMContentLoaded. Initial isLoading:", isLoading, "Error:", errorMessage);
|
422 |
+
|
423 |
+
const loadingOverlay = document.getElementById('loading-overlay');
|
424 |
+
const contentWrapper = document.getElementById('content-wrapper');
|
425 |
+
const metricsGridEl = document.getElementById('metrics-grid');
|
426 |
+
const techSummaryEl = document.getElementById('tech-summary');
|
427 |
+
const predictionResultEl = document.getElementById('predictionResult');
|
428 |
+
const predictionDetailsContainerEl = document.getElementById('predictionDetailsContainer');
|
429 |
+
const predictionTableEl = document.getElementById('predictionTable');
|
430 |
+
const dashboardErrorMessageEl = document.getElementById('dashboard-error-message');
|
431 |
+
|
432 |
+
let priceChart;
|
433 |
+
let volumeChart;
|
434 |
+
|
435 |
+
function parseData(jsonData) {{
|
436 |
+
try {{
|
437 |
+
if (!jsonData || !jsonData.columns) return null;
|
438 |
+
return {{
|
439 |
+
dates: jsonData.data.map(row => new Date(row[jsonData.columns.indexOf('Date')])),
|
440 |
+
prices: jsonData.data.map(row => row[jsonData.columns.indexOf('Close')]),
|
441 |
+
volumes: jsonData.data.map(row => row[jsonData.columns.indexOf('Volume')]),
|
442 |
+
highs: jsonData.data.map(row => row[jsonData.columns.indexOf('High')]),
|
443 |
+
}};
|
444 |
+
}} catch (e) {{
|
445 |
+
console.error("JS: Error parsing historical data:", e);
|
446 |
+
return null;
|
447 |
+
}}
|
448 |
}}
|
|
|
|
|
|
|
|
|
|
|
449 |
|
450 |
+
function parsePredictions(jsonData) {{
|
451 |
+
try {{
|
452 |
+
if (!jsonData || !jsonData.columns) return [];
|
453 |
+
return jsonData.data.map(row => ({{
|
454 |
+
x: new Date(row[jsonData.columns.indexOf('Date')]),
|
455 |
+
y: row[jsonData.columns.indexOf('Predicted Price')],
|
456 |
+
upperCI: row[jsonData.columns.indexOf('Upper CI')],
|
457 |
+
lowerCI: row[jsonData.columns.indexOf('Lower CI')]
|
458 |
+
}}));
|
459 |
+
}} catch (e) {{
|
460 |
+
console.error("JS: Error parsing prediction data:", e);
|
461 |
+
return [];
|
462 |
+
}}
|
463 |
}}
|
464 |
+
|
465 |
+
function displayMetric(elementId, value, prefix = '', suffix = '', decimals = 0) {{
|
466 |
+
const el = document.getElementById(elementId);
|
467 |
+
if (el) {{
|
468 |
+
el.textContent = prefix + value.toLocaleString(undefined, {{ minimumFractionDigits: decimals, maximumFractionDigits: decimals }}) + suffix;
|
469 |
+
}}
|
470 |
+
}}
|
471 |
+
|
472 |
+
function updateMetrics(data) {{
|
473 |
+
if (!data || data.prices.length < 2) {{
|
474 |
+
metricsGridEl.innerHTML = `<div class="col-span-full text-center text-slate-500 p-4">Not enough historical data to display metrics.</div>`;
|
475 |
+
return;
|
476 |
+
}}
|
477 |
+
const currentPrice = data.prices[data.prices.length - 1];
|
478 |
+
const prevPrice = data.prices[data.prices.length - 2];
|
479 |
+
const change = currentPrice - prevPrice;
|
480 |
+
const changePct = (change / prevPrice) * 100;
|
481 |
+
const volume = data.volumes[data.volumes.length - 1];
|
482 |
+
const sharesOutstanding = 10.33 * 1e9; // Example value
|
483 |
+
const marketCap = currentPrice * sharesOutstanding;
|
484 |
+
|
485 |
+
const metrics = [
|
486 |
+
{{ id: 'price', title: 'Current Price', value: currentPrice, change: `${{change >= 0 ? '+' : ''}}${{change.toFixed(2)}} (${{changePct.toFixed(2)}}%)`, status: change >= 0 ? 'positive' : 'negative', icon: 'fa-dollar-sign', prefix: '$', decimals: 2 }},
|
487 |
+
{{ id: 'market-cap', title: 'Market Cap', value: marketCap, change: 'USD', status: 'neutral', icon: 'fa-building', prefix: '$', suffix: '', decimals: 2, isCurrency: true }},
|
488 |
+
{{ id: 'volume', title: 'Daily Volume', value: volume, change: 'Shares Traded', status: 'neutral', icon: 'fa-chart-bar', suffix: '', decimals: 0 }},
|
489 |
+
{{ id: '52-week-high', title: '52-Week High', value: Math.max(...data.highs.slice(-252)), change: 'Annual Peak', status: 'neutral', icon: 'fa-arrow-trend-up', prefix: '$', decimals: 2 }},
|
490 |
+
];
|
491 |
+
|
492 |
+
metricsGridEl.innerHTML = metrics.map(metric => `<div class="metric-card p-5"><div class="flex items-center justify-between"><p class="text-sm font-medium text-slate-500">${{metric.title}}</p><div class="text-2xl text-slate-300"><i class="fas ${{metric.icon}}"></i></div></div><p class="text-3xl font-bold text-slate-900 mt-2" id="${{metric.id}}">0</p><p class="text-xs ${{metric.status}} mt-1 font-semibold">${{metric.change}}</p></div>`).join('');
|
493 |
+
|
494 |
+
metrics.forEach(metric => {{
|
495 |
+
let displayValue = metric.value;
|
496 |
+
let displaySuffix = metric.suffix;
|
497 |
+
let displayDecimals = metric.decimals;
|
498 |
+
|
499 |
+
if (metric.isCurrency) {{
|
500 |
+
if (metric.value >= 1e12) {{
|
501 |
+
displayValue = metric.value / 1e12;
|
502 |
+
displaySuffix = 'T';
|
503 |
+
displayDecimals = 2;
|
504 |
+
}} else if (metric.value >= 1e9) {{
|
505 |
+
displayValue = metric.value / 1e9;
|
506 |
+
displaySuffix = 'B';
|
507 |
+
displayDecimals = 2;
|
508 |
+
}} else if (metric.value >= 1e6) {{
|
509 |
+
displayValue = metric.value / 1e6;
|
510 |
+
displaySuffix = 'M';
|
511 |
+
displayDecimals = 2;
|
512 |
+
}}
|
513 |
+
}}
|
514 |
+
|
515 |
+
if (metric.id === 'volume') {{
|
516 |
+
if (metric.value >= 1e9) {{
|
517 |
+
displayValue = metric.value / 1e9;
|
518 |
+
displaySuffix = 'B';
|
519 |
+
displayDecimals = 2;
|
520 |
+
}} else if (metric.value >= 1e6) {{
|
521 |
+
displayValue = metric.value / 1e6;
|
522 |
+
displaySuffix = 'M';
|
523 |
+
displayDecimals = 2;
|
524 |
+
}} else if (metric.value >= 1e3) {{
|
525 |
+
displayValue = metric.value / 1e3;
|
526 |
+
displaySuffix = 'K';
|
527 |
+
displayDecimals = 2;
|
528 |
+
}}
|
529 |
+
}}
|
530 |
|
531 |
+
displayMetric(metric.id, displayValue, metric.prefix || '', displaySuffix, displayDecimals);
|
532 |
+
}});
|
533 |
}}
|
534 |
+
|
535 |
+
function updateTechSummary(data) {{
|
536 |
+
if (!data || data.prices.length < 50) {{ // Need enough data for 50-day SMA
|
537 |
+
techSummaryEl.innerHTML = '<p class="text-sm text-slate-500">Not enough data for full technical analysis (min 50 days required).</p>';
|
538 |
+
return;
|
539 |
+
}}
|
540 |
+
const prices = data.prices;
|
541 |
+
const lastPrice = prices[prices.length - 1];
|
542 |
+
|
543 |
+
// Ensure slice has enough elements
|
544 |
+
const sma20 = prices.slice(-20).length >= 20 ? prices.slice(-20).reduce((a, b) => a + b, 0) / 20 : NaN;
|
545 |
+
const sma50 = prices.slice(-50).length >= 50 ? prices.slice(-50).reduce((a, b) => a + b, 0) / 50 : NaN;
|
546 |
+
|
547 |
+
let gains = [];
|
548 |
+
let losses = [];
|
549 |
+
for (let i = 1; i < prices.length; i++) {{
|
550 |
+
let diff = prices[i] - prices[i-1];
|
551 |
+
if (diff > 0) {{
|
552 |
+
gains.push(diff);
|
553 |
+
losses.push(0);
|
554 |
+
}} else {{
|
555 |
+
gains.push(0);
|
556 |
+
losses.push(Math.abs(diff));
|
557 |
+
}}
|
558 |
+
}}
|
559 |
|
560 |
+
let avgGain = 0;
|
561 |
+
let avgLoss = 0;
|
562 |
+
if (gains.length >= 14) {{
|
563 |
+
avgGain = gains.slice(-14).reduce((a, b) => a + b, 0) / 14;
|
564 |
+
avgLoss = losses.slice(-14).reduce((a, b) => a + b, 0) / 14;
|
565 |
+
}} else if (gains.length > 0) {{
|
566 |
+
avgGain = gains.reduce((a, b) => a + b, 0) / gains.length;
|
567 |
+
avgLoss = losses.reduce((a, b) => a + b, 0) / losses.length;
|
568 |
+
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
569 |
|
570 |
+
let rs = (avgLoss === 0 || isNaN(avgLoss)) ? (avgGain > 0 ? Infinity : 0) : avgGain / avgLoss;
|
571 |
+
let rsi = 100 - (100 / (1 + rs));
|
572 |
+
if (isNaN(rsi)) rsi = 0;
|
573 |
|
574 |
+
let rsiClass = 'neutral';
|
575 |
+
if (rsi > 70) rsiClass = 'negative';
|
576 |
+
else if (rsi < 30) rsiClass = 'positive';
|
577 |
+
|
578 |
+
const summary = [
|
579 |
+
{{ label: 'SMA (20 Day)', value: isNaN(sma20) ? 'N/A' : `$${{sma20.toFixed(2)}}`, status: lastPrice > sma20 ? 'positive' : (isNaN(sma20) ? 'neutral' : 'negative') }},
|
580 |
+
{{ label: 'SMA (50 Day)', value: isNaN(sma50) ? 'N/A' : `$${{sma50.toFixed(2)}}`, status: lastPrice > sma50 ? 'positive' : (isNaN(sma50) ? 'neutral' : 'negative') }},
|
581 |
+
{{ label: 'RSI (14 Day)', value: rsi.toFixed(1), status: rsiClass }}
|
582 |
+
];
|
583 |
+
techSummaryEl.innerHTML = summary.map(item => `<div class="flex justify-between items-center text-sm"><span class="text-slate-600">${{item.label}}</span><span class="font-semibold ${{item.status}}">${{item.value}}</span></div>`).join('');
|
584 |
+
}}
|
585 |
+
|
586 |
+
function renderCharts(data, predictions) {{
|
587 |
+
// Render Price Chart
|
588 |
+
const priceCtx = document.getElementById('priceChart').getContext('2d');
|
589 |
+
if (priceChart) priceChart.destroy();
|
590 |
+
|
591 |
+
const priceDatasets = [
|
592 |
+
{{
|
593 |
+
label: 'Historical Price',
|
594 |
+
data: data.dates.map((d, i) => ({{x: d, y: data.prices[i]}})),
|
595 |
+
borderColor: '#3b82f6',
|
596 |
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
597 |
+
borderWidth: 2,
|
598 |
+
pointRadius: 0,
|
599 |
+
fill: true,
|
600 |
+
tension: 0.3
|
601 |
}}
|
602 |
+
];
|
603 |
+
|
604 |
+
if (predictions.length > 0) {{
|
605 |
+
priceDatasets.push({{
|
606 |
+
label: 'AI Prediction',
|
607 |
+
data: predictions,
|
608 |
+
borderColor: '#10b981',
|
609 |
+
borderWidth: 2,
|
610 |
+
pointRadius: 2,
|
611 |
+
borderDash: [5, 5],
|
612 |
+
fill: false,
|
613 |
+
tension: 0.3
|
614 |
+
}});
|
615 |
+
|
616 |
+
// Add confidence interval
|
617 |
+
const confidenceData = [
|
618 |
+
...predictions.map(p => ({{x: p.x, y: p.lowerCI}})),
|
619 |
+
...predictions.map(p => ({{x: p.x, y: p.upperCI}})).reverse()
|
620 |
+
];
|
621 |
+
|
622 |
+
priceDatasets.push({{
|
623 |
+
label: '95% Confidence',
|
624 |
+
data: confidenceData,
|
625 |
+
fill: '1',
|
626 |
+
backgroundColor: 'rgba(234, 179, 8, 0.2)',
|
627 |
+
borderColor: 'transparent',
|
628 |
+
pointRadius: 0
|
629 |
+
}});
|
630 |
+
}}
|
631 |
+
|
632 |
+
priceChart = new Chart(priceCtx, {{
|
633 |
+
type: 'line', // Explicitly define type
|
634 |
+
data: {{ datasets: priceDatasets }},
|
635 |
+
options: {{
|
636 |
+
responsive: true,
|
637 |
+
maintainAspectRatio: false,
|
638 |
+
scales: {{
|
639 |
+
x: {{
|
640 |
+
type: 'time',
|
641 |
+
time: {{
|
642 |
+
unit: 'month',
|
643 |
+
tooltipFormat: 'MMM d, yyyy'
|
644 |
+
}},
|
645 |
+
grid: {{ display: false }}
|
646 |
+
}},
|
647 |
+
y: {{
|
648 |
+
title: {{ display: true, text: 'Price (USD)' }},
|
649 |
+
grid: {{ color: '#f1f5f9' }}
|
650 |
+
}}
|
651 |
+
}},
|
652 |
+
plugins: {{
|
653 |
+
legend: {{
|
654 |
+
display: true,
|
655 |
+
position: 'top',
|
656 |
+
align: 'end'
|
657 |
+
}},
|
658 |
+
tooltip: {{
|
659 |
+
mode: 'index',
|
660 |
+
intersect: false,
|
661 |
+
callbacks: {{
|
662 |
+
title: function(context) {{
|
663 |
+
return context[0].label;
|
664 |
+
}},
|
665 |
+
label: function(context) {{
|
666 |
+
let label = context.dataset.label || '';
|
667 |
+
if (label) label += ': ';
|
668 |
+
label += '$' + context.parsed.y.toFixed(2);
|
669 |
+
if (context.dataset.label === 'AI Prediction' && predictions.length > 0) {{
|
670 |
+
const predictionPoint = predictions.find(p => p.x.getTime() === context.parsed.x);
|
671 |
+
if (predictionPoint) {{
|
672 |
+
label += ` (CI: $${{predictionPoint.lowerCI.toFixed(2)}} - $${{predictionPoint.upperCI.toFixed(2)}})`;
|
673 |
+
}}
|
674 |
+
}}
|
675 |
+
return label;
|
676 |
+
}}
|
677 |
+
}}
|
678 |
+
}}
|
679 |
+
}}
|
680 |
+
}}
|
681 |
+
}});
|
682 |
+
|
683 |
+
// Render Volume Chart
|
684 |
+
const volumeCtx = document.getElementById('volumeChart').getContext('2d');
|
685 |
+
if (volumeChart) volumeChart.destroy();
|
686 |
+
|
687 |
+
volumeChart = new Chart(volumeCtx, {{
|
688 |
+
type: 'bar', // Explicitly define type
|
689 |
+
data: {{
|
690 |
+
datasets: [{{
|
691 |
+
label: 'Volume',
|
692 |
+
data: data.dates.map((d, i) => ({{x: d, y: data.volumes[i]}})),
|
693 |
+
backgroundColor: '#e2e8f0',
|
694 |
+
borderColor: '#cbd5e1',
|
695 |
+
borderWidth: 1
|
696 |
+
}}]
|
697 |
+
}},
|
698 |
+
options: {{
|
699 |
+
responsive: true,
|
700 |
+
maintainAspectRatio: false,
|
701 |
+
scales: {{
|
702 |
+
x: {{
|
703 |
+
type: 'time',
|
704 |
+
time: {{
|
705 |
+
unit: 'month'
|
706 |
+
}},
|
707 |
+
grid: {{ display: false }}
|
708 |
+
}},
|
709 |
+
y: {{
|
710 |
+
title: {{ display: true, text: 'Volume' }},
|
711 |
+
grid: {{ color: '#f1f5f9' }},
|
712 |
+
ticks: {{
|
713 |
+
callback: function(value) {{
|
714 |
+
if (value >= 1e9) return (value / 1e9).toFixed(0) + 'B';
|
715 |
+
if (value >= 1e6) return (value / 1e6).toFixed(0) + 'M';
|
716 |
+
if (value >= 1e3) return (value / 1e3).toFixed(0) + 'K';
|
717 |
+
return value;
|
718 |
+
}}
|
719 |
+
}}
|
720 |
+
}}
|
721 |
+
}},
|
722 |
+
plugins: {{
|
723 |
+
legend: {{
|
724 |
+
display: false
|
725 |
+
}},
|
726 |
+
tooltip: {{
|
727 |
+
callbacks: {{
|
728 |
+
label: function(context) {{
|
729 |
+
let label = context.dataset.label || '';
|
730 |
+
if (label) label += ': ';
|
731 |
+
let value = context.parsed.y;
|
732 |
+
if (value >= 1e9) label += (value / 1e9).toLocaleString(undefined, {{maximumFractionDigits: 1}}) + 'B';
|
733 |
+
else if (value >= 1e6) label += (value / 1e6).toLocaleString(undefined, {{maximumFractionDigits: 1}}) + 'M';
|
734 |
+
else if (value >= 1e3) label += (value / 1e3).toLocaleString(undefined, {{maximumFractionDigits: 1}}) + 'K';
|
735 |
+
else label += value.toLocaleString();
|
736 |
+
return label;
|
737 |
+
}}
|
738 |
+
}}
|
739 |
+
}}
|
740 |
+
}}
|
741 |
+
}}
|
742 |
+
}});
|
743 |
+
}}
|
744 |
|
745 |
+
function displayPredictions(data, predictions) {{
|
746 |
+
if (!data || predictions.length === 0) {{
|
747 |
+
predictionDetailsContainerEl.classList.add('hidden');
|
748 |
+
predictionResultEl.innerHTML = '<p class="text-sm text-slate-500">No predictions available or not enough data for prediction.</p>';
|
749 |
+
return;
|
750 |
+
}}
|
751 |
+
predictionDetailsContainerEl.classList.remove('hidden');
|
752 |
+
const lastHistoricalPrice = data.prices[data.prices.length - 1];
|
753 |
+
const finalPredictedPrice = predictions[predictions.length - 1].y;
|
|
|
|
|
|
|
754 |
|
755 |
+
const changeOverall = finalPredictedPrice - lastHistoricalPrice;
|
756 |
+
const changePctOverall = (changeOverall / lastHistoricalPrice) * 100;
|
757 |
+
const statusClass = changeOverall >= 0 ? 'positive' : 'negative';
|
758 |
+
|
759 |
+
predictionResultEl.innerHTML = `<p class="text-sm text-slate-500">Predicted price in ${{predictions.length}} days:</p><p class="text-3xl font-bold mt-1 ${{statusClass}}">$${{finalPredictedPrice.toFixed(2)}} <span class="text-base font-normal">(${{changeOverall >= 0 ? '+' : ''}}${{changeOverall.toFixed(2)}} / ${{changePctOverall.toFixed(2)}}%)</span></p>`;
|
760 |
+
|
761 |
+
const tableRows = predictions.map(p => `
|
762 |
+
<tr>
|
763 |
+
<td>${{new Date(p.x).toLocaleDateString()}}</td>
|
764 |
+
<td class="font-semibold">$${{p.y.toFixed(2)}}</td>
|
765 |
+
<td>$${{p.lowerCI.toFixed(2)}} - $${{p.upperCI.toFixed(2)}}</td>
|
766 |
+
</tr>
|
767 |
+
`).join('');
|
768 |
+
predictionTableEl.innerHTML = `
|
769 |
+
<table>
|
770 |
+
<thead>
|
771 |
+
<tr>
|
772 |
+
<th>Date</th>
|
773 |
+
<th>Predicted Price</th>
|
774 |
+
<th>95% Confidence Interval</th>
|
775 |
+
</tr>
|
776 |
+
</thead>
|
777 |
+
<tbody>${{tableRows}}</tbody>
|
778 |
+
</table>
|
779 |
+
`;
|
780 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
781 |
|
782 |
+
function loadDashboard() {{
|
783 |
+
console.log("JS: loadDashboard() called. Current isLoading:", isLoading, "Error:", errorMessage);
|
784 |
+
const statusMessageEl = document.getElementById('status-message');
|
785 |
+
|
786 |
+
// Handle loading overlay visibility
|
787 |
+
|
788 |
+
if (isLoading === 'true') {{
|
789 |
+
statusMessageEl.classList.remove('hidden');
|
790 |
+
dashboardErrorMessageEl.classList.add('hidden'); // Hide any previous error
|
791 |
+
return; // Stop execution, let Streamlit re-run and call again when done
|
792 |
+
}} else {{
|
793 |
+
console.log("JS: in() called. Current isLoading:", isLoading, "Error:", errorMessage);
|
794 |
+
statusMessageEl.classList.add('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
795 |
|
796 |
+
}}
|
797 |
+
|
798 |
+
// Handle errors
|
799 |
+
if (errorMessage && errorMessage !== 'null') {{
|
800 |
+
dashboardErrorMessageEl.textContent = "Error: " + errorMessage;
|
801 |
+
dashboardErrorMessageEl.classList.remove('hidden');
|
802 |
+
// Clear existing charts if any, and other content
|
803 |
+
if (priceChart) priceChart.destroy();
|
804 |
+
if (volumeChart) volumeChart.destroy();
|
805 |
+
metricsGridEl.innerHTML = `<div class="col-span-full text-center text-slate-500 p-8 info-card">An error occurred. Please check the ticker or model.</div>`;
|
806 |
+
predictionDetailsContainerEl.classList.add('hidden');
|
807 |
+
predictionResultEl.innerHTML = '<p class="text-sm text-slate-500">No results due to error.</p>';
|
808 |
+
techSummaryEl.innerHTML = '<p class="text-sm text-slate-500">No technical summary due to error.</p>';
|
809 |
+
return;
|
810 |
+
}} else {{
|
811 |
+
dashboardErrorMessageEl.classList.add('hidden'); // Ensure error message is hidden if no error
|
812 |
+
}}
|
813 |
+
|
814 |
+
// If no error and not loading, proceed to render dashboard
|
815 |
+
const historicalData = parseData(historicalDataJson);
|
816 |
+
const predictionData = parsePredictions(predictionDataJson);
|
817 |
+
|
818 |
+
if (!historicalData) {{
|
819 |
+
metricsGridEl.innerHTML = `<div class="col-span-full text-center text-slate-500 p-8 info-card">Click "Generate Dashboard" in the sidebar to load data.</div>`;
|
820 |
+
predictionDetailsContainerEl.classList.add('hidden');
|
821 |
+
predictionResultEl.innerHTML = '<p class="text-sm text-slate-500">No data loaded yet.</p>';
|
822 |
+
techSummaryEl.innerHTML = '<p class="text-sm text-slate-500">No data for technical summary.</p>';
|
823 |
+
if (priceChart) priceChart.destroy();
|
824 |
+
if (volumeChart) volumeChart.destroy();
|
825 |
+
console.log("JS: No historical data available to render dashboard.");
|
826 |
+
return;
|
827 |
+
}}
|
828 |
+
|
829 |
+
updateMetrics(historicalData);
|
830 |
+
updateTechSummary(historicalData);
|
831 |
+
renderCharts(historicalData, predictionData); // Renamed to plural as it handles both
|
832 |
+
displayPredictions(historicalData, predictionData);
|
833 |
+
console.log("JS: Dashboard loaded successfully.");
|
834 |
+
}}
|
835 |
+
|
836 |
+
loadDashboard(); // Initial call when DOM is ready
|
837 |
+
}});
|
838 |
+
</script>
|
839 |
+
</body>
|
840 |
+
</html>
|
841 |
+
"""
|
842 |
+
|
843 |
+
# --- Embed HTML Component in Streamlit ---
|
844 |
+
# No need for st.error here, as the JS will handle displaying the error in the HTML component
|
845 |
+
components.html(html_code, height=1200, scrolling=True)
|