consider the code below:
import sys
import os
from PyQt5.QtWidgets import (
    QApplication, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, 
    QHBoxLayout, QTextEdit, QMessageBox, QFileDialog, QComboBox, QProgressBar, 
    QCheckBox, QListWidget
)
from PyQt5.QtGui import QFont, QPalette, QColor, QIntValidator, QRegExpValidator
from PyQt5.QtCore import Qt, QDate, QRegExp
import yfinance as yf
import pandas as pd
import numpy as np
from statsmodels.tsa.arima.model import ARIMA
from pmdarima import auto_arima
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error
from sklearn.model_selection import GridSearchCV
from xgboost import XGBRegressor
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
from textblob import TextBlob
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from tqdm import tqdm
import requests
import json
import warnings
import pandas_ta as ta  # Replaced 'ta' with 'pandas-ta' for technical indicators
import logging

# Configure logging
logging.basicConfig(
    filename='stock_prediction_app.log',
    filemode='a',
    format='%(asctime)s - %(levelname)s - %(message)s',
    level=logging.ERROR
)

warnings.filterwarnings("ignore")

class StockPredictionApp(QWidget):
    def __init__(self):
        super().__init__()
        self.init_ui()
        self.all_predictions = {}
        self.analyzer = SentimentIntensityAnalyzer()
        self.prediction_data = pd.DataFrame()

    def init_ui(self):
        # Set window properties
        self.setWindowTitle('Advanced Indian Stock Price Prediction App')
        self.setGeometry(100, 100, 1400, 900)

        # Create main layout
        main_layout = QVBoxLayout()

        # Welcome Label
        welcome_label = QLabel('Advanced Indian Stock Price Prediction App')
        welcome_label.setFont(QFont('Arial', 24, QFont.Bold))
        welcome_label.setAlignment(Qt.AlignCenter)
        welcome_label.setStyleSheet("""
            QLabel {
                color: #ffffff;
                margin: 20px;
            }
        """)
        main_layout.addWidget(welcome_label)

        # Input Fields Layout
        input_layout = QVBoxLayout()

        # Stock Symbols Input
        stock_layout = QHBoxLayout()
        stock_label = QLabel('Enter Stock Symbol(s):')
        stock_label.setStyleSheet("color: #ffffff; font-size: 14px;")
        self.stock_input = QLineEdit()
        self.stock_input.setPlaceholderText('e.g., RELIANCE.NS, TCS.NS, INFY.NS')
        stock_layout.addWidget(stock_label)
        stock_layout.addWidget(self.stock_input)
        input_layout.addLayout(stock_layout)

        # Button to Load CSV or Text File
        load_file_layout = QHBoxLayout()
        load_file_label = QLabel('Or Load from File:')
        load_file_label.setStyleSheet("color: #ffffff; font-size: 14px;")
        self.load_file_button = QPushButton('Load CSV/Text')
        self.load_file_button.setFont(QFont('Arial', 10))
        self.load_file_button.setStyleSheet("""
            QPushButton {
                background-color: #FF9800; 
                color: white;
                padding: 6px;
                border: none;
                border-radius: 5px;
            }
            QPushButton:hover {
                background-color: #e68900;
            }
        """)
        self.load_file_button.clicked.connect(self.load_file)
        load_file_layout.addWidget(load_file_label)
        load_file_layout.addWidget(self.load_file_button)
        input_layout.addLayout(load_file_layout)

        # Prediction Days Input
        days_layout = QHBoxLayout()
        days_label = QLabel('Number of Days to Predict:')
        days_label.setStyleSheet("color: #ffffff; font-size: 14px;")
        self.days_input = QLineEdit()
        self.days_input.setPlaceholderText('e.g., 5')
        # Adding QIntValidator to ensure only integers are entered
        self.days_input.setValidator(QIntValidator(1, 365, self))
        days_layout.addWidget(days_label)
        days_layout.addWidget(self.days_input)
        input_layout.addLayout(days_layout)

        # Historical Data Duration
        duration_layout = QHBoxLayout()
        duration_label = QLabel('Historical Data Duration:')
        duration_label.setStyleSheet("color: #ffffff; font-size: 14px;")
        self.duration_combo = QComboBox()
        self.duration_combo.addItems(['1 Year', '2 Years', '3 Years', '4 Years', '5 Years', '10 Years'])
        duration_layout.addWidget(duration_label)
        duration_layout.addWidget(self.duration_combo)
        input_layout.addLayout(duration_layout)

        # End Date Input for Backtesting
        end_date_layout = QHBoxLayout()
        end_date_label = QLabel('Data End Date:')
        end_date_label.setStyleSheet("color: #ffffff; font-size: 14px;")
        self.end_date_input = QLineEdit()
        self.end_date_input.setPlaceholderText('YYYY-MM-DD (e.g., 2024-09-25)')
        # Adding QRegExpValidator to ensure correct date format
        date_regex = QRegExp(r'\d{4}-\d{2}-\d{2}')
        date_validator = QRegExpValidator(date_regex, self)
        self.end_date_input.setValidator(date_validator)
        end_date_layout.addWidget(end_date_label)
        end_date_layout.addWidget(self.end_date_input)
        input_layout.addLayout(end_date_layout)

        # API Key Input
        api_key_layout = QHBoxLayout()
        api_key_label = QLabel('NewsAPI Key:')
        api_key_label.setStyleSheet("color: #ffffff; font-size: 14px;")
        self.api_key_input = QLineEdit()
        self.api_key_input.setPlaceholderText('Enter your NewsAPI.org key or leave blank to disable sentiment analysis')
        self.api_key_input.textChanged.connect(self.toggle_sentiment_checkbox)
        api_key_layout.addWidget(api_key_label)
        api_key_layout.addWidget(self.api_key_input)
        input_layout.addLayout(api_key_layout)

        # News Sentiment Analysis Checkbox
        sentiment_layout = QHBoxLayout()
        self.sentiment_checkbox = QCheckBox('Include News Sentiment Analysis')
        self.sentiment_checkbox.setStyleSheet("color: #ffffff;")
        self.sentiment_checkbox.setChecked(False)
        self.sentiment_checkbox.setEnabled(False)  # Initially disabled until API key is provided
        sentiment_layout.addWidget(self.sentiment_checkbox)
        input_layout.addLayout(sentiment_layout)

        # Choose Models to Use
        models_layout = QHBoxLayout()
        models_label = QLabel('Select Models:')
        models_label.setStyleSheet("color: #ffffff; font-size: 14px;")
        self.arima_checkbox = QCheckBox('ARIMA')
        self.arima_checkbox.setChecked(True)
        self.rf_checkbox = QCheckBox('Random Forest')
        self.rf_checkbox.setChecked(True)
        self.xgb_checkbox = QCheckBox('XGBoost')
        self.xgb_checkbox.setChecked(True)
        # Uncomment the following lines if you implement LSTM
        # self.lstm_checkbox = QCheckBox('LSTM')
        # self.lstm_checkbox.setChecked(False)
        # self.lstm_checkbox.setEnabled(False)  # Disabled by default
        models_layout.addWidget(models_label)
        models_layout.addWidget(self.arima_checkbox)
        models_layout.addWidget(self.rf_checkbox)
        models_layout.addWidget(self.xgb_checkbox)
        # models_layout.addWidget(self.lstm_checkbox)  # Uncomment if LSTM is implemented
        input_layout.addLayout(models_layout)

        main_layout.addLayout(input_layout)

        # Progress Bar
        self.progress_bar = QProgressBar()
        self.progress_bar.setValue(0)
        self.progress_bar.setStyleSheet("""
            QProgressBar {
                border: 2px solid grey;
                border-radius: 5px;
                text-align: center;
            }
            QProgressBar::chunk {
                background-color: #05B8CC;
                width: 20px;
            }
        """)
        main_layout.addWidget(self.progress_bar)

        # Predict Button
        self.predict_button = QPushButton('Predict')
        self.predict_button.setFont(QFont('Arial', 14))
        self.predict_button.setStyleSheet("""
            QPushButton {
                background-color: #4CAF50; 
                color: white;
                padding: 10px;
                border: none;
                border-radius: 5px;
                width: 150px;
                align: center;
            }
            QPushButton:hover {
                background-color: #45a049;
            }
        """)
        self.predict_button.clicked.connect(self.predict_stock)
        main_layout.addWidget(self.predict_button, alignment=Qt.AlignCenter)

        # Output Section
        self.output = QTextEdit()
        self.output.setReadOnly(True)
        self.output.setStyleSheet("background-color: #f0f0f0; padding: 10px; color: black;")
        main_layout.addWidget(self.output)

        # Stock Selection Dropdown for Individual Graphs
        graph_selection_layout = QHBoxLayout()
        graph_label = QLabel('Select Stock to View Graph:')
        graph_label.setStyleSheet("color: #ffffff; font-size: 14px;")
        self.stock_dropdown = QComboBox()
        self.stock_dropdown.currentIndexChanged.connect(self.update_graph)
        graph_selection_layout.addWidget(graph_label)
        graph_selection_layout.addWidget(self.stock_dropdown)
        main_layout.addLayout(graph_selection_layout)

        # Add Matplotlib Figure
        self.figure = Figure(figsize=(12, 8), dpi=100)
        self.canvas = FigureCanvas(self.figure)
        main_layout.addWidget(self.canvas)

        # Save Predictions Button
        self.save_button = QPushButton('Save Predictions to CSV')
        self.save_button.setFont(QFont('Arial', 12))
        self.save_button.setStyleSheet("""
            QPushButton {
                background-color: #2196F3; 
                color: white;
                padding: 8px;
                border: none;
                border-radius: 5px;
                width: 200px;
            }
            QPushButton:hover {
                background-color: #0b7dda;
            }
        """)
        self.save_button.clicked.connect(self.save_predictions)
        self.save_button.setEnabled(False)  # Disabled until predictions are made
        main_layout.addWidget(self.save_button, alignment=Qt.AlignCenter)

        # Set the main layout
        self.setLayout(main_layout)

        # Apply a background color
        palette = self.palette()
        palette.setColor(QPalette.Window, QColor(53, 53, 53))  # Dark background
        self.setPalette(palette)

    def toggle_sentiment_checkbox(self):
        api_key = self.api_key_input.text().strip()
        if api_key:
            self.sentiment_checkbox.setEnabled(True)
        else:
            self.sentiment_checkbox.setChecked(False)
            self.sentiment_checkbox.setEnabled(False)

    def load_file(self):
        options = QFileDialog.Options()
        file_name, _ = QFileDialog.getOpenFileName(
            self, "Load Stock Symbols File", "", "CSV Files (*.csv);;Text Files (*.txt);;All Files (*)", options=options
        )
        if file_name:
            try:
                if file_name.endswith('.csv'):
                    df = pd.read_csv(file_name)
                    if 'Symbol' not in df.columns:
                        QMessageBox.warning(self, 'CSV Format Error', 'CSV must contain a "Symbol" column.')
                        return
                    symbols = df['Symbol'].astype(str).str.strip().tolist()
                elif file_name.endswith('.txt'):
                    with open(file_name, 'r') as f:
                        symbols = [line.strip() for line in f.readlines() if line.strip()]
                else:
                    QMessageBox.warning(self, 'File Format Error', 'Unsupported file format.')
                    return
                self.stock_input.setText(', '.join(symbols))
                QMessageBox.information(self, 'Success', f'Loaded {len(symbols)} stock symbols from file.')
            except Exception as e:
                logging.error(f"Failed to load file: {str(e)}", exc_info=True)
                QMessageBox.warning(self, 'Error', f'Failed to load file: {str(e)}')

    def predict_stock(self):
        # Disable the Predict button to prevent multiple clicks
        self.predict_button.setEnabled(False)
        self.save_button.setEnabled(False)

        # Reset progress bar
        self.progress_bar.setValue(0)
        QApplication.processEvents()

        # Retrieve user inputs
        stock_symbols = self.stock_input.text().upper().replace(' ', '').split(',')
        stock_symbols = [symbol.strip() for symbol in stock_symbols if symbol.strip()]
        days = self.days_input.text()
        duration_text = self.duration_combo.currentText()
        end_date_str = self.end_date_input.text()
        include_sentiment = self.sentiment_checkbox.isChecked()
        models_selected = {
            'ARIMA': self.arima_checkbox.isChecked(),
            'Random Forest': self.rf_checkbox.isChecked(),
            'XGBoost': self.xgb_checkbox.isChecked()
            # 'LSTM': self.lstm_checkbox.isChecked()  # Uncomment if implementing LSTM
        }

        if not stock_symbols:
            QMessageBox.warning(self, 'Input Error', 'Please enter at least one stock symbol or load from a file.')
            self.predict_button.setEnabled(True)
            return

        if not days.isdigit():
            QMessageBox.warning(self, 'Input Error', 'Please enter a valid number of days to predict.')
            self.predict_button.setEnabled(True)
            return

        # Validate End Date
        if end_date_str:
            try:
                end_date = pd.to_datetime(end_date_str)
                today = pd.to_datetime('today')
                if end_date > today:
                    QMessageBox.warning(self, 'Date Error', 'End date cannot be in the future.')
                    self.predict_button.setEnabled(True)
                    return
            except:
                QMessageBox.warning(self, 'Date Format Error', 'Please enter the end date in YYYY-MM-DD format.')
                self.predict_button.setEnabled(True)
                return
        else:
            end_date = pd.to_datetime('today')

        days = int(days)

        # Map duration text to pandas DateOffset
        duration_mapping = {
            '1 Year': pd.DateOffset(years=1),
            '2 Years': pd.DateOffset(years=2),
            '3 Years': pd.DateOffset(years=3),
            '4 Years': pd.DateOffset(years=4),
            '5 Years': pd.DateOffset(years=5),
            '10 Years': pd.DateOffset(years=10)
        }
        duration_offset = duration_mapping.get(duration_text, pd.DateOffset(years=1))
        start_date = end_date - duration_offset

        # Display the date range in output
        self.output.clear()
        self.output.append(f"Fetching data from {start_date.date()} to {end_date.date()}...\n")

        # Initialize progress
        total_models = sum(models_selected.values())
        total_tasks = len(stock_symbols) * total_models
        current_task = 0
        self.progress_bar.setMaximum(total_tasks)
        self.progress_bar.setValue(0)
        QApplication.processEvents()

        # Clear previous plot
        self.figure.clear()
        ax = self.figure.add_subplot(111)

        # Reset predictions storage
        self.all_predictions = {}
        self.prediction_data = pd.DataFrame()

        # Populate the stock dropdown
        self.stock_dropdown.clear()
        self.stock_dropdown.addItems(stock_symbols)

        for stock in tqdm(stock_symbols, desc="Processing Stocks"):
            try:
                # Fetch historical data
                data = yf.download(stock, start=start_date, end=end_date)
                if data.empty:
                    self.output.append(f"Stock: {stock} - No data found for the specified date range.\n")
                    continue

                # Ensure the data is sorted
                data.sort_index(inplace=True)

                # Handle missing data
                data = data.asfreq('B')  # Business days
                data.fillna(method='ffill', inplace=True)

                # Include sentiment analysis if selected and API key is provided
                if include_sentiment:
                    sentiment_score = self.get_news_sentiment(stock)
                    data['Sentiment'] = sentiment_score
                else:
                    data['Sentiment'] = 0  # Neutral sentiment

                # Feature Engineering with Technical Indicators
                data = self.add_technical_indicators(data)

                # Drop rows with NaN values after technical indicators
                data.dropna(inplace=True)

                # Initialize DataFrame to store predictions for this stock
                stock_predictions = pd.DataFrame()

                ### ARIMA Model
                if models_selected['ARIMA']:
                    current_task += 1
                    self.progress_bar.setValue(current_task)
                    QApplication.processEvents()

                    try:
                        # Parameter Optimization using auto_arima
                        stepwise_fit = auto_arima(
                            data['Close'], 
                            start_p=1, 
                            start_q=1,
                            max_p=5, 
                            max_q=5,
                            seasonal=False, 
                            trace=False,
                            error_action='ignore',
                            suppress_warnings=True
                        )

                        arima_order = stepwise_fit.order
                        model = ARIMA(data['Close'], order=arima_order)
                        model_fit = model.fit()
                        forecast = model_fit.forecast(steps=days)

                        # Calculate training RMSE
                        fitted = model_fit.fittedvalues
                        rmse = np.sqrt(mean_squared_error(data['Close'], fitted))

                        # Store predictions
                        stock_predictions['ARIMA_Predicted'] = forecast.values
                        self.all_predictions.setdefault(stock, {})['ARIMA'] = forecast.values

                        # Display Output
                        self.output.append(f"Stock: {stock} - ARIMA")
                        self.output.append(f"Model Order: {arima_order}")
                        self.output.append(f"Training RMSE: {rmse:.2f}")
                        self.output.append("Predicted Prices:")
                        for i, price in enumerate(forecast, 1):
                            pred_date = end_date + pd.Timedelta(days=i)
                            while pred_date.weekday() >= 5:  # Skip weekends
                                pred_date += pd.Timedelta(days=1)
                            self.output.append(f"{pred_date.date()}: ₹{price:.2f}")
                        self.output.append("")

                        # Plotting
                        future_dates = pd.bdate_range(start=data.index[-1] + pd.Timedelta(days=1), periods=len(forecast))
                        ax.plot(data.index, data['Close'], label=f'{stock} Historical')
                        ax.plot(future_dates, forecast, label=f'{stock} ARIMA Predicted', linestyle='--')

                    except Exception as e:
                        logging.error(f"ARIMA Model Error for {stock}: {str(e)}", exc_info=True)
                        self.output.append(f"ARIMA Model Error for {stock}: {str(e)}\n")

                ### Random Forest Model
                if models_selected['Random Forest']:
                    current_task += 1
                    self.progress_bar.setValue(current_task)
                    QApplication.processEvents()

                    try:
                        # Prepare features and target
                        data_rf = data.copy()
                        feature_cols = ['EMA_10', 'EMA_50', 'RSI', 'MACD', 'Stochastic', 'ADX', 'Volume', 'Sentiment']
                        X = data_rf[feature_cols]
                        y = data_rf['Close']

                        # Train-Test Split
                        split = int(0.8 * len(X))
                        X_train, X_test = X.iloc[:split], X.iloc[split:]
                        y_train, y_test = y.iloc[:split], y.iloc[split:]

                        # Parameter Grid for Random Forest
                        param_grid = {
                            'n_estimators': [100, 200],
                            'max_depth': [5, 10, None],
                            'min_samples_split': [2, 5]
                        }

                        rf = RandomForestRegressor(random_state=42)
                        grid_search = GridSearchCV(estimator=rf, param_grid=param_grid, cv=3, n_jobs=-1, verbose=0)
                        grid_search.fit(X_train, y_train)
                        best_rf = grid_search.best_estimator_

                        # Prepare for iterative prediction
                        last_features = X.iloc[-1].values.reshape(1, -1)
                        rf_forecast = []

                        for _ in range(days):
                            pred_price = best_rf.predict(last_features)[0]
                            rf_forecast.append(pred_price)

                            # Update features with predicted price
                            # For simplicity, we assume technical indicators remain the same or are updated based on predicted price
                            # Ideally, you should recalculate technical indicators based on the new price
                            # Here, we'll keep them static for demonstration
                            new_features = last_features.flatten().tolist()
                            # Assuming 'EMA_10' is at index 0, 'EMA_50' at 1, ..., 'Sentiment' at 7
                            # Update 'Close' price for next prediction if needed
                            # However, since features are technical indicators, updating them accurately requires recalculating
                            # For demonstration, we'll keep them unchanged except 'Sentiment'
                            # Increment day is not applicable here; hence, it's removed
                            # Sentiment is assumed neutral
                            new_features[-1] = 0
                            last_features = np.array(new_features).reshape(1, -1)

                        # Calculate RMSE on training data
                        rf_train_pred = best_rf.predict(X_train)
                        rf_rmse = np.sqrt(mean_squared_error(y_train, rf_train_pred))

                        # Store predictions
                        stock_predictions['RandomForest_Predicted'] = rf_forecast
                        self.all_predictions.setdefault(stock, {})['Random Forest'] = rf_forecast

                        # Display Output
                        self.output.append(f"Stock: {stock} - Random Forest")
                        self.output.append(f"Best Parameters: {grid_search.best_params_}")
                        self.output.append(f"Training RMSE: {rf_rmse:.2f}")
                        self.output.append("Predicted Prices:")
                        for i, price in enumerate(rf_forecast, 1):
                            pred_date = end_date + pd.Timedelta(days=i)
                            while pred_date.weekday() >= 5:  # Skip weekends
                                pred_date += pd.Timedelta(days=1)
                            self.output.append(f"{pred_date.date()}: ₹{price:.2f}")
                        self.output.append("")

                        # Plotting
                        future_dates = pd.bdate_range(start=data.index[-1] + pd.Timedelta(days=1), periods=len(rf_forecast))
                        ax.plot(future_dates, rf_forecast, label=f'{stock} Random Forest Predicted', linestyle='--')

                    except Exception as e:
                        logging.error(f"Random Forest Model Error for {stock}: {str(e)}", exc_info=True)
                        self.output.append(f"Random Forest Model Error for {stock}: {str(e)}\n")

                ### XGBoost Model
                if models_selected['XGBoost']:
                    current_task += 1
                    self.progress_bar.setValue(current_task)
                    QApplication.processEvents()

                    try:
                        # Prepare features and target
                        data_xgb = data.copy()
                        feature_cols = ['EMA_10', 'EMA_50', 'RSI', 'MACD', 'Stochastic', 'ADX', 'Volume', 'Sentiment']
                        X = data_xgb[feature_cols]
                        y = data_xgb['Close']

                        # Train-Test Split
                        split = int(0.8 * len(X))
                        X_train_xgb, X_test_xgb = X.iloc[:split], X.iloc[split:]
                        y_train_xgb, y_test_xgb = y.iloc[:split], y.iloc[split:]

                        # Parameter Grid for XGBoost
                        param_grid_xgb = {
                            'n_estimators': [100, 200],
                            'max_depth': [3, 5, 7],
                            'learning_rate': [0.01, 0.1, 0.2]
                        }

                        xgb_regressor = XGBRegressor(objective='reg:squarederror', random_state=42)
                        grid_search_xgb = GridSearchCV(estimator=xgb_regressor, param_grid=param_grid_xgb, cv=3, n_jobs=-1, verbose=0)
                        grid_search_xgb.fit(X_train_xgb, y_train_xgb)
                        best_xgb = grid_search_xgb.best_estimator_

                        # Prepare for iterative prediction
                        last_features = X.iloc[-1].values.reshape(1, -1)
                        xgb_forecast = []

                        for _ in range(days):
                            pred_price = best_xgb.predict(last_features)[0]
                            xgb_forecast.append(pred_price)

                            # Update features with predicted price
                            new_features = last_features.flatten().tolist()
                            # Similar to Random Forest, keep technical indicators static except 'Sentiment'
                            new_features[-1] = 0
                            last_features = np.array(new_features).reshape(1, -1)

                        # Calculate RMSE on training data
                        xgb_train_pred = best_xgb.predict(X_train_xgb)
                        xgb_rmse = np.sqrt(mean_squared_error(y_train_xgb, xgb_train_pred))

                        # Store predictions
                        stock_predictions['XGBoost_Predicted'] = xgb_forecast
                        self.all_predictions.setdefault(stock, {})['XGBoost'] = xgb_forecast

                        # Display Output
                        self.output.append(f"Stock: {stock} - XGBoost")
                        self.output.append(f"Best Parameters: {grid_search_xgb.best_params_}")
                        self.output.append(f"Training RMSE: {xgb_rmse:.2f}")
                        self.output.append("Predicted Prices:")
                        for i, price in enumerate(xgb_forecast, 1):
                            pred_date = end_date + pd.Timedelta(days=i)
                            while pred_date.weekday() >= 5:  # Skip weekends
                                pred_date += pd.Timedelta(days=1)
                            self.output.append(f"{pred_date.date()}: ₹{price:.2f}")
                        self.output.append("")

                        # Plotting
                        future_dates = pd.bdate_range(start=data.index[-1] + pd.Timedelta(days=1), periods=len(xgb_forecast))
                        ax.plot(future_dates, xgb_forecast, label=f'{stock} XGBoost Predicted', linestyle='--')

                    except Exception as e:
                        logging.error(f"XGBoost Model Error for {stock}: {str(e)}", exc_info=True)
                        self.output.append(f"XGBoost Model Error for {stock}: {str(e)}\n")

                # Update progress bar after each stock
                QApplication.processEvents()

            except Exception as e:
                logging.error(f"Unexpected error for {stock}: {str(e)}", exc_info=True)
                self.output.append(f"Unexpected Error for {stock}: {str(e)}\n")

        # Finalize Plot
        ax.set_title("Stock Price Prediction")
        ax.set_xlabel("Date")
        ax.set_ylabel("Price (₹)")
        ax.legend()
        self.canvas.draw()

        # Enable Save Predictions Button if predictions exist
        if self.all_predictions:
            self.save_button.setEnabled(True)
            QMessageBox.information(self, 'Success', 'Predictions generated successfully.')
        else:
            self.save_button.setEnabled(False)
            QMessageBox.warning(self, 'No Predictions', 'No predictions were generated.')

        # Re-enable the Predict button
        self.predict_button.setEnabled(True)

    def add_technical_indicators(self, data):
        """
        Adds various technical indicators to the DataFrame using pandas-ta.
        """
        try:
            # Ensure 'data' has the necessary columns
            required_columns = ['Open', 'High', 'Low', 'Close', 'Volume']
            for col in required_columns:
                if col not in data.columns:
                    raise ValueError(f"Missing required column: {col}")

            # Exponential Moving Averages
            data['EMA_10'] = ta.ema(data['Close'], length=10)
            data['EMA_50'] = ta.ema(data['Close'], length=50)

            # Relative Strength Index
            data['RSI'] = ta.rsi(data['Close'], length=14)

            # Moving Average Convergence Divergence
            macd = ta.macd(data['Close'], fast=12, slow=26, signal=9)
            data['MACD'] = macd['MACDh_12_26_9']

            # Stochastic Oscillator
            stochastic = ta.stoch(data['High'], data['Low'], data['Close'], k=14, d=3)
            data['Stochastic'] = stochastic['STOCHk_14_3_3']

            # Average Directional Index
            adx = ta.adx(data['High'], data['Low'], data['Close'], length=14)
            data['ADX'] = adx['ADX_14']

            # Additional Indicators (Optional)
            # Bollinger Bands
            bollinger = ta.bbands(data['Close'], length=20, std=2)
            data['Bollinger_High'] = bollinger['BBU_20_2.0']
            data['Bollinger_Low'] = bollinger['BBL_20_2.0']

            return data
        except Exception as e:
            logging.error(f"Error in adding technical indicators: {str(e)}", exc_info=True)
            self.output.append(f"Error in adding technical indicators: {str(e)}\n")
            return data  # Return data even if indicators couldn't be added

    def get_news_sentiment(self, stock_symbol):
        try:
            api_key = self.api_key_input.text().strip()
            if not api_key:
                return 0  # Neutral sentiment if no API key is provided
            query = stock_symbol.replace('.NS', '').replace('.BO', '')
            url = f'https://newsapi.org/v2/everything?q={query}&language=en&apiKey={api_key}'
            response = requests.get(url)
            articles = response.json().get('articles', [])
            if not articles:
                return 0  # Neutral sentiment if no news

            sentiments = []
            for article in articles[:10]:  # Limit to latest 10 articles
                title = article.get('title', '')
                # Use VADER for sentiment
                vs = self.analyzer.polarity_scores(title)
                sentiments.append(vs['compound'])

            if sentiments:
                average_sentiment = np.mean(sentiments)
                return average_sentiment
            else:
                return 0
        except Exception as e:
            import traceback
            error_msg = f"Error fetching sentiment for {stock_symbol}: {str(e)}\n{traceback.format_exc()}"
            self.output.append(error_msg)
            logging.error(error_msg)
            return 0

    def update_graph(self):
        selected_stock = self.stock_dropdown.currentText()
        if not selected_stock:
            return

        self.figure.clear()
        ax = self.figure.add_subplot(111)

        # Fetch historical data for plotting
        try:
            # Define a fixed historical period for plotting (e.g., 5 years)
            plot_start_date = pd.to_datetime('today') - pd.DateOffset(years=5)
            plot_end_date = pd.to_datetime('today')
            data = yf.download(selected_stock, start=plot_start_date, end=plot_end_date)
            if data.empty:
                QMessageBox.warning(self, 'Data Error', f'No data found for {selected_stock}.')
                return

            data.reset_index(inplace=True)
            data['Date'] = pd.to_datetime(data['Date'])
            data.set_index('Date', inplace=True)

            # Plot historical data
            ax.plot(data.index, data['Close'], label='Historical')

            # Plot predictions if available
            if selected_stock in self.all_predictions:
                predictions = self.all_predictions[selected_stock]
                for model_name, forecast in predictions.items():
                    # Determine the start date for predictions
                    last_date = data.index[-1]
                    future_dates = pd.bdate_range(start=last_date + pd.Timedelta(days=1), periods=len(forecast))
                    ax.plot(future_dates, forecast, label=f'{model_name} Predicted', linestyle='--')

            ax.set_title(f"{selected_stock} Price Prediction")
            ax.set_xlabel("Date")
            ax.set_ylabel("Price (₹)")
            ax.legend()
            self.canvas.draw()

        except Exception as e:
            import traceback
            error_msg = f"Failed to plot graph for {selected_stock}: {str(e)}\n{traceback.format_exc()}"
            self.output.append(error_msg)
            logging.error(error_msg)

    def save_predictions(self):
        if not self.all_predictions:
            QMessageBox.warning(self, 'No Predictions', 'There are no predictions to save.')
            return

        options = QFileDialog.Options()
        file_name, _ = QFileDialog.getSaveFileName(
            self, "Save Predictions CSV", "", "CSV Files (*.csv);;All Files (*)", options=options
        )
        if file_name:
            try:
                # Prepare DataFrame
                combined_df = pd.DataFrame()
                for stock, models in self.all_predictions.items():
                    for model, forecast in models.items():
                        # Align dates with the specified end_date
                        # Assuming that 'end_date' is the same as the one used for plotting
                        end_date_str = self.end_date_input.text().strip()
                        if end_date_str:
                            end_date = pd.to_datetime(end_date_str)
                        else:
                            end_date = pd.to_datetime('today')
                        future_dates = pd.bdate_range(start=end_date + pd.Timedelta(days=1), periods=len(forecast))
                        df = pd.DataFrame({
                            'Stock': stock,
                            'Model': model,
                            'Date': future_dates,
                            'Predicted_Price': forecast
                        })
                        combined_df = pd.concat([combined_df, df], ignore_index=True)

                # Save to CSV
                combined_df.to_csv(file_name, index=False)
                QMessageBox.information(self, 'Success', f'Predictions saved to {file_name}')
            except Exception as e:
                import traceback
                error_msg = f'Failed to save CSV: {str(e)}\n{traceback.format_exc()}'
                self.output.append(error_msg)
                logging.error(error_msg)
                QMessageBox.warning(self, 'Error', f'Failed to save CSV: {str(e)}')

def main():
    app = QApplication(sys.argv)
    app.setStyle('Fusion')  # Optional: Improve the look and feel
    window = StockPredictionApp()
    window.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()