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 drei besten Strikes pro Symbol werden zu Gold, Silber und Bronze.

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 → Praemie p.a. DESC → DTE ASC → Strike
            |
            v
       Top-3 Medals (🥇🥈🥉)

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/🚫
5Welche drei sind die Top-Picks?Gold/Silber/Bronze

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 Liquiditaet trotzdem in die Top-3 rutschen, wenn nichts Besseres da ist.

Eingaben aus dem SymbolContext

Vor der Bewertung laedt load_symbol_context pro Symbol:

FeldQuelleVerwendet fuer
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-Praemie 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 Praemien fuer Bestandshalter (L2) attraktiver bewertet als fuer 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 Liquiditaets-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 Praemie → 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 fuer L1 und L2, nicht fuer 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 fuer den User (keine Anlageberatung, keine persoenliche 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 (Praemie/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 Top-3

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

Tie-Breaker also: Score > Praemie p.a. > kurze Laufzeit > kleiner Strike. Alle Kandidaten muessen 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 (fuer 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

  • "Medals & Risk-Flags" – wie aus dem Score Gold/Silber/Bronze wird
  • "Volatilitaets-Regime (VIX)" – die VIX-Stufen im Detail
  • "Options-Screening" – die Filter-Stufe vor dem Scoring
  • "Sicherheits-Gates" – was zwischen Klassifikation und Order-Send liegt