File size: 40,469 Bytes
6af75b9
 
4f8e612
6af75b9
4f8e612
 
6af75b9
 
4f8e612
 
6537369
 
6af75b9
4f8e612
 
a0ce4fe
 
 
 
 
 
 
 
 
 
 
 
 
4f8e612
 
6af75b9
4f8e612
 
 
 
 
 
 
 
6af75b9
 
 
 
 
 
 
 
 
 
 
 
 
 
4f8e612
6af75b9
 
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6af75b9
4f8e612
6af75b9
 
 
 
 
4f8e612
0724716
4f8e612
 
 
a0ce4fe
 
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d396b0d
 
 
 
 
 
 
 
 
 
 
 
 
 
fff6ecb
d396b0d
3971804
fff6ecb
d396b0d
 
3971804
 
d396b0d
 
3971804
4f8e612
d396b0d
 
 
 
4f8e612
 
f7ff14c
 
 
 
 
38b16ca
 
 
d762f15
121d7ab
6537369
 
38b16ca
4f8e612
 
 
 
 
 
 
a0ce4fe
 
4f8e612
 
 
 
 
 
 
6537369
 
 
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a0ce4fe
4f8e612
 
121d7ab
4f8e612
 
 
 
0c1c11f
4f8e612
7e02bbf
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38b16ca
6537369
 
4f8e612
 
c9d7bb3
143de8c
c9d7bb3
 
4f8e612
a0ce4fe
4f8e612
 
 
 
6537369
 
 
 
 
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6af75b9
4f8e612
 
 
 
 
 
 
 
6af75b9
4f8e612
 
 
 
6af75b9
4f8e612
 
 
6af75b9
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6af75b9
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6af75b9
 
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
6af75b9
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6af75b9
4f8e612
 
6af75b9
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6af75b9
4f8e612
 
 
 
 
 
 
 
 
6af75b9
4f8e612
 
 
6af75b9
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6af75b9
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6af75b9
4f8e612
 
 
 
 
 
 
 
 
6af75b9
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6af75b9
 
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
6af75b9
4f8e612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
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
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
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
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
import streamlit as st
import streamlit.components.v1 as components
import yfinance as yf
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from keras.models import load_model
from sklearn.preprocessing import MinMaxScaler
import time
import os
import torch.nn as nn
import torch

# --- Page Configuration ---
st.set_page_config(layout="wide")
# --- Streamlit Session State Initialization ---
if 'run_button_clicked' not in st.session_state:
    st.session_state.run_button_clicked = False
if 'loading' not in st.session_state:
    st.session_state.loading = False
if 'data' not in st.session_state:
    st.session_state.data = None
if 'predictions' not in st.session_state:
    st.session_state.predictions = None
if 'error' not in st.session_state:
    st.session_state.error = None
if 'last_ticker' not in st.session_state:
    st.session_state['last_ticker'] = 'AMZN'
# --- Custom CSS ---
st.markdown("""
    <style>
        /* Hide Streamlit's default header, footer, and hamburger menu */
        #MainMenu, header, footer { visibility: hidden; }

        /* Remove padding from the main block container for a full-width feel */
        .block-container {
            padding: 0 !important;
        }
        div.stButton > button {               
        background: rgba(255, 255, 255, 0.2);
        color: orange !important; /* White text */
        font-family:  "Times New Roman " !important; /* Font */
        font-size: 18px !important; /* Font size */
        font-weight: bold !important; /* Bold text */
        padding: 10px 20px; /* Padding for buttons */
        border: none; /* Remove border */
        border-radius: 35px; /* Rounded corners */
        box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2); /* Shadow effect */
        transition: all 0.3s ease-in-out; /* Smooth transition */
         display: flex;
        align-items: center;
        justify-content: center;
        margin: 10px 0;
        width:190px;
        height:50px;
        margin-top:5px;
        }
       div[data-testid="stSelectbox"]
        {
                background-color: white !important; 
                position: relative;
                border-bottom:1px solid #ccc;
                border-radius:0px;
               
                

             }
        div[data-testid="stTextInput"]{
            
        }
      div[data-testid="stTextInput"] > div >div {
        background-color: rgba(255, 158, 87, 0.12) !important;

      }
      div[data-testid="stTextInputRootElement"]{
          border: 1px solid white !important;
      }
    /* Hover effect */
    div.stButton > button:hover {
        background: rgba(255, 255, 255, 0.2);
        box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.4); /* Enhanced shadow on hover */
        transform: scale(1.05); /* Slightly enlarge button */
        transform: scale(1.1); /* Slight zoom on hover */
        box-shadow: 0px 4px 12px rgba(255, 255, 255, 0.4); /* Glow effect */
    }
        /* Styling the sidebar to have a modern, dark look */
        section[data-testid="stSidebar"] {
            backdrop-filter: blur(10px);
            background: rgba(255, 255, 255, 0.15);
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.01);
            height:100px;

        [data-testid="stSidebar"] h2 {
            color: #FFFFFF; /* White headers in the sidebar */
            font-family:time new roman !important;

        }
        [data-testid="stSidebar"] .st-emotion-cache-1629p8f a {
            color: #94A3B8; /* Lighter text color for links */
            font-family:time new roman !important;
        }
        [data-testid="stImageContainer"]>img{
         max-width:70% !important;
         margin-top:-70px;   
        }
        div[data-testid="stMarkdownContainer"] >p{
        font-family:time new roman !important;

        }
        
    </style>
""", unsafe_allow_html=True)

# --- Python Backend Functions ---
# Outside of any function

class GRUModel(nn.Module):
    def __init__(self, input_dim=1, hidden_dim=100, num_layers=2, output_dim=1, dropout_prob=0.2):
        super(GRUModel, self).__init__()
        self.gru = nn.GRU(input_dim, hidden_dim, num_layers, batch_first=True, dropout=dropout_prob)
        self.fc = nn.Linear(hidden_dim, output_dim)

    def forward(self, x):
        h0 = torch.zeros(2, x.size(0), 100).to(x.device)
        out, _ = self.gru(x, h0)
        return self.fc(out[:, -1, :])

class BiLSTMModel(nn.Module):
    def __init__(self):
        super(BiLSTMModel, self).__init__()
        self.lstm = nn.LSTM(input_size=1, hidden_size=100, num_layers=1, batch_first=True, dropout=0.2, bidirectional=True)
        self.fc = nn.Linear(200, 1)

    def forward(self, x):
        h0 = torch.zeros(2, x.size(0), 100)
        c0 = torch.zeros(2, x.size(0), 100)
        out, _ = self.lstm(x, (h0, c0))
        return self.fc(out[:, -1, :])

@st.cache_resource(ttl=3600)
def load_model_from_disk(path, model_type):
    model = BiLSTMModel() if model_type == "Bi-Directional LSTM" else GRUModel()
    state = torch.load(path, map_location=torch.device("cpu"))
    model.load_state_dict(state['model_state_dict'] if 'model_state_dict' in state else state)
    model.eval()
    return model
@st.cache_resource(ttl=3600)
def load_scripted_model(path):
    model = torch.jit.load(path, map_location=torch.device("cpu"))
    model.eval()
    return model
@st.cache_resource
def preload_models():
    return {
        "Bi-Directional LSTM": load_scripted_model("bilstm_scriptes.pt"),
        "Gated Recurrent Unit": load_model_from_disk("best_gru_model.pth", model_type="GRU")
    }

MODELS = preload_models()
@st.cache_data(ttl=900) # Cache data for 15 minutes
def get_stock_data(ticker):
    """Fetches historical stock data from Yahoo Finance for the last 4 years."""
    end_date = datetime.now()
    start_date = end_date - timedelta(days=4 * 365)
    print(f"Fetching data for ticker: {ticker} from {start_date.date()} to {end_date.date()}")
    data = yf.download(ticker, period="4y", multi_level_index=False)
    data.to_csv("AMZN_data.csv")

    if data.empty:
        print(f"No data found for ticker: {ticker}")
        return None
    data.reset_index(inplace=True)
    print(f"Successfully fetched {len(data)} rows for {ticker}")
    return data

def predict_with_model(data, n_days, model_path, model_type, model=None)-> pd.DataFrame:
    if model is None:
        model = load_model_from_disk(model_path, model_type=model_type)
    close_prices = data['Close'].values.reshape(-1, 1)
    scaler = MinMaxScaler(feature_range=(0, 1))
    scaled_prices = scaler.fit_transform(close_prices)

    sequence_length = 90
    if len(scaled_prices) < sequence_length:
        raise ValueError(f"Not enough historical data ({len(scaled_prices)} points) to create a sequence of {sequence_length} for prediction.")

    last_sequence = scaled_prices[-sequence_length:]
    current_seq = torch.tensor(last_sequence.reshape(1, sequence_length, 1), dtype=torch.float32)

    predictions_scaled = []
    with torch.no_grad():
        for _ in range(n_days):
            pred = model(current_seq)
            predictions_scaled.append(pred.item())
            next_input = pred.view(1, 1, 1)
            current_seq = torch.cat((current_seq[:, 1:, :], next_input), dim=1)

    predictions = scaler.inverse_transform(np.array(predictions_scaled).reshape(-1, 1)).flatten()
    print("predictions",predictions)
    last_date = pd.to_datetime(data['Date'].iloc[-1])
    future_dates = [last_date + timedelta(days=i) for i in range(1, n_days + 1)]

    prediction_df = pd.DataFrame({'Date': future_dates, 'Predicted Price': predictions})

    historical_returns = data['Close'].pct_change().dropna()
    volatility = historical_returns.std() if not historical_returns.empty else 0.01

    error_std_growth = volatility * np.sqrt(np.arange(1, n_days + 1))
    prediction_df['Upper CI'] = predictions * (1 + 1.96 * error_std_growth)
    prediction_df['Lower CI'] = predictions * (1 - 1.96 * error_std_growth)

    return prediction_df



# --- Streamlit Sidebar Controls ---
with st.sidebar:
    st.image("logo2.png", use_container_width=True)
    st.markdown("Dashboard Controls")
    ticker = st.text_input("Stock Ticker", st.session_state.get('last_ticker', "AMZN"), disabled=True).upper()
    model_type = st.selectbox(
        "Prediction Model",
        ("Bi-Directional LSTM", "Gated Recurrent Unit"),
        key="model_choice",
        help="Select the neural network architecture for prediction."
    )

    prediction_days = st.slider("Prediction Horizon (Days)", 7, 21, st.session_state.get('last_prediction_days', 7))

    if st.button("21 days ahead of the market", use_container_width=True, disabled=True):
        st.session_state.run_button_clicked = True
        st.session_state.loading = True
        st.session_state.last_ticker = ticker
        st.session_state.last_prediction_days = prediction_days
        st.session_state.error = None
        print("Generate Dashboard button clicked. Loading state set to True.")
        st.rerun()
    # Check if model or prediction days have changed
    if (
        ticker != st.session_state.get('last_ticker', '') or
        model_type != st.session_state.get('last_model_type', '') or
        prediction_days != st.session_state.get('last_prediction_days', 7)
    ):
        st.session_state.run_button_clicked = True
        st.session_state.loading = True
        st.session_state.last_ticker = ticker
        st.session_state.last_model_type = model_type
        st.session_state.last_prediction_days = prediction_days
# --- Main Application Logic ---
if st.session_state.run_button_clicked:
        model = MODELS[model_type]
        
        
        print(f"Inside main logic block. Current loading state: {st.session_state.loading}")
        try:
            if os.path.exists("AMZN_data.csv"):
                st.session_state.data = pd.read_csv("AMZN_data.csv")
            else:
                st.session_state.data = get_stock_data(ticker)
            
            
            if st.session_state.data is None:
                st.session_state.error = f"Could not fetch data for ticker '{ticker}'. It may be an invalid symbol or network issue."
            else:
                model_path = "best_bilstm_model.pth" if model_type == "Bi-Directional LSTM" else "best_gru_model.pth"
                st.session_state.predictions = predict_with_model(
                st.session_state.data, prediction_days, model_path=None, model_type=model_type, model=model
            )
                print("model",model)
                print("data", st.session_state.data)
                st.session_state.error = None

        except FileNotFoundError as e:
           st.session_state.error = str(e)
           print(f"Caught FileNotFoundError: {e}")
        except ValueError as e:
            st.session_state.error = str(e)
            print(f"Caught ValueError: {e}")
        except Exception as e:
            st.session_state.error = f"An unexpected error occurred: {str(e)}"
            print(f"Caught general Exception: {e}")
    
        st.session_state.loading = False
        st.session_state.run_button_clicked = False
        print(f"Processing complete. Loading state set to False. Error: {st.session_state.error}")
        st.rerun()

# --- Data Preparation for Front-End ---
historical_data_json = 'null'
prediction_data_json = 'null'
is_loading_js = str(st.session_state.get('loading', False)).lower()
error_message_js = 'null'

if st.session_state.get('error'):
    error_message_js = f"'{st.session_state.error}'" # Pass error to JS

if st.session_state.data is not None and st.session_state.get('error') is None:
    historical_data_json = st.session_state.data.to_json(orient='split', date_format='iso')
    prediction_data_json = st.session_state.predictions.to_json(orient='split', date_format='iso')

# --- HTML Front-End ---
html_code = f"""
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Stock Intelligence Dashboard</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
    <style>
        body {{ font-family: 'time new roman'; background-color: #f1f5f9;scrollbar-width: 2px !important; scrollbar-color: rgba(100, 100, 100, 0.4) transparent;}}
        .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; }}
        .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); }}
        .positive {{ color: #10B981; }}
        .negative {{ color: #EF4444; }}
        .neutral {{ color: #64748b; }}
        ::-webkit-scrollbar {{
            width: 6px;
        }}

        ::-webkit-scrollbar-thumb {{
            background-color: rgba(100, 100, 100, 0.4);
            border-radius: 3px;
        }}

        ::-webkit-scrollbar-track {{
            background: transparent;
        }}
        #predictionTable table {{ width: 100%; border-collapse: collapse; }}
        #predictionTable th, #predictionTable td {{ padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #e2e8f0; }}
        #predictionTable th {{ background-color: #f8fafc; font-weight: 600; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; }}
        #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; }}
        .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; }}
        @keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }}
        .hidden {{ display: none !important; }}
        .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; }}
    </style>
</head>
<body class="antialiased text-slate-800">


    <main id="content-wrapper">
        <header class="bg-white/80 backdrop-blur-lg sticky top-0 z-50 border-b border-slate-200">
            <div class="max-w-8xl mx-auto px-4 sm:px-6 lg:px-8">
                <div class="flex items-center justify-between h-16">
                    <div class="flex items-center">
                        <i class="fas fa-chart-line text-2xl text-orange-400"></i>
                        <h1 id="dashboard-title" class="text-xl font-bold text-slate-900 ml-3">{ticker} Intelligence Dashboard</h1>
                    </div>
                    <div class="text-sm text-slate-500 flex items-center">
                    <div id="status-message" class="text-center text-sm text-slate-500 mt-4 hidden">Loading updated data...</div>
                        <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>&nbsp model
                    </div>
                </div>
            </div>
        </header>

        <div class="p-4 sm:p-6 lg:p-8">
            <div class="max-w-8xl mx-auto">
                <div id="dashboard-error-message" class="hidden error-message"></div>
                <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8" id="metrics-grid"></div>
                <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
                    <div class="lg:col-span-2 space-y-8">
                        <div class="info-card p-4 sm:p-6">
                            <canvas id="priceChart" style="height: 350px;"></canvas>
                        </div>
                        <div class="info-card p-4 sm:p-6">
                            <canvas id="volumeChart" style="height: 200px;"></canvas>
                        </div>
                        <div id="predictionDetailsContainer" class="info-card p-4 sm:p-6 hidden">
                            <h3 class="text-lg font-semibold mb-4 text-slate-800">AI Prediction Details</h3>
                            <div class="overflow-x-auto" id="predictionTable"></div>
                        </div>
                    </div>
                    <div class="lg:col-span-1 space-y-8">
                        <div class="info-card p-6">
                            <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>
                            <div id="predictionResult" class="mt-4 text-center"></div>
                        </div>
                        <div class="info-card p-6">
                            <h3 class="text-lg font-semibold mb-4 text-slate-800">Technical Summary</h3>
                            <div class="space-y-3" id="tech-summary"></div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </main>

    <script>
    document.addEventListener('DOMContentLoaded', function () {{
         const {{
        LineController,
        LineElement,
        PointElement,
        LinearScale,
        TimeScale,
        Legend,
        Tooltip,
        BarController,
        BarElement,
        CategoryScale // Although you use TimeScale for X, CategoryScale might be needed for other internal reasons or for completeness for Bar charts
    }} = Chart;

    Chart.register(
        LineController,
        LineElement,
        PointElement,
        LinearScale,
        TimeScale,
        Legend,
        Tooltip,
        BarController,
        BarElement,
        CategoryScale
    );
    console.log("JS: Chart.js components registered.");

        const historicalDataJson = {historical_data_json};
        const predictionDataJson = {prediction_data_json};
        const isLoading = {is_loading_js};
        const errorMessage = {error_message_js}; // Now receiving Python error
        
        console.log("JS: DOMContentLoaded. Initial isLoading:", isLoading, "Error:", errorMessage);

        const loadingOverlay = document.getElementById('loading-overlay');
        const contentWrapper = document.getElementById('content-wrapper');
        const metricsGridEl = document.getElementById('metrics-grid');
        const techSummaryEl = document.getElementById('tech-summary');
        const predictionResultEl = document.getElementById('predictionResult');
        const predictionDetailsContainerEl = document.getElementById('predictionDetailsContainer');
        const predictionTableEl = document.getElementById('predictionTable');
        const dashboardErrorMessageEl = document.getElementById('dashboard-error-message');
        
        let priceChart;
        let volumeChart;

        function parseData(jsonData) {{
            try {{
                if (!jsonData || !jsonData.columns) return null;
                return {{
                    dates: jsonData.data.map(row => new Date(row[jsonData.columns.indexOf('Date')])),
                    prices: jsonData.data.map(row => row[jsonData.columns.indexOf('Close')]),
                    volumes: jsonData.data.map(row => row[jsonData.columns.indexOf('Volume')]),
                    highs: jsonData.data.map(row => row[jsonData.columns.indexOf('High')]),
                }};
            }} catch (e) {{
                console.error("JS: Error parsing historical data:", e);
                return null;
            }}
        }}

        function parsePredictions(jsonData) {{
             try {{
                if (!jsonData || !jsonData.columns) return [];
                return jsonData.data.map(row => ({{
                    x: new Date(row[jsonData.columns.indexOf('Date')]),
                    y: row[jsonData.columns.indexOf('Predicted Price')],
                    upperCI: row[jsonData.columns.indexOf('Upper CI')],
                    lowerCI: row[jsonData.columns.indexOf('Lower CI')]
                }}));
             }} catch (e) {{
                console.error("JS: Error parsing prediction data:", e);
                return [];
             }}
        }}
        
        function displayMetric(elementId, value, prefix = '', suffix = '', decimals = 0) {{
            const el = document.getElementById(elementId);
            if (el) {{
                el.textContent = prefix + value.toLocaleString(undefined, {{ minimumFractionDigits: decimals, maximumFractionDigits: decimals }}) + suffix;
            }}
        }}

        function updateMetrics(data) {{
            if (!data || data.prices.length < 2) {{
                metricsGridEl.innerHTML = `<div class="col-span-full text-center text-slate-500 p-4">Not enough historical data to display metrics.</div>`;
                return;
            }}
            const currentPrice = data.prices[data.prices.length - 1];
            const prevPrice = data.prices[data.prices.length - 2];
            const change = currentPrice - prevPrice;
            const changePct = (change / prevPrice) * 100;
            const volume = data.volumes[data.volumes.length - 1];
            const sharesOutstanding = 10.33 * 1e9; // Example value
            const marketCap = currentPrice * sharesOutstanding;

            const metrics = [
                {{ 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 }},
                {{ id: 'market-cap', title: 'Market Cap', value: marketCap, change: 'USD', status: 'neutral', icon: 'fa-building', prefix: '$', suffix: '', decimals: 2, isCurrency: true }},
                {{ id: 'volume', title: 'Daily Volume', value: volume, change: 'Shares Traded', status: 'neutral', icon: 'fa-chart-bar', suffix: '', decimals: 0 }},
                {{ 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 }},
            ];
            
            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('');
            
            metrics.forEach(metric => {{
                let displayValue = metric.value;
                let displaySuffix = metric.suffix;
                let displayDecimals = metric.decimals;

                if (metric.isCurrency) {{
                    if (metric.value >= 1e12) {{
                        displayValue = metric.value / 1e12;
                        displaySuffix = 'T';
                        displayDecimals = 2;
                    }} else if (metric.value >= 1e9) {{
                        displayValue = metric.value / 1e9;
                        displaySuffix = 'B';
                        displayDecimals = 2;
                    }} else if (metric.value >= 1e6) {{
                        displayValue = metric.value / 1e6;
                        displaySuffix = 'M';
                        displayDecimals = 2;
                    }}
                }}
                
                if (metric.id === 'volume') {{
                    if (metric.value >= 1e9) {{
                        displayValue = metric.value / 1e9;
                        displaySuffix = 'B';
                        displayDecimals = 2;
                    }} else if (metric.value >= 1e6) {{
                        displayValue = metric.value / 1e6;
                        displaySuffix = 'M';
                        displayDecimals = 2;
                    }} else if (metric.value >= 1e3) {{
                        displayValue = metric.value / 1e3;
                        displaySuffix = 'K';
                        displayDecimals = 2;
                    }}
                }}

                displayMetric(metric.id, displayValue, metric.prefix || '', displaySuffix, displayDecimals);
            }});
        }}
        
        function updateTechSummary(data) {{
            if (!data || data.prices.length < 50) {{ // Need enough data for 50-day SMA
                techSummaryEl.innerHTML = '<p class="text-sm text-slate-500">Not enough data for full technical analysis (min 50 days required).</p>';
                return;
            }}
            const prices = data.prices;
            const lastPrice = prices[prices.length - 1];
            
            // Ensure slice has enough elements
            const sma20 = prices.slice(-20).length >= 20 ? prices.slice(-20).reduce((a, b) => a + b, 0) / 20 : NaN;
            const sma50 = prices.slice(-50).length >= 50 ? prices.slice(-50).reduce((a, b) => a + b, 0) / 50 : NaN;
            
            let gains = [];
            let losses = [];
            for (let i = 1; i < prices.length; i++) {{
                let diff = prices[i] - prices[i-1];
                if (diff > 0) {{
                    gains.push(diff);
                    losses.push(0);
                }} else {{
                    gains.push(0);
                    losses.push(Math.abs(diff));
                }}
            }}

            let avgGain = 0;
            let avgLoss = 0;
            if (gains.length >= 14) {{
                avgGain = gains.slice(-14).reduce((a, b) => a + b, 0) / 14;
                avgLoss = losses.slice(-14).reduce((a, b) => a + b, 0) / 14;
            }} else if (gains.length > 0) {{ 
                avgGain = gains.reduce((a, b) => a + b, 0) / gains.length;
                avgLoss = losses.reduce((a, b) => a + b, 0) / losses.length;
            }}

            let rs = (avgLoss === 0 || isNaN(avgLoss)) ? (avgGain > 0 ? Infinity : 0) : avgGain / avgLoss; 
            let rsi = 100 - (100 / (1 + rs));
            if (isNaN(rsi)) rsi = 0; 

            let rsiClass = 'neutral';
            if (rsi > 70) rsiClass = 'negative'; 
            else if (rsi < 30) rsiClass = 'positive'; 
            
            const summary = [
                {{ label: 'SMA (20 Day)', value: isNaN(sma20) ? 'N/A' : `$${{sma20.toFixed(2)}}`, status: lastPrice > sma20 ? 'positive' : (isNaN(sma20) ? 'neutral' : 'negative') }},
                {{ label: 'SMA (50 Day)', value: isNaN(sma50) ? 'N/A' : `$${{sma50.toFixed(2)}}`, status: lastPrice > sma50 ? 'positive' : (isNaN(sma50) ? 'neutral' : 'negative') }},
                {{ label: 'RSI (14 Day)', value: rsi.toFixed(1), status: rsiClass }}
            ];
            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('');
        }}
        
        function renderCharts(data, predictions) {{
            // Render Price Chart
            const priceCtx = document.getElementById('priceChart').getContext('2d');
            if (priceChart) priceChart.destroy();
            
            const priceDatasets = [
                {{ 
                    label: 'Historical Price', 
                    data: data.dates.map((d, i) => ({{x: d, y: data.prices[i]}})), 
                    borderColor: '#3b82f6', 
                    backgroundColor: 'rgba(59, 130, 246, 0.1)', 
                    borderWidth: 2, 
                    pointRadius: 0, 
                    fill: true, 
                    tension: 0.3
                }}
            ];
            
            if (predictions.length > 0) {{
                priceDatasets.push({{ 
                    label: 'AI Prediction', 
                    data: predictions, 
                    borderColor: '#10b981', 
                    borderWidth: 2, 
                    pointRadius: 2, 
                    borderDash: [5, 5], 
                    fill: false, 
                    tension: 0.3
                }});
                
                // Add confidence interval
                const confidenceData = [
                    ...predictions.map(p => ({{x: p.x, y: p.lowerCI}})),
                    ...predictions.map(p => ({{x: p.x, y: p.upperCI}})).reverse()
                ];
                
                priceDatasets.push({{ 
                    label: '95% Confidence', 
                    data: confidenceData, 
                    fill: '1', 
                    backgroundColor: 'rgba(234, 179, 8, 0.2)', 
                    borderColor: 'transparent', 
                    pointRadius: 0
                }});
            }}
            
            priceChart = new Chart(priceCtx, {{ 
                type: 'line', // Explicitly define type
                data: {{ datasets: priceDatasets }}, 
                options: {{ 
                    responsive: true, 
                    maintainAspectRatio: false, 
                    scales: {{ 
                        x: {{ 
                            type: 'time', 
                            time: {{ 
                                unit: 'month',
                                tooltipFormat: 'MMM d, yyyy' 
                            }}, 
                            grid: {{ display: false }} 
                        }}, 
                        y: {{ 
                            title: {{ display: true, text: 'Price (USD)' }}, 
                            grid: {{ color: '#f1f5f9' }} 
                        }}
                    }}, 
                    plugins: {{ 
                        legend: {{ 
                            display: true, 
                            position: 'top', 
                            align: 'end' 
                        }},
                        tooltip: {{
                            mode: 'index',
                            intersect: false,
                            callbacks: {{
                                title: function(context) {{
                                    return context[0].label;
                                }},
                                label: function(context) {{
                                    let label = context.dataset.label || '';
                                    if (label) label += ': ';
                                    label += '$' + context.parsed.y.toFixed(2);
                                    if (context.dataset.label === 'AI Prediction' && predictions.length > 0) {{
                                        const predictionPoint = predictions.find(p => p.x.getTime() === context.parsed.x);
                                        if (predictionPoint) {{
                                            label += ` (CI: $${{predictionPoint.lowerCI.toFixed(2)}} - $${{predictionPoint.upperCI.toFixed(2)}})`;
                                        }}
                                    }}
                                    return label;
                                }}
                            }}
                        }}
                    }} 
                }} 
            }});
            
            // Render Volume Chart
            const volumeCtx = document.getElementById('volumeChart').getContext('2d');
            if (volumeChart) volumeChart.destroy();
            
            volumeChart = new Chart(volumeCtx, {{ 
                type: 'bar', // Explicitly define type
                data: {{ 
                    datasets: [{{
                        label: 'Volume', 
                        data: data.dates.map((d, i) => ({{x: d, y: data.volumes[i]}})), 
                        backgroundColor: '#e2e8f0', 
                        borderColor: '#cbd5e1',
                        borderWidth: 1
                    }}]
                }}, 
                options: {{ 
                    responsive: true, 
                    maintainAspectRatio: false, 
                    scales: {{ 
                        x: {{ 
                            type: 'time', 
                            time: {{ 
                                unit: 'month'
                            }}, 
                            grid: {{ display: false }} 
                        }}, 
                        y: {{ 
                            title: {{ display: true, text: 'Volume' }},
                            grid: {{ color: '#f1f5f9' }}, 
                            ticks: {{
                                callback: function(value) {{
                                    if (value >= 1e9) return (value / 1e9).toFixed(0) + 'B';
                                    if (value >= 1e6) return (value / 1e6).toFixed(0) + 'M';
                                    if (value >= 1e3) return (value / 1e3).toFixed(0) + 'K';
                                    return value;
                                }}
                            }}
                        }} 
                    }},
                    plugins: {{
                        legend: {{
                            display: false
                        }},
                        tooltip: {{
                            callbacks: {{
                                label: function(context) {{
                                    let label = context.dataset.label || '';
                                    if (label) label += ': ';
                                    let value = context.parsed.y;
                                    if (value >= 1e9) label += (value / 1e9).toLocaleString(undefined, {{maximumFractionDigits: 1}}) + 'B';
                                    else if (value >= 1e6) label += (value / 1e6).toLocaleString(undefined, {{maximumFractionDigits: 1}}) + 'M';
                                    else if (value >= 1e3) label += (value / 1e3).toLocaleString(undefined, {{maximumFractionDigits: 1}}) + 'K';
                                    else label += value.toLocaleString();
                                    return label;
                                }}
                            }}
                        }}
                    }}
                }} 
            }});
        }}
        
        function displayPredictions(data, predictions) {{
            if (!data || predictions.length === 0) {{
                predictionDetailsContainerEl.classList.add('hidden');
                predictionResultEl.innerHTML = '<p class="text-sm text-slate-500">No predictions available or not enough data for prediction.</p>';
                return;
            }}
            predictionDetailsContainerEl.classList.remove('hidden');
            const lastHistoricalPrice = data.prices[data.prices.length - 1];
            const finalPredictedPrice = predictions[predictions.length - 1].y;

            const changeOverall = finalPredictedPrice - lastHistoricalPrice;
            const changePctOverall = (changeOverall / lastHistoricalPrice) * 100;
            const statusClass = changeOverall >= 0 ? 'positive' : 'negative';

            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>`;
            
            const tableRows = predictions.map(p => `
                <tr>
                    <td>${{new Date(p.x).toLocaleDateString()}}</td>
                    <td class="font-semibold">$${{p.y.toFixed(2)}}</td>
                    <td>$${{p.lowerCI.toFixed(2)}} - $${{p.upperCI.toFixed(2)}}</td>
                </tr>
            `).join('');
            predictionTableEl.innerHTML = `
                <table>
                    <thead>
                        <tr>
                            <th>Date</th>
                            <th>Predicted Price</th>
                            <th>95% Confidence Interval</th>
                        </tr>
                    </thead>
                    <tbody>${{tableRows}}</tbody>
                </table>
            `;
        }}

        function loadDashboard() {{
            console.log("JS: loadDashboard() called. Current isLoading:", isLoading, "Error:", errorMessage);
            const statusMessageEl = document.getElementById('status-message');
           
            // Handle loading overlay visibility
            
            if (isLoading === 'true') {{ 
                statusMessageEl.classList.remove('hidden');
                dashboardErrorMessageEl.classList.add('hidden'); // Hide any previous error
                return; // Stop execution, let Streamlit re-run and call again when done
            }} else {{
                console.log("JS: in() called. Current isLoading:", isLoading, "Error:", errorMessage);
                statusMessageEl.classList.add('hidden');

            }}

            // Handle errors
            if (errorMessage && errorMessage !== 'null') {{
                dashboardErrorMessageEl.textContent = "Error: " + errorMessage;
                dashboardErrorMessageEl.classList.remove('hidden');
                // Clear existing charts if any, and other content
                if (priceChart) priceChart.destroy();
                if (volumeChart) volumeChart.destroy();
                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>`;
                predictionDetailsContainerEl.classList.add('hidden');
                predictionResultEl.innerHTML = '<p class="text-sm text-slate-500">No results due to error.</p>';
                techSummaryEl.innerHTML = '<p class="text-sm text-slate-500">No technical summary due to error.</p>';
                return;
            }} else {{
                dashboardErrorMessageEl.classList.add('hidden'); // Ensure error message is hidden if no error
            }}

            // If no error and not loading, proceed to render dashboard
            const historicalData = parseData(historicalDataJson);
            const predictionData = parsePredictions(predictionDataJson);

            if (!historicalData) {{
                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>`;
                predictionDetailsContainerEl.classList.add('hidden');
                predictionResultEl.innerHTML = '<p class="text-sm text-slate-500">No data loaded yet.</p>';
                techSummaryEl.innerHTML = '<p class="text-sm text-slate-500">No data for technical summary.</p>';
                if (priceChart) priceChart.destroy();
                if (volumeChart) volumeChart.destroy();
                console.log("JS: No historical data available to render dashboard.");
                return;
            }}
            
            updateMetrics(historicalData);
            updateTechSummary(historicalData);
            renderCharts(historicalData, predictionData); // Renamed to plural as it handles both
            displayPredictions(historicalData, predictionData);
            console.log("JS: Dashboard loaded successfully.");
        }}

        loadDashboard(); // Initial call when DOM is ready
    }});
    </script>
</body>
</html>
"""

# --- Embed HTML Component in Streamlit ---
# No need for st.error here, as the JS will handle displaying the error in the HTML component
components.html(html_code, height=1200, scrolling=True)