initial commit

This commit is contained in:
2025-08-10 15:18:12 +02:00
commit 262ba13e7a
6 changed files with 814 additions and 0 deletions

459
main_gui.py Normal file
View File

@@ -0,0 +1,459 @@
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())