Download app.py from Swobby/cc: direct link, hf CLI and curl.
- Browser
- Download file 44.7 kB
-
https://huggingface.co/spaces/Swobby/cc/resolve/main/app.py
- Command line
-
hf download hf://spaces/Swobby/cc/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Swobby/cc/resolve/main/app.py
44.7 kB
| # -*- coding: utf-8 -*- | |
| import streamlit as st | |
| import yfinance as yf | |
| import pandas as pd | |
| import numpy as np | |
| import plotly.graph_objects as go | |
| from datetime import datetime, timedelta | |
| import time # Für Fehlerbehandlung Ticker | |
| # --- Konfiguration --- | |
| st.set_page_config(layout="wide", page_title="Option Yield Pro (Premium Focus)") | |
| # --- Hilfsfunktionen --- | |
| # -- Caching -- | |
| # Cache das Ticker-OBJEKT für 10 Min. | |
| def get_ticker_object(ticker_symbol): | |
| """Holt und cacht das yfinance Ticker-Objekt.""" | |
| if not ticker_symbol: return None # Frühzeitiger Ausstieg, wenn kein Ticker vorhanden | |
| try: | |
| ticker = yf.Ticker(ticker_symbol) | |
| # Prüfe Gültigkeit (löst einen kleinen Download aus) | |
| # .info kann manchmal leer sein, auch wenn Ticker gültig. Besser: History-Check? | |
| if ticker.history(period="1d").empty: | |
| raise ValueError(f"Keine Daten für {ticker_symbol} abrufbar. Ungültiger Ticker?") | |
| return ticker | |
| except Exception as e: | |
| # Fehler nicht im UI anzeigen, wenn die Funktion im Hintergrund läuft | |
| print(f"Fehler beim Erstellen des Ticker-Objekts für {ticker_symbol}: {e}") | |
| return None # Return None on failure | |
| # Cache die AKTIENDATEN (dict) für 5 Minuten | |
| def get_stock_data(_ticker_obj): # Nimm Objekt statt Symbol | |
| """Holt Aktieninformationen und Preis mithilfe des Ticker-Objekts.""" | |
| if _ticker_obj is None: | |
| return None, "Ungültiges Ticker-Objekt" | |
| try: | |
| info = _ticker_obj.info | |
| # Verschiedene Felder für den aktuellen Preis prüfen | |
| price = info.get('currentPrice') or info.get('regularMarketPrice') or info.get('bid') or info.get('ask') | |
| prev_close = info.get('previousClose') or info.get('regularMarketPreviousClose') | |
| # History-Fallback, wenn Preis/Schlusskurs fehlen | |
| if price is None or prev_close is None: | |
| hist = _ticker_obj.history(period="2d") | |
| if not hist.empty: | |
| price = hist['Close'].iloc[-1] | |
| prev_close = hist['Close'].iloc[-2] if len(hist) > 1 else price # Fallback für nur einen Tag History | |
| else: | |
| # Fallback: Versuche 'financialData' | |
| fdata = info.get('financialData', {}) | |
| price = fdata.get('currentPrice') | |
| if price is None: | |
| return None, "Aktueller Preis konnte nicht ermittelt werden." | |
| # Wenn prev_close immer noch fehlt, nehmen wir einfach den aktuellen Preis (suboptimal) | |
| if prev_close is None: | |
| prev_close = price | |
| change = price - prev_close | |
| change_percent = (change / prev_close) * 100 if prev_close != 0 else 0 # Vermeide Division durch Null | |
| name = info.get('shortName', _ticker_obj.ticker) | |
| currency = info.get('currency', '$') # Währung holen | |
| fifty_two_week_low = info.get('fiftyTwoWeekLow') | |
| fifty_two_week_high = info.get('fiftyTwoWeekHigh') | |
| stock_data = { | |
| "name": name, | |
| "price": price, | |
| "change": change, | |
| "change_percent": change_percent, | |
| "currency": currency, | |
| "fifty_two_week_low": fifty_two_week_low, | |
| "fifty_two_week_high": fifty_two_week_high, | |
| } | |
| return stock_data, None # Return data dict and None for error | |
| except Exception as e: | |
| return None, f"Fehler beim Abrufen der Aktiendaten für {_ticker_obj.ticker}: {e}" | |
| # Cache die Liste der ABLAUFDATEN | |
| def get_option_dates(_ticker_obj): | |
| """Holt verfügbare Optionsablaufdaten.""" | |
| if _ticker_obj is None: return [] | |
| try: | |
| # Manchmal gibt .options einen Fehler, wenn keine Optionen existieren | |
| opts = _ticker_obj.options | |
| return opts if opts else [] | |
| except Exception as e: | |
| print(f"Fehler beim Abrufen der Optionsdaten ({_ticker_obj.ticker}): {e}") | |
| return [] | |
| # Cache die OPTIONSKETTE DataFrame für 5 Min. | |
| def get_option_chain(_ticker_obj, expiration_date): | |
| """Holt die Optionskette für ein spezifisches Datum.""" | |
| if _ticker_obj is None or not expiration_date: return pd.DataFrame() | |
| try: | |
| chain = _ticker_obj.option_chain(expiration_date) | |
| # Nur Calls, Index zurücksetzen und sicherstellen, dass Spalten existieren | |
| # Check if 'calls' key exists before accessing | |
| if 'calls' not in chain or chain.calls is None: | |
| return pd.DataFrame() # Return empty if no calls data | |
| calls = chain.calls.reset_index(drop=True).copy() | |
| # Standardwerte für fehlende Griechen setzen (falls yfinance sie nicht liefert) | |
| for greek in ['delta', 'gamma', 'theta', 'vega']: | |
| if greek not in calls.columns: | |
| calls[greek] = np.nan | |
| return calls | |
| except Exception as e: | |
| # Oft passiert "KeyError: 'calls'" wenn keine Kette für das Datum existiert | |
| print(f"Fehler/Warnung beim Abrufen der Optionskette für {_ticker_obj.ticker} am {expiration_date}: {e}") | |
| return pd.DataFrame() # Return empty dataframe on error | |
| # Cache History länger (30 Min) | |
| def get_stock_history(_ticker_obj, period='1y', interval='1d'): | |
| """Holt historische Kurse für den Chart.""" | |
| if _ticker_obj is None: return pd.DataFrame() | |
| try: | |
| # yfinance erwartet manchmal Großbuchstaben für Intervalle wie '1WK' | |
| interval_map = {'1d': '1d', '1wk': '1wk', '1mo': '1mo'} # Mapping zur Sicherheit | |
| hist = _ticker_obj.history(period=period, interval=interval_map.get(interval.lower(), '1d')) | |
| return hist | |
| except Exception as e: | |
| st.error(f"Fehler beim Laden der Historie für {_ticker_obj.ticker} (Periode: {period}, Intervall: {interval}): {e}") | |
| return pd.DataFrame() | |
| # -- Berechnungen & Logik -- | |
| def calculate_covered_calls(calls_df, current_price, shares_owned, cost_basis, expiration_date, assumed_premium_price_type='bid', custom_premium=None, commission_per_contract=0.0): | |
| """Berechnet Covered Call Metriken und fügt sie dem DataFrame hinzu.""" | |
| if calls_df.empty: return calls_df | |
| df = calls_df.copy() | |
| # --- Prämienberechnung --- | |
| if custom_premium is not None and custom_premium > 0: | |
| df['assumedPremiumPerShare'] = float(custom_premium) # Stelle sicher, dass es float ist | |
| elif assumed_premium_price_type == 'mid': | |
| bid_filled = df['bid'].fillna(0) | |
| ask_filled = df['ask'].fillna(df['lastPrice']).fillna(bid_filled) # Fallback: Last -> Bid -> 0 | |
| df['assumedPremiumPerShare'] = (bid_filled + ask_filled) / 2 | |
| elif assumed_premium_price_type == 'last': | |
| df['assumedPremiumPerShare'] = df['lastPrice'].fillna(0) | |
| else: # Default to Bid | |
| df['assumedPremiumPerShare'] = df['bid'].fillna(0) | |
| # Stelle sicher, dass Prämie nicht negativ ist und float ist | |
| df['assumedPremiumPerShare'] = df['assumedPremiumPerShare'].apply(lambda x: max(0, float(x))) | |
| num_contracts = float(shares_owned) / 100.0 | |
| df['premiumGrossTotal'] = df['assumedPremiumPerShare'] * float(shares_owned) | |
| total_commission = num_contracts * float(commission_per_contract) | |
| df['premiumNetTotal'] = df['premiumGrossTotal'] - total_commission # Nettoprämie nach Provision | |
| # --- Kostenbasis & Kernberechnungen --- | |
| cost_basis = float(cost_basis) | |
| shares_owned = float(shares_owned) | |
| current_price = float(current_price) | |
| df['costBasisTotal'] = cost_basis * shares_owned | |
| df['strike'] = df['strike'].astype(float) | |
| # Max Profit if Assigned (Nettoprämie verwenden) | |
| df['maxProfitAssignedNet'] = ((df['strike'] - cost_basis) * shares_owned) + df['premiumNetTotal'] # Behalten für optionale Anzeige | |
| # --- Prozentuale Metriken (auf Basis der Nettoprämie) --- | |
| cost_basis_total_val = df['costBasisTotal'].iloc[0] if not df.empty else 0 | |
| # Berechne premium_net_per_share als Series | |
| premium_net_per_share = df['premiumNetTotal'] / shares_owned if shares_owned > 0 else pd.Series([0.0] * len(df), index=df.index) | |
| # Sicherstellen, dass es eine Series ist, falls shares_owned=0 galt | |
| if not isinstance(premium_net_per_share, pd.Series): | |
| premium_net_per_share = pd.Series([premium_net_per_share] * len(df), index=df.index) | |
| # Fall: Kostenbasis > 0 | |
| if cost_basis_total_val > 0: | |
| df['maxProfitPercentNet'] = (df['maxProfitAssignedNet'] / cost_basis_total_val) * 100 # Behalten für optionale Anzeige | |
| df['returnIfFlatPercentNet'] = (df['premiumNetTotal'] / cost_basis_total_val) * 100 | |
| # Downside Protection braucht premium_net_per_share (Series) und cost_basis (Skalar) | |
| df['downsideProtectionPercentNet'] = (premium_net_per_share / cost_basis) * 100 if cost_basis > 0 else np.inf | |
| # Return on Risk braucht premium_net_per_share (Series) und capital_at_risk (Series) | |
| capital_at_risk = cost_basis - premium_net_per_share | |
| df['returnOnRiskPercentNet'] = np.where( | |
| capital_at_risk > 0, (premium_net_per_share / capital_at_risk) * 100, np.inf | |
| ) | |
| # Fall: Kostenbasis <= 0 (oder nicht berechenbar) | |
| else: | |
| for col in ['maxProfitPercentNet', 'returnIfFlatPercentNet', 'downsideProtectionPercentNet', 'returnOnRiskPercentNet']: | |
| df[col] = np.inf | |
| # --- DTE (Days To Expiration) --- | |
| try: | |
| # Verwende datetime.date für reinen Datumsvergleich | |
| exp_date_obj = datetime.strptime(expiration_date, '%Y-%m-%d').date() | |
| today = datetime.now().date() | |
| dte = (exp_date_obj - today).days | |
| dte = max(0, dte) # Sicherstellen, dass DTE nicht negativ ist | |
| except ValueError: | |
| dte = 0 | |
| # st.warning nicht in cached function, print ist besser für Debugging | |
| print(f"Konnte Datum {expiration_date} nicht für DTE-Berechnung parsen.") | |
| df['DTE'] = dte | |
| # --- Annualisierte Rendite (Basis: Nettoprämie, wenn Flat) --- | |
| df['annualizedReturnPercentNet'] = 0.0 # Initialisiere Spalte | |
| # Stelle sicher, dass returnIfFlatPercentNet existiert und numerisch ist | |
| if 'returnIfFlatPercentNet' in df.columns: | |
| df['returnIfFlatPercentNet'] = pd.to_numeric(df['returnIfFlatPercentNet'], errors='coerce') # Umwandeln, Fehler -> NaN | |
| valid_annual_mask = (df['DTE'] > 0) & df['returnIfFlatPercentNet'].notna() & (df['returnIfFlatPercentNet'] > 0) | |
| safe_dte = df.loc[valid_annual_mask, 'DTE'].replace(0, 1) # Ersetze 0 durch 1 für Berechnung | |
| # Überprüfe ob safe_dte leer ist, bevor darauf zugegriffen wird | |
| if not safe_dte.empty: | |
| df.loc[valid_annual_mask, 'annualizedReturnPercentNet'] = \ | |
| df.loc[valid_annual_mask, 'returnIfFlatPercentNet'] * (365.0 / safe_dte) | |
| # Setze NaN für ungültige Fälle und fülle dann mit 0.0 | |
| df['annualizedReturnPercentNet'] = df['annualizedReturnPercentNet'].fillna(0.0) | |
| # --- Breakeven (Basis: Nettoprämie pro Aktie) --- | |
| df['breakeven'] = cost_basis - premium_net_per_share | |
| # --- % OTM (Out of the Money) --- | |
| df['percentOTM'] = 0.0 | |
| otm_mask = df['strike'] > current_price | |
| # Wende Berechnung nur auf OTM-Strikes an | |
| df.loc[otm_mask, 'percentOTM'] = ((df.loc[otm_mask, 'strike'] - current_price) / current_price) * 100 | |
| # --- Griechen & P.OTM --- | |
| for greek in ['delta', 'gamma', 'theta', 'vega']: | |
| if greek in df.columns: | |
| df[greek] = pd.to_numeric(df[greek], errors='coerce').fillna(np.nan) # Sicherstellen, dass numerisch oder NaN | |
| else: | |
| df[greek] = np.nan # Sicherstellen, dass Spalte existiert | |
| # Wahrscheinlichkeit OTM (vereinfacht: 1 - Delta) | |
| # Fülle fehlende Deltas mit 0.5 (50/50 Chance) oder NaN? NaN ist neutraler. | |
| df['prob_OTM'] = (1.0 - df['delta']).clip(0, 1) # Auf 0-1 begrenzen, NaN bleibt NaN | |
| return df | |
| # -- Datums-Prüfungen -- | |
| def is_monthly_expiration(date_str): | |
| """Prüft, ob ein Datum wahrscheinlich ein Monatsverfall ist (3. Freitag).""" | |
| try: | |
| date_obj = datetime.strptime(date_str, '%Y-%m-%d') | |
| # Standard US equity monthly expiration | |
| return 15 <= date_obj.day <= 21 and date_obj.weekday() == 4 # 4 = Freitag | |
| except ValueError: | |
| return False | |
| def is_weekly_expiration(date_str): | |
| """Prüft, ob ein Datum wahrscheinlich ein Wochenverfall ist (Freitag, aber nicht der 3.).""" | |
| try: | |
| date_obj = datetime.strptime(date_str, '%Y-%m-%d') | |
| # Freitag, aber nicht der 3. Freitag des Monats | |
| is_friday = date_obj.weekday() == 4 | |
| is_third_friday = 15 <= date_obj.day <= 21 | |
| return is_friday and not is_third_friday | |
| except ValueError: | |
| return False | |
| # --- Plotting Funktion --- | |
| def plot_stock_chart(hist_df, ticker_symbol, indicators, sma_periods): | |
| """Erstellt einen interaktiven Plotly Chart.""" | |
| if hist_df.empty: return go.Figure() # Leere Figur zurückgeben, wenn keine Daten | |
| fig = go.Figure() | |
| # Candlestick Chart hinzufügen (nur wenn notwendige Spalten vorhanden sind) | |
| required_cols = ['Open', 'High', 'Low', 'Close'] | |
| if all(col in hist_df.columns for col in required_cols): | |
| fig.add_trace(go.Candlestick(x=hist_df.index, | |
| open=hist_df['Open'], | |
| high=hist_df['High'], | |
| low=hist_df['Low'], | |
| close=hist_df['Close'], | |
| name='Kurs')) | |
| else: | |
| # Fallback: Linienchart für Schlusskurs, wenn verfügbar | |
| if 'Close' in hist_df.columns: | |
| st.warning("Candlestick Daten unvollständig, zeige Linienchart (Close).") | |
| fig.add_trace(go.Scatter(x=hist_df.index, y=hist_df['Close'], mode='lines', name='Schlusskurs')) | |
| else: | |
| st.warning("Keine ausreichenden Daten für Chart vorhanden.") | |
| return fig # Leere Figur zurückgeben | |
| # Indikatoren hinzufügen | |
| if 'SMA' in indicators and 'Close' in hist_df.columns: | |
| for period in sma_periods: | |
| if not isinstance(period, int) or period <= 0: continue # Ungültige Periode überspringen | |
| sma_col = f'SMA_{period}' | |
| hist_df[sma_col] = hist_df['Close'].rolling(window=period, min_periods=1).mean() # min_periods=1 für Anfangswerte | |
| fig.add_trace(go.Scatter(x=hist_df.index, y=hist_df[sma_col], mode='lines', name=sma_col)) | |
| if 'Bollinger Bands' in indicators and 'Close' in hist_df.columns: | |
| # Nutze erste gültige SMA-Periode oder Standard 20 | |
| bb_period = next((p for p in sma_periods if isinstance(p, int) and p > 0), 20) | |
| sma_col = f'SMA_{bb_period}' | |
| # Berechne SMA falls noch nicht vorhanden oder Periode ungültig war | |
| if sma_col not in hist_df.columns: | |
| hist_df[sma_col] = hist_df['Close'].rolling(window=bb_period, min_periods=1).mean() | |
| std_dev = hist_df['Close'].rolling(window=bb_period, min_periods=1).std() | |
| upper_band = hist_df[sma_col] + (std_dev * 2) | |
| lower_band = hist_df[sma_col] - (std_dev * 2) | |
| # Nur hinzufügen, wenn Bänder berechnet werden konnten | |
| if upper_band.notna().any(): # Prüfen ob nicht nur NaNs enthalten sind | |
| fig.add_trace(go.Scatter(x=hist_df.index, y=upper_band, mode='lines', name=f'Upper Band ({bb_period})', line=dict(width=1, dash='dash', color='rgba(150,150,150,0.7)'))) | |
| if lower_band.notna().any(): | |
| fig.add_trace(go.Scatter(x=hist_df.index, y=lower_band, mode='lines', name=f'Lower Band ({bb_period})', line=dict(width=1, dash='dash', color='rgba(150,150,150,0.7)'), fill='tonexty', fillcolor='rgba(173,216,230,0.2)')) | |
| fig.update_layout( | |
| title=f'{ticker_symbol} Kursverlauf & Indikatoren', | |
| xaxis_title='Datum', | |
| yaxis_title='Preis', | |
| xaxis_rangeslider_visible=False, # Range Slider unten ausblenden | |
| legend_title_text='Legende', | |
| height=500, # Höhe des Charts anpassen | |
| margin=dict(l=20, r=20, t=40, b=20) # Ränder reduzieren | |
| ) | |
| return fig | |
| # --- Session State Initialisierung --- | |
| # Neue Standardspalten mit Fokus auf Prämie/Risiko | |
| base_default_columns_premium = [ | |
| 'Strike', 'Bid', 'Ask', '% OTM', 'Delta', 'prob_OTM', 'Impl. Vol (%)', | |
| 'Prämie Netto ({currency})', 'Rendite (Statisch %)', 'Rendite p.a. (%)', | |
| 'Risikopuffer (%)', 'Return on Risk (%)', 'Theta', 'DTE' | |
| ] | |
| # Optionale Spalten (inkl. der alten Max-Profit-Spalten) | |
| other_possible_cols_premium = [ | |
| 'Last', 'Volumen', 'Open Int.', 'Gamma', 'Vega', | |
| 'Max. Gewinn ({currency})', 'Max. Gewinn (%)', 'Breakeven ({currency})' | |
| ] | |
| # Initialisiere State | |
| if 'ticker_symbol' not in st.session_state: st.session_state.ticker_symbol = "TSLA" | |
| if 'shares_owned' not in st.session_state: st.session_state.shares_owned = 100 | |
| if 'cost_basis' not in st.session_state: st.session_state.cost_basis = 100.00 | |
| if 'stock_data' not in st.session_state: st.session_state.stock_data = None | |
| if 'commission' not in st.session_state: st.session_state.commission = 0.65 | |
| if 'premium_price_type' not in st.session_state: st.session_state.premium_price_type = 'bid' | |
| if 'custom_premium' not in st.session_state: st.session_state.custom_premium = None | |
| # Initialisiere mit den *neuen* Premium-fokussierten Defaults | |
| if 'selected_columns' not in st.session_state: | |
| st.session_state.selected_columns = [col.format(currency='$') for col in base_default_columns_premium] | |
| if 'min_annual_return_filter' not in st.session_state: st.session_state.min_annual_return_filter = 5.0 # Höherer Standard-Min.-Filter | |
| if 'max_annual_return_filter' not in st.session_state: st.session_state.max_annual_return_filter = 1000.0 | |
| if 'min_delta_filter' not in st.session_state: st.session_state.min_delta_filter = 0.0 | |
| if 'max_delta_filter' not in st.session_state: st.session_state.max_delta_filter = 0.35 # Standardmäßig OTM fokussieren | |
| if 'highlight_min_return' not in st.session_state: st.session_state.highlight_min_return = 25.0 # Höherer Schwellenwert | |
| if 'highlight_max_delta' not in st.session_state: st.session_state.highlight_max_delta = 0.30 # Delta-Schwelle für Hervorhebung | |
| if 'show_only_otm' not in st.session_state: st.session_state.show_only_otm = True # Standardmäßig nur OTM zeigen | |
| # Chart & Datum Filter State (bleiben gleich) | |
| if 'chart_period' not in st.session_state: st.session_state.chart_period = '1y' | |
| if 'chart_interval' not in st.session_state: st.session_state.chart_interval = '1d' | |
| if 'chart_indicators' not in st.session_state: st.session_state.chart_indicators = ['SMA'] | |
| if 'chart_sma_periods' not in st.session_state: st.session_state.chart_sma_periods = [50, 200] | |
| if 'date_filter_type' not in st.session_state: st.session_state.date_filter_type = 'Alle' | |
| # --- Währung und Spaltennamen VOR der Sidebar bestimmen --- | |
| current_currency_symbol = '$' # Default | |
| if st.session_state.stock_data and isinstance(st.session_state.stock_data, dict): | |
| current_currency_symbol = st.session_state.stock_data.get('currency', '$') | |
| # Verwende die *neuen* Basis-Spaltenlisten | |
| current_default_columns = [col.format(currency=current_currency_symbol) for col in base_default_columns_premium] | |
| current_all_possible_columns = current_default_columns + [col.format(currency=current_currency_symbol) for col in other_possible_cols_premium] | |
| # Stelle sicher, dass die Defaults im State die aktuelle Währung verwenden | |
| temp_selected_columns = [] | |
| # Use a temporary list to avoid modifying while iterating if necessary | |
| current_selection_in_state = list(st.session_state.selected_columns) | |
| for col in current_selection_in_state: | |
| updated_col = col # Start with the column as is | |
| # Try replacing known currency symbols/placeholders | |
| updated_col = updated_col.replace("($)", f"({current_currency_symbol})") | |
| updated_col = updated_col.replace("(USD)", f"({current_currency_symbol})") | |
| updated_col = updated_col.replace("(EUR)", f"({current_currency_symbol})") | |
| # Add if it's now a valid possible column | |
| if updated_col in current_all_possible_columns: | |
| if updated_col not in temp_selected_columns: # Avoid duplicates | |
| temp_selected_columns.append(updated_col) | |
| # Fallback: If original base column name (without currency) exists, format it | |
| else: | |
| for base_col in base_default_columns_premium + other_possible_cols_premium: | |
| # Check if the base name (placeholder removed) is part of the current column name | |
| if base_col.replace("{currency}", "") in col: | |
| formatted_base_col = base_col.format(currency=current_currency_symbol) | |
| if formatted_base_col in current_all_possible_columns and formatted_base_col not in temp_selected_columns: | |
| temp_selected_columns.append(formatted_base_col) | |
| break # Found match for this base column | |
| # Update the state with the cleaned list | |
| st.session_state.selected_columns = temp_selected_columns | |
| # Filter defaults to only those that are valid *now* | |
| valid_defaults_for_multiselect = [ | |
| col for col in st.session_state.selected_columns if col in current_all_possible_columns | |
| ] | |
| # If filtering removed all defaults, reset to the current default list | |
| if not valid_defaults_for_multiselect: | |
| valid_defaults_for_multiselect = current_default_columns | |
| # --- SIDEBAR --- | |
| with st.sidebar: | |
| st.header("⚙️ Eingaben & Einstellungen") | |
| # -- Positionsdetails -- | |
| st.subheader("Positionsdetails") | |
| ticker_input = st.text_input( | |
| "Aktien Ticker", value=st.session_state.ticker_symbol, key="ticker_input_key" | |
| ).upper() | |
| shares_input = st.number_input( | |
| "Anzahl Aktien", min_value=100, step=100, value=st.session_state.shares_owned, key="shares_input_key" | |
| ) | |
| cost_basis_input = st.number_input( | |
| "Ø Einstandskurs / Aktie", min_value=0.01, step=0.01, value=st.session_state.cost_basis, format="%.2f", key="cost_basis_input_key", | |
| help="Wird für Breakeven & optionale Max-Profit-Spalten verwendet." | |
| ) | |
| # -- Annahmen -- | |
| st.subheader("Annahmen") | |
| premium_price_type_input = st.selectbox( | |
| "Preis für Prämie?", options=['bid', 'mid', 'last'], | |
| index=['bid', 'mid', 'last'].index(st.session_state.premium_price_type), # Use state for index | |
| help="Welcher Preis für Prämienberechnung? 'Bid' konservativ.", key="premium_select_key" | |
| ) | |
| custom_premium_input = st.number_input( | |
| "Manuelle Prämie / Aktie", min_value=0.0, step=0.01, | |
| value=st.session_state.custom_premium if st.session_state.custom_premium is not None else 0.0, | |
| format="%.2f", help="> 0 überschreibt Auswahl oben.", key="custom_prem_key" | |
| ) | |
| commission_input = st.number_input( | |
| "Provision / Kontrakt", min_value=0.0, step=0.01, value=st.session_state.commission, format="%.2f", key="commission_key" | |
| ) | |
| # -- Analyse Knopf -- | |
| if st.button("Analyse Starten / Werte Übernehmen", key="analyze_sidebar", use_container_width=True): | |
| ticker_changed = (ticker_input.strip() != st.session_state.ticker_symbol.strip()) # Ignore whitespace | |
| # Update state from inputs | |
| st.session_state.ticker_symbol = ticker_input.strip() # Store cleaned ticker | |
| st.session_state.shares_owned = shares_input | |
| st.session_state.cost_basis = cost_basis_input | |
| st.session_state.premium_price_type = premium_price_type_input | |
| st.session_state.custom_premium = custom_premium_input if custom_premium_input > 0 else None | |
| st.session_state.commission = commission_input | |
| # Reset stock data if ticker changed to force reload | |
| if ticker_changed: | |
| st.session_state.stock_data = None | |
| # Reset selected columns to the new premium defaults with '$' placeholder | |
| st.session_state.selected_columns = [col.format(currency='$') for col in base_default_columns_premium] | |
| st.rerun() # Force rerun to apply changes and reload data if needed | |
| st.divider() # Visuelle Trennung | |
| # -- Tabellen-Anpassung -- | |
| st.subheader("Tabellen-Anzeige") | |
| # Neuer Filter: Nur OTM anzeigen | |
| st.session_state.show_only_otm = st.toggle( | |
| "Nur OTM/ATM anzeigen", value=st.session_state.show_only_otm, key="otm_toggle", | |
| help="Zeigt nur Optionen, deren Strike größer oder gleich dem aktuellen Aktienkurs ist." | |
| ) | |
| selected_cols_input = st.multiselect( | |
| "Angezeigte Spalten:", | |
| options=sorted(list(set(current_all_possible_columns))), # Use current options | |
| default=valid_defaults_for_multiselect, # Use filtered current defaults | |
| key="col_select_key" | |
| ) | |
| # Update state only if selection actually changed to prevent unnecessary reruns | |
| if selected_cols_input != st.session_state.selected_columns: | |
| st.session_state.selected_columns = selected_cols_input | |
| st.rerun() # Rerun to apply column selection | |
| # -- Hervorhebung -- | |
| st.subheader("Hervorhebung (Tabelle)") | |
| hc1, hc2 = st.columns(2) | |
| with hc1: | |
| st.session_state.highlight_min_return = st.number_input( | |
| "Min. Rendite p.a. >", min_value=0.0, step=1.0, | |
| value=st.session_state.highlight_min_return, | |
| format="%.1f", key="highlight_ret_key" | |
| ) | |
| with hc2: | |
| st.session_state.highlight_max_delta = st.number_input( | |
| "Max. Delta <", min_value=0.0, max_value=1.0, step=0.01, | |
| value=st.session_state.highlight_max_delta, | |
| format="%.2f", key="highlight_delta_key" | |
| ) | |
| st.divider() | |
| # -- Chart-Einstellungen -- | |
| st.subheader("Chart-Einstellungen") | |
| st.session_state.chart_period = st.selectbox("Zeitraum:", ['1mo', '3mo', '6mo', 'ytd', '1y', '2y', '5y', 'max'], index=4, key="chart_period_key") | |
| st.session_state.chart_interval = st.selectbox("Intervall:", ['1d', '1wk', '1mo'], index=0, key="chart_interval_key") | |
| st.session_state.chart_indicators = st.multiselect("Indikatoren:", ['SMA', 'Bollinger Bands'], default=st.session_state.chart_indicators, key="chart_ind_key") | |
| if 'SMA' in st.session_state.chart_indicators or 'Bollinger Bands' in st.session_state.chart_indicators: | |
| sma_periods_str = st.text_input( | |
| "SMA Perioden (kommagetrennt):", | |
| value=", ".join(map(str, st.session_state.chart_sma_periods)), | |
| key="chart_sma_key" | |
| ) | |
| try: | |
| parsed_periods = [int(p.strip()) for p in sma_periods_str.split(',') if p.strip().isdigit() and int(p.strip()) > 0] # Nur positive Zahlen | |
| if parsed_periods: # Nur updaten, wenn gültige Perioden gefunden wurden | |
| st.session_state.chart_sma_periods = parsed_periods | |
| else: # Wenn Eingabe leer oder ungültig war | |
| if sma_periods_str.strip(): # Nur warnen wenn etwas eingegeben wurde | |
| st.warning("Ungültige Eingabe für SMA Perioden. Bsp: 50, 200") | |
| st.session_state.chart_sma_periods = [50, 200] # Fallback | |
| except Exception as e: # Allgemeinere Fehlerbehandlung | |
| st.warning(f"Fehler beim Verarbeiten der SMA Perioden: {e}") | |
| st.session_state.chart_sma_periods = [50, 200] # Fallback | |
| # --- HAUPTSEITE --- | |
| st.title("📈 Option Yield Pro (Premium Focus)") | |
| # Nur weitermachen, wenn Ticker im State ist | |
| if not st.session_state.ticker_symbol: | |
| st.info("👈 Bitte geben Sie links einen Aktien-Ticker ein und klicken Sie auf 'Analyse Starten'.") | |
| st.stop() | |
| # --- Aktiendaten Laden & Anzeigen --- | |
| # Versuche Ticker Objekt zu holen (aus Cache oder neu) | |
| ticker_obj = get_ticker_object(st.session_state.ticker_symbol) | |
| if ticker_obj is None: | |
| st.error(f"Konnte Ticker-Objekt für '{st.session_state.ticker_symbol}' nicht laden. Ist der Ticker gültig?") | |
| st.session_state.ticker_symbol = "" # Reset ticker | |
| st.session_state.stock_data = None # Reset data | |
| time.sleep(2) # Allow user to see the message | |
| st.rerun() | |
| # Lade Aktiendaten nur, wenn sie noch nicht im State sind für diesen Ticker | |
| rerun_needed_after_data_load = False | |
| if st.session_state.stock_data is None: | |
| with st.spinner(f"Lade Daten für {st.session_state.ticker_symbol}..."): | |
| stock_data_result, error_msg = get_stock_data(ticker_obj) | |
| if error_msg: | |
| st.error(f"Fehler Aktiendaten: {error_msg}") | |
| # Setze Ticker zurück, wenn Daten nicht geladen werden konnten | |
| st.session_state.ticker_symbol = "" | |
| time.sleep(2) | |
| st.rerun() | |
| st.stop() | |
| st.session_state.stock_data = stock_data_result | |
| rerun_needed_after_data_load = True # Mark that a rerun is needed | |
| # Wichtig: Wenn Daten gerade geladen wurden, neu ausführen, damit die Währung | |
| # korrekt in der Sidebar *beim nächsten Durchlauf* verwendet wird! | |
| if rerun_needed_after_data_load: | |
| st.rerun() | |
| # --- Ab hier gehen wir davon aus, dass stock_data im State ist --- | |
| stock_data = st.session_state.stock_data | |
| currency_symbol = stock_data.get('currency', '$') | |
| # --- Dynamische Spaltennamen für die Anzeige DEFINIEREN --- | |
| display_columns = { | |
| 'strike': 'Strike', 'bid': 'Bid', 'ask': 'Ask', 'lastPrice': 'Last', | |
| 'volume': 'Volumen', 'openInterest': 'Open Int.', 'impliedVolatility': 'Impl. Vol (%)', | |
| 'premiumNetTotal': f'Prämie Netto ({currency_symbol})', | |
| 'returnIfFlatPercentNet': 'Rendite (Statisch %)', # Prämienrendite | |
| 'annualizedReturnPercentNet': 'Rendite p.a. (%)', # Prämie p.a. | |
| 'maxProfitAssignedNet': f'Max. Gewinn ({currency_symbol})', # Optional | |
| 'maxProfitPercentNet': 'Max. Gewinn (%)', # Optional | |
| 'breakeven': f'Breakeven ({currency_symbol})', # Optional | |
| 'downsideProtectionPercentNet': 'Risikopuffer (%)', | |
| 'returnOnRiskPercentNet': 'Return on Risk (%)', | |
| 'percentOTM': '% OTM', 'delta': 'Delta', 'theta': 'Theta', | |
| 'gamma': 'Gamma', 'vega': 'Vega', 'prob_OTM': 'W\'keit OTM (%)', # Neu | |
| 'DTE': 'DTE' | |
| } | |
| # --- Anzeige der Aktiendaten (mit korrigierter 52W-Spanne) --- | |
| st.header(f"📊 {stock_data['name']} ({st.session_state.ticker_symbol})") | |
| price_color = "green" if stock_data.get('change', 0) >= 0 else "red" # Default 0 if change missing | |
| mcol1, mcol2, mcol3 = st.columns(3) # Behalte die Spaltendefinition | |
| with mcol1: | |
| st.metric( | |
| f"Akt. Kurs ({currency_symbol})", | |
| f"{stock_data.get('price', 0):.2f}", # Default 0 | |
| f"{stock_data.get('change', 0):.2f} ({stock_data.get('change_percent', 0):.2f}%)", # Default 0 | |
| delta_color="normal" if price_color == "green" else "inverse" | |
| ) | |
| low_52w = stock_data.get('fifty_two_week_low') | |
| high_52w = stock_data.get('fifty_two_week_high') | |
| with mcol2: # Definiere den Kontext für Spalte 2 | |
| if low_52w is not None and high_52w is not None: # Prüfe *innerhalb* des Kontexts | |
| st.metric("52W Spanne", f"{currency_symbol}{low_52w:.2f} - {high_52w:.2f}") | |
| else: | |
| st.metric("52W Spanne", "-") # Platzhalter | |
| with mcol3: # Spalte 3 leer lassen | |
| pass | |
| # --- Chart Sektion --- | |
| with st.expander("📈 Detailierter Aktienchart Anzeigen/Verbergen"): | |
| hist_df = get_stock_history(ticker_obj, st.session_state.chart_period, st.session_state.chart_interval) | |
| if not hist_df.empty: | |
| fig = plot_stock_chart(hist_df, st.session_state.ticker_symbol, st.session_state.chart_indicators, st.session_state.chart_sma_periods) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.warning("Keine historischen Daten für die gewählten Chart-Parameter verfügbar.") | |
| st.divider(); st.header("🔍 Options-Analyse") | |
| # --- Ablaufdatum Auswahl --- | |
| col_d1, col_d2 = st.columns([1, 3]) | |
| with col_d1: | |
| st.markdown("###### Verfallstyp Filter:") | |
| date_filter_options = ['Alle', 'Monatlich', 'Wöchentlich']; date_filter_idx = 0 | |
| try: date_filter_idx = date_filter_options.index(st.session_state.date_filter_type) | |
| except ValueError: pass | |
| st.session_state.date_filter_type = st.radio("Typ:", date_filter_options, index=date_filter_idx, horizontal=True, label_visibility="collapsed") | |
| with col_d2: | |
| st.markdown("###### Ablaufdatum Wählen:") | |
| option_dates_all = get_option_dates(ticker_obj) | |
| if not option_dates_all: st.warning("Keine Optionsdaten gefunden."); st.stop() | |
| # Filter dates based on selection | |
| if st.session_state.date_filter_type == 'Monatlich': option_dates_filtered = [d for d in option_dates_all if is_monthly_expiration(d)] | |
| elif st.session_state.date_filter_type == 'Wöchentlich': option_dates_filtered = [d for d in option_dates_all if is_weekly_expiration(d)] | |
| else: option_dates_filtered = option_dates_all # Alle | |
| if not option_dates_filtered: st.warning(f"Keine '{st.session_state.date_filter_type}' Ablaufdaten."); selected_exp_date = None | |
| else: selected_exp_date = st.selectbox("Verfügbare Daten:", option_dates_filtered, index=0, label_visibility="collapsed") | |
| # --- Optionskette Laden & Berechnen --- | |
| if selected_exp_date: | |
| # Lade Kette mit Spinner | |
| with st.spinner(f"Lade Optionskette für {selected_exp_date}..."): | |
| calls_chain = get_option_chain(ticker_obj, selected_exp_date) | |
| if not calls_chain.empty: | |
| # Berechne Metriken mit Spinner | |
| with st.spinner("Berechne Metriken..."): | |
| calculated_df_unfiltered = calculate_covered_calls( | |
| calls_chain, stock_data['price'], st.session_state.shares_owned, | |
| st.session_state.cost_basis, selected_exp_date, st.session_state.premium_price_type, | |
| st.session_state.custom_premium, st.session_state.commission | |
| ) | |
| # --- Tabellenfilter Widgets --- | |
| st.markdown("###### Tabellenfilter:") | |
| fcol1, fcol2 = st.columns(2) | |
| with fcol1: | |
| min_r, max_r = st.slider("Filter: Rendite p.a. (%)", 0.0, 500.0, (st.session_state.min_annual_return_filter, st.session_state.max_annual_return_filter), 5.0); | |
| st.session_state.min_annual_return_filter, st.session_state.max_annual_return_filter = min(min_r, max_r), max(min_r, max_r) | |
| with fcol2: | |
| min_d, max_d = st.slider("Filter: Delta", 0.0, 1.0, (st.session_state.min_delta_filter, st.session_state.max_delta_filter), 0.01); | |
| st.session_state.min_delta_filter, st.session_state.max_delta_filter = min(min_d, max_d), max(min_d, max_d) | |
| # --- Filter anwenden --- | |
| filtered_df = calculated_df_unfiltered.copy() | |
| # OTM/ATM Filter (wenn aktiviert) | |
| if st.session_state.show_only_otm: | |
| # Behalte auch NaNs bei strike oder wenn strike >= current_price | |
| current_stock_price_filter = stock_data.get('price', 0) # Sicherer Zugriff | |
| if current_stock_price_filter > 0: # Nur filtern wenn Preis bekannt | |
| filtered_df = filtered_df[(filtered_df['strike'] >= current_stock_price_filter) | filtered_df['strike'].isna()] | |
| # Rendite Filter | |
| rendite_col = 'annualizedReturnPercentNet'; delta_col = 'delta' | |
| if rendite_col in filtered_df.columns: | |
| numeric_rendite = pd.to_numeric(filtered_df[rendite_col], errors='coerce') | |
| filtered_df = filtered_df[numeric_rendite.between(st.session_state.min_annual_return_filter, st.session_state.max_annual_return_filter, inclusive='both') | numeric_rendite.isna()] | |
| # Delta Filter | |
| if delta_col in filtered_df.columns: | |
| numeric_delta = pd.to_numeric(filtered_df[delta_col], errors='coerce') | |
| filtered_df = filtered_df[numeric_delta.between(st.session_state.min_delta_filter, st.session_state.max_delta_filter, inclusive='both') | numeric_delta.isna()] | |
| # --- Tabelle Anzeigen --- | |
| st.markdown(f"###### Call-Optionen für {datetime.strptime(selected_exp_date, '%Y-%m-%d').strftime('%d. %B %Y')} ({len(filtered_df)} angezeigt)") | |
| # Spaltenauswahl Logik | |
| selected_display_names = st.session_state.selected_columns | |
| original_cols_keys_to_display = [key for key, value in display_columns.items() if value in selected_display_names] | |
| cols_to_display_final = [col for col in original_cols_keys_to_display if col in filtered_df.columns] | |
| if not cols_to_display_final: | |
| st.warning("Keine gültigen Spalten zur Anzeige ausgewählt oder in den Daten vorhanden.") | |
| display_df_render = pd.DataFrame() | |
| else: | |
| # Sortiere standardmäßig nach Strike (optional) | |
| if 'strike' in cols_to_display_final: | |
| display_df_render = filtered_df[cols_to_display_final].sort_values(by='strike').rename(columns=display_columns) | |
| else: | |
| display_df_render = filtered_df[cols_to_display_final].rename(columns=display_columns) | |
| if not display_df_render.empty: | |
| # Formatierung | |
| format_dict = { col: '{:.2f}' for col in display_df_render.columns if col.startswith(('Strike', 'Bid', 'Ask', 'Last')) } | |
| format_dict.update({ col: '{:,.2f}' for col in display_df_render.columns if f'({currency_symbol})' in col }) | |
| format_dict.update({ col: '{:.1f}%' for col in display_df_render.columns if '%' in col and col not in ['Impl. Vol (%)', 'W\'keit OTM (%)']}) # Explizit ausschließen | |
| format_dict.update({ 'Impl. Vol (%)': '{:.1%}', 'W\'keit OTM (%)': '{:.1%}', 'Delta': '{:.3f}', 'Theta': '{:.3f}', 'Gamma': '{:.4f}', 'Vega': '{:.3f}', 'Volumen': '{:,}', 'Open Int.': '{:,}', 'DTE': '{:,d}' }) | |
| final_format_dict = {k: v for k, v in format_dict.items() if k in display_df_render.columns} | |
| # Styler | |
| styler = display_df_render.style.format(final_format_dict, na_rep="-") | |
| # Highlight basierend auf Rendite UND Delta | |
| highlight_ret_col = 'Rendite p.a. (%)'; highlight_delta_col = 'Delta' | |
| min_ret = st.session_state.highlight_min_return; max_delta = st.session_state.highlight_max_delta | |
| if highlight_ret_col in display_df_render.columns and highlight_delta_col in display_df_render.columns: | |
| try: | |
| def highlight_premium_sweetspot(row): | |
| # Konvertiere zu Numerisch, Fehler werden zu NaN | |
| ret_val = pd.to_numeric(row[highlight_ret_col], errors='coerce') | |
| delta_val = pd.to_numeric(row[highlight_delta_col], errors='coerce') | |
| # Prüfe ob beide Werte gültige Zahlen sind und die Bedingungen erfüllen | |
| if pd.notna(ret_val) and pd.notna(delta_val) and ret_val >= min_ret and delta_val <= max_delta: | |
| return ['background-color: #e8f5e9'] * len(row) # Helles Grün | |
| return [''] * len(row) # Kein Stil | |
| styler.apply(highlight_premium_sweetspot, axis=1) | |
| except Exception as e: print(f"Styling Error (Highlight Sweetspot): {e}") | |
| # Farbverläufe etc. | |
| gradient_cols = {'Rendite p.a. (%)': 'Greens', 'Return on Risk (%)': 'Greens', 'Impl. Vol (%)': 'RdYlGn', 'Delta': 'Blues', 'Theta': 'Reds_r', 'W\'keit OTM (%)': 'Greens'} | |
| for col, cmap in gradient_cols.items(): | |
| if col in display_df_render.columns: | |
| try: styler.background_gradient(cmap=cmap, subset=[col], axis=0) | |
| except Exception as e: print(f"Styling Error (Gradient {col}): {e}") | |
| bar_cols = ["Volumen", "Open Int.", "% OTM"] | |
| for col in bar_cols: | |
| if col in display_df_render.columns: | |
| try: styler.bar(subset=[col], color='lightblue', align='zero', vmin=0) | |
| except Exception as e: print(f"Styling Error (Bar {col}): {e}") | |
| st.dataframe(styler, use_container_width=True, height=500) | |
| st.caption("🖱️ Spalten sortieren. Grüner Hintergrund: Erfüllt Hervorhebungs-Kriterien (Rendite & Delta).") | |
| # --- Payoff Visualisierung --- | |
| st.divider(); st.subheader("💡 Payoff & Kennzahlen (Auswahl)") | |
| available_strikes = sorted(calculated_df_unfiltered['strike'].round(2).unique().tolist()) | |
| if available_strikes: | |
| current_p = stock_data.get('price', 0) # Sicherer Zugriff | |
| default_strike_idx = 0 # Default zum ersten Strike | |
| if current_p > 0: # Nur berechnen wenn Preis > 0 | |
| default_strike_idx = np.abs(np.array(available_strikes) - current_p).argmin() | |
| selected_strike_for_chart = st.selectbox( | |
| "Strike zur Visualisierung wählen:", options=available_strikes, | |
| index=default_strike_idx, # Standardmäßig nächster Strike | |
| format_func=lambda x: f"{currency_symbol}{x:.2f}", | |
| key="payoff_strike_select" | |
| ) | |
| # Finde die Zeile im *ungefilterten* DF mit Toleranz | |
| selected_rows = calculated_df_unfiltered[np.isclose(calculated_df_unfiltered['strike'], selected_strike_for_chart)] | |
| if not selected_rows.empty: | |
| selected_row = selected_rows.iloc[0] | |
| # Payoff Chart | |
| premium_net_chart = selected_row['premiumNetTotal']; strike_chart = selected_row['strike']; cost_basis_chart = st.session_state.cost_basis; shares_chart = st.session_state.shares_owned | |
| min_price_chart = cost_basis_chart * 0.7; max_price_chart = max(current_p, strike_chart) * 1.3; stock_prices_at_exp = np.linspace(min_price_chart, max_price_chart, 150) | |
| profit_loss = [((strike_chart - cost_basis_chart) * shares_chart + premium_net_chart) if px > strike_chart else ((px - cost_basis_chart) * shares_chart + premium_net_chart) for px in stock_prices_at_exp] | |
| payoff_df = pd.DataFrame({'Aktienkurs bei Ablauf': stock_prices_at_exp, f'Gewinn/Verlust ({currency_symbol})': profit_loss}) | |
| st.line_chart(payoff_df, x='Aktienkurs bei Ablauf', y=f'Gewinn/Verlust ({currency_symbol})'); st.caption(f"Payoff für {currency_symbol}{strike_chart:.2f} Call (Netto).") | |
| # Kennzahlen (Fokus auf Prämie & Risiko OTM) | |
| st.markdown("###### Kennzahlen für ausgewählten Strike:") | |
| kcol1, kcol2, kcol3, kcol4 = st.columns(4) | |
| with kcol1: st.metric("Prämie (Netto)", f"{currency_symbol}{selected_row.get('premiumNetTotal', 0):,.2f}", f"{selected_row.get('returnIfFlatPercentNet', 0):.1f}% Stat." if pd.notna(selected_row.get('returnIfFlatPercentNet')) and np.isfinite(selected_row.get('returnIfFlatPercentNet')) else "N/A") | |
| with kcol2: st.metric("Rendite p.a.", f"{selected_row.get('annualizedReturnPercentNet', 0):.1f}%" if pd.notna(selected_row.get('annualizedReturnPercentNet')) and selected_row.get('annualizedReturnPercentNet', 0) > 0 else "N/A") | |
| with kcol3: st.metric("Risikopuffer", f"{selected_row.get('downsideProtectionPercentNet', 0):.1f}%" if pd.notna(selected_row.get('downsideProtectionPercentNet')) and np.isfinite(selected_row.get('downsideProtectionPercentNet')) else "N/A") | |
| with kcol4: st.metric("W'keit OTM", f"{selected_row.get('prob_OTM', 0):.1%}" if pd.notna(selected_row.get('prob_OTM')) else "-", help="Ca. W'keit, dass die Option wertlos verfällt (1 - Delta).") | |
| kcol5, kcol6, kcol7, kcol8 = st.columns(4) | |
| with kcol5: st.metric("Return on Risk", f"{selected_row.get('returnOnRiskPercentNet', 0):.1f}%" if pd.notna(selected_row.get('returnOnRiskPercentNet')) and np.isfinite(selected_row.get('returnOnRiskPercentNet')) else "N/A") | |
| with kcol6: st.metric("Delta", f"{selected_row.get('delta', 0):.3f}" if pd.notna(selected_row.get('delta')) else "-") | |
| with kcol7: st.metric("Theta", f"{selected_row.get('theta', 0):.3f}" if pd.notna(selected_row.get('theta')) else "-") | |
| with kcol8: st.metric("DTE", f"{selected_row.get('DTE', 0):.0f}" if pd.notna(selected_row.get('DTE')) else "-") | |
| else: | |
| st.warning(f"Keine Daten für den ausgewählten Strike {selected_strike_for_chart} gefunden.") | |
| else: st.info("Keine Strikes zur Visualisierung verfügbar.") | |
| else: st.info("Keine Optionen entsprechen den aktuellen Filterkriterien.") | |
| else: st.warning(f"Keine Call-Optionen für {st.session_state.ticker_symbol} am {selected_exp_date} gefunden.") | |
| # --- Footer --- | |
| st.divider() | |
| st.caption(f"Haftungsausschluss: Nur zu Informationszwecken. Keine Finanzberatung. Daten von yfinance können verzögert sein. Berechnungen ohne Gewähr. Letzte Aktualisierung: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") |