import sys from PyQt6.QtWidgets import (QApplication, QMainWindow, QTabWidget, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QComboBox, QLineEdit, QListWidget, QListWidgetItem, QMessageBox, QDateEdit, QSizePolicy) from PyQt6.QtCore import QDate, Qt from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure import pandas as pd import datetime from stock_data_manager import StockDataManager from financial_tools import FinancialTools class MplCanvas(FigureCanvas): """Matplotlib Canvas für die Einbettung in PyQt.""" def __init__(self, parent=None, width=5, height=4, dpi=100): fig = Figure(figsize=(width, height), dpi=dpi) self.axes = fig.add_subplot(111) super(MplCanvas, self).__init__(fig) self.setParent(parent) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.updateGeometry() class StockAnalyzer(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Aktienanalyse-Tool") self.setGeometry(100, 100, 1200, 800) self.db_manager = StockDataManager("stock_analysis.db") self.financial_tools = FinancialTools() self.central_widget = QWidget() self.setCentralWidget(self.central_widget) self.main_layout = QVBoxLayout(self.central_widget) self._create_top_panel() self._create_tabs() self._load_initial_data() def _create_top_panel(self): """Erstellt das obere Panel für CSV-Import und Symbol-Auswahl.""" top_panel_layout = QHBoxLayout() # CSV Import csv_group_layout = QVBoxLayout() csv_group_layout.addWidget(QLabel("CSV-Datei mit Symbolen importieren (Symbol,CompanyName):")) self.csv_path_input = QLineEdit("stocks.csv") csv_group_layout.addWidget(self.csv_path_input) import_csv_button = QPushButton("CSV importieren & Daten holen") import_csv_button.clicked.connect(self._import_csv_and_fetch) csv_group_layout.addWidget(import_csv_button) top_panel_layout.addLayout(csv_group_layout) top_panel_layout.addStretch(1) # Abstandhalter # Symbol Auswahl symbol_selection_layout = QVBoxLayout() symbol_selection_layout.addWidget(QLabel("Aktie auswählen:")) self.symbol_combo = QComboBox() self.symbol_combo.currentIndexChanged.connect(self._on_symbol_selected) symbol_selection_layout.addWidget(self.symbol_combo) # Datumsbereich date_range_layout = QHBoxLayout() date_range_layout.addWidget(QLabel("Von:")) self.start_date_edit = QDateEdit(QDate.currentDate().addYears(-1)) self.start_date_edit.setCalendarPopup(True) date_range_layout.addWidget(self.start_date_edit) date_range_layout.addWidget(QLabel("Bis:")) self.end_date_edit = QDateEdit(QDate.currentDate()) self.end_date_edit.setCalendarPopup(True) date_range_layout.addWidget(self.end_date_edit) symbol_selection_layout.addLayout(date_range_layout) # Refresh Button refresh_data_button = QPushButton("Daten aktualisieren") refresh_data_button.clicked.connect(self._refresh_selected_stock_data) symbol_selection_layout.addWidget(refresh_data_button) top_panel_layout.addLayout(symbol_selection_layout) self.main_layout.addLayout(top_panel_layout) def _create_tabs(self): """Erstellt die Reiter für verschiedene Ansichten.""" self.tabs = QTabWidget() self.main_layout.addWidget(self.tabs) self.tab_overview = QWidget() self.tabs.addTab(self.tab_overview, "Übersicht & Kurse") self._setup_overview_tab() self.tab_returns = QWidget() self.tabs.addTab(self.tab_returns, "Renditen") self._setup_returns_tab() self.tab_ma = QWidget() self.tabs.addTab(self.tab_ma, "Gleitender Durchschnitt") self._setup_ma_tab() self.tab_volatility = QWidget() self.tabs.addTab(self.tab_volatility, "Volatilität") self._setup_volatility_tab() self.tab_beta = QWidget() self.tabs.addTab(self.tab_beta, "Beta (Marktabhängigkeit)") self._setup_beta_tab() def _setup_overview_tab(self): layout = QVBoxLayout(self.tab_overview) self.overview_canvas = MplCanvas(self.tab_overview, width=10, height=6) layout.addWidget(self.overview_canvas) self.overview_text = QLabel("Kursdaten:") layout.addWidget(self.overview_text) def _setup_returns_tab(self): layout = QVBoxLayout(self.tab_returns) self.returns_canvas = MplCanvas(self.tab_returns, width=10, height=6) layout.addWidget(self.returns_canvas) self.returns_text = QLabel("Renditen:") layout.addWidget(self.returns_text) def _setup_ma_tab(self): layout = QVBoxLayout(self.tab_ma) ma_controls_layout = QHBoxLayout() ma_controls_layout.addWidget(QLabel("Fenster (Tage):")) self.ma_window_input = QLineEdit("20") self.ma_window_input.setFixedWidth(50) ma_controls_layout.addWidget(self.ma_window_input) self.ma_apply_button = QPushButton("Anwenden") self.ma_apply_button.clicked.connect(self._plot_ma) ma_controls_layout.addWidget(self.ma_apply_button) ma_controls_layout.addStretch(1) layout.addLayout(ma_controls_layout) self.ma_canvas = MplCanvas(self.tab_ma, width=10, height=6) layout.addWidget(self.ma_canvas) self.ma_text = QLabel("Gleitender Durchschnitt:") layout.addWidget(self.ma_text) def _setup_volatility_tab(self): layout = QVBoxLayout(self.tab_volatility) vol_controls_layout = QHBoxLayout() vol_controls_layout.addWidget(QLabel("Fenster (Tage):")) self.vol_window_input = QLineEdit("20") self.vol_window_input.setFixedWidth(50) vol_controls_layout.addWidget(self.vol_window_input) self.vol_apply_button = QPushButton("Anwenden") self.vol_apply_button.clicked.connect(self._plot_volatility) vol_controls_layout.addStretch(1) layout.addLayout(vol_controls_layout) self.vol_canvas = MplCanvas(self.tab_volatility, width=10, height=6) layout.addWidget(self.vol_canvas) self.vol_text = QLabel("Volatilität:") layout.addWidget(self.vol_text) def _setup_beta_tab(self): layout = QVBoxLayout(self.tab_beta) beta_controls_layout = QHBoxLayout() beta_controls_layout.addWidget(QLabel("Markt-Symbol (z.B. SPY):")) self.market_symbol_input = QLineEdit("SPY") self.market_symbol_input.setFixedWidth(100) beta_controls_layout.addWidget(self.market_symbol_input) beta_controls_layout.addWidget(QLabel("Fenster (Tage):")) self.beta_window_input = QLineEdit("60") self.beta_window_input.setFixedWidth(50) beta_controls_layout.addWidget(self.beta_window_input) self.beta_apply_button = QPushButton("Anwenden") self.beta_apply_button.clicked.connect(self._plot_beta) beta_controls_layout.addStretch(1) layout.addLayout(beta_controls_layout) self.beta_canvas = MplCanvas(self.tab_beta, width=10, height=6) layout.addWidget(self.beta_canvas) self.beta_text = QLabel("Beta-Wert:") layout.addWidget(self.beta_text) def _load_initial_data(self): """Lädt initial alle Symbole in die ComboBox.""" symbols = self.db_manager.get_all_symbols() if not symbols: # Füge Standard-Symbole hinzu, wenn DB leer ist default_symbols = ["AAPL", "MSFT", "GOOGL"] for s in default_symbols: self.db_manager.add_stock(s) symbols = default_symbols # Aktualisiere die Liste # Optional: Initialdaten für Standard-Symbole holen # for s in symbols: # self.db_manager.fetch_and_store_data(s, period="1y") self.symbol_combo.clear() self.symbol_combo.addItems(symbols) if symbols: self._on_symbol_selected(0) # Wähle das erste Symbol aus und zeige Daten def _import_csv_and_fetch(self): """Importiert Symbole aus CSV und holt Daten.""" csv_file = self.csv_path_input.text() try: df_symbols = pd.read_csv(csv_file) imported_count = 0 fetched_count = 0 for index, row in df_symbols.iterrows(): symbol = row['Symbol'].upper() company_name = row.get('CompanyName', '') if self.db_manager.add_stock(symbol, company_name): imported_count += 1 if self.db_manager.fetch_and_store_data(symbol, period="max"): # Holt max. verfügbare Daten fetched_count += 1 QMessageBox.information(self, "Import abgeschlossen", f"{imported_count} neue Symbole hinzugefügt.\n{fetched_count} Symbole aktualisiert/Daten geholt.") self._load_initial_data() # Symbole in ComboBox aktualisieren except FileNotFoundError: QMessageBox.warning(self, "Fehler", f"Datei '{csv_file}' nicht gefunden.") except KeyError: QMessageBox.warning(self, "Fehler", f"Die CSV-Datei muss Spalten 'Symbol' und optional 'CompanyName' enthalten.") except Exception as e: QMessageBox.critical(self, "Fehler", f"Ein unerwarteter Fehler ist aufgetreten: {e}") def _refresh_selected_stock_data(self): """Holt aktuelle Daten für das gerade ausgewählte Symbol.""" selected_symbol = self.symbol_combo.currentText() if selected_symbol: # Holen der neuesten Daten (yfinance holt automatisch nur die fehlenden) if self.db_manager.fetch_and_store_data(selected_symbol, period="max"): QMessageBox.information(self, "Aktualisiert", f"Daten für {selected_symbol} wurden aktualisiert.") self._on_symbol_selected(self.symbol_combo.currentIndex()) # Ansichten neu laden else: QMessageBox.warning(self, "Fehler", f"Konnte Daten für {selected_symbol} nicht aktualisieren.") else: QMessageBox.information(self, "Info", "Kein Symbol ausgewählt.") def _get_current_stock_data(self): """Hilfsfunktion zum Abrufen der aktuellen Aktiendaten basierend auf Auswahl.""" selected_symbol = self.symbol_combo.currentText() if not selected_symbol: return pd.DataFrame() start_date_q = self.start_date_edit.date() end_date_q = self.end_date_edit.date() start_date_str = start_date_q.toString("yyyy-MM-dd") end_date_str = end_date_q.toString("yyyy-MM-dd") data = self.db_manager.get_stock_data(selected_symbol, start_date_str, end_date_str) return data def _on_symbol_selected(self, index): """Wird aufgerufen, wenn ein neues Symbol in der ComboBox ausgewählt wird.""" self.plot_all_tabs() def plot_all_tabs(self): """Aktualisiert alle Diagramme und Texte in den Tabs.""" data = self._get_current_stock_data() if data.empty: self._clear_all_plots() self.overview_text.setText("Keine Daten verfügbar für das ausgewählte Symbol oder den Zeitraum.") self.returns_text.setText("Keine Daten verfügbar.") self.ma_text.setText("Keine Daten verfügbar.") self.vol_text.setText("Keine Daten verfügbar.") self.beta_text.setText("Keine Daten verfügbar.") return self._plot_overview(data) self._plot_returns(data) self._plot_ma(data) # Kann überschrieben werden, wenn MA-Button geklickt wird self._plot_volatility(data) # Kann überschrieben werden, wenn Vol-Button geklickt wird self._plot_beta(data) # Kann überschrieben werden, wenn Beta-Button geklickt wird def _clear_plot(self, canvas): """Löscht einen Plot.""" canvas.axes.clear() canvas.draw() def _clear_all_plots(self): self._clear_plot(self.overview_canvas) self._clear_plot(self.returns_canvas) self._clear_plot(self.ma_canvas) self._clear_plot(self.vol_canvas) self._clear_plot(self.beta_canvas) def _plot_overview(self, data): self._clear_plot(self.overview_canvas) ax = self.overview_canvas.axes ax.plot(data.index, data['adj_close'], label='Schlusskurs', color='blue') ax.set_title(f"{self.symbol_combo.currentText()} - Schlusskurs") ax.set_xlabel("Datum") ax.set_ylabel("Kurs") ax.legend() ax.grid(True) self.overview_canvas.draw() self.overview_text.setText(f"Aktueller Kurs: {data['adj_close'].iloc[-1]:.2f}\n" f"Datum: {data.index[-1].strftime('%Y-%m-%d')}\n" f"Anzahl Datenpunkte: {len(data)}") def _plot_returns(self, data): self._clear_plot(self.returns_canvas) ax = self.returns_canvas.axes # Sicherstellen, dass calculate_returns und calculate_cumulative_returns die Series erhalten # und die Ausgabe der calculate_returns Funktion in Dezimalwerten ist, nicht in Prozent. # Ich habe das in FinancialTools vorgeschlagen, falls du es dort nicht direkt *100 machst. # Hier ist es wichtig, dass 'data['adj_close']' übergeben wird. daily_returns = self.financial_tools.calculate_returns(data['adj_close']) cumulative_returns = self.financial_tools.calculate_cumulative_returns(data['adj_close']) if not daily_returns.empty: ax.plot(daily_returns.index, daily_returns, label='Tägliche Rendite', color='green', alpha=0.7) ax.set_title(f"{self.symbol_combo.currentText()} - Tägliche Renditen") ax.set_xlabel("Datum") ax.set_ylabel("Rendite (%)") # Hier ist "%" wichtig ax.grid(True) self.returns_canvas.draw() # Jetzt die Fehlerbehebung für die Textausgabe avg_daily_return = daily_returns.mean() # Überprüfe, ob es ein gültiger Zahlenwert ist, bevor formatiert wird avg_daily_return_str = f"{avg_daily_return:.4f}" if pd.notna(avg_daily_return) else "N/A" last_cumulative_return = cumulative_returns.iloc[-1] last_cumulative_return_str = f"{last_cumulative_return:.4f}" if pd.notna(last_cumulative_return) else "N/A" self.returns_text.setText(f"Durchschn. tägl. Rendite: {avg_daily_return_str}\n" f"Kumulierte Rendite (Gesamt): {last_cumulative_return_str}") else: self._clear_plot(self.returns_canvas) self.returns_text.setText("Nicht genügend Daten für Renditeberechnung.") def _plot_ma(self, data): self._clear_plot(self.ma_canvas) ax = self.ma_canvas.axes try: window = int(self.ma_window_input.text()) except ValueError: QMessageBox.warning(self, "Eingabefehler", "Gleitender Durchschnitt: Fenster muss eine Zahl sein.") return ma = self.financial_tools.calculate_moving_average(data, window=window) if not ma.empty: ax.plot(data.index, data['adj_close'], label='Schlusskurs', color='blue', alpha=0.7) ax.plot(ma.index, ma, label=f'MA {window} Tage', color='red') ax.set_title(f"{self.symbol_combo.currentText()} - Gleitender Durchschnitt ({window} Tage)") ax.set_xlabel("Datum") ax.set_ylabel("Kurs") ax.legend() ax.grid(True) self.ma_canvas.draw() self.ma_text.setText(f"Gleitender Durchschnitt ({window} Tage) berechnet.") else: self._clear_plot(self.ma_canvas) self.ma_text.setText("Nicht genügend Daten für gleitenden Durchschnitt.") def _plot_volatility(self, data): self._clear_plot(self.vol_canvas) ax = self.vol_canvas.axes try: window = int(self.vol_window_input.text()) except ValueError: QMessageBox.warning(self.tab_volatility, "Eingabefehler", "Volatilität: Fenster muss eine Zahl sein.") return volatility = self.financial_tools.calculate_volatility(data, window=window) if not volatility.empty: ax.plot(volatility.index, volatility, label=f'Volatilität {window} Tage (annualisiert)', color='purple') ax.set_title(f"{self.symbol_combo.currentText()} - Rollierende Volatilität ({window} Tage)") ax.set_xlabel("Datum") ax.set_ylabel("Volatilität (annualisiert)") ax.grid(True) self.vol_canvas.draw() self.vol_text.setText(f"Rollierende Volatilität ({window} Tage) berechnet.") else: self._clear_plot(self.vol_canvas) self.vol_text.setText("Nicht genügend Daten für Volatilitätsberechnung.") def _plot_beta(self, data): self._clear_plot(self.beta_canvas) ax = self.beta_canvas.axes market_symbol = self.market_symbol_input.text().upper() try: window = int(self.beta_window_input.text()) except ValueError: QMessageBox.warning(self.tab_beta, "Eingabefehler", "Beta: Fenster muss eine Zahl sein.") return if not market_symbol: QMessageBox.warning(self.tab_beta, "Eingabefehler", "Bitte geben Sie ein Markt-Symbol ein (z.B. SPY).") return # Zuerst das Markt-Symbol zur DB hinzufügen und Daten holen, falls nicht vorhanden if not market_symbol in self.db_manager.get_all_symbols(): if not self.db_manager.add_stock(market_symbol): QMessageBox.warning(self.tab_beta, "Datenfehler", f"Ungültiges Markt-Symbol: {market_symbol}") self._clear_plot(self.beta_canvas) self.beta_text.setText("Ungültiges Markt-Symbol.") return if not self.db_manager.fetch_and_store_data(market_symbol, period="max"): QMessageBox.warning(self.tab_beta, "Datenfehler", f"Konnte Daten für Markt-Symbol {market_symbol} nicht holen.") self._clear_plot(self.beta_canvas) self.beta_text.setText("Keine Marktdaten verfügbar.") return start_date_q = self.start_date_edit.date() end_date_q = self.end_date_edit.date() start_date_str = start_date_q.toString("yyyy-MM-dd") end_date_str = end_date_q.toString("yyyy-MM-dd") market_data = self.db_manager.get_stock_data(market_symbol, start_date_str, end_date_str) if market_data.empty: QMessageBox.warning(self.tab_beta, "Datenfehler", f"Keine Daten für Markt-Symbol {market_symbol} im angegebenen Zeitraum.") self._clear_plot(self.beta_canvas) self.beta_text.setText("Keine Marktdaten für Beta-Berechnung.") return beta = self.financial_tools.calculate_beta(data, market_data, window=window) if not beta.empty: ax.plot(beta.index, beta, label=f'Beta vs {market_symbol} ({window} Tage)', color='orange') ax.set_title(f"{self.symbol_combo.currentText()} - Rollierendes Beta vs {market_symbol} ({window} Tage)") ax.set_xlabel("Datum") ax.set_ylabel("Beta") ax.axhline(1, color='gray', linestyle='--', linewidth=0.8, label='Beta = 1') ax.legend() ax.grid(True) self.beta_canvas.draw() self.beta_text.setText(f"Rollierendes Beta ({window} Tage) vs {market_symbol} berechnet. Aktuelles Beta: {beta.iloc[-1]:.2f}") else: self._clear_plot(self.beta_canvas) self.beta_text.setText("Nicht genügend Daten für Beta-Berechnung. Stellen Sie sicher, dass sowohl Aktie als auch Markt ausreichend historische Daten haben.") def closeEvent(self, event): """Wird aufgerufen, wenn das Fenster geschlossen wird.""" self.db_manager.close() event.accept() if __name__ == "__main__": app = QApplication(sys.argv) window = StockAnalyzer() window.show() sys.exit(app.exec())