File size: 44,717 Bytes
acccd27
0c530c0
 
 
 
b659a9a
 
cd63333
0c530c0
 
acccd27
0c530c0
 
 
cd63333
 
7c4a8b5
cd63333
 
7c4a8b5
 
cd63333
 
7c4a8b5
cd63333
7c4a8b5
 
cd63333
 
 
 
 
 
 
 
 
7c4a8b5
 
 
cd63333
7c4a8b5
 
cd63333
 
7c4a8b5
 
 
 
cd63333
7c4a8b5
cd63333
7c4a8b5
 
cd63333
 
 
 
 
 
 
7c4a8b5
cd63333
7c4a8b5
cd63333
7c4a8b5
 
cd63333
7c4a8b5
cd63333
 
 
 
 
 
 
7c4a8b5
cd63333
7c4a8b5
cd63333
7c4a8b5
cd63333
7c4a8b5
cd63333
7c4a8b5
 
cd63333
7c4a8b5
 
 
cd63333
7c4a8b5
 
cd63333
7c4a8b5
cd63333
7c4a8b5
 
 
cd63333
 
 
 
 
7c4a8b5
cd63333
7c4a8b5
cd63333
 
7c4a8b5
 
cd63333
 
 
7c4a8b5
cd63333
7c4a8b5
cd63333
7c4a8b5
 
cd63333
 
7c4a8b5
 
 
cd63333
7c4a8b5
 
 
 
cd63333
7c4a8b5
cd63333
7c4a8b5
 
cd63333
 
 
7c4a8b5
cd63333
 
7c4a8b5
cd63333
 
 
 
 
 
7c4a8b5
cd63333
7c4a8b5
 
 
cd63333
 
 
 
 
 
7c4a8b5
 
 
cd63333
 
acccd27
7c4a8b5
cd63333
7c4a8b5
cd63333
7c4a8b5
cd63333
 
acccd27
7c4a8b5
cd63333
 
7c4a8b5
acccd27
7c4a8b5
cd63333
7c4a8b5
cd63333
7c4a8b5
cd63333
 
 
 
7c4a8b5
cd63333
 
7c4a8b5
cd63333
7c4a8b5
cd63333
 
 
 
 
 
 
 
 
 
7c4a8b5
 
cd63333
 
 
7c4a8b5
cd63333
7c4a8b5
cd63333
 
 
 
 
 
7c4a8b5
 
cd63333
 
7c4a8b5
 
cd63333
7c4a8b5
 
cd63333
7c4a8b5
 
cd63333
7c4a8b5
cd63333
 
 
 
acccd27
 
cd63333
 
7c4a8b5
 
 
cd63333
7c4a8b5
cd63333
 
 
 
 
 
 
7c4a8b5
cd63333
 
 
 
 
 
 
 
 
 
 
 
acccd27
cd63333
 
 
7c4a8b5
cd63333
 
7c4a8b5
 
cd63333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c4a8b5
 
cd63333
7c4a8b5
cd63333
7c4a8b5
cd63333
7c4a8b5
cd63333
 
7c4a8b5
cd63333
 
 
 
7c4a8b5
cd63333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c4a8b5
0c530c0
cd63333
0c530c0
acccd27
 
 
 
 
 
 
 
 
 
b659a9a
a5ee1fd
acccd27
a5ee1fd
0c530c0
a5ee1fd
0c530c0
b659a9a
0c530c0
b659a9a
acccd27
a5ee1fd
acccd27
 
a5ee1fd
b659a9a
acccd27
 
 
 
 
b659a9a
 
 
 
 
 
acccd27
a5ee1fd
cd63333
a5ee1fd
 
 
acccd27
 
 
a5ee1fd
cd63333
7c4a8b5
cd63333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c4a8b5
cd63333
 
 
 
 
 
 
 
a5ee1fd
 
b659a9a
 
 
acccd27
cd63333
 
 
 
 
 
 
 
 
 
 
 
 
 
b659a9a
cd63333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c4a8b5
cd63333
 
 
 
 
 
 
 
 
 
 
a5ee1fd
 
cd63333
acccd27
a5ee1fd
cd63333
a5ee1fd
cd63333
a5ee1fd
cd63333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acccd27
 
cd63333
 
 
 
 
 
 
 
 
 
 
 
b659a9a
7c4a8b5
cd63333
 
b659a9a
 
7c4a8b5
b659a9a
 
cd63333
 
 
 
 
b659a9a
cd63333
 
 
 
 
 
 
 
 
 
b659a9a
 
acccd27
b659a9a
cd63333
 
 
 
b659a9a
 
cd63333
b659a9a
 
cd63333
 
 
 
 
acccd27
cd63333
7c4a8b5
b659a9a
cd63333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b659a9a
a5ee1fd
 
 
 
 
 
7c4a8b5
acccd27
 
 
 
 
7c4a8b5
 
acccd27
 
 
a5ee1fd
 
cd63333
a5ee1fd
cd63333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a5ee1fd
 
 
 
cd63333
 
 
 
 
a5ee1fd
acccd27
a5ee1fd
 
 
 
acccd27
 
 
 
 
a5ee1fd
 
 
acccd27
cd63333
acccd27
 
cd63333
acccd27
 
a5ee1fd
 
 
cd63333
 
 
 
a5ee1fd
cd63333
 
 
 
 
 
 
a5ee1fd
 
 
 
cd63333
 
 
 
 
 
a5ee1fd
 
 
cd63333
acccd27
cd63333
 
 
 
acccd27
 
 
7c4a8b5
 
acccd27
 
7c4a8b5
 
acccd27
bf281d6
7c4a8b5
 
bf281d6
7c4a8b5
 
 
 
a5ee1fd
cd63333
 
 
 
 
 
 
 
 
 
a5ee1fd
 
acccd27
7c4a8b5
 
cd63333
acccd27
7c4a8b5
a5ee1fd
7c4a8b5
a5ee1fd
 
acccd27
 
 
 
7c4a8b5
acccd27
cd63333
 
 
 
 
acccd27
cd63333
acccd27
 
 
cd63333
acccd27
a5ee1fd
 
acccd27
a5ee1fd
 
 
 
acccd27
 
a5ee1fd
 
acccd27
a5ee1fd
 
acccd27
a5ee1fd
 
cd63333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acccd27
 
0c530c0
 
7c4a8b5
cd63333
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
# -*- 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 --
@st.cache_resource(ttl=600) # 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

@st.cache_data(ttl=300) # 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}"

@st.cache_data(ttl=600) # 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 []

@st.cache_data(ttl=300) # 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

@st.cache_data(ttl=1800) # 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')}")