Files
Aktienmanager/main_gui.py
2025-08-10 15:18:12 +02:00

459 lines
20 KiB
Python

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