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:

StufeFrageErgebnis
1Ist der Strike ueberhaupt einen Blick wert?gefilterte Liste
2Welche Risiken haengen daran?Flag-Set
3Wie attraktiv ist der Strike numerisch?Score (0–110 / 0–100)
4Erfuellt der Strike die L1/L2/CC-Schwellen?Label L1/L2/CC/🚫
5Wie 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:

FeldQuelleVerwendet für
has_stockoffene Long-Stock-PositionPUT L1 vs L2, CC erlaubt?
purchase_priceAvg. Einstand der Long-PositionCC ATR-Distanz + Capital-Gain
atr_52EOD-Daten / DBATR-Distanz
last_closeEODFallback-Underlying
vix_valuetagesaktuellRegel-16-Bonus, VIX-Band
next_earnings_dateEarnings-CalendarRisk-Flag + Warn-Icon
next_ex_div_dateDividenden-CalendarRisk-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:

Strategiehas_stocktarget_returnBedeutung
L1nein1.5 %Initial-Eintritt, hoehere Renditeforderung
L2ja0.8 %Wheel-Phase 2: Cost-Basis-Reduktion
fallbackn/a0.6 %Sonderfall (sehr schwacher Markt)

Auswirkung an einem Beispiel (mid 1.10, strike 100 → premium_pct 1.1 %):

StrategieScoreMaximum
L11.1 / 1.5 * 40 = 29.340
L21.1 / 0.8 * 40 = 55 → 4040

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_distanceScore
0.012.5
1.520.0
2.525.0
3.520.0
5.012.5
7.50.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

Komponentemax PunkteAnteil am Maximum
Premium %4036.4 %
ATR-Distanz2522.7 %
Open-Interest2018.2 %
IV1513.6 %
Regel-16-Bonus109.1 %
Summe110100 %

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..10

1. 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

Komponentemax PunkteAnteil am Maximum
Premium %3030 %
Capital-Gain3030 %
ATR-Distanz2020 %
Open-Interest1010 %
IV1010 %
Summe100100 %

CMG-Beispiel

cost_basis 34.00, Spot 36.20, ATR52 1.31:

Strikeatr vs Spotatr vs cost_basiscapital_gainScore-Bewegung
36−0.15 ITM+1.535.9 %klar besser
38+1.37+3.0511.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 Strikes

PUT-Klassifikation

annual_return = premium_pct * 365 / dte
BedingungLabel
not has_stock und annual ≥ 12 % und atr ≥ min_atrL1 ✅
not has_stock und annual ≥ 12 % und atr ≥ min_atr−0.5L1 ⚠️
has_stock und annual ≥ 6 % und atr ≥ min_atrL2 ✅
has_stock und annual ≥ 6 % und atr ≥ min_atr−0.5L2 ⚠️
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
BedingungLabel
strike < min_strike⚠️
atr_distance ≥ sc_atr_min und annual ≥ 12 % und atr ≥ min_atrCC ✅
Wie oben, aber atr ≥ min_atr − 0.5CC ⚠️
Ex-Date < Expiry + 7 d⚠️
sonst-

Warn-Icons (an L1 / L2 / CC angehaengt)

IconBedeutung
📅Earnings ≤ 7 Tage vor Expiry
💰Ex-Dividend zwischen heute und Expiry

Sortierung der Kandidaten

ORDER BY score DESC,
         premium_pct DESC,
         dte ASC,
         strike ASC

Tie-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.5

Klassifikations-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