Update the script to operate more reliably, and provide a smoother user experience without breaking it : 

import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
import yfinance as yf
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
from datetime import datetime
import pandas as pd
import feedparser
from ta.trend import SMAIndicator
from ta.momentum import RSIIndicator
from statsmodels.tsa.arima.model import ARIMA
import threading
import time
import webbrowser
import re
import logging
from threading import Event

# FIX: Configure logging
logging.basicConfig(
    filename='finance_app.log',
    level=logging.INFO,
    format='%(asctime)s:%(levelname)s:%(message)s'
)

class FinanceApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Analyse Financière")
        self.root.geometry("1200x800")
        self.alert_thresholds = []  # List to store multiple alert thresholds
        self.alert_active = False
        self.alert_lock = threading.Lock()  # FIX: Added for thread safety
        self.stop_event = Event()  # FIX: Event to signal thread termination
        self.create_widgets()
        self.news_data = []
        
        # FIX: Handle graceful closure
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

    def create_widgets(self):
        # Utiliser un Notebook pour les onglets
        notebook = ttk.Notebook(self.root)
        notebook.pack(fill=tk.BOTH, expand=True)

        # Onglet Données Boursières
        stock_tab = ttk.Frame(notebook)
        notebook.add(stock_tab, text='Données Boursières')
        self.create_stock_widgets(stock_tab)

        # Onglet Indicateurs Macroéconomiques
        macro_tab = ttk.Frame(notebook)
        notebook.add(macro_tab, text='Macroéconomie')
        self.create_macro_widgets(macro_tab)

        # Onglet Actualités Économiques
        news_tab = ttk.Frame(notebook)
        notebook.add(news_tab, text='Actualités Économiques')
        self.create_news_widgets(news_tab)

    ## Onglet Données Boursières ##
    def create_stock_widgets(self, parent):
        control_frame = ttk.Frame(parent)
        control_frame.pack(pady=10, padx=10, fill=tk.X)

        # Symbole Boursier
        ttk.Label(control_frame, text="Symbole Boursier:").grid(row=0, column=0, padx=5, pady=5, sticky='E')
        self.symbol_entry = ttk.Entry(control_frame, width=20)
        self.symbol_entry.grid(row=0, column=1, padx=5, pady=5, sticky='W')
        self.symbol_entry.insert(0, "AAPL")  # Symbole par défaut

        # Date de début
        ttk.Label(control_frame, text="Date de début (YYYY-MM-DD):").grid(row=1, column=0, padx=5, pady=5, sticky='E')
        self.start_entry = ttk.Entry(control_frame, width=20)
        self.start_entry.grid(row=1, column=1, padx=5, pady=5, sticky='W')
        self.start_entry.insert(0, "2020-01-01")

        # Date de fin
        ttk.Label(control_frame, text="Date de fin (YYYY-MM-DD):").grid(row=2, column=0, padx=5, pady=5, sticky='E')
        self.end_entry = ttk.Entry(control_frame, width=20)
        self.end_entry.grid(row=2, column=1, padx=5, pady=5, sticky='W')
        self.end_entry.insert(0, datetime.today().strftime('%Y-%m-%d'))

        # Bouton Récupérer et Afficher
        fetch_button = ttk.Button(control_frame, text="Récupérer et Afficher", command=self.fetch_data)
        fetch_button.grid(row=3, column=0, columnspan=2, pady=10)

        # Frame pour le graphique
        self.plot_frame = ttk.Frame(parent)
        self.plot_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # Section Alertes
        alert_frame = ttk.LabelFrame(parent, text="Alertes Personnalisées")
        alert_frame.pack(pady=10, padx=10, fill=tk.X)

        ttk.Label(alert_frame, text="Alerte: Prix ≥ ").grid(row=0, column=0, padx=5, pady=5, sticky='E')
        self.alert_entry = ttk.Entry(alert_frame, width=15)
        self.alert_entry.grid(row=0, column=1, padx=5, pady=5, sticky='W')
        self.alert_entry.insert(0, "150")  # Valeur par défaut

        set_alert_button = ttk.Button(alert_frame, text="Définir Alerte", command=self.set_alert)
        set_alert_button.grid(row=0, column=2, padx=5, pady=5)

        # Liste des alertes définies
        self.alerts_listbox = tk.Listbox(alert_frame, height=5)
        self.alerts_listbox.grid(row=1, column=0, columnspan=3, padx=5, pady=5, sticky='WE')

        remove_alert_button = ttk.Button(alert_frame, text="Supprimer Alerte Sélectionnée",
                                        command=self.remove_selected_alert)
        remove_alert_button.grid(row=2, column=0, columnspan=3, pady=5)

    def fetch_data(self):
        # FIX: Run fetch_data in a separate thread to keep GUI responsive
        threading.Thread(target=self.fetch_data_thread, daemon=True).start()

    def fetch_data_thread(self):
        symbol = self.symbol_entry.get().strip().upper()
        start_date = self.start_entry.get().strip()
        end_date = self.end_entry.get().strip()

        # FIX: Rearrange the hyphen to avoid escape sequence issues
        if not re.match("^[A-Z.-]{1,10}$", symbol):
            self.show_error("Erreur", "Veuillez entrer un symbole boursier valide (lettres, points, tirets).")
            return

        # Validation des dates
        try:
            start_dt = datetime.strptime(start_date, '%Y-%m-%d')
            end_dt = datetime.strptime(end_date, '%Y-%m-%d')
            if start_dt >= end_dt:
                self.show_error("Erreur", "La date de début doit être antérieure à la date de fin.")
                return
        except ValueError:
            self.show_error("Erreur", "Veuillez entrer des dates au format YYYY-MM-DD.")
            return

        try:
            data = yf.download(symbol, start=start_date, end=end_date)
            if data.empty:
                self.show_error("Erreur", "Aucune donnée trouvée pour ce symbole.")
                return

            # Calculer les indicateurs techniques
            data = self.compute_technical_indicators(data)

            # FIX: Schedule plot_data to run in the main thread
            self.root.after(0, lambda: self.plot_data(data, symbol))
        except Exception as e:
            logging.error(f"Erreur lors de la récupération des données: {e}")  # FIX: Log the error
            self.show_error("Erreur", f"Impossible de récupérer les données : {e}")

    def compute_technical_indicators(self, data):
        try:
            # Ensure 'Close' is a 1D Series, not a 2D DataFrame
            if isinstance(data['Close'], pd.DataFrame):
                # Assuming only one symbol is being processed here
                data_close = data['Close'].iloc[:, 0]
            else:
                data_close = data['Close']

            # Calcul de la Moyenne Mobile Simple (SMA) à 20 jours
            sma = SMAIndicator(close=data_close, window=20)
            data['SMA20'] = sma.sma_indicator()

            # Calcul du Relative Strength Index (RSI) à 14 jours
            rsi = RSIIndicator(close=data_close, window=14)
            data['RSI'] = rsi.rsi()
        except Exception as e:
            logging.error(f"Erreur lors du calcul des indicateurs techniques: {e}")  # FIX: Log the error
            self.show_error("Erreur", f"Erreur lors du calcul des indicateurs techniques : {e}")
        return data

    def plot_data(self, data, symbol):
        try:
            # Effacer le graphique précédent
            for widget in self.plot_frame.winfo_children():
                widget.destroy()

            plt.close('all')  # FIX: Close previous figures to prevent memory leaks

            fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)

            # Graphe des prix de clôture et SMA
            ax1.plot(data.index, data['Close'], label='Prix de Clôture')
            ax1.plot(data.index, data['SMA20'], label='SMA 20')
            ax1.set_title(f"Prix de Clôture Historique de {symbol} avec SMA 20")
            ax1.set_ylabel("Prix (USD)")
            ax1.legend()
            ax1.grid(True)

            # Graphe RSI
            ax2.plot(data.index, data['RSI'], label='RSI', color='orange')
            ax2.axhline(70, color='red', linestyle='--')
            ax2.axhline(30, color='green', linestyle='--')
            ax2.set_title("Indice de Force Relative (RSI)")
            ax2.set_ylabel("RSI")
            ax2.legend()
            ax2.grid(True)

            fig.tight_layout()

            canvas = FigureCanvasTkAgg(fig, master=self.plot_frame)
            canvas.draw()
            canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

            # Ajouter bouton pour les prévisions
            forecast_button = ttk.Button(self.plot_frame, text="Afficher Prévisions",
                                         command=lambda: self.plot_forecast(data, symbol))
            forecast_button.pack(pady=5)
        except Exception as e:
            logging.error(f"Erreur lors de l'affichage des graphiques: {e}")  # FIX: Log the error
            self.show_error("Erreur", f"Erreur lors de l'affichage des graphiques : {e}")

    def plot_forecast(self, data, symbol, periods=30):
        # FIX: Run plot_forecast in a separate thread to keep GUI responsive
        threading.Thread(target=self.plot_forecast_thread, args=(data, symbol, periods), daemon=True).start()

    def plot_forecast_thread(self, data, symbol, periods):
        try:
            # Préparation des données pour ARIMA
            df = data['Close'].dropna()
            # FIX: Check if data is sufficient
            if isinstance(df, pd.DataFrame):
                df = df.iloc[:, 0]  # Assume first column if multiple
            required_length = 30  # Example minimum length for ARIMA
            if len(df) < required_length:
                self.show_error("Erreur", f"Données insuffisantes pour générer des prévisions. Au moins {required_length} points de données sont requis.")
                return

            model = ARIMA(df, order=(5, 1, 0))
            model_fit = model.fit()
            forecast = model_fit.forecast(steps=periods)

            # Dates pour les prévisions (jours ouvrés)
            last_date = df.index[-1]
            forecast_dates = pd.bdate_range(last_date + pd.Timedelta(days=1), periods=periods)

            # Création du graphique
            fig, ax = plt.subplots(figsize=(10, 6))
            ax.plot(data.index, data['Close'], label='Prix de Clôture')
            ax.plot(forecast_dates, forecast, label='Prévisions', linestyle='--', color='red')
            ax.set_title(f"Prévision des Prix de {symbol} avec ARIMA")
            ax.set_xlabel("Date")
            ax.set_ylabel("Prix (USD)")
            ax.legend()
            ax.grid(True)

            fig.tight_layout()

            # FIX: Schedule the creation of forecast window in the main thread
            self.root.after(0, lambda: self.display_forecast(fig))
        except Exception as e:
            logging.error(f"Erreur lors de la génération des prévisions: {e}")  # FIX: Log the error
            self.show_error("Erreur", f"Impossible de générer les prévisions : {e}")

    def display_forecast(self, fig):
        try:
            # Afficher dans un nouveau cadre
            forecast_window = tk.Toplevel(self.root)
            forecast_window.title("Prévisions des Prix")
            forecast_window.geometry("800x600")

            canvas = FigureCanvasTkAgg(fig, master=forecast_window)
            canvas.draw()
            canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
        except Exception as e:
            logging.error(f"Erreur lors de l'affichage des prévisions: {e}")  # FIX: Log the error
            self.show_error("Erreur", f"Erreur lors de l'affichage des prévisions : {e}")

    ## Onglet Indicateurs Macroéconomiques ##
    def create_macro_widgets(self, parent):
        macro_frame = ttk.Frame(parent)
        macro_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # Titre de la section
        ttk.Label(macro_frame, text="Indicateurs Macroéconomiques", font=("Helvetica", 16)).pack(pady=10)

        # Tableau des indicateurs
        self.macro_tree = ttk.Treeview(macro_frame, columns=("Indicateur", "Valeur"), show='headings')
        self.macro_tree.heading("Indicateur", text="Indicateur")
        self.macro_tree.heading("Valeur", text="Valeur")
        self.macro_tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # Bouton pour récupérer les indicateurs
        fetch_macro_button = ttk.Button(macro_frame, text="Récupérer Indicateurs", command=self.fetch_macro_data)
        fetch_macro_button.pack(pady=5)

    def fetch_macro_data(self):
        try:
            # Placeholder: Replace with a reliable API or a properly parsed source
            # For demonstration, using static data
            macro_indicators = {
                "PIB (USD)": "21.43 Trillion",
                "Taux de Chômage (%)": "3.5%",
                "Inflation Annuelle (%)": "2.4%",
                "Taux d'Intérêt (%)": "1.75%"
            }

            # Effacer les anciennes données
            for row in self.macro_tree.get_children():
                self.macro_tree.delete(row)

            # Insérer les nouvelles données
            for indicateur, valeur in macro_indicators.items():
                self.macro_tree.insert("", "end", values=(indicateur, valeur))

        except Exception as e:
            logging.error(f"Erreur lors de la récupération des indicateurs macroéconomiques: {e}")  # FIX: Log the error
            self.show_error("Erreur", f"Impossible de récupérer les indicateurs macroéconomiques : {e}")

    ## Onglet Actualités Économiques ##
    def create_news_widgets(self, parent):
        news_frame = ttk.Frame(parent)
        news_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # Titre de la section
        ttk.Label(news_frame, text="Actualités Économiques", font=("Helvetica", 16)).pack(pady=10)

        # Bouton pour récupérer les actualités
        fetch_news_button = ttk.Button(news_frame, text="Récupérer Actualités", command=self.fetch_news)
        fetch_news_button.pack(pady=5)

        # Liste pour afficher les actualités
        self.news_list = tk.Listbox(news_frame, height=20)
        self.news_list.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        self.news_list.bind('<<ListboxSelect>>', self.open_news_link)

        # Barre de défilement pour la liste
        scrollbar = ttk.Scrollbar(news_frame, orient="vertical", command=self.news_list.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.news_list.config(yscrollcommand=scrollbar.set)

    def fetch_news(self):
        # FIX: Run fetch_news in a separate thread to keep GUI responsive
        threading.Thread(target=self.fetch_news_thread, daemon=True).start()

    def fetch_news_thread(self):
        try:
            # Utiliser un flux RSS économique public
            rss_url = 'http://feeds.bbci.co.uk/news/business/rss.xml'  # Exemple avec BBC Business
            feed = feedparser.parse(rss_url)

            if feed.bozo:
                raise Exception("Erreur dans le parsing du flux RSS.")

            news = []
            for entry in feed.entries[:20]:  # Limiter à 20 articles
                title = entry.title if 'title' in entry else 'Sans Titre'  # FIX: Robust parsing
                link = entry.link if 'link' in entry else None
                published = entry.published if 'published' in entry else 'No Date'
                if link:
                    news.append({'title': title, 'link': link, 'published': published})

            self.news_data = news  # Stocker les données pour utilisation future

            # FIX: Schedule GUI updates on the main thread
            self.root.after(0, self.update_news_list)

        except Exception as e:
            logging.error(f"Erreur lors de la récupération des actualités: {e}")  # FIX: Log the error
            self.show_error("Erreur", f"Impossible de récupérer les actualités : {e}")

    def update_news_list(self):
        # Effacer les anciennes données
        self.news_list.delete(0, tk.END)

        # Insérer les nouvelles données
        for item in self.news_data:
            self.news_list.insert(tk.END, item['title'])

    def open_news_link(self, event):
        selection = event.widget.curselection()
        if selection:
            index = selection[0]
            link = self.news_data[index]['link']
            if re.match(r'^https?:\/\/', link):  # FIX: Validate URL
                try:
                    webbrowser.open(link, new=1)
                except Exception as e:
                    logging.error(f"Erreur lors de l'ouverture du lien: {e}")  # FIX: Log the error
                    self.show_error("Erreur", f"Impossible d'ouvrir le lien : {e}")
            else:
                self.show_error("Erreur", "URL invalide.")

    ## Système d'Alertes ##
    def set_alert(self):
        try:
            threshold = float(self.alert_entry.get())
            symbol = self.symbol_entry.get().strip().upper()

            if not symbol:
                self.show_error("Erreur", "Veuillez définir d'abord le symbole boursier.")
                return

            alert = {'symbol': symbol, 'threshold': threshold}

            with self.alert_lock:  # FIX: Ensure thread-safe access
                if alert not in self.alert_thresholds:
                    self.alert_thresholds.append(alert)
                    self.alerts_listbox.insert(tk.END, f"{symbol} ≥ {threshold} USD")
                    self.show_info("Alerte Définie", f"Alerte définie pour {symbol} à {threshold} USD.")
                else:
                    self.show_error("Erreur", f"L'alerte pour {symbol} ≥ {threshold} USD existe déjà.")

                if not self.alert_active:
                    self.alert_active = True
                    alert_thread = threading.Thread(target=self.check_price_alert, daemon=True)
                    alert_thread.start()
        except ValueError:
            self.show_error("Erreur", "Veuillez entrer une valeur numérique valide pour le seuil d'alerte.")

    def remove_selected_alert(self):
        try:
            selection = self.alerts_listbox.curselection()
            if not selection:
                self.show_error("Erreur", "Veuillez sélectionner une alerte à supprimer.")
                return
            index = selection[0]
            with self.alert_lock:  # FIX: Ensure thread-safe access
                self.alerts_listbox.delete(index)
                del self.alert_thresholds[index]
            self.show_info("Alerte Supprimée", f"Alerte numéro {index + 1} supprimée.")

            # If no alerts left, deactivate the alert thread
            with self.alert_lock:
                if not self.alert_thresholds:
                    self.alert_active = False
        except IndexError:
            self.show_error("Erreur", "Erreur lors de la suppression de l'alerte.")
        except Exception as e:
            logging.error(f"Erreur lors de la suppression de l'alerte: {e}")  # FIX: Log the error
            self.show_error("Erreur", "Erreur lors de la suppression de l'alerte.")

    def check_price_alert(self):
        while not self.stop_event.is_set() and self.alert_active:
            with self.alert_lock:
                if not self.alert_thresholds:
                    self.alert_active = False
                    break
                alerts_copy = self.alert_thresholds.copy()

            try:
                symbols = list({alert['symbol'] for alert in alerts_copy})  # Unique symbols
                if not symbols:
                    break
                data = yf.download(tickers=symbols, period='1d', interval='1m', threads=True)

                for alert in alerts_copy:
                    symbol = alert['symbol']
                    threshold = alert['threshold']
                    if symbol in data['Close']:
                        close_series = data['Close'][symbol]
                        if not close_series.empty:
                            current_price = close_series.iloc[-1]
                            if current_price >= threshold:
                                # FIX: Use self.root.after to ensure GUI updates occur in main thread
                                self.root.after(0, lambda s=symbol, p=current_price: self.show_info(
                                    "Alerte de Prix", f"Le prix de {s} a atteint {p:.2f} USD."))
                                with self.alert_lock:
                                    if alert in self.alert_thresholds:
                                        index = self.alert_thresholds.index(alert)
                                        self.alerts_listbox.delete(index)
                                        del self.alert_thresholds[index]
                with self.alert_lock:
                    if not self.alert_thresholds:
                        self.alert_active = False
            except Exception as e:
                logging.error(f"Erreur lors de la vérification des alertes: {e}")  # FIX: Log the error
            # FIX: Use Event.wait for responsive shutdown
            self.stop_event.wait(60)  # Vérifier toutes les minutes ou jusqu'à stop_event

    def on_closing(self):
        self.alert_active = False  # FIX: Deactivate alert thread
        self.stop_event.set()  # Signal threads to stop
        self.root.destroy()

    def show_error(self, title, message):
        # FIX: Encapsulate messagebox for consistency and thread safety
        self.root.after(0, lambda: messagebox.showerror(title, message))

    def show_info(self, title, message):
        # FIX: Encapsulate messagebox for consistency and thread safety
        self.root.after(0, lambda: messagebox.showinfo(title, message))


def main():
    root = tk.Tk()

    # Appliquer un thème
    style = ttk.Style()
    style.theme_use('clam')  # Options : 'default', 'clam', 'alt', 'classic'

    app = FinanceApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()