Swobby commited on
Commit
acccd27
·
verified ·
1 Parent(s): 7c4a8b5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +234 -499
app.py CHANGED
@@ -1,316 +1,217 @@
 
1
  import streamlit as st
2
  import yfinance as yf
3
  import pandas as pd
4
  import numpy as np
5
  import plotly.graph_objects as go
6
  from datetime import datetime, timedelta
7
- import time # Für Fehlerbehandlung Ticker
8
 
9
  # --- Konfiguration ---
10
- st.set_page_config(layout="wide", page_title="Option Yield Pro (Lokal)")
11
 
12
  # --- Hilfsfunktionen ---
13
 
14
- # -- Caching --
15
- @st.cache_resource(ttl=600) # Cache das Ticker-OBJEKT für 10 Min.
16
  def get_ticker_object(ticker_symbol):
17
- """Holt und cacht das yfinance Ticker-Objekt."""
18
- if not ticker_symbol: return None # Frühzeitiger Ausstieg, wenn kein Ticker vorhanden
19
  try:
20
  ticker = yf.Ticker(ticker_symbol)
21
- # Prüfe Gültigkeit (löst einen kleinen Download aus)
22
- # .info kann manchmal leer sein, auch wenn Ticker gültig. Besser: History-Check?
23
  if ticker.history(period="1d").empty:
24
- raise ValueError(f"Keine Daten für {ticker_symbol} abrufbar. Ungültiger Ticker?")
25
  return ticker
26
  except Exception as e:
27
- # Fehler nicht im UI anzeigen, wenn die Funktion im Hintergrund läuft
28
- print(f"Fehler beim Erstellen des Ticker-Objekts für {ticker_symbol}: {e}")
29
- return None # Return None on failure
30
-
31
- @st.cache_data(ttl=300) # Cache die AKTIENDATEN (dict) für 5 Minuten
32
- def get_stock_data(_ticker_obj): # Nimm Objekt statt Symbol
33
- """Holt Aktieninformationen und Preis mithilfe des Ticker-Objekts."""
34
- if _ticker_obj is None:
35
- return None, "Ungültiges Ticker-Objekt"
36
 
 
 
 
37
  try:
38
  info = _ticker_obj.info
39
- # Verschiedene Felder für den aktuellen Preis prüfen
40
  price = info.get('currentPrice') or info.get('regularMarketPrice') or info.get('bid') or info.get('ask')
41
  prev_close = info.get('previousClose') or info.get('regularMarketPreviousClose')
42
-
43
- # History-Fallback, wenn Preis/Schlusskurs fehlen
44
  if price is None or prev_close is None:
45
  hist = _ticker_obj.history(period="2d")
46
  if not hist.empty:
47
  price = hist['Close'].iloc[-1]
48
- prev_close = hist['Close'].iloc[-2] if len(hist) > 1 else price # Fallback für nur einen Tag History
49
  else:
50
- # Fallback: Versuche 'financialData'
51
  fdata = info.get('financialData', {})
52
  price = fdata.get('currentPrice')
53
- if price is None:
54
- return None, "Aktueller Preis konnte nicht ermittelt werden."
55
- # Wenn prev_close immer noch fehlt, nehmen wir einfach den aktuellen Preis (suboptimal)
56
- if prev_close is None:
57
- prev_close = price
58
-
59
-
60
  change = price - prev_close
61
- change_percent = (change / prev_close) * 100 if prev_close != 0 else 0 # Vermeide Division durch Null
62
  name = info.get('shortName', _ticker_obj.ticker)
63
- currency = info.get('currency', '$') # Währung holen
64
  fifty_two_week_low = info.get('fiftyTwoWeekLow')
65
  fifty_two_week_high = info.get('fiftyTwoWeekHigh')
66
-
67
  stock_data = {
68
- "name": name,
69
- "price": price,
70
- "change": change,
71
- "change_percent": change_percent,
72
- "currency": currency,
73
- "fifty_two_week_low": fifty_two_week_low,
74
- "fifty_two_week_high": fifty_two_week_high,
75
  }
76
- return stock_data, None # Return data dict and None for error
77
  except Exception as e:
78
- return None, f"Fehler beim Abrufen der Aktiendaten für {_ticker_obj.ticker}: {e}"
79
 
80
- @st.cache_data(ttl=600) # Cache die Liste der ABLAUFDATEN
81
  def get_option_dates(_ticker_obj):
82
- """Holt verfügbare Optionsablaufdaten."""
83
  if _ticker_obj is None: return []
84
  try:
85
- # Manchmal gibt .options einen Fehler, wenn keine Optionen existieren
86
  opts = _ticker_obj.options
87
  return opts if opts else []
88
  except Exception as e:
89
- print(f"Fehler beim Abrufen der Optionsdaten ({_ticker_obj.ticker}): {e}")
90
  return []
91
 
92
- @st.cache_data(ttl=300) # Cache die OPTIONSKETTE DataFrame für 5 Min.
93
  def get_option_chain(_ticker_obj, expiration_date):
94
- """Holt die Optionskette für ein spezifisches Datum."""
95
  if _ticker_obj is None or not expiration_date: return pd.DataFrame()
96
  try:
97
  chain = _ticker_obj.option_chain(expiration_date)
98
- # Nur Calls, Index zurücksetzen und sicherstellen, dass Spalten existieren
99
  calls = chain.calls.reset_index(drop=True).copy()
100
- # Standardwerte für fehlende Griechen setzen (falls yfinance sie nicht liefert)
101
  for greek in ['delta', 'gamma', 'theta', 'vega']:
102
- if greek not in calls.columns:
103
- calls[greek] = np.nan
104
  return calls
105
  except Exception as e:
106
- # Oft passiert "KeyError: 'calls'" wenn keine Kette für das Datum existiert
107
- print(f"Fehler/Warnung beim Abrufen der Optionskette für {_ticker_obj.ticker} am {expiration_date}: {e}")
108
- return pd.DataFrame() # Return empty dataframe on error
109
 
110
- @st.cache_data(ttl=1800) # Cache History länger (30 Min)
111
  def get_stock_history(_ticker_obj, period='1y', interval='1d'):
112
- """Holt historische Kurse für den Chart."""
113
  if _ticker_obj is None: return pd.DataFrame()
114
  try:
115
- # yfinance erwartet manchmal Großbuchstaben für Intervalle wie '1WK'
116
- interval_map = {'1d': '1d', '1wk': '1wk', '1mo': '1mo'} # Mapping zur Sicherheit
117
  hist = _ticker_obj.history(period=period, interval=interval_map.get(interval.lower(), '1d'))
118
  return hist
119
  except Exception as e:
120
- st.error(f"Fehler beim Laden der Historie für {_ticker_obj.ticker} (Periode: {period}, Intervall: {interval}): {e}")
121
  return pd.DataFrame()
122
 
123
  # -- Berechnungen & Logik --
124
  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):
125
- """Berechnet Covered Call Metriken und fügt sie dem DataFrame hinzu."""
126
  if calls_df.empty: return calls_df
127
-
128
  df = calls_df.copy()
129
 
130
- # --- Prämienberechnung ---
131
- if custom_premium is not None and custom_premium > 0:
132
- df['assumedPremiumPerShare'] = float(custom_premium) # Stelle sicher, dass es float ist
133
  elif assumed_premium_price_type == 'mid':
134
- bid_filled = df['bid'].fillna(0)
135
- ask_filled = df['ask'].fillna(df['lastPrice']).fillna(bid_filled) # Fallback: Last -> Bid -> 0
136
  df['assumedPremiumPerShare'] = (bid_filled + ask_filled) / 2
137
- elif assumed_premium_price_type == 'last':
138
- df['assumedPremiumPerShare'] = df['lastPrice'].fillna(0)
139
- else: # Default to Bid
140
- df['assumedPremiumPerShare'] = df['bid'].fillna(0)
141
-
142
- # Stelle sicher, dass Prämie nicht negativ ist und float ist
143
  df['assumedPremiumPerShare'] = df['assumedPremiumPerShare'].apply(lambda x: max(0, float(x)))
144
-
145
  num_contracts = float(shares_owned) / 100.0
146
  df['premiumGrossTotal'] = df['assumedPremiumPerShare'] * float(shares_owned)
147
  total_commission = num_contracts * float(commission_per_contract)
148
- df['premiumNetTotal'] = df['premiumGrossTotal'] - total_commission # Nettoprämie nach Provision
149
-
150
- # --- Kostenbasis & Kernberechnungen ---
151
- cost_basis = float(cost_basis)
152
- shares_owned = float(shares_owned)
153
- current_price = float(current_price)
154
 
 
 
155
  df['costBasisTotal'] = cost_basis * shares_owned
156
  df['strike'] = df['strike'].astype(float)
 
157
 
158
- # Max Profit if Assigned (Nettoprämie verwenden)
159
- df['maxProfitAssignedNet'] = ((df['strike'] - cost_basis) * shares_owned) + df['premiumNetTotal']
160
-
161
- # --- Prozentuale Metriken (auf Basis der Nettoprämie) ---
162
  cost_basis_total_val = df['costBasisTotal'].iloc[0] if not df.empty else 0
163
- # Berechne premium_net_per_share als Series
164
  premium_net_per_share = df['premiumNetTotal'] / shares_owned if shares_owned > 0 else pd.Series([0.0] * len(df), index=df.index)
 
 
165
 
166
- # Fall: Kostenbasis > 0
167
  if cost_basis_total_val > 0:
168
- df['maxProfitPercentNet'] = (df['maxProfitAssignedNet'] / cost_basis_total_val) * 100
169
  df['returnIfFlatPercentNet'] = (df['premiumNetTotal'] / cost_basis_total_val) * 100
170
- # Downside Protection braucht premium_net_per_share (Series) und cost_basis (Skalar)
171
  df['downsideProtectionPercentNet'] = (premium_net_per_share / cost_basis) * 100 if cost_basis > 0 else np.inf
172
- # Return on Risk braucht premium_net_per_share (Series) und capital_at_risk (Series)
173
  capital_at_risk = cost_basis - premium_net_per_share
174
- df['returnOnRiskPercentNet'] = np.where(
175
- capital_at_risk > 0, (premium_net_per_share / capital_at_risk) * 100, np.inf
176
- )
177
- # Fall: Kostenbasis <= 0 (oder nicht berechenbar)
178
  else:
179
- for col in ['maxProfitPercentNet', 'returnIfFlatPercentNet', 'downsideProtectionPercentNet', 'returnOnRiskPercentNet']:
180
- df[col] = np.inf
181
 
182
- # --- DTE (Days To Expiration) ---
183
  try:
184
- # Verwende datetime.date für reinen Datumsvergleich
185
- exp_date_obj = datetime.strptime(expiration_date, '%Y-%m-%d').date()
186
- today = datetime.now().date()
187
- dte = (exp_date_obj - today).days
188
- if dte < 0: dte = 0 # Falls bereits abgelaufen
189
- except ValueError:
190
- dte = 0
191
- # st.warning nicht in cached function, print ist besser für Debugging
192
- print(f"Konnte Datum {expiration_date} nicht für DTE-Berechnung parsen.")
193
-
194
  df['DTE'] = dte
195
 
196
- # --- Annualisierte Rendite (Basis: Nettoprämie, wenn Flat) ---
197
- df['annualizedReturnPercentNet'] = 0.0 # Initialisiere Spalte
198
- # Stelle sicher, dass returnIfFlatPercentNet existiert und numerisch ist
199
  if 'returnIfFlatPercentNet' in df.columns:
200
- df['returnIfFlatPercentNet'] = pd.to_numeric(df['returnIfFlatPercentNet'], errors='coerce') # Umwandeln, Fehler -> NaN
201
  valid_annual_mask = (df['DTE'] > 0) & df['returnIfFlatPercentNet'].notna() & (df['returnIfFlatPercentNet'] > 0)
202
- safe_dte = df.loc[valid_annual_mask, 'DTE'].replace(0, 1) # Ersetze 0 durch 1 für Berechnung
203
- df.loc[valid_annual_mask, 'annualizedReturnPercentNet'] = \
204
- df.loc[valid_annual_mask, 'returnIfFlatPercentNet'] * (365.0 / safe_dte)
205
- # Setze NaN für ungültige Fälle
206
  df['annualizedReturnPercentNet'] = df['annualizedReturnPercentNet'].fillna(0.0)
207
 
208
-
209
- # --- Breakeven (Basis: Nettoprämie pro Aktie) ---
210
- # premium_net_per_share ist bereits eine Series
211
  df['breakeven'] = cost_basis - premium_net_per_share
212
 
213
- # --- % OTM (Out of the Money) ---
214
  df['percentOTM'] = 0.0
215
  otm_mask = df['strike'] > current_price
216
- # Wende Berechnung nur auf OTM-Strikes an
217
  df.loc[otm_mask, 'percentOTM'] = ((df.loc[otm_mask, 'strike'] - current_price) / current_price) * 100
218
 
219
- # --- Griechen (Standardwerte NaN, wenn nicht vorhanden) ---
220
  for greek in ['delta', 'gamma', 'theta', 'vega']:
221
- if greek in df.columns:
222
- df[greek] = pd.to_numeric(df[greek], errors='coerce').fillna(np.nan) # Sicherstellen, dass numerisch oder NaN
 
 
 
 
223
 
224
  return df
225
 
 
226
  def is_monthly_expiration(date_str):
227
- """Prüft, ob ein Datum wahrscheinlich ein Monatsverfall ist (3. Freitag)."""
228
- try:
229
- date_obj = datetime.strptime(date_str, '%Y-%m-%d')
230
- # Standard US equity monthly expiration
231
- return 15 <= date_obj.day <= 21 and date_obj.weekday() == 4 # 4 = Freitag
232
- except ValueError:
233
- return False
234
-
235
  def is_weekly_expiration(date_str):
236
- """Prüft, ob ein Datum wahrscheinlich ein Wochenverfall ist (Freitag, aber nicht der 3.)."""
237
- try:
238
- date_obj = datetime.strptime(date_str, '%Y-%m-%d')
239
- # Freitag, aber nicht der 3. Freitag des Monats
240
- is_friday = date_obj.weekday() == 4
241
- is_third_friday = 15 <= date_obj.day <= 21
242
- return is_friday and not is_third_friday
243
- except ValueError:
244
- return False
245
-
246
- # --- Plotting Funktion ---
247
- def plot_stock_chart(hist_df, ticker_symbol, indicators, sma_periods):
248
- """Erstellt einen interaktiven Plotly Chart."""
249
- if hist_df.empty: return go.Figure() # Leere Figur zurückgeben, wenn keine Daten
250
 
 
 
 
251
  fig = go.Figure()
252
-
253
- # Candlestick Chart hinzufügen (nur wenn notwendige Spalten vorhanden sind)
254
  required_cols = ['Open', 'High', 'Low', 'Close']
255
  if all(col in hist_df.columns for col in required_cols):
256
- fig.add_trace(go.Candlestick(x=hist_df.index,
257
- open=hist_df['Open'],
258
- high=hist_df['High'],
259
- low=hist_df['Low'],
260
- close=hist_df['Close'],
261
- name='Kurs'))
262
- else:
263
- st.warning("Notwendige Spalten (Open, High, Low, Close) für Candlestick fehlen in historischen Daten.")
264
- # Fallback: Linienchart für Schlusskurs
265
- if 'Close' in hist_df.columns:
266
- fig.add_trace(go.Scatter(x=hist_df.index, y=hist_df['Close'], mode='lines', name='Schlusskurs'))
267
-
268
- # Indikatoren hinzufügen
269
  if 'SMA' in indicators and 'Close' in hist_df.columns:
270
  for period in sma_periods:
271
- if period <= 0: continue # Ungültige Periode überspringen
272
  sma_col = f'SMA_{period}'
273
- hist_df[sma_col] = hist_df['Close'].rolling(window=period, min_periods=1).mean() # min_periods=1 für Anfangswerte
274
  fig.add_trace(go.Scatter(x=hist_df.index, y=hist_df[sma_col], mode='lines', name=sma_col))
275
-
276
  if 'Bollinger Bands' in indicators and 'Close' in hist_df.columns:
277
- # Nutze erste gültige SMA-Periode oder Standard 20
278
  bb_period = next((p for p in sma_periods if p > 0), 20)
279
  sma_col = f'SMA_{bb_period}'
280
- # Berechne SMA falls noch nicht vorhanden oder Periode ungültig war
281
- if sma_col not in hist_df.columns:
282
- hist_df[sma_col] = hist_df['Close'].rolling(window=bb_period, min_periods=1).mean()
283
-
284
  std_dev = hist_df['Close'].rolling(window=bb_period, min_periods=1).std()
285
- upper_band = hist_df[sma_col] + (std_dev * 2)
286
- lower_band = hist_df[sma_col] - (std_dev * 2)
287
- # Nur hinzufügen, wenn Bänder berechnet werden konnten
288
- if not upper_band.empty:
289
- 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)')))
290
- if not lower_band.empty:
291
- 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)'))
292
-
293
- fig.update_layout(
294
- title=f'{ticker_symbol} Kursverlauf & Indikatoren',
295
- xaxis_title='Datum',
296
- yaxis_title='Preis',
297
- xaxis_rangeslider_visible=False,
298
- legend_title_text='Legende',
299
- height=500,
300
- margin=dict(l=20, r=20, t=40, b=20) # Ränder reduzieren
301
- )
302
  return fig
303
 
304
  # --- Session State Initialisierung ---
305
- # Definiere die Basis-Spaltennamen mit einem Platzhalter
306
- base_default_columns = [
307
- 'Strike', 'Bid', 'Ask', 'Last', 'Impl. Vol (%)', 'Prämie Netto ({currency})',
308
- 'Rendite (Statisch %)', 'Rendite p.a. (%)', 'Max. Gewinn ({currency})', 'Max. Gewinn (%)',
309
- 'Breakeven ({currency})', 'Risikopuffer (%)', 'Return on Risk (%)', '% OTM', 'Delta', 'Theta', 'DTE'
 
 
 
 
 
310
  ]
311
- other_possible_cols = ['Volumen', 'Open Int.', 'Gamma', 'Vega']
312
 
313
- # Initialisiere State, falls nicht vorhanden
314
  if 'ticker_symbol' not in st.session_state: st.session_state.ticker_symbol = "TSLA"
315
  if 'shares_owned' not in st.session_state: st.session_state.shares_owned = 100
316
  if 'cost_basis' not in st.session_state: st.session_state.cost_basis = 100.00
@@ -318,190 +219,112 @@ if 'stock_data' not in st.session_state: st.session_state.stock_data = None
318
  if 'commission' not in st.session_state: st.session_state.commission = 0.65
319
  if 'premium_price_type' not in st.session_state: st.session_state.premium_price_type = 'bid'
320
  if 'custom_premium' not in st.session_state: st.session_state.custom_premium = None
 
321
  if 'selected_columns' not in st.session_state:
322
- st.session_state.selected_columns = [col.format(currency='$') for col in base_default_columns] # Initial mit $
323
- if 'min_annual_return_filter' not in st.session_state: st.session_state.min_annual_return_filter = 0.0
324
  if 'max_annual_return_filter' not in st.session_state: st.session_state.max_annual_return_filter = 1000.0
325
  if 'min_delta_filter' not in st.session_state: st.session_state.min_delta_filter = 0.0
326
- if 'max_delta_filter' not in st.session_state: st.session_state.max_delta_filter = 1.0
327
- if 'highlight_min_return' not in st.session_state: st.session_state.highlight_min_return = 20.0
 
 
 
328
  if 'chart_period' not in st.session_state: st.session_state.chart_period = '1y'
329
  if 'chart_interval' not in st.session_state: st.session_state.chart_interval = '1d'
330
  if 'chart_indicators' not in st.session_state: st.session_state.chart_indicators = ['SMA']
331
  if 'chart_sma_periods' not in st.session_state: st.session_state.chart_sma_periods = [50, 200]
332
  if 'date_filter_type' not in st.session_state: st.session_state.date_filter_type = 'Alle'
333
 
 
334
  # --- Währung und Spaltennamen VOR der Sidebar bestimmen ---
335
- current_currency_symbol = '$' # Default
336
  if st.session_state.stock_data and isinstance(st.session_state.stock_data, dict):
337
  current_currency_symbol = st.session_state.stock_data.get('currency', '$')
338
 
339
- current_default_columns = [col.format(currency=current_currency_symbol) for col in base_default_columns]
340
- current_all_possible_columns = current_default_columns + other_possible_cols
 
341
 
342
- # Stelle sicher, dass die Defaults im State die aktuelle Währung verwenden
343
- # Ersetze bekannte Währungssymbole im State, falls sie nicht zur aktuellen passen
344
  temp_selected_columns = []
345
  for col in st.session_state.selected_columns:
346
- # Ersetze bekannte Währungssymbole oder Platzhalter
347
- updated_col = col.replace("($)", f"({current_currency_symbol})")
348
- updated_col = updated_col.replace("(USD)", f"({current_currency_symbol})")
349
- updated_col = updated_col.replace("(EUR)", f"({current_currency_symbol})")
350
- # Füge hinzu, wenn es jetzt eine gültige Spalte ist
351
- if updated_col in current_all_possible_columns:
352
- temp_selected_columns.append(updated_col)
353
- # Fallback: Wenn Originalspalte (ohne Währung) existiert, füge sie mit neuer Währung hinzu
354
- elif any(base_col.replace("{currency}", "") in updated_col for base_col in base_default_columns) and updated_col.format(currency=current_currency_symbol) in current_all_possible_columns:
355
- temp_selected_columns.append(updated_col.format(currency=current_currency_symbol))
356
-
357
-
358
  st.session_state.selected_columns = temp_selected_columns
359
- # Filtere Defaults auf nur die, die jetzt in den aktuellen Optionen vorkommen
360
- valid_defaults_for_multiselect = [
361
- col for col in st.session_state.selected_columns if col in current_all_possible_columns
362
- ]
363
- # Wenn nach Filterung keine Defaults mehr übrig sind, nimm die Standard-Defaults
364
- if not valid_defaults_for_multiselect:
365
- valid_defaults_for_multiselect = current_default_columns
366
 
367
 
368
  # --- SIDEBAR ---
369
  with st.sidebar:
370
  st.header("⚙️ Eingaben & Einstellungen")
371
-
372
- # -- Positionsdetails --
373
  st.subheader("Positionsdetails")
374
- ticker_input = st.text_input(
375
- "Aktien Ticker", value=st.session_state.ticker_symbol, key="ticker_input_key"
376
- ).upper()
377
- shares_input = st.number_input(
378
- "Anzahl Aktien", min_value=100, step=100, value=st.session_state.shares_owned, key="shares_input_key"
379
- )
380
- cost_basis_input = st.number_input(
381
- "Ø Einstandskurs / Aktie", min_value=0.01, step=0.01, value=st.session_state.cost_basis, format="%.2f", key="cost_basis_input_key"
382
- )
383
-
384
- # -- Annahmen --
385
  st.subheader("Annahmen")
386
- premium_price_type_input = st.selectbox(
387
- "Preis für Prämie?", options=['bid', 'mid', 'last'],
388
- index=['bid', 'mid', 'last'].index(st.session_state.premium_price_type),
389
- help="Welcher Preis für Prämienberechnung? 'Bid' konservativ.", key="premium_select_key"
390
- )
391
- custom_premium_input = st.number_input(
392
- "Manuelle Prämie / Aktie", min_value=0.0, step=0.01,
393
- value=st.session_state.custom_premium if st.session_state.custom_premium is not None else 0.0,
394
- format="%.2f", help="> 0 überschreibt Auswahl oben.", key="custom_prem_key"
395
- )
396
- commission_input = st.number_input(
397
- "Provision / Kontrakt", min_value=0.0, step=0.01, value=st.session_state.commission, format="%.2f", key="commission_key"
398
- )
399
-
400
- # -- Analyse Knopf --
401
  if st.button("Analyse Starten / Werte Übernehmen", key="analyze_sidebar", use_container_width=True):
402
  ticker_changed = (ticker_input != st.session_state.ticker_symbol)
403
-
404
- # Update state from inputs
405
- st.session_state.ticker_symbol = ticker_input
406
- st.session_state.shares_owned = shares_input
407
- st.session_state.cost_basis = cost_basis_input
408
- st.session_state.premium_price_type = premium_price_type_input
409
- st.session_state.custom_premium = custom_premium_input if custom_premium_input > 0 else None
410
- st.session_state.commission = commission_input
411
-
412
- # Reset stock data if ticker changed to force reload
413
  if ticker_changed:
414
  st.session_state.stock_data = None
415
- # Reset selected columns to default with '$' placeholder when ticker changes,
416
- # as we don't know the new currency yet.
417
- st.session_state.selected_columns = [col.format(currency='$') for col in base_default_columns]
418
 
419
- st.rerun() # Force rerun to apply changes and reload data if needed
 
 
 
420
 
421
- st.divider() # Visuelle Trennung
 
422
 
423
- # -- Tabellen-Anpassung --
424
- st.subheader("Tabellen-Anzeige")
425
- selected_cols_input = st.multiselect(
426
- "Angezeigte Spalten:",
427
- options=sorted(list(set(current_all_possible_columns))), # Use current options
428
- default=valid_defaults_for_multiselect, # Use filtered current defaults
429
- key="col_select_key"
430
- )
431
- # Update state only if selection actually changed to prevent unnecessary reruns
432
- if selected_cols_input != st.session_state.selected_columns:
433
- st.session_state.selected_columns = selected_cols_input
434
- st.rerun() # Rerun to apply column selection
435
-
436
- # -- Hervorhebung --
437
- st.session_state.highlight_min_return = st.number_input(
438
- "Rendite p.a. hervorheben > X%",
439
- min_value=0.0, step=1.0,
440
- value=st.session_state.highlight_min_return,
441
- format="%.1f", key="highlight_key"
442
- )
443
 
444
  st.divider()
445
-
446
- # -- Chart-Einstellungen --
447
  st.subheader("Chart-Einstellungen")
 
448
  st.session_state.chart_period = st.selectbox("Zeitraum:", ['1mo', '3mo', '6mo', 'ytd', '1y', '2y', '5y', 'max'], index=4, key="chart_period_key")
449
  st.session_state.chart_interval = st.selectbox("Intervall:", ['1d', '1wk', '1mo'], index=0, key="chart_interval_key")
450
  st.session_state.chart_indicators = st.multiselect("Indikatoren:", ['SMA', 'Bollinger Bands'], default=st.session_state.chart_indicators, key="chart_ind_key")
451
  if 'SMA' in st.session_state.chart_indicators or 'Bollinger Bands' in st.session_state.chart_indicators:
452
- sma_periods_str = st.text_input(
453
- "SMA Perioden (kommagetrennt):",
454
- value=", ".join(map(str, st.session_state.chart_sma_periods)),
455
- key="chart_sma_key"
456
- )
457
  try:
458
- parsed_periods = [int(p.strip()) for p in sma_periods_str.split(',') if p.strip().isdigit() and int(p.strip()) > 0] # Nur positive Zahlen
459
- if parsed_periods: # Nur updaten, wenn gültige Perioden gefunden wurden
460
- st.session_state.chart_sma_periods = parsed_periods
461
- else: # Wenn Eingabe leer oder ungültig war
462
- if sma_periods_str.strip(): # Nur warnen wenn etwas eingegeben wurde
463
- st.warning("Ungültige Eingabe für SMA Perioden. Bsp: 50, 200")
464
- st.session_state.chart_sma_periods = [50, 200] # Fallback
465
- except:
466
- st.warning("Fehler beim Verarbeiten der SMA Perioden.")
467
- st.session_state.chart_sma_periods = [50, 200] # Fallback
468
 
469
  # --- HAUPTSEITE ---
470
- st.title("📈 Option Yield Pro (Lokal)")
471
 
472
- # Nur weitermachen, wenn Ticker im State ist
473
- if not st.session_state.ticker_symbol:
474
- st.info("👈 Bitte geben Sie links einen Aktien-Ticker ein und klicken Sie auf 'Analyse Starten'.")
475
- st.stop()
476
 
477
  # --- Aktiendaten Laden & Anzeigen ---
478
- # Versuche Ticker Objekt zu holen (aus Cache oder neu)
479
  ticker_obj = get_ticker_object(st.session_state.ticker_symbol)
480
  if ticker_obj is None:
481
- st.error(f"Konnte Ticker-Objekt für '{st.session_state.ticker_symbol}' nicht laden. Ist der Ticker gültig?")
482
- st.session_state.ticker_symbol = "" # Reset ticker
483
- st.session_state.stock_data = None # Reset data
484
- time.sleep(2) # Allow user to see the message
485
- st.rerun()
486
- # st.stop() # Alternative
487
-
488
- # Lade Aktiendaten nur, wenn sie noch nicht im State sind für diesen Ticker
489
  rerun_needed_after_data_load = False
490
  if st.session_state.stock_data is None:
491
- with st.spinner(f"Lade Daten für {st.session_state.ticker_symbol}..."):
492
- stock_data_result, error_msg = get_stock_data(ticker_obj)
493
- if error_msg:
494
- st.error(f"Fehler Aktiendaten: {error_msg}")
495
- st.stop()
496
- st.session_state.stock_data = stock_data_result
497
- rerun_needed_after_data_load = True # Mark that a rerun is needed
498
-
499
- # Wichtig: Wenn Daten gerade geladen wurden, neu ausführen, damit die Währung
500
- # korrekt in der Sidebar *beim nächsten Durchlauf* verwendet wird!
501
- if rerun_needed_after_data_load:
502
- st.rerun()
503
-
504
- # --- Ab hier gehen wir davon aus, dass stock_data im State ist ---
505
  stock_data = st.session_state.stock_data
506
  currency_symbol = stock_data.get('currency', '$')
507
 
@@ -510,134 +333,80 @@ display_columns = {
510
  'strike': 'Strike', 'bid': 'Bid', 'ask': 'Ask', 'lastPrice': 'Last',
511
  'volume': 'Volumen', 'openInterest': 'Open Int.', 'impliedVolatility': 'Impl. Vol (%)',
512
  'premiumNetTotal': f'Prämie Netto ({currency_symbol})',
513
- 'returnIfFlatPercentNet': 'Rendite (Statisch %)',
514
- 'annualizedReturnPercentNet': 'Rendite p.a. (%)',
515
- 'maxProfitAssignedNet': f'Max. Gewinn ({currency_symbol})',
516
- 'maxProfitPercentNet': 'Max. Gewinn (%)',
517
- 'breakeven': f'Breakeven ({currency_symbol})',
518
  'downsideProtectionPercentNet': 'Risikopuffer (%)',
519
  'returnOnRiskPercentNet': 'Return on Risk (%)',
520
- '% OTM': '% OTM', 'delta': 'Delta', 'theta': 'Theta',
521
- 'gamma': 'Gamma', 'vega': 'Vega','DTE': 'DTE'
 
522
  }
523
 
524
  # --- Anzeige der Aktiendaten ---
525
  st.header(f"📊 {stock_data['name']} ({st.session_state.ticker_symbol})")
526
  price_color = "green" if stock_data['change'] >= 0 else "red"
527
  mcol1, mcol2, mcol3 = st.columns(3)
528
- with mcol1:
529
- st.metric(f"Akt. Kurs ({currency_symbol})", f"{stock_data.get('price', 0):.2f}", f"{stock_data.get('change', 0):.2f} ({stock_data.get('change_percent', 0):.2f}%)", delta_color="normal" if price_color == "green" else "inverse")
530
- with mcol2:
531
- low_52w = stock_data.get('fifty_two_week_low')
532
- if low_52w is not None: st.metric("52W Tief", f"{currency_symbol}{low_52w:.2f}")
533
- with mcol3:
534
- high_52w = stock_data.get('fifty_two_week_high')
535
- if high_52w is not None: st.metric("52W Hoch", f"{currency_symbol}{high_52w:.2f}")
536
 
537
  # --- Chart Sektion ---
538
  with st.expander("📈 Detailierter Aktienchart Anzeigen/Verbergen"):
539
  hist_df = get_stock_history(ticker_obj, st.session_state.chart_period, st.session_state.chart_interval)
540
- if not hist_df.empty:
541
- fig = plot_stock_chart(hist_df, st.session_state.ticker_symbol, st.session_state.chart_indicators, st.session_state.chart_sma_periods)
542
- st.plotly_chart(fig, use_container_width=True)
543
- else:
544
- st.warning("Keine historischen Daten für die gewählten Chart-Parameter verfügbar.")
545
 
546
- st.divider()
547
- st.header("🔍 Options-Analyse")
548
 
549
  # --- Ablaufdatum Auswahl ---
550
  col_d1, col_d2 = st.columns([1, 3])
551
  with col_d1:
552
- st.markdown("###### Verfallstyp Filtern:")
553
- date_filter_options = ['Alle', 'Monatlich', 'Wöchentlich']
554
- # Stelle sicher, dass der Index gültig ist, falls Option nicht existiert (sollte nicht passieren)
555
- try:
556
- date_filter_idx = date_filter_options.index(st.session_state.date_filter_type)
557
- except ValueError:
558
- date_filter_idx = 0 # Fallback auf 'Alle'
559
- st.session_state.date_filter_type = st.radio(
560
- "Typ:", options=date_filter_options, index=date_filter_idx,
561
- horizontal=True, label_visibility="collapsed"
562
- )
563
  with col_d2:
564
  st.markdown("###### Ablaufdatum Wählen:")
565
  option_dates_all = get_option_dates(ticker_obj)
566
- if not option_dates_all:
567
- st.warning("Keine Optionsdaten gefunden.")
568
- st.stop()
569
-
570
- # Filter dates based on selection
571
- if st.session_state.date_filter_type == 'Monatlich':
572
- option_dates_filtered = [d for d in option_dates_all if is_monthly_expiration(d)]
573
- elif st.session_state.date_filter_type == 'Wöchentlich':
574
- option_dates_filtered = [d for d in option_dates_all if is_weekly_expiration(d)]
575
- else: # Alle
576
- option_dates_filtered = option_dates_all
577
-
578
- if not option_dates_filtered:
579
- st.warning(f"Keine '{st.session_state.date_filter_type}' Ablaufdaten gefunden.")
580
- selected_exp_date = None
581
- else:
582
- selected_exp_date = st.selectbox(
583
- "Verfügbare Daten:", options=option_dates_filtered, index=0, label_visibility="collapsed"
584
- )
585
 
586
  # --- Optionskette Laden & Berechnen ---
587
  if selected_exp_date:
588
- # Lade Kette mit Spinner
589
- with st.spinner(f"Lade Optionskette für {selected_exp_date}..."):
590
- calls_chain = get_option_chain(ticker_obj, selected_exp_date)
591
-
592
  if not calls_chain.empty:
593
- # Berechne Metriken mit Spinner
594
- with st.spinner("Berechne Metriken..."):
595
- calculated_df_unfiltered = calculate_covered_calls(
596
- calls_chain, stock_data['price'], st.session_state.shares_owned,
597
- st.session_state.cost_basis, selected_exp_date, st.session_state.premium_price_type,
598
- st.session_state.custom_premium, st.session_state.commission
599
- )
600
 
601
  # --- Tabellenfilter Widgets ---
602
  st.markdown("###### Tabellenfilter:")
603
  fcol1, fcol2 = st.columns(2)
604
- with fcol1:
605
- # Stelle sicher, dass min <= max
606
- min_r, max_r = st.slider(
607
- "Filter: Rendite p.a. (%)", 0.0, 500.0,
608
- (st.session_state.min_annual_return_filter, st.session_state.max_annual_return_filter),
609
- step=5.0
610
- )
611
- st.session_state.min_annual_return_filter = min(min_r, max_r)
612
- st.session_state.max_annual_return_filter = max(min_r, max_r)
613
-
614
- with fcol2:
615
- min_d, max_d = st.slider(
616
- "Filter: Delta", 0.0, 1.0,
617
- (st.session_state.min_delta_filter, st.session_state.max_delta_filter),
618
- step=0.01
619
- )
620
- st.session_state.min_delta_filter = min(min_d, max_d)
621
- st.session_state.max_delta_filter = max(min_d, max_d)
622
 
623
  # --- Filter anwenden ---
624
  filtered_df = calculated_df_unfiltered.copy()
625
- # Rendite (behandle NaN und unendliche Werte korrekt)
626
- rendite_col = 'annualizedReturnPercentNet'
 
 
 
 
 
627
  if rendite_col in filtered_df.columns:
628
  numeric_rendite = pd.to_numeric(filtered_df[rendite_col], errors='coerce')
629
- filtered_df = filtered_df[
630
- numeric_rendite.between(st.session_state.min_annual_return_filter, st.session_state.max_annual_return_filter, inclusive='both') |
631
- numeric_rendite.isna() # Behalte NaNs bei
632
- ]
633
- # Delta (behandle NaN korrekt)
634
- delta_col = 'delta'
635
  if delta_col in filtered_df.columns:
636
  numeric_delta = pd.to_numeric(filtered_df[delta_col], errors='coerce')
637
- filtered_df = filtered_df[
638
- numeric_delta.between(st.session_state.min_delta_filter, st.session_state.max_delta_filter, inclusive='both') |
639
- numeric_delta.isna() # Behalte NaNs bei
640
- ]
641
 
642
  # --- Tabelle Anzeigen ---
643
  st.markdown(f"###### Call-Optionen für {datetime.strptime(selected_exp_date, '%Y-%m-%d').strftime('%d. %B %Y')} ({len(filtered_df)} angezeigt)")
@@ -647,115 +416,81 @@ if selected_exp_date:
647
  original_cols_keys_to_display = [key for key, value in display_columns.items() if value in selected_display_names]
648
  cols_to_display_final = [col for col in original_cols_keys_to_display if col in filtered_df.columns]
649
 
650
- if not cols_to_display_final:
651
- st.warning("Keine gültigen Spalten zur Anzeige ausgewählt oder in den Daten vorhanden.")
652
- display_df_render = pd.DataFrame()
653
- else:
654
- display_df_render = filtered_df[cols_to_display_final].rename(columns=display_columns)
655
 
656
  if not display_df_render.empty:
657
- # Formatierungsregeln dynamisch anwenden
658
  format_dict = { col: '{:.2f}' for col in display_df_render.columns if col.startswith(('Strike', 'Bid', 'Ask', 'Last')) }
659
  format_dict.update({ col: '{:,.2f}' for col in display_df_render.columns if f'({currency_symbol})' in col })
660
- format_dict.update({ col: '{:.1f}%' for col in display_df_render.columns if '%' in col and 'Impl. Vol' not in col })
661
- format_dict.update({
662
- 'Impl. Vol (%)': '{:.1%}', 'Delta': '{:.3f}', 'Theta': '{:.3f}',
663
- 'Gamma': '{:.4f}', 'Vega': '{:.3f}', 'Volumen': '{:,}',
664
- 'Open Int.': '{:,}', 'DTE': '{:,d}'
665
- })
666
  final_format_dict = {k: v for k, v in format_dict.items() if k in display_df_render.columns}
667
 
668
  # Styler
669
  styler = display_df_render.style.format(final_format_dict, na_rep="-")
670
 
671
- # --- Dynamisches Styling anwenden (mit Fehlerbehandlung) ---
672
- # Highlight Rendite
673
- highlight_return_col = 'Rendite p.a. (%)'
674
- if highlight_return_col in display_df_render.columns:
675
- min_ret_highlight = st.session_state.highlight_min_return
676
  try:
677
- styler.applymap(
678
- lambda x: f'background-color: #e8f5e9' if pd.notna(x) and isinstance(x, (int, float)) and x >= min_ret_highlight else '',
679
- subset=[highlight_return_col]
680
- )
681
- except Exception as e: print(f"Styling Error (Highlight): {e}")
682
-
683
- # Farbverläufe
684
- gradient_cols = {
685
- 'Rendite p.a. (%)': 'Greens', 'Return on Risk (%)': 'Greens',
686
- 'Impl. Vol (%)': 'RdYlGn', 'Delta': 'Blues', 'Theta': 'Reds_r'
687
- }
688
  for col, cmap in gradient_cols.items():
689
  if col in display_df_render.columns:
690
- try: styler.background_gradient(cmap=cmap, subset=[col], axis=0) # axis=0 für spaltenweisen Verlauf
691
  except Exception as e: print(f"Styling Error (Gradient {col}): {e}")
692
-
693
- # Balken
694
  bar_cols = ["Volumen", "Open Int.", "% OTM"]
695
  for col in bar_cols:
696
  if col in display_df_render.columns:
697
- try: styler.bar(subset=[col], color='lightblue', align='zero', vmin=0)
698
- except Exception as e: print(f"Styling Error (Bar {col}): {e}")
699
 
700
- # Tabelle anzeigen
701
  st.dataframe(styler, use_container_width=True, height=500)
702
- st.caption("🖱️ Klicken Sie auf Spaltenüberschriften zum Sortieren.")
703
 
704
  # --- Payoff Visualisierung ---
705
- st.divider()
706
- st.subheader("💡 Payoff & Kennzahlen (Auswahl)")
707
  available_strikes = sorted(calculated_df_unfiltered['strike'].round(2).unique().tolist())
708
  if available_strikes:
709
- current_p = stock_data['price']
710
- # Index des Strikes finden, der dem aktuellen Preis am nächsten ist
711
- default_strike_idx = np.abs(np.array(available_strikes) - current_p).argmin()
712
-
713
- selected_strike_for_chart = st.selectbox(
714
- "Strike zur Visualisierung wählen:", options=available_strikes,
715
- index=default_strike_idx, # Standardmäßig nächster Strike
716
- format_func=lambda x: f"{currency_symbol}{x:.2f}",
717
- key="payoff_strike_select"
718
- )
719
- # Finde die Zeile im *ungefilterten* DF mit Toleranz
720
  selected_row = calculated_df_unfiltered[np.isclose(calculated_df_unfiltered['strike'], selected_strike_for_chart)].iloc[0]
721
 
722
- # Payoff-Berechnung und Chart
723
- premium_net_chart = selected_row['premiumNetTotal']
724
- strike_chart = selected_row['strike']
725
- cost_basis_chart = st.session_state.cost_basis
726
- shares_chart = st.session_state.shares_owned
727
- min_price_chart = cost_basis_chart * 0.7
728
- max_price_chart = max(current_p, strike_chart) * 1.3
729
- stock_prices_at_exp = np.linspace(min_price_chart, max_price_chart, 150)
730
- profit_loss = [
731
- ((strike_chart - cost_basis_chart) * shares_chart + premium_net_chart) if price_exp > strike_chart
732
- else ((price_exp - cost_basis_chart) * shares_chart + premium_net_chart)
733
- for price_exp in stock_prices_at_exp
734
- ]
735
  payoff_df = pd.DataFrame({'Aktienkurs bei Ablauf': stock_prices_at_exp, f'Gewinn/Verlust ({currency_symbol})': profit_loss})
736
- st.line_chart(payoff_df, x='Aktienkurs bei Ablauf', y=f'Gewinn/Verlust ({currency_symbol})')
737
- st.caption(f"Payoff für Verkauf des {currency_symbol}{strike_chart:.2f} Calls (Netto).")
738
 
739
- # Kennzahlen anzeigen (mit Prüfung auf NaN/Inf für Formatierung)
740
  st.markdown("###### Kennzahlen für ausgewählten Strike:")
741
  kcol1, kcol2, kcol3, kcol4 = st.columns(4)
742
- with kcol1: st.metric("Max. Gewinn (Netto)", f"{currency_symbol}{selected_row['maxProfitAssignedNet']:,.2f}", f"{selected_row['maxProfitPercentNet']:.1f}%" if pd.notna(selected_row['maxProfitPercentNet']) and np.isfinite(selected_row['maxProfitPercentNet']) else "N/A")
743
- with kcol2: st.metric("Prämie (Netto)", f"{currency_symbol}{selected_row['premiumNetTotal']:,.2f}", f"{selected_row['returnIfFlatPercentNet']:.1f}% Stat." if pd.notna(selected_row['returnIfFlatPercentNet']) and np.isfinite(selected_row['returnIfFlatPercentNet']) else "N/A")
744
- with kcol3: st.metric("Breakeven Kurs", f"{currency_symbol}{selected_row['breakeven']:,.2f}" if pd.notna(selected_row['breakeven']) else "-")
745
- with kcol4: st.metric("Risikopuffer", f"{selected_row['downsideProtectionPercentNet']:.1f}%" if pd.notna(selected_row['downsideProtectionPercentNet']) and np.isfinite(selected_row['downsideProtectionPercentNet']) else "N/A")
746
  kcol5, kcol6, kcol7, kcol8 = st.columns(4)
747
- with kcol5: st.metric("Rendite p.a.", f"{selected_row['annualizedReturnPercentNet']:.1f}%" if pd.notna(selected_row['annualizedReturnPercentNet']) and selected_row['annualizedReturnPercentNet'] > 0 else "N/A") # Zeige N/A für 0 oder negativ
748
- with kcol6: st.metric("Return on Risk", f"{selected_row['returnOnRiskPercentNet']:.1f}%" if pd.notna(selected_row['returnOnRiskPercentNet']) and np.isfinite(selected_row['returnOnRiskPercentNet']) else "N/A")
749
- with kcol7: st.metric("Delta", f"{selected_row['delta']:.3f}" if pd.notna(selected_row['delta']) else "-", help="Sensitivität / ca. W'keit ITM.")
750
- with kcol8: st.metric("Theta", f"{selected_row['theta']:.3f}" if pd.notna(selected_row['theta']) else "-", help="Zeitwertverfall / Tag.")
751
-
752
- else: # Falls keine Strikes für Payoff verfügbar
753
- st.info("Keine Strikes zur Visualisierung verfügbar (möglicherweise alle herausgefiltert).")
754
-
755
- else: # Falls keine Kette für Datum gefunden wurde
756
- st.warning(f"Keine Call-Optionen für {st.session_state.ticker_symbol} am {selected_exp_date} gefunden.")
757
 
 
 
 
758
 
759
  # --- Footer ---
760
  st.divider()
761
- 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')}")
 
1
+ # -*- coding: utf-8 -*-
2
  import streamlit as st
3
  import yfinance as yf
4
  import pandas as pd
5
  import numpy as np
6
  import plotly.graph_objects as go
7
  from datetime import datetime, timedelta
8
+ import time
9
 
10
  # --- Konfiguration ---
11
+ st.set_page_config(layout="wide", page_title="Option Yield Pro (Premium Focus)")
12
 
13
  # --- Hilfsfunktionen ---
14
 
15
+ # -- Caching -- (Funktionen bleiben gleich wie vorher)
16
+ @st.cache_resource(ttl=600)
17
  def get_ticker_object(ticker_symbol):
18
+ if not ticker_symbol: return None
 
19
  try:
20
  ticker = yf.Ticker(ticker_symbol)
 
 
21
  if ticker.history(period="1d").empty:
22
+ raise ValueError(f"Keine Daten für {ticker_symbol}")
23
  return ticker
24
  except Exception as e:
25
+ print(f"Ticker Fehler {ticker_symbol}: {e}")
26
+ return None
 
 
 
 
 
 
 
27
 
28
+ @st.cache_data(ttl=300)
29
+ def get_stock_data(_ticker_obj):
30
+ if _ticker_obj is None: return None, "Ungültiges Ticker-Objekt"
31
  try:
32
  info = _ticker_obj.info
 
33
  price = info.get('currentPrice') or info.get('regularMarketPrice') or info.get('bid') or info.get('ask')
34
  prev_close = info.get('previousClose') or info.get('regularMarketPreviousClose')
 
 
35
  if price is None or prev_close is None:
36
  hist = _ticker_obj.history(period="2d")
37
  if not hist.empty:
38
  price = hist['Close'].iloc[-1]
39
+ prev_close = hist['Close'].iloc[-2] if len(hist) > 1 else price
40
  else:
 
41
  fdata = info.get('financialData', {})
42
  price = fdata.get('currentPrice')
43
+ if price is None: return None, "Aktueller Preis nicht ermittelbar."
44
+ if prev_close is None: prev_close = price
 
 
 
 
 
45
  change = price - prev_close
46
+ change_percent = (change / prev_close) * 100 if prev_close != 0 else 0
47
  name = info.get('shortName', _ticker_obj.ticker)
48
+ currency = info.get('currency', '$')
49
  fifty_two_week_low = info.get('fiftyTwoWeekLow')
50
  fifty_two_week_high = info.get('fiftyTwoWeekHigh')
 
51
  stock_data = {
52
+ "name": name, "price": price, "change": change, "change_percent": change_percent,
53
+ "currency": currency, "fifty_two_week_low": fifty_two_week_low, "fifty_two_week_high": fifty_two_week_high,
 
 
 
 
 
54
  }
55
+ return stock_data, None
56
  except Exception as e:
57
+ return None, f"Fehler Aktiendaten ({_ticker_obj.ticker}): {e}"
58
 
59
+ @st.cache_data(ttl=600)
60
  def get_option_dates(_ticker_obj):
 
61
  if _ticker_obj is None: return []
62
  try:
 
63
  opts = _ticker_obj.options
64
  return opts if opts else []
65
  except Exception as e:
66
+ print(f"Fehler Optionsdaten ({_ticker_obj.ticker}): {e}")
67
  return []
68
 
69
+ @st.cache_data(ttl=300)
70
  def get_option_chain(_ticker_obj, expiration_date):
 
71
  if _ticker_obj is None or not expiration_date: return pd.DataFrame()
72
  try:
73
  chain = _ticker_obj.option_chain(expiration_date)
 
74
  calls = chain.calls.reset_index(drop=True).copy()
 
75
  for greek in ['delta', 'gamma', 'theta', 'vega']:
76
+ if greek not in calls.columns: calls[greek] = np.nan
 
77
  return calls
78
  except Exception as e:
79
+ print(f"Fehler/Warnung Optionskette ({_ticker_obj.ticker} / {expiration_date}): {e}")
80
+ return pd.DataFrame()
 
81
 
82
+ @st.cache_data(ttl=1800)
83
  def get_stock_history(_ticker_obj, period='1y', interval='1d'):
 
84
  if _ticker_obj is None: return pd.DataFrame()
85
  try:
86
+ interval_map = {'1d': '1d', '1wk': '1wk', '1mo': '1mo'}
 
87
  hist = _ticker_obj.history(period=period, interval=interval_map.get(interval.lower(), '1d'))
88
  return hist
89
  except Exception as e:
90
+ st.error(f"Fehler Historie ({_ticker_obj.ticker}, {period}, {interval}): {e}")
91
  return pd.DataFrame()
92
 
93
  # -- Berechnungen & Logik --
94
  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):
 
95
  if calls_df.empty: return calls_df
 
96
  df = calls_df.copy()
97
 
98
+ # Prämie
99
+ if custom_premium is not None and custom_premium > 0: df['assumedPremiumPerShare'] = float(custom_premium)
 
100
  elif assumed_premium_price_type == 'mid':
101
+ bid_filled = df['bid'].fillna(0); ask_filled = df['ask'].fillna(df['lastPrice']).fillna(bid_filled)
 
102
  df['assumedPremiumPerShare'] = (bid_filled + ask_filled) / 2
103
+ elif assumed_premium_price_type == 'last': df['assumedPremiumPerShare'] = df['lastPrice'].fillna(0)
104
+ else: df['assumedPremiumPerShare'] = df['bid'].fillna(0)
 
 
 
 
105
  df['assumedPremiumPerShare'] = df['assumedPremiumPerShare'].apply(lambda x: max(0, float(x)))
 
106
  num_contracts = float(shares_owned) / 100.0
107
  df['premiumGrossTotal'] = df['assumedPremiumPerShare'] * float(shares_owned)
108
  total_commission = num_contracts * float(commission_per_contract)
109
+ df['premiumNetTotal'] = df['premiumGrossTotal'] - total_commission
 
 
 
 
 
110
 
111
+ # Basis & Kernberechnungen
112
+ cost_basis = float(cost_basis); shares_owned = float(shares_owned); current_price = float(current_price)
113
  df['costBasisTotal'] = cost_basis * shares_owned
114
  df['strike'] = df['strike'].astype(float)
115
+ df['maxProfitAssignedNet'] = ((df['strike'] - cost_basis) * shares_owned) + df['premiumNetTotal'] # Behalten für optionale Anzeige
116
 
117
+ # Prozentuale Metriken (Netto)
 
 
 
118
  cost_basis_total_val = df['costBasisTotal'].iloc[0] if not df.empty else 0
 
119
  premium_net_per_share = df['premiumNetTotal'] / shares_owned if shares_owned > 0 else pd.Series([0.0] * len(df), index=df.index)
120
+ if not isinstance(premium_net_per_share, pd.Series): # Sicherstellen, dass es Series ist
121
+ premium_net_per_share = pd.Series([premium_net_per_share] * len(df), index=df.index)
122
 
 
123
  if cost_basis_total_val > 0:
124
+ df['maxProfitPercentNet'] = (df['maxProfitAssignedNet'] / cost_basis_total_val) * 100 # Behalten für optionale Anzeige
125
  df['returnIfFlatPercentNet'] = (df['premiumNetTotal'] / cost_basis_total_val) * 100
 
126
  df['downsideProtectionPercentNet'] = (premium_net_per_share / cost_basis) * 100 if cost_basis > 0 else np.inf
 
127
  capital_at_risk = cost_basis - premium_net_per_share
128
+ df['returnOnRiskPercentNet'] = np.where(capital_at_risk > 0, (premium_net_per_share / capital_at_risk) * 100, np.inf)
 
 
 
129
  else:
130
+ for col in ['maxProfitPercentNet', 'returnIfFlatPercentNet', 'downsideProtectionPercentNet', 'returnOnRiskPercentNet']: df[col] = np.inf
 
131
 
132
+ # DTE
133
  try:
134
+ exp_date_obj = datetime.strptime(expiration_date, '%Y-%m-%d').date(); today = datetime.now().date()
135
+ dte = (exp_date_obj - today).days; dte = max(0, dte)
136
+ except ValueError: dte = 0
 
 
 
 
 
 
 
137
  df['DTE'] = dte
138
 
139
+ # Annualisierte Rendite (Netto)
140
+ df['annualizedReturnPercentNet'] = 0.0
 
141
  if 'returnIfFlatPercentNet' in df.columns:
142
+ df['returnIfFlatPercentNet'] = pd.to_numeric(df['returnIfFlatPercentNet'], errors='coerce')
143
  valid_annual_mask = (df['DTE'] > 0) & df['returnIfFlatPercentNet'].notna() & (df['returnIfFlatPercentNet'] > 0)
144
+ safe_dte = df.loc[valid_annual_mask, 'DTE'].replace(0, 1)
145
+ df.loc[valid_annual_mask, 'annualizedReturnPercentNet'] = df.loc[valid_annual_mask, 'returnIfFlatPercentNet'] * (365.0 / safe_dte)
 
 
146
  df['annualizedReturnPercentNet'] = df['annualizedReturnPercentNet'].fillna(0.0)
147
 
148
+ # Breakeven (Netto)
 
 
149
  df['breakeven'] = cost_basis - premium_net_per_share
150
 
151
+ # % OTM
152
  df['percentOTM'] = 0.0
153
  otm_mask = df['strike'] > current_price
 
154
  df.loc[otm_mask, 'percentOTM'] = ((df.loc[otm_mask, 'strike'] - current_price) / current_price) * 100
155
 
156
+ # Griechen & P.OTM
157
  for greek in ['delta', 'gamma', 'theta', 'vega']:
158
+ if greek in df.columns: df[greek] = pd.to_numeric(df[greek], errors='coerce').fillna(np.nan)
159
+ else: df[greek] = np.nan # Sicherstellen, dass Spalte existiert
160
+
161
+ # Wahrscheinlichkeit OTM (vereinfacht: 1 - Delta)
162
+ # Fülle fehlende Deltas mit 0.5 (50/50 Chance) oder 0? 0.5 ist konservativer.
163
+ df['prob_OTM'] = (1.0 - df['delta'].fillna(0.5)).clip(0, 1) # Auf 0-1 begrenzen
164
 
165
  return df
166
 
167
+ # -- Datums-Prüfungen -- (Funktionen bleiben gleich)
168
  def is_monthly_expiration(date_str):
169
+ try: date_obj = datetime.strptime(date_str, '%Y-%m-%d'); return 15 <= date_obj.day <= 21 and date_obj.weekday() == 4
170
+ except ValueError: return False
 
 
 
 
 
 
171
  def is_weekly_expiration(date_str):
172
+ try: date_obj = datetime.strptime(date_str, '%Y-%m-%d'); is_friday = date_obj.weekday() == 4; is_third_friday = 15 <= date_obj.day <= 21; return is_friday and not is_third_friday
173
+ except ValueError: return False
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
+ # --- Plotting Funktion --- (Funktion bleibt gleich)
176
+ def plot_stock_chart(hist_df, ticker_symbol, indicators, sma_periods):
177
+ if hist_df.empty: return go.Figure()
178
  fig = go.Figure()
 
 
179
  required_cols = ['Open', 'High', 'Low', 'Close']
180
  if all(col in hist_df.columns for col in required_cols):
181
+ 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'))
182
+ elif 'Close' in hist_df.columns:
183
+ fig.add_trace(go.Scatter(x=hist_df.index, y=hist_df['Close'], mode='lines', name='Schlusskurs'))
 
 
 
 
 
 
 
 
 
 
184
  if 'SMA' in indicators and 'Close' in hist_df.columns:
185
  for period in sma_periods:
186
+ if period <= 0: continue
187
  sma_col = f'SMA_{period}'
188
+ hist_df[sma_col] = hist_df['Close'].rolling(window=period, min_periods=1).mean()
189
  fig.add_trace(go.Scatter(x=hist_df.index, y=hist_df[sma_col], mode='lines', name=sma_col))
 
190
  if 'Bollinger Bands' in indicators and 'Close' in hist_df.columns:
 
191
  bb_period = next((p for p in sma_periods if p > 0), 20)
192
  sma_col = f'SMA_{bb_period}'
193
+ if sma_col not in hist_df.columns: hist_df[sma_col] = hist_df['Close'].rolling(window=bb_period, min_periods=1).mean()
 
 
 
194
  std_dev = hist_df['Close'].rolling(window=bb_period, min_periods=1).std()
195
+ upper_band = hist_df[sma_col] + (std_dev * 2); lower_band = hist_df[sma_col] - (std_dev * 2)
196
+ if not upper_band.empty: 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)')))
197
+ if not lower_band.empty: 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)'))
198
+ fig.update_layout(title=f'{ticker_symbol} Kursverlauf & Indikatoren', xaxis_title='Datum', yaxis_title='Preis', xaxis_rangeslider_visible=False, legend_title_text='Legende', height=500, margin=dict(l=20, r=20, t=40, b=20))
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  return fig
200
 
201
  # --- Session State Initialisierung ---
202
+ # Neue Standardspalten mit Fokus auf Prämie/Risiko
203
+ base_default_columns_premium = [
204
+ 'Strike', 'Bid', 'Ask', '% OTM', 'Delta', 'prob_OTM', 'Impl. Vol (%)',
205
+ 'Prämie Netto ({currency})', 'Rendite (Statisch %)', 'Rendite p.a. (%)',
206
+ 'Risikopuffer (%)', 'Return on Risk (%)', 'Theta', 'DTE'
207
+ ]
208
+ # Optionale Spalten (inkl. der alten Max-Profit-Spalten)
209
+ other_possible_cols_premium = [
210
+ 'Last', 'Volumen', 'Open Int.', 'Gamma', 'Vega',
211
+ 'Max. Gewinn ({currency})', 'Max. Gewinn (%)', 'Breakeven ({currency})'
212
  ]
 
213
 
214
+ # Initialisiere State
215
  if 'ticker_symbol' not in st.session_state: st.session_state.ticker_symbol = "TSLA"
216
  if 'shares_owned' not in st.session_state: st.session_state.shares_owned = 100
217
  if 'cost_basis' not in st.session_state: st.session_state.cost_basis = 100.00
 
219
  if 'commission' not in st.session_state: st.session_state.commission = 0.65
220
  if 'premium_price_type' not in st.session_state: st.session_state.premium_price_type = 'bid'
221
  if 'custom_premium' not in st.session_state: st.session_state.custom_premium = None
222
+ # Initialisiere mit den *neuen* Premium-fokussierten Defaults
223
  if 'selected_columns' not in st.session_state:
224
+ st.session_state.selected_columns = [col.format(currency='$') for col in base_default_columns_premium]
225
+ if 'min_annual_return_filter' not in st.session_state: st.session_state.min_annual_return_filter = 5.0 # Höherer Standard-Min.-Filter
226
  if 'max_annual_return_filter' not in st.session_state: st.session_state.max_annual_return_filter = 1000.0
227
  if 'min_delta_filter' not in st.session_state: st.session_state.min_delta_filter = 0.0
228
+ if 'max_delta_filter' not in st.session_state: st.session_state.max_delta_filter = 0.35 # Standardmäßig OTM fokussieren
229
+ if 'highlight_min_return' not in st.session_state: st.session_state.highlight_min_return = 25.0 # Höherer Schwellenwert
230
+ if 'highlight_max_delta' not in st.session_state: st.session_state.highlight_max_delta = 0.30 # Delta-Schwelle für Hervorhebung
231
+ if 'show_only_otm' not in st.session_state: st.session_state.show_only_otm = True # Standardmäßig nur OTM zeigen
232
+ # Chart & Datum Filter State (bleiben gleich)
233
  if 'chart_period' not in st.session_state: st.session_state.chart_period = '1y'
234
  if 'chart_interval' not in st.session_state: st.session_state.chart_interval = '1d'
235
  if 'chart_indicators' not in st.session_state: st.session_state.chart_indicators = ['SMA']
236
  if 'chart_sma_periods' not in st.session_state: st.session_state.chart_sma_periods = [50, 200]
237
  if 'date_filter_type' not in st.session_state: st.session_state.date_filter_type = 'Alle'
238
 
239
+
240
  # --- Währung und Spaltennamen VOR der Sidebar bestimmen ---
241
+ current_currency_symbol = '$'
242
  if st.session_state.stock_data and isinstance(st.session_state.stock_data, dict):
243
  current_currency_symbol = st.session_state.stock_data.get('currency', '$')
244
 
245
+ # Verwende die *neuen* Basis-Spaltenlisten
246
+ current_default_columns = [col.format(currency=current_currency_symbol) for col in base_default_columns_premium]
247
+ current_all_possible_columns = current_default_columns + [col.format(currency=current_currency_symbol) for col in other_possible_cols_premium]
248
 
249
+ # Passe Defaults im State an aktuelle Währung an
 
250
  temp_selected_columns = []
251
  for col in st.session_state.selected_columns:
252
+ updated_col = col.replace("($)", f"({current_currency_symbol})").replace("(USD)", f"({current_currency_symbol})").replace("(EUR)", f"({current_currency_symbol})")
253
+ if updated_col in current_all_possible_columns: temp_selected_columns.append(updated_col)
 
 
 
 
 
 
 
 
 
 
254
  st.session_state.selected_columns = temp_selected_columns
255
+ valid_defaults_for_multiselect = [col for col in st.session_state.selected_columns if col in current_all_possible_columns]
256
+ if not valid_defaults_for_multiselect: valid_defaults_for_multiselect = current_default_columns
 
 
 
 
 
257
 
258
 
259
  # --- SIDEBAR ---
260
  with st.sidebar:
261
  st.header("⚙️ Eingaben & Einstellungen")
 
 
262
  st.subheader("Positionsdetails")
263
+ ticker_input = st.text_input("Aktien Ticker", value=st.session_state.ticker_symbol, key="ticker_input_key").upper()
264
+ shares_input = st.number_input("Anzahl Aktien", min_value=100, step=100, value=st.session_state.shares_owned, key="shares_input_key")
265
+ 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.")
266
+
 
 
 
 
 
 
 
267
  st.subheader("Annahmen")
268
+ premium_price_type_input = st.selectbox("Preis für Prämie?", ['bid', 'mid', 'last'], index=['bid', 'mid', 'last'].index(st.session_state.premium_price_type), help="Welcher Preis für Prämienberechnung?", key="premium_select_key")
269
+ custom_premium_input = st.number_input("Manuelle Prämie / Aktie", 0.0, step=0.01, value=st.session_state.custom_premium or 0.0, format="%.2f", help="> 0 überschreibt Auswahl.", key="custom_prem_key")
270
+ commission_input = st.number_input("Provision / Kontrakt", 0.0, step=0.01, value=st.session_state.commission, format="%.2f", key="commission_key")
271
+
 
 
 
 
 
 
 
 
 
 
 
272
  if st.button("Analyse Starten / Werte Übernehmen", key="analyze_sidebar", use_container_width=True):
273
  ticker_changed = (ticker_input != st.session_state.ticker_symbol)
274
+ st.session_state.ticker_symbol = ticker_input; st.session_state.shares_owned = shares_input; st.session_state.cost_basis = cost_basis_input
275
+ 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
 
 
 
 
 
 
 
 
276
  if ticker_changed:
277
  st.session_state.stock_data = None
278
+ # Setze auf neue Premium-Defaults zurück
279
+ st.session_state.selected_columns = [col.format(currency='$') for col in base_default_columns_premium]
280
+ st.rerun()
281
 
282
+ st.divider()
283
+ st.subheader("Tabellen-Anzeige")
284
+ # Neuer Filter: Nur OTM anzeigen
285
+ st.session_state.show_only_otm = st.toggle("Nur OTM-Optionen anzeigen", value=st.session_state.show_only_otm, key="otm_toggle")
286
 
287
+ selected_cols_input = st.multiselect("Angezeigte Spalten:", options=sorted(list(set(current_all_possible_columns))), default=valid_defaults_for_multiselect, key="col_select_key")
288
+ if selected_cols_input != st.session_state.selected_columns: st.session_state.selected_columns = selected_cols_input; st.rerun()
289
 
290
+ st.subheader("Hervorhebung (Tabelle)")
291
+ hc1, hc2 = st.columns(2)
292
+ with hc1: st.session_state.highlight_min_return = st.number_input("Min. Rendite p.a. >", 0.0, step=1.0, value=st.session_state.highlight_min_return, format="%.1f", key="highlight_ret_key")
293
+ with hc2: st.session_state.highlight_max_delta = st.number_input("Max. Delta <", 0.0, 1.0, step=0.01, value=st.session_state.highlight_max_delta, format="%.2f", key="highlight_delta_key")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
 
295
  st.divider()
 
 
296
  st.subheader("Chart-Einstellungen")
297
+ # Chart Inputs bleiben gleich
298
  st.session_state.chart_period = st.selectbox("Zeitraum:", ['1mo', '3mo', '6mo', 'ytd', '1y', '2y', '5y', 'max'], index=4, key="chart_period_key")
299
  st.session_state.chart_interval = st.selectbox("Intervall:", ['1d', '1wk', '1mo'], index=0, key="chart_interval_key")
300
  st.session_state.chart_indicators = st.multiselect("Indikatoren:", ['SMA', 'Bollinger Bands'], default=st.session_state.chart_indicators, key="chart_ind_key")
301
  if 'SMA' in st.session_state.chart_indicators or 'Bollinger Bands' in st.session_state.chart_indicators:
302
+ sma_periods_str = st.text_input("SMA Perioden (kommagetrennt):", value=", ".join(map(str, st.session_state.chart_sma_periods)), key="chart_sma_key")
 
 
 
 
303
  try:
304
+ parsed_periods = [int(p.strip()) for p in sma_periods_str.split(',') if p.strip().isdigit() and int(p.strip()) > 0]
305
+ if parsed_periods: st.session_state.chart_sma_periods = parsed_periods
306
+ else:
307
+ if sma_periods_str.strip(): st.warning("Ungültige SMA Perioden.")
308
+ st.session_state.chart_sma_periods = [50, 200]
309
+ except: st.warning("Fehler bei SMA Perioden."); st.session_state.chart_sma_periods = [50, 200]
 
 
 
 
310
 
311
  # --- HAUPTSEITE ---
312
+ st.title("📈 Option Yield Pro (Premium Focus)")
313
 
314
+ if not st.session_state.ticker_symbol: st.info("👈 Bitte Ticker eingeben & Analysieren."); st.stop()
 
 
 
315
 
316
  # --- Aktiendaten Laden & Anzeigen ---
 
317
  ticker_obj = get_ticker_object(st.session_state.ticker_symbol)
318
  if ticker_obj is None:
319
+ st.error(f"Ticker-Objekt für '{st.session_state.ticker_symbol}' ungültig."); st.session_state.ticker_symbol = ""; st.session_state.stock_data = None; time.sleep(2); st.rerun()
320
+
 
 
 
 
 
 
321
  rerun_needed_after_data_load = False
322
  if st.session_state.stock_data is None:
323
+ with st.spinner(f"Lade Daten für {st.session_state.ticker_symbol}..."): stock_data_result, error_msg = get_stock_data(ticker_obj)
324
+ if error_msg: st.error(f"Fehler Aktiendaten: {error_msg}"); st.stop()
325
+ st.session_state.stock_data = stock_data_result; rerun_needed_after_data_load = True
326
+ if rerun_needed_after_data_load: st.rerun()
327
+
 
 
 
 
 
 
 
 
 
328
  stock_data = st.session_state.stock_data
329
  currency_symbol = stock_data.get('currency', '$')
330
 
 
333
  'strike': 'Strike', 'bid': 'Bid', 'ask': 'Ask', 'lastPrice': 'Last',
334
  'volume': 'Volumen', 'openInterest': 'Open Int.', 'impliedVolatility': 'Impl. Vol (%)',
335
  'premiumNetTotal': f'Prämie Netto ({currency_symbol})',
336
+ 'returnIfFlatPercentNet': 'Rendite (Statisch %)', # Prämienrendite
337
+ 'annualizedReturnPercentNet': 'Rendite p.a. (%)', # Prämie p.a.
338
+ 'maxProfitAssignedNet': f'Max. Gewinn ({currency_symbol})', # Optional
339
+ 'maxProfitPercentNet': 'Max. Gewinn (%)', # Optional
340
+ 'breakeven': f'Breakeven ({currency_symbol})', # Optional
341
  'downsideProtectionPercentNet': 'Risikopuffer (%)',
342
  'returnOnRiskPercentNet': 'Return on Risk (%)',
343
+ 'percentOTM': '% OTM', 'delta': 'Delta', 'theta': 'Theta',
344
+ 'gamma': 'Gamma', 'vega': 'Vega', 'prob_OTM': 'W\'keit OTM (%)', # Neu
345
+ 'DTE': 'DTE'
346
  }
347
 
348
  # --- Anzeige der Aktiendaten ---
349
  st.header(f"📊 {stock_data['name']} ({st.session_state.ticker_symbol})")
350
  price_color = "green" if stock_data['change'] >= 0 else "red"
351
  mcol1, mcol2, mcol3 = st.columns(3)
352
+ with mcol1: st.metric(f"Akt. Kurs ({currency_symbol})", f"{stock_data.get('price', 0):.2f}", f"{stock_data.get('change', 0):.2f} ({stock_data.get('change_percent', 0):.2f}%)", delta_color="normal" if price_color == "green" else "inverse")
353
+ with mcol2: low_52w = stock_data.get('fifty_two_week_low'); high_52w = stock_data.get('fifty_two_week_high') # Hole beide
354
+ if low_52w is not None and high_52w is not None: with mcol2: st.metric("52W Spanne", f"{currency_symbol}{low_52w:.2f} - {high_52w:.2f}")
 
 
 
 
 
355
 
356
  # --- Chart Sektion ---
357
  with st.expander("📈 Detailierter Aktienchart Anzeigen/Verbergen"):
358
  hist_df = get_stock_history(ticker_obj, st.session_state.chart_period, st.session_state.chart_interval)
359
+ 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)
360
+ else: st.warning("Keine Chart-Daten verfügbar.")
 
 
 
361
 
362
+ st.divider(); st.header("🔍 Options-Analyse")
 
363
 
364
  # --- Ablaufdatum Auswahl ---
365
  col_d1, col_d2 = st.columns([1, 3])
366
  with col_d1:
367
+ st.markdown("###### Verfallstyp Filter:")
368
+ date_filter_options = ['Alle', 'Monatlich', 'Wöchentlich']; date_filter_idx = 0
369
+ try: date_filter_idx = date_filter_options.index(st.session_state.date_filter_type)
370
+ except ValueError: pass
371
+ st.session_state.date_filter_type = st.radio("Typ:", date_filter_options, index=date_filter_idx, horizontal=True, label_visibility="collapsed")
 
 
 
 
 
 
372
  with col_d2:
373
  st.markdown("###### Ablaufdatum Wählen:")
374
  option_dates_all = get_option_dates(ticker_obj)
375
+ if not option_dates_all: st.warning("Keine Optionsdaten gefunden."); st.stop()
376
+ if st.session_state.date_filter_type == 'Monatlich': option_dates_filtered = [d for d in option_dates_all if is_monthly_expiration(d)]
377
+ elif st.session_state.date_filter_type == 'Wöchentlich': option_dates_filtered = [d for d in option_dates_all if is_weekly_expiration(d)]
378
+ else: option_dates_filtered = option_dates_all
379
+ if not option_dates_filtered: st.warning(f"Keine '{st.session_state.date_filter_type}' Ablaufdaten."); selected_exp_date = None
380
+ else: selected_exp_date = st.selectbox("Verfügbare Daten:", option_dates_filtered, index=0, label_visibility="collapsed")
 
 
 
 
 
 
 
 
 
 
 
 
 
381
 
382
  # --- Optionskette Laden & Berechnen ---
383
  if selected_exp_date:
384
+ with st.spinner(f"Lade Optionskette für {selected_exp_date}..."): calls_chain = get_option_chain(ticker_obj, selected_exp_date)
 
 
 
385
  if not calls_chain.empty:
386
+ 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)
 
 
 
 
 
 
387
 
388
  # --- Tabellenfilter Widgets ---
389
  st.markdown("###### Tabellenfilter:")
390
  fcol1, fcol2 = st.columns(2)
391
+ 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)
392
+ 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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
 
394
  # --- Filter anwenden ---
395
  filtered_df = calculated_df_unfiltered.copy()
396
+ # OTM Filter (wenn aktiviert)
397
+ if st.session_state.show_only_otm:
398
+ # Behalte auch NaNs bei percentOTM oder wenn percentOTM == 0 (ATM) - strike >= current_price
399
+ filtered_df = filtered_df[(filtered_df['strike'] >= stock_data['price']) | filtered_df['percentOTM'].isna()]
400
+
401
+ # Rendite Filter
402
+ rendite_col = 'annualizedReturnPercentNet'; delta_col = 'delta'
403
  if rendite_col in filtered_df.columns:
404
  numeric_rendite = pd.to_numeric(filtered_df[rendite_col], errors='coerce')
405
+ 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()]
406
+ # Delta Filter
 
 
 
 
407
  if delta_col in filtered_df.columns:
408
  numeric_delta = pd.to_numeric(filtered_df[delta_col], errors='coerce')
409
+ filtered_df = filtered_df[numeric_delta.between(st.session_state.min_delta_filter, st.session_state.max_delta_filter, inclusive='both') | numeric_delta.isna()]
 
 
 
410
 
411
  # --- Tabelle Anzeigen ---
412
  st.markdown(f"###### Call-Optionen für {datetime.strptime(selected_exp_date, '%Y-%m-%d').strftime('%d. %B %Y')} ({len(filtered_df)} angezeigt)")
 
416
  original_cols_keys_to_display = [key for key, value in display_columns.items() if value in selected_display_names]
417
  cols_to_display_final = [col for col in original_cols_keys_to_display if col in filtered_df.columns]
418
 
419
+ if not cols_to_display_final: st.warning("Keine gültigen Spalten zur Anzeige ausgewählt."); display_df_render = pd.DataFrame()
420
+ else: display_df_render = filtered_df[cols_to_display_final].rename(columns=display_columns)
 
 
 
421
 
422
  if not display_df_render.empty:
423
+ # Formatierung
424
  format_dict = { col: '{:.2f}' for col in display_df_render.columns if col.startswith(('Strike', 'Bid', 'Ask', 'Last')) }
425
  format_dict.update({ col: '{:,.2f}' for col in display_df_render.columns if f'({currency_symbol})' in col })
426
+ format_dict.update({ col: '{:.1f}%' for col in display_df_render.columns if '%' in col and 'Impl. Vol' not in col and 'W\'keit OTM' not in col}) # W'keit extra formatieren
427
+ format_dict.update({ 'Impl. Vol (%)': '{:.1%}', 'W\'keit OTM (%)': '{:.1%}', 'Delta': '{:.3f}', 'Theta': '{:.3f}', 'Gamma': '{:.4f}', 'Vega': '{:.3f}', 'Volumen': '{:,}', 'Open Int.': '{:,}', 'DTE': '{:,d}' })
 
 
 
 
428
  final_format_dict = {k: v for k, v in format_dict.items() if k in display_df_render.columns}
429
 
430
  # Styler
431
  styler = display_df_render.style.format(final_format_dict, na_rep="-")
432
 
433
+ # Highlight basierend auf Rendite UND Delta
434
+ highlight_ret_col = 'Rendite p.a. (%)'; highlight_delta_col = 'Delta'
435
+ min_ret = st.session_state.highlight_min_return; max_delta = st.session_state.highlight_max_delta
436
+ if highlight_ret_col in display_df_render.columns and highlight_delta_col in display_df_render.columns:
 
437
  try:
438
+ def highlight_premium_sweetspot(row):
439
+ ret = pd.to_numeric(row[highlight_ret_col], errors='coerce')
440
+ delta = pd.to_numeric(row[highlight_delta_col], errors='coerce')
441
+ if pd.notna(ret) and pd.notna(delta) and ret >= min_ret and delta <= max_delta:
442
+ return ['background-color: #e8f5e9'] * len(row) # Helles Grün
443
+ return [''] * len(row)
444
+ styler.apply(highlight_premium_sweetspot, axis=1)
445
+ except Exception as e: print(f"Styling Error (Highlight Sweetspot): {e}")
446
+
447
+ # Farbverläufe etc. (wie vorher)
448
+ gradient_cols = {'Rendite p.a. (%)': 'Greens', 'Return on Risk (%)': 'Greens', 'Impl. Vol (%)': 'RdYlGn', 'Delta': 'Blues', 'Theta': 'Reds_r', 'W\'keit OTM (%)': 'Greens'}
449
  for col, cmap in gradient_cols.items():
450
  if col in display_df_render.columns:
451
+ try: styler.background_gradient(cmap=cmap, subset=[col], axis=0)
452
  except Exception as e: print(f"Styling Error (Gradient {col}): {e}")
 
 
453
  bar_cols = ["Volumen", "Open Int.", "% OTM"]
454
  for col in bar_cols:
455
  if col in display_df_render.columns:
456
+ try: styler.bar(subset=[col], color='lightblue', align='zero', vmin=0)
457
+ except Exception as e: print(f"Styling Error (Bar {col}): {e}")
458
 
 
459
  st.dataframe(styler, use_container_width=True, height=500)
460
+ st.caption("🖱️ Spalten sortieren. Grüner Hintergrund: Erfüllt Hervorhebungs-Kriterien (Rendite & Delta).")
461
 
462
  # --- Payoff Visualisierung ---
463
+ st.divider(); st.subheader("💡 Payoff & Kennzahlen (Auswahl)")
 
464
  available_strikes = sorted(calculated_df_unfiltered['strike'].round(2).unique().tolist())
465
  if available_strikes:
466
+ current_p = stock_data['price']; default_strike_idx = np.abs(np.array(available_strikes) - current_p).argmin()
467
+ selected_strike_for_chart = st.selectbox("Strike zur Visualisierung:", available_strikes, index=default_strike_idx, format_func=lambda x: f"{currency_symbol}{x:.2f}", key="payoff_strike_select")
 
 
 
 
 
 
 
 
 
468
  selected_row = calculated_df_unfiltered[np.isclose(calculated_df_unfiltered['strike'], selected_strike_for_chart)].iloc[0]
469
 
470
+ # Payoff Chart (wie vorher)
471
+ 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
472
+ 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)
473
+ 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]
 
 
 
 
 
 
 
 
 
474
  payoff_df = pd.DataFrame({'Aktienkurs bei Ablauf': stock_prices_at_exp, f'Gewinn/Verlust ({currency_symbol})': profit_loss})
475
+ 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).")
 
476
 
477
+ # Kennzahlen (Fokus auf Prämie & Risiko OTM)
478
  st.markdown("###### Kennzahlen für ausgewählten Strike:")
479
  kcol1, kcol2, kcol3, kcol4 = st.columns(4)
480
+ with kcol1: st.metric("Prämie (Netto)", f"{currency_symbol}{selected_row['premiumNetTotal']:,.2f}", f"{selected_row['returnIfFlatPercentNet']:.1f}% Stat." if pd.notna(selected_row['returnIfFlatPercentNet']) and np.isfinite(selected_row['returnIfFlatPercentNet']) else "N/A")
481
+ with kcol2: st.metric("Rendite p.a.", f"{selected_row['annualizedReturnPercentNet']:.1f}%" if pd.notna(selected_row['annualizedReturnPercentNet']) and selected_row['annualizedReturnPercentNet'] > 0 else "N/A")
482
+ with kcol3: st.metric("Risikopuffer", f"{selected_row['downsideProtectionPercentNet']:.1f}%" if pd.notna(selected_row['downsideProtectionPercentNet']) and np.isfinite(selected_row['downsideProtectionPercentNet']) else "N/A")
483
+ with kcol4: st.metric("W'keit OTM", f"{selected_row['prob_OTM']:.1%}" if pd.notna(selected_row['prob_OTM']) else "-", help="Ca. W'keit, dass die Option wertlos verfällt (1 - Delta).")
484
  kcol5, kcol6, kcol7, kcol8 = st.columns(4)
485
+ with kcol5: st.metric("Return on Risk", f"{selected_row['returnOnRiskPercentNet']:.1f}%" if pd.notna(selected_row['returnOnRiskPercentNet']) and np.isfinite(selected_row['returnOnRiskPercentNet']) else "N/A")
486
+ with kcol6: st.metric("Delta", f"{selected_row['delta']:.3f}" if pd.notna(selected_row['delta']) else "-")
487
+ with kcol7: st.metric("Theta", f"{selected_row['theta']:.3f}" if pd.notna(selected_row['theta']) else "-")
488
+ with kcol8: st.metric("DTE", f"{selected_row['DTE']:.0f}" if pd.notna(selected_row['DTE']) else "-")
 
 
 
 
 
 
489
 
490
+ else: st.info("Keine Strikes zur Visualisierung.")
491
+ else: st.info("Keine Optionen entsprechen den aktuellen Filterkriterien.")
492
+ else: st.warning(f"Keine Call-Optionen für {st.session_state.ticker_symbol} am {selected_exp_date} gefunden.")
493
 
494
  # --- Footer ---
495
  st.divider()
496
+ st.caption(f"Haftungsausschluss: Nur zu Informationszwecken...") # Gekürzt