Marqueurs de Transactions (Échelle Linéaire) en echelle logarithmique : 

# Importing libraries 
import tkinter as tk
from tkinter import messagebox, filedialog
from tkinter import ttk  # Use ttk for themed widgets
from ttkthemes import ThemedTk  # Import ThemedTk for theming
import threading
from datetime import datetime, timedelta
import yfinance as yf
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.neural_network import MLPClassifier
from xgboost import XGBClassifier
from sklearn.model_selection import TimeSeriesSplit, RandomizedSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
from imblearn.over_sampling import SMOTE  # Imported SMOTE for handling class imbalance
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import mplcursors
import warnings
import logging
import sys

# Suppress warnings
warnings.filterwarnings("ignore")

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

class ToolTip:
    """
    It creates a tooltip for a given widget as the mouse goes on it.
    """
    def __init__(self, widget, text='widget info'):
        self.widget = widget
        self.text = text
        self.tooltip = None
        self.widget.bind("<Enter>", self.enter)
        self.widget.bind("<Leave>", self.leave)

    def enter(self, event=None):
        x = y = 0
        try:
            x, y, cx, cy = self.widget.bbox("insert")
        except:
            x, y = 0, 0
        x += self.widget.winfo_rootx() + 25
        y += self.widget.winfo_rooty() + 20
        # Creates a toplevel window
        self.tooltip = tk.Toplevel(self.widget)
        # Leaves only the label and removes the app window
        self.tooltip.wm_overrideredirect(True)
        self.tooltip.wm_geometry(f"+{x}+{y}")
        label = tk.Label(self.tooltip, text=self.text, justify='left',
                         background="#ffffe0", relief='solid', borderwidth=1,
                         font=("tahoma", "8", "normal"))
        label.pack(ipadx=1)

    def leave(self, event=None):
        if self.tooltip:
            self.tooltip.destroy()
            self.tooltip = None

class TradingApp(ThemedTk):
    """
    A Tkinter-based application for analyzing trading strategies using machine learning models.
    """

    def __init__(self):
        # Initialize ThemedTk with a Windows-like theme
        super().__init__(theme="vista")  # 'vista' theme closely resembles Windows 7
        self.title("Trading Strategy Analyzer")
        self.geometry("1300x800")  # Adjusted size for better fit with new layout
        self.create_menu()
        self.create_widgets()
        self.plot_window = None  # Initialize plot window reference
        self.current_figure = None  # Reference to the current matplotlib figure

    def create_menu(self):
        """
        Create a menu bar with File and Help menus.
        """
        menubar = tk.Menu(self)
        
        # File Menu
        file_menu = tk.Menu(menubar, tearoff=0)
        file_menu.add_command(label="Enregistrer les Résultats", command=self.save_results)
        file_menu.add_separator()
        file_menu.add_command(label="Quitter", command=self.quit_application)
        menubar.add_cascade(label="Fichier", menu=file_menu)
        
        # Help Menu
        help_menu = tk.Menu(menubar, tearoff=0)
        help_menu.add_command(label="À propos", command=self.show_about)
        menubar.add_cascade(label="Aide", menu=help_menu)
        
        self.config(menu=menubar)

    def show_about(self):
        """
        Show the About dialog.
        """
        messagebox.showinfo("À propos", "Trading Strategy Analyzer\nVersion 1.0\nDéveloppé par OpenAI")

    def quit_application(self):
        """
        Quit the application gracefully.
        """
        if messagebox.askokcancel("Quitter", "Voulez-vous vraiment quitter l'application ?"):
            self.destroy()

    def create_widgets(self):
        """
        Create and layout all the widgets in the application.
        """
        # Use a style object for consistent styling
        style = ttk.Style(self)
        style.configure('TFrame', background=self["background"])
        style.configure('TLabel', background=self["background"])
        style.configure('TButton', padding=6)
        style.configure('TCheckbutton', background=self["background"])

        # Create Notebook for organizing input sections
        notebook = ttk.Notebook(self)
        notebook.pack(pady=10, padx=10, fill=tk.BOTH, expand=True)

        # --- Tab 1: Paramètres des Données ---
        data_frame = ttk.Frame(notebook)
        notebook.add(data_frame, text='Paramètres des Données')

        # Ticker Symbol Input
        ttk.Label(data_frame, text="Ticker Symbol:").grid(row=0, column=0, sticky='e', padx=5, pady=5)
        self.ticker_entry = ttk.Entry(data_frame)
        self.ticker_entry.insert(0, "BTC-USD")
        self.ticker_entry.grid(row=0, column=1, padx=5, pady=5)
        ToolTip(self.ticker_entry, "Entrez le symbole du ticker (ex. BTC-USD, AAPL)")

        # Start Date Input
        ttk.Label(data_frame, text="Start Date (YYYY-MM-DD):").grid(row=1, column=0, sticky='e', padx=5, pady=5)
        self.start_date_entry = ttk.Entry(data_frame)
        self.start_date_entry.insert(0, "2015-01-01")
        self.start_date_entry.grid(row=1, column=1, padx=5, pady=5)
        ToolTip(self.start_date_entry, "Entrez la date de début pour l'analyse")

        # End Date Input
        ttk.Label(data_frame, text="End Date (YYYY-MM-DD):").grid(row=2, column=0, sticky='e', padx=5, pady=5)
        self.end_date_entry = ttk.Entry(data_frame)
        # Adjust end date to include current day's data
        self.end_date_entry.insert(0, (datetime.today() + timedelta(days=1)).strftime('%Y-%m-%d'))
        self.end_date_entry.grid(row=2, column=1, padx=5, pady=5)
        ToolTip(self.end_date_entry, "Entrez la date de fin pour l'analyse")

        # Minimum Days Between Trades Input
        ttk.Label(data_frame, text="Minimum Days Between Trades:").grid(row=3, column=0, sticky='e', padx=5, pady=5)
        self.cooldown_entry = ttk.Entry(data_frame)
        self.cooldown_entry.insert(0, "1")  # Default to 1 day
        self.cooldown_entry.grid(row=3, column=1, padx=5, pady=5)
        ToolTip(self.cooldown_entry, "Définissez le nombre minimum de jours entre les transactions consécutives")

        # Commission Rate Input
        ttk.Label(data_frame, text="Commission Rate (%):").grid(row=4, column=0, sticky='e', padx=5, pady=5)
        self.commission_entry = ttk.Entry(data_frame)
        self.commission_entry.insert(0, "0.08")  # Default to 0.08%
        self.commission_entry.grid(row=4, column=1, padx=5, pady=5)
        ToolTip(self.commission_entry, "Entrez le taux de commission par transaction en pourcentage")

        # Risk-Free Rate Input
        ttk.Label(data_frame, text="Risk-Free Rate (%):").grid(row=5, column=0, sticky='e', padx=5, pady=5)
        self.risk_free_rate_entry = ttk.Entry(data_frame)
        self.risk_free_rate_entry.insert(0, "2.0")  # Default to 2%
        self.risk_free_rate_entry.grid(row=5, column=1, padx=5, pady=5)
        ToolTip(self.risk_free_rate_entry, "Entrez le taux sans risque annuel en pourcentage")

        # --- Tab 2: Indicateurs Techniques ---
        indicators_frame = ttk.Frame(notebook)
        notebook.add(indicators_frame, text='Indicateurs Techniques')

        # SMA Window Size
        ttk.Label(indicators_frame, text="SMA Window Size:").grid(row=0, column=0, sticky='e', padx=5, pady=5)
        self.sma_window_entry = ttk.Entry(indicators_frame)
        self.sma_window_entry.insert(0, "50")  # Default to 50
        self.sma_window_entry.grid(row=0, column=1, padx=5, pady=5)
        ToolTip(self.sma_window_entry, "Définissez la taille de la fenêtre pour la Moyenne Mobile Simple (SMA)")

        # RSI Window Size
        ttk.Label(indicators_frame, text="RSI Window Size:").grid(row=1, column=0, sticky='e', padx=5, pady=5)
        self.rsi_window_entry = ttk.Entry(indicators_frame)
        self.rsi_window_entry.insert(0, "14")  # Default to 14
        self.rsi_window_entry.grid(row=1, column=1, padx=5, pady=5)
        ToolTip(self.rsi_window_entry, "Définissez la taille de la fenêtre pour l'Indice de Force Relative (RSI)")

        # Volatility Window Size
        ttk.Label(indicators_frame, text="Volatility Window Size:").grid(row=2, column=0, sticky='e', padx=5, pady=5)
        self.volatility_window_entry = ttk.Entry(indicators_frame)
        self.volatility_window_entry.insert(0, "30")  # Default to 30
        self.volatility_window_entry.grid(row=2, column=1, padx=5, pady=5)
        ToolTip(self.volatility_window_entry, "Définissez la taille de la fenêtre pour le calcul de la Volatilité")

        # --- Tab 3: Sélection des Modèles ---
        models_frame = ttk.Frame(notebook)
        notebook.add(models_frame, text='Sélection des Modèles')

        # Model Selection
        ttk.Label(models_frame, text="Sélectionnez les Modèles:").grid(row=0, column=0, sticky='nw', padx=5, pady=5)
        self.model_vars = {
            'Random Forest': tk.BooleanVar(value=True),
            'Gradient Boosting': tk.BooleanVar(value=False),
            'Logistic Regression': tk.BooleanVar(value=False),
            'Support Vector Machine': tk.BooleanVar(value=False),  # New Model
            'XGBoost': tk.BooleanVar(value=False),  # New Model
            'Neural Network': tk.BooleanVar(value=False)  # New Model
        }
        model_check_frame = ttk.Frame(models_frame)
        model_check_frame.grid(row=0, column=1, padx=5, pady=5, sticky='w')
        for model, var in self.model_vars.items():
            chk = ttk.Checkbutton(model_check_frame, text=model, variable=var)
            chk.pack(anchor='w', pady=2)
            ToolTip(chk, f"Inclure {model} dans l'analyse")

        # --- Tab 4: Options ---
        options_frame = ttk.Frame(notebook)
        notebook.add(options_frame, text='Options')

        # Reset Button
        self.reset_button = ttk.Button(options_frame, text="Réinitialiser les Paramètres", command=self.reset_parameters)
        self.reset_button.pack(pady=10)

        # Run and Save Buttons
        buttons_frame = ttk.Frame(options_frame)
        buttons_frame.pack(pady=10)

        self.run_button = ttk.Button(buttons_frame, text="Lancer l'Analyse", command=self.run_analysis)
        self.run_button.pack(side=tk.LEFT, padx=5)

        self.save_button = ttk.Button(buttons_frame, text="Enregistrer les Résultats", command=self.save_results, state=tk.DISABLED)
        self.save_button.pack(side=tk.LEFT, padx=5)

        # Progress Bar
        self.progress = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(self, variable=self.progress, maximum=100, mode='determinate')
        self.progress_bar.pack(pady=5, fill=tk.X, padx=10)

        # Output Text
        self.output_text = tk.Text(self, height=20, wrap='word')
        self.output_text.pack(pady=10, padx=10, fill=tk.BOTH, expand=True)
        self.output_text.configure(state='disabled')  # Make it read-only initially

        # Status Bar
        self.status_var = tk.StringVar()
        self.status_var.set("Prêt")
        self.status_bar = ttk.Label(self, textvariable=self.status_var, relief='sunken', anchor='w')
        self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)

    def reset_parameters(self):
        """
        Reset all input fields to their default values.
        """
        try:
            # Reset Data Parameters
            self.ticker_entry.delete(0, tk.END)
            self.ticker_entry.insert(0, "BTC-USD")
            self.start_date_entry.delete(0, tk.END)
            self.start_date_entry.insert(0, "2015-01-01")
            self.end_date_entry.delete(0, tk.END)
            self.end_date_entry.insert(0, (datetime.today() + timedelta(days=1)).strftime('%Y-%m-%d'))
            self.cooldown_entry.delete(0, tk.END)
            self.cooldown_entry.insert(0, "1")
            self.commission_entry.delete(0, tk.END)
            self.commission_entry.insert(0, "0.08")
            self.risk_free_rate_entry.delete(0, tk.END)
            self.risk_free_rate_entry.insert(0, "2.0")

            # Reset Indicators
            self.sma_window_entry.delete(0, tk.END)
            self.sma_window_entry.insert(0, "50")
            self.rsi_window_entry.delete(0, tk.END)
            self.rsi_window_entry.insert(0, "14")
            self.volatility_window_entry.delete(0, tk.END)
            self.volatility_window_entry.insert(0, "30")

            # Reset Model Selection
            for var in self.model_vars.values():
                var.set(False)
            self.model_vars['Random Forest'].set(True)

            messagebox.showinfo("Réinitialisation", "Les paramètres ont été réinitialisés aux valeurs par défaut.")
            logging.info("Paramètres réinitialisés aux valeurs par défaut.")
        except Exception as e:
            messagebox.showerror("Erreur", f"Une erreur est survenue lors de la réinitialisation : {e}")
            logging.error(f"Erreur lors de la réinitialisation des paramètres : {e}")

    def run_analysis(self):
        """
        Initiates the analysis by validating inputs and starting the analysis thread.
        """
        # Disable input fields and buttons
        self.disable_inputs()
        self.save_button.config(state=tk.DISABLED)
        self.progress.set(0)
        self.status_var.set("Démarrage de l'analyse...")

        # Clear previous output
        self.output_text.configure(state='normal')
        self.output_text.delete(1.0, tk.END)
        self.output_text.configure(state='disabled')

        # Start analysis in a separate thread
        threading.Thread(target=self.perform_analysis, daemon=True).start()

    def disable_inputs(self):
        """
        Disable all input widgets to prevent changes during analysis.
        """
        self.run_button.config(state=tk.DISABLED)
        self.reset_button.config(state=tk.DISABLED)
        for child in self.winfo_children():
            self._disable_widget(child)

    def _disable_widget(self, widget):
        """
        Recursively disable widgets.
        """
        try:
            widget_state = widget['state']
            widget.configure(state='disabled')
        except:
            pass
        for child in widget.winfo_children():
            self._disable_widget(child)

    def enable_inputs(self):
        """
        Enable all input widgets after analysis.
        """
        self.run_button.config(state=tk.NORMAL)
        self.reset_button.config(state=tk.NORMAL)
        for child in self.winfo_children():
            self._enable_widget(child)

    def _enable_widget(self, widget):
        """
        Recursively enable widgets.
        """
        try:
            widget_state = widget['state']
            if isinstance(widget, ttk.Checkbutton) or isinstance(widget, ttk.Button) or isinstance(widget, ttk.Entry):
                widget.configure(state='!disabled')
        except:
            pass
        for child in widget.winfo_children():
            self._enable_widget(child)

    def save_results(self):
        """
        Allows the user to save the analysis results and plot.
        """
        try:
            # Save output text
            self.output_text.configure(state='normal')
            output_content = self.output_text.get(1.0, tk.END)
            self.output_text.configure(state='disabled')
            if not output_content.strip():
                messagebox.showwarning("Pas de Données", "Il n'y a pas de données à enregistrer.")
                return

            # Ask user where to save the text results
            file_path = filedialog.asksaveasfilename(defaultextension=".txt",
                                                     filetypes=[("Text Files", "*.txt"),
                                                                ("All Files", "*.*")],
                                                     title="Enregistrer les Résultats de l'Analyse")
            if file_path:
                with open(file_path, 'w') as file:
                    file.write(output_content)
                logging.info(f"Résultats de l'analyse enregistrés dans {file_path}")

            # Save plot if available
            if self.current_figure:
                plot_file_path = filedialog.asksaveasfilename(defaultextension=".png",
                                                              filetypes=[("PNG Files", "*.png"),
                                                                         ("All Files", "*.*")],
                                                              title="Enregistrer le Graphique")
                if plot_file_path:
                    self.current_figure.savefig(plot_file_path)
                    logging.info(f"Graphique enregistré dans {plot_file_path}")

            messagebox.showinfo("Enregistrement Réussi", "Les résultats et les graphiques ont été enregistrés avec succès.")
        except Exception as e:
            messagebox.showerror("Erreur d'Enregistrement", f"Une erreur est survenue lors de l'enregistrement : {e}")
            logging.error(f"Erreur lors de l'enregistrement des résultats : {e}")

    def update_progress(self, value, status_message=None):
        """
        Updates the progress bar and optionally the status bar.
        """
        self.progress.set(value)
        if status_message:
            self.status_var.set(status_message)

    def update_output(self, output_info):
        """
        Updates the output text widget with analysis results.
        """
        self.output_text.configure(state='normal')
        self.output_text.delete(1.0, tk.END)
        self.output_text.insert(tk.END, output_info)
        self.output_text.configure(state='disabled')

    def update_plot(self, fig, data_real):
        """
        Displays the analysis plot in a separate window with enhanced interactivity.
        """
        # Check if a plot window already exists and destroy it to avoid multiple windows
        if self.plot_window and self.plot_window.winfo_exists():
            self.plot_window.destroy()

        # Create a new Toplevel window for the plot
        self.plot_window = tk.Toplevel(self)
        self.plot_window.title("Performance de la Stratégie vs. Marché")
        self.plot_window.geometry("1200x800")  # Set desired size for the plot window

        # Embed the matplotlib figure in the new window
        canvas = FigureCanvasTkAgg(fig, master=self.plot_window)
        canvas.draw()
        canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

        # Add the navigation toolbar for enhanced interactivity (zoom, pan, save)
        toolbar_frame = ttk.Frame(self.plot_window)
        toolbar_frame.pack(fill=tk.X)
        toolbar = NavigationToolbar2Tk(canvas, toolbar_frame)
        toolbar.update()

        # Make the plot interactive with mplcursors
        cursor = mplcursors.cursor(canvas.get_tk_widget(), hover=True)

        @cursor.connect("add")
        def on_add(sel):
            try:
                # Find the closest index based on the x-coordinate
                x_val = sel.target[0]
                y_val = sel.target[1]
                closest_idx = data_real.index.get_loc(pd.Timestamp(sel.target[0]), method='nearest')
                x = data_real.index[closest_idx]
                if isinstance(x, pd.Timestamp):
                    x_str = x.strftime('%Y-%m-%d')
                else:
                    x_str = str(x)
                sel.annotation.set(text=f"Date: {x_str}\nCumulative Return: {y_val:.2%}")
            except Exception as e:
                sel.annotation.set(text=f"Error: {e}")

        # Store the current figure reference for saving
        self.current_figure = fig

    def perform_analysis(self):
        """
        Performs the trading strategy analysis.
        """
        try:
            logging.info("Démarrage de l'analyse...")
            self.after(0, self.update_progress, 10, "Téléchargement des données...")

            # Fetch historical data for ticker
            ticker = self.ticker_entry.get().strip().upper()
            start_date = self.start_date_entry.get().strip()
            end_date = self.end_date_entry.get().strip()
            cooldown_days = self.cooldown_entry.get().strip()
            commission_rate = self.commission_entry.get().strip()
            risk_free_rate = self.risk_free_rate_entry.get().strip()
            selected_models = [model for model, var in self.model_vars.items() if var.get()]

            # New Parameters for Indicators
            sma_window = self.sma_window_entry.get().strip()
            rsi_window = self.rsi_window_entry.get().strip()
            volatility_window = self.volatility_window_entry.get().strip()

            # Input validation
            try:
                # Validate dates
                start_dt = datetime.strptime(start_date, '%Y-%m-%d')
                end_dt = datetime.strptime(end_date, '%Y-%m-%d')
                if start_dt >= end_dt:
                    raise ValueError("La date de début doit être antérieure à la date de fin.")

                # Validate cooldown days
                cooldown_days = int(cooldown_days)
                if cooldown_days < 1:
                    raise ValueError("Le nombre minimum de jours entre les transactions doit être au moins de 1.")

                # Validate commission rate
                commission_rate = float(commission_rate) / 100  # Convert percentage to decimal
                if commission_rate < 0:
                    raise ValueError("Le taux de commission doit être non négatif.")

                # Validate risk-free rate
                risk_free_rate = float(risk_free_rate) / 100  # Convert percentage to decimal
                if risk_free_rate < 0:
                    raise ValueError("Le taux sans risque doit être non négatif.")

                if not selected_models:
                    raise ValueError("Au moins un modèle doit être sélectionné.")

                # Validate SMA window
                sma_window = int(sma_window)
                if sma_window < 1:
                    raise ValueError("La taille de la fenêtre SMA doit être au moins de 1.")

                # Validate RSI window
                rsi_window = int(rsi_window)
                if rsi_window < 1:
                    raise ValueError("La taille de la fenêtre RSI doit être au moins de 1.")

                # Validate Volatility window
                volatility_window = int(volatility_window)
                if volatility_window < 1:
                    raise ValueError("La taille de la fenêtre de volatilité doit être au moins de 1.")

            except ValueError as ve:
                self.after(0, self.update_progress, 0, "Erreur de validation des entrées.")
                messagebox.showerror("Erreur de Saisie", str(ve))
                logging.error(f"Erreur de validation des entrées : {ve}")
                self.after(0, self.enable_inputs)
                self.status_var.set("Prêt")
                return
            except Exception as e:
                self.after(0, self.update_progress, 0, "Erreur inattendue.")
                messagebox.showerror("Erreur", f"Une erreur inattendue est survenue : {e}")
                logging.error(f"Erreur inattendue lors de la validation des entrées : {e}")
                self.after(0, self.enable_inputs)
                self.status_var.set("Prêt")
                return

            # Fetch data
            data_real = yf.download(ticker, start=start_date, end=end_date)
            if data_real.empty:
                raise ValueError("Aucune donnée récupérée pour le ticker et la plage de dates spécifiés.")
            logging.info("Données récupérées avec succès.")
            self.after(0, self.update_progress, 20, "Ingénierie des fonctionnalités...")

            # Feature Engineering
            data_real = data_real[['Open', 'High', 'Low', 'Close', 'Volume']].copy()
            data_real.rename(columns={'Close': 'close'}, inplace=True)

            # Define RSI calculation function
            def calculate_rsi(data, window):
                delta = data.diff()
                gain = delta.clip(lower=0)
                loss = -delta.clip(upper=0)
                avg_gain = gain.rolling(window=window, min_periods=window).mean()
                avg_loss = loss.rolling(window=window, min_periods=window).mean()
                rs = avg_gain / avg_loss
                rsi = 100 - (100 / (1 + rs))
                return rsi

            # Feature engineering with dynamic parameters
            data_real['returns'] = data_real['close'].pct_change()
            data_real['SMA'] = data_real['close'].rolling(window=sma_window).mean()
            data_real['RSI'] = calculate_rsi(data_real['close'], rsi_window)
            data_real['volatility'] = data_real['returns'].rolling(window=volatility_window).std()
            data_real['sentiment_index'] = 1 / (1 + data_real['volatility'])
            data_real['future_return'] = data_real['returns'].shift(-1)
            data_real['target'] = (data_real['future_return'] > 0).astype(int)
            data_real.dropna(inplace=True)
            logging.info("Ingénierie des fonctionnalités terminée.")

            self.after(0, self.update_progress, 40, "Préparation des données...")

            # Prepare the dataset for machine learning
            features = ['close', 'SMA', 'RSI', 'sentiment_index', 'Volume']
            X_real = data_real[features]
            y_real = data_real['target']

            # Time-based train-test split
            split_point = int(len(X_real) * 0.8)
            X_train_real = X_real.iloc[:split_point]
            X_test_real = X_real.iloc[split_point:]
            y_train_real = y_real.iloc[:split_point]
            y_test_real = y_real.iloc[split_point:]

            # Handle class imbalance using SMOTE
            logging.info("Application de SMOTE pour gérer le déséquilibre des classes.")
            smote = SMOTE(random_state=42)
            X_train_resampled, y_train_resampled = smote.fit_resample(X_train_real, y_train_real)
            logging.info("SMOTE appliqué avec succès.")

            # Standardize the features using training data
            scaler = StandardScaler()
            X_train_scaled = scaler.fit_transform(X_train_resampled)
            X_test_scaled = scaler.transform(X_test_real)

            self.after(0, self.update_progress, 50, "Entraînement des modèles...")

            # Initialize models based on selection
            models = {}
            for model_name in selected_models:
                if model_name == 'Random Forest':
                    models['Random Forest'] = RandomForestClassifier(random_state=42, class_weight='balanced')
                elif model_name == 'Gradient Boosting':
                    models['Gradient Boosting'] = GradientBoostingClassifier(random_state=42)
                elif model_name == 'Logistic Regression':
                    models['Logistic Regression'] = LogisticRegression(random_state=42, class_weight='balanced', max_iter=1000)
                elif model_name == 'Support Vector Machine':
                    models['Support Vector Machine'] = SVC(probability=True, class_weight='balanced', random_state=42)
                elif model_name == 'XGBoost':
                    pos_weight = sum(y_train_real == 0) / sum(y_train_real == 1) if sum(y_train_real == 1) != 0 else 1
                    models['XGBoost'] = XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42, scale_pos_weight=pos_weight)
                elif model_name == 'Neural Network':
                    models['Neural Network'] = MLPClassifier(random_state=42, max_iter=1000)
                else:
                    logging.warning(f"Modèle {model_name} non reconnu et sera ignoré.")

            # Hyperparameter tuning with RandomizedSearchCV using TimeSeriesSplit
            param_distributions = {
                'Random Forest': {
                    'n_estimators': [100, 200, 300],
                    'max_depth': [None, 10, 20, 30],
                    'min_samples_split': [2, 5, 10],
                    'min_samples_leaf': [1, 2, 4],
                },
                'Gradient Boosting': {
                    'n_estimators': [100, 200, 300],
                    'learning_rate': [0.01, 0.05, 0.1],
                    'max_depth': [3, 5, 7],
                    'min_samples_split': [2, 5, 10],
                },
                'Logistic Regression': {
                    'C': [0.01, 0.1, 1, 10],
                    'penalty': ['l2'],
                    'solver': ['lbfgs', 'saga']
                },
                'Support Vector Machine': {
                    'C': [0.1, 1, 10, 100],
                    'gamma': ['scale', 'auto'],
                    'kernel': ['rbf', 'poly', 'sigmoid']
                },
                'XGBoost': {
                    'n_estimators': [100, 200, 300],
                    'learning_rate': [0.01, 0.05, 0.1],
                    'max_depth': [3, 5, 7],
                    'subsample': [0.6, 0.8, 1.0],
                    'colsample_bytree': [0.6, 0.8, 1.0]
                },
                'Neural Network': {
                    'hidden_layer_sizes': [(50,50), (100,)],
                    'activation': ['tanh', 'relu'],
                    'solver': ['sgd', 'adam'],
                    'alpha': [0.0001, 0.001, 0.01],
                    'learning_rate': ['constant','adaptive'],
                }
            }

            cv = TimeSeriesSplit(n_splits=5)

            total_models = len(models)
            if total_models == 0:
                raise ValueError("Aucun modèle valide sélectionné pour l'entraînement.")

            self.after(0, self.update_progress, 60, "Tuning des hyperparamètres...")

            best_models = {}
            model_evaluations = {}
            model_strategy_returns = {}
            model_trades = {}  # Dictionary to store trades per model
            cross_val_results = {}  # To store cross-validation ROC AUC scores
            for idx, (model_name, model) in enumerate(models.items(), start=1):
                logging.info(f"Démarrage du tuning des hyperparamètres pour {model_name}")
                random_search = RandomizedSearchCV(
                    model,
                    param_distributions.get(model_name, {}),
                    n_iter=30 if model_name in ['XGBoost', 'Neural Network'] else 20,  # Increased n_iter for complex models
                    cv=cv,
                    scoring='roc_auc',
                    n_jobs=-1,
                    random_state=42
                )
                random_search.fit(X_train_scaled, y_train_resampled)
                best_models[model_name] = random_search.best_estimator_
                logging.info(f"Meilleurs paramètres pour {model_name} : {random_search.best_params_}")

                # Store cross-validation ROC AUC scores
                cross_val_scores = random_search.cv_results_['mean_test_score']
                cross_val_results[model_name] = cross_val_scores

                # Update progress based on the number of models
                progress_increment = 30 / total_models
                current_progress = 60 + (30 * idx) / total_models
                self.after(0, self.update_progress, min(current_progress, 90), f"Entraînement des modèles ({model_name})...")

            self.after(0, self.update_progress, 90, "Évaluation des modèles...")

            # Evaluate the models and generate strategy returns for each
            evaluation_results = ""
            cross_val_evaluation = "--- Scores ROC AUC de la Validation Croisée ---\n\n"
            model_roc_auc = {}
            for model_name, model in best_models.items():
                y_pred = model.predict(X_test_scaled)
                y_proba = model.predict_proba(X_test_scaled)[:,1]
                accuracy = accuracy_score(y_test_real, y_pred)
                precision = precision_score(y_test_real, y_pred)
                recall = recall_score(y_test_real, y_pred)
                f1 = f1_score(y_test_real, y_pred)
                roc_auc = roc_auc_score(y_test_real, y_proba)
                model_roc_auc[model_name] = roc_auc
                evaluation_results += (
                    f"--- {model_name} ---\n"
                    f"Précision : {accuracy:.2%}\n"
                    f"Précision (Precision) : {precision:.2%}\n"
                    f"Rappel (Recall) : {recall:.2%}\n"
                    f"Score F1 : {f1:.2%}\n"
                    f"ROC AUC : {roc_auc:.2f}\n\n"
                )
                logging.info(f"Évaluation pour {model_name} : Précision={accuracy}, ROC AUC={roc_auc}")

                # Add cross-validation results
                cv_scores = cross_val_results.get(model_name, [])
                cv_mean = np.mean(cv_scores)
                cv_std = np.std(cv_scores)
                cross_val_evaluation += (
                    f"--- {model_name} ---\n"
                    f"ROC AUC de la Validation Croisée : {cv_mean:.2f} ± {cv_std:.2f}\n\n"
                )

            self.after(0, self.update_progress, 95, "Génération des graphiques...")

            if not best_models:
                raise ValueError("Aucun modèle n'a été entraîné avec succès.")

            # Generate signals and calculate strategy returns for each model
            for model_name, model in best_models.items():
                logging.info(f"Génération des signaux de trading pour {model_name}.")
                # Scale the entire dataset using the scaler fitted on training data
                X_scaled_real = scaler.transform(X_real)
                predicted_signal = pd.Series(model.predict(X_scaled_real), index=data_real.index)
                data_real[f'predicted_signal_{model_name}'] = predicted_signal

                # Apply trade cooldown constraint
                logging.info(f"Application des contraintes de cooldown pour {model_name}.")
                data_real[f'position_{model_name}'] = self.apply_trade_cooldown(data_real, cooldown_days, f'predicted_signal_{model_name}')

                # Extract trades for the model
                trades = self.extract_trades(data_real, f'position_{model_name}')
                model_trades[model_name] = trades  # Store trades

                # Calculate strategy metrics with slippage
                logging.info(f"Calcul des rendements de la stratégie avec glissement pour {model_name}.")
                slippage_rate = 0.001  # 0.1% slippage
                data_real[f'strategy_returns_{model_name}'] = data_real[f'position_{model_name}'].shift(1) * data_real['returns']
                data_real[f'strategy_returns_net_{model_name}'] = data_real[f'strategy_returns_{model_name}'] - (commission_rate + slippage_rate) * data_real[f'position_{model_name}'].diff().abs()
                data_real[f'cumulative_strategy_returns_{model_name}'] = (1 + data_real[f'strategy_returns_net_{model_name}']).cumprod()

                # Store cumulative returns for plotting
                model_strategy_returns[model_name] = data_real[f'cumulative_strategy_returns_{model_name}']

            # Also calculate market cumulative returns
            data_real['cumulative_market_returns'] = (1 + data_real['returns']).cumprod()

            # Financial Metrics
            days_in_year = 252
            financial_metrics = {}
            for model_name in best_models.keys():
                annualized_strategy_returns = data_real[f'strategy_returns_net_{model_name}'].mean() * days_in_year
                annualized_strategy_volatility = data_real[f'strategy_returns_net_{model_name}'].std() * np.sqrt(days_in_year)
                sharpe_ratio_strategy = (annualized_strategy_returns - risk_free_rate) / annualized_strategy_volatility if annualized_strategy_volatility != 0 else np.nan

                negative_returns = data_real[f'strategy_returns_net_{model_name}'][data_real[f'strategy_returns_net_{model_name}'] < 0]
                negative_volatility = negative_returns.std() * np.sqrt(days_in_year)
                sortino_ratio_strategy = (annualized_strategy_returns - risk_free_rate) / negative_volatility if negative_volatility != 0 else np.nan

                cumulative_max = data_real[f'cumulative_strategy_returns_{model_name}'].cummax()
                drawdown = (data_real[f'cumulative_strategy_returns_{model_name}'] - cumulative_max) / cumulative_max
                max_drawdown_strategy = drawdown.min()
                calmar_ratio_strategy = annualized_strategy_returns / abs(max_drawdown_strategy) if max_drawdown_strategy != 0 else np.nan

                financial_metrics[model_name] = {
                    'Annualized Return': annualized_strategy_returns,
                    'Annualized Volatility': annualized_strategy_volatility,
                    'Sharpe Ratio': sharpe_ratio_strategy,
                    'Sortino Ratio': sortino_ratio_strategy,
                    'Calmar Ratio': calmar_ratio_strategy,
                    'Max Drawdown': max_drawdown_strategy
                }

            # Market Metrics
            annualized_market_returns = data_real['returns'].mean() * days_in_year
            annualized_market_volatility = data_real['returns'].std() * np.sqrt(days_in_year)
            sharpe_ratio_market = (annualized_market_returns - risk_free_rate) / annualized_market_volatility if annualized_market_volatility != 0 else np.nan

            negative_market_returns = data_real['returns'][data_real['returns'] < 0]
            negative_market_volatility = negative_market_returns.std() * np.sqrt(days_in_year)
            sortino_ratio_market = (annualized_market_returns - risk_free_rate) / negative_market_volatility if negative_market_volatility != 0 else np.nan

            cumulative_market_max = data_real['cumulative_market_returns'].cummax()
            market_drawdown = (data_real['cumulative_market_returns'] - cumulative_market_max) / cumulative_market_max
            max_drawdown_market = market_drawdown.min()
            calmar_ratio_market = annualized_market_returns / abs(max_drawdown_market) if max_drawdown_market != 0 else np.nan

            # Number of Trades per Model
            number_of_trades = {}
            for model_name in best_models.keys():
                number_of_trades[model_name] = data_real[f'position_{model_name}'].diff().abs().sum()

            # Calcul de l'Indice de Confiance
            if model_roc_auc:
                confidence_index = np.mean(list(model_roc_auc.values())) * 100  # Normalisé sur 0-100
                confidence_index = round(confidence_index, 2)
                logging.info(f"Indice de Confiance Calculé : {confidence_index}")
            else:
                confidence_index = "N/A"

            # Display the information
            output_info = "--- Évaluation des Modèles ---\n\n" + evaluation_results
            output_info += cross_val_evaluation  # Added cross-validation results
            output_info += "--- Résumé de la Stratégie ---\n\n"
            for model_name, metrics in financial_metrics.items():
                output_info += f"--- {model_name} ---\n"
                output_info += f"Rendement Annualisé : {metrics['Annualized Return']:.2%}\n"
                output_info += f"Volatilité Annualisée : {metrics['Annualized Volatility']:.2%}\n"
                output_info += f"Ratio de Sharpe : {metrics['Sharpe Ratio']:.2f}\n"
                output_info += f"Ratio de Sortino : {metrics['Sortino Ratio']:.2f}\n"
                output_info += f"Ratio de Calmar : {metrics['Calmar Ratio']:.2f}\n"
                output_info += f"Max Drawdown : {metrics['Max Drawdown']:.2%}\n"
                output_info += f"Nombre de Transactions : {number_of_trades[model_name]:.0f}\n\n"

                # Display Last Trade Information
                if model_trades[model_name]:
                    last_trade = model_trades[model_name][-1]
                    last_trade_date = last_trade['Date'].strftime('%Y-%m-%d')
                    last_trade_type = last_trade['Type']
                    output_info += f"Dernière Transaction pour {model_name} : {last_trade_type} le {last_trade_date}\n\n"
                else:
                    output_info += f"Aucune transaction exécutée pour {model_name}.\n\n"

            # Indice de Confiance
            output_info += f"--- Indice de Confiance ---\n\n"
            output_info += f"Indice de Confiance pour le Mois à Venir : {confidence_index}\n\n"

            # Market Metrics
            output_info += "--- Métriques du Marché ---\n"
            output_info += f"Rendement Annualisé : {annualized_market_returns:.2%}\n"
            output_info += f"Volatilité Annualisée : {annualized_market_volatility:.2%}\n"
            output_info += f"Ratio de Sharpe : {sharpe_ratio_market:.2f}\n"
            output_info += f"Ratio de Sortino : {sortino_ratio_market:.2f}\n"
            output_info += f"Ratio de Calmar : {calmar_ratio_market:.2f}\n"

            self.after(0, self.update_output, output_info)

            # Plot the strategy and market performance with dual scales
            fig, ax1 = plt.subplots(figsize=(12, 6), dpi=100)
            ax2 = ax1.twinx()  # Create a second y-axis for linear scale

            # Plot market returns on ax1 (log scale)
            ax1.plot(data_real.index, data_real['cumulative_market_returns'], label='Rendements du Marché', color='black', linestyle='--')
            ax1.set_yscale('log')

            # Plot each model's strategy returns on ax1 (log scale)
            colors = plt.cm.get_cmap('tab10', len(best_models))
            for idx, (model_name, cum_returns) in enumerate(model_strategy_returns.items()):
                ax1.plot(data_real.index, cum_returns, label=f'Rendements {model_name}', color=colors(idx))

            # Plot Trade Markers for Each Model on ax2 (linear scale)
            for idx, (model_name, trades) in enumerate(model_trades.items()):
                buy_dates = [trade['Date'] for trade in trades if trade['Type'] == 'Achat']
                sell_dates = [trade['Date'] for trade in trades if trade['Type'] == 'Vente']
                if buy_dates:
                    buy_values = data_real.loc[buy_dates, f'cumulative_strategy_returns_{model_name}']
                    ax2.scatter(buy_dates, buy_values, marker='^', color='green', s=100, label=f'{model_name} Transactions d\'Achat')
                if sell_dates:
                    sell_values = data_real.loc[sell_dates, f'cumulative_strategy_returns_{model_name}']
                    ax2.scatter(sell_dates, sell_values, marker='v', color='red', s=100, label=f'{model_name} Transactions de Vente')

            ax1.set_title('Performance de la Stratégie vs. Marché')
            ax1.set_xlabel('Date')
            ax1.set_ylabel('Rendements Cumulés (Échelle Log)')
            ax2.set_ylabel('Marqueurs de Transactions (Échelle Linéaire)')
            ax1.legend(loc='upper left', bbox_to_anchor=(0.1, 0.9))
            plt.tight_layout()

            # Add metrics text box
            textstr = (
                f"Rendement Annualisé du Marché : {annualized_market_returns:.2%}\n"
                f"Volatilité Annualisée du Marché : {annualized_market_volatility:.2%}\n"
                f"Ratio de Sharpe du Marché : {sharpe_ratio_market:.2f}\n"
                f"Ratio de Sortino du Marché : {sortino_ratio_market:.2f}\n"
                f"Ratio de Calmar du Marché : {calmar_ratio_market:.2f}\n"
                f"\nIndice de Confiance : {confidence_index}"
            )
            ax1.text(0.05, 0.95, textstr, transform=ax1.transAxes, fontsize=10,
                     verticalalignment='top', bbox=dict(facecolor='white', alpha=0.8))

            # Update the plot in a separate window with enhanced interactivity
            self.after(0, self.update_plot, fig, data_real)

            self.after(0, self.update_progress, 100, "Analyse terminée.")
            # Re-enable the run button and other inputs
            self.after(0, self.enable_inputs)
            # Enable the save button
            self.after(0, lambda: self.save_button.config(state=tk.NORMAL))
            logging.info("Analyse terminée avec succès.")

        except ValueError as ve:
            self.after(0, self.update_progress, 0, "Erreur d'analyse.")
            messagebox.showerror("Erreur d'Analyse", str(ve))
            logging.error(f"Erreur d'analyse : {ve}")
            self.after(0, self.enable_inputs)
            self.status_var.set("Prêt")
        except Exception as e:
            self.after(0, self.update_progress, 0, "Erreur d'analyse.")
            messagebox.showerror("Erreur d'Analyse", f"Une erreur est survenue lors de l'analyse : {e}")
            logging.error(f"Erreur lors de l'analyse : {e}")
            self.after(0, self.enable_inputs)
            self.status_var.set("Prêt")

    def apply_trade_cooldown(self, data, cooldown_days, signal_column):
        """
        Applies a cooldown period between trades to avoid overtrading.

        Parameters:
            data (pd.DataFrame): The DataFrame containing predicted signals.
            cooldown_days (int): The minimum number of days between trades.
            signal_column (str): The column name for the predicted signals.

        Returns:
            pd.Series: The adjusted position series after applying cooldown.
        """
        positions = data[signal_column].copy()
        position_change = positions.diff().fillna(0).abs() > 0
        cooldown = cooldown_days

        # Initialize the last trade index to a value that allows the first trade
        last_trade = -cooldown - 1

        for i in range(len(positions)):
            if position_change.iloc[i]:
                if (i - last_trade) >= cooldown:
                    last_trade = i
                else:
                    positions.iloc[i] = positions.iloc[i - 1]
        return positions

    def extract_trades(self, data, position_col):
        """
        Extracts trade information based on position changes.

        Parameters:
            data (pd.DataFrame): The DataFrame containing positions.
            position_col (str): The column name for positions.

        Returns:
            list of dict: A list containing trade details with date and type.
        """
        trades = []
        prev_position = data[position_col].iloc[0]
        for date, current_position in data[position_col].items():
            if current_position != prev_position:
                if current_position > prev_position:
                    trade_type = 'Achat'
                else:
                    trade_type = 'Vente'
                trades.append({'Date': date, 'Type': trade_type})
                prev_position = current_position
        return trades

if __name__ == "__main__":
    try:
        app = TradingApp()
        app.mainloop()
    except Exception as e:
        logging.critical(f"Application crash : {e}")
        sys.exit(1)