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>
242 lines
15 KiB
Markdown
242 lines
15 KiB
Markdown
# 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,15–0,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 | 30–45 | Bestes Verhältnis Zeitwertverfall/Handelskosten |
|
||
| Ziel-Delta | 0,20 (Band 0,15–0,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.** `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.
|