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>
This commit is contained in:
+11
-2
@@ -125,10 +125,19 @@ internal static class Program
|
|||||||
services.AddSingleton<IPortfolioService, PortfolioService>();
|
services.AddSingleton<IPortfolioService, PortfolioService>();
|
||||||
services.AddSingleton<IExecutionService, ExecutionService>();
|
services.AddSingleton<IExecutionService, ExecutionService>();
|
||||||
// Echter TWS-Broker nur, wenn ausdrücklich aktiviert – sonst der NullBroker, der nie handelt.
|
// Echter TWS-Broker nur, wenn ausdrücklich aktiviert – sonst der NullBroker, der nie handelt.
|
||||||
|
// Beide Rollen (Handel + lesender Bestandsabgleich) bedient dieselbe Instanz.
|
||||||
if (settingsService.Settings.IBKR.UseTwsApi)
|
if (settingsService.Settings.IBKR.UseTwsApi)
|
||||||
services.AddSingleton<IBrokerClient, IbkrBrokerClient>();
|
{
|
||||||
|
services.AddSingleton<IbkrBrokerClient>();
|
||||||
|
services.AddSingleton<IBrokerClient>(sp => sp.GetRequiredService<IbkrBrokerClient>());
|
||||||
|
services.AddSingleton<IBrokerPortfolioReader>(sp => sp.GetRequiredService<IbkrBrokerClient>());
|
||||||
|
}
|
||||||
else
|
else
|
||||||
services.AddSingleton<IBrokerClient, NullBrokerClient>();
|
{
|
||||||
|
services.AddSingleton<NullBrokerClient>();
|
||||||
|
services.AddSingleton<IBrokerClient>(sp => sp.GetRequiredService<NullBrokerClient>());
|
||||||
|
services.AddSingleton<IBrokerPortfolioReader>(sp => sp.GetRequiredService<NullBrokerClient>());
|
||||||
|
}
|
||||||
|
|
||||||
// Core-Worker/Services
|
// Core-Worker/Services
|
||||||
services.AddSingleton<BackupWorker>();
|
services.AddSingleton<BackupWorker>();
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ Pin `new MariaDbServerVersion(new Version(11, 8, 6))`. Verbindung aus `appsettin
|
|||||||
- [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] **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] 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**
|
- [x] Orderpfad bis zur Broker-Annahme per **What-If-Order** verifiziert (Aktie + Option, keine Ausführung); **Optionsberechtigung im Paper-Konto bestätigt**
|
||||||
|
- [x] **`IBrokerPortfolioReader`** (Bestand + Ausführungen beim Broker) – eigener Seam neben `IBrokerClient`, Grundlage für den Abgleich der eigenen Buchführung; gegen DUR371528 verifiziert (2 Positionen, 2 Ausführungen inkl. Kommissionen)
|
||||||
- [ ] **Offen:** `PlaceOrderAsync` mit echter Ausführung verifizieren (Fill → Buchung); asynchrone Fill-Verfolgung (Orders ohne sofortige Ausführung)
|
- [ ] **Offen:** `PlaceOrderAsync` mit echter Ausführung verifizieren (Fill → Buchung); asynchrone Fill-Verfolgung (Orders ohne sofortige Ausführung)
|
||||||
- [ ] IBKR-Account-Credentials mit `EncryptedStringConverter` speichern
|
- [ ] IBKR-Account-Credentials mit `EncryptedStringConverter` speichern
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,11 @@ kommen verzögerte Daten (siehe Marktdaten unten). Modul-Konzept:
|
|||||||
> **What-If als Testwerkzeug:** Damit lässt sich der gesamte Orderpfad bis zur Broker-Annahme prüfen,
|
> **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
|
> 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.
|
> (Margin-Deckung vor einer echten Order) wäre es aber ein naheliegender Ausbau.
|
||||||
|
Welche Daten die API auf diesem Konto tatsächlich liefert – und welche Strategien das trägt –
|
||||||
|
steht gemessen in [konzepte/KONZEPT-Datenlage-und-Strategien.md](konzepte/KONZEPT-Datenlage-und-Strategien.md).
|
||||||
|
Kurz: Kurshistorie (30 Jahre), Volatilitätshistorie, Optionsketten und Griechen ja;
|
||||||
|
Fundamentaldaten und Marktscanner nein (Abo nötig).
|
||||||
|
|
||||||
### Historie: welcher Gateway?
|
### Historie: welcher Gateway?
|
||||||
Aktuell laufen die Marktdaten-Worker über die **Client Portal Web API** (`IBKRGatewayService`).
|
Aktuell laufen die Marktdaten-Worker über die **Client Portal Web API** (`IBKRGatewayService`).
|
||||||
Später zu entscheiden:
|
Später zu entscheiden:
|
||||||
|
|||||||
@@ -0,0 +1,475 @@
|
|||||||
|
# Kapital- und Buchmodell
|
||||||
|
|
||||||
|
> **Status: Konzept (2026-08-03) – noch nicht implementiert.**
|
||||||
|
> Dieses Dokument beschreibt, wie mehrere Strategien gleichzeitig auf *einem* IBKR-Konto mit *einem*
|
||||||
|
> Guthaben arbeiten können, ohne sich gegenseitig zu stören, und wie das gegen die steuerliche
|
||||||
|
> Buchführung abgegrenzt ist. Es dient als Referenz, gegen die die spätere Umsetzung geprüft wird.
|
||||||
|
|
||||||
|
## 0. Problemstellung und Abgrenzung
|
||||||
|
|
||||||
|
TWS lässt sich pro Rechner nur einmal betreiben – wir sind an **ein** Konto mit **einem** Guthaben
|
||||||
|
gebunden. Trotzdem sollen mehrere Strategie-Module parallel handeln. Daraus folgen vier Anforderungen:
|
||||||
|
|
||||||
|
1. Jedes Modul bekommt eine **klar begrenzte Kapitalmenge**, die es binden darf.
|
||||||
|
2. Jede Position hat einen **eindeutigen Eigentümer**. Kein Modul darf die Position eines anderen
|
||||||
|
Moduls oder eine manuell angelegte Position anfassen.
|
||||||
|
3. Das Konto darf durch Modul-Handel **niemals ins Minus oder in Margin** laufen.
|
||||||
|
4. Die **steuerliche Buchführung** ist davon vollständig unabhängig und muss zu 100 % stimmen.
|
||||||
|
|
||||||
|
Nicht Gegenstand dieses Dokuments: Strategielogik, Signalerzeugung, Marktdatenversorgung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Die drei Wahrheiten
|
||||||
|
|
||||||
|
Es gibt drei Datenquellen mit unterschiedlicher Autorität. Sie dürfen sich nicht vermischen.
|
||||||
|
|
||||||
|
| Ebene | Quelle | Autoritativ für | Latenz |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Steuerbuch** (`acc_`) | Flex Query | Geld, Steuer, GuV – alles gegenüber der Steuerbehörde | T+1 |
|
||||||
|
| **Depot-Ist** | TWS-API-Snapshot | Was *jetzt* real im Konto liegt | Sekunden |
|
||||||
|
| **Managementbuch** (`core_`) | eigene Order-Events + Abgleich | Zuordnung von Positionen zu Büchern | live |
|
||||||
|
|
||||||
|
**Harte Regel:** Das Managementbuch beeinflusst das Steuerbuch **niemals** – weder korrigierend noch
|
||||||
|
ergänzend. Der Accounting-Ingest liest Flex Query, klassifiziert und bucht; ob dabei eine `BookId`
|
||||||
|
bekannt ist, ist ihm gleichgültig. Die Modulzuordnung ist eine *optionale, nicht-autoritative*
|
||||||
|
Beistelltabelle (Flex-Trade-ID ↔ BookId). Fehlt sie oder ist sie falsch, ändert sich am Steuerergebnis
|
||||||
|
exakt nichts.
|
||||||
|
|
||||||
|
Umgekehrt gilt: Das Managementbuch **bezieht** Korrekturen aus dem Steuerbuch (Gebühren, Dividenden,
|
||||||
|
Splits), nie andersherum.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Bücher (Books)
|
||||||
|
|
||||||
|
Ein **Buch** ist ein Kapitaltopf mit zugeordneten Positionen. Bücher sind:
|
||||||
|
|
||||||
|
| Buch | Bedeutung | Handelt | Limits gelten |
|
||||||
|
|---|---|---|---|
|
||||||
|
| je Strategie (`CT`, …) | ein Modul, ggf. ein Parametersatz | ja | ja |
|
||||||
|
| `MANUAL` | von Hand in TWS/IBKR angelegt | nein (nur extern) | **nein**, zählt aber mit |
|
||||||
|
| `UNASSIGNED` | am Broker gefunden, keinem Buch zugeordnet | nein | zählt mit |
|
||||||
|
| `HOUSE` | nicht allokierte Reserve, FX, Ausgleichsposten | nein | – |
|
||||||
|
|
||||||
|
**`BookId` ist ein eigenes Konzept, nicht der Modulname.** Default `BookId == Modulname`, aber die
|
||||||
|
Trennung erlaubt später mehrere Bücher desselben Moduls und macht `MANUAL`/`UNASSIGNED`/`HOUSE`
|
||||||
|
sauber modellierbar. Jetzt kostenlos, später teuer nachzurüsten.
|
||||||
|
|
||||||
|
### Eigentumsregeln (im Core erzwungen, nicht per Konvention)
|
||||||
|
|
||||||
|
1. Ein Verkaufssignal von Buch *b* kann **nur Positionen von *b*** reduzieren, gedeckelt auf
|
||||||
|
`Menge(b, Symbol)`. Darüber hinaus wird gekappt – **niemals** wird ein Short erzeugt.
|
||||||
|
2. `MANUAL` und `UNASSIGNED` senden keine Signale und empfangen keine.
|
||||||
|
3. Ein Modul kann seine `BookId` **nicht selbst wählen**. Sie stammt aus der Modul-Registrierung und
|
||||||
|
wird serverseitig gesetzt; `TradeSignal.SourceModule` aus dem Modul ist nicht vertrauenswürdig.
|
||||||
|
4. Module bekommen `IBrokerClient` **nicht** per DI – ausschließlich `IExecutionService`.
|
||||||
|
Sonst ist die gesamte Buchführung umgehbar.
|
||||||
|
5. Ein Kill-Switch/Supervisor darf alles liquidieren – als explizite Core-Autorität mit eigener
|
||||||
|
Konfiguration, standardmäßig unter Ausschluss von `MANUAL`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Kapitalmodell: ein Pool, Obergrenzen
|
||||||
|
|
||||||
|
**Es gibt keinen Kapitaltransfer zwischen Büchern.** Kein Buch-Cash, keine Rebalancing-Läufe, keine
|
||||||
|
Ausgleichsforderungen. Stattdessen ein gemeinsamer Cash-Pool und pro Buch eine Obergrenze:
|
||||||
|
|
||||||
|
```
|
||||||
|
Verteilbar = FreiesCash(gehaircuttet) + Σ Modul-Positionswert − HouseReserve
|
||||||
|
|
||||||
|
Cap(b) = konfigurierter Anteil × Verteilbar (Default: Gleichverteilung)
|
||||||
|
Gebunden(b) = Marktwert der Positionen von b + offene Reservierungen von b
|
||||||
|
Headroom(b) = min( Cap(b) − Gebunden(b), freies Pool-Cash )
|
||||||
|
```
|
||||||
|
|
||||||
|
### Eigenschaften, die bewusst so sind
|
||||||
|
|
||||||
|
- **Gewinne verteilen sich von selbst.** Realisiert ein Buch einen Gewinn, wächst der gemeinsame Pool;
|
||||||
|
da alle Caps aus `Verteilbar` abgeleitet werden, steigt der Headroom **aller** Bücher. Diversifikation
|
||||||
|
ohne einen einzigen Zwangsverkauf.
|
||||||
|
- **Ein Buch darf über seinem Cap liegen.** Steigen die Positionen im Wert, passiert nichts – außer
|
||||||
|
dass nicht mehr nachgekauft werden darf. Der Zustand „über Cap" ist **legal**, wird angezeigt und
|
||||||
|
gemeldet, aber **nie geheilt**. Es wird niemals eine Position geschlossen, nur um eine Zielquote
|
||||||
|
herzustellen.
|
||||||
|
- **`Σ Caps ≤ 100 % − Reserve` wird beim Start validiert.** Sonst könnte ein schnelles Buch ein
|
||||||
|
langsames aushungern („wer zuerst kommt").
|
||||||
|
- **Die Modul-Welt sieht nie den NAV**, sondern nur ihr eigenes Kapital plus freies Cash. Manuelle
|
||||||
|
Positionen – auch kreditfinanzierte – sind eine Black Box, die nur über das freie Cash wirkt.
|
||||||
|
|
||||||
|
### Kapitalführung ≠ Performance-Messung
|
||||||
|
|
||||||
|
Die Kapitalführung ist ein Pool plus Caps. Die GuV **pro Buch** ist eine reine *Auswertung* über die
|
||||||
|
Trade-Historie (für Supervisor und Reporting) und steuert nichts. Diese Entkopplung ist beabsichtigt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Keine Margin – die Sperre
|
||||||
|
|
||||||
|
**Modul-Handel ist strikt cash-only. Manueller Handel darf Margin nutzen.** Der Kontotyp ist
|
||||||
|
Cash-Konto; brokerseitig kann trotzdem Margin verfügbar sein, deshalb ist die Software-Sperre
|
||||||
|
zwingend und testpflichtig.
|
||||||
|
|
||||||
|
### Schicht A – richtige Bemessungsgrundlage
|
||||||
|
|
||||||
|
`BuyingPower` und `AvailableFunds` sind auf einem Margin-Konto bereits gehebelt gerechnet und als
|
||||||
|
Basis unbrauchbar. Maßgeblich ist:
|
||||||
|
|
||||||
|
```
|
||||||
|
FreiesCash = min(TotalCashValue, AvailableFunds) − HouseReserve − Σ offene Reservierungen
|
||||||
|
```
|
||||||
|
|
||||||
|
`TotalCashValue` ist das echte Bargeld. Eröffnest du manuell eine Margin-Position, sinkt es, und die
|
||||||
|
Modul-Kaufkraft schrumpft automatisch mit. Bei `TotalCashValue ≤ 0` ist die Modul-Kaufkraft null –
|
||||||
|
ohne Sonderlogik. Module können einen manuellen Kredit strukturell nicht ausweiten.
|
||||||
|
|
||||||
|
### Schicht B – kein Short
|
||||||
|
|
||||||
|
Verkäufe werden auf die Buchmenge gekappt. Shorts sind implizit Margin.
|
||||||
|
|
||||||
|
### Schicht C – Währung
|
||||||
|
|
||||||
|
Ein Kauf in Währung X darf **nur aus Cash in Währung X** finanziert werden. Andernfalls entsteht ein
|
||||||
|
Sollsaldo in X – ein Margin-Kredit, auch wenn das Konto in Summe positiv aussieht. Buch-Cash und
|
||||||
|
Reservierungen werden deshalb **pro Währung** geführt (siehe Abschnitt 7).
|
||||||
|
|
||||||
|
### Schicht D – Watchdog
|
||||||
|
|
||||||
|
Ein Hosted Service prüft Cash und Margin-Kennzahlen laufend. Bei Unterschreitung: **globaler
|
||||||
|
Kauf-Stopp + Alarm** – und **niemals automatisches Verkaufen** zur Heilung. Ein
|
||||||
|
Selbstheilungs-Amoklauf ist gefährlicher als der Zustand, den er beheben soll.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Reservierungen (Order-Lebenszyklus)
|
||||||
|
|
||||||
|
Verhindert, dass zwei Module gleichzeitig dasselbe Geld ausgeben. Persistent in der DB, nicht im RAM.
|
||||||
|
|
||||||
|
```
|
||||||
|
INTENT → RESERVED → SUBMITTED → FILLED | PARTIAL | CANCELLED | REJECTED | EXPIRED
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Reservierungshöhe:** `Menge × Referenzpreis × (1 + Puffer)` + geschätzte Kommission.
|
||||||
|
Puffer klein bei Limit-Orders (das Limit *ist* die Obergrenze), größer bei Market (Slippage),
|
||||||
|
zusätzlich FX-Puffer bei währungsübergreifenden Vorgängen.
|
||||||
|
- **Atomar** gegen Buch-Headroom *und* Pool-Cash, in einer DB-Transaktion hinter einem **globalen
|
||||||
|
Order-Lock**. Bei einer TWS-Verbindung reicht ein einfacher `SemaphoreSlim`; feingranulare Locks
|
||||||
|
wären hier nur eine zusätzliche Fehlerquelle.
|
||||||
|
- **Freigabe** beim Terminalzustand; Differenz zwischen Reservierung und echtem Fill fließt zurück.
|
||||||
|
- **TTL zwingend.** Erreicht eine Reservierung ohne Terminalzustand ihr Ablaufdatum oder den
|
||||||
|
Handelsschluss → **Alarm, keine stille Freigabe**. Eine hängende Order, deren Reservierung
|
||||||
|
freigegeben wird, gibt das Geld zweimal aus.
|
||||||
|
- **Idempotenz** über `SignalId` bzw. eine Client-Order-ID, damit ein Reconnect keine Doppelorder
|
||||||
|
erzeugt.
|
||||||
|
- **Crash-Recovery:** beim Start alle nicht-terminalen Reservierungen gegen die offenen Broker-Orders
|
||||||
|
abgleichen; Waisen → Quarantäne + Alarm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Limits und Feedback an die Module
|
||||||
|
|
||||||
|
### Konzentrationslimit pro Einzelwert
|
||||||
|
|
||||||
|
```
|
||||||
|
SymbolExposure(s) = Σ_b Marktwert(b, s) ← über ALLE Bücher, inkl. MANUAL
|
||||||
|
Prüfung: SymbolExposure(s) + geplanter Kauf ≤ MaxSymbolPercent × NAV
|
||||||
|
```
|
||||||
|
|
||||||
|
- **NAV als Nenner**, nicht der Positionswert – sonst wird das Limit bei viel Cash absurd eng und bei
|
||||||
|
wenig Cash absurd weit.
|
||||||
|
- **`MANUAL` ist von der Prüfung befreit, zählt aber voll in den Zähler.** Hältst du 50 % AAPL von
|
||||||
|
Hand, sind die Module bei AAPL vollständig gesperrt. So gewollt.
|
||||||
|
- **Verkäufe sind nie limitiert.** Ein Limit darf niemals das Schließen einer Position verhindern.
|
||||||
|
|
||||||
|
### Feedback-Kontrakt
|
||||||
|
|
||||||
|
Das Gate ist **hart**. Das Modul entscheidet nur, *wie es auf die Ablehnung reagiert* – nicht, ob das
|
||||||
|
Limit gilt. Ein erneut gesendetes zu großes Signal wird wieder abgelehnt. Der Core trimmt nicht
|
||||||
|
selbstständig; die Entscheidung liegt auf Strategieebene.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
record LimitBreach(
|
||||||
|
LimitKind Kind, // SymbolConcentration | BookCap | CashAvailable
|
||||||
|
// | FxBuffer | CurrencyNotAllowed | VenueNotAllowed
|
||||||
|
string Scope, // "AAPL" | "CT" | "USD"
|
||||||
|
decimal Current,
|
||||||
|
decimal Limit,
|
||||||
|
int MaxQuantity, // was jetzt noch ginge
|
||||||
|
decimal MaxNotional);
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergänzend eine Vorab-Query `GetHeadroom(book, symbol)`, damit ein gut gebautes Modul gar nicht erst
|
||||||
|
gegen die Wand fährt und seine Order gleich richtig dimensioniert.
|
||||||
|
|
||||||
|
**Drossel:** Ein Modul, das dasselbe Signal wiederholt gegen dieselbe Wand schickt, wird nach N
|
||||||
|
Versuchen pro `SignalId` gedämpft und protokolliert. Sonst produziert eine schlecht geschriebene
|
||||||
|
Strategie Logfluten und TWS-Last.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Währungen und Handelsplätze
|
||||||
|
|
||||||
|
**Kontobasiswährung:** konfigurierbar, bei Ersteinrichtung festgelegt, danach gesperrt
|
||||||
|
(siehe Abschnitt 11 – eine Änderung nach dem ersten Trade macht alle historischen Bewertungen,
|
||||||
|
Caps und GuV-Zahlen ungültig).
|
||||||
|
|
||||||
|
- Produktion (US LLC): `USD`
|
||||||
|
- Test/Paper (EU): `EUR`
|
||||||
|
|
||||||
|
### Contract-Auflösung ist Pflicht
|
||||||
|
|
||||||
|
Eine Order mit nacktem Symbol-String über `SMART` kann bei mehrdeutigen Tickern still an einer Börse
|
||||||
|
in einer anderen Währung landen. Deshalb:
|
||||||
|
|
||||||
|
- Vor jeder Order wird der Contract über `reqContractDetails` aufgelöst; `currency`, `exchange`,
|
||||||
|
`primaryExchange` und `conId` werden geprüft.
|
||||||
|
- **Whitelist auf zwei Achsen:** Währung ∈ `AllowedCurrencies` **und** Handelsplatz ∈ konfigurierter
|
||||||
|
Liste. Verstoß → harte Ablehnung, unabhängig vom Modul.
|
||||||
|
- Das Ergebnis wird gecacht; **die `conId` wird in der Position gespeichert.** Sie ist IBKRs
|
||||||
|
eindeutige Instrumenten-ID und deutlich sicherer als ein Ticker.
|
||||||
|
|
||||||
|
### FX-Marge als Sicherheitspuffer, nicht als Exposure-Deckel
|
||||||
|
|
||||||
|
Bei überwiegendem Handel in Kontobasiswährung wäre ein FX-Exposure-Limit meist verletzt und damit
|
||||||
|
nutzlos. Sinnvoll wirkt die Marge an drei anderen Stellen:
|
||||||
|
|
||||||
|
1. **Bewertungs-Haircut:** Fremdwährungs-Cash geht mit Abschlag in `Verteilbar` ein → das eigene
|
||||||
|
Kapital wird nie überschätzt.
|
||||||
|
2. **Reservierungspuffer** bei währungsübergreifenden Käufen, zusätzlich zum Slippage-Puffer.
|
||||||
|
3. **FX-Exposure als Kennzahl mit Alarmschwelle** – sichtbar und meldepflichtig, ohne Handelssperre.
|
||||||
|
|
||||||
|
### Kein Auto-FX im Order-Pfad
|
||||||
|
|
||||||
|
Fehlt Cash in der Zielwährung, scheitert der Modul-Kauf mit `LimitKind.CashAvailable`. Konvertiert
|
||||||
|
würde der Order-Pfad selbst, könnte ein Modul indirekt FX-Kosten und FX-Timing auslösen, und die
|
||||||
|
Reservierungslogik müsste zwei Währungen gleichzeitig sperren. FX bleibt ein expliziter Vorgang
|
||||||
|
auf `HOUSE`-Ebene.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Abgleich (Reconciliation)
|
||||||
|
|
||||||
|
Weil jederzeit manuell in TWS eingegriffen werden kann, ist der Abgleich kein Zusatz, sondern das
|
||||||
|
Fundament. Zwei Stufen:
|
||||||
|
|
||||||
|
**Stufe 1 – Ledger ↔ TWS (minütlich):** Handelssicherheit. Stimmen Stückzahlen und Cash?
|
||||||
|
|
||||||
|
| Befund | Bedeutung | Reaktion |
|
||||||
|
|---|---|---|
|
||||||
|
| Broker > Ledger | manuell gekauft, oder ein Fill kam nach | Überschuss → `UNASSIGNED` |
|
||||||
|
| Broker < Ledger | manuell verkauft / Corporate Action | **Break:** betroffene Bücher und Symbol für neue Orders sperren, Alarm |
|
||||||
|
| Cash weicht ab | Gebühren, Zinsen, FX | Differenz gegen `HOUSE`, ab Schwelle Break |
|
||||||
|
|
||||||
|
Bei „Broker < Ledger" wird bewusst **nicht** automatisch korrigiert. Anteiliges Wegkürzen zerstört
|
||||||
|
die Zurechenbarkeit – Halt-and-alert, der Mensch entscheidet.
|
||||||
|
|
||||||
|
**Stufe 2 – Ledger ↔ Flex Query (täglich, nach Ingest):** Qualitätssicherung. Hier kommen Gebühren,
|
||||||
|
Dividenden, Quellensteuer, FX-Differenzen und Splits an – Dinge, die die TWS-API gar nicht oder
|
||||||
|
schlecht liefert.
|
||||||
|
|
||||||
|
**Gebühren gehören dem verursachenden Buch.** Sonst subventioniert `HOUSE` die vieltradenden Module
|
||||||
|
und die Performance-Zahlen lügen. Beim Fill wird geschätzt, beim Flex-Abgleich exakt nachjustiert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Unzugeordnete Positionen und Eskalation
|
||||||
|
|
||||||
|
Zustände: `New → Notified → Acknowledged → Assigned(Book)`, mit `DetectedAt`-Zeitstempel.
|
||||||
|
|
||||||
|
- Ein Watchdog meldet alles, was **länger als 30 Minuten** unbearbeitet liegt.
|
||||||
|
- **Backoff:** nach 30 min, dann 2 h, dann täglich – sonst wird der Kanal unbrauchbar.
|
||||||
|
- **Abgestufte Sperrwirkung:** Eine `UNASSIGNED`-Position **zählt voll** in Exposure und
|
||||||
|
Konzentrationslimit, bremst die Module also automatisch, **sperrt aber nicht hart**. Hart gesperrt
|
||||||
|
wird nur der gefährliche Fall aus Abschnitt 8 (Broker < Ledger).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Benachrichtigungen
|
||||||
|
|
||||||
|
Im Code existiert derzeit **kein** Benachrichtigungssystem (die Treffer zu „Notification" im
|
||||||
|
Supervisor-MCP sind JSON-RPC-Notifications, etwas anderes). Sauberer Start.
|
||||||
|
|
||||||
|
```
|
||||||
|
INotificationSink Name, SendAsync(Notification, ct)
|
||||||
|
Notification Severity, Category, Title, Body, DedupKey, Data
|
||||||
|
NotificationService Fan-out über alle registrierten Sinks
|
||||||
|
```
|
||||||
|
|
||||||
|
Drei Eigenschaften müssen von Anfang an drin sein, weil sie später schwer nachzurüsten sind:
|
||||||
|
|
||||||
|
- **Outbox in der DB** (`core_notification`): erst persistieren, dann zustellen, Retry bei Fehler.
|
||||||
|
Solange kein Zielkanal existiert, sammeln sich Meldungen sichtbar an und werden zugestellt, sobald
|
||||||
|
ein Sink da ist. Nichts geht verloren.
|
||||||
|
- **Dedup/Throttling** über `DedupKey` (z. B. `unassigned:AAPL`).
|
||||||
|
- **Severity-Routing:** Info → Log/UI, Warning → Chat, Critical → alle Kanäle.
|
||||||
|
|
||||||
|
Ausbaureihenfolge: `LogSink` + `UiSink` sofort → `MatrixSink`, sobald der Server steht →
|
||||||
|
`TelegramSink` optional als Backup.
|
||||||
|
|
||||||
|
**Matrix** ist reines HTTP, keine Library nötig:
|
||||||
|
`PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId}` mit dem Access-Token eines
|
||||||
|
Bot-Users. Die `txnId` ist Matrix' eingebauter Idempotenz-Schlüssel und passt exakt auf die
|
||||||
|
Outbox-ID. Das Token gehört in `SecretProtection`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Steuerbuch: Jurisdiktionsprofil
|
||||||
|
|
||||||
|
**Pro Deployment gibt es genau eine Jurisdiktion, und sie wechselt nie.** Ein Kontowechsel zwischen
|
||||||
|
Ländern findet nicht statt – die Option „später auch in Deutschland" bedeutet ausdrücklich: anderer
|
||||||
|
Server, andere Datenbank, anderes IBKR-Konto. Daraus folgt: keine Migration, keine
|
||||||
|
Mehrmandantenfähigkeit, kein Umschalten zur Laufzeit.
|
||||||
|
|
||||||
|
Das Profil ist **kein Währungs-Flag**, sondern ein Austausch der gesamten Lot- und
|
||||||
|
Klassifikationslogik:
|
||||||
|
|
||||||
|
| Aspekt | US LLC (Hauptziel) | DE privat (Option) | DE GmbH (Option) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Berichtswährung | USD, keine Umrechnung | EUR, Umrechnung pro Vorgang | EUR, Umrechnung pro Vorgang |
|
||||||
|
| Lot-Matching | FIFO Standard, Specific ID wählbar | FIFO zwingend | Bilanzierung |
|
||||||
|
| Wash Sale (30 Tage) | **ja**, Basis-Anpassung | kein Äquivalent | kein Äquivalent |
|
||||||
|
| Haltefrist | short/long term ab 1 Jahr | irrelevant | irrelevant |
|
||||||
|
| Verlustverrechnung | Carryforward mit Deckel | getrennte Töpfe | wieder anders |
|
||||||
|
| Quellensteuer | – | Anrechnung nach DBA | Anrechnung |
|
||||||
|
| Ausgabe | 1099-B-Abgleich / K-1 | Anlage KAP | Bilanz/GuV |
|
||||||
|
|
||||||
|
Die **Wash-Sale-Regel** ist die einschneidendste: sie verändert Anschaffungskosten rückwirkend. Eine
|
||||||
|
Engine, die nur FIFO kann, lässt sich dafür nicht nachrüsten. Deshalb muss die Lot-Verwaltung von
|
||||||
|
Anfang an lot-basiert sein und **Basis-Anpassungen als eigene Buchungsart** kennen – auch wenn das
|
||||||
|
erste Profil sie noch nicht nutzt.
|
||||||
|
|
||||||
|
> Die konkreten steuerlichen Ausprägungen (u. a. ob die LLC als Disregarded Entity oder Partnership
|
||||||
|
> behandelt wird, was das Reporting ändert) sind mit dem Steuerberater abzustimmen. Hier steht nur,
|
||||||
|
> was die Software mechanisch abbilden können muss.
|
||||||
|
|
||||||
|
### Profile und Umgebungen
|
||||||
|
|
||||||
|
```
|
||||||
|
Test/Paper (EU) BaseCurrency EUR TaxProfile None
|
||||||
|
Produktion (US) BaseCurrency USD TaxProfile UsLlc
|
||||||
|
Option DE BaseCurrency EUR TaxProfile DePrivat | DeGmbh
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Profil **`None`/`Paper`** führt vollständig Buch, rechnet aber keine Steuer und ist in UI und
|
||||||
|
allen Exporten sichtbar als solches markiert. Deutsche Steuerregeln auf Paper-Trades zu rechnen wäre
|
||||||
|
sinnlos und produziert Zahlen, die jemand später für echt halten könnte.
|
||||||
|
|
||||||
|
### Wo der Lock hingehört
|
||||||
|
|
||||||
|
**Nicht in `appsettings.json`.** Eine Datei lässt sich editieren, und dann rechnet die Engine ab
|
||||||
|
morgen nach anderen Regeln über denselben Bestand – der schlimmstmögliche Fehler, weil er sich still
|
||||||
|
auswirkt.
|
||||||
|
|
||||||
|
Der Lock gehört **in die Datenbank, neben die Daten, die er regiert**: eine Stempelzeile im
|
||||||
|
Accounting-Schema mit Jurisdiktion, Berichtswährung, Kontobasiswährung und Einrichtungszeitpunkt.
|
||||||
|
Beim Start wird die Konfiguration dagegen geprüft; bei Abweichung startet das Accounting-Modul nicht,
|
||||||
|
sondern meldet einen Konflikt. Gesetzt wird der Stempel bei der Ersteinrichtung, geändert nur durch
|
||||||
|
eine leere Datenbank.
|
||||||
|
|
||||||
|
### Was deshalb *nicht* gebaut wird
|
||||||
|
|
||||||
|
Die EZB-Kursanbindung (Kursquelle, Stichtagsregeln, Kursarchivierung) ist für das Hauptziel nicht
|
||||||
|
nötig: USD-Konto, USD-Reporting, keine Umrechnung. Also nur die **Naht** bauen (`IFxRateSource`, im
|
||||||
|
US-Profil eine No-Op), nicht die Implementierung. Wird die DE-Option je gezogen, geschieht das
|
||||||
|
ohnehin auf einem anderen Server – dann wird sie dort ergänzt.
|
||||||
|
|
||||||
|
Falls doch: EZB-Referenzkurse autoritativ, IBKR-Bewertungskurse nur als Kontrollgröße; **verwendeter
|
||||||
|
Kurs, Quelle, Stichtag und Abrufzeitpunkt werden mitgespeichert**, nicht nur der umgerechnete Betrag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Datenmodell-Deltas
|
||||||
|
|
||||||
|
| Heute | Delta |
|
||||||
|
|---|---|
|
||||||
|
| `core_position` (PK `Module`+`Symbol`) | `Module` → `BookId`; **`ConId`, `Currency`, `Exchange` ergänzen** |
|
||||||
|
| `core_budget` (Total/Used/MaxPerTrade) | ersetzen durch `core_book`: Cap-Regel, Flags (ReadOnly, Active), Mindestordergröße |
|
||||||
|
| – | `core_reservation` (Zustandsmaschine, TTL, Währung) |
|
||||||
|
| – | `core_broker_snapshot` + `core_reconciliation_break` |
|
||||||
|
| – | `core_notification` (Outbox) |
|
||||||
|
| – | `core_instrument` (aufgelöste Contracts, `conId`-Cache, Whitelist-Status) |
|
||||||
|
| – | `acc_jurisdiction` (Stempel, Abschnitt 11) |
|
||||||
|
| ~~Buch-Cash, Transfers, Rebalance-Lauf~~ | **entfällt** – ein Pool plus Caps (Abschnitt 3) |
|
||||||
|
| `RiskContext.NetLiquidation` | → `Verteilbar` und `Headroom(b)`, **nicht** NAV |
|
||||||
|
| `IbkrConnection`: `NetLiquidation,AvailableFunds` | + `TotalCashValue`, `SettledCash`, Margin-Kennzahlen, **pro Währung** |
|
||||||
|
| `IBrokerClient` | + `GetPositionsAsync`, + `ResolveContractAsync` |
|
||||||
|
| `ExecutionService` (Check-then-Act ohne Lock) | Order-Gateway davor: Serialisierung + Reservierung |
|
||||||
|
| `ExecutionResult.Reason` (string) | + strukturiertes `LimitBreach` (Abschnitt 6) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Invarianten und Testkatalog
|
||||||
|
|
||||||
|
Die No-Margin-Bedingung wird als **eine ausführbare Invariante** formuliert, die an drei Stellen
|
||||||
|
dieselbe Codebahn nimmt:
|
||||||
|
|
||||||
|
```
|
||||||
|
∀ Währung c: FreiesCash(c) ≥ 0
|
||||||
|
∧ Σ offene Reservierungen(c) ≤ FreiesCash(c)
|
||||||
|
∀ Buch b, Symbol s: Menge(b, s) ≥ 0
|
||||||
|
∀ Symbol s: Σ_b Menge(b, s) = BrokerMenge(s)
|
||||||
|
```
|
||||||
|
|
||||||
|
1. als Assert am Ende **jedes** Trading-Tests (gemeinsamer Testhelfer, nicht pro Test neu geschrieben)
|
||||||
|
2. als Property-Test über zufällige Sequenzen aus Reservierung / Fill / Teilfill / Cancel / Slippage /
|
||||||
|
manuellem Eingriff
|
||||||
|
3. als Laufzeitprüfung im Watchdog – **identische Implementierung**, damit Test und Produktion nicht
|
||||||
|
auseinanderlaufen
|
||||||
|
|
||||||
|
### Pflichtfälle
|
||||||
|
|
||||||
|
| Fall | Erwartung |
|
||||||
|
|---|---|
|
||||||
|
| Zwei parallele Reservierungen, zusammen > freies Cash | zweite abgelehnt |
|
||||||
|
| N nebenläufige Tasks (Stresstest) | Σ Reservierungen nie > freies Cash |
|
||||||
|
| Fill teurer als reserviert, innerhalb Puffer | ok, Restfreigabe korrekt |
|
||||||
|
| Fill jenseits des Puffers | Alarm, Cash bleibt ≥ 0 |
|
||||||
|
| Manuelle Margin-Position taucht auf, Cash < Reserve | alle Modul-Käufe gesperrt |
|
||||||
|
| Cash in Zielwährung = 0, andere Währung vorhanden | abgelehnt – **kein** stiller Fremdwährungskredit |
|
||||||
|
| Verkaufssignal über Buchmenge hinaus | gekappt, kein Short |
|
||||||
|
| Reservierung erreicht TTL | Alarm, **keine** stille Freigabe |
|
||||||
|
| Neustart mit offenen Reservierungen | aus DB rekonstruiert, keine Doppelausgabe |
|
||||||
|
| Modul setzt fremde `BookId` im Signal | serverseitig überschrieben |
|
||||||
|
| Verkaufssignal gegen `MANUAL` | abgelehnt |
|
||||||
|
| Contract löst auf fremde Währung/Börse auf | abgelehnt vor Ordersendung |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Bewusst verschoben
|
||||||
|
|
||||||
|
- **Self-Cross-Netting** (Modul A kauft, Modul B verkauft dasselbe Symbol gleichzeitig). Bei der
|
||||||
|
erwarteten Handelsfrequenz unrealistisch und allenfalls eine Ausnahmeerscheinung.
|
||||||
|
**Billige Vorstufe jetzt:** Ist im Order-Gateway eine gegenläufige Order für dasselbe Symbol
|
||||||
|
pending, wird das protokolliert und gewarnt. Der Lock ist ohnehin da – das kostet fast nichts und
|
||||||
|
liefert Daten darüber, ob das Problem je real wird.
|
||||||
|
- **Corporate Actions** (Splits, Spin-offs) in der Buchzuordnung – vorerst über den Flex-Abgleich als
|
||||||
|
Break sichtbar, manuelle Zuordnung.
|
||||||
|
- **Echte Sub-Accounts** bei IBKR (Advisor/Family-Struktur) als brokerseitige Trennung. Erwogen und
|
||||||
|
verworfen: erfordert Kontotypwechsel und feste Vorabaufteilung des Kapitals, deutlich unflexibler
|
||||||
|
als virtuelle Bücher.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Offene Punkte
|
||||||
|
|
||||||
|
- **Mindestordergröße** pro Buch: ein Wert in Kontobasiswährung, Fremdwährungsorders per Tageskurs
|
||||||
|
dagegen geprüft. Konkreter Wert beim Bauen festzulegen.
|
||||||
|
- **Konkrete Limitwerte** (`MaxSymbolPercent`, `HouseReserve`, FX-Haircut, Slippage-Puffer, TTL)
|
||||||
|
– Startwerte beim Bauen festzulegen und in `settings.example.json` dokumentieren.
|
||||||
|
- **Handelsplatz-Whitelist**: konkrete Börsenliste je Währung.
|
||||||
|
- **Umsetzungsreihenfolge** – noch nicht besprochen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zusammenfassung der getroffenen Entscheidungen
|
||||||
|
|
||||||
|
| # | Entscheidung |
|
||||||
|
|---|---|
|
||||||
|
| 1 | Cash-Konto; Modul-Handel strikt cash-only, manueller Handel darf Margin nutzen |
|
||||||
|
| 2 | Kein Kapitaltransfer zwischen Büchern – ein Pool plus Obergrenzen; **niemals** aktives Schließen zum Balancieren |
|
||||||
|
| 3 | Limit-Verletzung: hartes Gate, strukturiertes Feedback; die Reaktion entscheidet die Strategie |
|
||||||
|
| 4 | Kontobasiswährung USD (Produktion) / EUR (Test), bei Ersteinrichtung gesetzt und danach gesperrt |
|
||||||
|
| 5 | Nur USD und EUR handelbar; Contract-Auflösung mit Währungs- und Handelsplatz-Whitelist ist Pflicht |
|
||||||
|
| 6 | Steuerbuch strikt getrennt, Flex Query als einzige Quelle, Jurisdiktionsprofil einmalig und in der DB verankert |
|
||||||
|
| 7 | Unzugeordnete Positionen: Quarantäne + Eskalation nach 30 Minuten mit Backoff |
|
||||||
|
| 8 | Benachrichtigungen über Outbox + Sink-Abstraktion; Matrix als Zielkanal, Telegram optional |
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# Analyse: Datenlage über die TWS API – und welche Strategien sie trägt
|
||||||
|
|
||||||
|
> Stand: 2026-08-04. **Alle Angaben in Abschnitt 1 und 2 sind gegen das laufende Paper-Gateway
|
||||||
|
> gemessen** (Konto DUR371528, TWS API 9.76.1, `MarketDataType = 4`), nicht aus der IBKR-Doku
|
||||||
|
> übernommen. Wo etwas nur plausibel, aber ungeprüft ist, steht es ausdrücklich dabei.
|
||||||
|
>
|
||||||
|
> Zweck: entscheiden, welche Strategien wir **ohne zusätzliche Datenanbieter** bauen können –
|
||||||
|
> und welche wir uns sparen, weil die Datengrundlage fehlt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Kurzfassung
|
||||||
|
|
||||||
|
Die TWS API deckt **Preis-, Volatilitäts- und Optionsdaten sehr gut ab** und **Fundamentaldaten
|
||||||
|
gar nicht**. Zwei Grenzen bestimmen den Zuschnitt jeder Strategie:
|
||||||
|
|
||||||
|
1. **Kein Screening.** Der Marktscanner ist gesperrt (Realtime-Abo nötig). Wir können den Markt
|
||||||
|
nicht nach Kandidaten durchsuchen – jede Strategie muss auf einer **fest gepflegten Watchlist**
|
||||||
|
arbeiten. Das deckt sich mit der Festlegung im [OptionsWheel-Konzept](KONZEPT-Modul-OptionsWheel.md).
|
||||||
|
2. **Nur verzögerte Kurse (~15 Min).** Alles, was auf Intraday-Reaktion beruht, fällt weg.
|
||||||
|
Entscheidungen auf Tages- oder Wochenbasis sind davon **nicht** betroffen.
|
||||||
|
|
||||||
|
Innerhalb dieser Grenzen ist die Lage gut: 30 Jahre Kurshistorie, dividendenbereinigte Serien,
|
||||||
|
Volatilitätshistorie, vollständige Optionsketten und **funktionierende Griechen auch mit
|
||||||
|
verzögerten Daten**. Das trägt Prämienstrategien und Trendfolge auf Tagesbasis ohne jeden
|
||||||
|
externen Anbieter.
|
||||||
|
|
||||||
|
**Empfehlung:** OptionsWheel als erstes Modul weiterbauen – es ist die Strategie mit dem besten
|
||||||
|
Verhältnis von vorhandener Datengrundlage zu Ertragserwartung, und die Voraussetzungen sind
|
||||||
|
inzwischen alle geprüft.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Was die API liefert (gemessen)
|
||||||
|
|
||||||
|
### 1.1 Kurshistorie – die stärkste Säule
|
||||||
|
|
||||||
|
| Auflösung | Verfügbarer Zeitraum | Gemessen an AAPL |
|
||||||
|
|---|---|---|
|
||||||
|
| Monatsbars | **30 Jahre** | 361 Bars ab 1996-08 |
|
||||||
|
| Wochenbars | 15 Jahre | 783 Bars ab 2011-08 |
|
||||||
|
| Tagesbars | 10 Jahre | 2.511 Bars ab 2016-08 |
|
||||||
|
| Stundenbars | 2 Jahre | 3.488 Bars |
|
||||||
|
| Minutenbars | 30 Tage | 11.453 Bars |
|
||||||
|
| Einzelticks | `reqHistoricalTicks` | 351 Ticks für 2 Tage zurück |
|
||||||
|
|
||||||
|
Zusätzlich:
|
||||||
|
- **`ADJUSTED_LAST`** – dividendenbereinigte Tageskurse. Wichtig: unbereinigte Serien erzeugen bei
|
||||||
|
jeder Ausschüttung ein Scheinsignal in Momentum- und Mean-Reversion-Rechnungen.
|
||||||
|
- **`HISTORICAL_VOLATILITY`** – realisierte Volatilität als Zeitreihe (122 Tage je Abruf).
|
||||||
|
- **`OPTION_IMPLIED_VOLATILITY`** – **IV-Historie des Basiswerts** (122 Tage je Abruf).
|
||||||
|
Längere Reihen lassen sich durch wiederholte Abrufe mit gesetztem `endDateTime` zusammensetzen.
|
||||||
|
|
||||||
|
Die letzten beiden sind der eigentliche Schatz: aus ihnen lässt sich **IV-Rank / IV-Perzentil**
|
||||||
|
rechnen – die zentrale Kennzahl für jede Prämienstrategie (verkaufe Volatilität, wenn sie relativ
|
||||||
|
zu ihrer eigenen Geschichte teuer ist).
|
||||||
|
|
||||||
|
### 1.2 Optionen
|
||||||
|
|
||||||
|
| Datenart | Ergebnis |
|
||||||
|
|---|---|
|
||||||
|
| Optionskette (`reqSecDefOptParams`) | 24 Verfallstermine, 127 Strikes, Multiplier 100, TradingClass |
|
||||||
|
| Griechen (`tickOptionComputation`) | **IV, Delta, Gamma, Vega, Theta + Basiswertkurs** |
|
||||||
|
| Handelsberechtigung | vorhanden (What-If-Order angenommen) |
|
||||||
|
|
||||||
|
**Wichtigster Einzelbefund:** Die Griechen kommen **auch ohne Realtime-Abo**. Gemessen an
|
||||||
|
`AAPL 20260821 C305`: IV 0,2730 / Delta 0,5443 / Gamma 0,0220 / Vega 0,2626 / Theta −0,2304.
|
||||||
|
Sie laufen über die verzögerten Tick-Felder 80–83, wobei **Feld 83 (Modell) die relevante Größe**
|
||||||
|
für eine delta-basierte Strike-Wahl ist.
|
||||||
|
|
||||||
|
Damit ist der offene Punkt 2 aus dem [OptionsWheel-Konzept](KONZEPT-Modul-OptionsWheel.md)
|
||||||
|
beantwortet: Die geplante delta-basierte Strike-Wahl im Zielband 0,15–0,30 ist umsetzbar, die
|
||||||
|
Ersatzlösung über prozentualen Abstand wird nicht gebraucht. Verzögerung heißt: Der Delta-Wert ist
|
||||||
|
~15 Minuten alt – für die Auswahl eines Strikes mit 30–45 Tagen Restlaufzeit ist das belanglos.
|
||||||
|
|
||||||
|
### 1.3 Stammdaten, Nachrichten, Sonstiges
|
||||||
|
|
||||||
|
| Datenart | Ergebnis |
|
||||||
|
|---|---|
|
||||||
|
| Kontraktstammdaten | Branche/Kategorie/Unterkategorie (AAPL: Technology / Computers / Computers), Langname, **Handelszeiten**, Zeitzone, MinTick, gültige Börsen |
|
||||||
|
| Symbolsuche (`reqMatchingSymbols`) | Fuzzy-Suche, liefert auch Indizes |
|
||||||
|
| Nachrichten | 3 Anbieter: **BRFG** (Briefing.com Markt), **BRFUPDN** (Analystenaktionen), **DJNL** (Dow Jones Newsletters) |
|
||||||
|
| Historische Nachrichten | funktioniert, Schlagzeilen mit Zeitstempel und Anbieter |
|
||||||
|
| Konto, Bestand, Ausführungen | siehe [IBKR-Integration.md](../IBKR-Integration.md) |
|
||||||
|
|
||||||
|
Die Branchenklassifikation ist brauchbar für **Klumpenrisiko-Prüfungen** ("nicht drei Positionen
|
||||||
|
im selben Sektor"). Die Handelszeiten sind operativ wichtig: Market-Orders außerhalb der RTH
|
||||||
|
bleiben ohne Fill hängen.
|
||||||
|
|
||||||
|
`BRFUPDN` ist bemerkenswert – die Schlagzeilen sind maschinell auswertbar strukturiert:
|
||||||
|
|
||||||
|
```
|
||||||
|
2026-04-17 12:00:57 [BRFUPDN] !BNP Paribas Exane upgraded Apple (AAPL) to Outperform
|
||||||
|
2026-04-14 14:58:42 [BRFUPDN] !BofA Securities reiterated Apple (AAPL) coverage with Buy and target $325
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Was die API nicht liefert
|
||||||
|
|
||||||
|
| Datenart | Fehler | Bedeutung |
|
||||||
|
|---|---|---|
|
||||||
|
| **Fundamentaldaten**, alle Reports (`ReportSnapshot`, `ReportRatios`, `ReportsFinStatements`, `RESC`, `CalendarReport`) | 10358 „Fundamentaldaten nicht zulässig" | Kein Refinitiv-Abo auf dem Konto |
|
||||||
|
| **Fundamentalkennzahlen-Tick 258** (KGV, Marktkapitalisierung, Dividendenrendite, Beta) | 10358 | dito |
|
||||||
|
| **Marktscanner-Ausführung** (alle getesteten scanCodes) | 492 „zusätzliche Berechtigungen" | Realtime-Marktdatenabo nötig |
|
||||||
|
| **Realtime-Kurse** | 10167 / 10091 | nur verzögerte Daten (~15 Min) |
|
||||||
|
| **Histogramm** (`reqHistogramData`) | leer | ungeklärt, vermutlich abo-abhängig |
|
||||||
|
|
||||||
|
Die **Scanner-Parameter** (762 scanCodes) lassen sich zwar abrufen, aber nur als Metadaten – jeder
|
||||||
|
Scanner-Lauf wird abgelehnt.
|
||||||
|
|
||||||
|
### Drei Konsequenzen, die den Zuschnitt bestimmen
|
||||||
|
|
||||||
|
1. **Keine fundamentale Titelauswahl.** Kennzahlenbasierte Ansätze (Value, Quality, Growth) sind
|
||||||
|
ohne Zusatzquelle nicht baubar. Das ist verkraftbar, weil unsere beiden geplanten Module
|
||||||
|
(CongressTrading, OptionsWheel) ihre Kandidaten ohnehin anders bestimmen.
|
||||||
|
2. **Keine Termine für Quartalszahlen.** `CalendarReport` ist gesperrt. Damit ist der offene
|
||||||
|
Punkt 4 des OptionsWheel-Konzepts („Earnings-Sperre") **nicht** über die TWS API lösbar – er
|
||||||
|
braucht eine externe Quelle oder eine manuell gepflegte Liste. Das ist die einzige Stelle, an
|
||||||
|
der uns eine externe Abhängigkeit ernsthaft fehlt.
|
||||||
|
Behelf ohne externe Quelle: Ein **IV-Anstieg** vor Quartalszahlen ist messbar. Ein Filter
|
||||||
|
„keine neuen Legs, wenn die IV des Basiswerts stark über ihrem 30-Tage-Mittel liegt" fängt
|
||||||
|
Earnings indirekt mit ab – unschärfer, aber ohne Fremddaten.
|
||||||
|
3. **Kein Universum-Screening.** Strategien müssen mit einer gepflegten Watchlist auskommen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Strategiebewertung
|
||||||
|
|
||||||
|
### 3.1 Gut umsetzbar – Datenlage vollständig
|
||||||
|
|
||||||
|
| Strategie | Benötigte Daten | Status |
|
||||||
|
|---|---|---|
|
||||||
|
| **Options-Wheel** (Cash-Secured Put → Zuteilung → Covered Call) | Kette, Griechen, IV-Historie, Bestand, Berechtigung | alles vorhanden, [Konzept steht](KONZEPT-Modul-OptionsWheel.md) |
|
||||||
|
| **Covered Calls auf Bestand** | wie oben, ohne Put-Seite | Teilmenge des Wheel |
|
||||||
|
| **IV-Rank-gesteuerter Prämienverkauf** | `OPTION_IMPLIED_VOLATILITY` + `HISTORICAL_VOLATILITY` | vorhanden; liefert das Timing-Kriterium für das Wheel |
|
||||||
|
| **Trendfolge auf Tagesbasis** (gleitende Durchschnitte, Ausbrüche, ATR-Stops) | `ADJUSTED_LAST` Tagesbars, 10 J | vorhanden |
|
||||||
|
| **Relative Stärke / Dual Momentum** über die Watchlist | Tagesbars mehrerer Titel | vorhanden |
|
||||||
|
| **Mean Reversion** (RSI, Bollinger, Abstand zum gleitenden Mittel) | Tagesbars | vorhanden |
|
||||||
|
| **Paar-Handel** korrelierter Titel | Tagesbars, lange Historie | vorhanden |
|
||||||
|
| **Saisonalität / Kalendereffekte** | 30 Jahre Monatsbars | vorhanden |
|
||||||
|
| **Sektor-Klumpenrisiko-Prüfung** | Branchenklassifikation | vorhanden; gehört in den `RiskService` |
|
||||||
|
|
||||||
|
Allen gemeinsam: Sie entscheiden **auf Schlusskursen oder mit Stunden-/Tagesbezug**. Die
|
||||||
|
15-Minuten-Verzögerung ist dabei irrelevant, weil die Signale ohnehin aus abgeschlossenen Bars
|
||||||
|
kommen. Genau deshalb passen sie zu unserer Datenlage.
|
||||||
|
|
||||||
|
### 3.2 Bedingt umsetzbar
|
||||||
|
|
||||||
|
| Strategie | Einschränkung |
|
||||||
|
|---|---|
|
||||||
|
| **Analystenaktionen als Filter oder Signal** (`BRFUPDN`) | Schlagzeilen sind strukturiert und auswertbar. **Ungeprüft**: ob `reqNewsArticle` den Volltext liefert und wie weit die Historie zurückreicht. Als *Risikofilter* („keine neue Position kurz nach einer Abstufung") wertvoller denn als Einstiegssignal. |
|
||||||
|
| **Volatilitäts-Ausbruch intraday** | Minutenbars gibt es (30 Tage), aber nur verzögert. Rückrechnung möglich, Live-Handel nicht. |
|
||||||
|
| **Gap-Strategien auf Eröffnung** | Eröffnungskurs kommt verzögert; die Ausführung träfe den Markt 15 Minuten zu spät. Nur mit Realtime-Abo sinnvoll. |
|
||||||
|
|
||||||
|
### 3.3 Nicht umsetzbar
|
||||||
|
|
||||||
|
| Strategie | Grund |
|
||||||
|
|---|---|
|
||||||
|
| Fundamentales Screening (Value, Quality, Growth) | keine Fundamentaldaten |
|
||||||
|
| Earnings-Strategien (Straddle vor Zahlen, Post-Earnings-Drift) | keine Termine für Quartalszahlen |
|
||||||
|
| Marktweite Anomalie-Suche / Screener-getriebene Auswahl | Scanner gesperrt |
|
||||||
|
| Daytrading, Scalping, Orderbuch-Strategien | verzögerte Daten, keine Markttiefe geprüft |
|
||||||
|
| Nachrichten-Sentiment in der Breite | nur 3 Anbieter, Schlagzeilen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Empfehlung
|
||||||
|
|
||||||
|
**Reihenfolge:**
|
||||||
|
|
||||||
|
1. **OptionsWheel** wie im [bestehenden Konzept](KONZEPT-Modul-OptionsWheel.md) bauen. Alle
|
||||||
|
Voraussetzungen sind jetzt geprüft: Berechtigung, Kette, Griechen mit verzögerten Daten,
|
||||||
|
Bestandsabgleich. Der einzige offene Punkt ist die Earnings-Sperre – dafür zunächst den
|
||||||
|
IV-Behelf aus Abschnitt 2 einsetzen und die Entscheidung über eine externe Quelle vertagen.
|
||||||
|
2. **IV-Rank als Core-Baustein** ziehen, nicht als Modul-Interna. Die Kennzahl ist für jede
|
||||||
|
Prämienstrategie nötig und gehört neben die Kurshistorie in die Datenschicht.
|
||||||
|
3. **Trendfolge/Momentum auf Tagesbasis** als zweites Modul, wenn ein zweites Standbein gewünscht
|
||||||
|
ist. Datenlage ist komfortabel, das Risiko liegt in der Strategie, nicht in den Daten.
|
||||||
|
|
||||||
|
**Was ein Zusatzabo ändern würde** (Reihenfolge nach Nutzen je Euro):
|
||||||
|
|
||||||
|
| Abo | Schaltet frei | Für uns relevant? |
|
||||||
|
|---|---|---|
|
||||||
|
| US-Aktien-Realtime (NYSE/AMEX/NASDAQ) | Scanner, Realtime-Kurse | Nur wenn wir Screening oder Intraday wollen. Für Tages- und Prämienstrategien **nicht nötig**. |
|
||||||
|
| OPRA (Optionen-Realtime) | Realtime-Optionskurse und -Griechen | Verbessert die Ausführungsqualität beim Wheel; für die Strike-Auswahl nicht erforderlich. |
|
||||||
|
| Refinitiv-Fundamentaldaten | Kennzahlen, Bilanzen, **Termine für Quartalszahlen** | Löst die Earnings-Sperre und öffnet fundamentale Ansätze. Der Kandidat mit dem größten qualitativen Sprung. |
|
||||||
|
|
||||||
|
Alle drei sind **Erweiterungen, keine Voraussetzungen**. Der aktuelle Stand trägt die geplanten
|
||||||
|
Strategien.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Offen / ungeprüft
|
||||||
|
|
||||||
|
1. **`reqNewsArticle`** (Volltext zu einer Schlagzeile) – nicht getestet. Entscheidet, ob
|
||||||
|
`BRFUPDN` mehr als ein Ereignis-Marker sein kann.
|
||||||
|
2. **Wie weit die Nachrichtenhistorie zurückreicht** – im Test kamen Meldungen bis 2026-03,
|
||||||
|
abgefragt waren 30 Tage. Die Zeitfilter-Semantik ist offenbar anders als angenommen und
|
||||||
|
sollte vor produktiver Nutzung geklärt werden.
|
||||||
|
3. **Markttiefe** (`reqMktDepth`) – nicht getestet, für unsere Strategien vermutlich unnötig.
|
||||||
|
4. **`reqHistogramData`** liefert leer – Ursache ungeklärt (Abo oder Parameter).
|
||||||
|
5. **Wie zuverlässig verzögerte Griechen außerhalb der Handelszeiten sind** – gemessen wurde
|
||||||
|
während der US-Handelszeit. Außerhalb liefert TWS ggf. eingefrorene Werte.
|
||||||
|
6. **Ratenbegrenzung** bei Historienabrufen (IBKR drosselt `reqHistoricalData` bei zu vielen
|
||||||
|
Anfragen). Für einen nächtlichen Watchlist-Abruf relevant, im Test nicht ausgereizt.
|
||||||
@@ -228,13 +228,19 @@ W-0 bis W-3 sind Core-Arbeit und nützen auch den anderen Modulen; erst ab W-4 e
|
|||||||
Multiplier 100 über SMART; eine **What-If-Order** auf `AAPL 20260812 C302.5` wurde von IBKR
|
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.
|
angenommen (Init-Margin 589,52) statt mit einem Berechtigungsfehler abgelehnt.
|
||||||
Damit ist die Grundvoraussetzung für dieses Modul gegeben.
|
Damit ist die Grundvoraussetzung für dieses Modul gegeben.
|
||||||
2. **Greeks bei verzögerten Daten.** `MarketDataType = 4` liefert Optionsberechnungen als verzögerte
|
2. ~~**Greeks bei verzögerten Daten.**~~ → **erledigt am 2026-08-04, funktioniert.** Gemessen an
|
||||||
Tick-Variante; ob Delta zuverlässig ankommt, muss gegen das laufende Gateway verifiziert werden.
|
`AAPL 20260821 C305`: IV 0,2730 / Delta 0,5443 / Gamma 0,0220 / Vega 0,2626 / Theta −0,2304.
|
||||||
Fällt es aus, greift ersatzweise eine Strike-Wahl über Abstand in % + Mindestprämie – der
|
Die Werte kommen über die verzögerten Tick-Felder 80–83; **Feld 83 (Modell)** ist die für die
|
||||||
`StrikeSelector` wird von vornherein so geschnitten, dass beide Kriterien einsetzbar sind.
|
Strike-Wahl maßgebliche Variante. Die delta-basierte Wahl im Band 0,15–0,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.
|
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
|
4. **Earnings-Sperre**: keine neuen Legs über Quartalszahlen hinweg. **Geprüft am 2026-08-04: über
|
||||||
Datenquelle für Earnings-Termine – Punkt bewusst offen.
|
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`
|
5. **Accounting-Anschluss**: Optionsprämien, Zuteilungen und Abrufe müssen im `AccountingClassifier`
|
||||||
eigene Buchungskategorien bekommen; der `RealizedPnlEngine` (FIFO) kennt weder Multiplikator noch
|
eigene Buchungskategorien bekommen; der `RealizedPnlEngine` (FIFO) kennt weder Multiplikator noch
|
||||||
die Einstandsverschiebung durch Zuteilung. Eigene Arbeit im Accounting-Modul.
|
die Einstandsverschiebung durch Zuteilung. Eigene Arbeit im Accounting-Modul.
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace IBKRTrader.Core.Trading;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lesender Zugriff auf Bestand und Ausführungen beim Broker.
|
||||||
|
///
|
||||||
|
/// Bewusst getrennt von <see cref="IBrokerClient"/>: dort geht es um das Auslösen von Handel,
|
||||||
|
/// hier um den Abgleich der eigenen Buchführung mit dem, was der Broker tatsächlich führt.
|
||||||
|
/// Wer nur handelt, braucht das nicht; wer abstimmt, braucht nur das.
|
||||||
|
/// </summary>
|
||||||
|
public interface IBrokerPortfolioReader
|
||||||
|
{
|
||||||
|
/// <summary>Alle offenen Positionen des Kontos, inklusive Bewertung. Leer, wenn nicht erreichbar.</summary>
|
||||||
|
Task<IReadOnlyList<BrokerPosition>> GetPositionsAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ausführungen ab <paramref name="since"/> (null = alles, was der Broker liefert).
|
||||||
|
///
|
||||||
|
/// <b>Grenze der TWS-API:</b> sie liefert nur den **aktuellen Handelstag**, unabhängig vom
|
||||||
|
/// gewünschten Zeitraum. Für ältere Trades bleibt der Flex-Query-Ingest des Accounting-Moduls
|
||||||
|
/// die richtige Quelle.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<BrokerExecution>> GetExecutionsAsync(DateTime? since = null, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ namespace IBKRTrader.Core.Trading.Ibkr;
|
|||||||
/// fehlgeschlagene Order). Ein Konto mit Wert 0 lässt die Risikoprüfung jedes Signal ablehnen –
|
/// fehlgeschlagene Order). Ein Konto mit Wert 0 lässt die Risikoprüfung jedes Signal ablehnen –
|
||||||
/// die sichere Richtung, wenn der Broker nicht erreichbar ist.
|
/// die sichere Richtung, wenn der Broker nicht erreichbar ist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class IbkrBrokerClient : IBrokerClient, IDisposable
|
public sealed class IbkrBrokerClient : IBrokerClient, IBrokerPortfolioReader, IDisposable
|
||||||
{
|
{
|
||||||
private const string LogModule = "IBKR";
|
private const string LogModule = "IBKR";
|
||||||
|
|
||||||
@@ -91,6 +91,21 @@ public sealed class IbkrBrokerClient : IBrokerClient, IDisposable
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<BrokerPosition>> GetPositionsAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!await IsReadyAsync(ct).ConfigureAwait(false)) return Array.Empty<BrokerPosition>();
|
||||||
|
|
||||||
|
return await _connection!.RequestPositionsAsync(_requestTimeout, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<BrokerExecution>> GetExecutionsAsync(DateTime? since = null,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!await IsReadyAsync(ct).ConfigureAwait(false)) return Array.Empty<BrokerExecution>();
|
||||||
|
|
||||||
|
return await _connection!.RequestExecutionsAsync(since, _requestTimeout, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
private static AccountState Empty => new(0m, 0m);
|
private static AccountState Empty => new(0m, 0m);
|
||||||
|
|
||||||
private async Task<bool> IsReadyAsync(CancellationToken ct)
|
private async Task<bool> IsReadyAsync(CancellationToken ct)
|
||||||
|
|||||||
@@ -33,8 +33,12 @@ internal sealed class IbkrConnection : DefaultEWrapper, IDisposable
|
|||||||
private readonly ConcurrentDictionary<int, AccountSlot> _accounts = new();
|
private readonly ConcurrentDictionary<int, AccountSlot> _accounts = new();
|
||||||
private readonly ConcurrentDictionary<int, ContractSlot> _contracts = new();
|
private readonly ConcurrentDictionary<int, ContractSlot> _contracts = new();
|
||||||
private readonly ConcurrentDictionary<int, OrderSlot> _orders = new();
|
private readonly ConcurrentDictionary<int, OrderSlot> _orders = new();
|
||||||
|
private readonly ConcurrentDictionary<int, ExecutionSlot> _executions = new();
|
||||||
private readonly ConcurrentDictionary<string, Contract> _contractCache = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, Contract> _contractCache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Portfolio-Abruf ist ein kontoweites Abonnement, keine reqId-Anfrage – daher nur ein Slot.
|
||||||
|
private PortfolioSlot? _portfolio;
|
||||||
|
|
||||||
private TaskCompletionSource<bool> _handshake = NewTcs();
|
private TaskCompletionSource<bool> _handshake = NewTcs();
|
||||||
private volatile bool _ready;
|
private volatile bool _ready;
|
||||||
private volatile bool _disposed;
|
private volatile bool _disposed;
|
||||||
@@ -256,6 +260,85 @@ internal sealed class IbkrConnection : DefaultEWrapper, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bestand samt Bewertung. Nutzt <c>reqAccountUpdates</c> statt <c>reqPositions</c>, weil nur
|
||||||
|
/// dieser Weg Marktwert und unrealisierten G/V mitliefert. Es ist ein Abonnement – wir melden
|
||||||
|
/// uns nach dem ersten vollständigen Stand wieder ab.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<BrokerPosition>> RequestPositionsAsync(TimeSpan timeout, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var account = _account;
|
||||||
|
if (string.IsNullOrWhiteSpace(account))
|
||||||
|
{
|
||||||
|
_logger.Warn(LogModule, "Kontonummer unbekannt – Positionen nicht abrufbar.");
|
||||||
|
return Array.Empty<BrokerPosition>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var slot = new PortfolioSlot();
|
||||||
|
if (Interlocked.CompareExchange(ref _portfolio, slot, null) is not null)
|
||||||
|
{
|
||||||
|
_logger.Warn(LogModule, "Es läuft bereits eine Positionsabfrage.");
|
||||||
|
return Array.Empty<BrokerPosition>();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_socket.reqAccountUpdates(true, account);
|
||||||
|
|
||||||
|
if (!await WaitAsync(slot.Done.Task, timeout, ct).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
_logger.Warn(LogModule, "Zeitüberschreitung bei der Positionsabfrage.");
|
||||||
|
return Array.Empty<BrokerPosition>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return slot.Positions;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { _socket.reqAccountUpdates(false, account); }
|
||||||
|
catch { /* Verbindung bereits weg. */ }
|
||||||
|
Interlocked.Exchange(ref _portfolio, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ausführungen. Kommissionen kommen über einen eigenen Callback und treffen oft erst nach
|
||||||
|
/// <c>execDetailsEnd</c> ein – deshalb die kurze Nachlauffrist, bevor zusammengeführt wird.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<BrokerExecution>> RequestExecutionsAsync(DateTime? since,
|
||||||
|
TimeSpan timeout, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var id = NextRequestId();
|
||||||
|
var slot = new ExecutionSlot();
|
||||||
|
_executions[id] = slot;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var filter = new ExecutionFilter();
|
||||||
|
if (since is { } from) filter.Time = IbkrMapping.FormatExecutionFilterTime(from);
|
||||||
|
|
||||||
|
_socket.reqExecutions(id, filter);
|
||||||
|
|
||||||
|
if (!await WaitAsync(slot.Done.Task, timeout, ct).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
_logger.Warn(LogModule, "Zeitüberschreitung bei der Abfrage der Ausführungen.");
|
||||||
|
return Array.Empty<BrokerExecution>();
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1), ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return slot.Items
|
||||||
|
.Select(e => slot.Commissions.TryGetValue(e.ExecId, out var c)
|
||||||
|
? e with { Commission = c.Amount, CommissionCurrency = c.Currency }
|
||||||
|
: e)
|
||||||
|
.OrderByDescending(e => e.Time)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_executions.TryRemove(id, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<OrderResult> PlaceOrderAsync(OrderRequest request, Contract contract,
|
public async Task<OrderResult> PlaceOrderAsync(OrderRequest request, Contract contract,
|
||||||
TimeSpan timeout, CancellationToken ct)
|
TimeSpan timeout, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -346,6 +429,60 @@ internal sealed class IbkrConnection : DefaultEWrapper, IDisposable
|
|||||||
if (_accounts.TryGetValue(reqId, out var slot)) slot.Complete();
|
if (_accounts.TryGetValue(reqId, out var slot)) slot.Complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void updatePortfolio(Contract contract, double position, double marketPrice,
|
||||||
|
double marketValue, double averageCost, double unrealizedPNL, double realizedPNL, string accountName)
|
||||||
|
{
|
||||||
|
// Glattgestellte Positionen meldet TWS mit Menge 0 weiter – die gehören nicht in den Bestand.
|
||||||
|
if (_portfolio is not { } slot || position == 0) return;
|
||||||
|
|
||||||
|
slot.Positions.Add(new BrokerPosition
|
||||||
|
{
|
||||||
|
Symbol = contract.Symbol,
|
||||||
|
SecType = contract.SecType,
|
||||||
|
Currency = contract.Currency,
|
||||||
|
ConId = contract.ConId,
|
||||||
|
Quantity = (decimal)position,
|
||||||
|
AvgCost = (decimal)averageCost,
|
||||||
|
MarketPrice = (decimal)marketPrice,
|
||||||
|
MarketValue = (decimal)marketValue,
|
||||||
|
UnrealizedPnl = (decimal)unrealizedPNL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void accountDownloadEnd(string account) => _portfolio?.Complete();
|
||||||
|
|
||||||
|
public override void execDetails(int reqId, Contract contract, Execution execution)
|
||||||
|
{
|
||||||
|
if (!_executions.TryGetValue(reqId, out var slot)) return;
|
||||||
|
|
||||||
|
slot.Items.Add(new BrokerExecution
|
||||||
|
{
|
||||||
|
ExecId = execution.ExecId,
|
||||||
|
Time = IbkrMapping.ParseExecutionTime(execution.Time) ?? DateTime.MinValue,
|
||||||
|
Symbol = contract.Symbol,
|
||||||
|
SecType = contract.SecType,
|
||||||
|
Side = IbkrMapping.ParseSide(execution.Side),
|
||||||
|
Quantity = (decimal)execution.Shares,
|
||||||
|
Price = (decimal)execution.Price,
|
||||||
|
Exchange = execution.Exchange ?? "",
|
||||||
|
OrderId = execution.OrderId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void execDetailsEnd(int reqId)
|
||||||
|
{
|
||||||
|
if (_executions.TryGetValue(reqId, out var slot)) slot.Complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void commissionReport(CommissionReport report)
|
||||||
|
{
|
||||||
|
// Der Callback trägt keine reqId; die ExecId ordnet ihn der Ausführung zu.
|
||||||
|
if (report.Commission is <= 0 or >= 1e100) return;
|
||||||
|
|
||||||
|
foreach (var slot in _executions.Values)
|
||||||
|
slot.Commissions[report.ExecId] = ((decimal)report.Commission, report.Currency ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
public override void orderStatus(int orderId, string status, double filled, double remaining,
|
public override void orderStatus(int orderId, string status, double filled, double remaining,
|
||||||
double avgFillPrice, int permId, int parentId, double lastFillPrice, int clientId,
|
double avgFillPrice, int permId, int parentId, double lastFillPrice, int clientId,
|
||||||
string whyHeld, double mktCapPrice)
|
string whyHeld, double mktCapPrice)
|
||||||
@@ -411,6 +548,7 @@ internal sealed class IbkrConnection : DefaultEWrapper, IDisposable
|
|||||||
if (_accounts .TryGetValue(id, out var a)) { a.Fail(error); return true; }
|
if (_accounts .TryGetValue(id, out var a)) { a.Fail(error); return true; }
|
||||||
if (_contracts .TryGetValue(id, out var c)) { c.Fail(error); return true; }
|
if (_contracts .TryGetValue(id, out var c)) { c.Fail(error); return true; }
|
||||||
if (_orders .TryGetValue(id, out var o)) { o.Fail(error); return true; }
|
if (_orders .TryGetValue(id, out var o)) { o.Fail(error); return true; }
|
||||||
|
if (_executions.TryGetValue(id, out var e)) { e.Fail(error); return true; }
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,6 +558,8 @@ internal sealed class IbkrConnection : DefaultEWrapper, IDisposable
|
|||||||
foreach (var slot in _accounts.Values) slot.Fail(error);
|
foreach (var slot in _accounts.Values) slot.Fail(error);
|
||||||
foreach (var slot in _contracts.Values) slot.Fail(error);
|
foreach (var slot in _contracts.Values) slot.Fail(error);
|
||||||
foreach (var slot in _orders.Values) slot.Fail(error);
|
foreach (var slot in _orders.Values) slot.Fail(error);
|
||||||
|
foreach (var slot in _executions.Values) slot.Fail(error);
|
||||||
|
_portfolio?.Fail(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static decimal ReadDecimal(IReadOnlyDictionary<string, string> values, string tag) =>
|
private static decimal ReadDecimal(IReadOnlyDictionary<string, string> values, string tag) =>
|
||||||
@@ -486,4 +626,15 @@ internal sealed class IbkrConnection : DefaultEWrapper, IDisposable
|
|||||||
public int Filled;
|
public int Filled;
|
||||||
public double AvgFillPrice;
|
public double AvgFillPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class PortfolioSlot : Slot
|
||||||
|
{
|
||||||
|
public readonly List<BrokerPosition> Positions = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ExecutionSlot : Slot
|
||||||
|
{
|
||||||
|
public readonly List<BrokerExecution> Items = new();
|
||||||
|
public readonly ConcurrentDictionary<string, (decimal Amount, string Currency)> Commissions = new();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Globalization;
|
||||||
using IBApi;
|
using IBApi;
|
||||||
|
|
||||||
namespace IBKRTrader.Core.Trading.Ibkr;
|
namespace IBKRTrader.Core.Trading.Ibkr;
|
||||||
@@ -88,6 +89,35 @@ internal static class IbkrMapping
|
|||||||
(decimal)(ask > 0 ? ask : price));
|
(decimal)(ask > 0 ? ask : price));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Ausführungsseite laut TWS: "BOT" = gekauft, "SLD" = verkauft.</summary>
|
||||||
|
public static TradeSide ParseSide(string side) =>
|
||||||
|
side.Trim().ToUpperInvariant() is "SLD" or "SELL" ? TradeSide.Sell : TradeSide.Buy;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zeitstempel einer Ausführung. TWS liefert je nach Aufruf "yyyyMMdd HH:mm:ss" (mit doppeltem
|
||||||
|
/// Leerzeichen) oder zusätzlich eine Zeitzone ("20260804 17:52:56 Europe/Berlin"). Die Zeitzone
|
||||||
|
/// wird verworfen – der Wert bleibt Ortszeit der Börse, wie ihn TWS meldet.
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime? ParseExecutionTime(string? raw)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(raw)) return null;
|
||||||
|
|
||||||
|
var parts = raw.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
if (parts.Length < 2) return null;
|
||||||
|
|
||||||
|
return DateTime.TryParseExact($"{parts[0]} {parts[1]}", "yyyyMMdd HH:mm:ss",
|
||||||
|
CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed)
|
||||||
|
? parsed
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zeitfilter für <c>reqExecutions</c>. TWS warnt (2174) bei Zeitangaben ohne Zeitzone und
|
||||||
|
/// kündigt an, das Format zu entfernen – deshalb ausdrücklich UTC.
|
||||||
|
/// </summary>
|
||||||
|
public static string FormatExecutionFilterTime(DateTime since) =>
|
||||||
|
since.ToUniversalTime().ToString("yyyyMMdd-HH:mm:ss", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
public static bool IsLastTick (int field) => field is TickType.LAST or TickType.DELAYED_LAST;
|
public static bool IsLastTick (int field) => field is TickType.LAST or TickType.DELAYED_LAST;
|
||||||
public static bool IsBidTick (int field) => field is TickType.BID or TickType.DELAYED_BID;
|
public static bool IsBidTick (int field) => field is TickType.BID or TickType.DELAYED_BID;
|
||||||
public static bool IsAskTick (int field) => field is TickType.ASK or TickType.DELAYED_ASK;
|
public static bool IsAskTick (int field) => field is TickType.ASK or TickType.DELAYED_ASK;
|
||||||
|
|||||||
@@ -7,12 +7,18 @@ namespace IBKRTrader.Core.Trading;
|
|||||||
/// Wird registriert, bis der echte IBKR-Adapter angebunden und gegen den
|
/// Wird registriert, bis der echte IBKR-Adapter angebunden und gegen den
|
||||||
/// Paper-Gateway verifiziert ist. So kann keine Order versehentlich rausgehen.
|
/// Paper-Gateway verifiziert ist. So kann keine Order versehentlich rausgehen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class NullBrokerClient : IBrokerClient
|
public sealed class NullBrokerClient : IBrokerClient, IBrokerPortfolioReader
|
||||||
{
|
{
|
||||||
private readonly LoggingService _logger;
|
private readonly LoggingService _logger;
|
||||||
|
|
||||||
public NullBrokerClient(LoggingService logger) => _logger = logger;
|
public NullBrokerClient(LoggingService logger) => _logger = logger;
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<BrokerPosition>> GetPositionsAsync(CancellationToken ct = default)
|
||||||
|
=> Task.FromResult<IReadOnlyList<BrokerPosition>>(Array.Empty<BrokerPosition>());
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<BrokerExecution>> GetExecutionsAsync(DateTime? since = null, CancellationToken ct = default)
|
||||||
|
=> Task.FromResult<IReadOnlyList<BrokerExecution>>(Array.Empty<BrokerExecution>());
|
||||||
|
|
||||||
public Task<Quote?> GetQuoteAsync(string symbol, CancellationToken ct = default)
|
public Task<Quote?> GetQuoteAsync(string symbol, CancellationToken ct = default)
|
||||||
=> Task.FromResult<Quote?>(null);
|
=> Task.FromResult<Quote?>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,56 @@ public sealed record Position(string Module, string Symbol, int Quantity, decima
|
|||||||
public decimal Notional => Quantity * AvgPrice;
|
public decimal Notional => Quantity * AvgPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Position, wie der Broker sie führt – die maßgebliche Wahrheit. Weicht bewusst von
|
||||||
|
/// <see cref="Position"/> ab: der Broker kennt kein Modul, dafür Bewertung und Instrumententyp.
|
||||||
|
/// Zuteilung und Verfall (Optionen) ändern Positionen ohne Order von uns; ohne Abgleich läuft
|
||||||
|
/// die eigene Buchführung deshalb zwangsläufig auseinander.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record BrokerPosition
|
||||||
|
{
|
||||||
|
public required string Symbol { get; init; }
|
||||||
|
/// <summary>"STK", "OPT", "FUT" …</summary>
|
||||||
|
public required string SecType { get; init; }
|
||||||
|
public required string Currency { get; init; }
|
||||||
|
/// <summary>IBKR-Kontrakt-ID – eindeutiger als das Symbol.</summary>
|
||||||
|
public required int ConId { get; init; }
|
||||||
|
/// <summary>Negativ bei Short-Positionen.</summary>
|
||||||
|
public required decimal Quantity { get; init; }
|
||||||
|
public required decimal AvgCost { get; init; }
|
||||||
|
// Achtung: Kurs, Wert und G/V stehen in der Währung der Position (<see cref="Currency"/>),
|
||||||
|
// nicht in der Kontobasiswährung. Positionen verschiedener Währungen dürfen deshalb nicht
|
||||||
|
// einfach aufsummiert werden – dafür ist NetLiquidation aus AccountState zuständig.
|
||||||
|
public decimal MarketPrice { get; init; }
|
||||||
|
public decimal MarketValue { get; init; }
|
||||||
|
public decimal UnrealizedPnl { get; init; }
|
||||||
|
|
||||||
|
public override string ToString() =>
|
||||||
|
$"{Symbol} ({SecType}) {Quantity:N0} @ {AvgCost:N2} {Currency}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Einzelne Ausführung (Teil-Fill) laut Broker.</summary>
|
||||||
|
public sealed record BrokerExecution
|
||||||
|
{
|
||||||
|
/// <summary>Eindeutige IBKR-Ausführungs-ID – geeignet als Idempotenzschlüssel beim Import.</summary>
|
||||||
|
public required string ExecId { get; init; }
|
||||||
|
public required DateTime Time { get; init; }
|
||||||
|
public required string Symbol { get; init; }
|
||||||
|
public required string SecType { get; init; }
|
||||||
|
public required TradeSide Side { get; init; }
|
||||||
|
public required decimal Quantity { get; init; }
|
||||||
|
public required decimal Price { get; init; }
|
||||||
|
public string Exchange { get; init; } = "";
|
||||||
|
/// <summary>0 bei Orders, die nicht über diese API platziert wurden (z. B. manuell in TWS).</summary>
|
||||||
|
public int OrderId { get; init; }
|
||||||
|
/// <summary>Kommt über einen eigenen Callback und kann fehlen, wenn er verspätet eintrifft.</summary>
|
||||||
|
public decimal? Commission { get; init; }
|
||||||
|
public string? CommissionCurrency { get; init; }
|
||||||
|
|
||||||
|
public override string ToString() =>
|
||||||
|
$"{Time:yyyy-MM-dd HH:mm:ss} {Side} {Quantity:N0}x {Symbol} @ {Price:N2}";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Kontext für die Risikobewertung eines Signals.</summary>
|
/// <summary>Kontext für die Risikobewertung eines Signals.</summary>
|
||||||
public sealed record RiskContext
|
public sealed record RiskContext
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -151,6 +151,48 @@ public class IbkrMappingTests
|
|||||||
public void BuildQuote_WithoutAnyPrice_ReturnsNull() =>
|
public void BuildQuote_WithoutAnyPrice_ReturnsNull() =>
|
||||||
IbkrMapping.BuildQuote("AAPL", last: 0, bid: 0, ask: 0, close: 0).Should().BeNull();
|
IbkrMapping.BuildQuote("AAPL", last: 0, bid: 0, ask: 0, close: 0).Should().BeNull();
|
||||||
|
|
||||||
|
// ─── Ausführungen ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("BOT", TradeSide.Buy)]
|
||||||
|
[InlineData("SLD", TradeSide.Sell)]
|
||||||
|
[InlineData("sld", TradeSide.Sell)]
|
||||||
|
[InlineData("SELL", TradeSide.Sell)]
|
||||||
|
public void ParseSide_MapsTwsExecutionSides(string raw, TradeSide expected) =>
|
||||||
|
IbkrMapping.ParseSide(raw).Should().Be(expected);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseExecutionTime_HandlesDoubleSpaceFormat()
|
||||||
|
{
|
||||||
|
// So liefert TWS es bei execDetails.
|
||||||
|
IbkrMapping.ParseExecutionTime("20260804 17:39:18")
|
||||||
|
.Should().Be(new DateTime(2026, 8, 4, 17, 39, 18));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseExecutionTime_IgnoresTrailingTimeZone()
|
||||||
|
{
|
||||||
|
IbkrMapping.ParseExecutionTime("20260804 17:52:56 Europe/Berlin")
|
||||||
|
.Should().Be(new DateTime(2026, 8, 4, 17, 52, 56));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" ")]
|
||||||
|
[InlineData("20260804")]
|
||||||
|
[InlineData("Unsinn")]
|
||||||
|
public void ParseExecutionTime_ReturnsNullForUnusableInput(string raw) =>
|
||||||
|
IbkrMapping.ParseExecutionTime(raw).Should().BeNull();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FormatExecutionFilterTime_UsesUtcWithExplicitFormat()
|
||||||
|
{
|
||||||
|
// TWS warnt (2174) bei Zeitangaben ohne Zeitzone und entfernt das Format künftig.
|
||||||
|
var since = new DateTime(2026, 8, 4, 12, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
IbkrMapping.FormatExecutionFilterTime(since).Should().Be("20260804-12:00:00");
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Tick- und Status-Klassifizierung ─────────────────────────────────────
|
// ─── Tick- und Status-Klassifizierung ─────────────────────────────────────
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|||||||
Reference in New Issue
Block a user