Files
IBKRTrader/docs/konzepte/KONZEPT-Modul-OptionsWheel.md
T
RichardandClaude Opus 5 b96a20788c R10: Lesender Bestandsabgleich (IBrokerPortfolioReader) + Datenlage-Konzepte
Eigener Seam neben IBrokerClient: Wer handelt, braucht ihn nicht; wer die
eigene Buchfuehrung gegen den Broker abstimmt, braucht nur ihn. Zuteilung
und Verfall aendern Positionen ohne Order von uns - ohne Abgleich laeuft
das Managementbuch zwangslaeufig auseinander.

- IBrokerPortfolioReader mit GetPositionsAsync/GetExecutionsAsync;
  implementiert von IbkrBrokerClient und NullBrokerClient (DI registriert
  beide Rollen auf derselben Instanz).
- IbkrConnection: reqAccountUpdates statt reqPositions (nur dieser Weg
  liefert Marktwert und unrealisierten G/V), reqExecutions inkl. Zuordnung
  der verspaetet eintreffenden commissionReport-Callbacks ueber die ExecId.
- BrokerPosition/BrokerExecution als Broker-Wahrheit neben Position;
  IbkrMapping: ParseSide, ParseExecutionTime, FormatExecutionFilterTime
  (UTC wegen TWS-Warnung 2174) - mit Unit-Tests.
- Verifiziert gegen Paper-Konto DUR371528: 2 Positionen, 2 Ausfuehrungen
  inkl. Kommissionen.

Doku: Kapital- und Buchmodell (drei Wahrheiten, Kapitalzuteilung),
KONZEPT-Datenlage-und-Strategien (gemessen, was die API auf diesem Konto
liefert). Options-Wheel: Greeks bei verzoegerten Daten funktionieren
(Feld 83); Earnings-Termine sind ueber die TWS API nicht erreichbar
(Fehler 10358) - Behelf ueber IV-Filter statt Fremddatenquelle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:51:51 +02:00

248 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](../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
```csharp
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`:
```csharp
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](../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üfen**~~**erledigt am 2026-08-04, vorhanden.**
Verifiziert gegen das laufende Gateway (Details siehe [IBKR-Integration.md](../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.**~~**erledigt am 2026-08-04, funktioniert.** Gemessen an
`AAPL 20260821 C305`: IV 0,2730 / Delta 0,5443 / Gamma 0,0220 / Vega 0,2626 / Theta 0,2304.
Die Werte kommen über die verzögerten Tick-Felder 8083; **Feld 83 (Modell)** ist die für die
Strike-Wahl maßgebliche Variante. Die delta-basierte Wahl im Band 0,150,30 ist damit umsetzbar,
die Ersatzlösung über prozentualen Abstand wird nicht gebraucht (der `StrikeSelector` behält sie
trotzdem als Rückfalllinie). Details: [KONZEPT-Datenlage-und-Strategien.md](KONZEPT-Datenlage-und-Strategien.md).
3. **Marktdatenabo (OPRA)** für Realtime-Optionskurse Kosten/Notwendigkeit später entscheiden.
4. **Earnings-Sperre**: keine neuen Legs über Quartalszahlen hinweg. **Geprüft am 2026-08-04: über
die TWS API nicht lösbar** `CalendarReport` und alle übrigen Fundamentaldaten sind auf dem
Konto gesperrt (Fehler 10358, Refinitiv-Abo nötig). Das ist die einzige Stelle, an der uns eine
externe Quelle ernsthaft fehlt. **Behelf ohne Fremddaten:** ein IV-Filter keine neuen Legs,
wenn die implizite Volatilität des Basiswerts deutlich über ihrem 30-Tage-Mittel liegt. Fängt
den Earnings-Anstieg indirekt mit ab, unschärfer, aber ohne Abhängigkeit.
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.