From 262ba13e7a83eaedd8ced98e8ff863f17ccd7f60 Mon Sep 17 00:00:00 2001 From: faraway Date: Sun, 10 Aug 2025 15:18:12 +0200 Subject: [PATCH] initial commit --- environment.yaml | 19 ++ financial_tools.py | 131 ++++++++++++ main_gui.py | 459 ++++++++++++++++++++++++++++++++++++++++++ stock_analysis.db | Bin 0 -> 151552 bytes stock_data_manager.py | 201 ++++++++++++++++++ stocks.csv | 4 + 6 files changed, 814 insertions(+) create mode 100644 environment.yaml create mode 100644 financial_tools.py create mode 100644 main_gui.py create mode 100644 stock_analysis.db create mode 100644 stock_data_manager.py create mode 100644 stocks.csv 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 0000000000000000000000000000000000000000..647be5a7fec441c8cc369074a592cdfbb3f66389 GIT binary patch literal 151552 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 literal 0 HcmV?d00001 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