the following error occurs with the code below :
ARIMA Model Error for HAL.NS: 'The `start` argument could not be matched to a location related to the index of the data.'

Error in rolling window forecast: 'Rolling_STD_7'

Stock: HAL.NS - Random Forest
Best Parameters: {'bootstrap': False, 'max_depth': None, 'min_samples_leaf': 1, 'min_samples_split': 2, 'n_estimators': 200}
Test RMSE: 157.26
Test MAE: 114.98
Test MAPE: 2.53%
Predicted Prices:

Error in rolling window forecast: 'Rolling_STD_7'

Stock: HAL.NS - XGBoost
Best Parameters: {'colsample_bytree': 1.0, 'learning_rate': 0.2, 'max_depth': 9, 'n_estimators': 200, 'subsample': 0.7}
Test RMSE: 150.55
Test MAE: 123.58
Test MAPE: 2.67%
Predicted Prices:


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, TimeSeriesSplit
from sklearn.preprocessing import StandardScaler
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
from sklearn.feature_selection import RFE
from sklearn.linear_model import LinearRegression

# 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, 1600, 1000)  # Increased size for better visibility

        # 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)
        models_layout.addWidget(models_label)
        models_layout.addWidget(self.arima_checkbox)
        models_layout.addWidget(self.rf_checkbox)
        models_layout.addWidget(self.xgb_checkbox)
        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=(14, 10), dpi=100)  # Increased figure size
        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()
        }

        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
                # Imputation Strategy: Forward-fill for prices, Zero for Volume if necessary
                data['Volume'].fillna(0, inplace=True)
                data.fillna(method='ffill', inplace=True)

                # Basic Outlier Detection and Removal
                data = self.remove_outliers(data)

                # 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, Lagged Features, and Rolling Statistics
                data = self.add_feature_eng(data)

                # Drop rows with NaN values after feature engineering
                data.dropna(inplace=True)

                # Feature Selection
                data = self.feature_selection(data)

                # Define features and target
                feature_cols = [col for col in self.selected_features if col != 'Close']
                X = data[feature_cols]
                y = data['Close']

                # Split data into features and target
                X_train, X_test = X.iloc[:int(0.8 * len(X))], X.iloc[int(0.8 * len(X)):]
                y_train, y_test = y.iloc[:int(0.8 * len(X))], y.iloc[int(0.8 * len(X)):]

                # 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(
                            y_train, 
                            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(y_train, order=arima_order)
                        model_fit = model.fit()

                        # Forecasting
                        forecast = model_fit.get_forecast(steps=days)
                        forecast_values = forecast.predicted_mean.values

                        # Calculate evaluation metrics on Test Set
                        arima_pred = model_fit.predict(start=X_test.index[0], end=X_test.index[-1])
                        arima_rmse = np.sqrt(mean_squared_error(y_test, arima_pred))
                        arima_mae = mean_absolute_error(y_test, arima_pred)
                        arima_mape = np.mean(np.abs((y_test - arima_pred) / y_test)) * 100

                        # 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"Test RMSE: {arima_rmse:.2f}")
                        self.output.append(f"Test MAE: {arima_mae:.2f}")
                        self.output.append(f"Test MAPE: {arima_mape:.2f}%")
                        self.output.append("Predicted Prices:")
                        for i, price in enumerate(forecast_values, 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_values))
                        ax.plot(data.index, data['Close'], label=f'{stock} Historical')
                        ax.plot(future_dates, forecast_values, 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:
                        # Feature Scaling
                        scaler_rf = StandardScaler()
                        X_train_scaled = scaler_rf.fit_transform(X_train)
                        X_test_scaled = scaler_rf.transform(X_test)

                        # Parameter Grid for Random Forest
                        param_grid_rf = {
                            'n_estimators': [100, 200, 300],
                            'max_depth': [None, 10, 20, 30],
                            'min_samples_split': [2, 5, 10],
                            'min_samples_leaf': [1, 2, 4],
                            'bootstrap': [True, False]
                        }

                        rf = RandomForestRegressor(random_state=42)
                        tscv = TimeSeriesSplit(n_splits=5)
                        grid_search_rf = GridSearchCV(estimator=rf, param_grid=param_grid_rf, cv=tscv, n_jobs=-1, verbose=0, scoring='neg_mean_squared_error')
                        grid_search_rf.fit(X_train_scaled, y_train)
                        best_rf = grid_search_rf.best_estimator_

                        # Calculate evaluation metrics on Test Set
                        rf_pred = best_rf.predict(X_test_scaled)
                        rf_rmse = np.sqrt(mean_squared_error(y_test, rf_pred))
                        rf_mae = mean_absolute_error(y_test, rf_pred)
                        rf_mape = np.mean(np.abs((y_test - rf_pred) / y_test)) * 100

                        # Store predictions
                        rf_forecast = self.rolling_window_forecast(best_rf, scaler_rf, X, y, days, feature_cols, end_date)
                        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_rf.best_params_}")
                        self.output.append(f"Test RMSE: {rf_rmse:.2f}")
                        self.output.append(f"Test MAE: {rf_mae:.2f}")
                        self.output.append(f"Test MAPE: {rf_mape:.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:
                        # Feature Scaling
                        scaler_xgb = StandardScaler()
                        X_train_scaled_xgb = scaler_xgb.fit_transform(X_train)
                        X_test_scaled_xgb = scaler_xgb.transform(X_test)

                        # Parameter Grid for XGBoost
                        param_grid_xgb = {
                            'n_estimators': [100, 200, 300],
                            'max_depth': [3, 5, 7, 9],
                            'learning_rate': [0.01, 0.05, 0.1, 0.2],
                            'subsample': [0.7, 0.8, 0.9, 1.0],
                            'colsample_bytree': [0.7, 0.8, 0.9, 1.0]
                        }

                        xgb_regressor = XGBRegressor(objective='reg:squarederror', random_state=42)
                        tscv = TimeSeriesSplit(n_splits=5)
                        grid_search_xgb = GridSearchCV(estimator=xgb_regressor, param_grid=param_grid_xgb, cv=tscv, n_jobs=-1, verbose=0, scoring='neg_mean_squared_error')
                        grid_search_xgb.fit(X_train_scaled_xgb, y_train)
                        best_xgb = grid_search_xgb.best_estimator_

                        # Calculate evaluation metrics on Test Set
                        xgb_pred = best_xgb.predict(X_test_scaled_xgb)
                        xgb_rmse = np.sqrt(mean_squared_error(y_test, xgb_pred))
                        xgb_mae = mean_absolute_error(y_test, xgb_pred)
                        xgb_mape = np.mean(np.abs((y_test - xgb_pred) / y_test)) * 100

                        # Store predictions
                        xgb_forecast = self.rolling_window_forecast(best_xgb, scaler_xgb, X, y, days, feature_cols, end_date)
                        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"Test RMSE: {xgb_rmse:.2f}")
                        self.output.append(f"Test MAE: {xgb_mae:.2f}")
                        self.output.append(f"Test MAPE: {xgb_mape:.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_feature_eng(self, data):
        """
        Adds technical indicators, lagged features, and rolling statistics.
        """
        try:
            # Technical Indicators
            data['EMA_10'] = ta.ema(data['Close'], length=10)
            data['EMA_50'] = ta.ema(data['Close'], length=50)
            data['RSI'] = ta.rsi(data['Close'], length=14)
            macd = ta.macd(data['Close'], fast=12, slow=26, signal=9)
            data['MACD'] = macd['MACDh_12_26_9']
            stochastic = ta.stoch(data['High'], data['Low'], data['Close'], k=14, d=3)
            data['Stochastic'] = stochastic['STOCHk_14_3_3']
            adx = ta.adx(data['High'], data['Low'], data['Close'], length=14)
            data['ADX'] = adx['ADX_14']
            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']

            # Lagged Features
            for lag in range(1, 8):
                data[f'Close_lag_{lag}'] = data['Close'].shift(lag)

            # Rolling Statistics
            data['Rolling_Mean_7'] = data['Close'].rolling(window=7).mean()
            data['Rolling_STD_7'] = data['Close'].rolling(window=7).std()
            data['Rolling_Mean_21'] = data['Close'].rolling(window=21).mean()
            data['Rolling_STD_21'] = data['Close'].rolling(window=21).std()

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

    def feature_selection(self, data):
        """
        Performs Recursive Feature Elimination (RFE) to select important features excluding 'Close'.
        """
        try:
            # Define features and target
            feature_cols = [col for col in data.columns if col != 'Close']
            X = data[feature_cols]
            y = data['Close']

            # Initialize model for RFE
            model = LinearRegression()
            rfe = RFE(model, n_features_to_select=10)  # Select top 10 features
            rfe = rfe.fit(X, y)

            # Get selected features
            selected_features = X.columns[rfe.support_]
            self.selected_features = selected_features.tolist()

            # Update data to include only selected features + 'Close' as target
            data = data[self.selected_features + ['Close']]

            # Log selected features
            logging.info(f"Selected Features for {self.selected_features}")

            return data
        except Exception as e:
            logging.error(f"Error in feature selection: {str(e)}", exc_info=True)
            self.output.append(f"Error in feature selection: {str(e)}\n")
            return data  # Return data even if feature selection fails

    def rolling_window_forecast(self, model, scaler, X, y, days, feature_cols, end_date):
        """
        Performs a rolling window forecast to predict future prices.
        Handles 'Close' separately to avoid KeyError.
        """
        try:
            forecast = []
            temp_X = X.copy()
            last_known_close = y.iloc[-1]

            for i in range(days):
                # Scale the latest data point
                latest_scaled = scaler.transform(temp_X.iloc[-1].values.reshape(1, -1))
                
                # Predict the next price
                next_price = model.predict(latest_scaled)[0]
                forecast.append(next_price)

                # Update last known close
                last_known_close = next_price

                # Create a new data point for features
                new_features = {}

                # Update lagged features
                for lag in range(1, 8):
                    if lag == 1:
                        new_features[f'Close_lag_{lag}'] = last_known_close
                    else:
                        new_features[f'Close_lag_{lag}'] = temp_X[f'Close_lag_{lag-1}'].iloc[-1]

                # Update rolling statistics
                new_features['Rolling_Mean_7'] = (temp_X['Rolling_Mean_7'].iloc[-1] * 6 + last_known_close) / 7
                new_features['Rolling_STD_7'] = temp_X['Rolling_STD_7'].iloc[-1]  # Simplified: kept constant
                new_features['Rolling_Mean_21'] = (temp_X['Rolling_Mean_21'].iloc[-1] * 20 + last_known_close) / 21
                new_features['Rolling_STD_21'] = temp_X['Rolling_STD_21'].iloc[-1]  # Simplified: kept constant

                # Note: Technical indicators like EMA, RSI, MACD, etc., require recalculation.
                # For simplicity, we'll assume they remain the same or use simplified updates.
                # A more accurate approach would involve recalculating these indicators based on the new close price.

                # Add other features as needed (e.g., EMA, RSI). Skipping for simplicity.

                # Append the new features to temp_X
                temp_X = temp_X.append(new_features, ignore_index=True)

            return forecast
        except Exception as e:
            logging.error(f"Error in rolling window forecast: {str(e)}", exc_info=True)
            self.output.append(f"Error in rolling window forecast: {str(e)}\n")
            return []

    def remove_outliers(self, data):
        """
        Removes outliers using the Z-score method.
        """
        try:
            from scipy import stats
            z_scores = np.abs(stats.zscore(data[['Open', 'High', 'Low', 'Close', 'Volume']]))
            filtered_entries = (z_scores < 3).all(axis=1)
            data = data[filtered_entries]
            return data
        except Exception as e:
            logging.error(f"Error in removing outliers: {str(e)}", exc_info=True)
            self.output.append(f"Error in removing outliers: {str(e)}\n")
            return data  # Return data even if outlier removal fails

    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():
                        # Determine end_date
                        end_date_str = self.end_date_input.text().strip()
                        if end_date_str:
                            end_date_dt = pd.to_datetime(end_date_str)
                        else:
                            end_date_dt = pd.to_datetime('today')
                        future_dates = pd.bdate_range(start=end_date_dt + 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 rolling_window_forecast(self, model, scaler, X, y, days, feature_cols, end_date):
        """
        Performs a rolling window forecast to predict future prices.
        Handles 'Close' separately to avoid KeyError.
        """
        try:
            forecast = []
            temp_X = X.copy()
            last_known_close = y.iloc[-1]

            for i in range(days):
                # Scale the latest data point
                latest_scaled = scaler.transform(temp_X.iloc[-1].values.reshape(1, -1))
                
                # Predict the next price
                next_price = model.predict(latest_scaled)[0]
                forecast.append(next_price)

                # Update last known close
                last_known_close = next_price

                # Create a new data point for features
                new_features = {}

                # Update lagged features
                for lag in range(1, 8):
                    if lag == 1:
                        new_features[f'Close_lag_{lag}'] = last_known_close
                    else:
                        new_features[f'Close_lag_{lag}'] = temp_X[f'Close_lag_{lag-1}'].iloc[-1]

                # Update rolling statistics
                new_features['Rolling_Mean_7'] = (temp_X['Rolling_Mean_7'].iloc[-1] * 6 + last_known_close) / 7
                new_features['Rolling_STD_7'] = temp_X['Rolling_STD_7'].iloc[-1]  # Simplified: kept constant
                new_features['Rolling_Mean_21'] = (temp_X['Rolling_Mean_21'].iloc[-1] * 20 + last_known_close) / 21
                new_features['Rolling_STD_21'] = temp_X['Rolling_STD_21'].iloc[-1]  # Simplified: kept constant

                # Note: Technical indicators like EMA, RSI, MACD, etc., require recalculation.
                # For simplicity, we'll assume they remain the same or use simplified updates.
                # A more accurate approach would involve recalculating these indicators based on the new close price.

                # Add other features as needed (e.g., EMA, RSI). Skipping for simplicity.

                # Append the new features to temp_X
                temp_X = temp_X.append(new_features, ignore_index=True)

            return forecast
        except Exception as e:
            logging.error(f"Error in rolling window forecast: {str(e)}", exc_info=True)
            self.output.append(f"Error in rolling window forecast: {str(e)}\n")
            return []

    def remove_outliers(self, data):
        """
        Removes outliers using the Z-score method.
        """
        try:
            from scipy import stats
            z_scores = np.abs(stats.zscore(data[['Open', 'High', 'Low', 'Close', 'Volume']]))
            filtered_entries = (z_scores < 3).all(axis=1)
            data = data[filtered_entries]
            return data
        except Exception as e:
            logging.error(f"Error in removing outliers: {str(e)}", exc_info=True)
            self.output.append(f"Error in removing outliers: {str(e)}\n")
            return data  # Return data even if outlier removal fails

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()