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>
This commit is contained in:
Richard
2026-08-04 17:40:00 +02:00
co-authored by Claude Opus 5
parent 2a312ca035
commit 80afcd49c1
14 changed files with 1427 additions and 45 deletions
+4 -1
View File
@@ -125,7 +125,10 @@ Pin `new MariaDbServerVersion(new Version(11, 8, 6))`. Verbindung aus `appsettin
- [x] Core-**Dashboard-View** (Gesamtüberblick: Trading-Modus, aggregierte Kennzahlen, geladene Module) + Icon
- [x] `DashboardService` (aggregiert Positionen/Exposure/Trades via EF) + **2 InMemory-Tests** → 58/58 grün
- [x] Tests durchgehend portiert; `--smoke-ui` deckt alle Views ab
- [ ] **Nächster Meilenstein (blockiert bis Paper-Zugang):** echter `IbkrBrokerClient` über die **TWS API / IB Gateway** (entschieden 2026-07-29). Setup + Design: siehe [IBKR-Integration.md](IBKR-Integration.md). Ports Paper 4002 / Live 4001, region-agnostisch (US↔IE), `NullBroker` bleibt Default bis verifiziert.
- [x] **Echter `IbkrBrokerClient` über die TWS API** (2026-07-31): `src/IBKRTrader.Core/Trading/Ibkr/` `IbkrMapping` (rein, unit-getestet), `IbkrConnection` (Socket + reqId-Korrelation), `IbkrBrokerClient` (`IBrokerClient`). Aktivierung über `IBKRSettings.UseTwsApi`; `NullBrokerClient` bleibt Default. Design und Grenzen: [IBKR-Integration.md](IBKR-Integration.md), Einstellungen: [TWS-Setup-Checkliste.md](TWS-Setup-Checkliste.md).
- [x] Gegen Paper-Konto DUR371528 verifiziert: Verbindung, Konto (NetLiquidation 100.105,50 EUR), Kurse (AAPL/MSFT/NVDA, verzögert) und Fehlerpfade
- [x] Orderpfad bis zur Broker-Annahme per **What-If-Order** verifiziert (Aktie + Option, keine Ausführung); **Optionsberechtigung im Paper-Konto bestätigt**
- [ ] **Offen:** `PlaceOrderAsync` mit echter Ausführung verifizieren (Fill → Buchung); asynchrone Fill-Verfolgung (Orders ohne sofortige Ausführung)
- [ ] IBKR-Account-Credentials mit `EncryptedStringConverter` speichern
---
+103 -33
View File
@@ -1,11 +1,13 @@
# IBKR-Anbindung Plan (TWS API via IB Gateway)
# IBKR-Anbindung (TWS API via IB Gateway)
**Entscheidung:** Der `IbkrBrokerClient` (ersetzt `NullBrokerClient`) nutzt die **TWS API** über das
**IB Gateway** native C#-Lib, robusteste Order-Ausführung, region-agnostisch.
**Entscheidung:** Der `IbkrBrokerClient` (Alternative zum `NullBrokerClient`) nutzt die **TWS API**
über das **IB Gateway** native C#-Lib, robusteste Order-Ausführung, region-agnostisch.
> Umsetzung ist **blockiert**, bis der Paper-Zugang + ein laufendes IB Gateway verfügbar sind
> (echte Verifikation von Quotes/Konto/Orders nur gegen den Gateway möglich). Dieses Dokument hält
> Setup und Design fest, damit die Umsetzung dann schnell und korrekt läuft.
> **Stand 2026-07-31:** Der Adapter ist implementiert (`src/IBKRTrader.Core/Trading/Ibkr/`) und wird
> über `IBKRSettings.UseTwsApi` aktiviert. Gegen das Paper-Konto **DUR371528** verifiziert sind
> Verbindung, `GetAccountStateAsync` und `GetQuoteAsync` (verzögerte Kurse, siehe unten) sowie der
> Fehlerpfad bei unbekanntem Symbol und nicht erreichbarer TWS.
> **Noch nicht verifiziert: `PlaceOrderAsync`** dafür ist eine echte Order auf dem Paper-Konto nötig.
## Regionen (US ↔ IE)
Gleiche API, gleiche Gateway-Software, **eine Codebasis**. Unterschied nur: Login/Entity
@@ -13,6 +15,8 @@ Gleiche API, gleiche Gateway-Software, **eine Codebasis**. Unterschied nur: Logi
Paper vs. Live = Konto + Port, kein Code-Unterschied.
## Betrieb (operativ)
- **Setup auf neuem System:** alle nötigen TWS-/Gateway-Einstellungen Schritt für Schritt in der
[TWS-Setup-Checkliste](TWS-Setup-Checkliste.md).
- **IB Gateway** (schlank, statt TWS) muss laufen und eingeloggt sein.
- **Ports:** Paper **4002**, Live **4001** (Auswahl über `TradingSettings.Mode`).
→ passt exakt zu den bestehenden `IBKRSettings` (Host `127.0.0.1`, Port, `ClientId`).
@@ -20,38 +24,104 @@ Paper vs. Live = Konto + Port, kein Code-Unterschied.
Server-Betrieb **IBC/IBController** (Auto-Login + geplanter Neustart).
## Bibliothek
- Empfohlen: offizielle C#-API als NuGet-Mirror **`IB.TWS.CSharpApi`** (keine Abhängigkeiten),
ggf. der vereinfachte Wrapper **`IB.CSharpApiClient`** (mathpaquette) für weniger Callback-Boilerplate.
- NuGet-Allowlist (`NuGet.config`) muss um das Paket ergänzt werden.
Offizielle C#-API als NuGet-Mirror **`IB.TWS.CSharpApi` 9.76.1**, referenziert im Core und in der
NuGet-Allowlist (`NuGet.config`) freigegeben. Das Paket zielt auf .NET Framework, ist aber reiner
managed Code und läuft auf net10 `NU1701` ist in der `.csproj` bewusst unterdrückt.
## Design `IbkrBrokerClient : IBrokerClient`
Die TWS API ist **callback-basiert** (`EClientSocket` sendet, `EWrapper` empfängt Events). Wir kapseln
das hinter unserem bestehenden async-Seam `IBrokerClient`:
- **Verbindung:** `EClientSocket.eConnect(host, port, clientId)` + Reader-Thread; Reconnect-Logik.
- **Korrelation:** je Request eine `reqId`; Antworten via `TaskCompletionSource` in einem
`ConcurrentDictionary<int, TCS>` auflösen. Timeout je Request.
- **`GetQuoteAsync(symbol)`** → Kontrakt (`reqContractDetails`/`reqMatchingSymbols`) → Snapshot-Kurs
(`reqMktData` snapshot bzw. `reqTickByTickData`) → `Quote(Last, Bid, Ask)`.
- **`GetAccountStateAsync()`** → `reqAccountSummary` (NetLiquidation, AvailableFunds).
- **`PlaceOrderAsync(OrderRequest)`** → `Order` bauen (MKT/LMT, Menge, Side) → `placeOrder(orderId, contract, order)`;
Ergebnis über `orderStatus`/`openOrder`/`execDetails`-Callbacks einsammeln → `OrderResult`.
`nextValidId` liefert die Order-IDs.
- **Sicherheit bleibt:** `NullBrokerClient` bleibt Default; `IbkrBrokerClient` wird erst registriert
(via Config-Schalter), wenn verifiziert. Handel zusätzlich weiter durch `TradingEnabled`-Gate geschützt.
## Umsetzung (`src/IBKRTrader.Core/Trading/Ibkr/`)
Die TWS API ist **callback-basiert** (`EClientSocket` sendet, `EWrapper` empfängt Events). Das ist
hinter dem bestehenden async-Seam `IBrokerClient` gekapselt, aufgeteilt in drei Dateien:
| Datei | Aufgabe |
|---|---|
| `IbkrMapping.cs` | Reine Abbildung Core ↔ TWS (Kontrakt, Order, Kurs, Port-/Statusregeln) **unit-getestet** |
| `IbkrConnection.cs` | Socket-Lebenszyklus, Reader-Thread, reqId-Korrelation über `TaskCompletionSource` |
| `IbkrBrokerClient.cs` | Implementiert `IBrokerClient`, übersetzt Fehler in leere Ergebnisse |
**Ablauf je Methode**
- **`GetQuoteAsync`** → `reqContractDetails` (Ergebnis wird gecacht) → `reqMktData` als Snapshot →
`Quote`. Preis-Reihenfolge: letzter Handelspreis, sonst Bid/Ask-Mitte, sonst Schlusskurs.
- **`GetAccountStateAsync`** → `reqAccountSummary` (NetLiquidation, AvailableFunds).
- **`PlaceOrderAsync`** → Kontrakt auflösen → `placeOrder`; das Ergebnis kommt über `orderStatus`,
die Order-IDs liefert `nextValidId`.
**Bewusste Entscheidungen**
- **Träges Verbinden mit Wiederholung statt Verbindungsaufbau beim Start.** TWS ist nach einem
Neustart minutenlang nicht bereit (erst abgelehnte Verbindungen, dann abgebrochene Handshakes);
ein einmaliger Versuch beim Programmstart würde das nicht überleben.
- **Port-Prüfung gegen den Handelsmodus.** Paper-Modus auf einem Live-Port (oder umgekehrt) wird
abgelehnt, der Broker bleibt dann inaktiv sonst würde echtes Geld bewegt, wo ein Test erwartet wird.
- **Fehler werden nie geworfen, sondern zu leeren Ergebnissen.** Konto 0 lässt die Risikoprüfung
jedes Signal ablehnen die sichere Richtung bei nicht erreichbarem Broker.
- **`MarketDataType` standardmäßig 4 (verzögert + Frozen).** Paper-Konten ohne Datenabo bekommen
sonst überhaupt keine Kurse, und der `ExecutionService` überspringt jedes Signal mit „kein Kurs".
- **Zwei Sicherungen bleiben:** `UseTwsApi` entscheidet über den Adapter, `TradingEnabled` über den
Handel. Ohne den zweiten Schalter platziert der `ExecutionService` keine Order.
### Bekannte Grenze: Orders ohne sofortige Ausführung
`IBrokerClient.PlaceOrderAsync` ist synchron gedacht (Ausführung oder Fehlschlag). Eine Order, die
innerhalb von `OrderTimeoutSeconds` keinen Endstatus erreicht etwa eine Limit-Order im Orderbuch
oder eine Market-Order außerhalb der Handelszeiten , wird als Fehlschlag zurückgegeben, **kann bei
IBKR aber weiterhin aktiv sein**. Die Meldung enthält deshalb Order-ID und letzten Status. Sauber
lösen ließe sich das nur mit asynchroner Fill-Verfolgung (Order-Zustand persistieren, `orderStatus`
und `execDetails` dauerhaft mitschreiben) das ändert den Seam und ist eigene Arbeit.
## Marktdaten
Das Paper-Konto hat **kein Marktdaten-Abo**. TWS meldet deshalb bei jeder Kursanfrage den Hinweis
**10167** („nicht abonniert, es werden verzögerte Marktdaten angezeigt") und liefert danach ganz
normal verzögerte Ticks. Das ist ein **Statushinweis, kein Fehler** wird er als Fehler behandelt,
bricht die Kursanfrage ab, bevor die Ticks eintreffen, und jedes Symbol liefert „kein Kurs".
`IbkrMapping.IsInformational` deckt den Code deshalb ausdrücklich ab (mit Test).
Verifiziert am 2026-07-31 gegen DUR371528: AAPL 302,94 / MSFT 476,88 / NVDA 209,81 (verzögert).
## Optionen (Berechtigung verifiziert 2026-08-04)
Das Paper-Konto **DUR371528 ist für den Optionshandel freigeschaltet**. Geprüft ohne jede Ausführung
über eine **What-If-Order** (`Order.WhatIf = true`): IBKR rechnet dabei Margin und Gebühren durch die
komplette Prüfkette inklusive Handelsberechtigung und verwirft die Order anschließend.
| Prüfung | Ergebnis |
|---|---|
| `reqSecDefOptParams` AAPL | 24 Verfallstermine, 127 Strikes (5600), Multiplier 100, TradingClass AAPL |
| Börsen | SMART, CBOE, ISE, EMERALD, NASDAQBX, PHLX, AMEX, MEMX, PSE |
| Kontrakt `AAPL 20260812 C302.5` | aufgelöst, ConId 906106141, LocalSymbol `AAPL 260812C00302500` |
| What-If BUY 1 Kontrakt | **angenommen**, Init-Margin 0 → 589,52 |
| What-If BUY 1 AAPL (Aktie) | **angenommen**, Init-Margin 0 → 87,61, Kommission 1,00 USD |
Fehlte die Berechtigung, hätte IBKR die What-If-Order mit einem Berechtigungsfehler abgelehnt statt
eine Margin zu liefern. Für **Realtime**-Optionskurse wäre zusätzlich ein OPRA-Abo nötig; ohne Abo
kommen verzögerte Daten (siehe Marktdaten unten). Modul-Konzept:
[konzepte/KONZEPT-Modul-OptionsWheel.md](konzepte/KONZEPT-Modul-OptionsWheel.md).
> **What-If als Testwerkzeug:** Damit lässt sich der gesamte Orderpfad bis zur Broker-Annahme prüfen,
> ohne eine Position zu eröffnen. Der Adapter nutzt es nicht produktiv für Vorabprüfungen
> (Margin-Deckung vor einer echten Order) wäre es aber ein naheliegender Ausbau.
### Historie: welcher Gateway?
Aktuell laufen die Marktdaten-Worker über die **Client Portal Web API** (`IBKRGatewayService`).
Optionen (später entscheiden):
Später zu entscheiden:
- Broker (Quotes/Konto/Orders) → TWS API; Marktdaten-Historie **vorerst auf CP Web API belassen**, oder
- Historie ebenfalls auf TWS (`reqHistoricalData`) migrieren → nur noch ein Gateway.
## Testbarkeit
- Reine Logik (Order-/Kontrakt-Mapping, Response-Parsing) → Unit-Tests möglich.
- Verbindung/Quotes/Orders → **manuell gegen das Paper-Gateway**, sobald verfügbar.
## Konfiguration (`settings.json`, Abschnitt `IBKR`)
## To-do bei Gateway-Zugang
1. `IB.TWS.CSharpApi` (+ NuGet-Allowlist) hinzufügen.
2. `IbkrBrokerClient` implementieren (Design oben), hinter Config-Schalter registrieren.
3. `TradingSettings.Mode` → Port 4002/4001; `IBKRSettings.ClientId` nutzen.
4. Gegen Paper verifizieren: Verbindung → Quote → Konto → Test-Order (Paper) → Buchung.
5. IBC für Auto-Login/Neustart einrichten.
| Schlüssel | Default | Bedeutung |
|---|---|---|
| `UseTwsApi` | `false` | Aktiviert den echten Adapter; sonst `NullBrokerClient` |
| `Host` / `Port` / `ClientId` | `127.0.0.1` / `4002` / `1` | Verbindung; Port muss zum Handelsmodus passen |
| `MarketDataType` | `4` | 1 Realtime, 2 Frozen, 3 verzögert, 4 verzögert+Frozen |
| `ConnectTimeoutSeconds` | `15` | Socket-Verbindung und Handshake |
| `RequestTimeoutSeconds` | `15` | Kurs-, Kontrakt- und Kontoantworten |
| `OrderTimeoutSeconds` | `60` | Wartezeit auf den Endstatus einer Order |
## Testbarkeit
- `IbkrMapping` ist vollständig unit-getestet (`tests/.../IbkrMappingTests.cs`): Port-/Modus-Regeln,
Kontrakt- und Order-Aufbau, Kursableitung, Tick- und Statusklassifizierung.
- Verbindung, Kurse und Orders bleiben **manuelle Verifikation gegen das Paper-Gateway** siehe
[TWS-Setup-Checkliste](TWS-Setup-Checkliste.md), Abschnitt Verifikation.
## Offen
1. `PlaceOrderAsync` gegen das Paper-Konto verifizieren (Order → Fill → Buchung).
2. IBC für Auto-Login/Neustart einrichten (Server-Betrieb).
3. Asynchrone Fill-Verfolgung, siehe „Bekannte Grenze" oben.
4. Entscheiden, ob die Marktdaten-Historie von der CP Web API auf `reqHistoricalData` wandert.
+119
View File
@@ -0,0 +1,119 @@
# TWS / IB-Gateway Setup-Checkliste (Neuinstallation)
Ziel: Auf einem frischen System alle Einstellungen so setzen, dass IBKRTrader **ohne manuelle
Eingriffe** (Popups, Bestätigungen, nächtliche Abbrüche) gegen die TWS API arbeiten kann.
Stand: 2026-07-31. **Verifiziert**: TWS Paper (Konto DUR371528), Verbindung über
`127.0.0.1:4002` erfolgreich, ServerVersion 151, Kontodaten + Positionen werden geliefert.
## 1. Voraussetzungen (einmalig, im IBKR Client Portal)
- [ ] Paper-Trading-Konto aktiv (Konto-ID beginnt mit `DU…`).
- [ ] Paper-**Benutzername** und Passwort bekannt: Client Portal → **Einstellungen →
Konto-Einstellungen → Paper-Trading-Konto**. (Login erfolgt mit dem Paper-Username,
nicht mit der Konto-ID!)
- [ ] Optional: **Marktdaten-Freigabe** Live → Paper aktivieren (sonst nur verzögerte Kurse):
ebenfalls unter Einstellungen → Paper-Trading-Konto.
- [ ] Software installieren: **TWS** (Desktop/Entwicklung) oder **IB Gateway** (Server, schlanker).
Version „Stable“ genügt.
## 2. Login
- [ ] Beim Start Modus **„Paper Trading“** wählen (nicht „Live Trading“).
- [ ] Mit Paper-Benutzername anmelden. Paper verlangt **kein 2FA** → automatisierbar.
## 3. API-Einstellungen
TWS: **File → Global Configuration → API → Einstellungen** (Gateway: Configure → Settings → API):
- [ ] **„ActiveX- und Socket-Clients aktivieren“** ☑
- [ ] **„Schreibgeschützte API“** ☐ (deaktiviert lassen, sonst sind keine Orders möglich)
- [ ] **Socket-Port:** Paper **4002**, Live **4001** (TWS-Default wäre 7497/7496 → umstellen,
damit es zur App-Konfiguration passt)
- [ ] **„Nur Verbindungen vom lokalen Host zulassen“** ☑ (App läuft auf derselben Maschine)
- [ ] **„Vertrauenswürdige IPs“: `127.0.0.1` eintragen** (Erstellen → IP → Übernehmen).
**Kritisch für den Automatikbetrieb:** Ohne Eintrag verlangt TWS für jede API-Verbindung
eine Popup-Bestätigung; ein übersehenes Popup = tote Verbindung.
- [ ] Optional fürs Debugging: „API-Nachrichten-Logdatei erstellen“ ☑
- [ ] **Master-API-Client-ID:** leer lassen (App nutzt `IBKRSettings.ClientId`, Default 1;
Test-Tools nutzen andere IDs, z. B. 99 — jede parallele Verbindung braucht eine eigene ID)
## 4. Vorsichtseinstellungen (API → Vorsichtseinstellungen / Precautions)
- [ ] **„Bypass Order Precautions for API Orders“** ☑ — sonst hält TWS automatische Orders mit
Bestätigungsdialogen an (Preis-/Größen-Warnungen). Die Risiko-Limits übernimmt stattdessen
unsere App (`TradingSettings`: MaxTradePercent, MaxSlippagePercent, …).
## 5. Dauerbetrieb (kein nächtlicher Ausfall)
- [ ] **Sperren und schließen** (Lock and Exit): **„Automatischer Neustart“** statt „Beenden“
wählen; Neustartzeit außerhalb der Handelszeiten legen (z. B. 03:00).
→ TWS läuft dann bis zu ~1 Woche ohne neuen Login durch.
- [ ] Wöchentlicher Neuanmelde-Zwang bleibt: beim Paper-Konto ohne 2FA unkritisch,
fürs Live-Konto später **IB Key mit „Weekly Re-Authentication“**.
- [ ] Server-Zielbild (siehe [IBKR-Integration.md](IBKR-Integration.md)): **IB Gateway + IBC**
(IBController) für Auto-Login und geplante Neustarts, als Autostart/geplanter Task.
## 6. App-Konfiguration (`settings.json`, lokal / gitignored)
- [ ] `IBKR.Host` = `127.0.0.1`
- [ ] `IBKR.Port` = **4002** (Paper) bzw. **4001** (Live)
- [ ] `IBKR.ClientId` = `1` (jede parallele API-Verbindung braucht eine eigene ID)
- [ ] `IBKR.UseTwsApi` = `true` → echter Broker; `false``NullBrokerClient` (handelt nie)
- [ ] `IBKR.MarketDataType` = `4` (verzögert + Frozen) ohne Datenabo liefert `1` keine Kurse
- [ ] `Trading.Mode` = `Paper`
- [ ] `Trading.TradingEnabled` = `false`, bis die Verbindung verifiziert ist
Port und Handelsmodus müssen zusammenpassen: Paper-Modus auf einem Live-Port (und umgekehrt) lehnt
der Adapter ab und bleibt inaktiv, statt auf dem falschen Konto zu handeln.
## 7. Verifikation
- [ ] Port erreichbar? `Test-NetConnection 127.0.0.1 -Port 4002``TcpTestSucceeded: True`
- [ ] API-Verbindung: Verbindungstest ausführen (Konto-ID, NetLiquidation, Positionen müssen
kommen). Hängt der Handshake > 10 s → Trusted IP fehlt (Punkt 3) oder Popup offen.
Erfolgreiche Ausgabe sieht so aus (Referenz vom 2026-07-31):
```
VERBUNDEN. ServerVersion=151, Konten: DUR371528
[2104] Verbindung zum Marktdatenzentrum ist OK: usfarm, eufarm, …
[2106] Verbindung zum HMDS-Datenzentrum ist OK: ushmds, euhmds, …
DUR371528 AccountType INDIVIDUAL
DUR371528 NetLiquidation 100047.93 EUR
DUR371528 TotalCashValue 100000.00 EUR
```
Die Meldungen 2104/2106/2158 sind **keine Fehler**, sondern Status-Infos („Datenzentrum OK”).
Dasselbe gilt für **10167** („nicht abonniert, verzögerte Marktdaten”) ohne Marktdaten-Abo ist das
bei jeder Kursanfrage der Normalfall, die Kurse kommen danach trotzdem.
## Troubleshooting
- **TCP-Verbindung wird angenommen, aber der API-Handshake bleibt ohne Antwort** (Client hängt
beim Verbinden): TWS wartet intern auf die Bestätigung eines Verbindungs-Popups — auch wenn
keines (mehr) sichtbar ist. Passiert, wenn die Trusted IP fehlte, das Popup unbemerkt verfiel
oder der Konfigurationsdialog offen war (blockiert als modaler Dialog die Popups).
**Lösung (am 2026-07-31 so verifiziert):** Konfigurationsdialog mit OK schließen, Trusted IP
`127.0.0.1` prüfen (Punkt 3), dann **TWS komplett neu starten** — das räumt hängengebliebene,
unbeantwortete Verbindungsanfragen ab. Danach verbindet sich der Client ohne Rückfrage.
Wichtig: Die Trusted IP allein reichte **nicht**, solange TWS noch die alten Anfragen hielt —
der Neustart war zwingend.
- **Nach dem TWS-Start kurz warten:** Direkt nach dem Login lehnt der Port die Verbindung noch ab
(„Verbindung verweigert“), danach folgt eine Phase mit abgebrochenem Handshake
(„Unable to read beyond the end of the stream“). Erst wenn TWS vollständig hochgefahren ist,
klappt der Login. Ein Client sollte deshalb **Reconnect mit Retry** machen statt einmalig zu
scheitern — relevant für `IbkrBrokerClient`.
- **`ClientId` muss je Verbindung eindeutig sein.** Eine zweite Verbindung mit derselben ID
verdrängt die erste. App nutzt `IBKRSettings.ClientId` (Default 1), Diagnose-Tools eine andere.
- Einstellungsänderungen im API-Dialog immer mit **Übernehmen/OK** abschließen; solange der
Dialog offen ist, gelten sie nicht.
## Unterschiede Live-Betrieb (später)
| Punkt | Paper | Live |
|--------------|------------------------|-----------------------------------------|
| Login-Modus | Paper Trading | Live Trading |
| Port | 4002 | 4001 |
| 2FA | nein | ja → IB Key, Weekly Re-Authentication |
| Marktdaten | geteilt vom Live-Konto | eigene Abos |
| App | `Trading.Mode=Paper` | `Trading.Mode=Live` + Port umstellen |
+241
View File
@@ -0,0 +1,241 @@
# 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.** `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.