Eine Roadmap statt sieben Konzepte; Quelldokumente ins Archiv
Build & Test / build (ubuntu-latest) (push) Waiting to run
Build & Test / build (windows-latest) (push) Waiting to run

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:
Richard
2026-08-23 18:14:51 +02:00
co-authored by Claude Opus 5
parent e1546bd1b1
commit 9f66183f1c
16 changed files with 362 additions and 32 deletions
@@ -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 8083, 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,150,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 3045 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 P1P6 und D1D7 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:** P1P4 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 (D1D7) 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 (P1P6) 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/`) | 17 | ✅ 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.
+714
View File
@@ -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 (L0L6). Zum Nachschlagen also weiterhin richtig,
> als Aufgabenliste nicht mehr.
---
# Analyse: Linux-Fähigkeit des IBKRTrader
> **UMGESETZT am 2026-08-07 (L0L5).** 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 2125 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 19 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 | **57 Personentage** |
| **B. Desktop-Linux** (zusätzlich die komplette UI auf Avalonia) | ~1.300 LOC WinForms neu bauen | **+1014 Personentage** |
| **C. LiveCharts2** (heute existiert **kein einziges** Diagramm) | Neubau, keine Migration | **+13 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 12 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: 12 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: 11,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 `102465535` 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.0002.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: 1014 Personentage** für jemanden, der Avalonia kennt. Ohne Vorerfahrung
realistisch +35 Tage Einarbeitung. LiveCharts2 zusätzlich 13 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: 12 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äß **12 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: 13 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 | 13 |
| **I** | Betrieb: systemd, Deployment, ggf. IB Gateway headless (IBC/Xvfb) | 13 |
| | **Gesamt** | **~2125 Personentage** |
Ohne Avalonia-Vorerfahrung auf Block G realistisch **+35 Tage** aufschlagen.
Zum Vergleich in Wochen: **headless in gut einer Woche**, **komplett mit Desktop-UI in etwa
45 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,53 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 12 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, 100138 | `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` | 99113 | verwirft Zeitzone, `Kind = Unspecified` ⚠ | D |
| `Core/IBKR/IBKRGatewayService.cs` | 247 | `.ToLower()` kulturabhängig, geht in URL | C |
| `Core/Workers/BuiltIn/BackupWorker.cs` | 95118 | `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, 270320 | `[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 |
+89
View File
@@ -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: EndsaldoAnfang = 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.
+257
View File
@@ -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,150,30).
> 4. Bei drohender Zuteilung wird **gerollt**, solange das per Netto-Kredit möglich ist.
## 0. Leitprinzipien
1. **Niemals nackt.** Ein Short Call ist nur zulässig mit 100 freien Aktien je Kontrakt, ein Short Put
nur mit reserviertem Cash über Strike × 100 je Kontrakt. Diese Deckungsprüfung gehört in den
**Core-`RiskService`**, nicht ins Modul ein Modulfehler darf keine ungedeckte Option schreiben können.
2. **Der Broker ist die Wahrheit.** Zuteilung und Verfall ändern Positionen **ohne** eine Order von uns.
Ohne regelmäßigen Positionsabgleich gegen IBKR läuft die eigene Buchführung zwangsläufig auseinander.
3. **Nur Watchlist.** Das Modul handelt ausschließlich explizit eingetragene Ticker. Keine Entdeckung,
kein Screening, keine Ausweitung zur Laufzeit.
4. **Jede Entscheidung ist nachlesbar.** Kandidatenbewertung, Zustandsübergang und Order gehen über
`core_decision_journal` / `core_order_events` mit gemeinsamer `SignalId` dieselbe Forensik-Grundlage,
die der Supervisor bereits nutzt.
---
## 1. Warum das nicht additiv geht: der Core ist heute aktienbasiert
| Stelle | Heutiger Stand | Konsequenz |
|---|---|---|
| `IbkrMapping.Stock()` | „der einzige Instrumententyp, den die Module handeln" | Kein Options-Kontrakt (Expiry/Strike/Right/Multiplier) baubar |
| `TradeSignal` / `OrderRequest` | nur `Symbol` + `Side` + `Quantity` | Ein Signal kann keine Option benennen |
| `Position.Notional` | `Quantity × AvgPrice` | Bei Optionen um Faktor 100 falsch → Risikolimits wirkungslos |
| `RiskService.EvaluateSell` | „Verkauf schließt Position", lehnt ohne Bestand ab | **Sell-to-open wird grundsätzlich abgelehnt** die Kernoperation des Moduls |
| `IBrokerClient.PlaceOrderAsync` | Fill-oder-Fehlschlag innerhalb `OrderTimeoutSeconds` | Limitorder am Mid liegt im Buch → gilt als Fehlschlag, ist aber aktiv (bekannte Grenze, [IBKR-Integration.md](../IBKR-Integration.md)) |
| `IBrokerClient` | kein `reqPositions`, keine Optionskette, keine Greeks | Zuteilung/Verfall unsichtbar, Strike-Wahl unmöglich |
Das Modul setzt also auf einem **Core-Options-Fundament** auf, das zuerst gebaut wird. Der Aktienpfad
bleibt dabei unverändert (neue Felder sind optional, `Kind = Stock` ist der Default) CongressTrading
darf nicht regressieren.
---
## 2. Core-Erweiterungen
### 2.1 Instrumententyp
```csharp
public enum InstrumentKind { Stock, Option }
public enum OptionRight { Call, Put }
public sealed record OptionSpec(
string Underlying,
DateOnly Expiry,
decimal Strike,
OptionRight Right,
int Multiplier = 100);
```
`TradeSignal` und `OrderRequest` bekommen je ein **optionales** `OptionSpec? Option`. Ist es null,
verhält sich alles exakt wie heute.
### 2.2 Positionen mit Instrument-Identität
`core_position` erhält `Kind`, `Expiry`, `Strike`, `Right`, `Multiplier`; der fachliche Schlüssel wird
`(Module, Symbol, Kind, Expiry, Strike, Right)`. Bestandszeilen sind `Kind = Stock` mit leeren
Optionsfeldern Migration ohne Datenumzug. `Notional` rechnet ab jetzt mit dem Multiplikator.
`IPortfolioService` bekommt entsprechend überladene Abfragen; die bestehenden Aktien-Signaturen bleiben.
### 2.3 Optionskette + Greeks
Neuer Seam am `IBrokerClient`:
```csharp
Task<OptionChain?> GetOptionChainAsync(string underlying, DateOnly from, DateOnly to, CancellationToken ct);
Task<OptionQuote?> GetOptionQuoteAsync(OptionSpec spec, CancellationToken ct); // + Delta, IV, OpenInterest
Task<IReadOnlyList<BrokerPosition>> GetPositionsAsync(CancellationToken ct);
```
Umsetzung in `IbkrConnection`:
- **Kette** über `reqSecDefOptParams` liefert Expiries und Strikes je Underlying **ohne** Marktdaten
und ohne Abo. Das ist der einzige gangbare Einstieg: eine volle Kette hat hunderte Kontrakte,
TWS erlaubt aber nur ~100 gleichzeitige Marktdatenzeilen.
- **Vorfilter, dann Quotes:** Expiry-Fenster aus dem DTE-Ziel, Strike-Fenster um den Spot (± x %)
erst für diese Handvoll Kontrakte `reqMktData` mit Greeks (`tickOptionComputation`).
- `IbkrMapping.Option(spec)``SecType="OPT"`, `Exchange="SMART"`, `LastTradeDateOrContractMonth`
als `yyyyMMdd`, `Strike`, `Right="C"/"P"`, `Multiplier="100"`, plus `TradingClass` (nötig, sobald ein
Underlying mehrere Klassen führt, z. B. nach Splits oder bei Mini-Optionen). Rein, unit-getestet
wie das bestehende `IbkrMapping`.
### 2.4 Asynchrone Order- und Fill-Verfolgung
Das ist der **teuerste, aber unumgängliche** Teil: eine Optionsorder wird limitiert am Mid platziert und
füllt oft erst Minuten später oder gar nicht.
- Neue Tabelle `core_order_state`: Order-ID, `SignalId`, Kontrakt, Menge, Limit, Status, gefüllte Menge,
Zeitstempel überlebt einen App-Neustart.
- `IBrokerClient.SubmitOrderAsync(...)` gibt die Order-ID **ohne Warten** zurück; `PlaceOrderAsync`
bleibt für den Aktienpfad erhalten (intern: submit + warten).
- Dauerabo auf `orderStatus` / `openOrder` / `execDetails` statt Slot-je-Order; ein
`OrderTrackingService` (`IHostedService`) trägt Endstatus nach, ruft dann `RecordFillAsync` und
schreibt `core_order_events`. Nicht gefüllte Orders werden nach einer Frist neu bepreist oder storniert.
- **Nebennutzen:** löst die in [IBKR-Integration.md](../IBKR-Integration.md) dokumentierte Grenze auch
für CongressTrading.
### 2.5 Positionsabgleich (Zuteilung & Verfall)
`ReconcilePositionsWorker` (`IHostedService`, z. B. stündlich und nach Marktschluss) vergleicht
`reqPositions` mit `core_position`:
- Aktien tauchen auf / Short Put verschwindet → **Zuteilung**.
- Short Call verschwindet, Aktien weg → **Abruf (called away)**.
- Option verschwindet am Verfallstag ohne Gegenbuchung → **wertlos verfallen**.
Jede Abweichung wird gebucht **und** ins Entscheidungsjournal geschrieben. Unerklärbare Differenzen
(manueller Eingriff in TWS) setzen den betroffenen Ticker auf `Halted`.
### 2.6 Risiko: sell-to-open + Deckung
`RiskContext` bekommt `FreeShares` (nicht bereits durch andere Short Calls gebunden) und
`AvailableCashForReservation`. Neue Regeln im `RiskService`:
| Fall | Regel |
|---|---|
| Sell-to-open Call | `FreeShares ≥ 100 × Kontrakte`, sonst Ablehnung „ungedeckt" |
| Sell-to-open Put | `AvailableCashForReservation ≥ Strike × 100 × Kontrakte`, sonst Ablehnung |
| Buy-to-close | zulässig bis zur offenen Short-Menge |
| Limits | `MaxTradePercent` / `MaxPositionPercentPerModule` rechnen mit dem **Multiplikator** |
---
## 3. Modul `IBKRTrader.Modules.OptionsWheel`
`IModule` mit `Name = "OptionsWheel"`, `DbPrefix = "ow_"`, referenziert nur den Core, eigener DbContext,
eigenes Fenster, eigene Settings-Sektion analog Accounting/CongressTrading.
### 3.1 Zustandsautomat je Ticker
```
Idle ──sell put──► ShortPut ──ITM/DTE──► Rolling ──► ShortPut
▲ │ │
│ │ Zuteilung └─ kein Netto-Kredit ─► (Zuteilung zulassen)
│ ▼
└── called away ── Stock ──sell call──► ShortCall ──ITM/DTE──► Rolling ──► ShortCall
▲ │
└─ verfallen ─────────┘
```
Zusätzlich `PendingOrder` (Order im Buch) und `Halted` (Sperre nach Fehler/Abweichung, nur manuell lösbar).
Der Automat ist **rein und unit-getestet** er kennt weder Broker noch Datenbank.
### 3.2 Persistenz (`ow_`)
| Tabelle | Inhalt |
|---|---|
| `ow_underlyings` | Watchlist: Ticker, aktiv, Parameter-Überschreibungen, aktueller Zustand |
| `ow_cycles` | Ein Wheel-Durchlauf (Start, Ende, vereinnahmte Prämie brutto/netto, Ergebnis, Aktien-Einstand) |
| `ow_legs` | Jede verkaufte/geschlossene Option: Kontrakt, Prämie, Delta+IV bei Eröffnung, DTE, Status, Roll-Kette |
| `ow_candidates` | Bewertete Strike-Kandidaten je Scan (auch die verworfenen, mit Grund) Nachvollziehbarkeit |
| `ow_events` | Zustandsübergänge mit Auslöser |
`ow_candidates` ist bewusst dabei: bei Vollautomatik ist die **nicht** getroffene Wahl die wichtigste
Information für die spätere Fehlersuche und für den Supervisor.
### 3.3 Reine Logik (`Logic/`, vollständig unit-getestet)
- **`StrikeSelector`** filtert die Kette: DTE-Fenster, Delta-Zielband, Mindestprämie (absolut und als
annualisierte Rendite), maximaler Bid/Ask-Spread, Mindest-Open-Interest. Bewertet die Verbleibenden
und begründet jede Verwerfung.
- **`RollDecider`** rollen ja/nein: Trigger, Zielkontrakt, Netto-Kredit-Bedingung.
- **`CoverageCalculator`** welche Aktien/welches Cash sind frei, welche durch offene Shorts gebunden.
- **`PremiumMath`** annualisierte Rendite, Break-even, effektiver Einstand nach Prämien.
- **`WheelStateMachine`** zulässige Übergänge, Ableitung des Zustands aus Positionen.
### 3.4 Worker
- `WheelScanWorker` im Handelszeitfenster: Zustand je Ticker prüfen, ggf. neues Leg eröffnen.
- `WheelManageWorker` offene Legs überwachen: rollen, schließen, Orders nachbepreisen.
### 3.5 UI (ein Fenster, Tabs)
Übersicht (je Ticker: Zustand, offenes Leg, DTE, aktuelles Delta, Prämie vereinnahmt/annualisiert),
Watchlist (Ticker + Parameter, Not-Aus je Ticker), Kandidaten (letzter Scan inkl. Verwerfungsgründe),
Zyklen/Historie, Log. DB-Zugriff nur auf Interaktion (Smoke-UI-sicher).
---
## 4. Regelwerk (Defaults, je Ticker überschreibbar)
| Parameter | Default | Begründung |
|---|---|---|
| DTE beim Öffnen | 3045 | Bestes Verhältnis Zeitwertverfall/Handelskosten |
| Ziel-Delta | 0,20 (Band 0,150,30) | ≈ 20 % Zuteilungswahrscheinlichkeit |
| Roll-Trigger | DTE ≤ 21 **und** (Delta > 0,50 oder ITM) | Gamma-Risiko steigt in der letzten Woche stark |
| Roll-Bedingung | nur bei **Netto-Kredit** | Ein Debit-Roll kauft nur Zeit und kostet Geld |
| Covered-Call-Strike | **≥ effektiver Einstand** der Aktien | Verhindert, dass der Wheel systematisch Verluste realisiert |
| Max. Kontrakte je Ticker | 1 | Bewusst klein starten |
| Max. gebundenes Cash gesamt | Anteil des Kontowerts | Zweite Grenze über die Core-Limits hinaus |
| Vorzeitiges Schließen bei Gewinnziel | **aus** | Nicht gewählt siehe Hinweis unten |
> **Hinweis zum Gewinnziel:** Der übliche Begleiter der Roll-Politik ist, ein Leg bei ~50 % vereinnahmter
> Prämie zurückzukaufen; das nimmt Gamma-Risiko aus der letzten Woche und ist der Grund, warum viele
> Roll-Trigger nie greifen. Der Parameter ist vorgesehen (`ProfitTargetPercent` existiert bereits in
> `TradingSettings`), steht per Vorgabe auf „aus" und lässt sich ohne Codeänderung zuschalten.
> **Zuteilung ist nicht abwählbar.** Auch bei konsequentem Rollen teilt IBKR zu amerikanische Optionen
> können jederzeit ausgeübt werden, beim Call besonders vor dem Ex-Dividenden-Tag, und ein Roll ist nicht
> immer per Netto-Kredit möglich. Zuteilung ist deshalb ein **regulärer Pfad** des Automaten, kein Fehler.
> Genau dafür ist der Positionsabgleich (2.5) Pflicht.
---
## 5. Sicherungen
Zu den bestehenden zwei Schaltern (`IBKR.UseTwsApi`, `Trading.TradingEnabled`) kommen:
1. `OptionsWheel.Enabled` Modulschalter.
2. **Watchlist als Whitelist** kein Ticker außerhalb.
3. **Naked-Sperre im Core-`RiskService`** (2.6) wirkt auch, wenn die Modullogik falsch liegt.
4. `Halted` je Ticker bei unerklärter Positionsabweichung; Neueröffnungen stoppen, bestehende Legs
werden weiter verwaltet.
5. Kill-Switch „keine Neueröffnungen" laufende Positionen bleiben handhabbar.
---
## 6. Phasen
| Phase | Inhalt | Abschlusskriterium |
|---|---|---|
| **W-0** | Core: `InstrumentKind`/`OptionSpec`, `IbkrMapping.Option`, Positionen mit Multiplikator | Build + Tests grün, Aktienpfad unverändert |
| **W-1** | Core: Optionskette + Greeks (`reqSecDefOptParams`, `tickOptionComputation`) | Kette eines Watchlist-Tickers gegen das Paper-Gateway abrufbar |
| **W-2** | Core: asynchrone Order-/Fill-Verfolgung (`core_order_state`, `OrderTrackingService`) | Limitorder überlebt Timeout und App-Neustart, Fill wird nachgebucht |
| **W-3** | Core: Positionsabgleich + `RiskService` sell-to-open/Deckung | Zuteilung im Paper erkannt und gebucht; ungedeckte Order wird abgelehnt |
| **W-4** | Modul-Gerüst: `IModule`, `ow_`-DbContext + Migration, UI-Tabs, Watchlist | App startet, `--smoke-ui` grün, kein Handel |
| **W-5** | Reine Strategie-Logik + Tests (StrikeSelector, StateMachine, RollDecider, PremiumMath) | Hohe Testabdeckung ohne Broker |
| **W-6** | Verdrahtung + vollautomatischer Paper-Betrieb, Beobachtung über mehrere Verfallszyklen | Mindestens ein vollständiger Wheel-Durchlauf im Paper |
| **W-7** | Live-Freigabe | Eigene Entscheidung nach W-6 |
W-0 bis W-3 sind Core-Arbeit und nützen auch den anderen Modulen; erst ab W-4 entsteht das Modul selbst.
---
## 7. Bewusst offen / zu klären
1. ~~**Optionsberechtigung im Paper-Konto DUR371528 prüfen**~~**erledigt am 2026-08-04, vorhanden.**
Verifiziert gegen das laufende Gateway (Details siehe [IBKR-Integration.md](../IBKR-Integration.md),
Abschnitt „Optionen"): `reqSecDefOptParams` für AAPL liefert 24 Verfallstermine, 127 Strikes,
Multiplier 100 über SMART; eine **What-If-Order** auf `AAPL 20260812 C302.5` wurde von IBKR
angenommen (Init-Margin 589,52) statt mit einem Berechtigungsfehler abgelehnt.
Damit ist die Grundvoraussetzung für dieses Modul gegeben.
2. ~~**Greeks bei verzögerten Daten.**~~**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 8083; **Feld 83 (Modell)** ist die für die
Strike-Wahl maßgebliche Variante. Die delta-basierte Wahl im Band 0,150,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.
+81
View File
@@ -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 023).
## 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.
+485
View File
@@ -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 |