initial commit
This commit is contained in:
19
environment.yaml
Normal file
19
environment.yaml
Normal file
@@ -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.
|
131
financial_tools.py
Normal file
131
financial_tools.py
Normal file
@@ -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())
|
459
main_gui.py
Normal file
459
main_gui.py
Normal 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())
|
BIN
stock_analysis.db
Normal file
BIN
stock_analysis.db
Normal file
Binary file not shown.
201
stock_data_manager.py
Normal file
201
stock_data_manager.py
Normal file
@@ -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()
|
4
stocks.csv
Normal file
4
stocks.csv
Normal file
@@ -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
|
|
Reference in New Issue
Block a user