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>
15 KiB
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):
- Put-Seite ist Cash-Secured Put, nicht Covered Put (kein Leerverkauf der Aktie).
- Vollautomatisch von Anfang an – Sicherungen sind Schalter und Limits, keine Klick-Freigabe.
- Strike-Wahl delta-basiert (Zielband 0,15–0,30).
- Bei drohender Zuteilung wird gerollt, solange das per Netto-Kredit möglich ist.
0. Leitprinzipien
- 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. - 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.
- Nur Watchlist. Das Modul handelt ausschließlich explizit eingetragene Ticker. Keine Entdeckung, kein Screening, keine Ausweitung zur Laufzeit.
- Jede Entscheidung ist nachlesbar. Kandidatenbewertung, Zustandsübergang und Order gehen über
core_decision_journal/core_order_eventsmit gemeinsamerSignalId– 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
reqMktDatamit Greeks (tickOptionComputation). IbkrMapping.Option(spec)→SecType="OPT",Exchange="SMART",LastTradeDateOrContractMonthalsyyyyMMdd,Strike,Right="C"/"P",Multiplier="100", plusTradingClass(nötig, sobald ein Underlying mehrere Klassen führt, z. B. nach Splits oder bei Mini-Optionen). Rein, unit-getestet – wie das bestehendeIbkrMapping.
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;PlaceOrderAsyncbleibt für den Aktienpfad erhalten (intern: submit + warten).- Dauerabo auf
orderStatus/openOrder/execDetailsstatt Slot-je-Order; einOrderTrackingService(IHostedService) trägt Endstatus nach, ruft dannRecordFillAsyncund schreibtcore_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 | 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 (
ProfitTargetPercentexistiert bereits inTradingSettings), 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:
OptionsWheel.Enabled– Modulschalter.- Watchlist als Whitelist – kein Ticker außerhalb.
- Naked-Sperre im Core-
RiskService(2.6) – wirkt auch, wenn die Modullogik falsch liegt. Haltedje Ticker bei unerklärter Positionsabweichung; Neueröffnungen stoppen, bestehende Legs werden weiter verwaltet.- 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
Optionsberechtigung im Paper-Konto DUR371528 prüfen→ erledigt am 2026-08-04, vorhanden. Verifiziert gegen das laufende Gateway (Details siehe IBKR-Integration.md, Abschnitt „Optionen"):reqSecDefOptParamsfür AAPL liefert 24 Verfallstermine, 127 Strikes, Multiplier 100 über SMART; eine What-If-Order aufAAPL 20260812 C302.5wurde von IBKR angenommen (Init-Margin 589,52) statt mit einem Berechtigungsfehler abgelehnt. Damit ist die Grundvoraussetzung für dieses Modul gegeben.- Greeks bei verzögerten Daten.
MarketDataType = 4liefert 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 – derStrikeSelectorwird von vornherein so geschnitten, dass beide Kriterien einsetzbar sind. - Marktdatenabo (OPRA) für Realtime-Optionskurse – Kosten/Notwendigkeit später entscheiden.
- Earnings-Sperre: keine neuen Legs über Quartalszahlen hinweg. Sinnvoll, aber es fehlt eine Datenquelle für Earnings-Termine – Punkt bewusst offen.
- Accounting-Anschluss: Optionsprämien, Zuteilungen und Abrufe müssen im
AccountingClassifiereigene Buchungskategorien bekommen; derRealizedPnlEngine(FIFO) kennt weder Multiplikator noch die Einstandsverschiebung durch Zuteilung. Eigene Arbeit im Accounting-Modul. - Steuer bleibt wie im Accounting-Konzept unberührt und offen. Keine Steuerberatung.