Update app.py
Browse files
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
|
| 8 |
|
| 9 |
# --- Konfiguration ---
|
| 10 |
-
st.set_page_config(layout="wide", page_title="Option Yield Pro (
|
| 11 |
|
| 12 |
# --- Hilfsfunktionen ---
|
| 13 |
|
| 14 |
-
# -- Caching --
|
| 15 |
-
@st.cache_resource(ttl=600)
|
| 16 |
def get_ticker_object(ticker_symbol):
|
| 17 |
-
|
| 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}
|
| 25 |
return ticker
|
| 26 |
except Exception as e:
|
| 27 |
-
|
| 28 |
-
|
| 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
|
| 49 |
else:
|
| 50 |
-
# Fallback: Versuche 'financialData'
|
| 51 |
fdata = info.get('financialData', {})
|
| 52 |
price = fdata.get('currentPrice')
|
| 53 |
-
if price is None:
|
| 54 |
-
|
| 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
|
| 62 |
name = info.get('shortName', _ticker_obj.ticker)
|
| 63 |
-
currency = info.get('currency', '$')
|
| 64 |
fifty_two_week_low = info.get('fiftyTwoWeekLow')
|
| 65 |
fifty_two_week_high = info.get('fiftyTwoWeekHigh')
|
| 66 |
-
|
| 67 |
stock_data = {
|
| 68 |
-
"name": name,
|
| 69 |
-
"
|
| 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
|
| 77 |
except Exception as e:
|
| 78 |
-
return None, f"Fehler
|
| 79 |
|
| 80 |
-
@st.cache_data(ttl=600)
|
| 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
|
| 90 |
return []
|
| 91 |
|
| 92 |
-
@st.cache_data(ttl=300)
|
| 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 |
-
|
| 107 |
-
|
| 108 |
-
return pd.DataFrame() # Return empty dataframe on error
|
| 109 |
|
| 110 |
-
@st.cache_data(ttl=1800)
|
| 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 |
-
|
| 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
|
| 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 |
-
#
|
| 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 |
-
|
| 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
|
| 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 |
-
#
|
| 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 |
-
#
|
| 183 |
try:
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 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 |
-
#
|
| 197 |
-
df['annualizedReturnPercentNet'] = 0.0
|
| 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')
|
| 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)
|
| 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 |
-
#
|
| 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 |
-
#
|
| 220 |
for greek in ['delta', 'gamma', 'theta', 'vega']:
|
| 221 |
-
if greek in df.columns:
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
return df
|
| 225 |
|
|
|
|
| 226 |
def is_monthly_expiration(date_str):
|
| 227 |
-
|
| 228 |
-
|
| 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 |
-
|
| 237 |
-
|
| 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 |
-
|
| 258 |
-
|
| 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
|
| 272 |
sma_col = f'SMA_{period}'
|
| 273 |
-
hist_df[sma_col] = hist_df['Close'].rolling(window=period, min_periods=1).mean()
|
| 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 |
-
|
| 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 |
-
|
| 287 |
-
|
| 288 |
-
|
| 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 |
-
#
|
| 306 |
-
|
| 307 |
-
'Strike', 'Bid', 'Ask', '
|
| 308 |
-
'
|
| 309 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
]
|
| 311 |
-
other_possible_cols = ['Volumen', 'Open Int.', 'Gamma', 'Vega']
|
| 312 |
|
| 313 |
-
# Initialisiere State
|
| 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
|
| 323 |
-
if 'min_annual_return_filter' not in st.session_state: st.session_state.min_annual_return_filter =
|
| 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 =
|
| 327 |
-
if 'highlight_min_return' not in st.session_state: st.session_state.highlight_min_return =
|
|
|
|
|
|
|
|
|
|
| 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 = '$'
|
| 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 |
-
|
| 340 |
-
|
|
|
|
| 341 |
|
| 342 |
-
#
|
| 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 |
-
|
| 347 |
-
updated_col
|
| 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 |
-
|
| 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 |
-
|
| 376 |
-
|
| 377 |
-
|
| 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 |
-
|
| 388 |
-
|
| 389 |
-
|
| 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 |
-
|
| 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 |
-
#
|
| 416 |
-
|
| 417 |
-
|
| 418 |
|
| 419 |
-
|
|
|
|
|
|
|
|
|
|
| 420 |
|
| 421 |
-
st.
|
|
|
|
| 422 |
|
| 423 |
-
|
| 424 |
-
st.
|
| 425 |
-
|
| 426 |
-
|
| 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]
|
| 459 |
-
if parsed_periods:
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 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 (
|
| 471 |
|
| 472 |
-
|
| 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"
|
| 482 |
-
|
| 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 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 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 |
-
'
|
| 521 |
-
'gamma': 'Gamma', 'vega': 'Vega','
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 553 |
-
date_filter_options = ['Alle', 'Monatlich', 'Wöchentlich']
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 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 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
#
|
| 626 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
#
|
| 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 |
-
#
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
if
|
| 675 |
-
min_ret_highlight = st.session_state.highlight_min_return
|
| 676 |
try:
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 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)
|
| 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 |
-
|
| 698 |
-
|
| 699 |
|
| 700 |
-
# Tabelle anzeigen
|
| 701 |
st.dataframe(styler, use_container_width=True, height=500)
|
| 702 |
-
st.caption("🖱️
|
| 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 |
-
|
| 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
|
| 723 |
-
premium_net_chart = selected_row['premiumNetTotal']
|
| 724 |
-
strike_chart =
|
| 725 |
-
|
| 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
|
| 740 |
st.markdown("###### Kennzahlen für ausgewählten Strike:")
|
| 741 |
kcol1, kcol2, kcol3, kcol4 = st.columns(4)
|
| 742 |
-
with kcol1: st.metric("
|
| 743 |
-
with kcol2: st.metric("
|
| 744 |
-
with kcol3: st.metric("
|
| 745 |
-
with kcol4: st.metric("
|
| 746 |
kcol5, kcol6, kcol7, kcol8 = st.columns(4)
|
| 747 |
-
with kcol5: st.metric("
|
| 748 |
-
with kcol6: st.metric("
|
| 749 |
-
with kcol7: st.metric("
|
| 750 |
-
with kcol8: st.metric("
|
| 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.
|
|
|
|
| 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
|