File size: 21,812 Bytes
4524156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f3d1206
 
 
 
 
 
 
4524156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4c9d2e9
4524156
4c9d2e9
4524156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e1dfca
4524156
 
 
9e1dfca
a770679
4524156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f3d1206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4524156
f3d1206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4524156
 
 
 
 
 
 
 
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
import streamlit as st
import numpy as np
import pandas as pd
import yfinance as yf
import plotly.graph_objects as go
from sklearn.preprocessing import MinMaxScaler
from keras.models import Sequential
from keras.layers import LSTM, Dense, Dropout
import warnings
warnings.filterwarnings("ignore")
import datetime

# Define the StockPredictor class with the plot_predictions function exactly as in your code
class StockPredictor:
    def __init__(self, ticker, start_date, end_date, look_back=60):
        self.ticker = ticker
        self.start_date = start_date
        self.end_date = end_date
        self.look_back = look_back
        self.data = None
        self.scaler = MinMaxScaler(feature_range=(0, 1))
        self.model = None
        self.X, self.Y = None, None
        self.predictions = None
        self.predicted_dates = None
        self.features = None  # To store the list of features

    def load_data(self):
        self.data = yf.download(self.ticker, start=self.start_date, end=self.end_date, auto_adjust=False)
        if isinstance(self.data.columns, pd.MultiIndex):
            self.data.columns = self.data.columns.get_level_values(0)
        if self.data.empty:
            raise ValueError(f"No data retrieved for {self.ticker}")
        if len(self.data) < self.look_back + 1:  # Ensure enough data for look_back period
            raise ValueError(f"Insufficient data points for {self.ticker}. Need at least {self.look_back + 1} days.")
        print(f'Data loaded for {self.ticker} from {self.start_date} to {self.end_date}')

    def calculate_wma(self, prices, window):
        weights = np.arange(1, window + 1)
        return prices.rolling(window).apply(lambda x: np.dot(x, weights) / weights.sum(), raw=True)

    def calculate_hull_moving_average(self, close_prices, window=14):
        """Function to calculate the Hull Moving Average (HMA)."""
        half_length = int(window / 2)
        sqrt_length = int(np.sqrt(window))

        wma_half = self.calculate_wma(close_prices, half_length)
        wma_full = self.calculate_wma(close_prices, window)
        raw_hma = 2 * wma_half - wma_full
        hma = self.calculate_wma(raw_hma, sqrt_length)
        return hma

    def calculate_rolling_zscore(self, close_prices, window=20):
        """Function to calculate rolling Z-Score."""
        rolling_mean = close_prices.rolling(window=window).mean()
        rolling_std = close_prices.rolling(window=window).std()
        z_score = (close_prices - rolling_mean) / rolling_std
        return z_score

    def calculate_technical_indicators(self):
        """Calculate technical indicators for the historical data."""
        # Hull Moving Average
        self.data['HMA'] = self.calculate_hull_moving_average(self.data['Close'], window=14)

        # Bollinger Bands
        rolling_std = self.data['Close'].rolling(window=20).std()
        self.data['BB_upper'] = self.data['HMA'] + (rolling_std * 2)
        self.data['BB_lower'] = self.data['HMA'] - (rolling_std * 2)

        # Relative Strength Index (RSI)
        delta = self.data['Close'].diff(1)
        gain = delta.where(delta > 0, 0)
        loss = -delta.where(delta < 0, 0)
        avg_gain = gain.rolling(window=14).mean()
        avg_loss = loss.rolling(window=14).mean()
        rs = avg_gain / avg_loss
        self.data['RSI'] = 100 - (100 / (1 + rs))

        # Rolling Z-Score
        self.data['Z_Score'] = self.calculate_rolling_zscore(self.data['Close'], window=20)

        # Fill missing values
        self.data.fillna(method='bfill', inplace=True)

    def prepare_data(self):
        self.calculate_technical_indicators()

        # Select features
        features = ['Close', 'HMA', 'BB_upper', 'BB_lower', 'RSI', 'Z_Score']
        self.features = features
        data = self.data[features]
        data_scaled = self.scaler.fit_transform(data)

        X, Y = [], []
        for i in range(self.look_back, len(data_scaled)):
            X.append(data_scaled[i - self.look_back:i])
            Y.append(data_scaled[i, 0])  # Assuming 'Close' is the target
        self.X, self.Y = np.array(X), np.array(Y)
        print('Data prepared for training.')

    def build_model(self):
        self.model = Sequential()
        self.model.add(LSTM(units=300, return_sequences=True, input_shape=(self.X.shape[1], self.X.shape[2])))
        #self.model.add(Dropout(0.2))
        self.model.add(LSTM(units=300))
        #self.model.add(Dropout(0.2))
        self.model.add(Dense(1))
        self.model.compile(loss='mean_squared_error', optimizer='adam')
        print('Model built and compiled.')

    def train_model(self, epochs=10, batch_size=64):
        self.model.fit(self.X, self.Y, epochs=epochs, batch_size=batch_size, verbose=1)
        print('Model trained.')

    def make_predictions(self):
        self.predictions = self.model.predict(self.X)
        # Inverse transform predictions
        # We need to inverse transform the Close price
        # Since we have multiple features, we need to create a full array
        predictions_extended = np.zeros((self.predictions.shape[0], len(self.features)))
        predictions_extended[:, 0] = self.predictions[:, 0]
        predictions_inverse = self.scaler.inverse_transform(predictions_extended)
        self.predictions = predictions_inverse[:, 0]

        # Similarly for Y
        Y_extended = np.zeros((self.Y.shape[0], len(self.features)))
        Y_extended[:, 0] = self.Y
        Y_inverse = self.scaler.inverse_transform(Y_extended)
        self.Y = Y_inverse[:, 0]
        # Store corresponding dates
        self.predicted_dates = self.data.index[self.look_back:]
        print('Predictions made and stored.')

    def forecast_future(self, days=5, n_mc=100):
        # Start with the last 'look_back' periods from data
        last_sequence = self.data[self.features].iloc[-self.look_back:].copy()
        future_predictions = []
        future_std = []

        for _ in range(days):
            # Scale the input data
            last_sequence_scaled = self.scaler.transform(last_sequence)
            # Reshape to (1, look_back, num_features)
            input_data = last_sequence_scaled.reshape(1, self.look_back, len(self.features))

            # Perform N_mc stochastic forward passes
            predictions = []
            for _ in range(n_mc):
                prediction = self.model(input_data, training=True)
                predictions.append(prediction.numpy()[0, 0])

            predictions = np.array(predictions)
            mean_prediction = predictions.mean()
            std_prediction = predictions.std()
            future_std.append(std_prediction)

            # Create an array to inverse transform
            prediction_extended = np.zeros((1, len(self.features)))
            prediction_extended[0, 0] = mean_prediction
            predicted_price = self.scaler.inverse_transform(prediction_extended)[0, 0]
            future_predictions.append(predicted_price)

            # Prepare the next input
            # Create a new row with the predicted price and updated technical indicators
            new_row = {}
            new_row['Close'] = predicted_price

            # Append the new predicted price to the sequence
            # Use pd.concat instead of append
            new_row_df = pd.DataFrame([new_row], index=[last_sequence.index[-1] + pd.Timedelta(days=1)])
            last_sequence = pd.concat([last_sequence, new_row_df])

            # Recalculate technical indicators on the updated last_sequence
            last_close_prices = last_sequence['Close']
            # HMA
            hma_series = self.calculate_hull_moving_average(last_close_prices)
            last_sequence['HMA'] = hma_series
            # Bollinger Bands
            rolling_std = last_close_prices.rolling(window=20).std()
            last_sequence['BB_upper'] = last_sequence['HMA'] + (rolling_std * 2)
            last_sequence['BB_lower'] = last_sequence['HMA'] - (rolling_std * 2)
            # RSI
            delta = last_close_prices.diff(1)
            gain = delta.where(delta > 0, 0)
            loss = -delta.where(delta < 0, 0)
            avg_gain = gain.rolling(window=14).mean()
            avg_loss = loss.rolling(window=14).mean()
            rs = avg_gain / avg_loss
            last_sequence['RSI'] = 100 - (100 / (1 + rs))
            # Z_Score
            z_score_series = self.calculate_rolling_zscore(last_close_prices)
            last_sequence['Z_Score'] = z_score_series

            # Fill any missing values
            last_sequence.fillna(method='bfill', inplace=True)

            # Keep only the last 'look_back' periods
            last_sequence = last_sequence.iloc[-self.look_back:]

        # Generate future dates
        future_dates = pd.date_range(start=self.data.index[-1] + pd.Timedelta(days=1), periods=days, freq='B')

        # Calculate confidence intervals
        future_std = np.array(future_std).reshape(-1, 1)
        # Scale the standard deviations appropriately
        future_std_scaled = future_std * (self.scaler.data_max_[0] - self.scaler.data_min_[0])

        # Create a DataFrame with future dates and predicted prices
        future_df = pd.DataFrame({
            'Date': future_dates,
            'Predicted_Close': future_predictions,
            'Std_Dev': future_std_scaled.flatten()
        })

        # Calculate 68% and 95% confidence intervals
        future_df['CI_68_Lower'] = future_df['Predicted_Close'] - future_df['Std_Dev']
        future_df['CI_68_Upper'] = future_df['Predicted_Close'] + future_df['Std_Dev']
        future_df['CI_95_Lower'] = future_df['Predicted_Close'] - 2 * future_df['Std_Dev']
        future_df['CI_95_Upper'] = future_df['Predicted_Close'] + 2 * future_df['Std_Dev']

        print('Future predictions made and stored with confidence intervals.')
        return future_df

    def plot_predictions(self, future_predictions_df):
        # Create plotly figure
        fig = go.Figure()

        # Plot actual prices
        fig.add_trace(go.Scatter(
            x=self.data.index,
            y=self.data['Close'],
            mode='lines',
            name='Actual Price'
        ))

        # Plot predicted prices for historical data
        fig.add_trace(go.Scatter(
            x=self.predicted_dates,
            y=self.predictions,
            mode='lines',
            name='Predicted Price'
        ))

        # Plot future predictions
        fig.add_trace(go.Scatter(
            x=future_predictions_df['Date'],
            y=future_predictions_df['Predicted_Close'],
            mode='lines',
            name=f'Future {len(future_predictions_df)} Days Prediction',
            line=dict(dash='dash')
        ))

        # Plot 95% confidence interval as shaded area
        fig.add_trace(go.Scatter(
            x=future_predictions_df['Date'].tolist() + future_predictions_df['Date'][::-1].tolist(),
            y=future_predictions_df['CI_95_Upper'].tolist() + future_predictions_df['CI_95_Lower'][::-1].tolist(),
            fill='toself',
            fillcolor='rgba(255, 192, 0, 0.2)',
            line=dict(color='rgba(255,255,255,0)'),
            hoverinfo='skip',
            showlegend=True,
            name='95% Confidence Interval'
        ))

        # Plot 68% confidence interval as shaded area (on top of 95%)
        fig.add_trace(go.Scatter(
            x=future_predictions_df['Date'].tolist() + future_predictions_df['Date'][::-1].tolist(),
            y=future_predictions_df['CI_68_Upper'].tolist() + future_predictions_df['CI_68_Lower'][::-1].tolist(),
            fill='toself',
            fillcolor='rgba(0, 100, 80, 0.2)',
            line=dict(color='rgba(255,255,255,0)'),
            hoverinfo='skip',
            showlegend=True,
            name='68% Confidence Interval'
        ))

        # Determine if the final prediction is bullish or bearish
        last_actual_price = self.data['Close'].iloc[-1]
        final_predicted_price = future_predictions_df['Predicted_Close'].iloc[-1]
        bullish = final_predicted_price > last_actual_price

        # Add a triangle marker at the end of the actual price plot, colored based on bullish/bearish
        fig.add_trace(go.Scatter(
            x=[self.data.index[-1]],
            y=[last_actual_price],
            mode='markers',
            marker=dict(
                color='green' if bullish else 'red',
                size=12,
                symbol='triangle-up' if bullish else 'triangle-down'
            ),
            name='Bullish' if bullish else 'Bearish'
        ))

        # Add labels for the final confidence intervals and final price, placed next to the lines
        annotations = []

        # Set x offset for labels to avoid overlapping with the data points
        x_offset = 40  # Adjust as needed

        # Final predicted price annotation
        annotations.append(dict(
            x=future_predictions_df['Date'].iloc[-1],
            y=final_predicted_price,
            xref='x', yref='y',
            text=f"Final Predicted Price: ${final_predicted_price:.2f}",
            showarrow=False,
            xanchor='left',
            xshift=x_offset
        ))

        # 68% Confidence Interval Upper Bound Annotation
        annotations.append(dict(
            x=future_predictions_df['Date'].iloc[-1],
            y=future_predictions_df['CI_68_Upper'].iloc[-1],
            xref='x', yref='y',
            text=f"68% CI Upper: ${future_predictions_df['CI_68_Upper'].iloc[-1]:.2f}",
            showarrow=False,
            xanchor='left',
            xshift=x_offset
        ))

        # 68% Confidence Interval Lower Bound Annotation
        annotations.append(dict(
            x=future_predictions_df['Date'].iloc[-1],
            y=future_predictions_df['CI_68_Lower'].iloc[-1],
            xref='x', yref='y',
            text=f"68% CI Lower: ${future_predictions_df['CI_68_Lower'].iloc[-1]:.2f}",
            showarrow=False,
            xanchor='left',
            xshift=x_offset
        ))

        # 95% Confidence Interval Upper Bound Annotation
        annotations.append(dict(
            x=future_predictions_df['Date'].iloc[-1],
            y=future_predictions_df['CI_95_Upper'].iloc[-1],
            xref='x', yref='y',
            text=f"95% CI Upper: ${future_predictions_df['CI_95_Upper'].iloc[-1]:.2f}",
            showarrow=False,
            xanchor='left',
            xshift=x_offset
        ))

        # 95% Confidence Interval Lower Bound Annotation
        annotations.append(dict(
            x=future_predictions_df['Date'].iloc[-1],
            y=future_predictions_df['CI_95_Lower'].iloc[-1],
            xref='x', yref='y',
            text=f"95% CI Lower: ${future_predictions_df['CI_95_Lower'].iloc[-1]:.2f}",
            showarrow=False,
            xanchor='left',
            xshift=x_offset
        ))

        # Update layout with annotations
        fig.update_layout(
            title=f'Stock Price Prediction for {self.ticker}',
            xaxis_title='Date',
            yaxis_title='Price',
            hovermode='x unified',
            annotations=annotations
        )

        # Return the figure
        return fig

# Streamlit App
st.set_page_config(layout="wide")
st.title("Deep Learning Asset Price Forecasting")

# Include a short description in the main body of the app
st.markdown("""
This tool forecasts future stock prices and cryptocurrency pairs using a deep neural network with a large number of proprietary historical external variables. 
The model provides both 68% and 95% confidence intervals to represent uncertainty. Zoom into the forecast area of the plot for a clearer view.
""")

with st.expander("Additional Trading Tools and Analysis", expanded=False):
    st.markdown("""
    To implement the forecasts into trading, users are advised to further combine the predictions with the following tools to assess the latest asset price patterns:
    - [Expected Stock Price Movement Using Volatility Multipliers](https://entreprenerdly.com/expected-stock-price-movement-using-volatility-multipliers/)
    - [Future Stock Price Movements with Monte Carlo Simulations](https://entreprenerdly.com/future-stock-price-movements-with-monte-carlo-simulations/)
    - [Technical Analysis with Trading Indicators](https://entreprenerdly.com/technical-analysis/)

    Additionally, comparing the results with Entreprenerdly's algorithmic indicators can provide further insights for making the final decision.
    """)

# Sidebar
st.sidebar.title("Input Parameters")

with st.sidebar.expander("How to Use", expanded=False):
    st.write("""
    **Instructions:**
    - Enter the stock ticker symbol or crypto pair you want to forecast (e.g., AAPL or BTC-USD).
    - Select the number of days ahead you want to forecast using the slider. We recommend choosing a small number.
    - Click on "Run Model" to generate the predictions and view the results.
    """)

with st.sidebar.expander("Input Parameters", expanded=True):
    ticker = st.text_input(
        "Stock Ticker",
        value="AAPL",
        help="Enter the stock ticker symbol or Cryptocurrency pair you want to forecast (e.g., AAPL, BTC-USD)."
    )
    forecast_days = st.slider(
        "Forecast Horizon (Days)",
        min_value=1,
        max_value=10,
        value=5,
        help="Select the number of future days to forecast. We recommend choosing a smaller number, as longer horizons generally result in higher prediction errors."
    )

# Set end date to today's date plus one in the backend
end_date = (datetime.date.today() + datetime.timedelta(days=1)).strftime("%Y-%m-%d")
start_date = '2015-01-01'  # Fixed start date

# Run button
run_button = st.sidebar.button("Run Model")

if run_button:
    try:
        # Progress bar
        progress_bar = st.progress(0)
        status_text = st.empty()

        # Instantiate and run the predictor
        predictor = StockPredictor(
            ticker,
            start_date,
            end_date,
            look_back=60
        )
        status_text.text("Loading data...")
        predictor.load_data()
        progress_bar.progress(10)

        status_text.text("Preparing data...")
        predictor.prepare_data()
        progress_bar.progress(30)

        status_text.text("Building model...")
        predictor.build_model()
        progress_bar.progress(50)

        status_text.text("Training model...This may take a few minutes")
        predictor.train_model(epochs=10, batch_size=16)
        progress_bar.progress(80)

        status_text.text("Making predictions...")
        predictor.make_predictions()
        progress_bar.progress(90)

        status_text.text("Forecasting future prices...")
        future_predictions_df = predictor.forecast_future(days=forecast_days, n_mc=100)
        progress_bar.progress(100)

        status_text.text("Generating plot...")
        fig = predictor.plot_predictions(future_predictions_df)

        # Clear the progress bar and status text
        progress_bar.empty()
        status_text.empty()

        # Display the plot
        st.plotly_chart(fig)

        # Include a short interpretation of the results
        last_actual_price = predictor.data['Close'].iloc[-1]
        final_predicted_price = future_predictions_df['Predicted_Close'].iloc[-1]
        bullish = final_predicted_price > last_actual_price

        # Interpretation of results
        ci_68_lower = future_predictions_df['CI_68_Lower'].iloc[-1]
        ci_68_upper = future_predictions_df['CI_68_Upper'].iloc[-1]
        ci_95_lower = future_predictions_df['CI_95_Lower'].iloc[-1]
        ci_95_upper = future_predictions_df['CI_95_Upper'].iloc[-1]
        mean_prediction = future_predictions_df['Predicted_Close'].mean()
        
        st.write(f"### Final Predicted Price: ${final_predicted_price:.2f}")
        
        if bullish:
            st.success("The model predicts a **bullish** trend over the forecast horizon.")
        else:
            st.error("The model predicts a **bearish** trend over the forecast horizon.")
        
        # Build the interpretation text step by step
        interpretation_text = "**Interpretation of Results:**\n\n"
        interpretation_text += f"The model forecasts that the stock price is expected to be around **${final_predicted_price:.2f}** by the end of the forecast horizon.\n\n"
        interpretation_text += f"The **68% confidence interval** suggests that the price is likely to fall between **${ci_68_lower:.2f}** and **${ci_68_upper:.2f}**, "
        interpretation_text += f"while the broader **95% confidence interval** ranges from **${ci_95_lower:.2f}** to **${ci_95_upper:.2f}**.\n\n"
        interpretation_text += f"On average, the forecasted prices over the horizon have a mean value of **${mean_prediction:.2f}**.\n\n"
        interpretation_text += f"The predicted trend indicates a **{'bullish' if bullish else 'bearish'}** signal, suggesting the price is expected to **{'rise' if bullish else 'decline'}** "
        interpretation_text += f"compared to the last recorded price of **${last_actual_price:.2f}**.\n\n"
        interpretation_text += "The confidence intervals represent the uncertainty in the model's predictions, with wider intervals indicating higher uncertainty. "
        interpretation_text += "The shaded areas on the plot provide a visual representation of this uncertainty."
        
        st.markdown(interpretation_text)
    except Exception as e:
        st.error(f"An error occurred while running the analysis: {e}")
        progress_bar.empty()
        status_text.empty()

hide_streamlit_style = """
<style>
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
</style>
"""
st.markdown(hide_streamlit_style, unsafe_allow_html=True)