Scoring-Modell (PUT L1, PUT L2, Covered Call)
Das Score-Modell ist das Herz des Options-Screeners. Aus jedem Strike einer gefilterten Optionskette berechnet der Backend-Service einen numerischen Score; die Strikes mit der höchsten Kennzahl werden weiter oben einsortiert.
Identische Formel wie in der Desktop-App – Web ist 1:1 portiert. Bei Differenzen gewinnt Desktop. Quellen:
- Web:
wheeltrading-backend/services/option_scoring.py,
services/medal_ranking_service.py, services/option_recommendation.py
- Desktop:
app/utils/option_utils.py
(calculate_put_score, calculate_call_score, calculate_atr_distance_put, calculate_atr_distance_call)
Pipeline auf einen Blick
Optionskette (Provider)
|
v
[1] Filter: DTE, |Δ|, OI, Volume, Moneyness, ITM-Sperre
|
v
[2] Risk-Flags: earnings, ex_div, iv_low, iv_extreme, low_liquidity
|
v
[3] SCORE → calculate_put_score / calculate_call_score
|
v
[4] KLASSIFIKATION → recommend_put / recommend_call
(L1 / L2 / CC / 🚫 / ⚠️ + Warn-Icons)
|
v
[5] Sortierung: Score DESC → Prämie p.a. DESC → DTE ASC → Strike
|
v
Kandidaten (nach Kennzahl)Die fuenf Stufen sind klar getrennt:
| Stufe | Frage | Ergebnis |
|---|---|---|
| 1 | Ist der Strike ueberhaupt einen Blick wert? | gefilterte Liste |
| 2 | Welche Risiken haengen daran? | Flag-Set |
| 3 | Wie attraktiv ist der Strike numerisch? | Score (0–110 / 0–100) |
| 4 | Erfuellt der Strike die L1/L2/CC-Schwellen? | Label L1/L2/CC/🚫 |
| 5 | Wie werden die Kandidaten sortiert? | Reihenfolge nach Kennzahl |
Score und Klassifikation sind getrennte Welten: ein Strike kann hoch scoren und trotzdem 🚫 bekommen (z. B. Strike unter Kaufpreis bei CC), oder umgekehrt eine schwache Klassifikation haben aber durch Liquidität trotzdem weiter oben gelistet werden, wenn keine stärkere Übereinstimmung vorliegt.
Eingaben aus dem SymbolContext
Vor der Bewertung laedt load_symbol_context pro Symbol:
| Feld | Quelle | Verwendet für |
|---|---|---|
has_stock | offene Long-Stock-Position | PUT L1 vs L2, CC erlaubt? |
purchase_price | Avg. Einstand der Long-Position | CC ATR-Distanz + Capital-Gain |
atr_52 | EOD-Daten / DB | ATR-Distanz |
last_close | EOD | Fallback-Underlying |
vix_value | tagesaktuell | Regel-16-Bonus, VIX-Band |
next_earnings_date | Earnings-Calendar | Risk-Flag + Warn-Icon |
next_ex_div_date | Dividenden-Calendar | Risk-Flag + Warn-Icon |
Der Underlying-Preis wird aus dem Snapshot des Provider-Quotes genommen und nur dann durch last_close ersetzt, wenn der Live-Preis fehlt.
PUT-Score (max 110 Punkte)
score = premium_score # 0..40
+ atr_score # 0..25
+ oi_score # 0..20
+ iv_score # 0..15
+ regel_16_bonus # 0..10 (nur L1/L2)1. Premium-Score (0–40, 40 % Gewicht)
premium_score = min(40, premium_pct / target_return * 40)premium_pct = mid / strike * 100 (Einperioden-Prämie in %, NICHT annualisiert).
target_return haengt von der Strategie ab:
| Strategie | has_stock | target_return | Bedeutung |
|---|---|---|---|
| L1 | nein | 1.5 % | Initial-Eintritt, hoehere Renditeforderung |
| L2 | ja | 0.8 % | Wheel-Phase 2: Cost-Basis-Reduktion |
fallback | n/a | 0.6 % | Sonderfall (sehr schwacher Markt) |
Auswirkung an einem Beispiel (mid 1.10, strike 100 → premium_pct 1.1 %):
| Strategie | Score | Maximum |
|---|---|---|
| L1 | 1.1 / 1.5 * 40 = 29.3 | 40 |
| L2 | 1.1 / 0.8 * 40 = 55 → 40 | 40 |
Genau dadurch werden moderate Prämien für Bestandshalter (L2) attraktiver bewertet als für Neueinsteiger (L1).
2. ATR-Score (0–25, 23 % Gewicht)
atr_score = max(0, min(25, 25 - |2.5 - atr_distance| * 5))
atr_distance_put = (current_price - strike) / atr52- Optimum bei 2.5 ATR unter dem aktuellen Kurs (full points).
- Pro 1 ATR Abweichung: −5 Punkte. Bei 0 oder 5 ATR: 0 Punkte.
Warum `current_price` und nicht cost_basis? Beim Put-Verkauf besitzen Sie die Aktien noch nicht – der Strike ist der zukuenftige Andienungspreis. Die Distanz misst die Sicherheits-Cushion gegen den heutigen Kurs.
| atr_distance | Score |
|---|---|
| 0.0 | 12.5 |
| 1.5 | 20.0 |
| 2.5 | 25.0 |
| 3.5 | 20.0 |
| 5.0 | 12.5 |
| 7.5 | 0.0 |
3. OI-Score (0–20, 18 % Gewicht)
oi_score = min(20, open_interest / 100 * 20)Lineare Skalierung bis 100 Kontrakte. Reine Liquiditäts-Komponente – kein Volumen-Gewicht (Volume nur als Filter, nicht als Score).
4. IV-Score (0–15, 14 % Gewicht)
iv_score = min(15, iv * 100 / 50 * 15) # iv als Dezimal (0..1)Hoehere IV → groessere Prämie → wir wollen sie. Saturation bei IV ≥ 50 %.
5. Regel-16-Bonus (0–10, max 9 % Gewicht)
Bonus wenn die Aktie ruhiger laeuft als der VIX impliziert:
expected_daily_move = vix_value / 16 # %
actual_daily_move = atr52 / 52 / stock_price * 100
if actual < expected:
bonus = min(10, (expected / actual - 1) * 20)Greift nur für L1 und L2, nicht für Fallback. Macht den Unterschied zwischen einem 70-Punkte- und einem 80-Punkte-Strike, wenn alles andere gleich ist.
Einordnung der PUT-Sub-Scores
| Komponente | max Punkte | Anteil am Maximum |
|---|---|---|
| Premium % | 40 | 36.4 % |
| ATR-Distanz | 25 | 22.7 % |
| Open-Interest | 20 | 18.2 % |
| IV | 15 | 13.6 % |
| Regel-16-Bonus | 10 | 9.1 % |
| Summe | 110 | 100 % |
Covered-Call-Score (max 100 Punkte)
score = premium_score # 0..30
+ capital_gain_score # 0..30
+ atr_score # 0..20
+ oi_score # 0..10
+ iv_score # 0..101. Premium-Score (0–30)
premium_score = min(30, premium_pct / 0.6 * 30)Basis 0.6 % Mid/Strike – ueber 0.6 % volle Punktzahl.
2. Capital-Gain-Score (0–30) [neu mit cost_basis]
reference = cost_basis if (has_stock and cost_basis > 0) else current_price
capital_gain_pct = max(0, (strike - reference) / reference * 100)
capital_gain_score = min(30, capital_gain_pct / 5 * 30)Saturation bei 5 % Aufschlag ueber dem Einstand. Genau hier sitzt der Kern der CC-Logik:
Sie sind Aktien-Owner und willst im Falle der Andienung mit Gewinn verkaufen. Reference ist daher Ihr Einstand, nicht der Tagespreis.
3. ATR-Score (0–20) [neu mit cost_basis]
atr_distance_call = (strike - reference) / atr52
atr_score = max(0, min(20, 20 - |2.5 - atr_distance| * 4))Selbe Glocke wie beim Put, aber mit −4 statt −5 pro ATR und Cap auf 20.
4. OI-Score (0–10) und 5. IV-Score (0–10)
oi_score = min(10, open_interest / 100 * 10)
iv_score = min(10, iv * 100 / 50 * 10)Einordnung der CC-Sub-Scores
| Komponente | max Punkte | Anteil am Maximum |
|---|---|---|
| Premium % | 30 | 30 % |
| Capital-Gain | 30 | 30 % |
| ATR-Distanz | 20 | 20 % |
| Open-Interest | 10 | 10 % |
| IV | 10 | 10 % |
| Summe | 100 | 100 % |
CMG-Beispiel
cost_basis 34.00, Spot 36.20, ATR52 1.31:
| Strike | atr vs Spot | atr vs cost_basis | capital_gain | Score-Bewegung |
|---|---|---|---|---|
| 36 | −0.15 ITM | +1.53 | 5.9 % | klar besser |
| 38 | +1.37 | +3.05 | 11.8 % | sehr stark |
Mit Spot-Reference waere Strike 36 gar nicht erst aufgetaucht (ITM). Mit cost_basis-Reference ist er ein attraktiver CC.
Klassifikation (Stufe 4): von Score zu Label
Der Score sortiert. Die Klassifikation labelt den Strike algorithmisch für den User (keine Anlageberatung, keine persönliche Empfehlung). Beide Funktionen sind in services/option_recommendation.py.
VIX-Band (Mindest-ATR)
VIX < 15 → min_atr 1.0 👌 niedrig - stabiler Markt
VIX < 20 → min_atr 1.5 🟢 normal
VIX < 30 → min_atr 2.0 🟡 erhoeht - strikteres Algorithmus-Profil
VIX ≥ 30 → min_atr 2.5 🔴 EXTREM - nur sichere StrikesPUT-Klassifikation
annual_return = premium_pct * 365 / dte| Bedingung | Label |
|---|---|
not has_stock und annual ≥ 12 % und atr ≥ min_atr | L1 ✅ |
not has_stock und annual ≥ 12 % und atr ≥ min_atr−0.5 | L1 ⚠️ |
has_stock und annual ≥ 6 % und atr ≥ min_atr | L2 ✅ |
has_stock und annual ≥ 6 % und atr ≥ min_atr−0.5 | L2 ⚠️ |
| sonst (Prämie/ATR zu niedrig) | - |
Risk-Icon kommt zusaetzlich aus _risk_band:
risk_score = (vix / 20) * (1 / max(atr_distance, 0.1))
< 0.6 → Niedrig ✅
< 1.2 → Mittel ⚠️
≥ 1.2 → Hoch 🔴Covered-Call-Klassifikation
Vorab-Hard-Stops (geben sofort 🚫):
- Strike ≤ aktueller Kurs (sofort ITM)
- Strike ≤ Kaufpreis (Verlust bei Assignment)
- Kein Aktien-Bestand (
has_stock=False)
Danach Min-Renditeschwelle:
sc_min_roc = 1 % monatlich → annualisiert ~12 % p.a.
min_strike = purchase_price * (1 + sc_min_roc)
sc_atr_min = 1.5 ATR| Bedingung | Label |
|---|---|
strike < min_strike | ⚠️ |
atr_distance ≥ sc_atr_min und annual ≥ 12 % und atr ≥ min_atr | CC ✅ |
| Wie oben, aber atr ≥ min_atr − 0.5 | CC ⚠️ |
| Ex-Date < Expiry + 7 d | ⚠️ |
| sonst | - |
Warn-Icons (an L1 / L2 / CC angehaengt)
| Icon | Bedeutung |
|---|---|
📅 | Earnings ≤ 7 Tage vor Expiry |
💰 | Ex-Dividend zwischen heute und Expiry |
Sortierung der Kandidaten
ORDER BY score DESC,
premium_pct DESC,
dte ASC,
strike ASCTie-Breaker also: Score > Prämie p.a. > kurze Laufzeit > kleiner Strike. Alle Kandidaten müssen die Filter UND die Risk-Flag-Excludes ueberlebt haben.
Beispielrechnung (CMG, Bestandshalter, VIX 18)
Annahmen: cost_basis 34.00, Spot 36.20, ATR52 1.31, IV 0.32, OI 800, VIX 18, DTE 40.
CC Strike 36, mid 0.55:
premium_pct = 0.55 / 36 * 100 = 1.53
capital_gain_pct = (36 - 34) / 34 * 100 = 5.88 → satur. → 30
atr_distance_call = (36 - 34) / 1.31 = 1.53
→ atr_score = 20 - |2.5 - 1.53| * 4 = 16.1
premium_score = min(30, 1.53 / 0.6 * 30) = 30
oi_score = min(10, 800 / 100 * 10) = 10
iv_score = min(10, 0.32 * 100 / 50 * 10) = 6.4
score = 30 + 30 + 16.1 + 10 + 6.4 = 92.5Klassifikations-Check:
annual = 1.53 * 365 / 40 = 13.96 % p.a.
min_strike = 34 * 1.01 = 34.34 → 36 ≥ 34.34 ✅
atr_distance 1.53 ≥ 1.5 (sc_atr_min) ✅
atr_distance 1.53 ≥ 1.5 (VIX-Band, normal) → grenzwertig
risk_score = 18/20 * 1/1.53 = 0.59 → Niedrig ✅
→ Label: "CC ✅"Erweiterungspunkte (für Devs)
- Zusaetzliche Sub-Scores ueber
option_scoring.calculate_*_score
ergaenzen – das maximale Score-Total muss in den Tests dokumentiert werden, damit Sortierung und Tooltips konsistent bleiben.
- VIX-Baender und Schwellen (
12 % p.a.L1,6 % p.a.L2,
sc_min_roc 1 %) in option_recommendation.py zentralisieren – aenderbar pro Plan/User-Override.
- Saturationspunkte pro Sub-Score sind bewusst absolut, nicht
prozentual: 0.6 %/30, 1.5 %/40, 5 %/30 etc. Bei Anpassungen Desktop- Pendant in app/utils/option_utils.py zuerst aendern.
Verwandte Themen
- "Kennzahlen-Auswertung & Risk-Flags" – wie der Score ausgewertet wird
- "Volatilitäts-Regime (VIX)" – die VIX-Stufen im Detail
- "Options-Screening" – die Filter-Stufe vor dem Scoring
- "Sicherheits-Gates" – was zwischen Klassifikation und Order-Send liegt