Files
IBKRTrader/docs/konzepte/KONZEPT-Modul-OptionsWheel.md
T
RichardandClaude Opus 5 80afcd49c1 R9: Echter IbkrBrokerClient über die TWS API
Broker-Adapter gegen TWS/IB Gateway, aktivierbar über IBKRSettings.UseTwsApi;
NullBrokerClient bleibt Default. TradingEnabled bleibt als zweite, unabhängige
Sicherung bestehen – ohne ihn platziert der ExecutionService keine Order.

Aufteilung (src/IBKRTrader.Core/Trading/Ibkr/):
- IbkrMapping      – reine Abbildung Core <-> TWS (Kontrakt, Order, Kurs, Port-
                     und Statusregeln), vollständig unit-getestet
- IbkrConnection   – Socket-Lebenszyklus, Reader-Thread, reqId-Korrelation über
                     TaskCompletionSource
- IbkrBrokerClient – implementiert IBrokerClient, übersetzt Fehler in leere
                     Ergebnisse (Konto 0 lässt die Risikoprüfung alles ablehnen)

Bewusste Entscheidungen:
- Träges Verbinden mit Wiederholung statt Verbindungsaufbau beim Start: TWS ist
  nach einem Neustart minutenlang nicht bereit.
- Port wird gegen den Handelsmodus geprüft; Paper-Modus auf Live-Port (oder
  umgekehrt) lässt den Broker inaktiv, statt auf dem falschen Konto zu handeln.
- MarketDataType Default 4: Paper-Konten ohne Datenabo bekommen sonst keine Kurse.
- Fehlercode 10167 ist ein Statushinweis (verzögerte Daten folgen), kein Fehler.
  Als Fehler behandelt scheiterte jede einzelne Kursabfrage.

Verifiziert gegen Paper-Konto DUR371528: Verbindung, Konto (100.105,50 EUR),
Kurse (AAPL/MSFT/NVDA, verzögert), Fehlerpfade. Orderpfad bis zur Broker-Annahme
per What-If-Order geprüft (Aktie + Option, ohne Ausführung); dabei zugleich die
Optionsberechtigung des Kontos bestätigt. Offen: echte Ausführung (Fill ->
Buchung) und asynchrone Fill-Verfolgung – beides in IBKR-Integration.md notiert.

Doku: TWS-Setup-Checkliste.md (Einstellungen für Neuinstallation) neu,
IBKR-Integration.md / ARCHITECTURE.md / README.md nachgezogen.

154/154 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 17:40:00 +02:00

15 KiB
Raw Blame History

Konzept: Modul „OptionsWheel" (Covered Call / Cash-Secured Put)

Stand: 2026-08-03 Ziel: Auf einer kleinen Watchlist von Tickern, an deren langfristigen Erfolg wir glauben, systematisch Optionsprämien vereinnahmen Cash-Secured Put → (Zuteilung) → Aktienbestand → Covered Call → (Abruf) → wieder Cash. Vollautomatisch, delta-basierte Strike-Wahl, aktives Rollen.

Festlegungen (2026-08-03, mit dem Betreiber abgestimmt):

  1. Put-Seite ist Cash-Secured Put, nicht Covered Put (kein Leerverkauf der Aktie).
  2. Vollautomatisch von Anfang an Sicherungen sind Schalter und Limits, keine Klick-Freigabe.
  3. Strike-Wahl delta-basiert (Zielband 0,150,30).
  4. Bei drohender Zuteilung wird gerollt, solange das per Netto-Kredit möglich ist.

0. Leitprinzipien

  1. Niemals nackt. Ein Short Call ist nur zulässig mit 100 freien Aktien je Kontrakt, ein Short Put nur mit reserviertem Cash über Strike × 100 je Kontrakt. Diese Deckungsprüfung gehört in den Core-RiskService, nicht ins Modul ein Modulfehler darf keine ungedeckte Option schreiben können.
  2. Der Broker ist die Wahrheit. Zuteilung und Verfall ändern Positionen ohne eine Order von uns. Ohne regelmäßigen Positionsabgleich gegen IBKR läuft die eigene Buchführung zwangsläufig auseinander.
  3. Nur Watchlist. Das Modul handelt ausschließlich explizit eingetragene Ticker. Keine Entdeckung, kein Screening, keine Ausweitung zur Laufzeit.
  4. Jede Entscheidung ist nachlesbar. Kandidatenbewertung, Zustandsübergang und Order gehen über core_decision_journal / core_order_events mit gemeinsamer SignalId dieselbe Forensik-Grundlage, die der Supervisor bereits nutzt.

1. Warum das nicht additiv geht: der Core ist heute aktienbasiert

Stelle Heutiger Stand Konsequenz
IbkrMapping.Stock() „der einzige Instrumententyp, den die Module handeln" Kein Options-Kontrakt (Expiry/Strike/Right/Multiplier) baubar
TradeSignal / OrderRequest nur Symbol + Side + Quantity Ein Signal kann keine Option benennen
Position.Notional Quantity × AvgPrice Bei Optionen um Faktor 100 falsch → Risikolimits wirkungslos
RiskService.EvaluateSell „Verkauf schließt Position", lehnt ohne Bestand ab Sell-to-open wird grundsätzlich abgelehnt die Kernoperation des Moduls
IBrokerClient.PlaceOrderAsync Fill-oder-Fehlschlag innerhalb OrderTimeoutSeconds Limitorder am Mid liegt im Buch → gilt als Fehlschlag, ist aber aktiv (bekannte Grenze, IBKR-Integration.md)
IBrokerClient kein reqPositions, keine Optionskette, keine Greeks Zuteilung/Verfall unsichtbar, Strike-Wahl unmöglich

Das Modul setzt also auf einem Core-Options-Fundament auf, das zuerst gebaut wird. Der Aktienpfad bleibt dabei unverändert (neue Felder sind optional, Kind = Stock ist der Default) CongressTrading darf nicht regressieren.


2. Core-Erweiterungen

2.1 Instrumententyp

public enum InstrumentKind { Stock, Option }
public enum OptionRight   { Call, Put }

public sealed record OptionSpec(
    string      Underlying,
    DateOnly    Expiry,
    decimal     Strike,
    OptionRight Right,
    int         Multiplier = 100);

TradeSignal und OrderRequest bekommen je ein optionales OptionSpec? Option. Ist es null, verhält sich alles exakt wie heute.

2.2 Positionen mit Instrument-Identität

core_position erhält Kind, Expiry, Strike, Right, Multiplier; der fachliche Schlüssel wird (Module, Symbol, Kind, Expiry, Strike, Right). Bestandszeilen sind Kind = Stock mit leeren Optionsfeldern Migration ohne Datenumzug. Notional rechnet ab jetzt mit dem Multiplikator. IPortfolioService bekommt entsprechend überladene Abfragen; die bestehenden Aktien-Signaturen bleiben.

2.3 Optionskette + Greeks

Neuer Seam am IBrokerClient:

Task<OptionChain?>  GetOptionChainAsync(string underlying, DateOnly from, DateOnly to, CancellationToken ct);
Task<OptionQuote?>  GetOptionQuoteAsync(OptionSpec spec, CancellationToken ct);   // + Delta, IV, OpenInterest
Task<IReadOnlyList<BrokerPosition>> GetPositionsAsync(CancellationToken ct);

Umsetzung in IbkrConnection:

  • Kette über reqSecDefOptParams liefert Expiries und Strikes je Underlying ohne Marktdaten und ohne Abo. Das ist der einzige gangbare Einstieg: eine volle Kette hat hunderte Kontrakte, TWS erlaubt aber nur ~100 gleichzeitige Marktdatenzeilen.
  • Vorfilter, dann Quotes: Expiry-Fenster aus dem DTE-Ziel, Strike-Fenster um den Spot (± x %) erst für diese Handvoll Kontrakte reqMktData mit Greeks (tickOptionComputation).
  • IbkrMapping.Option(spec)SecType="OPT", Exchange="SMART", LastTradeDateOrContractMonth als yyyyMMdd, Strike, Right="C"/"P", Multiplier="100", plus TradingClass (nötig, sobald ein Underlying mehrere Klassen führt, z. B. nach Splits oder bei Mini-Optionen). Rein, unit-getestet wie das bestehende IbkrMapping.

2.4 Asynchrone Order- und Fill-Verfolgung

Das ist der teuerste, aber unumgängliche Teil: eine Optionsorder wird limitiert am Mid platziert und füllt oft erst Minuten später oder gar nicht.

  • Neue Tabelle core_order_state: Order-ID, SignalId, Kontrakt, Menge, Limit, Status, gefüllte Menge, Zeitstempel überlebt einen App-Neustart.
  • IBrokerClient.SubmitOrderAsync(...) gibt die Order-ID ohne Warten zurück; PlaceOrderAsync bleibt für den Aktienpfad erhalten (intern: submit + warten).
  • Dauerabo auf orderStatus / openOrder / execDetails statt Slot-je-Order; ein OrderTrackingService (IHostedService) trägt Endstatus nach, ruft dann RecordFillAsync und schreibt core_order_events. Nicht gefüllte Orders werden nach einer Frist neu bepreist oder storniert.
  • Nebennutzen: löst die in IBKR-Integration.md dokumentierte Grenze auch für CongressTrading.

2.5 Positionsabgleich (Zuteilung & Verfall)

ReconcilePositionsWorker (IHostedService, z. B. stündlich und nach Marktschluss) vergleicht reqPositions mit core_position:

  • Aktien tauchen auf / Short Put verschwindet → Zuteilung.
  • Short Call verschwindet, Aktien weg → Abruf (called away).
  • Option verschwindet am Verfallstag ohne Gegenbuchung → wertlos verfallen. Jede Abweichung wird gebucht und ins Entscheidungsjournal geschrieben. Unerklärbare Differenzen (manueller Eingriff in TWS) setzen den betroffenen Ticker auf Halted.

2.6 Risiko: sell-to-open + Deckung

RiskContext bekommt FreeShares (nicht bereits durch andere Short Calls gebunden) und AvailableCashForReservation. Neue Regeln im RiskService:

Fall Regel
Sell-to-open Call FreeShares ≥ 100 × Kontrakte, sonst Ablehnung „ungedeckt"
Sell-to-open Put AvailableCashForReservation ≥ Strike × 100 × Kontrakte, sonst Ablehnung
Buy-to-close zulässig bis zur offenen Short-Menge
Limits MaxTradePercent / MaxPositionPercentPerModule rechnen mit dem Multiplikator

3. Modul IBKRTrader.Modules.OptionsWheel

IModule mit Name = "OptionsWheel", DbPrefix = "ow_", referenziert nur den Core, eigener DbContext, eigenes Fenster, eigene Settings-Sektion analog Accounting/CongressTrading.

3.1 Zustandsautomat je Ticker

Idle ──sell put──► ShortPut ──ITM/DTE──► Rolling ──► ShortPut
  ▲                   │                     │
  │                   │ Zuteilung           └─ kein Netto-Kredit ─► (Zuteilung zulassen)
  │                   ▼
  └── called away ── Stock ──sell call──► ShortCall ──ITM/DTE──► Rolling ──► ShortCall
                       ▲                     │
                       └─ verfallen ─────────┘

Zusätzlich PendingOrder (Order im Buch) und Halted (Sperre nach Fehler/Abweichung, nur manuell lösbar). Der Automat ist rein und unit-getestet er kennt weder Broker noch Datenbank.

3.2 Persistenz (ow_)

Tabelle Inhalt
ow_underlyings Watchlist: Ticker, aktiv, Parameter-Überschreibungen, aktueller Zustand
ow_cycles Ein Wheel-Durchlauf (Start, Ende, vereinnahmte Prämie brutto/netto, Ergebnis, Aktien-Einstand)
ow_legs Jede verkaufte/geschlossene Option: Kontrakt, Prämie, Delta+IV bei Eröffnung, DTE, Status, Roll-Kette
ow_candidates Bewertete Strike-Kandidaten je Scan (auch die verworfenen, mit Grund) Nachvollziehbarkeit
ow_events Zustandsübergänge mit Auslöser

ow_candidates ist bewusst dabei: bei Vollautomatik ist die nicht getroffene Wahl die wichtigste Information für die spätere Fehlersuche und für den Supervisor.

3.3 Reine Logik (Logic/, vollständig unit-getestet)

  • StrikeSelector filtert die Kette: DTE-Fenster, Delta-Zielband, Mindestprämie (absolut und als annualisierte Rendite), maximaler Bid/Ask-Spread, Mindest-Open-Interest. Bewertet die Verbleibenden und begründet jede Verwerfung.
  • RollDecider rollen ja/nein: Trigger, Zielkontrakt, Netto-Kredit-Bedingung.
  • CoverageCalculator welche Aktien/welches Cash sind frei, welche durch offene Shorts gebunden.
  • PremiumMath annualisierte Rendite, Break-even, effektiver Einstand nach Prämien.
  • WheelStateMachine zulässige Übergänge, Ableitung des Zustands aus Positionen.

3.4 Worker

  • WheelScanWorker im Handelszeitfenster: Zustand je Ticker prüfen, ggf. neues Leg eröffnen.
  • WheelManageWorker offene Legs überwachen: rollen, schließen, Orders nachbepreisen.

3.5 UI (ein Fenster, Tabs)

Übersicht (je Ticker: Zustand, offenes Leg, DTE, aktuelles Delta, Prämie vereinnahmt/annualisiert), Watchlist (Ticker + Parameter, Not-Aus je Ticker), Kandidaten (letzter Scan inkl. Verwerfungsgründe), Zyklen/Historie, Log. DB-Zugriff nur auf Interaktion (Smoke-UI-sicher).


4. Regelwerk (Defaults, je Ticker überschreibbar)

Parameter Default Begründung
DTE beim Öffnen 3045 Bestes Verhältnis Zeitwertverfall/Handelskosten
Ziel-Delta 0,20 (Band 0,150,30) ≈ 20 % Zuteilungswahrscheinlichkeit
Roll-Trigger DTE ≤ 21 und (Delta > 0,50 oder ITM) Gamma-Risiko steigt in der letzten Woche stark
Roll-Bedingung nur bei Netto-Kredit Ein Debit-Roll kauft nur Zeit und kostet Geld
Covered-Call-Strike ≥ effektiver Einstand der Aktien Verhindert, dass der Wheel systematisch Verluste realisiert
Max. Kontrakte je Ticker 1 Bewusst klein starten
Max. gebundenes Cash gesamt Anteil des Kontowerts Zweite Grenze über die Core-Limits hinaus
Vorzeitiges Schließen bei Gewinnziel aus Nicht gewählt siehe Hinweis unten

Hinweis zum Gewinnziel: Der übliche Begleiter der Roll-Politik ist, ein Leg bei ~50 % vereinnahmter Prämie zurückzukaufen; das nimmt Gamma-Risiko aus der letzten Woche und ist der Grund, warum viele Roll-Trigger nie greifen. Der Parameter ist vorgesehen (ProfitTargetPercent existiert bereits in TradingSettings), steht per Vorgabe auf „aus" und lässt sich ohne Codeänderung zuschalten.

Zuteilung ist nicht abwählbar. Auch bei konsequentem Rollen teilt IBKR zu amerikanische Optionen können jederzeit ausgeübt werden, beim Call besonders vor dem Ex-Dividenden-Tag, und ein Roll ist nicht immer per Netto-Kredit möglich. Zuteilung ist deshalb ein regulärer Pfad des Automaten, kein Fehler. Genau dafür ist der Positionsabgleich (2.5) Pflicht.


5. Sicherungen

Zu den bestehenden zwei Schaltern (IBKR.UseTwsApi, Trading.TradingEnabled) kommen:

  1. OptionsWheel.Enabled Modulschalter.
  2. Watchlist als Whitelist kein Ticker außerhalb.
  3. Naked-Sperre im Core-RiskService (2.6) wirkt auch, wenn die Modullogik falsch liegt.
  4. Halted je Ticker bei unerklärter Positionsabweichung; Neueröffnungen stoppen, bestehende Legs werden weiter verwaltet.
  5. Kill-Switch „keine Neueröffnungen" laufende Positionen bleiben handhabbar.

6. Phasen

Phase Inhalt Abschlusskriterium
W-0 Core: InstrumentKind/OptionSpec, IbkrMapping.Option, Positionen mit Multiplikator Build + Tests grün, Aktienpfad unverändert
W-1 Core: Optionskette + Greeks (reqSecDefOptParams, tickOptionComputation) Kette eines Watchlist-Tickers gegen das Paper-Gateway abrufbar
W-2 Core: asynchrone Order-/Fill-Verfolgung (core_order_state, OrderTrackingService) Limitorder überlebt Timeout und App-Neustart, Fill wird nachgebucht
W-3 Core: Positionsabgleich + RiskService sell-to-open/Deckung Zuteilung im Paper erkannt und gebucht; ungedeckte Order wird abgelehnt
W-4 Modul-Gerüst: IModule, ow_-DbContext + Migration, UI-Tabs, Watchlist App startet, --smoke-ui grün, kein Handel
W-5 Reine Strategie-Logik + Tests (StrikeSelector, StateMachine, RollDecider, PremiumMath) Hohe Testabdeckung ohne Broker
W-6 Verdrahtung + vollautomatischer Paper-Betrieb, Beobachtung über mehrere Verfallszyklen Mindestens ein vollständiger Wheel-Durchlauf im Paper
W-7 Live-Freigabe Eigene Entscheidung nach W-6

W-0 bis W-3 sind Core-Arbeit und nützen auch den anderen Modulen; erst ab W-4 entsteht das Modul selbst.


7. Bewusst offen / zu klären

  1. Optionsberechtigung im Paper-Konto DUR371528 prüfenerledigt am 2026-08-04, vorhanden. Verifiziert gegen das laufende Gateway (Details siehe IBKR-Integration.md, Abschnitt „Optionen"): reqSecDefOptParams für AAPL liefert 24 Verfallstermine, 127 Strikes, Multiplier 100 über SMART; eine What-If-Order auf AAPL 20260812 C302.5 wurde von IBKR angenommen (Init-Margin 589,52) statt mit einem Berechtigungsfehler abgelehnt. Damit ist die Grundvoraussetzung für dieses Modul gegeben.
  2. Greeks bei verzögerten Daten. MarketDataType = 4 liefert Optionsberechnungen als verzögerte Tick-Variante; ob Delta zuverlässig ankommt, muss gegen das laufende Gateway verifiziert werden. Fällt es aus, greift ersatzweise eine Strike-Wahl über Abstand in % + Mindestprämie der StrikeSelector wird von vornherein so geschnitten, dass beide Kriterien einsetzbar sind.
  3. Marktdatenabo (OPRA) für Realtime-Optionskurse Kosten/Notwendigkeit später entscheiden.
  4. Earnings-Sperre: keine neuen Legs über Quartalszahlen hinweg. Sinnvoll, aber es fehlt eine Datenquelle für Earnings-Termine Punkt bewusst offen.
  5. Accounting-Anschluss: Optionsprämien, Zuteilungen und Abrufe müssen im AccountingClassifier eigene Buchungskategorien bekommen; der RealizedPnlEngine (FIFO) kennt weder Multiplikator noch die Einstandsverschiebung durch Zuteilung. Eigene Arbeit im Accounting-Modul.
  6. Steuer bleibt wie im Accounting-Konzept unberührt und offen. Keine Steuerberatung.