Eine Roadmap statt sieben Konzepte; Quelldokumente ins Archiv
Der offene Stand lag ueber sieben Konzepte, zwei Referenzdokumente und die
Phasen-Checkliste der Architektur verteilt. Dieselbe Aufgabe stand teils
doppelt unter zwei Namen - die asynchrone Fill-Verfolgung etwa als "bekannte
Grenze" in IBKR-Integration.md und zugleich als W-2 im OptionsWheel-Konzept.
Wer wissen wollte, was als Naechstes ansteht, musste alles neun lesen.
docs/ROADMAP.md fuehrt das zusammen:
- Fuenf Stufen in Abhaengigkeitsreihenfolge, von "Fundament schliessen" bis
zum OptionsWheel, dazu vier laufende Bahnen (Auslieferung, Accounting,
Supervisor, technische Schulden).
- Jede Zeile traegt ihre Herkunft (W-2, P5, Kapitalmodell 5, ...), damit die
Herleitung im Archiv auffindbar bleibt.
- Eigene Abschnitte fuer Zurueckgestelltes und Verworfenes. Zurueckgestellte
Ideen nennen ausdruecklich, WAS sie wieder aktuell macht; verworfene nennen
den Grund, damit sie nicht in sechs Monaten erneut vorgeschlagen werden.
- Erledigtes bleibt als Zeile mit Datum stehen statt zu verschwinden.
Archiv (git erkennt alle sieben als Umbenennung, Historie bleibt):
docs/konzepte/* -> docs/archiv/
docs/Kapital-und-Buchmodell.md -> docs/archiv/
Jedes archivierte Dokument bekommt oben einen Vermerk, warum es erhalten bleibt
und wo der lebende Stand steht. Inhaltlich ist keines veraendert.
Weiter gepflegt werden ARCHITECTURE.md (Aufbau + Phasen-Historie),
IBKR-Integration.md (Adapter-Design und Grenzen) und die TWS-Setup-Checkliste -
das sind Referenzen, keine Planung. Ihre eigenen Offen-Listen verweisen jetzt
mit Roadmap-Kennung dorthin, statt einen zweiten Stand zu fuehren.
Alle 63 relativen Markdown-Links geprueft, keiner tot. Fuenf Pfadverweise in
Code, csproj und systemd-Unit mitgezogen; die Unit zeigte auf die
Portierungsanalyse, die ausdruecklich den Stand VOR dem Umbau beschreibt - jetzt
auf ARCHITECTURE.md. Build 0 Warnungen, 198/198 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
> ### 📦 Archiviert am 2026-08-23
|
||||
> Dieses Dokument wird **nicht mehr gepflegt**. Was davon noch offen ist, steht in der
|
||||
> [Roadmap](../ROADMAP.md) – dort und nur dort wird der Stand nachgeführt.
|
||||
>
|
||||
> Es bleibt erhalten, weil es **gegen das laufende Paper-Gateway gemessen** ist und nicht aus der IBKR-Doku uebernommen.
|
||||
> Die Zurueckgestellt- und Verworfen-Listen der Roadmap stuetzen sich auf diese Messwerte. Zum Nachschlagen also weiterhin richtig,
|
||||
> als Aufgabenliste nicht mehr.
|
||||
|
||||
---
|
||||
|
||||
# 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.
|
||||
@@ -0,0 +1,672 @@
|
||||
> ### 📦 Archiviert am 2026-08-23
|
||||
> Dieses Dokument wird **nicht mehr gepflegt**. Was davon noch offen ist, steht in der
|
||||
> [Roadmap](../ROADMAP.md) (Bahn „Auslieferung / Deploymentcenter") – dort und nur dort wird der Stand nachgeführt.
|
||||
>
|
||||
> Es bleibt erhalten, weil es die bewussten Abweichungen vom DC-Leitfaden begruendet – Sperrbetrieb statt
|
||||
> Prozessende, `exitCurrentApp: false` – und die Befunde P1–P6 und D1–D7 nachweist. Zum Nachschlagen also weiterhin richtig,
|
||||
> als Aufgabenliste nicht mehr.
|
||||
|
||||
---
|
||||
|
||||
# KONZEPT: Deploymentcenter-Integration
|
||||
|
||||
> Stand: 2026-08-14 · Deploymentcenter-Version **2.5.1** · Quelle: `J:\Softwareprojekte\Deploymentcenter\docs`,
|
||||
> `GET /api/updateservice/v1/changelog?since=2.4`
|
||||
> Zielprojekt: IBKRTrader (App + Daemon), Projekt-Slug **`ibkrtrader`**
|
||||
> SDK-Zielversion: `Deploymentcenter.Client` **2.5.1** (`DeploymentcenterSdk.Version` mitführen, damit
|
||||
> künftige Anbindungen über `?since=2.5.1` direkt sehen, was sich seither geändert hat)
|
||||
|
||||
> **Nachtrag 2026-08-14:** Die ursprüngliche Fassung dieses Dokuments (Stand DC 2.4) enthielt sieben
|
||||
> Befunde/Bedenken in §8, die dem Deploymentcenter-Entwickler mitgeteilt wurden. Alle sieben sind mit
|
||||
> 2.5.0/2.5.1 behoben — Einzelheiten weiterhin in §8, jetzt mit Fix-Vermerk. Zwei Neuerungen aus
|
||||
> derselben Runde gehen über die ursprünglichen Befunde hinaus und sind in §5.1, §7.2 und §2.2
|
||||
> eingearbeitet: `exitCurrentApp` ist jetzt als harter Kill dokumentiert, und `BuildInfo.targets` liegt
|
||||
> im NuGet-Paket selbst.
|
||||
|
||||
Das Deploymentcenter (`https://dc.mhdf.de`) bündelt fünf Dienste, die IBKRTrader bisher
|
||||
alle nicht hat: **Lizenz**, **Watchdog**, **Error-Reporting**, **Bugtracker** und
|
||||
**UpdateService inkl. Erstinstallation**. Dieses Dokument beschreibt, in welcher
|
||||
Reihenfolge sie angebunden werden, was dabei am Projekt vorher aufgeräumt werden muss
|
||||
und wo die vom Deploymentcenter vorgeschlagenen Muster für ein *handelndes* System
|
||||
nicht ohne Weiteres passen.
|
||||
|
||||
---
|
||||
|
||||
## 0. Ausgangslage
|
||||
|
||||
| | Zustand heute |
|
||||
|---|---|
|
||||
| DC-Anbindung | **keine** — kein Treffer für `Deploymentcenter`, `dc.mhdf.de` oder `DC_TOKEN` im Quelltext |
|
||||
| Version | Zeichenkette `"1.0.0"` in `App/Program.cs:34` und `Daemon/Program.cs:43`, keine `<Version>`, keine `Directory.Build.props` |
|
||||
| Globale Fehlerbehandlung | **keine** — weder `AppDomain.UnhandledException` noch `TaskScheduler.UnobservedTaskException` |
|
||||
| Auslieferung | `dotnet publish` von Hand, `deploy/README.md`, systemd-Unit |
|
||||
| CI | Gitea Actions, Matrix `ubuntu-latest` / `windows-latest`, Checkout **nur** von IBKRTrader |
|
||||
|
||||
Referenzintegration ist **Predictalytics** (`src/Predictalytics.Hosting/Dc*.cs`,
|
||||
`docs/BETRIEB-Deploymentcenter.md`). Sie liegt auf SDK-Stand 2.2 und ist damit
|
||||
unvollständig — siehe [§8](#8-befunde-und-offene-punkte), Punkt D2. Übernommen wird
|
||||
das Muster, nicht der Code eins zu eins.
|
||||
|
||||
Der Einbauort ist **`IBKRTrader.Hosting`**: den Host teilen sich Shell und Daemon
|
||||
(`AppHostBuilder`), damit gilt jede Anbindung automatisch für beide Betriebsformen.
|
||||
`IBKRTrader.Core` bleibt frei davon — der Core soll keine Netzabhängigkeit zu einem
|
||||
Verwaltungsserver bekommen.
|
||||
|
||||
---
|
||||
|
||||
## 1. Voraussetzungen im Deploymentcenter (vor jedem Code)
|
||||
|
||||
Ohne diese Schritte läuft nichts von dem, was danach kommt:
|
||||
|
||||
1. **Projekt `ibkrtrader` anlegen** (WebUI → Projekte). Slug wird von Lizenz,
|
||||
UpdateService, Fehler-Stream und Bugtracker gemeinsam benutzt.
|
||||
2. **Offline-Gnadenfrist festlegen** (`default_cache_ttl_hours`). Vorgabe 168 h.
|
||||
Für einen Handelsdienst, der auch bei gestörter Internetverbindung weiterlaufen
|
||||
soll, ist das die entscheidende Stellschraube — siehe [§5](#5-lizenz).
|
||||
3. **Lizenz für `ibkrtrader` ausstellen.** Ohne sie kommt die Anwendung nach dem
|
||||
ersten Release nicht mehr an ihre Updates (Zugangsschutz seit 2.4).
|
||||
4. **Master-Token** mit `watchdog:ping`, `bugtracker:report` — für die Anwendung.
|
||||
Zweites Token mit `updateservice:publish` — für die Release-Pipeline, gehört
|
||||
**nicht** in die Anwendung.
|
||||
5. **Installationskonto** (Rolle `installer`) für die Erstinstallation.
|
||||
6. **Evaluator-Cron prüfen.** `* * * * * php cli/tick.php --quiet`. Ohne ihn bleibt
|
||||
ein abgestürzter Trading-Daemon im Dashboard dauerhaft grün — der Watchdog wäre
|
||||
dekorativ. Derselbe Job erzeugt auch den Release-Zugangsschutz.
|
||||
|
||||
---
|
||||
|
||||
## 2. Build-Grundlage
|
||||
|
||||
### 2.1 `Directory.Build.props` mit `<Version>`
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.0.0</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
`pack-and-deploy` liest die Version aus der Hauptassembly und **bricht bei einer
|
||||
Abweichung zu `--version` ab**. Der Grund ist unangenehm genug dafür: Wird `1.0.1`
|
||||
als `1.0.2` veröffentlicht, aktualisiert die gesamte Installationsbasis, meldet
|
||||
danach weiterhin `1.0.1`, hält das Release erneut für neu — und aktualisiert bei
|
||||
jedem Start wieder.
|
||||
|
||||
Gleichzeitig entfallen die beiden hartkodierten `"1.0.0"` in den `Program.cs`;
|
||||
sie werden durch `BuildInfo.Version` ersetzt.
|
||||
|
||||
### 2.2 SDK-Anbindung — entschieden, in zwei Stufen
|
||||
|
||||
`Deploymentcenter.Client` (`netstandard2.0;net8.0`) wird von `net10.0` problemlos
|
||||
konsumiert. Seit SDK 2.5.0 liegt `BuildInfo.targets` im Paket selbst
|
||||
(`build/`-Ordner, von NuGet automatisch importiert) — der frühere `<Import>` per
|
||||
Cross-Repo-Pfad ist damit **nicht mehr nötig**. Das entscheidet die Frage
|
||||
grundsätzlich zugunsten eines echten NuGet-Bezugs. Geprüft (2026-08-14): Weder
|
||||
`https://dc.mhdf.de/nuget/` noch die Gitea-Package-Registry unter
|
||||
`http://192.168.178.10:8418/api/packages/Richard/nuget/` führen aktuell ein
|
||||
Paket `Deploymentcenter.Client` — es gibt also noch keine Quelle, gegen die eine
|
||||
`PackageReference` auflösen könnte.
|
||||
|
||||
**Deshalb zweistufig:**
|
||||
|
||||
| Stufe | Weg | Zustand |
|
||||
|---|---|---|
|
||||
| **Jetzt (umgesetzt)** | Cross-Repo-`ProjectReference` (Muster Predictalytics), `Deploymentcenter.BuildInfo.targets` weiterhin per `<Import>` eingebunden — der pfadbasierte Import funktioniert laut Leitfaden unverändert weiter, auch wenn er nicht mehr nötig wäre | funktioniert sofort, keine neue Infrastruktur, keine Push-Entscheidung nötig |
|
||||
| **Folgeschritt (offen)** | `dotnet pack` im Deploymentcenter-Repo, Push nach `.../api/packages/Richard/nuget/`, `NuGet.config` um Quelle `gitea` mit `packageSourceMapping` auf `Deploymentcenter.*` ergänzen, `<Import>` durch `<PackageReference Include="Deploymentcenter.Client" Version="2.5.1" />` ersetzen | braucht einen Push in ein fremdes Repo (Deploymentcenter) und Package-Registry-Zugangsdaten — bewusst nicht Teil dieses Durchlaufs |
|
||||
|
||||
Der Cross-Repo-Bezug bricht die Gitea-CI von IBKRTrader (der Runner checkt nur
|
||||
dieses eine Repo aus) — siehe **P5**. Das ist mit der Wahl von Stufe 1 bewusst in
|
||||
Kauf genommen, weil eine funktionierende lokale Integration wichtiger ist als eine
|
||||
grüne CI, die eine noch nicht existierende Paketquelle voraussetzt.
|
||||
|
||||
### 2.3 `BuildInfo`
|
||||
|
||||
`Deploymentcenter.BuildInfo.targets` in die csproj von `IBKRTrader.Hosting`
|
||||
importieren. Die Klasse entsteht in `$(RootNamespace)`, also `IBKRTrader.Hosting.BuildInfo`
|
||||
— erreichbar ohne `using`. Liefert `Version`, `GitCommit`, `GitCommitShort`,
|
||||
`BuildDateUtc`, `Channel`, `Summary`.
|
||||
|
||||
> **Erledigt mit 2.5.1** (vormals §8 D4): `BuildDateUtc` trug die volle Uhrzeit,
|
||||
> `WriteOnlyWhenDifferent` griff dadurch nie, und jedes importierende Projekt wurde
|
||||
> bei jedem Build neu übersetzt. Seit 2.5.1 ist das Feld tagesgenau — höchstens ein
|
||||
> Neubau pro Tag. Vorbedingung: die Targets-Datei im Schwester-Repo muss auf 2.5.1
|
||||
> oder neuer stehen (`git -C ../Deploymentcenter log -1 --oneline -- client-dotnet/Deploymentcenter.Client/Deploymentcenter.BuildInfo.targets`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Konfiguration und gemeinsame Bausteine
|
||||
|
||||
### 3.1 `DcConfig` (kompiliert, nicht einstellbar)
|
||||
|
||||
```csharp
|
||||
namespace IBKRTrader.Hosting.Deploymentcenter;
|
||||
|
||||
public static class DcConfig
|
||||
{
|
||||
public const string BaseUrl = "https://dc.mhdf.de"; // bewusst KEINE Einstellung
|
||||
public const string ProductSlug = "ibkrtrader";
|
||||
public const string MonitorGroup = "Trading";
|
||||
public static string AppVersion => BuildInfo.Version;
|
||||
public static string GitCommitShort => BuildInfo.GitCommitShort;
|
||||
}
|
||||
```
|
||||
|
||||
Die Basis-URL entscheidet, wohin die Lizenzprüfung geht und woher ausführbarer
|
||||
Code kommt. Einstellbar wäre sie eine Einladung, die Anwendung auf einen
|
||||
gefälschten Update-Server zu zeigen.
|
||||
|
||||
### 3.2 Neuer Abschnitt in `AppSettings`
|
||||
|
||||
Ergänzt `src/IBKRTrader.Core/Settings/AppSettings.cs` um
|
||||
`DeploymentcenterSettings` (gleiches Muster wie `LoggingSettings`, mit
|
||||
`Category`/`DisplayName`/`Description`, damit es in der Settings-Ansicht auftaucht):
|
||||
|
||||
| Feld | Vorgabe | Bedeutung |
|
||||
|---|---|---|
|
||||
| `ApiToken` | *(leer)* | `Authorization: Bearer`. Leer = kein Heartbeat, keine Fehlermeldung. `[PasswordPropertyText]` |
|
||||
| `HeartbeatEnabled` | `true` | Dead-Man's-Switch |
|
||||
| `MonitorSource` | *(Hostname)* | Monitorname im Dashboard |
|
||||
| `MonitorInstance` | `default` | mehrere Instanzen (EU/US) desselben Dienstes |
|
||||
| `HeartbeatIntervalSeconds` | `60` | `warning` nach 2×, `down` nach 4× |
|
||||
| `ReportErrors` | `true` | Error/Fatal an den Fehler-Stream |
|
||||
| `CheckUpdatesOnStart` | `true` | prüft nur, installiert nie von selbst |
|
||||
| `UpdateChannel` | `prod` | `prod`, `beta`, `dev` |
|
||||
| `LicenseKey` | *(leer)* | nur Rückfall; Regelfall ist der verschlüsselte Cache |
|
||||
|
||||
`ApiToken` und `LicenseKey` sind schutzbedürftig und laufen über
|
||||
`SecretProtection` (AES-256-GCM at rest), wie die übrigen Geheimnisse des Projekts.
|
||||
|
||||
### 3.3 `DcApiClient`
|
||||
|
||||
Schlanker JSON-Client für Heartbeat und Fehler-Stream (Lizenz und Update bringen
|
||||
ihren eigenen mit). Übernimmt von Predictalytics:
|
||||
|
||||
- `DcApiException` mit stabilem `error.code` aus dem Umschlag
|
||||
`{"status":"error","error":{"code":"…"}}` — reagiert wird auf den Code, nie auf den Text.
|
||||
- `IsPermanent` für `401`/`403`/`unauthorized`: danach wird nicht weiter versucht.
|
||||
- Zeitgrenze 10 s. **Ein nicht erreichbares Deploymentcenter darf den Handel
|
||||
niemals beeinflussen** — jeder Aufruf ist best effort.
|
||||
|
||||
---
|
||||
|
||||
## 4. Watchdog
|
||||
|
||||
Neuer Worker `DcHeartbeatWorker` in `IBKRTrader.Hosting`, registriert wie die
|
||||
übrigen (`IWorker` + `AddHostedService`), damit er in der Worker-Ansicht
|
||||
auftaucht und manuell auslösbar ist.
|
||||
|
||||
`POST /api/watchdog/v1/ping` alle *n* Sekunden mit `source`, `instance`,
|
||||
`interval`, `os`, `version = BuildInfo.Version`, `group = "Trading"`.
|
||||
|
||||
### 4.1 `checks` — was IBKRTrader wirklich über sich weiß
|
||||
|
||||
Ein Heartbeat beweist nur, dass ein Timer läuft. Der klassische Fehlerfall dieses
|
||||
Projekts ist ein anderer: der Prozess lebt, aber die TWS-Verbindung ist seit einer
|
||||
Stunde tot und es kommen keine Kurse mehr.
|
||||
|
||||
| Check | Quelle | `ok` wenn |
|
||||
|---|---|---|
|
||||
| `db` | EF/`MySqlConnector`, `SELECT 1`, gedeckelt auf 5 s | Verbindung steht |
|
||||
| `ibkr_gateway` | `IBKRGatewayService` | verbunden **oder** `UseTwsApi = false` (NullBroker ist ein gewollter Zustand, kein Fehler) |
|
||||
| `market_data` | `IBKRMarketDataRepository` | letzter Tick jünger als *x* min — nur wenn TWS aktiv |
|
||||
| `worker_engine` | `WorkerEngine` | kein Worker im Fehlerzustand |
|
||||
|
||||
Bewusst **kein** Check auf „Trading aktiv": `TradingEnabled = false` ist ein
|
||||
gewollter Betriebszustand und würde den Monitor sonst dauerhaft auf `warning`
|
||||
halten. Er wandert als Metrik mit.
|
||||
|
||||
### 4.2 `metrics`
|
||||
|
||||
`uptime_sec`, `trading_enabled` (0/1), `open_positions`, `orders_today`,
|
||||
`account_value`, `workers_running`, `memory_mb`, `decision_journal_lag_sec`.
|
||||
|
||||
Serverseitig 14 Tage Verlauf mit Abweichung zum 7-Tage-Mittel desselben Monitors —
|
||||
damit lässt sich „heute plötzlich 40 Orders statt 3" erkennen, ohne im
|
||||
Deploymentcenter Schwellwerte für jedes Projekt zu pflegen.
|
||||
|
||||
### 4.3 Sauberes Beenden
|
||||
|
||||
Beim geordneten Herunterfahren (SIGTERM/systemd, Fenster schließen) geht ein Ping
|
||||
mit `status: "stopped"` raus. Ohne ihn folgt wenige Minuten nach jedem
|
||||
planmäßigen Neustart ein Fehlalarm. Der Aufruf gehört in `StopAsync`
|
||||
**vor** das Beenden der übrigen Dienste und braucht eine eigene, kurze Zeitgrenze
|
||||
(3 s), damit ein hängender Monitoring-Server das Herunterfahren nicht blockiert.
|
||||
|
||||
### 4.4 Hierarchie
|
||||
|
||||
Läuft der Daemon auf einem eigenen Host, gehört im WebUI unter
|
||||
**WatchDog → System-Hierarchie** die *Übergeordnete Entität* auf diesen Host
|
||||
gesetzt. Sonst erzeugt ein Maschinenausfall zwei Meldungen für ein Problem.
|
||||
|
||||
---
|
||||
|
||||
## 5. Lizenz
|
||||
|
||||
`LicenseGuard` in `IBKRTrader.Hosting`, aufgerufen aus `RunStartupChecks` —
|
||||
nach der Zeitzone (Logausgaben) und **vor** dem Start der Worker.
|
||||
|
||||
- Produkt-Slug `ibkrtrader`, Hardware-ID v2 (`2:win:…` / `2:lin:…`).
|
||||
- `LicenseClient.DefaultAppVersion = BuildInfo.Version` einmalig beim Start,
|
||||
sonst trägt jede Installation in der Aktivierungsliste dieselbe `1.0.0`.
|
||||
- Revalidierung zur Laufzeit alle 12 h; Warnung, wenn die Gnadenfrist in unter
|
||||
48 h abläuft.
|
||||
- **`LicenseConfig.SetStorageDirectory(AppPaths.ConfigFile("license"))`** —
|
||||
ausdrücklich setzen. Die Vorgabe des SDK löst unter Linux über `$XDG_CONFIG_HOME`
|
||||
bzw. `$HOME` auf; ein systemd-Dienst mit `User=ibkrtrader` und ohne Heimatverzeichnis
|
||||
landet im Rückfall auf `AppContext.BaseDirectory/license`, also unter `/opt` —
|
||||
dort ist nichts schreibbar, und es gäbe **keinen Offline-Cache**. Mit `AppPaths`
|
||||
fügt sich die Ablage in die bereits vorhandene Verzeichnisauflösung des Projekts ein.
|
||||
**Erledigt/entschärft mit 2.5.1** (vormals §8 D4): der Rückfall ohne `$HOME` zeigt
|
||||
jetzt serverseitig auf `/var/lib/<slug>` statt ins Installationsverzeichnis — unser
|
||||
eigenes explizites Setzen bleibt trotzdem die robustere Wahl und ist unabhängig vom
|
||||
SDK-Rückfall.
|
||||
⚠️ **Breaking Change 2.5.1:** `SetStorageDirectory` hängt jetzt selbst `<slug>/license`
|
||||
an (vorher wurde der Pfad roh benutzt). Der Aufruf wird zu
|
||||
`LicenseConfig.SetStorageDirectory(AppPaths.Config)` — **ohne** das eigene
|
||||
`"license"`-Suffix, sonst entstünde `…/license/ibkrtrader/license`.
|
||||
|
||||
### 5.1 Kopflose Schalter
|
||||
|
||||
Konvention aus dem Lizenz-Leitfaden, im Daemon umzusetzen (das SDK bringt keine CLI mit):
|
||||
|
||||
```
|
||||
IBKRTrader.Daemon --license-status
|
||||
IBKRTrader.Daemon --license-set-key XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
|
||||
```
|
||||
|
||||
`--license-deactivate` **nicht** verdrahten: es verlangt den `shared_key` des
|
||||
Servers, der gehört nicht in eine ausgelieferte Anwendung. Freigeben läuft über
|
||||
das WebUI (Hardware-Liste → „Freigeben").
|
||||
|
||||
> **Geändert mit 2.5.0:** der `update-agent` nimmt den Lizenzschlüssel nicht mehr
|
||||
> als `--license-key`-Argument entgegen — `ps` zeigt Prozessargumente jedem Nutzer
|
||||
> der Maschine, dieselbe Begründung wie beim Crontab-Verzicht in UPGRADE §5. Der
|
||||
> Weg ist jetzt die Umgebungsvariable `DC_LICENSE_KEY` (`LaunchUpdateAgent` setzt
|
||||
> sie selbst, wenn `licenseKey` übergeben wird — kein eigener Code nötig). Unsere
|
||||
> `--license-set-key`-Implementierung ruft also `LicenseClient.ValidateAsync`
|
||||
> direkt auf und schreibt **nicht** `--license-key` in einen eigenen Aufruf des
|
||||
> Agenten.
|
||||
>
|
||||
> Ebenfalls neu: `EnsureLicensedAsync()` auf `LicenseClient` — nimmt den
|
||||
> zwischengespeicherten Schlüssel, fragt bei Bedarf über `ILicensePrompt` nach,
|
||||
> prüft, fragt bei Ablehnung erneut. `allowPrompt:false` lehnt ohne Cache ab, statt
|
||||
> im Daemon auf eine Eingabe zu warten, die nie kommt — **das ist für den Daemon
|
||||
> die richtige Einstiegsmethode**, nicht das manuelle Zusammensetzen aus
|
||||
> `TryGetCachedKey` + `ValidateAsync`.
|
||||
|
||||
### 5.2 Reaktion — hier weicht IBKRTrader bewusst vom Leitfaden ab
|
||||
|
||||
Die DC-Doku empfiehlt bei einem endgültigen Urteil `Environment.Exit(1)`.
|
||||
**Für einen Dienst mit offenen Positionen ist das die falsche Reaktion.** Ein
|
||||
abgelaufener Vertrag ist kein Grund, ein Portfolio unbeaufsichtigt zu lassen —
|
||||
Stop-Loss- und Exit-Logik müssen weiterlaufen, sonst richtet die Lizenzdurchsetzung
|
||||
mehr Schaden an als der unlizenzierte Betrieb.
|
||||
|
||||
Vorschlag — drei Stufen statt zwei:
|
||||
|
||||
| Ergebnis | Zustände | Reaktion |
|
||||
|---|---|---|
|
||||
| `IsValid` | `valid`, `valid_offline` | Normalbetrieb |
|
||||
| `IsTransient` | `server_unavailable`, `cache_expired` | **kein Urteil, nur keine Auskunft.** Warnen, weiterlaufen, beim nächsten Durchlauf erneut fragen |
|
||||
| Urteil | `revoked`, `expired`, `not_found`, `activation_limit`, `suspended`, `clock_rollback` | **Sperrbetrieb:** `TradingEnabled` hart auf `false`, keine neuen Einstiege, Risiko-/Exit-Worker und Buchhaltung laufen weiter, Meldung an Watchdog (`status: "warning"`) und Fehler-Stream. Beendet wird erst, wenn keine Position mehr offen ist — oder auf ausdrückliche Anweisung |
|
||||
|
||||
Bei der Desktop-Shell darf es beim Start weiterhin ein Lizenzfenster geben; der
|
||||
**Daemon** darf niemals auf eine Eingabe warten.
|
||||
|
||||
---
|
||||
|
||||
## 6. Error-Reporting
|
||||
|
||||
Zwei Wege in `POST /api/errors/v1/report`, beide in `IBKRTrader.Hosting`:
|
||||
|
||||
1. **Senke am `LoggingService`.** `LoggingService.EntryWritten` existiert bereits
|
||||
und ist genau dafür gedacht (die Oberfläche hängt schon dran). Ein
|
||||
`DcErrorSink` filtert auf `AppLogLevel.Error` und meldet mit
|
||||
`level: "error"`, `build: BuildInfo.Version`, `file`/`line` aus dem Stacktrace,
|
||||
`context` mit `GitCommitShort` und `CorrelationId`/`SignalId`, wenn vorhanden.
|
||||
Die `SignalId` mitzuschicken ist der eigentliche Gewinn: eine Fehlermeldung
|
||||
lässt sich damit im Deploymentcenter direkt gegen das Entscheidungsjournal halten.
|
||||
2. **Globale Handler** — `AppDomain.UnhandledException` (`level: "fatal"`,
|
||||
**blockierend** gesendet, der Prozess ist gleich weg) und
|
||||
`TaskScheduler.UnobservedTaskException`. Beide fehlen im Projekt heute
|
||||
vollständig; sie müssen sowieso her.
|
||||
|
||||
### 6.1 Bremse
|
||||
|
||||
Verpflichtend, sonst verbrennt ein Fehler in einer Handelsschleife das
|
||||
serverseitige Rate-Limit binnen Sekunden:
|
||||
|
||||
- höchstens 20 Meldungen pro Minute,
|
||||
- derselbe Fehler höchstens alle 5 Minuten,
|
||||
- bei `401`/`403` dauerhaft abschalten,
|
||||
- bei `429` Intervall verdoppeln.
|
||||
|
||||
Betriebsbedingtes Rauschen gehört nicht in diese Bremse, sondern in die
|
||||
**Ignore-Regeln** des WebUI: dort wird weitergezählt, aber nicht gemeldet — und
|
||||
eine Alarmschwelle schlägt an, wenn ein bekannter Fehler plötzlich hundertmal so
|
||||
oft auftritt.
|
||||
|
||||
> **Nicht melden:** alles, was Kontodaten, Positionen oder Schlüsselmaterial im
|
||||
> Text führen könnte. Der Fehler-Stream ist eine externe Ablage. Vor dem Senden
|
||||
> wird der Text durch dieselbe Maskierung geschickt, die schon für die Logs gilt.
|
||||
|
||||
---
|
||||
|
||||
## 7. UpdateService, Erstinstallation und Release-Pipeline
|
||||
|
||||
### 7.1 Prüfen, nicht installieren
|
||||
|
||||
`DcUpdateService.CheckAsync` beim Start (`GET /api/updateservice/v1/check`).
|
||||
**Mit Zugangsdaten**, sonst gibt es seit 2.4 nur noch `401`:
|
||||
|
||||
```csharp
|
||||
var key = settings.Deploymentcenter.LicenseKey is { Length: > 0 } k
|
||||
? k
|
||||
: LicenseClient.TryGetCachedKey(DcConfig.ProductSlug);
|
||||
|
||||
var check = await new UpdateClient().CheckForUpdateAsync(
|
||||
baseUrl: DcConfig.BaseUrl,
|
||||
projectId: DcConfig.ProductSlug,
|
||||
currentVersion: BuildInfo.Version,
|
||||
channel: settings.Deploymentcenter.UpdateChannel,
|
||||
credentials: ReleaseCredentials.FromLicenseKey(key),
|
||||
cancellationToken: ct);
|
||||
|
||||
if (check.Unauthorized) { /* Lizenzproblem, KEIN Netzwerkfehler — eigene Meldung */ }
|
||||
```
|
||||
|
||||
Gefunden wird nur, installiert wird nichts von selbst. Ein Trading-Dienst, der
|
||||
sich unangekündigt neu startet, ist keine akzeptable Vorgabe.
|
||||
|
||||
- Kein Update → Info ins Log.
|
||||
- Update verfügbar → Warnung + Hinweis in der Shell.
|
||||
- Kritisch → zusätzlich Dialog (Shell) bzw. Watchdog-Event (Daemon).
|
||||
|
||||
### 7.2 Anwenden
|
||||
|
||||
Über `UpdateClient.LaunchUpdateAgent(..., licenseKey: key)`; der Agent bekommt
|
||||
`--restart`, `--wait-for-pid`, `--platform` von selbst, den Lizenzschlüssel seit
|
||||
2.5.0 **nicht mehr** als `--license-key`-Argument, sondern über die
|
||||
Umgebungsvariable `DC_LICENSE_KEY` (siehe §5.1). Vorher geht ein
|
||||
`status: "stopped"` an den Watchdog.
|
||||
|
||||
**Vorbedingung, die die Anwendung selbst prüfen muss:** kein offener Auftrag,
|
||||
keine laufende Order. Ein Update, das den Prozess zwischen Order-Absendung und
|
||||
Statusrückmeldung abräumt, hinterlässt eine Order bei IBKR ohne Gegenstück im
|
||||
Journal. Das Update wird also erst nach dem Handelsschluss bzw. bei leerem
|
||||
Auftragsbuch angeboten.
|
||||
|
||||
> **Verschärft mit 2.5.0:** `exitCurrentApp: true` ist jetzt ausdrücklich als
|
||||
> **harter Kill** dokumentiert — laufende `finally`-Blöcke, `IHostApplicationLifetime`
|
||||
> und Destruktoren kommen nicht mehr zum Zug. Für IBKRTrader heißt das: Solange
|
||||
> die Vorbedingung oben nicht sicher geprüft ist (offenes Auftragsbuch, laufende
|
||||
> Buchungstransaktion), muss `exitCurrentApp: false` gesetzt werden. Der Agent
|
||||
> wartet über `--wait-for-pid` ohnehin auf das Prozessende; das eigene
|
||||
> Herunterfahren läuft dann über `host.StopAsync(...)` wie in `App/Program.cs`
|
||||
> bereits vorhanden — **nicht** über `LaunchUpdateAgent` selbst. Zusätzlich
|
||||
> `waitTimeoutSeconds` heraufsetzen, falls das geordnete Beenden (EF-Flush,
|
||||
> Kestrel, Log-Flush, DB-Backup-Worker) länger als die Vorgabe von 60 s dauern
|
||||
> kann — läuft die Zeit ab, bricht der Agent ohne Änderung ab.
|
||||
|
||||
Für den Linux-Dienst siehe [§8 D3](#8-befunde-und-offene-punkte) — dafür gibt es
|
||||
im Deploymentcenter derzeit keinen fertigen Weg.
|
||||
|
||||
### 7.3 Was ausgeliefert wird — Ausschließen vs. Schützen
|
||||
|
||||
Das ist die Stelle, an der Updates in anderen Projekten schon Konfigurationen
|
||||
zerstört haben. Für IBKRTrader gilt:
|
||||
|
||||
| Datei | Liste | Begründung |
|
||||
|---|---|---|
|
||||
| `settings.example.json` | *(im Paket)* | Vorlage mit Platzhaltern — Erstinstallation braucht sie |
|
||||
| `settings.json` | `preservePatterns` | enthält DB-Zugang, DC-Token, Flex-Token. Muss ein Update überleben |
|
||||
| `appsettings.Local.json` | `preservePatterns` | Verbindungszeichenfolge des Zielsystems |
|
||||
| `master.key`, `openrouter.key` | `excludePatterns` | **darf nie ins Paket** |
|
||||
| `Logs/**`, `Backups/**` | `excludePatterns` | Laufzeitdaten |
|
||||
| `*.pdb`, `*.xml` | `excludePatterns` | Build-Artefakte |
|
||||
| `update-agent*` | bewusst entscheiden | mitliefern = aktualisiert sich selbst mit |
|
||||
|
||||
`pack-and-deploy` warnt von sich aus bei Dateien, die nach Zugangsdaten aussehen.
|
||||
Die Warnung ist ernst zu nehmen: Release-Pakete liegen zwar hinter einem
|
||||
Zugangsschutz, aber jeder lizenzierte Kunde kann sie auspacken.
|
||||
|
||||
### 7.4 `setup.json`
|
||||
|
||||
Beschreibt der Erstinstallation, was abzufragen ist; wird ins Paket kopiert.
|
||||
Entwurf für IBKRTrader:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": 1,
|
||||
"displayName": "IBKRTrader",
|
||||
"targets": [
|
||||
{ "id": "app", "file": "settings.json", "location": "install", "format": "json" }
|
||||
],
|
||||
"fields": [
|
||||
{ "key": "Database:Host", "label": "MySQL-Host", "type": "string" },
|
||||
{ "key": "Database:Database", "label": "Datenbank", "type": "string" },
|
||||
{ "key": "Database:User", "label": "Benutzer", "type": "string" },
|
||||
{ "key": "Database:Password", "label": "Passwort", "type": "secret" },
|
||||
{ "key": "IBKR:Host", "label": "TWS/Gateway", "default": "127.0.0.1" },
|
||||
{ "key": "IBKR:Port", "label": "Port (Paper 4002 / Live 4001)", "type": "int", "default": "4002" },
|
||||
{ "key": "Trading:ApplicationTimeZoneId", "label": "Betriebszeitzone", "default": "Europe/Berlin" },
|
||||
{ "key": "Deploymentcenter:BaseUrl", "source": "detect:baseurl", "type": "url" },
|
||||
{ "key": "Deploymentcenter:ApiToken", "label": "Token dieser Installation",
|
||||
"source": "provision", "scopes": ["watchdog:ping", "bugtracker:report"] },
|
||||
{ "key": "Deploymentcenter:MonitorSource", "source": "detect:hostname" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Zwei Punkte dazu:
|
||||
|
||||
- **`location: "install"` ist nur unter Windows richtig** — und auch dort nur,
|
||||
solange nicht nach `C:\Program Files` installiert wird. Siehe
|
||||
[§8 P2](#8-befunde-und-offene-punkte); solange `AppPaths` nicht korrigiert ist,
|
||||
passen `setup.json` und die tatsächliche Leseposition der Anwendung unter Linux
|
||||
nicht zusammen. **Dieser Punkt blockiert die Erstinstallation und gehört vor §7.4.**
|
||||
- Der Installer schreibt **Klartext**. `SecretProtection` muss also Klartext
|
||||
unverändert durchreichen und beim ersten Speichern verschlüsseln — die Weiche
|
||||
ist zu prüfen und im Test ausdrücklich als *aktiv genutzt* zu beschreiben, damit
|
||||
sie nicht eines Tages als tot entfernt wird.
|
||||
|
||||
### 7.5 Release-Pipeline
|
||||
|
||||
`scripts/release.ps1` und `scripts/release.sh` sind per `curl` von
|
||||
`/docs/release-template/` geholt (wie `release.md` empfiehlt, statt aus dem
|
||||
lokalen Schwester-Repo kopiert — damit ist sichergestellt, dass die kanonische
|
||||
Fassung im Projekt liegt) und **unverändert** gelassen. Nur
|
||||
`scripts/release.config.json` ist ausgefüllt:
|
||||
|
||||
```json
|
||||
{
|
||||
"project": "ibkrtrader",
|
||||
"csproj": "src/IBKRTrader.Daemon/IBKRTrader.Daemon.csproj",
|
||||
"runtimes": ["win-x64", "linux-x64"],
|
||||
"selfContained": true,
|
||||
"setupJson": "setup.json"
|
||||
}
|
||||
```
|
||||
|
||||
**Entschieden: nur der Daemon, keine Zwei-Produkte-Aufteilung.** IBKRTrader hat
|
||||
zwei Einstiegspunkte (`App` = Avalonia-Shell, `Daemon` = kopflos). Die Vorlage
|
||||
nimmt aber genau **eine** `csproj` für alle Runtimes in der Liste — sie ruft
|
||||
`dotnet publish <csproj> -r <rid>` in einer Schleife auf, es gibt keine
|
||||
Möglichkeit, App für `win-x64` und Daemon für `linux-x64` in **demselben** Lauf
|
||||
zu veröffentlichen, ohne das (laut Anleitung unveränderte) Skript anzufassen.
|
||||
Ursprünglich in diesem Dokument vorgeschlagen war genau das ("Windows = App,
|
||||
Linux = Daemon") — das ist mit der Vorlage so nicht umsetzbar und wurde beim
|
||||
Umsetzen korrigiert.
|
||||
|
||||
`IBKRTrader.Daemon` ist der bewusste Kandidat: laut README „plattformneutral"
|
||||
(`net10.0` ohne Plattformbindung), läuft kopflos auf beiden Zielsystemen und
|
||||
ist ohnehin die für den Serverbetrieb vorgesehene Form (`deploy/README.md`).
|
||||
Eine automatisierte Auslieferung der Avalonia-Shell (`App`) als
|
||||
Windows-Desktop-Distribution ist damit **nicht** Teil dieses Durchlaufs — dafür
|
||||
bräuchte es entweder einen zweiten Produkt-Slug (`ibkrtrader-app`, doppelte
|
||||
Lizenz-/Update-Pflege) oder ein angepasstes Release-Skript. Zurückgestellt, bis
|
||||
tatsächlich Bedarf an einer automatisierten Desktop-Auslieferung besteht — bis
|
||||
dahin bleibt `dotnet run --project src/IBKRTrader.App` bzw. eine manuelle
|
||||
`dotnet publish` der übliche Weg für die Shell.
|
||||
|
||||
Zugangsdaten kommen aus `DC_FTP_HOST` / `DC_FTP_USER` / `DC_FTP_PASS` / `DC_TOKEN`,
|
||||
`.dc-tools/` und `artifacts/` sind in der `.gitignore`.
|
||||
|
||||
**`packager.config.json` (`excludePatterns`/`preservePatterns`) ist nicht Teil
|
||||
dieses Durchlaufs.** Sie liegt "neben dem Werkzeug", also unter `.dc-tools/` —
|
||||
das entsteht erst, wenn `pack-and-deploy` beim ersten echten Lauf von
|
||||
`release.ps1`/`release.sh` selbst heruntergeladen wird (Prüfsummen-geprüft).
|
||||
Ein erster Release-Lauf braucht `DC_FTP_*`/`DC_TOKEN` und ist damit eine
|
||||
credentialierte, vom Betreiber auszulösende Aktion. **Sicherheitsnetz bis
|
||||
dahin:** Der Packager bricht seit 2.5.0 ohnehin hart ab, wenn er im
|
||||
Publish-Verzeichnis nach Zugangsdaten aussehende Dateien findet (Name *und*
|
||||
Inhalt) — die in §7.3 beschriebenen Dateien (`settings.json`,
|
||||
`appsettings.Local.json`) sind ohnehin gitignored und landen nur dann im
|
||||
Publish-Output, wenn sie lokal im Projektverzeichnis liegen (`<None Update
|
||||
Condition="Exists(...)">` in den `.csproj`-Dateien kopiert sie dann mit). Bricht
|
||||
`release.ps1` mit einem Hinweis auf eine vermutete Zugangsdatei ab, ist das der
|
||||
Packager, der genau das abfängt — kein Fehlschlag, sondern die Absicherung, die
|
||||
2.5.0 dafür eingeführt hat.
|
||||
|
||||
---
|
||||
|
||||
## 8. Befunde und offene Punkte
|
||||
|
||||
> **Status 2026-08-23:** P1–P4 sind umgesetzt (Einzelheiten in §9). **P5 ist eingetreten**,
|
||||
> P6 entscheidet sich erst beim ersten Release. Der ursprüngliche Vermerk von 2026-08-14:
|
||||
>
|
||||
> **Status 2026-08-14:** Alle sieben ursprünglich an den Deploymentcenter-Entwickler
|
||||
> gemeldeten Befunde (D1–D7) sind mit den Versionen 2.5.0/2.5.1 behoben — bestätigt über
|
||||
> `GET /api/updateservice/v1/changelog?since=2.4`. Der Abschnitt bleibt als Nachweis
|
||||
> stehen, jeder Punkt trägt jetzt einen Fix-Vermerk. Die Projekt-Befunde (P1–P6) sind
|
||||
> davon unabhängig und weiterhin offen — sie werden in diesem Durchlauf abgearbeitet.
|
||||
|
||||
### Am Projekt (P) — muss vor der Integration bereinigt werden
|
||||
|
||||
**P1 · Echte Zugangsdaten als Vorgabewerte im Quelltext.**
|
||||
`src/IBKRTrader.Core/Settings/AppSettings.cs:14-35` trägt Host, Datenbankname,
|
||||
Benutzer und **Passwort** einer produktiven MariaDB als Vorgabe. Die Datei ist
|
||||
versioniert, die Werte stehen damit auch in der Git-Historie — `settings.json`
|
||||
im `.gitignore` hilft dagegen nicht. Das Passwort ist als kompromittiert zu
|
||||
behandeln und zu wechseln; die Vorgaben gehören auf Platzhalter. Zusätzlich
|
||||
brisant, weil die Vorlage künftig im Release-Paket ausgeliefert wird.
|
||||
|
||||
> **Verschärft mit 2.5.0:** der Packager **bricht jetzt hart ab**, wenn er
|
||||
> Zugangsdaten im Publish-Verzeichnis erkennt (Dateiname *und* Inhalt, u. a.
|
||||
> gefülltes `Password=`) — vorher nur eine Warnung, die auf der Preserve-Liste
|
||||
> sogar unterdrückt wurde. Damit ist P1 nicht mehr „sollte bereinigt werden",
|
||||
> sondern **release-blockierend**: Ohne Fix lässt sich `settings.example.json`
|
||||
> gar nicht mehr veröffentlichen.
|
||||
|
||||
**P2 · `AppPaths` fällt unter Windows auf einen Linux-Pfad zurück.**
|
||||
`Configuration/AppPaths.cs:57-71`: ist das Binärverzeichnis nicht beschreibbar,
|
||||
gilt der FHS-Rückfall `/etc/ibkrtrader`. Unter Windows löst .NET das zu
|
||||
`C:\etc\ibkrtrader` auf. Genau dieser Fall tritt ein, sobald der DC-Installer
|
||||
seinen Windows-Vorschlag `Programme\<slug>` benutzt — dann liegen `settings.json`,
|
||||
`master.key` und `Logs/` in `C:\etc\`. Nötig: plattformabhängiger Rückfall
|
||||
(`%ProgramData%\IBKRTrader` bzw. `%APPDATA%`).
|
||||
|
||||
**P3 · Keine globalen Ausnahmebehandler.** Ohne
|
||||
`AppDomain.UnhandledException` und `TaskScheduler.UnobservedTaskException` gibt es
|
||||
keine `fatal`-Meldungen — und heute auch keinen Logeintrag, wenn der Prozess
|
||||
wegbricht. Unabhängig vom Deploymentcenter überfällig.
|
||||
|
||||
**P4 · Version an zwei Stellen hartkodiert.** `App/Program.cs:34` und
|
||||
`Daemon/Program.cs:43`. Ohne `Directory.Build.props` bricht `pack-and-deploy` beim
|
||||
ersten Release ab — zu Recht.
|
||||
|
||||
**P5 · CI kennt das Schwester-Repo nicht.** `.gitea/workflows/build.yml` checkt
|
||||
nur IBKRTrader aus. Eine `ProjectReference` nach `..\..\..\..\Deploymentcenter\…`
|
||||
macht den Build auf beiden Matrix-Zielen rot. Siehe die Entscheidung in §2.2.
|
||||
|
||||
> **Seit 2026-08-23 eingetreten, nicht mehr nur vorhergesagt.** Die
|
||||
> `ProjectReference` ist mit der Integration auf `main` gelandet, die CI ist
|
||||
> damit rot. Das war die bewusst in Kauf genommene Folge der Interimslösung aus
|
||||
> §2.2 — lokal baut die Projektmappe, sobald das Schwester-Repo daneben liegt.
|
||||
> Auflösung ist Schritt 10: SDK als NuGet-Paket in die Gitea-Registry, dann
|
||||
> `PackageReference` statt Cross-Repo-Pfad.
|
||||
|
||||
**P6 · Konfiguration liegt im Installationsverzeichnis.** Unter Windows schreibt
|
||||
`AppPaths` neben die Binärdatei. Das ist mit `preservePatterns` beherrschbar
|
||||
(§7.3), muss aber beim ersten Release stimmen — ein Update, das `settings.json`
|
||||
überschreibt, nimmt einer laufenden Installation Datenbank, Token und Flex-Zugang
|
||||
gleichzeitig.
|
||||
|
||||
### Am Deploymentcenter (D) — gemeldet, Stand vor 2.5.0
|
||||
|
||||
**D1 · Der Lizenzschlüssel als Update-Zugang war nirgends vollständig erklärt.**
|
||||
✅ **Behoben (2.5.1, Doku).** Laut Changelog explizit als „Lückenschluss" genannt:
|
||||
„Woher die Anwendung den Lizenzschlüssel für den Update-Zugang nimmt
|
||||
(`TryGetCachedKey`)". Für uns bleibt zusätzlich `EnsureLicensedAsync()` (neu in
|
||||
2.5.0) relevant — siehe §5.1.
|
||||
|
||||
**D2 · Predictalytics wurde vom Zugangsschutz ausgesperrt.**
|
||||
✅ **Bestätigt und behoben (2.5.1).** Der Changelog bestätigt den Verdacht wörtlich:
|
||||
„bei Predictalytics ist genau das passiert". Fix: Zugangsschutz ist jetzt je
|
||||
Produkt abschaltbar (WebUI → UpdateService → Zugangsschutz) — für Installationsbasen
|
||||
ohne SDK 2.4 lässt sich das Produkt ausnehmen, mit Schlüssel neu ausliefern, danach
|
||||
wieder einschalten. **Für IBKRTrader heißt das:** genau diese Falle von Anfang an
|
||||
vermeiden, indem der erste ausgelieferte Build bereits `licenseKey` mitgibt (§7.5,
|
||||
§9 Reihenfolge).
|
||||
|
||||
**D3 · Für einen systemd-Dienst gab es keinen dokumentierten Update-Weg.**
|
||||
✅ **Behoben (2.5.1, Doku).** Laut Changelog ergänzt: „wie ein laufender
|
||||
systemd-Dienst aktualisiert wird". Bleibt in §7.2/§8-P laufend zu beobachten, ob
|
||||
die neue Beschreibung mit unserem tatsächlichen Vorgehen (`--target-dir`,
|
||||
Dienstbenutzer-Rechte) übereinstimmt — bei der Umsetzung von Aufgabe „Update-Prüfung
|
||||
mit ReleaseCredentials" gegenprüfen.
|
||||
|
||||
**D4 · `LicenseConfig.GetStorageDirectory`/`SetStorageDirectory` war für Dienste
|
||||
unglücklich.** ✅ **Behoben, mit Breaking Change (2.5.1).** Zwei Korrekturen:
|
||||
(a) Rückfall ohne `$HOME` zeigt jetzt auf `/var/lib/<slug>` statt ins
|
||||
Installationsverzeichnis. (b) `SetStorageDirectory` hängt jetzt selbst
|
||||
`<slug>/license` an, statt den Pfad roh zu verwenden — **das ändert unseren
|
||||
Aufruf**, siehe §5 (`AppPaths.Config` statt `AppPaths.ConfigFile("license")`).
|
||||
|
||||
**D5 · `BuildInfo.targets` verhinderte inkrementelle Builds.**
|
||||
✅ **Behoben (2.5.1).** `BuildDateUtc` ist jetzt tagesgenau statt mit voller Uhrzeit,
|
||||
`WriteOnlyWhenDifferent` greift wieder. Siehe §2.3.
|
||||
|
||||
**D6 · Die Empfehlung „bei einem Urteil beenden" passte für handelnde Systeme nicht.**
|
||||
✅ **Behoben (2.5.1, Doku)** — als Lückenschluss genannt: „was statt
|
||||
`Environment.Exit(1)` zu tun ist, wenn Beenden die gefährlichere Option wäre".
|
||||
Unser Sperrbetrieb-Ansatz (§5.2) bleibt die konkrete Umsetzung für IBKRTrader.
|
||||
**Zusätzlich, über D6 hinausgehend:** 2.5.0 dokumentiert `exitCurrentApp: true`
|
||||
jetzt als harten Kill ohne `finally`/Lifecycle-Hooks — praktisch relevanter für uns
|
||||
als die Lizenz-Frage allein, siehe §7.2.
|
||||
|
||||
**D7 · Kleinigkeiten** (README-Verweis auf `AGENT_RELEASE_GUIDE.md`, Rate-Limits
|
||||
nicht durchgängig beziffert). ✅ **Behoben (2.5.1, Doku)** — Changelog nennt
|
||||
„die Drosselungsgrenzen aller Endpunkte" als ergänzt; `docs/README.md` führt jetzt
|
||||
`/docs/release.md` und den Changelog-Endpunkt im Index.
|
||||
|
||||
---
|
||||
|
||||
## 9. Reihenfolge
|
||||
|
||||
| Schritt | Inhalt | Abhängig von | Stand |
|
||||
|---|---|---|---|
|
||||
| 0 | **P1** Zugangsdaten wechseln, Vorgaben auf Platzhalter | — | ✅ Vorgaben sind Platzhalter. **Die Rotation des geleakten Passworts steht weiterhin aus** (Nutzer-Aktion) |
|
||||
| 1 | **P2** `AppPaths`-Rückfall plattformabhängig, **P4** `Directory.Build.props` | — | ✅ 2026-08-23 |
|
||||
| 2 | SDK-Bezug Stufe 1: Cross-Repo-`ProjectReference` (§2.2) | — | ✅ 2026-08-23, inkl. Prüf-Target mit lesbarer Fehlermeldung |
|
||||
| 3 | `DcConfig`, `DeploymentcenterSettings`, `DcApiClient`, `BuildInfo` | 1, 2 | ✅ 2026-08-23 |
|
||||
| 4 | **P3** globale Handler + Fehler-Stream | 3 | ✅ 2026-08-23 (`DcCrashHandlers`, `DcErrorSink`, `DcErrorReporter`) |
|
||||
| 5 | Watchdog-Heartbeat inkl. `stopped` | 3 | ✅ 2026-08-23 (`DcHeartbeatWorker`) |
|
||||
| 6 | Lizenz mit Sperrbetrieb statt Abbruch (§5.2), `EnsureLicensedAsync` | 3 | ✅ 2026-08-23 (`LicenseGuard`) |
|
||||
| 7 | Update-Prüfung **mit** `ReleaseCredentials`, `exitCurrentApp:false` bei offenem Zustand | 6 (braucht den Schlüssel) | 🔶 Die **Prüfung** läuft beim Start. Das **Anwenden** ist es nicht: `LaunchAgent` ist fertig, aber kein Aufrufer fährt danach geordnet herunter |
|
||||
| 8 | `setup.json`, `preserve`/`exclude`, Release-Pipeline (Vorlage per `curl` von `/docs/release-template/`) | 1–7 | ✅ 2026-08-23 (`setup.json`, `scripts/release.*`). **Noch nie ausgeführt** – das erste Release steht aus |
|
||||
| 9 | Bugtracker-Baustein nach `AGENTS.md` / `.agents/rules` | Projekt-Slug angelegt | ⬜ offen — Projekt `ibkrtrader` muss im WebUI erst angelegt werden |
|
||||
| 10 | **Folgeschritt, nicht Teil dieses Durchlaufs:** `Deploymentcenter.Client` als NuGet-Paket in die Gitea-Registry pushen, `<Import>` durch `PackageReference` ersetzen, Gitea-CI (**P5**) auf den Paketbezug umstellen | 2 | ⬜ offen — siehe §2.2 |
|
||||
|
||||
Schritt 7 und 8 hängen zusammen: **der erste ausgelieferte Build muss die
|
||||
Zugangsdaten schon mitbringen.** Für ein Produkt, das noch nie veröffentlicht hat,
|
||||
entsteht `/releases/ibkrtrader/` mit dem ersten Upload und wird vom nächsten
|
||||
`tick.php`-Lauf geschützt — es gibt kein Zeitfenster, das SDK danach nachzurüsten.
|
||||
Wird Schritt 7 übersprungen, fällt die Tür hinter dem ersten Release zu (genau der
|
||||
Fall, der laut Changelog bei Predictalytics bereits eingetreten ist, siehe D2).
|
||||
|
||||
Schritt 9 (Bugtracker) setzt voraus, dass das Projekt `ibkrtrader` im
|
||||
Deploymentcenter-WebUI angelegt ist und ein Token mit `bugtracker:report`
|
||||
vorliegt — beides serverseitige Handarbeit, die dieser Durchlauf nicht auslösen
|
||||
kann. Wird zurückgestellt.
|
||||
@@ -0,0 +1,714 @@
|
||||
> ### 📦 Archiviert am 2026-08-23
|
||||
> Dieses Dokument wird **nicht mehr gepflegt**. Was davon noch offen ist, steht in der
|
||||
> [Roadmap](../ROADMAP.md) – dort und nur dort wird der Stand nachgeführt.
|
||||
>
|
||||
> Es bleibt erhalten, weil es die Analyse vor dem Umbau ist: Fundstellenverzeichnis, Aufwandsschaetzung und
|
||||
> was anders kam als geschaetzt. Die Portierung selbst ist abgeschlossen (L0–L6). Zum Nachschlagen also weiterhin richtig,
|
||||
> als Aufgabenliste nicht mehr.
|
||||
|
||||
---
|
||||
|
||||
# Analyse: Linux-Fähigkeit des IBKRTrader
|
||||
|
||||
> **UMGESETZT am 2026-08-07 (L0–L5).** Dieses Dokument ist die Analyse, die der Portierung
|
||||
> vorausging, und bleibt als Begründung erhalten – es beschreibt den Stand **vor** dem Umbau.
|
||||
> Was tatsächlich gebaut wurde, steht in der Phasen-Checkliste von
|
||||
> [../ARCHITECTURE.md](../ARCHITECTURE.md#l0l5--linux-portierung-avalonia-statt-winforms--2026-08-07);
|
||||
> die Pfadangaben im Fundstellenverzeichnis unten beziehen sich auf den alten Aufbau.
|
||||
>
|
||||
> Zwei Punkte sind gegenüber der Schätzung anders gekommen:
|
||||
> * Der Aufwand lag deutlich unter den veranschlagten 21–25 Personentagen, weil PolytraderSharp
|
||||
> dieselbe Portierung bereits durchlaufen hatte und als Vorlage diente (Avalonia-Pinnung,
|
||||
> toolkit-neutraler Contract, `AppTimeZone`).
|
||||
> * `InvariantGlobalization=true` – in der Analyse noch als Empfehlung für ein schlankes Image
|
||||
> genannt – wäre ein Fehler gewesen: ohne ICU fällt die Auflösung von Windows-Zeitzonen-IDs aus
|
||||
> und die feste `de-DE`-Formatierung des PDF-Exports kippt auf invariant. Beides lautlos.
|
||||
> Der Daemon setzt es deshalb ausdrücklich auf `false`.
|
||||
|
||||
> Stand: 2026-08-06. **Reine Analyse – es wurde kein Code geändert.**
|
||||
> Grundlage ist der Commit `b96a207` (main): 154 C#-Dateien, ~16.200 LOC, 6 Projekte, 165 Tests.
|
||||
> Alle Aussagen in Abschnitt 1–9 sind am Quelltext bzw. an einem Probe-Restore verifiziert;
|
||||
> Stellen, die nur plausibel und ungeprüft sind, stehen ausdrücklich als solche gekennzeichnet.
|
||||
|
||||
---
|
||||
|
||||
## 0. Kurzfassung
|
||||
|
||||
**Die gute Nachricht:** Der portabilitätskritische Teil der Anwendung ist bereits sauber. Kein
|
||||
einziges `DllImport`, keine Registry, kein WMI, kein DPAPI, kein `TimeZoneInfo.FindSystemTimeZoneById`.
|
||||
Die Verschlüsselung (`AesGcm`), die Datenbank (Pomelo/EF Core) und der TWS-Adapter laufen ohne
|
||||
Änderung auf Linux. Die WinForms-Kopplung im **Core** beschränkt sich auf **drei Dateien**, und
|
||||
jedes Modul trägt **genau eine** Form-Datei.
|
||||
|
||||
**Die eigentliche Arbeit** liegt an zwei Stellen und sie sind unterschiedlich groß:
|
||||
|
||||
| | Umfang | Aufwand |
|
||||
|---|---|---|
|
||||
| **A. Headless-Linux** (Worker, Trading, Accounting, Supervisor, REST/MCP – ohne UI) | ~15 Fundstellen, 3 Core-Dateien entkoppeln | **5–7 Personentage** |
|
||||
| **B. Desktop-Linux** (zusätzlich die komplette UI auf Avalonia) | ~1.300 LOC WinForms neu bauen | **+10–14 Personentage** |
|
||||
| **C. LiveCharts2** (heute existiert **kein einziges** Diagramm) | Neubau, keine Migration | **+1–3 Personentage** |
|
||||
|
||||
**Empfehlung:** Die beiden Schritte trennen. Eine headless Linux-Variante ist mit ~1 Woche
|
||||
erreichbar und liefert sofort den größten praktischen Nutzen (Dauerbetrieb auf einem Server statt
|
||||
auf dem Windows-Desktop). Der Avalonia-Umbau ist danach eine unabhängige Etappe, die man ohne
|
||||
Zeitdruck und ohne laufenden Betrieb zu gefährden angehen kann. Details in Abschnitt 11.
|
||||
|
||||
**Ein Punkt ist unabhängig von der UI der gefährlichste:** die Zeitzonen-Behandlung (Abschnitt 6).
|
||||
Der Code mischt heute `DateTime.Now` und `DateTime.UtcNow` und verwirft in `ParseExecutionTime`
|
||||
bewusst die Zeitzone. Auf einem Windows-Rechner mit `Europe/Berlin` und in einem Linux-Container
|
||||
mit `UTC` liefert **derselbe Code unterschiedliche Werte** – lautlos, ohne Fehler, in
|
||||
Buchungszeitstempeln. Das muss vor der Portierung geklärt werden, nicht danach.
|
||||
|
||||
---
|
||||
|
||||
## 1. Was **nicht** geändert werden muss
|
||||
|
||||
Diese Prüfungen sind negativ ausgefallen – das sind ersparte Personentage:
|
||||
|
||||
| Geprüft | Ergebnis |
|
||||
|---|---|
|
||||
| `DllImport` / `LibraryImport` / `Marshal` | **keine Fundstelle** |
|
||||
| Windows Registry, WMI, EventLog, `WindowsIdentity` | **keine Fundstelle** |
|
||||
| DPAPI / `ProtectedData` | **keine Fundstelle** – `SecretProtection` nutzt `AesGcm` + `SHA256`, voll portabel |
|
||||
| `TimeZoneInfo.FindSystemTimeZoneById` (Windows- vs. IANA-IDs) | **keine Fundstelle** |
|
||||
| Hartkodierte Laufwerksbuchstaben im Produktivpfad | nur in `BackupWorker` (s. 7.1) |
|
||||
| EF Core / Pomelo / MySqlConnector | voll portabel, Migrationen unberührt |
|
||||
| WinForms-Designer-Dateien (`*.Designer.cs`, Layout-`.resx`) | **keine** – die gesamte UI ist handgeschriebener Code-Behind. Das erspart die übliche Designer-Konvertierung vollständig. |
|
||||
|
||||
### 1.1 Die TWS-API läuft auf Linux (geprüft)
|
||||
|
||||
Das Paket `IB.TWS.CSharpApi 9.76.1` liefert `lib/net45/CSharpAPI.dll` – deshalb steht heute
|
||||
`NoWarn="NU1701"` im Core-csproj. Die Assembly-Referenzen wurden ausgelesen:
|
||||
|
||||
```
|
||||
mscorlib 4.0.0.0
|
||||
System 4.0.0.0
|
||||
System.Core 4.0.0.0
|
||||
```
|
||||
|
||||
Nur diese drei, alle auf .NET 10 vollständig typweitergeleitet. Es gibt keine Abhängigkeit auf
|
||||
`System.Configuration`, `System.Web` oder sonst etwas Windows-Gebundenes. Die DLL wird auf Linux
|
||||
laufen. **Verbleibendes Restrisiko: gering, aber ungeprüft** – ein Verbindungs-Smoke-Test gegen
|
||||
das Gateway von einem Linux-Host aus gehört in die erste Etappe.
|
||||
|
||||
> **Sauberere Alternative:** IBKR liefert im offiziellen TWS-API-Download den C#-Quelltext mit.
|
||||
> Den als eigenes `netstandard2.0`-Projekt in `src/` aufzunehmen, ersetzt das NuGet-Mirror-Paket,
|
||||
> beseitigt `NU1701` und macht die Herkunft nachvollziehbar. Halber Tag, optional.
|
||||
|
||||
---
|
||||
|
||||
## 2. Sofort-Blocker: der Restore schlägt heute auf **jedem** frischen Rechner fehl
|
||||
|
||||
Das ist keine Linux-Besonderheit, aber es ist das Erste, worüber man auf einer neuen Maschine
|
||||
stolpert – und deshalb Teil dieser Analyse. `NuGet.config` nutzt `packageSourceMapping` als
|
||||
Allowlist mit `<clear/>`. Drei Pakete haben kein passendes Muster. Verifiziert mit einem Restore
|
||||
gegen einen leeren Paket-Ordner:
|
||||
|
||||
```
|
||||
error NU1100: "PDFsharp-MigraDoc (>= 6.2.4)" kann für "net10.0-windows" nicht aufgelöst werden.
|
||||
error NU1100: "Microsoft.EntityFrameworkCore (>= 8.0.13)" kann für "net10.0-windows" nicht aufgelöst werden.
|
||||
error NU1100: "Microsoft.CodeAnalysis.CSharp.Workspaces (>= 4.5.0)" kann für "net10.0-windows" nicht aufgelöst werden.
|
||||
```
|
||||
|
||||
Auf dem Entwicklungsrechner fällt das nicht auf, weil alle drei längst im globalen Paket-Cache
|
||||
liegen. Die Ursachen:
|
||||
|
||||
- `PDFsharp-MigraDoc` – gar kein Muster vorhanden.
|
||||
- `Microsoft.EntityFrameworkCore` – das Muster lautet `Microsoft.EntityFrameworkCore.*`; der Glob
|
||||
matcht das Paket **ohne** Suffix nicht.
|
||||
- `Microsoft.CodeAnalysis.CSharp.Workspaces` – transitiv über `EntityFrameworkCore.Design`, kein Muster.
|
||||
|
||||
**Aufwand: 10 Minuten.** Muss vor allem anderen erledigt sein, sonst startet der erste
|
||||
Linux-Build nicht.
|
||||
|
||||
---
|
||||
|
||||
## 3. Block A – Projekt- und Build-Ebene
|
||||
|
||||
**Alle sechs Projekte** stehen heute auf `net10.0-windows` mit `UseWindowsForms=true` – auch der
|
||||
Core, alle drei Module und das Testprojekt.
|
||||
|
||||
| Projekt | heute | Ziel |
|
||||
|---|---|---|
|
||||
| `IBKRTrader.Core` | `net10.0-windows`, WinForms | `net10.0`, **keine** UI-Abhängigkeit |
|
||||
| `Modules.CongressTrading` | `net10.0-windows`, WinForms | `net10.0` |
|
||||
| `Modules.Accounting` | `net10.0-windows`, WinForms | `net10.0` |
|
||||
| `Modules.Supervisor` | `net10.0-windows`, WinForms | `net10.0` |
|
||||
| `IBKRTrader.Tests` | `net10.0-windows`, WinForms | `net10.0` |
|
||||
| `IBKRTrader.App` | `WinExe`, `net10.0-windows` | neu: `IBKRTrader.Desktop` (Avalonia) + `IBKRTrader.Daemon` (Konsole) |
|
||||
|
||||
Zwei Nebenwirkungen, die man kennen muss:
|
||||
|
||||
1. **`ImplicitUsings` + `UseWindowsForms` fügt `System.Windows.Forms` und `System.Drawing` als
|
||||
globale Usings hinzu.** Sobald das wegfällt, brechen Dateien, die unbemerkt `Point`, `Size`,
|
||||
`Color`, `Font` oder `Padding` benutzt haben. Das ist *nützlich* – der Compiler findet die
|
||||
Arbeit für uns – aber es erklärt, warum die erste Umstellung mehr Fehler wirft als die 3
|
||||
bekannten Core-Dateien vermuten lassen.
|
||||
2. `ApplicationHighDpiMode`, `Properties/Resources.resx` (24 `System.Drawing.Bitmap`-Icons) und
|
||||
`ApplicationConfiguration.Initialize()` verschwinden mit dem App-Projekt. Die 24 PNGs unter
|
||||
`Resources/` bleiben brauchbar, werden aber in Avalonia über `AvaloniaResource` + `Bitmap`
|
||||
eingebunden statt über den `ResourceManager`.
|
||||
|
||||
**Aufwand Block A: 0,5 Tage.**
|
||||
|
||||
---
|
||||
|
||||
## 4. Block B – den Core von WinForms lösen
|
||||
|
||||
Das ist der architektonisch wichtigste Schritt und erfreulich klein. **Genau drei Dateien** im
|
||||
Core kennen WinForms:
|
||||
|
||||
### 4.1 `Core/Logging/LoggingService.cs` (139 LOC, davon ~40 betroffen)
|
||||
|
||||
Hält direkt ein `RichTextBox?`, benutzt `System.Drawing.Color` und marshallt selbst per
|
||||
`InvokeRequired`/`BeginInvoke`:
|
||||
|
||||
```csharp
|
||||
private RichTextBox? _rtb;
|
||||
public void AttachRichTextBox(RichTextBox rtb) => _rtb = rtb;
|
||||
private static readonly Color ColorInfo = Color.FromArgb(150, 210, 150);
|
||||
```
|
||||
|
||||
**Umbau:** `AttachRichTextBox` durch ein `event Action<LogEntry>? EntryWritten` bzw. ein
|
||||
`ILogSink`-Interface ersetzen. Das Einfärben und das Thread-Marshalling wandern in die UI-Schicht
|
||||
(Avalonia: `Dispatcher.UIThread.Post`). Der `LogEntry`-Record ist bereits sauber und braucht
|
||||
keine Änderung.
|
||||
|
||||
Nebenbei zu bereinigen: `e.Level.ToString().ToUpper()` in Zeile 115 ist kulturabhängig
|
||||
(s. Abschnitt 5).
|
||||
|
||||
### 4.2 `Core/Modularity/ModuleView.cs` (57 LOC)
|
||||
|
||||
```csharp
|
||||
public System.Drawing.Image? Icon { get; set; }
|
||||
public Func<Form> CreateForm { get; init; } = () => new Form();
|
||||
```
|
||||
|
||||
**Umbau:** Der Contract muss toolkit-neutral werden. Zwei Wege:
|
||||
|
||||
- **Pragmatisch:** `Func<object> CreateWindow` + `string IconKey` (Ressourcenname statt Bitmap).
|
||||
Der Core kennt dann keine UI-Typen mehr, die Shell castet.
|
||||
- **Sauber:** ein `IModuleWindow`-Marker-Interface, das die Desktop-Schicht auf `Window` abbildet.
|
||||
|
||||
Das `IModuleUiHost`-Interface selbst (`RegisterView`, `IsOpen`, `OpenView`, `ActivateMain`,
|
||||
`RequestShutdown`, `OpenStateChanged`) ist **bereits toolkit-neutral** und kann unverändert bleiben.
|
||||
|
||||
### 4.3 `Core/Modularity/WindowMenu.cs` (79 LOC)
|
||||
|
||||
Vollständig WinForms (`MenuStrip`, `ToolStripMenuItem`, `Font`, `FontStyle`). Wird ersatzlos
|
||||
gelöscht und in der Avalonia-Schicht neu gebaut. Die *Logik* dahinter (Launcher + alle Views +
|
||||
kontextabhängige rechte Aktion) ist trivial und in ~60 Zeilen XAML/C# nachgebaut.
|
||||
|
||||
### 4.4 Die Module
|
||||
|
||||
**Je Modul genau eine betroffene Datei:**
|
||||
|
||||
- `Modules.Accounting/Ui/AccountingMainForm.cs` (302 LOC)
|
||||
- `Modules.CongressTrading/UI/CongressTradingForm.cs` (129 LOC)
|
||||
- `Modules.Supervisor/Ui/SupervisorMainForm.cs` (188 LOC)
|
||||
|
||||
`Modules.Accounting/Logic/PdfExporter.cs` sieht in einer naiven Suche nach WinForms aus, ist es
|
||||
aber nicht – `Font` und `Colors` stammen dort aus `MigraDoc.DocumentObjectModel`. (Der PdfExporter
|
||||
hat ein *anderes* Linux-Problem, s. 7.2.)
|
||||
|
||||
**Das heißt:** Zieht man diese drei Dateien heraus, sind Core und alle Module sofort headless-fähig.
|
||||
Genau darauf baut die Empfehlung in Abschnitt 11 auf.
|
||||
|
||||
**Aufwand Block B: 1 Tag.**
|
||||
|
||||
---
|
||||
|
||||
## 5. Block C – Kultur und Stringformatierung
|
||||
|
||||
Der Bestand ist überwiegend gut: `StringComparison.Ordinal`/`OrdinalIgnoreCase` wird konsequent
|
||||
benutzt, der `IbkrMapping` und der `CsvExporter` arbeiten korrekt mit `CultureInfo.InvariantCulture`.
|
||||
Es gibt aber fünf konkrete Fundstellen.
|
||||
|
||||
### 5.1 `PdfExporter` formatiert Geldbeträge kulturabhängig — **relevant, weil Finanzdokument**
|
||||
|
||||
`src/IBKRTrader.Modules.Accounting/Logic/PdfExporter.cs:25`
|
||||
|
||||
```csharp
|
||||
string M(decimal baseAmount) => V(baseAmount).ToString("N2") + " " + currencyCode;
|
||||
```
|
||||
|
||||
Ebenso Zeile 86: `e.Quantity.ToString("0.###")`, `e.PriceNative.ToString("0.###")`.
|
||||
|
||||
Alle ohne `IFormatProvider`, also **CurrentCulture**. Auf dem heutigen deutschen Windows kommt
|
||||
`1.234,56` heraus. In einem Linux-Container mit `LANG=C` oder mit
|
||||
`InvariantGlobalization=true` wird daraus `1,234.56` – dieselbe Zahl, andere Bedeutung für einen
|
||||
Leser, und der PDF-Export ist ausdrücklich als *prüfbare Aufstellung* gedacht.
|
||||
|
||||
Auffällig: der `CsvExporter` im selben Modul macht es richtig (`CultureInfo.InvariantCulture`).
|
||||
Der PDF-Export sollte bewusst festgelegt werden – entweder fest `de-DE` (Leserfreundlichkeit) oder
|
||||
fest invariant (Maschinenlesbarkeit), aber nicht "was der Host gerade meint".
|
||||
|
||||
### 5.2 `CapitolTradesScraper.ParseDate` parst kulturabhängig
|
||||
|
||||
`src/IBKRTrader.Modules.CongressTrading/Scraper/CapitolTradesScraper.cs:333`
|
||||
|
||||
```csharp
|
||||
return DateOnly.TryParse(datePart, out var d) ? d : null;
|
||||
```
|
||||
|
||||
Quelle ist capitoltrades.com mit ISO-Datum, das unter den meisten Kulturen durchgeht – aber
|
||||
`TryParse` ohne `IFormatProvider` ist bei einem externen, unkontrollierten Eingabeformat die
|
||||
falsche Wahl. `TryParseExact("yyyy-MM-dd", InvariantCulture)` ist hier auch fachlich richtiger:
|
||||
ein Formatwechsel bei der Quelle soll *auffallen*, nicht stillschweigend zu einem falschen Datum
|
||||
werden. Gleiches gilt für `int.Parse(match.Groups[1].Value)` in Zeile 124.
|
||||
|
||||
### 5.3 Kulturabhängiges `ToUpper()` / `ToLower()`
|
||||
|
||||
- `Core/Logging/LoggingService.cs:115` – `e.Level.ToString().ToUpper()`
|
||||
- `Core/IBKR/IBKRGatewayService.cs:247` – `outsideRth.ToString().ToLower()`, fließt in eine URL
|
||||
|
||||
Beide sind das klassische Türkisch-I-Problem und beide mit `…Invariant()` in einer Minute erledigt.
|
||||
Der zweite ist der unangenehmere, weil er in einen HTTP-Query-String geht.
|
||||
|
||||
### 5.4 Die Grundsatzentscheidung: ICU oder Invariant?
|
||||
|
||||
Auf Linux kommt die Kulturdatenbank aus **ICU** (`libicu`). Das muss entschieden und im csproj
|
||||
festgeschrieben werden:
|
||||
|
||||
- **Mit ICU** (`libicu` im Image installieren): Kulturen verhalten sich weitgehend wie auf
|
||||
Windows ab .NET 5, das ebenfalls ICU benutzt. Größeres Image.
|
||||
- **`InvariantGlobalization=true`**: schlankes Image, keine ICU-Abhängigkeit – aber *jede*
|
||||
kulturabhängige Formatierung wird lautlos invariant. Genau dann schlagen 5.1 und 5.2 durch.
|
||||
|
||||
Solange 5.1/5.2 nicht behoben sind, ist die Wahl sicherheitsrelevant. Danach ist sie beliebig.
|
||||
Empfehlung: Fundstellen explizit machen, dann `InvariantGlobalization=true` (schlank und
|
||||
deterministisch).
|
||||
|
||||
**Aufwand Block C: 0,5 Tage.**
|
||||
|
||||
---
|
||||
|
||||
## 6. Block D – Zeit und Zeitzonen ⚠ **der kritischste Punkt**
|
||||
|
||||
Hier liegt das einzige Risiko, das *stillschweigend falsche Daten* erzeugt statt einen Fehler.
|
||||
|
||||
### 6.1 Die Ausgangslage
|
||||
|
||||
Der Code mischt drei Konventionen:
|
||||
|
||||
| Konvention | Fundstellen (Auswahl) |
|
||||
|---|---|
|
||||
| `DateTime.UtcNow` – korrekt für Persistenz | ~25 Stellen: alle EF-Entitäten, `ExecutionService`, `PortfolioService`, `TradeHistoryService`, `AccountingIngestService`, `BudgetService` |
|
||||
| `DateTime.Now` – Ortszeit des Hosts | `LoggingService:51`, `BackupWorker:34`, `WorkerBase:114/118/146`, `DailyReportService:44`, drei UI-Statuszeilen, `PdfExporter:51` |
|
||||
| `Kind = Unspecified` – weder noch | `IbkrMapping.ParseExecutionTime` |
|
||||
|
||||
### 6.2 `ParseExecutionTime` verwirft die Zeitzone – bewusst, aber jetzt folgenreich
|
||||
|
||||
`src/IBKRTrader.Core/Trading/Ibkr/IbkrMapping.cs:99` (aus R10, gerade committet):
|
||||
|
||||
```csharp
|
||||
/// Die Zeitzone wird verworfen – der Wert bleibt Ortszeit der Börse, wie ihn TWS meldet.
|
||||
```
|
||||
|
||||
TWS liefert je nach Aufruf `20260804 17:52:56` oder `20260804 17:52:56 Europe/Berlin`. Der
|
||||
Suffix wird abgeschnitten, es entsteht ein `DateTime` mit `Kind = Unspecified`. Dieser Wert
|
||||
landet in `BrokerExecution.Time` und von dort perspektivisch in der Buchführung – wo er neben
|
||||
`DateTime.UtcNow`-Feldern liegt.
|
||||
|
||||
Solange alles auf **einem** Windows-Rechner mit `Europe/Berlin` läuft, ist das konsistent genug,
|
||||
um nicht aufzufallen. Auf einem Linux-Container mit `TZ=UTC` bedeutet derselbe abgeschnittene
|
||||
Zeitstempel plötzlich etwas anderes als vorher – **ohne dass sich eine Zeile Code ändert.** Es
|
||||
gibt keine Exception, keinen Log-Eintrag, nur um 1–2 Stunden verschobene Ausführungszeiten.
|
||||
|
||||
**Das ist zu klären, bevor die erste Zeile portiert wird.** Der saubere Weg: die von TWS
|
||||
gemeldete Zeitzone *nicht* verwerfen, sondern über `TimeZoneInfo` (IANA-IDs, die TWS liefert
|
||||
bereits `Europe/Berlin`-Format) nach UTC konvertieren und als `DateTimeOffset` führen. Das ist
|
||||
auch unabhängig von Linux die bessere Lösung, weil Ausführungen an US-Börsen sonst
|
||||
Berliner Ortszeit tragen.
|
||||
|
||||
> **Nebenbefund:** IANA-IDs (`Europe/Berlin`) funktionieren mit `TimeZoneInfo` auf .NET 6+ auch
|
||||
> auf Windows. Es braucht also keine ID-Übersetzung – ein Problem, das man bei solchen
|
||||
> Portierungen sonst regelmäßig hat, entfällt hier.
|
||||
|
||||
### 6.3 `DailyReportService` feuert zur falschen Uhrzeit
|
||||
|
||||
`src/IBKRTrader.Modules.Supervisor/Services/DailyReportService.cs:56`
|
||||
|
||||
```csharp
|
||||
var candidate = new DateTime(now.Year, now.Month, now.Day, hour, 0, 0, DateTimeKind.Local);
|
||||
```
|
||||
|
||||
`IBKRTRADER_SUPERVISOR_DAILY=18` heißt heute "18 Uhr deutscher Zeit". Auf einem UTC-Container
|
||||
heißt es "20 Uhr deutscher Zeit" (Sommerzeit). Fachlich meint man aber eine Börsen- bzw.
|
||||
Ortszeit. Lösung: eine explizite Report-Zeitzone konfigurierbar machen statt `Local` zu benutzen.
|
||||
Der Test `NextRun` existiert bereits und lässt sich mitziehen.
|
||||
|
||||
### 6.4 Logdatei- und Backup-Namen
|
||||
|
||||
`LoggingService` benennt Dateien nach `DateTime.Now` (`{Level}-dd-MM-yy.txt` bzw.
|
||||
`{yyyy-MM-dd}.jsonl`), `BackupWorker` nach `DateTime.Now` (`yyyy-MM-dd_HH-mm`). Beim Umzug auf
|
||||
UTC entsteht ein einmaliger Bruch in der Dateibenennung: der Tageswechsel liegt woanders, es
|
||||
kann für einen Tag zwei Teil-Dateien geben. Nicht kritisch, aber der `SupervisorTools`-Zugriff
|
||||
`Logs/{date}.jsonl` und `DossierService` lesen genau diese Namen – man sollte es wissen und
|
||||
bewusst umstellen, statt es zu entdecken.
|
||||
|
||||
### 6.5 Datenbank
|
||||
|
||||
MariaDB speichert `DATETIME` ohne Offset. Solange die App UTC schreibt und UTC liest, ist der
|
||||
Server-`time_zone` egal. Wandert die DB später auch, ist das der Punkt, an dem man `SET time_zone`
|
||||
prüfen muss. **Ungeprüft** – für den reinen App-Umzug nicht relevant.
|
||||
|
||||
**Aufwand Block D: 1–2 Tage**, davon der größere Teil Audit und Tests, nicht Code.
|
||||
|
||||
---
|
||||
|
||||
## 7. Block E – Dateisystem, Pfade, Prozesse
|
||||
|
||||
### 7.1 `BackupWorker` ist der einzige echt Windows-gebundene Codeteil
|
||||
|
||||
`src/IBKRTrader.Core/Workers/BuiltIn/BackupWorker.cs:95-118`
|
||||
|
||||
```csharp
|
||||
var candidates = new[] {
|
||||
"mysqldump.exe",
|
||||
@"C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqldump.exe",
|
||||
@"C:\Program Files\MySQL\MySQL Server 8.4\bin\mysqldump.exe",
|
||||
@"C:\xampp\mysql\bin\mysqldump.exe"
|
||||
};
|
||||
foreach (var dir in pathVar.Split(';'))
|
||||
var full = Path.Combine(dir.Trim(), "mysqldump.exe");
|
||||
```
|
||||
|
||||
Drei Fehler auf einmal: `.exe`-Endung, Windows-Installationspfade, und `PATH` mit `;` getrennt –
|
||||
Linux nutzt `:`. Letzteres ist `Path.PathSeparator`. Lösung: Kandidatenliste und Endung über
|
||||
`OperatingSystem.IsWindows()` verzweigen, `mariadb-dump` als Kandidat aufnehmen (heißt auf
|
||||
aktuellen MariaDB-Versionen so), Trennzeichen aus `Path.PathSeparator`.
|
||||
|
||||
> **Zusätzlicher Sicherheitsbefund, der erst auf Linux entsteht:** Zeile 66 übergibt das
|
||||
> DB-Passwort als Kommandozeilenargument (`--password={db.Password}`). Unter Linux ist
|
||||
> `/proc/<pid>/cmdline` für **jeden lokalen Nutzer lesbar** – das Passwort steht damit für die
|
||||
> Dauer des Dumps offen im Prozessbaum. Auf Windows ist das weniger exponiert. Beim Umzug also
|
||||
> gleich auf `MYSQL_PWD` (Umgebungsvariable) oder eine temporäre Options-Datei mit `chmod 600`
|
||||
> umstellen. Halber Tag, und unabhängig von Linux ohnehin die bessere Lösung.
|
||||
|
||||
### 7.2 PDF-Export findet auf Linux keine Schriftart
|
||||
|
||||
`src/IBKRTrader.Modules.Accounting/Logic/PdfExporter.cs:30`
|
||||
|
||||
```csharp
|
||||
style.Font.Name = "Segoe UI";
|
||||
```
|
||||
|
||||
"Segoe UI" ist eine Windows-Schrift und auf Linux nicht vorhanden. PDFsharp 6.x löst Schriften
|
||||
auf Nicht-Windows-Plattformen nicht automatisch auf – es braucht einen eigenen
|
||||
`GlobalFontSettings.FontResolver` (`IFontResolver`), der die Schriftdatei liefert. Ohne ihn
|
||||
scheitert der Export zur Laufzeit.
|
||||
|
||||
Lösung: eine freie Schrift (z. B. DejaVu Sans oder Inter) als `EmbeddedResource` ins
|
||||
Accounting-Modul legen und einen kleinen `IFontResolver` implementieren. Das macht den PDF-Export
|
||||
gleichzeitig **plattformunabhängig reproduzierbar** – dasselbe Dokument sieht auf jedem Host
|
||||
identisch aus, was für ein prüfbares Finanzdokument ein Gewinn ist. Rund ein halber Tag.
|
||||
|
||||
### 7.3 Schreibzugriff neben der Binärdatei
|
||||
|
||||
`Logs/`, `Backups/`, `settings.json`, `master.key` und `openrouter.key` liegen alle unter
|
||||
`AppDomain.CurrentDomain.BaseDirectory` bzw. `AppContext.BaseDirectory`. Auf Windows neben der
|
||||
`.exe` üblich. Auf Linux liegt eine Anwendung typischerweise unter `/opt/…` oder `/usr/local/…`
|
||||
und der Dienstbenutzer hat dort **keinen Schreibzugriff**. Erwartet werden `/var/log/ibkrtrader`,
|
||||
`/var/lib/ibkrtrader`, `/etc/ibkrtrader`.
|
||||
|
||||
Das ist kein Einzeiler, sondern eine kleine Entwurfsentscheidung: die vier Pfade sollten aus einer
|
||||
zentralen `IAppPaths`-Abstraktion kommen, die unter Windows das heutige Verhalten beibehält und
|
||||
unter Linux den FHS-Konventionen folgt (oder per Umgebungsvariable überschreibbar ist).
|
||||
Betroffen sind `LoggingService:18`, `SettingsService:13`, `BackupWorker:35/126`,
|
||||
`Program.cs:227`, `OpenRouterClient:39`, `SupervisorTools:30`, `DossierService:29`.
|
||||
|
||||
Dazu: `master.key` und `openrouter.key` brauchen auf Linux `chmod 600`. Ein Startup-Check, der
|
||||
zu weite Rechte meldet, wäre angemessen – Windows-ACLs übertragen sich nicht.
|
||||
|
||||
### 7.4 Groß-/Kleinschreibung
|
||||
|
||||
Linux-Dateisysteme sind case-sensitiv. Zwei Stellen sind zu beachten:
|
||||
|
||||
- Der Ordner heißt bei CongressTrading `UI/`, bei Accounting und Supervisor `Ui/`. MSBuild-Globbing
|
||||
stört das nicht, aber es ist eine Inkonsistenz, die man bei der Gelegenheit begradigen sollte.
|
||||
- `LoggingService:63` baut Log-Verzeichnisse aus dem `Module`-String: `Logs/Core`, `Logs/IBKR`,
|
||||
`Logs/CT`, `Logs/Supervisor`, `Logs/Accounting`, `Logs/AI`. Diese Strings sind über den Code
|
||||
verstreut (~90 Aufrufstellen). Auf Windows wären `Logs/CT` und `Logs/ct` dasselbe Verzeichnis,
|
||||
auf Linux zwei. Die Schreibweisen sind heute konsistent – aber es ist eine Fußangel, die eine
|
||||
Konstantenklasse (`LogModules.Core` etc.) endgültig entschärfen würde.
|
||||
|
||||
### 7.5 Kleinigkeiten
|
||||
|
||||
- `LoggingService:69/72` schreibt hartkodiert `"\r\n"` in die `.txt`-Logs. Auf Linux kosmetisch
|
||||
störend; `Environment.NewLine` wäre richtig. (Das JSONL nutzt korrekt `"\n"`.)
|
||||
- `.gitattributes` ist vorhanden und korrekt konfiguriert (`* text=auto`, Binärdateien ausgenommen).
|
||||
**Kein Handlungsbedarf** – gemischte Zeilenenden werden beim Arbeiten von Linux aus nicht churnen.
|
||||
- `scripts/provision-db.ps1` ist PowerShell. `pwsh` gibt es auf Linux, aber ein `.sh`-Pendant wäre
|
||||
freundlicher. Optional, 1 Stunde.
|
||||
- Der Code mischt `AppDomain.CurrentDomain.BaseDirectory` (ältere Dateien) und
|
||||
`AppContext.BaseDirectory` (neuere). Identischer Wert, rein kosmetisch – erledigt sich mit 7.3.
|
||||
|
||||
**Aufwand Block E: 1–1,5 Tage.**
|
||||
|
||||
---
|
||||
|
||||
## 8. Block F – Netzwerk und Dienste
|
||||
|
||||
Drei `HttpListener`-basierte Dienste:
|
||||
|
||||
| Datei | Prefix |
|
||||
|---|---|
|
||||
| `Core/Workers/BuiltIn/WebserverService.cs:38` | `http://localhost:{port}/` |
|
||||
| `Core/Workers/BuiltIn/WebApiService.cs:46` | `http://localhost:{port}/api/` |
|
||||
| `Modules.Supervisor/Mcp/McpLightServer.cs:44` | `http://127.0.0.1:{port}/mcp/` |
|
||||
|
||||
`HttpListener` **funktioniert auf Linux** (dort als verwaltete Socket-Implementierung statt über
|
||||
`http.sys`). Zu beachten:
|
||||
|
||||
- Ports unter 1024 brauchen root. Genutzt werden 5001 und ein per Env gesetzter MCP-Port, der
|
||||
bereits auf `1024–65535` geprüft wird – **passt**.
|
||||
- Kein HTTPS ohne Zusatzarbeit. Alle drei binden auf localhost, also derzeit kein Thema.
|
||||
- Auf Linux entfällt die `netsh urlacl`-Registrierung – eine Erleichterung, kein Problem.
|
||||
|
||||
`IBKRGatewayService:39` setzt `ServerCertificateCustomValidationCallback = (_,_,_,_) => true`, akzeptiert
|
||||
also jedes Zertifikat. Das ist für den lokalen Client-Portal-Gateway mit Selbstsignat gedacht und
|
||||
funktioniert auf Linux identisch. Es ist unabhängig von dieser Portierung eine Stelle, die man
|
||||
irgendwann auf Pinning des Gateway-Zertifikats einengen sollte – hier nur der Vollständigkeit halber.
|
||||
|
||||
**Aufwand Block F: 0 Tage** (nur Verifikation). Langfristig wäre ein Umstieg von `HttpListener`
|
||||
auf Kestrel/Minimal-API sauberer – `HttpListener` gilt als Altlast –, das ist aber **nicht**
|
||||
Voraussetzung für Linux.
|
||||
|
||||
---
|
||||
|
||||
## 9. Block G – Die UI: Avalonia + LiveCharts2
|
||||
|
||||
### 9.1 Umfang
|
||||
|
||||
| Datei | LOC |
|
||||
|---|---|
|
||||
| `LauncherForm.cs` | 162 |
|
||||
| `UI/ShellUiHost.cs` | 107 |
|
||||
| `UI/WorkerListBindingSource.cs` | 77 |
|
||||
| `UI/LogPanelController.cs` | 33 |
|
||||
| `UI/Views/DashboardView.cs` | 98 |
|
||||
| `UI/Views/{Logs,Settings,Workers}View.cs` | 64 |
|
||||
| `Core/Modularity/WindowMenu.cs` + `ModuleView.cs` | 136 |
|
||||
| `Modules.Accounting/Ui/AccountingMainForm.cs` | 302 |
|
||||
| `Modules.Supervisor/Ui/SupervisorMainForm.cs` | 188 |
|
||||
| `Modules.CongressTrading/UI/CongressTradingForm.cs` | 129 |
|
||||
| **Summe** | **~1.300** |
|
||||
|
||||
Erfahrungsgemäß wächst das bei einer Umsetzung mit XAML + ViewModels auf 2.000–2.500 LOC, weil
|
||||
MVVM Struktur kostet, die im Code-Behind heute implizit ist.
|
||||
|
||||
**Kein einziges Diagramm im Bestand.** Die Suche nach `Chart`/`Series`/`Plot` liefert null Treffer.
|
||||
LiveCharts2 ist damit **kein Migrationsposten, sondern ein Feature-Neubau** – und sollte auch so
|
||||
geplant und geschätzt werden, getrennt vom Rest.
|
||||
|
||||
### 9.2 Was gut übertragbar ist
|
||||
|
||||
Der Shell-Entwurf passt bemerkenswert gut auf Avalonia:
|
||||
|
||||
- `ShellUiHost` (Dictionary offener Fenster, eine Instanz je View, `OpenStateChanged`-Event) ist
|
||||
**fast vollständig toolkit-neutral**. Nur der Typ `Form` und `MessageBox.Show` müssen getauscht
|
||||
werden. Die 107 LOC überleben zu ~80 %.
|
||||
- Mehrere gleichrangige Top-Level-Fenster + Launcher entsprechen exakt Avalonias
|
||||
`IClassicDesktopStyleApplicationLifetime` mit mehreren `Window`-Instanzen.
|
||||
- `WorkerInfo` implementiert bereits `INotifyPropertyChanged` – das ist genau das, was Avalonias
|
||||
Binding erwartet. `WorkerListBindingSource` (`BindingList<T>`) wird zu `ObservableCollection<T>`.
|
||||
- Das UI-Thread-Marshalling (`InvokeRequired`/`BeginInvoke`, 6 Stellen) wird zu
|
||||
`Dispatcher.UIThread.Post` – ein mechanischer 1:1-Ersatz.
|
||||
|
||||
### 9.3 Was echte Arbeit ist
|
||||
|
||||
| WinForms | Avalonia | Anmerkung |
|
||||
|---|---|---|
|
||||
| `DataGridView` (4×) | `DataGrid` | **eigenes Paket** `Avalonia.Controls.DataGrid` + Theme-Include in `App.axaml`. `AccountingMainForm:187` bindet heute anonyme Typen – die müssen zu echten Record-ViewModels werden. |
|
||||
| `MessageBox.Show` (`ShellUiHost:46`) | – | Avalonia hat **keine** eingebaute MessageBox. Eigener Dialog oder Zusatzpaket. |
|
||||
| `SaveFileDialog` (2×) | `IStorageProvider.SaveFilePickerAsync` | asynchron, anderer API-Zuschnitt |
|
||||
| `RichTextBox` mit `SelectionColor` | `ItemsControl`/`SelectableTextBlock` | Das farbige Log-Panel muss anders gebaut werden (eingefärbte Items statt Selection-Färbung) – im Ergebnis sauberer. |
|
||||
| `ToolStrip`/`MenuStrip`/`StatusStrip` | `Menu` + Panel-Layout | Direkte Entsprechungen fehlen; wird handgebaut. |
|
||||
| `DateTimePicker`, `ComboBox` | `DatePicker`, `ComboBox` | unkritisch |
|
||||
| `SystemColors.GrayText` etc. | Theme-Ressourcen | Fluent-Theme, gleichzeitig Hell/Dunkel möglich |
|
||||
| `Dock`/`Anchor`, absolute `Point`-Positionen | Grid/DockPanel/StackPanel | Layout muss neu gedacht, nicht übersetzt werden |
|
||||
|
||||
Die Steuerelement-Inventur über alle UI-Dateien: 12 `Label`, 10 `Button`, 9 `Panel`,
|
||||
4 `FlowLayoutPanel`, 4 `DataGridView`, 2 `ToolStrip`, 2 `TabControl`, 2 `SaveFileDialog`,
|
||||
2 `MenuStrip`, 1 `StatusStrip`, 1 `RichTextBox`, 1 `BindingSource`. Überschaubar – es gibt keine
|
||||
exotischen Controls und kein Custom-Drawing.
|
||||
|
||||
### 9.4 Lizenz und Laufzeitabhängigkeiten
|
||||
|
||||
- **Avalonia ist MIT-lizenziert**, ohne Kosten und ohne Umsatzschwelle. "Avalonia Accelerate" ist
|
||||
nur ein optionales kommerzielles Support-/Tooling-Paket. Für das Projekt entstehen keine
|
||||
Lizenzkosten. (Das passt zur bereits getroffenen Linie – PDFsharp wurde ausdrücklich statt
|
||||
QuestPDF gewählt, um Umsatzschwellen zu vermeiden.)
|
||||
- **LiveCharts2** (`LiveChartsCore.SkiaSharpView.Avalonia`) ist ebenfalls MIT.
|
||||
- Beide bringen **SkiaSharp** mit. Auf Linux braucht das `libfontconfig1` und (für die Desktop-UI)
|
||||
X11- oder Wayland-Bibliotheken im Image. Bei einer headless Variante entfällt das komplett –
|
||||
ein weiteres Argument für die Trennung in Abschnitt 11.
|
||||
- Zusätzliche `packageSourceMapping`-Muster: `Avalonia*`, `LiveChartsCore*`, `SkiaSharp*`,
|
||||
`HarfBuzzSharp*`, `Tmds.DBus*`, `MicroCom*`.
|
||||
|
||||
**Aufwand Block G: 10–14 Personentage** für jemanden, der Avalonia kennt. Ohne Vorerfahrung
|
||||
realistisch +3–5 Tage Einarbeitung. LiveCharts2 zusätzlich 1–3 Tage je nach gewünschtem Umfang.
|
||||
|
||||
---
|
||||
|
||||
## 10. Block H – Tests, Smoke-Check und CI
|
||||
|
||||
165 Tests, 2.302 LOC. Das Testprojekt steht auf `net10.0-windows` + WinForms – **wegen genau einer
|
||||
Datei**: `UiConstructionTests.cs` (87 LOC), die die Modul-Fenster auf einem STA-Thread konstruiert.
|
||||
Dieselbe Idee steckt hinter dem `--smoke-ui`-Schalter in `Program.cs:270`.
|
||||
|
||||
Nach der Portierung:
|
||||
|
||||
- Die restlichen ~2.200 LOC Tests laufen **unverändert** auf `net10.0`/Linux. Sie testen Mapping,
|
||||
Risiko, Portfolio, Accounting, Supervisor, Krypto, Logging – alles portabel, mit
|
||||
EF-InMemory statt echter DB.
|
||||
- `UiConstructionTests` und `--smoke-ui` werden auf `Avalonia.Headless` umgestellt. **Das ist ein
|
||||
Gewinn, kein Verlust:** WinForms lässt sich in CI ohne Desktop-Session nicht sinnvoll
|
||||
instanziieren, Avalonia.Headless ist genau dafür gebaut. Der Smoke-Check wird damit CI-fähig,
|
||||
was er heute nicht ist.
|
||||
- Eine Test-Fixture hängt an einem Pfad: `tests/…csproj` bindet `..\..\ct_raw.html` mit
|
||||
Backslashes ein. MSBuild normalisiert das – **kein Problem**.
|
||||
|
||||
Empfehlung für die CI: eine GitHub-Actions-/Gitea-Actions-Matrix `ubuntu-latest` + `windows-latest`
|
||||
einrichten, sobald Etappe 1 steht. Das hält die Portabilität dauerhaft und fängt Rückfälle
|
||||
(neues `DateTime.Now`, neues `.ToString("N2")`) sofort ab.
|
||||
|
||||
**Aufwand Block H: 1–2 Tage.**
|
||||
|
||||
---
|
||||
|
||||
## 11. Betrieb: der Punkt, den man leicht übersieht
|
||||
|
||||
**Die App auf Linux zu bringen, macht das IB Gateway nicht Linux-tauglich.** Das ist eine getrennte
|
||||
Baustelle:
|
||||
|
||||
- TWS bzw. IB Gateway ist eine Java-Anwendung und läuft auf Linux – aber mit GUI. Für den
|
||||
Dauerbetrieb ohne Bildschirm braucht es **IBC** (IBController) plus **Xvfb** als virtuellen
|
||||
X-Server, dazu die Behandlung des täglichen Auto-Restarts und des 2FA-Handlings.
|
||||
- Das ist erfahrungsgemäß **1–2 Tage** eigener Arbeit und hat mit dem C#-Code nichts zu tun.
|
||||
- Alternative: Gateway bleibt auf dem Windows-Rechner, die Linux-App verbindet sich über das Netz
|
||||
auf Port 4002. Dann muss in der TWS-Konfiguration die erlaubte Client-IP eingetragen werden
|
||||
(heute steht in `settings.example.json` `127.0.0.1`) – und der TWS-API-Verkehr ist unverschlüsselt,
|
||||
gehört also nicht über ein unvertrautes Netz.
|
||||
|
||||
Weitere Betriebspunkte: `systemd`-Unit statt Autostart, Log-Rotation über `logrotate` statt
|
||||
`BackupWorker`-Kopien, Dienstbenutzer ohne Login-Shell.
|
||||
|
||||
**Aufwand Block I: 1–3 Tage**, je nachdem ob das Gateway mitwandert.
|
||||
|
||||
---
|
||||
|
||||
## 12. Aufwandsübersicht
|
||||
|
||||
| Block | Inhalt | Tage |
|
||||
|---|---|---:|
|
||||
| **0** | `NuGet.config` reparieren (blockiert alles andere) | 0,1 |
|
||||
| **A** | Zielframeworks, csproj-Aufteilung, Projektstruktur | 0,5 |
|
||||
| **B** | Core von WinForms lösen (3 Dateien + 3 Modul-Forms herauslösen) | 1,0 |
|
||||
| **C** | Kultur/Formatierung (5 Fundstellen + Globalisierungsentscheidung) | 0,5 |
|
||||
| **D** | Zeit/Zeitzonen ⚠ (Audit, `ParseExecutionTime`, `DailyReportService`, Tests) | 1,5 |
|
||||
| **E** | Dateisystem (`BackupWorker`, PDF-Fonts, `IAppPaths`, Secrets-Rechte) | 1,5 |
|
||||
| **F** | Netzwerk/Dienste (nur Verifikation) | 0,0 |
|
||||
| **H** | Tests auf `net10.0`, Smoke-Check headless, CI-Matrix | 1,5 |
|
||||
| | **Zwischensumme: headless Linux lauffähig** | **~6,5** |
|
||||
| **G1** | Avalonia: Shell, Launcher, 4 Core-Views | 5,0 |
|
||||
| **G2** | Avalonia: 3 Modul-Fenster (Accounting ist das größte) | 5,0 |
|
||||
| **G3** | Theming, Feinschliff, Dialoge, Icons | 2,0 |
|
||||
| | **Zwischensumme: Desktop-Linux** | **~12** |
|
||||
| **G4** | LiveCharts2 – Neubau, kein Bestand vorhanden | 1–3 |
|
||||
| **I** | Betrieb: systemd, Deployment, ggf. IB Gateway headless (IBC/Xvfb) | 1–3 |
|
||||
| | **Gesamt** | **~21–25 Personentage** |
|
||||
|
||||
Ohne Avalonia-Vorerfahrung auf Block G realistisch **+3–5 Tage** aufschlagen.
|
||||
|
||||
Zum Vergleich in Wochen: **headless in gut einer Woche**, **komplett mit Desktop-UI in etwa
|
||||
4–5 Wochen** Vollzeit.
|
||||
|
||||
---
|
||||
|
||||
## 13. Vorschlag: in zwei Etappen, nicht in einem Zug
|
||||
|
||||
### Etappe 1 – Headless Linux (~1,5 Wochen)
|
||||
|
||||
Ziel: Core + alle drei Module + Worker + REST/MCP laufen als `systemd`-Dienst auf Linux. Die
|
||||
Windows-UI bleibt **unverändert bestehen** und läuft weiter.
|
||||
|
||||
1. `NuGet.config` reparieren.
|
||||
2. Die drei Core-Dateien und die drei Modul-Forms herauslösen; Core und Module auf `net10.0`.
|
||||
3. Neues Projekt `IBKRTrader.Daemon` (Konsole, `net10.0`) – nimmt `Program.cs` ab Zeile 51
|
||||
(`Host.CreateDefaultBuilder`) fast unverändert auf. Der Generic Host und die
|
||||
`IHostedService`-Worker sind dafür bereits die richtige Grundlage; das wurde in R4 gelegt.
|
||||
4. Blöcke C, D, E abarbeiten.
|
||||
5. Testprojekt auf `net10.0`, CI-Matrix Linux + Windows.
|
||||
|
||||
Danach ist die App auf Linux **im Dauerbetrieb einsatzfähig** – ohne dass ein einziges Fenster
|
||||
angefasst wurde. Für ein System, das rund um die Uhr Marktdaten zieht und Signale verarbeitet, ist
|
||||
das der eigentliche Gewinn.
|
||||
|
||||
### Etappe 2 – Avalonia-Desktop (~2,5–3 Wochen)
|
||||
|
||||
Ziel: `IBKRTrader.Desktop` ersetzt `IBKRTrader.App` und läuft auf beiden Plattformen.
|
||||
|
||||
6. Shell + Launcher + Core-Views.
|
||||
7. Die drei Modul-Fenster.
|
||||
8. Erst danach LiveCharts2 – als eigenständiges Feature mit eigener Anforderung, nicht als
|
||||
Nebenprodukt der Portierung.
|
||||
|
||||
**Warum diese Reihenfolge:** Etappe 1 bringt den vollen Betriebsnutzen bei einem Sechstel des
|
||||
Aufwands, und sie ist reversibel – wenn Etappe 2 liegen bleibt, steht trotzdem ein funktionierendes
|
||||
System da. Umgekehrt (erst UI) hätte man nach drei Wochen eine schöne Oberfläche und immer noch
|
||||
keinen Linux-Betrieb.
|
||||
|
||||
---
|
||||
|
||||
## 14. Was du entscheiden musst
|
||||
|
||||
Vier Punkte, die nicht aus dem Code ableitbar sind:
|
||||
|
||||
1. **Zeitzonen-Konvention.** Alles UTC in der Persistenz und nur an der Oberfläche umrechnen? Oder
|
||||
eine feste "Betriebszeitzone"? Das entscheidet den Zuschnitt von Block D – und es entscheidet,
|
||||
wie `ParseExecutionTime` künftig aussieht. **Das ist der einzige Punkt, der vor dem ersten
|
||||
Handgriff geklärt sein muss.**
|
||||
2. **Wandert das IB Gateway mit auf Linux** (IBC + Xvfb, eigene 1–2 Tage) oder bleibt es auf dem
|
||||
Windows-Rechner und die Linux-App verbindet sich über das lokale Netz?
|
||||
3. **Soll die Windows-Desktop-Version erhalten bleiben?** Avalonia läuft auf beiden Plattformen –
|
||||
die Frage ist nur, ob Windows weiter *getestet* werden muss (CI-Matrix) oder ob Linux das
|
||||
alleinige Ziel wird.
|
||||
4. **Zahlenformat im PDF-Export:** fest deutsch oder fest invariant? Für ein prüfbares
|
||||
Finanzdokument sollte es festgelegt und nicht vom Host abhängig sein (5.1).
|
||||
|
||||
---
|
||||
|
||||
## Anhang: Fundstellenverzeichnis
|
||||
|
||||
Kurzliste aller konkret zu ändernden Stellen außerhalb der UI, nach Datei sortiert:
|
||||
|
||||
| Datei | Zeile | Befund | Block |
|
||||
|---|---|---|---|
|
||||
| `NuGet.config` | – | 3 fehlende `packageSourceMapping`-Muster → Restore schlägt fehl | 0 |
|
||||
| alle 6 `*.csproj` | – | `net10.0-windows` + `UseWindowsForms` | A |
|
||||
| `Core/Logging/LoggingService.cs` | 12, 22, 100–138 | `RichTextBox`, `System.Drawing.Color` | B |
|
||||
| `Core/Logging/LoggingService.cs` | 115 | `.ToUpper()` kulturabhängig | C |
|
||||
| `Core/Logging/LoggingService.cs` | 18, 63, 66, 88 | Pfade unter `BaseDirectory`, Modulnamen als Verzeichnisse | E |
|
||||
| `Core/Logging/LoggingService.cs` | 69, 72 | hartkodiertes `\r\n` | E |
|
||||
| `Core/Modularity/ModuleView.cs` | 27, 30 | `System.Drawing.Image`, `Func<Form>` | B |
|
||||
| `Core/Modularity/WindowMenu.cs` | gesamt | vollständig WinForms, wird ersetzt | B |
|
||||
| `Core/Trading/Ibkr/IbkrMapping.cs` | 99–113 | verwirft Zeitzone, `Kind = Unspecified` ⚠ | D |
|
||||
| `Core/IBKR/IBKRGatewayService.cs` | 247 | `.ToLower()` kulturabhängig, geht in URL | C |
|
||||
| `Core/Workers/BuiltIn/BackupWorker.cs` | 95–118 | `mysqldump.exe`, `C:\`-Pfade, `PATH.Split(';')` | E |
|
||||
| `Core/Workers/BuiltIn/BackupWorker.cs` | 66 | DB-Passwort in der Kommandozeile (auf Linux exponiert) | E |
|
||||
| `Core/Workers/BuiltIn/BackupWorker.cs` | 34, 35, 126 | `DateTime.Now`, `BaseDirectory` | D/E |
|
||||
| `Core/Workers/WorkerBase.cs` | 114, 118, 146 | `DateTime.Now` in der Ablaufsteuerung | D |
|
||||
| `Core/Settings/SettingsService.cs` | 13 | `settings.json` neben der Binärdatei | E |
|
||||
| `Modules.Accounting/Logic/PdfExporter.cs` | 30 | `"Segoe UI"` – auf Linux nicht vorhanden | E |
|
||||
| `Modules.Accounting/Logic/PdfExporter.cs` | 25, 86 | `ToString("N2")`/`("0.###")` kulturabhängig | C |
|
||||
| `Modules.Accounting/Ui/AccountingMainForm.cs` | gesamt (302) | WinForms, 3 `DataGridView`, 2 `SaveFileDialog` | G |
|
||||
| `Modules.CongressTrading/Scraper/CapitolTradesScraper.cs` | 124, 333 | `int.Parse`/`DateOnly.TryParse` ohne `IFormatProvider` | C |
|
||||
| `Modules.CongressTrading/UI/CongressTradingForm.cs` | gesamt (129) | WinForms | G |
|
||||
| `Modules.Supervisor/Services/DailyReportService.cs` | 44, 56 | `DateTime.Now` + `DateTimeKind.Local` → falsche Uhrzeit auf UTC-Host | D |
|
||||
| `Modules.Supervisor/Agent/OpenRouterClient.cs` | 39 | `openrouter.key` neben der Binärdatei, Dateirechte | E |
|
||||
| `Modules.Supervisor/Ui/SupervisorMainForm.cs` | gesamt (188) | WinForms, `RichTextBox` | G |
|
||||
| `Program.cs` | 31, 48, 227, 270–320 | `[STAThread]`, `ApplicationConfiguration`, `master.key`, Smoke-UI | A/B/E |
|
||||
| `LauncherForm.cs`, `UI/**` | gesamt (541) | WinForms-Shell | G |
|
||||
| `tests/…csproj` + `UiConstructionTests.cs` | – | `net10.0-windows` nur wegen einer Datei | H |
|
||||
@@ -0,0 +1,89 @@
|
||||
> ### 📦 Archiviert am 2026-08-23
|
||||
> Dieses Dokument wird **nicht mehr gepflegt**. Was davon noch offen ist, steht in der
|
||||
> [Roadmap](../ROADMAP.md) (Bahn „Accounting") – dort und nur dort wird der Stand nachgeführt.
|
||||
>
|
||||
> Es bleibt erhalten, weil es die Leitprinzipien traegt – unabhaengige Quelle, Idempotenz, append-only –
|
||||
> und die Datenbeschaffung ueber die Flex Query beschreibt. Zum Nachschlagen also weiterhin richtig,
|
||||
> als Aufgabenliste nicht mehr.
|
||||
|
||||
---
|
||||
|
||||
# Konzept: Modul „Accounting" (Buchhaltung/Reporting aller Konten)
|
||||
|
||||
> **UMGESETZT (Modulgerüst).** Das Modul steht: `acc_`-Schema mit Migration `InitialAccounting`,
|
||||
> `AccountingIngestService` (append-only, idempotent über `IdempotencyKey`), `AccountingClassifier`,
|
||||
> `AccountingEngine`, `FxConverter`, `AccountingReportService` sowie CSV- und PDF-Export. Die
|
||||
> Ingest-Quellen liegen hinter Interfaces mit **Offline-Null-Stubs** — das Modul läuft vollständig
|
||||
> und bucht dabei korrekt nichts.
|
||||
>
|
||||
> **Weiterhin offen ist genau die Zielland-Arbeit aus §6** — vor allem der Live-Flex-Abruf, ohne den
|
||||
> keine echten Buchungen entstehen, und die Steuerschicht, deren Jurisdiktion nicht festgelegt ist.
|
||||
> Das Modul ist damit lauffähig, aber noch nicht in Betrieb.
|
||||
|
||||
> Stand: 2026-07-30
|
||||
> Ziel: Vollständige, **von unserer Trading-DB unabhängige**, buchhalterisch korrekte Erfassung ALLER
|
||||
> Kontobewegungen der IBKR-Konten. Periodische (meist monatliche), vor einer Steuerbehörde
|
||||
> nachvollziehbare Aufstellungen — je Konto ODER über alle Konten, für frei wählbare Zeiträume.
|
||||
> BWA-artige Kennzahlen-Übersicht in der UI. Export als CSV und PDF. **Kein Handel; reines
|
||||
> Ingest-/Reporting-Modul.**
|
||||
>
|
||||
> Vorbild: gleichnamiges Modul in PolytraderSharp (Polymarket). Hier auf IBKR-Aktien übertragen.
|
||||
|
||||
## 0. Leitprinzipien
|
||||
1. **Unabhängige Quelle = IBKR-Kontoauszug, NICHT unsere DB.** Das Modul erhebt die Buchungsgrundlage
|
||||
ausschließlich über eigene Abrufe des **IBKR Activity Flex Query (XML)** und speichert sie roh +
|
||||
normalisiert in eigenen `acc_`-Tabellen. Der Flex Web Service (Token + Query-Id) braucht **keine**
|
||||
laufende TWS-Socket-Verbindung. Unsere eigenen Trade-Logs dienen nur dem optionalen Abgleich, nie
|
||||
als Buchungsgrundlage.
|
||||
2. **Nachvollziehbarkeit / Audit.** Jeder Buchungssatz führt über `TransactionId` (IBKR tradeID /
|
||||
transactionID) und den unveränderlichen `IdempotencyKey` auf einen prüfbaren Nachweis zurück. Der
|
||||
Roh-Ingest ist **append-only**; Abrechnungen sind daraus reproduzierbar.
|
||||
3. **Lesend / idempotent.** Überlappende Wiederholungs-Abrufe buchen nichts doppelt (Unique-Index auf
|
||||
`IdempotencyKey`, Upsert statt Insert).
|
||||
|
||||
## 1. Architektur-Einbettung
|
||||
Projekt `src/IBKRTrader.Modules.Accounting/` als `IModule` (`Name="Accounting"`, `DbPrefix="acc_"`),
|
||||
Registrierung in `Program.cs`. Referenziert nur den Core. Eigener `AccountingDbContext`, eigene UI
|
||||
(ein Fenster mit Tabs), eigene Settings-Sektion.
|
||||
|
||||
## 2. Datenbeschaffung
|
||||
- **Activity Flex Query** = primärer Kontoauszug: `<Trade>` (Käufe/Verkäufe: Preis, Menge, Kommission,
|
||||
Währung, FX-Rate zur Basiswährung, tradeID) und `<CashTransaction>` (Dividenden, Quellensteuer,
|
||||
Zinsen, Ein-/Auszahlungen, Gebühren).
|
||||
- **Backfill + Inkrementell**: Erstlauf lädt die volle Historie, danach nur Neues ab dem letzten
|
||||
bekannten Zeitpunkt mit Sicherheits-Lookback (Standard 24 h).
|
||||
- **Idempotenz-Schlüssel** je Satz: `TRD|<Typ>|<tradeID>` bzw. `CASH|<Typ>|<transactionID>`.
|
||||
- **Balance-Anker**: gemeldeter Kontosaldo je Abruf als Soll-Ist-Kontrollpunkt.
|
||||
- Der Abruf liegt hinter Interfaces (`IStatementSource`/`IBalanceAnchorSource`/`IAccountingAccountSource`)
|
||||
mit **Offline-Null-Stubs** — das Modul läuft ohne Live-Anbindung vollständig (bucht dann korrekt nichts).
|
||||
Der Live-Flex-Abruf ist **Zielland-Arbeit**.
|
||||
|
||||
## 3. Persistenz (`acc_`-Tabellen, append-only)
|
||||
| Tabelle | Inhalt |
|
||||
|---|---|
|
||||
| `acc_ledger` | Normalisierte, unveränderliche Buchungssätze (Typ, Vorzeichen=Cash-Wirkung, native + Basiswährung, TransactionId, **IdempotencyKey unique**) |
|
||||
| `acc_ingest_runs` | Abruf-Protokoll je Konto (Von/Bis, #neu/#Duplikate, Balance-Anker-Δ) |
|
||||
| `acc_raw` | Rohdaten-Snapshots je Batch (Nachweis) |
|
||||
| `acc_fx_rates` | amtliche USD→EUR-Tageskurse (EZB) je Datum |
|
||||
|
||||
## 4. Logik (pur, unit-getestet — `Logic/`)
|
||||
- `AccountingClassifier` — Flex-Zeile → Buchungssatz (Typ, Vorzeichen, Idempotenz-Key). Ein-/Auszahlung
|
||||
per Vorzeichen (kombinierte IBKR-Kategorie).
|
||||
- `AccountingEngine` — Periodenabrechnung (Anfangs-/Endsaldo, Einlagen/Entnahmen, Handelsvolumen,
|
||||
Dividenden, Zinsen, Fees, Quellensteuer, Netto-Handelsergebnis Cash-Basis) + Monatsvergleich.
|
||||
Invariante: Endsaldo−Anfang = Ergebnis + Einzahlungen − Auszahlungen.
|
||||
- `FxConverter` — USD→EUR (Nearest-on-or-before). `CsvExporter` (RFC-4180, kulturinvariant).
|
||||
`PdfExporter` (PDFsharp/MigraDoc, MIT).
|
||||
- Realisierte GuV nutzt den Core-`RealizedPnlEngine` (FIFO) — kein Duplikat.
|
||||
|
||||
## 5. UI (Avalonia, ein Fenster mit Registerkarten)
|
||||
Übersicht/BWA (KPI-Kacheln + Monatsvergleich, Zeitraum-/Konto-/Währungswahl), Ledger (filterbar),
|
||||
Steuer (Platzhalter, s. u.), Abrechnung/Export (CSV/PDF), Abruf/Status (Ingest-Läufe, Soll-Ist, manueller
|
||||
Trigger). DB-Zugriff nur auf Interaktion (Smoke-UI-sicher).
|
||||
|
||||
## 6. Bewusst offen / Zielland-Arbeit
|
||||
- **Live-IBKR-Flex-Abruf** (Token/Query-Id) + Balance-Anker → echte Buchungen (heute Null-Stub).
|
||||
- **Steuerschicht**: Jurisdiktion (DE-Kapitalertragsteuer / US Form 8949) noch **nicht festgelegt**.
|
||||
Der neutrale Ledger + die Abrechnung gelten unabhängig davon; die Steuer-UI/Engine ist als klar
|
||||
abgetrennter, später füllbarer Platzhalter angelegt. **Keine Steuerberatung.**
|
||||
- **EZB-FX-Ingest** (`acc_fx_rates` füllen) → EUR-Ansicht; USD (Basis) ist sofort verfügbar.
|
||||
@@ -0,0 +1,257 @@
|
||||
> ### 📦 Archiviert am 2026-08-23
|
||||
> Dieses Dokument wird **nicht mehr gepflegt**. Was davon noch offen ist, steht in der
|
||||
> [Roadmap](../ROADMAP.md) (Stufe 5, dazu die Core-Voraussetzungen in Stufe 3) – dort und nur dort wird der Stand nachgeführt.
|
||||
>
|
||||
> Es bleibt erhalten, weil es den Zustandsautomaten, das Regelwerk mit allen Vorgabewerten und die Begruendung enthaelt,
|
||||
> warum das Modul nicht additiv auf den heutigen aktienbasierten Core passt. Zum Nachschlagen also weiterhin richtig,
|
||||
> als Aufgabenliste nicht mehr.
|
||||
|
||||
---
|
||||
|
||||
# Konzept: Modul „OptionsWheel" (Covered Call / Cash-Secured Put)
|
||||
|
||||
> Stand: 2026-08-03
|
||||
> Ziel: Auf einer kleinen Watchlist von Tickern, an deren langfristigen Erfolg wir glauben,
|
||||
> **systematisch Optionsprämien vereinnahmen** – Cash-Secured Put → (Zuteilung) → Aktienbestand →
|
||||
> Covered Call → (Abruf) → wieder Cash. Vollautomatisch, delta-basierte Strike-Wahl, aktives Rollen.
|
||||
>
|
||||
> **Festlegungen (2026-08-03, mit dem Betreiber abgestimmt):**
|
||||
> 1. Put-Seite ist **Cash-Secured Put**, nicht Covered Put (kein Leerverkauf der Aktie).
|
||||
> 2. **Vollautomatisch** von Anfang an – Sicherungen sind Schalter und Limits, keine Klick-Freigabe.
|
||||
> 3. Strike-Wahl **delta-basiert** (Zielband 0,15–0,30).
|
||||
> 4. Bei drohender Zuteilung wird **gerollt**, solange das per Netto-Kredit möglich ist.
|
||||
|
||||
## 0. Leitprinzipien
|
||||
1. **Niemals nackt.** Ein Short Call ist nur zulässig mit 100 freien Aktien je Kontrakt, ein Short Put
|
||||
nur mit reserviertem Cash über Strike × 100 je Kontrakt. Diese Deckungsprüfung gehört in den
|
||||
**Core-`RiskService`**, nicht ins Modul – ein Modulfehler darf keine ungedeckte Option schreiben können.
|
||||
2. **Der Broker ist die Wahrheit.** Zuteilung und Verfall ändern Positionen **ohne** eine Order von uns.
|
||||
Ohne regelmäßigen Positionsabgleich gegen IBKR läuft die eigene Buchführung zwangsläufig auseinander.
|
||||
3. **Nur Watchlist.** Das Modul handelt ausschließlich explizit eingetragene Ticker. Keine Entdeckung,
|
||||
kein Screening, keine Ausweitung zur Laufzeit.
|
||||
4. **Jede Entscheidung ist nachlesbar.** Kandidatenbewertung, Zustandsübergang und Order gehen über
|
||||
`core_decision_journal` / `core_order_events` mit gemeinsamer `SignalId` – dieselbe Forensik-Grundlage,
|
||||
die der Supervisor bereits nutzt.
|
||||
|
||||
---
|
||||
|
||||
## 1. Warum das nicht additiv geht: der Core ist heute aktienbasiert
|
||||
|
||||
| Stelle | Heutiger Stand | Konsequenz |
|
||||
|---|---|---|
|
||||
| `IbkrMapping.Stock()` | „der einzige Instrumententyp, den die Module handeln" | Kein Options-Kontrakt (Expiry/Strike/Right/Multiplier) baubar |
|
||||
| `TradeSignal` / `OrderRequest` | nur `Symbol` + `Side` + `Quantity` | Ein Signal kann keine Option benennen |
|
||||
| `Position.Notional` | `Quantity × AvgPrice` | Bei Optionen um Faktor 100 falsch → Risikolimits wirkungslos |
|
||||
| `RiskService.EvaluateSell` | „Verkauf schließt Position", lehnt ohne Bestand ab | **Sell-to-open wird grundsätzlich abgelehnt** – die Kernoperation des Moduls |
|
||||
| `IBrokerClient.PlaceOrderAsync` | Fill-oder-Fehlschlag innerhalb `OrderTimeoutSeconds` | Limitorder am Mid liegt im Buch → gilt als Fehlschlag, ist aber aktiv (bekannte Grenze, [IBKR-Integration.md](../IBKR-Integration.md)) |
|
||||
| `IBrokerClient` | kein `reqPositions`, keine Optionskette, keine Greeks | Zuteilung/Verfall unsichtbar, Strike-Wahl unmöglich |
|
||||
|
||||
Das Modul setzt also auf einem **Core-Options-Fundament** auf, das zuerst gebaut wird. Der Aktienpfad
|
||||
bleibt dabei unverändert (neue Felder sind optional, `Kind = Stock` ist der Default) – CongressTrading
|
||||
darf nicht regressieren.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core-Erweiterungen
|
||||
|
||||
### 2.1 Instrumententyp
|
||||
```csharp
|
||||
public enum InstrumentKind { Stock, Option }
|
||||
public enum OptionRight { Call, Put }
|
||||
|
||||
public sealed record OptionSpec(
|
||||
string Underlying,
|
||||
DateOnly Expiry,
|
||||
decimal Strike,
|
||||
OptionRight Right,
|
||||
int Multiplier = 100);
|
||||
```
|
||||
`TradeSignal` und `OrderRequest` bekommen je ein **optionales** `OptionSpec? Option`. Ist es null,
|
||||
verhält sich alles exakt wie heute.
|
||||
|
||||
### 2.2 Positionen mit Instrument-Identität
|
||||
`core_position` erhält `Kind`, `Expiry`, `Strike`, `Right`, `Multiplier`; der fachliche Schlüssel wird
|
||||
`(Module, Symbol, Kind, Expiry, Strike, Right)`. Bestandszeilen sind `Kind = Stock` mit leeren
|
||||
Optionsfeldern – Migration ohne Datenumzug. `Notional` rechnet ab jetzt mit dem Multiplikator.
|
||||
`IPortfolioService` bekommt entsprechend überladene Abfragen; die bestehenden Aktien-Signaturen bleiben.
|
||||
|
||||
### 2.3 Optionskette + Greeks
|
||||
Neuer Seam am `IBrokerClient`:
|
||||
```csharp
|
||||
Task<OptionChain?> GetOptionChainAsync(string underlying, DateOnly from, DateOnly to, CancellationToken ct);
|
||||
Task<OptionQuote?> GetOptionQuoteAsync(OptionSpec spec, CancellationToken ct); // + Delta, IV, OpenInterest
|
||||
Task<IReadOnlyList<BrokerPosition>> GetPositionsAsync(CancellationToken ct);
|
||||
```
|
||||
Umsetzung in `IbkrConnection`:
|
||||
- **Kette** über `reqSecDefOptParams` – liefert Expiries und Strikes je Underlying **ohne** Marktdaten
|
||||
und ohne Abo. Das ist der einzige gangbare Einstieg: eine volle Kette hat hunderte Kontrakte,
|
||||
TWS erlaubt aber nur ~100 gleichzeitige Marktdatenzeilen.
|
||||
- **Vorfilter, dann Quotes:** Expiry-Fenster aus dem DTE-Ziel, Strike-Fenster um den Spot (± x %) –
|
||||
erst für diese Handvoll Kontrakte `reqMktData` mit Greeks (`tickOptionComputation`).
|
||||
- `IbkrMapping.Option(spec)` → `SecType="OPT"`, `Exchange="SMART"`, `LastTradeDateOrContractMonth`
|
||||
als `yyyyMMdd`, `Strike`, `Right="C"/"P"`, `Multiplier="100"`, plus `TradingClass` (nötig, sobald ein
|
||||
Underlying mehrere Klassen führt, z. B. nach Splits oder bei Mini-Optionen). Rein, unit-getestet –
|
||||
wie das bestehende `IbkrMapping`.
|
||||
|
||||
### 2.4 Asynchrone Order- und Fill-Verfolgung
|
||||
Das ist der **teuerste, aber unumgängliche** Teil: eine Optionsorder wird limitiert am Mid platziert und
|
||||
füllt oft erst Minuten später oder gar nicht.
|
||||
- Neue Tabelle `core_order_state`: Order-ID, `SignalId`, Kontrakt, Menge, Limit, Status, gefüllte Menge,
|
||||
Zeitstempel – überlebt einen App-Neustart.
|
||||
- `IBrokerClient.SubmitOrderAsync(...)` gibt die Order-ID **ohne Warten** zurück; `PlaceOrderAsync`
|
||||
bleibt für den Aktienpfad erhalten (intern: submit + warten).
|
||||
- Dauerabo auf `orderStatus` / `openOrder` / `execDetails` statt Slot-je-Order; ein
|
||||
`OrderTrackingService` (`IHostedService`) trägt Endstatus nach, ruft dann `RecordFillAsync` und
|
||||
schreibt `core_order_events`. Nicht gefüllte Orders werden nach einer Frist neu bepreist oder storniert.
|
||||
- **Nebennutzen:** löst die in [IBKR-Integration.md](../IBKR-Integration.md) dokumentierte Grenze auch
|
||||
für CongressTrading.
|
||||
|
||||
### 2.5 Positionsabgleich (Zuteilung & Verfall)
|
||||
`ReconcilePositionsWorker` (`IHostedService`, z. B. stündlich und nach Marktschluss) vergleicht
|
||||
`reqPositions` mit `core_position`:
|
||||
- Aktien tauchen auf / Short Put verschwindet → **Zuteilung**.
|
||||
- Short Call verschwindet, Aktien weg → **Abruf (called away)**.
|
||||
- Option verschwindet am Verfallstag ohne Gegenbuchung → **wertlos verfallen**.
|
||||
Jede Abweichung wird gebucht **und** ins Entscheidungsjournal geschrieben. Unerklärbare Differenzen
|
||||
(manueller Eingriff in TWS) setzen den betroffenen Ticker auf `Halted`.
|
||||
|
||||
### 2.6 Risiko: sell-to-open + Deckung
|
||||
`RiskContext` bekommt `FreeShares` (nicht bereits durch andere Short Calls gebunden) und
|
||||
`AvailableCashForReservation`. Neue Regeln im `RiskService`:
|
||||
|
||||
| Fall | Regel |
|
||||
|---|---|
|
||||
| Sell-to-open Call | `FreeShares ≥ 100 × Kontrakte`, sonst Ablehnung „ungedeckt" |
|
||||
| Sell-to-open Put | `AvailableCashForReservation ≥ Strike × 100 × Kontrakte`, sonst Ablehnung |
|
||||
| Buy-to-close | zulässig bis zur offenen Short-Menge |
|
||||
| Limits | `MaxTradePercent` / `MaxPositionPercentPerModule` rechnen mit dem **Multiplikator** |
|
||||
|
||||
---
|
||||
|
||||
## 3. Modul `IBKRTrader.Modules.OptionsWheel`
|
||||
|
||||
`IModule` mit `Name = "OptionsWheel"`, `DbPrefix = "ow_"`, referenziert nur den Core, eigener DbContext,
|
||||
eigenes Fenster, eigene Settings-Sektion – analog Accounting/CongressTrading.
|
||||
|
||||
### 3.1 Zustandsautomat je Ticker
|
||||
```
|
||||
Idle ──sell put──► ShortPut ──ITM/DTE──► Rolling ──► ShortPut
|
||||
▲ │ │
|
||||
│ │ Zuteilung └─ kein Netto-Kredit ─► (Zuteilung zulassen)
|
||||
│ ▼
|
||||
└── called away ── Stock ──sell call──► ShortCall ──ITM/DTE──► Rolling ──► ShortCall
|
||||
▲ │
|
||||
└─ verfallen ─────────┘
|
||||
```
|
||||
Zusätzlich `PendingOrder` (Order im Buch) und `Halted` (Sperre nach Fehler/Abweichung, nur manuell lösbar).
|
||||
Der Automat ist **rein und unit-getestet** – er kennt weder Broker noch Datenbank.
|
||||
|
||||
### 3.2 Persistenz (`ow_`)
|
||||
| Tabelle | Inhalt |
|
||||
|---|---|
|
||||
| `ow_underlyings` | Watchlist: Ticker, aktiv, Parameter-Überschreibungen, aktueller Zustand |
|
||||
| `ow_cycles` | Ein Wheel-Durchlauf (Start, Ende, vereinnahmte Prämie brutto/netto, Ergebnis, Aktien-Einstand) |
|
||||
| `ow_legs` | Jede verkaufte/geschlossene Option: Kontrakt, Prämie, Delta+IV bei Eröffnung, DTE, Status, Roll-Kette |
|
||||
| `ow_candidates` | Bewertete Strike-Kandidaten je Scan (auch die verworfenen, mit Grund) – Nachvollziehbarkeit |
|
||||
| `ow_events` | Zustandsübergänge mit Auslöser |
|
||||
|
||||
`ow_candidates` ist bewusst dabei: bei Vollautomatik ist die **nicht** getroffene Wahl die wichtigste
|
||||
Information für die spätere Fehlersuche und für den Supervisor.
|
||||
|
||||
### 3.3 Reine Logik (`Logic/`, vollständig unit-getestet)
|
||||
- **`StrikeSelector`** – filtert die Kette: DTE-Fenster, Delta-Zielband, Mindestprämie (absolut und als
|
||||
annualisierte Rendite), maximaler Bid/Ask-Spread, Mindest-Open-Interest. Bewertet die Verbleibenden
|
||||
und begründet jede Verwerfung.
|
||||
- **`RollDecider`** – rollen ja/nein: Trigger, Zielkontrakt, Netto-Kredit-Bedingung.
|
||||
- **`CoverageCalculator`** – welche Aktien/welches Cash sind frei, welche durch offene Shorts gebunden.
|
||||
- **`PremiumMath`** – annualisierte Rendite, Break-even, effektiver Einstand nach Prämien.
|
||||
- **`WheelStateMachine`** – zulässige Übergänge, Ableitung des Zustands aus Positionen.
|
||||
|
||||
### 3.4 Worker
|
||||
- `WheelScanWorker` – im Handelszeitfenster: Zustand je Ticker prüfen, ggf. neues Leg eröffnen.
|
||||
- `WheelManageWorker` – offene Legs überwachen: rollen, schließen, Orders nachbepreisen.
|
||||
|
||||
### 3.5 UI (ein Fenster, Tabs)
|
||||
Übersicht (je Ticker: Zustand, offenes Leg, DTE, aktuelles Delta, Prämie vereinnahmt/annualisiert),
|
||||
Watchlist (Ticker + Parameter, Not-Aus je Ticker), Kandidaten (letzter Scan inkl. Verwerfungsgründe),
|
||||
Zyklen/Historie, Log. DB-Zugriff nur auf Interaktion (Smoke-UI-sicher).
|
||||
|
||||
---
|
||||
|
||||
## 4. Regelwerk (Defaults, je Ticker überschreibbar)
|
||||
|
||||
| Parameter | Default | Begründung |
|
||||
|---|---|---|
|
||||
| DTE beim Öffnen | 30–45 | Bestes Verhältnis Zeitwertverfall/Handelskosten |
|
||||
| Ziel-Delta | 0,20 (Band 0,15–0,30) | ≈ 20 % Zuteilungswahrscheinlichkeit |
|
||||
| Roll-Trigger | DTE ≤ 21 **und** (Delta > 0,50 oder ITM) | Gamma-Risiko steigt in der letzten Woche stark |
|
||||
| Roll-Bedingung | nur bei **Netto-Kredit** | Ein Debit-Roll kauft nur Zeit und kostet Geld |
|
||||
| Covered-Call-Strike | **≥ effektiver Einstand** der Aktien | Verhindert, dass der Wheel systematisch Verluste realisiert |
|
||||
| Max. Kontrakte je Ticker | 1 | Bewusst klein starten |
|
||||
| Max. gebundenes Cash gesamt | Anteil des Kontowerts | Zweite Grenze über die Core-Limits hinaus |
|
||||
| Vorzeitiges Schließen bei Gewinnziel | **aus** | Nicht gewählt – siehe Hinweis unten |
|
||||
|
||||
> **Hinweis zum Gewinnziel:** Der übliche Begleiter der Roll-Politik ist, ein Leg bei ~50 % vereinnahmter
|
||||
> Prämie zurückzukaufen; das nimmt Gamma-Risiko aus der letzten Woche und ist der Grund, warum viele
|
||||
> Roll-Trigger nie greifen. Der Parameter ist vorgesehen (`ProfitTargetPercent` existiert bereits in
|
||||
> `TradingSettings`), steht per Vorgabe auf „aus" und lässt sich ohne Codeänderung zuschalten.
|
||||
|
||||
> **Zuteilung ist nicht abwählbar.** Auch bei konsequentem Rollen teilt IBKR zu – amerikanische Optionen
|
||||
> können jederzeit ausgeübt werden, beim Call besonders vor dem Ex-Dividenden-Tag, und ein Roll ist nicht
|
||||
> immer per Netto-Kredit möglich. Zuteilung ist deshalb ein **regulärer Pfad** des Automaten, kein Fehler.
|
||||
> Genau dafür ist der Positionsabgleich (2.5) Pflicht.
|
||||
|
||||
---
|
||||
|
||||
## 5. Sicherungen
|
||||
Zu den bestehenden zwei Schaltern (`IBKR.UseTwsApi`, `Trading.TradingEnabled`) kommen:
|
||||
1. `OptionsWheel.Enabled` – Modulschalter.
|
||||
2. **Watchlist als Whitelist** – kein Ticker außerhalb.
|
||||
3. **Naked-Sperre im Core-`RiskService`** (2.6) – wirkt auch, wenn die Modullogik falsch liegt.
|
||||
4. `Halted` je Ticker bei unerklärter Positionsabweichung; Neueröffnungen stoppen, bestehende Legs
|
||||
werden weiter verwaltet.
|
||||
5. Kill-Switch „keine Neueröffnungen" – laufende Positionen bleiben handhabbar.
|
||||
|
||||
---
|
||||
|
||||
## 6. Phasen
|
||||
|
||||
| Phase | Inhalt | Abschlusskriterium |
|
||||
|---|---|---|
|
||||
| **W-0** | Core: `InstrumentKind`/`OptionSpec`, `IbkrMapping.Option`, Positionen mit Multiplikator | Build + Tests grün, Aktienpfad unverändert |
|
||||
| **W-1** | Core: Optionskette + Greeks (`reqSecDefOptParams`, `tickOptionComputation`) | Kette eines Watchlist-Tickers gegen das Paper-Gateway abrufbar |
|
||||
| **W-2** | Core: asynchrone Order-/Fill-Verfolgung (`core_order_state`, `OrderTrackingService`) | Limitorder überlebt Timeout und App-Neustart, Fill wird nachgebucht |
|
||||
| **W-3** | Core: Positionsabgleich + `RiskService` sell-to-open/Deckung | Zuteilung im Paper erkannt und gebucht; ungedeckte Order wird abgelehnt |
|
||||
| **W-4** | Modul-Gerüst: `IModule`, `ow_`-DbContext + Migration, UI-Tabs, Watchlist | App startet, `--smoke-ui` grün, kein Handel |
|
||||
| **W-5** | Reine Strategie-Logik + Tests (StrikeSelector, StateMachine, RollDecider, PremiumMath) | Hohe Testabdeckung ohne Broker |
|
||||
| **W-6** | Verdrahtung + vollautomatischer Paper-Betrieb, Beobachtung über mehrere Verfallszyklen | Mindestens ein vollständiger Wheel-Durchlauf im Paper |
|
||||
| **W-7** | Live-Freigabe | Eigene Entscheidung nach W-6 |
|
||||
|
||||
W-0 bis W-3 sind Core-Arbeit und nützen auch den anderen Modulen; erst ab W-4 entsteht das Modul selbst.
|
||||
|
||||
---
|
||||
|
||||
## 7. Bewusst offen / zu klären
|
||||
1. ~~**Optionsberechtigung im Paper-Konto DUR371528 prüfen**~~ → **erledigt am 2026-08-04, vorhanden.**
|
||||
Verifiziert gegen das laufende Gateway (Details siehe [IBKR-Integration.md](../IBKR-Integration.md),
|
||||
Abschnitt „Optionen"): `reqSecDefOptParams` für AAPL liefert 24 Verfallstermine, 127 Strikes,
|
||||
Multiplier 100 über SMART; eine **What-If-Order** auf `AAPL 20260812 C302.5` wurde von IBKR
|
||||
angenommen (Init-Margin 589,52) statt mit einem Berechtigungsfehler abgelehnt.
|
||||
Damit ist die Grundvoraussetzung für dieses Modul gegeben.
|
||||
2. ~~**Greeks bei verzögerten Daten.**~~ → **erledigt am 2026-08-04, funktioniert.** Gemessen an
|
||||
`AAPL 20260821 C305`: IV 0,2730 / Delta 0,5443 / Gamma 0,0220 / Vega 0,2626 / Theta −0,2304.
|
||||
Die Werte kommen über die verzögerten Tick-Felder 80–83; **Feld 83 (Modell)** ist die für die
|
||||
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.
|
||||
4. **Earnings-Sperre**: keine neuen Legs über Quartalszahlen hinweg. **Geprüft am 2026-08-04: über
|
||||
die TWS API nicht lösbar** – `CalendarReport` und alle übrigen Fundamentaldaten sind auf dem
|
||||
Konto gesperrt (Fehler 10358, Refinitiv-Abo nötig). Das ist die einzige Stelle, an der uns eine
|
||||
externe Quelle ernsthaft fehlt. **Behelf ohne Fremddaten:** ein IV-Filter – keine neuen Legs,
|
||||
wenn die implizite Volatilität des Basiswerts deutlich über ihrem 30-Tage-Mittel liegt. Fängt
|
||||
den Earnings-Anstieg indirekt mit ab, unschärfer, aber ohne Abhängigkeit.
|
||||
5. **Accounting-Anschluss**: Optionsprämien, Zuteilungen und Abrufe müssen im `AccountingClassifier`
|
||||
eigene Buchungskategorien bekommen; der `RealizedPnlEngine` (FIFO) kennt weder Multiplikator noch
|
||||
die Einstandsverschiebung durch Zuteilung. Eigene Arbeit im Accounting-Modul.
|
||||
6. **Steuer** bleibt wie im Accounting-Konzept unberührt und offen. Keine Steuerberatung.
|
||||
@@ -0,0 +1,81 @@
|
||||
> ### 📦 Archiviert am 2026-08-23
|
||||
> Dieses Dokument wird **nicht mehr gepflegt**. Was davon noch offen ist, steht in der
|
||||
> [Roadmap](../ROADMAP.md) (Bahn „Supervisor") – dort und nur dort wird der Stand nachgeführt.
|
||||
>
|
||||
> Es bleibt erhalten, weil es S-0 bis S-4 im Einzelnen beschreibt, samt Tool-Registry und den
|
||||
> Sicherheitsgrenzen des Agenten. Zum Nachschlagen also weiterhin richtig,
|
||||
> als Aufgabenliste nicht mehr.
|
||||
|
||||
---
|
||||
|
||||
# Konzept: Modul „Supervisor" (KI-gestützte Handels-Analyse & Forensik)
|
||||
|
||||
> **UMGESETZT (S-0 bis S-4).** Datenfundament im Core (`core_decision_journal`, `core_order_events`,
|
||||
> durchgereichte `SignalId`, JSONL-Log-Sink), `DossierService`/`DossierBuilder`, der
|
||||
> OpenRouter-Agent mit read-only Tool-Registry und Profilen, `sup_reports`, `CounterfactualJob`,
|
||||
> `DailyReportService` und der MCP-Server (`McpLightServer`, `McpJsonRpc`).
|
||||
>
|
||||
> **Weiterhin offen** sind die beiden Punkte am Ende dieses Dokuments: die Counterfactual-Kursauflösung
|
||||
> (Interface + Stub vorhanden) und der externe Versand des Tagesberichts.
|
||||
|
||||
> Stand: 2026-07-30
|
||||
> Ziel: ALLES, was IBKRTrader getan (und bewusst NICHT getan) hat, detailliert analysierbar machen —
|
||||
> Entscheidungen, Orders, Trades und Logs — und die Analyse durch ein KI-Modell (OpenRouter) durchführen
|
||||
> lassen: Warum hat ein Trade funktioniert? Warum nicht? Woran lag es?
|
||||
> **Leitidee: Erst das Datenfundament, dann die KI.** Ein Modell kann nur erklären, was aufgezeichnet wurde.
|
||||
>
|
||||
> Vorbild: gleichnamiges Modul in PolytraderSharp. Hier auf IBKR übertragen; strikt read-only.
|
||||
|
||||
## S-0 Datenfundament (Core — umgesetzt)
|
||||
Grundlage jeder guten Analyse, sofort auch OHNE KI nützlich (abfragbare Rejects, Log-Forensik):
|
||||
- **`core_decision_journal`** — JEDE Entscheidung (Executed/Rejected/Skipped/Failed) mit `ReasonCode`
|
||||
(Enum, als String persistiert), SignalId, Kontext-JSON und Freitext. Geschrieben vom `ExecutionService`.
|
||||
- **`core_order_events`** — Order-Lifecycle (Placed/Filled/PlaceFailed/…): Preise, Menge, Broker-Antwort.
|
||||
- **`SignalId`** wird durch `TradeSignal → ExecutionService → Portfolio → core_trade_history`
|
||||
durchgereicht → verbindet Signal → Entscheidung(en) → Order(s) → Trade.
|
||||
- **JSONL-Log-Sink** — zusätzlich zur Textdatei eine Zeile je Event nach `Logs/{yyyy-MM-dd}.jsonl`
|
||||
(`ts, level, source, cid=SignalId, message`); zeilenweise filter-/parsebar.
|
||||
- Pure Core-Analytik: `RealizedPnlEngine` (FIFO), `TradeAnalytics` (KPIs), `DossierBuilder`.
|
||||
- Schreibpfade sind **fehlertolerant** — ein Journal-/DB-Fehler bricht den Handel nie.
|
||||
|
||||
## S-1 Dossier
|
||||
`DossierService` setzt zu einer SignalId Entscheidungen + Order-Events + Trades + JSONL-Log-Auszug
|
||||
zusammen; `DossierBuilder` (Core, pur) rendert JSON (fürs Modell) und Markdown (für Menschen).
|
||||
|
||||
## S-2 Agent + Tool-Registry
|
||||
- In-Prozess-Function-Calling-Loop gegen **OpenRouter** (`OpenRouterClient`, OpenAI-kompatibel).
|
||||
- **Read-only-Tools** (`SupervisorTools`): `query_decisions`, `query_order_events`, `query_trades`,
|
||||
`get_dossier`, `read_logs`, `get_kpis`, `get_architecture_context`, `query_counterfactuals`.
|
||||
**Kein Tool kann handeln, canceln oder schreiben.**
|
||||
- **Profile** (`SupervisorProfiles`): Allgemein / Technik / CongressTrading = System-Prompt + Tool-Subset
|
||||
über EINER Infrastruktur (bewusst keine Agent-zu-Agent-Orchestrierung).
|
||||
- Harte Iterationsgrenze gegen Endlosschleifen; jeder Tool-Aufruf wird in der UI sichtbar geloggt.
|
||||
- System-Kontext: kuratiertes Architektur-/Verhaltensdokument (`ArchitectureContext`, inline versioniert).
|
||||
|
||||
## S-3 Berichte & Counterfactual
|
||||
- `sup_reports` — jede Analyse (Frage/Antwort/Profil/Modell/Tool-Aufrufe) → der Supervisor ist selbst
|
||||
auditierbar.
|
||||
- `CounterfactualJob` — „was wäre aus abgelehnten BUYs geworden?" (späterer Kurs vs. Signalpreis).
|
||||
Die Kursauflösung liegt hinter `ICounterfactualResolutionSource` mit **Null-Stub** (Zielland-Arbeit).
|
||||
- `DailyReportService` — täglicher Bericht, **opt-in** via `IBKRTRADER_SUPERVISOR_DAILY` (Stunde 0–23).
|
||||
|
||||
## S-4 MCP-Light
|
||||
`McpLightServer` exponiert dieselbe read-only Tool-Registry als lokalen MCP-Endpoint für externe Clients
|
||||
(z. B. Claude Code). **Opt-in** via `IBKRTRADER_MCP_PORT`, bindet nur `127.0.0.1`. Handler `McpJsonRpc`
|
||||
ist pur + unit-getestet (initialize/ping/tools.list/tools.call).
|
||||
|
||||
## Architektur & Unterbringung
|
||||
Projekt `src/IBKRTrader.Modules.Supervisor/` als `IModule` (`Name="Supervisor"`, `DbPrefix="sup_"`),
|
||||
referenziert nur den Core. Eigenes Fenster mit Tabs: Analyse (Chat), Dossier-Browser, Berichte, Settings.
|
||||
|
||||
## Sicherheit
|
||||
- **OpenRouter = bewusst freigegebener externer Datenempfänger.** Es werden nur Analyse-Daten der Tools
|
||||
gesendet, niemals Secrets/Keys/Connection-Strings.
|
||||
- Separater API-Key (`IBKRTRADER_OPENROUTER_KEY` oder gitignorierte `openrouter.key`), getrennt von
|
||||
künftigen Trading-Keys.
|
||||
- **Read-only by design** — kein Order-/Schreib-Tool. Prompt-Injection über Fremdtexte bleibt auf
|
||||
„falsche Analyse" begrenzt, kann nie handeln.
|
||||
|
||||
## Bewusst offen / Zielland-Arbeit
|
||||
- Counterfactual-Kursauflösung für Aktien (späterer Kurs) — Interface + Stub vorhanden.
|
||||
- Externer Versand des Tagesberichts (z. B. Threema) — heute nur Persistenz/Log.
|
||||
@@ -0,0 +1,485 @@
|
||||
> ### 📦 Archiviert am 2026-08-23
|
||||
> Dieses Dokument wird **nicht mehr gepflegt**. Was davon noch offen ist, steht in der
|
||||
> [Roadmap](../ROADMAP.md) (Stufe 4) – dort und nur dort wird der Stand nachgeführt.
|
||||
>
|
||||
> Es bleibt erhalten, weil es die vollstaendige Spezifikation ist, gegen die die Umsetzung geprueft wird –
|
||||
> die drei Wahrheiten, Eigentumsregeln, Reservierungs-Lebenszyklus, Invarianten und Testkatalog. Zum Nachschlagen also weiterhin richtig,
|
||||
> als Aufgabenliste nicht mehr.
|
||||
|
||||
---
|
||||
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user