Améliore le Module de Nouvelles Économiques :
Catégorisation et Filtrage : Permettre aux utilisateurs de filtrer les nouvelles par catégorie, date ou source.
Recherche Intégrée : Ajouter une fonctionnalité de recherche pour trouver rapidement des articles spécifiques.

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

# 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()  # For thread safety
        self.stop_event = Event()  # Event to signal thread termination

        # Define multiple news sources **before** creating widgets
        self.news_sources = {
            "BBC Business": "http://feeds.bbci.co.uk/news/business/rss.xml",
            "Reuters Business": "http://feeds.reuters.com/reuters/businessNews",
            "CNBC": "https://www.cnbc.com/id/100003114/device/rss/rss.html",
            "Bloomberg": "https://www.bloomberg.com/feed/podcast/etf-report.xml",  # Example feed
            "Custom": ""  # Option to add a custom RSS URL
        }

        self.create_widgets()  # Now safe to call create_widgets
        self.news_data = []

        # Handle graceful closure
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

    def create_widgets(self):
        # Use a Notebook for tabs
        notebook = ttk.Notebook(self.root)
        notebook.pack(fill=tk.BOTH, expand=True)

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

        # Macroeconomic Indicators Tab
        macro_tab = ttk.Frame(notebook)
        notebook.add(macro_tab, text='Macroéconomie')
        self.create_macro_widgets(macro_tab)

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

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

    ## Stock Data Tab ##
    def create_stock_widgets(self, parent):
        control_frame = ttk.Frame(parent)
        control_frame.pack(pady=10, padx=10, fill=tk.X)

        # Stock Symbol
        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")  # Default symbol

        # Start Date
        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")

        # End Date
        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'))

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

        # Frame for the plot
        self.plot_frame = ttk.Frame(parent)
        self.plot_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # Alert Section
        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")  # Default value

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

        # Listbox for defined alerts
        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')

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

    def fetch_data(self):
        # Disable fetch button and show status
        self.fetch_button.config(state='disabled')
        self.status_var.set("Récupération des données...")
        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()

        # Validate symbol
        if not re.match("^[A-Z0-9.-]{1,10}$", symbol):
            self.show_error("Erreur", "Veuillez entrer un symbole boursier valide (lettres, chiffres, points, tirets).")
            self.update_fetch_status(False)
            return

        # Validate 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.")
                self.update_fetch_status(False)
                return
        except ValueError:
            self.show_error("Erreur", "Veuillez entrer des dates au format YYYY-MM-DD.")
            self.update_fetch_status(False)
            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.")
                self.update_fetch_status(False)
                return

            # Compute technical indicators
            data = self.compute_technical_indicators(data)

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

    def update_fetch_status(self, success):
        # Re-enable fetch button and update status
        if success:
            self.status_var.set("Données récupérées avec succès.")
        else:
            self.status_var.set("Erreur lors de la récupération des données.")
        self.fetch_button.config(state='normal')

    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']

            # Calculate 20-day Simple Moving Average (SMA)
            sma = SMAIndicator(close=data_close, window=20)
            data['SMA20'] = sma.sma_indicator()

            # Calculate 14-day Relative Strength Index (RSI)
            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}")
            self.show_error("Erreur", f"Erreur lors du calcul des indicateurs techniques : {e}")
        return data

    def plot_data(self, data, symbol):
        try:
            # Clear previous plot
            for widget in self.plot_frame.winfo_children():
                widget.destroy()

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

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

            # Plot Close Prices and 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)

            # Plot 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)

            # Add forecast button
            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}")
            self.show_error("Erreur", f"Erreur lors de l'affichage des graphiques : {e}")

    def plot_forecast(self, data, symbol, periods=30):
        # Disable forecast button and show status
        self.status_var.set("Génération des prévisions...")
        threading.Thread(target=self.plot_forecast_thread, args=(data, symbol, periods), daemon=True).start()

    def plot_forecast_thread(self, data, symbol, periods):
        try:
            # Prepare data for ARIMA
            df = data['Close'].dropna()
            if isinstance(df, pd.DataFrame):
                df = df.iloc[:, 0]  # Assume first column if multiple
            required_length = 30  # 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.")
                self.update_fetch_status(False)
                return

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

            # Dates for forecasts (business days)
            last_date = df.index[-1]
            forecast_dates = pd.bdate_range(last_date + pd.Timedelta(days=1), periods=periods)

            # Create the plot
            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()

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

    def display_forecast(self, fig):
        try:
            # Display in a new window
            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)

            # Add a close button
            close_button = ttk.Button(forecast_window, text="Fermer", command=forecast_window.destroy)
            close_button.pack(pady=5)
        except Exception as e:
            logging.error(f"Erreur lors de l'affichage des prévisions: {e}")
            self.show_error("Erreur", f"Erreur lors de l'affichage des prévisions : {e}")

    ## Macroeconomic Indicators Tab ##
    def create_macro_widgets(self, parent):
        macro_frame = ttk.Frame(parent)
        macro_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # Section Title
        ttk.Label(macro_frame, text="Indicateurs Macroéconomiques", font=("Helvetica", 16)).pack(pady=10)

        # Indicators Table
        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)

        # Fetch Indicators Button
        self.fetch_macro_button = ttk.Button(macro_frame, text="Récupérer Indicateurs", command=self.fetch_macro_data)
        self.fetch_macro_button.pack(pady=5)

    def fetch_macro_data(self):
        # Disable fetch button and show status
        self.fetch_macro_button.config(state='disabled')
        self.set_status("Récupération des indicateurs macroéconomiques...")
        threading.Thread(target=self.fetch_macro_data_thread, daemon=True).start()

    def fetch_macro_data_thread(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%"
            }

            # Clear old data
            self.root.after(0, self.clear_macro_tree)

            # Insert new data
            for indicateur, valeur in macro_indicators.items():
                self.root.after(0, lambda i=indicateur, v=valeur: self.macro_tree.insert("", "end", values=(i, v)))

            self.set_status("Indicateurs macroéconomiques récupérés avec succès.")
        except Exception as e:
            logging.error(f"Erreur lors de la récupération des indicateurs macroéconomiques: {e}")
            self.show_error("Erreur", f"Impossible de récupérer les indicateurs macroéconomiques : {e}")
            self.set_status("Erreur lors de la récupération des indicateurs macroéconomiques.")
        finally:
            self.fetch_macro_button.config(state='normal')

    def clear_macro_tree(self):
        for row in self.macro_tree.get_children():
            self.macro_tree.delete(row)

    ## Economic News Tab ##
    def create_news_widgets(self, parent):
        news_frame = ttk.Frame(parent)
        news_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # Section Title
        ttk.Label(news_frame, text="Actualités Économiques", font=("Helvetica", 16)).pack(pady=10)

        # News Source Selection
        source_frame = ttk.Frame(news_frame)
        source_frame.pack(pady=5, padx=10, fill=tk.X)

        ttk.Label(source_frame, text="Source d'Actualités:").pack(side=tk.LEFT, padx=(0, 5))

        self.news_source_var = tk.StringVar()
        self.news_source_combo = ttk.Combobox(source_frame, textvariable=self.news_source_var,
                                             values=list(self.news_sources.keys()), state='readonly')
        self.news_source_combo.current(0)  # Set default to the first source
        self.news_source_combo.pack(side=tk.LEFT, padx=(0, 5))

        # Button to add a custom source
        add_source_button = ttk.Button(source_frame, text="Ajouter Source", command=self.add_custom_source)
        add_source_button.pack(side=tk.LEFT, padx=(5, 0))

        # Fetch News Button
        fetch_frame = ttk.Frame(news_frame)
        fetch_frame.pack(pady=5, padx=10, fill=tk.X)

        self.fetch_news_button = ttk.Button(fetch_frame, text="Récupérer Actualités", command=self.fetch_news)
        self.fetch_news_button.pack(side=tk.LEFT, pady=5)

        # Copy All Titles Button
        copy_button = ttk.Button(fetch_frame, text="Copier Tous les Titres", command=self.copy_all_titles)
        copy_button.pack(side=tk.LEFT, padx=5, pady=5)

        # Listbox to display news
        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)

        # Scrollbar for the list
        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 add_custom_source(self):
        # Prompt user to enter a custom RSS URL
        custom_url = simpledialog.askstring("Ajouter Source", "Entrez l'URL du flux RSS:")
        if custom_url:
            # Validate the URL format
            if not re.match(r'^https?:\/\/', custom_url):
                self.show_error("Erreur", "Veuillez entrer une URL valide commençant par http:// ou https://")
                return
            # Find a unique key for the custom source
            custom_keys = [key for key in self.news_sources.keys() if key.startswith("Custom")]
            new_custom_key = f"Custom {len(custom_keys) + 1}"
            self.news_sources[new_custom_key] = custom_url
            # Refresh the Combobox to include the updated sources
            self.news_source_combo['values'] = list(self.news_sources.keys())
            self.news_source_combo.set(new_custom_key)
            self.show_info("Source Ajoutée", f"La source personnalisée '{new_custom_key}' a été ajoutée avec succès.")

    def fetch_news(self):
        # Disable fetch button and show status
        self.fetch_news_button.config(state='disabled')
        self.set_status("Récupération des actualités...")
        threading.Thread(target=self.fetch_news_thread, daemon=True).start()

    def fetch_news_thread(self):
        try:
            selected_source = self.news_source_var.get()
            rss_url = self.news_sources.get(selected_source, '')

            if selected_source.startswith("Custom") and not rss_url:
                self.show_error("Erreur", "Veuillez fournir une URL valide pour la source personnalisée.")
                self.set_status("Erreur lors de la récupération des actualités.")
                return

            # If RSS URL is empty, use the selected source's URL
            if not rss_url:
                rss_url = self.news_sources.get(selected_source, 'http://feeds.bbci.co.uk/news/business/rss.xml')  # Default to 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]:  # Limit to 20 articles
                title = entry.title if 'title' in entry else 'Sans Titre'
                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  # Store data for future use

            # Schedule GUI updates on the main thread
            self.root.after(0, self.update_news_list)
            self.set_status("Actualités récupérées avec succès.")
        except Exception as e:
            logging.error(f"Erreur lors de la récupération des actualités: {e}")
            self.show_error("Erreur", f"Impossible de récupérer les actualités : {e}")
            self.set_status("Erreur lors de la récupération des actualités.")
        finally:
            self.fetch_news_button.config(state='normal')

    def update_news_list(self):
        # Clear old data
        self.news_list.delete(0, tk.END)

        # Insert new data
        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):  # Validate URL
                try:
                    webbrowser.open(link, new=1)
                except Exception as e:
                    logging.error(f"Erreur lors de l'ouverture du lien: {e}")
                    self.show_error("Erreur", f"Impossible d'ouvrir le lien : {e}")
            else:
                self.show_error("Erreur", "URL invalide.")

    def copy_all_titles(self):
        if not self.news_data:
            self.show_error("Erreur", "Aucun titre à copier.")
            return
        all_titles = "\n".join([item['title'] for item in self.news_data])
        try:
            self.root.clipboard_clear()
            self.root.clipboard_append(all_titles)
            self.show_info("Succès", "Tous les titres ont été copiés dans le presse-papiers.")
        except Exception as e:
            logging.error(f"Erreur lors de la copie des titres: {e}")
            self.show_error("Erreur", f"Impossible de copier les titres : {e}")

    ## Alert System ##
    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:
                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.stop_event.clear()  # Reset the stop_event before starting the thread
                    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:
                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
                    self.stop_event.set()
        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}")
            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:
                                # Use 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
                        break
            except Exception as e:
                logging.error(f"Erreur lors de la vérification des alertes: {e}")
            # Use Event.wait for responsive shutdown
            self.stop_event.wait(60)  # Check every minute or until stop_event

    def on_closing(self):
        if messagebox.askokcancel("Quitter", "Voulez-vous vraiment quitter l'application?"):
            self.alert_active = False  # Deactivate alert thread
            self.stop_event.set()  # Signal threads to stop
            self.root.destroy()

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

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

    def set_status(self, message):
        # Allows setting any custom status messages
        self.root.after(0, lambda: self.status_var.set(message))


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

    # Apply a theme
    style = ttk.Style()
    style.theme_use('clam')  # Options: 'default', 'clam', 'alt', 'classic'

    app = FinanceApp(root)
    root.mainloop()


if __name__ == "__main__":
    main()