commit 262ba13e7a83eaedd8ced98e8ff863f17ccd7f60 Author: faraway Date: Sun Aug 10 15:18:12 2025 +0200 initial commit diff --git a/environment.yaml b/environment.yaml new file mode 100644 index 0000000..9dca666 --- /dev/null +++ b/environment.yaml @@ -0,0 +1,19 @@ +name: aktien_analyse +channels: + - conda-forge + - defaults +dependencies: + - python=3.11 + - pandas + - numpy + + - yfinance + + - sqlalchemy # Für eine etwas höhere Abstraktion bei der DB-Interaktion, optional aber nützlich + - openpyxl # Nützlich, falls CSVs auch mal in Excel geöffnet/gespeichert werden + - pip + - pip: + - matplotlib + - pyqt6 + # Hier könnten spezifische Pakete, die nicht direkt auf conda-forge sind, per pip installiert werden. + # Für dieses Projekt sollte alles über conda-forge verfügbar sein. \ No newline at end of file diff --git a/financial_tools.py b/financial_tools.py new file mode 100644 index 0000000..f9be473 --- /dev/null +++ b/financial_tools.py @@ -0,0 +1,131 @@ +import pandas as pd +import numpy as np + +class FinancialTools: + @staticmethod + def calculate_returns(prices_series, in_percent=True): # <-- NEUER PARAMETER HIER! + """Berechnet die täglichen Renditen.""" + if prices_series.empty: + return pd.Series(dtype='float64') + if isinstance(prices_series, pd.DataFrame): + if 'adj_close' in prices_series.columns: + prices_series = prices_series['adj_close'] + elif 'close' in prices_series.columns: + prices_series = prices_series['close'] + else: + return pd.Series(dtype='float64') + + returns = prices_series.pct_change().dropna() + if in_percent: + return returns * 100 # Für die Anzeige in Prozent + return returns # Für interne Berechnungen (Dezimalwerte) + + @staticmethod + def calculate_cumulative_returns(prices_series): + """Berechnet die kumulativen Renditen.""" + if prices_series.empty: + return pd.Series(dtype='float64') + # HIER: Rufe calculate_returns mit in_percent=False auf, um Dezimalwerte zu bekommen + daily_returns_raw = FinancialTools.calculate_returns(prices_series, in_percent=False) + if daily_returns_raw.empty: + return pd.Series(dtype='float64') + return (1 + daily_returns_raw).cumprod() - 1 + + @staticmethod + def calculate_moving_average(prices_series, window=20): + """Berechnet den gleitenden Durchschnitt.""" + if prices_series.empty: + return pd.Series(dtype='float64') + if isinstance(prices_series, pd.DataFrame): + if 'adj_close' in prices_series.columns: + prices_series = prices_series['adj_close'] + elif 'close' in prices_series.columns: + prices_series = prices_series['close'] + else: + return pd.Series(dtype='float64') + + return prices_series.rolling(window=window).mean() + + + @staticmethod + def calculate_volatility(prices_series, window=20): + """Berechnet die rollierende Volatilität (Standardabweichung der Renditen).""" + if prices_series.empty: + return pd.Series(dtype='float64') + + # HIER: Rufe calculate_returns mit in_percent=False auf, um Dezimalwerte zu bekommen + daily_returns_for_vol = FinancialTools.calculate_returns(prices_series, in_percent=False) + + if daily_returns_for_vol.empty: + return pd.Series(dtype='float64') + + # Annualisiere die Volatilität + return daily_returns_for_vol.rolling(window=window).std() * np.sqrt(252) + + + @staticmethod + def calculate_beta(stock_prices, market_prices, window=60): + """ + Berechnet das rollierende Beta eines Wertpapiers relativ zu einem Marktindex. + stock_prices: Pandas Series der Aktie + market_prices: Pandas Series des Marktindex + """ + if stock_prices.empty or market_prices.empty: + return pd.Series(dtype='float64') + + # Sicherstellen, dass es Series sind, falls doch DataFrames übergeben werden + if isinstance(stock_prices, pd.DataFrame): + if 'adj_close' in stock_prices.columns: + stock_prices = stock_prices['adj_close'] + elif 'close' in stock_prices.columns: + stock_prices = stock_prices['close'] + else: + return pd.Series(dtype='float64') + + if isinstance(market_prices, pd.DataFrame): + if 'adj_close' in market_prices.columns: + market_prices = market_prices['adj_close'] + elif 'close' in market_prices.columns: + market_prices = market_prices['close'] + else: + return pd.Series(dtype='float64') + + # HIER: Rufe calculate_returns mit in_percent=False auf, um Dezimalwerte zu bekommen + returns_stock = FinancialTools.calculate_returns(stock_prices, in_percent=False) + returns_market = FinancialTools.calculate_returns(market_prices, in_percent=False) + + # Kombiniere die Renditen und richte sie an den Daten aus + combined_returns = pd.concat([returns_stock.rename('stock'), returns_market.rename('market')], axis=1).dropna() + + if combined_returns.empty: + return pd.Series(dtype='float64') + + # Berechne die rollierende Kovarianz und Varianz + rolling_covariance = combined_returns['stock'].rolling(window=window).cov(combined_returns['market']) + rolling_variance = combined_returns['market'].rolling(window=window).var() + + # Berechne Beta + beta = rolling_covariance / rolling_variance + return beta.dropna() + +# Beispielnutzung (für Tests) - Muss an die neuen Funktionssignaturen angepasst werden +if __name__ == "__main__": + # Erzeuge fiktive Daten für Tests + dates = pd.to_datetime(pd.date_range(start='2023-01-01', periods=100)) + stock_prices_series = pd.Series(np.random.rand(100) * 100 + 50, index=dates) # direkt als Series + market_prices_series = pd.Series(np.random.rand(100) * 10 + 200, index=dates) # direkt als Series + + print("Tägliche Renditen (in Prozent):") + print(FinancialTools.calculate_returns(stock_prices_series, in_percent=True).head()) # Für Anzeige + + print("\nKumulative Renditen (Dezimal):") + print(FinancialTools.calculate_cumulative_returns(stock_prices_series).head()) + + print("\nGleitender 20-Tage-Durchschnitt:") + print(FinancialTools.calculate_moving_average(stock_prices_series, window=20).head()) + + print("\nRollierende 20-Tage-Volatilität (annualisiert, Dezimal):") + print(FinancialTools.calculate_volatility(stock_prices_series, window=20).head()) + + print("\nRollierendes 60-Tage-Beta (Aktie vs. Markt, Dezimal):") + print(FinancialTools.calculate_beta(stock_prices_series, market_prices_series, window=60).head()) \ No newline at end of file diff --git a/main_gui.py b/main_gui.py new file mode 100644 index 0000000..a2cfa3b --- /dev/null +++ b/main_gui.py @@ -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()) \ No newline at end of file diff --git a/stock_analysis.db b/stock_analysis.db new file mode 100644 index 0000000..647be5a Binary files /dev/null and b/stock_analysis.db differ diff --git a/stock_data_manager.py b/stock_data_manager.py new file mode 100644 index 0000000..6eb8331 --- /dev/null +++ b/stock_data_manager.py @@ -0,0 +1,201 @@ +import yfinance as yf +import pandas as pd +import sqlite3 +from datetime import datetime + +class StockDataManager: + def __init__(self, db_name="stock_data.db"): + self.db_name = db_name + self.conn = None + self.cursor = None + self._connect_db() + self._create_tables() + + def _connect_db(self): + """Stellt eine Verbindung zur SQLite-Datenbank her.""" + try: + self.conn = sqlite3.connect(self.db_name) + self.cursor = self.conn.cursor() + print(f"Erfolgreich mit Datenbank '{self.db_name}' verbunden.") + except sqlite3.Error as e: + print(f"Fehler beim Verbinden mit der Datenbank: {e}") + + def _create_tables(self): + """Erstellt die Tabellen für Aktiendaten, falls sie noch nicht existieren.""" + if not self.conn: + print("Keine Datenbankverbindung vorhanden.") + return + + try: + self.cursor.execute(''' + CREATE TABLE IF NOT EXISTS stocks ( + symbol TEXT PRIMARY KEY, + company_name TEXT + ) + ''') + self.cursor.execute(''' + CREATE TABLE IF NOT EXISTS daily_prices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT NOT NULL, + date TEXT NOT NULL, + open REAL, + high REAL, + low REAL, + close REAL, + adj_close REAL, + volume INTEGER, + FOREIGN KEY (symbol) REFERENCES stocks (symbol) ON DELETE CASCADE, + UNIQUE (symbol, date) + ) + ''') + self.conn.commit() + print("Datenbanktabellen überprüft/erstellt.") + except sqlite3.Error as e: + print(f"Fehler beim Erstellen der Tabellen: {e}") + + def add_stock(self, symbol, company_name=""): + """Fügt ein Aktiensymbol zur 'stocks'-Tabelle hinzu.""" + if not self.conn: return + try: + self.cursor.execute("INSERT OR IGNORE INTO stocks (symbol, company_name) VALUES (?, ?)", (symbol.upper(), company_name)) + self.conn.commit() + print(f"Aktie '{symbol.upper()}' zur Datenbank hinzugefügt (falls neu).") + return True + except sqlite3.Error as e: + print(f"Fehler beim Hinzufügen der Aktie {symbol}: {e}") + return False + + def get_all_symbols(self): + """Gibt eine Liste aller in der Datenbank gespeicherten Symbole zurück.""" + if not self.conn: return [] + self.cursor.execute("SELECT symbol FROM stocks") + return [row[0] for row in self.cursor.fetchall()] + + def fetch_and_store_data(self, symbol, period="1y"): + """ + Holt historische Kursdaten für ein Symbol und speichert sie in der Datenbank. + period: '1d', '5d', '1mo', '3mo', '6mo', '1y', '2y', '5y', '10y', 'ytd', 'max' + """ + symbol = symbol.upper() + print(f"Hole Daten für {symbol}...") + try: + ticker = yf.Ticker(symbol) + hist = ticker.history(period="1y", auto_adjust=False) + if hist.empty: + print(f"Keine Daten für {symbol} gefunden oder ungültiges Symbol.") + return False + + # Füge das Symbol hinzu, falls es noch nicht existiert (z.B. wenn es direkt per API geholt wird) + self.add_stock(symbol, ticker.info.get('longName', '')) + + data_to_insert = [] + for date, row in hist.iterrows(): + # Formatiere Datum als YYYY-MM-DD + date_str = date.strftime('%Y-%m-%d') + data_to_insert.append(( + symbol, + date_str, + row['Open'], + row['High'], + row['Low'], + row['Close'], + row['Adj Close'], + row['Volume'] + )) + + # Verwende INSERT OR IGNORE, um Duplikate zu vermeiden + self.cursor.executemany(""" + INSERT OR IGNORE INTO daily_prices (symbol, date, open, high, low, close, adj_close, volume) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, data_to_insert) + self.conn.commit() + print(f"Daten für {symbol} erfolgreich gespeichert/aktualisiert.") + return True + except Exception as e: + print(f"Fehler beim Holen/Speichern der Daten für {symbol}: {e}") + return False + + def get_stock_data(self, symbol, start_date=None, end_date=None): + """ + Holt historische Kursdaten für ein Symbol aus der Datenbank als Pandas DataFrame. + Optional: Filter nach Start- und Enddatum. + """ + symbol = symbol.upper() + query = "SELECT date, open, high, low, close, adj_close, volume FROM daily_prices WHERE symbol = ?" + params = [symbol] + + if start_date and end_date: + query += " AND date BETWEEN ? AND ?" + params.append(start_date) + params.append(end_date) + elif start_date: + query += " AND date >= ?" + params.append(start_date) + elif end_date: + query += " AND date <= ?" + params.append(end_date) + + query += " ORDER BY date ASC" + + try: + df = pd.read_sql(query, self.conn, params=params, parse_dates=['date'], index_col='date') + if df.empty: + print(f"Keine Daten für {symbol} im angegebenen Zeitraum in der Datenbank gefunden.") + return df + except sqlite3.Error as e: + print(f"Fehler beim Abrufen der Daten für {symbol} aus der Datenbank: {e}") + return pd.DataFrame() + + def close(self): + """Schließt die Datenbankverbindung.""" + if self.conn: + self.conn.close() + print("Datenbankverbindung geschlossen.") + +# Beispielnutzung (kann später entfernt werden, wenn GUI fertig ist) +if __name__ == "__main__": + manager = StockDataManager("my_stocks.db") + + # Symbole aus CSV hinzufügen (Beispiel) + # Angenommen, du hast eine stocks.csv mit 'Symbol,CompanyName' + # Beispiel-CSV-Inhalt: + # Symbol,CompanyName + # AAPL,Apple Inc. + # MSFT,Microsoft Corp. + # GOOGL,Alphabet Inc. (GOOGL) + # AMZN,Amazon.com Inc. + + csv_file = "stocks.csv" # Erstelle diese Datei manuell für den Test + + try: + df_symbols = pd.read_csv(csv_file) + for index, row in df_symbols.iterrows(): + manager.add_stock(row['Symbol'], row.get('CompanyName', '')) + except FileNotFoundError: + print(f"'{csv_file}' nicht gefunden. Bitte erstellen Sie eine CSV-Datei mit 'Symbol,CompanyName'.") + except KeyError: + print(f"Fehler: '{csv_file}' muss Spalten 'Symbol' und optional 'CompanyName' enthalten.") + + + symbols_to_fetch = manager.get_all_symbols() + if not symbols_to_fetch: + # Fallback: Wenn CSV leer ist oder nicht existiert, einige bekannte Symbole holen + symbols_to_fetch = ["AAPL", "MSFT", "GOOGL"] + for s in symbols_to_fetch: + manager.add_stock(s) + + for symbol in symbols_to_fetch: + manager.fetch_and_store_data(symbol, period="1y") # Holt Daten für 1 Jahr + + # Testen des Datenabrufs + aapl_data = manager.get_stock_data("AAPL") + if not aapl_data.empty: + print("\nAAPL Daten (erste 5 Reihen):") + print(aapl_data.head()) + + msft_data = manager.get_stock_data("MSFT", start_date="2024-01-01") + if not msft_data.empty: + print("\nMSFT Daten seit 2024-01-01 (letzte 5 Reihen):") + print(msft_data.tail()) + + manager.close() \ No newline at end of file diff --git a/stocks.csv b/stocks.csv new file mode 100644 index 0000000..910a3f9 --- /dev/null +++ b/stocks.csv @@ -0,0 +1,4 @@ +Symbol,Name,Branche,Anzahl_Aktien,Kaufpreis_pro_Aktie,Kaufdatum +MSFT,Microsoft Corp,Technologie,10,250.00,2023-01-15 +AAPL,Apple Inc,Technologie,15,165.50,2023-03-10 +SHEL,Shell PLC,Energie,50,25.75,2023-02-20 \ No newline at end of file