Eine Roadmap statt fuenfzehn Plandokumente; Altbestand ins Archiv

Der Status des Projekts stand verstreut in elf Umsetzungsplaenen, drei
Konzepten, der Linux-Analyse und dem Projektstand - teils widersprechend, teils
wochenlang veraltet. Ab jetzt gibt es genau eine Statusquelle.

docs/ROADMAP.md (neu):
- Alle Vorhaben in vier Stufen A bis D, plus technische Schuld und Verlauf.
  Die Stufen sind eine Reihenfolge, keine Termine: jede schafft die
  Voraussetzung fuer die naechste.
- Statuszeichen: erledigt / offen / blockiert (mit Ursache) / bewusst
  zurueckgestellt / Idee, nicht beschlossen. Damit ist das, was wir NICHT bauen
  wollen, sichtbar vorgehalten statt unauffindbar in einem Plan zu schlummern.
- Inhaltlich getragen, nicht nur verlinkt: je Vorhaben Ziel, Phasen,
  Akzeptanzkriterien, offene Entscheidungen und Leitplanken aus den Quelldokumenten.
- Sichtbar gemacht, was vorher zwischen den Dokumenten verborgen lag:
  CopyTrading Phase 1 ist der Engpass der gesamten Roadmap (MarketMaking und
  BundleArbitrage haben harte Voraussetzungen darauf), und die Sniper-Metriken
  aus Phase 3.2 sind ein Spezialfall des StrategieDrift-Fingerprints - zusammen
  bauen statt doppelt.

Archiv (docs/archiv/):
- 15 Dokumente verschoben (11 Umsetzungsplaene, 3 Konzepte, ANALYSE-Linux-Portierung).
  Sie bleiben die Bauanleitungen mit Code-Bezuegen, Risikotabellen und
  Begruendungen - eingefroren ist nur ihr Status.
- archiv/README.md ordnet jedes Dokument seinem Roadmap-Punkt zu.

Verweise nachgezogen - der eigentliche Aufwand:
- 25 Markdown-Links repariert. 15 davon verschiebungsbedingt (eine Ebene
  tiefer), der Rest war schon vorher falsch: die Ideensammlung verlinkte
  Quellcode relativ zum Repo-Wurzelverzeichnis statt zu docs/.
- 12 Dateien ausserhalb von docs/ verwiesen in Kommentaren auf die Plaene
  (csproj, props, setup.json, sechs Quelldateien) - alle auf archiv/ umgebogen.
- Verweise auf Dateien, die der Fruehjahrsputz geloescht hat (Ui/,
  Program.cs, WindowMenuBar), zu Klartext entschaerft statt tote Links zu lassen.
- Gegenprobe: 85 Links geprueft, 0 kaputt. Build gruen, 476 Tests gruen.

PROJEKTSTAND.md entdoppelt: Abschnitt "Offen" verweist jetzt auf die Roadmap.
Arbeitsteilung ist damit klar - Projektstand sagt was IST, Roadmap was KOMMT.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-23 18:56:50 +02:00
co-authored by Claude Opus 5
parent 5507db3e32
commit 6218a04fe4
33 changed files with 569 additions and 129 deletions
+564
View File
@@ -0,0 +1,564 @@
# Analyse & Umsetzungsrahmen: Linux-Fähigkeit von PolyTrader
**Stand:** 22.08.2026 · **Revision 6** (Avalonia-Portierung P5P8 durch, Deploymentcenter D-0D-5 code-seitig durch, **WinForms ausgebaut**; offen sind CI, systemd/Feldtest und die Abnahmen)
**Basis:** 7 Projekte auf `net10.0`, **alle plattformneutral** — kein `net10.0-windows` mehr im Repo. 476 Tests.
> **Als Nächstes:** die Integration des überarbeiteten **Deploymentcenters** (P3c). Sie ersetzt
> **Lizenzverwaltung, Watchdog und Error-Reporting** grundlegend. Alles, was in diesem Dokument zu
> LicenseLabrador oder zum Watchdog-Heartbeat steht, ist damit Übergangszustand — dort nichts mehr
> ausbauen.
>
> **P3c hat seit dem 14.08.2026 einen eigenen Plan:**
> **[UMSETZUNGSPLAN-Deploymentcenter-Integration.md](./umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md)**
> — er ist ab jetzt maßgeblich und korrigiert zwei Aussagen dieses Dokuments:
> §5.2 unterschätzt die Watchdog-Umstellung („nur Header und URL"; tatsächlich kommen `version`,
> `os`, `checks`, `metrics` und `status: "stopped"` dazu), und §5.2 wie Risiko **R6** stützen die
> headless-Lizenzprüfung auf `ILicensePrompt` aus dem SDK — diese Schnittstelle wird dort jedoch
> **nirgends benutzt** und trägt nichts. Der Umfang liegt bei **47 PT** statt 24 PT, weil
> Error-Reporting, Auslieferung und Erstinstallation hier noch nicht enthalten waren.
---
## Entscheidungen (06.08.2026)
| # | Thema | Entscheidung | Auswirkung |
|---|---|---|---|
| 1 | Kultur-Bug | **Sofort gefixt** ✅ | Erledigt, siehe [Abschnitt 4.1](#41-erledigt--der-aktive-bug-ist-behoben) |
| 2 | Designer-Regel | **Neue Formulierung übernommen:** Layout deklarativ in `.axaml`, nicht zur Laufzeit im Code | ersetzt die WinForms-Designer-Regel |
| 3 | Windows-UI | **Variante B**: Modul-UI **entfernt** ✅, Core-UI folgt; Fallback ist der Tag `winforms-final` | umgesetzt 06.08.2026 |
| 4 | `PropertyGrid` | wird durch **normale Steuerelemente** ersetzt | +5 PT ggü. Fremdbibliothek, dafür bessere UX |
| 5 | Lizenz/Watchdog | laufen jetzt über das **Deploymentcenter**, nicht mehr über Einzeldienste | **Linux-Blocker entfällt**, siehe [5.2](#52-lizenz--watchdog--jetzt-über-das-deploymentcenter) |
| 6 | Zeitzone | **einmalig bei der Installation** festgelegt, nicht im laufenden Betrieb gewechselt | vereinfacht die Umsetzung, siehe [3.3](#33-anforderung-konfigurierbare-zeitzone-entscheidung-6) |
| 7 | Threema | **entfernt** ✅; Nachfolger: **RocketChat + Telegram** | umgesetzt am 06.08.2026, siehe [5.3](#53-threema--entfernt-) |
| 8 | Charts | **LiveCharts2** bestätigt (nach Klarstellung, dass die Integration nicht komplex ist) | +1 PT ggü. ScottPlot.Avalonia |
**Netto-Effekt auf den Gesamtaufwand:** damals auf **4161 PT** geschätzt. **Überholt — Stand 13.08.2026 sind noch 713 PT offen**, weil die Avalonia-Portierung (P5P8) inzwischen durch ist; siehe [Abschnitt 7](#7-fahrplan).
---
## 0. Kurzfassung
| Frage | Antwort |
|---|---|
| Ist es machbar? | Ja, ohne architektonische Sackgassen. |
| Was ist der Löwenanteil? | War die UI: ~8.300 LOC (34 % des Produktivcodes) hingen an WinForms. **Seit dem 22.08.2026 ist davon nichts mehr im Repo** — portiert nach Avalonia, Rest gelöscht. |
| Was ist überraschend gut? | Die **gesamte Trading-Kernlogik ist bereits portabel**: kein einziger `DllImport`, keine Registry, keine WMI, keine DPAPI, keine `SpecialFolder`, saubere `Path.Combine`-Nutzung. Nethereum, Pomelo/EF, `HttpClient`, `ClientWebSocket`, AES-GCM laufen unverändert. |
| Was war der teuerste Einzelpunkt? | Der **`PropertyGrid`-Ersatz** — erledigt, ersetzt durch den kategorisierten `SettingsEditor`. |
| Gesamtaufwand | **~49 Personentage offen** (Stand 22.08.2026). Die UI-Portierung ist inhaltlich durch (P5P8), das Deploymentcenter code-seitig (D-0D-5), der WinForms-Ausbau vollzogen. Offen sind noch **CI** (es gibt bis heute keinerlei Konfiguration), **systemd/Feldtest** sowie die Abnahmen **A5** und die Live-Abnahme des Deploymentcenters. |
| Wichtigste Erkenntnis | Galt: „Für ~15 PT bekommst du 80 % des Nutzens." **Eingetreten und überholt** — die UI wurde nicht als Fallback eingefroren, sondern vollständig nach Avalonia portiert. Der Rückfallstand liegt im Tag `winforms-final`. Siehe [Abschnitt 7](#7-fahrplan). |
---
## 1. Bestandsaufnahme
### 1.1 Projektstruktur und Zielframeworks
| Projekt | TargetFramework | `UseWindowsForms` | Warum WinForms? |
|---|---|---|---|
| `PolyTrader.App` | `net8.0-windows7.0` (`WinExe`) | ja | Shell, Launcher, 4 Core-Views |
| `PolyTrader.Core` | `net8.0-windows` | ja | **nur** UI-Contract (`ModuleUi.cs`, `WindowMenu.cs`) |
| `Modules.CopyTrading` | `net8.0-windows` | ja | 5 Views + `TradeRowColoring` (`System.Drawing.Color`) |
| `Modules.ResolutionFarming` | `net8.0-windows` | ja | 1 Fenster |
| `Modules.Supervisor` | `net8.0-windows` | ja | 1 Fenster |
| `Modules.Accounting` | `net8.0-windows` | ja | 1 Fenster |
| `PolyTrader.Tests` | `net8.0-windows` | ja | nur weil die Referenzen `-windows` sind |
| ~~`IcgSoftware.Threema.CoreMsgApi`~~ (lib) | — | — | **entfernt am 06.08.2026** (Entscheidung 7) |
Alle verbleibenden Projekte sind `-windows` **aber die Bindung ist dünn**. Core und Module brauchen WinForms ausschließlich für den UI-Contract; die Services darunter sind plattformneutral.
### 1.2 Wo sitzt der Windows-Code?
```
WinForms/System.Drawing berührt 41 Dateien davon:
├─ 33 reine UI-Dateien (Ui/-Ordner) → müssen ohnehin neu
└─ 5 „Ausreißer" außerhalb der Ui-Ordner → das ist die eigentliche Kopplung
├─ src/PolyTrader.Core/Modularity/ModuleUi.cs (Func<Form>, Image)
├─ src/PolyTrader.Core/Modularity/WindowMenu.cs (MenuStrip, Font)
├─ src/…CopyTrading/Logic/TradeRowColoring.cs (Color)
├─ Models/DashboardRow.cs (using System.Drawing)
└─ Program.cs (ApplicationConfiguration.Initialize)
```
**Nur 5 Dateien** stehen zwischen „Core/Module sind plattformneutral" und dem Ist-Zustand.
### 1.3 Code-Verteilung
| Kategorie | LOC | Anteil |
|---|---:|---:|
| UI (Designer + Code-Behind), App + Module | ~8.300 | 34 % |
| davon Designer-generiert (entfällt ersatzlos) | ~4.500 | 19 % |
| Services / Logik / Persistenz / Modelle | ~15.950 | 66 % |
| Tests (448 Testfälle, 48 Dateien) | ~5.300 | — |
| EF-Migrationen (unverändert übernehmbar) | 6.891 | — |
**Nur 1 von 48 Testdateien** ist UI-abhängig (`WindowMenuTests.cs`).
---
## 2. Die UI-Portierung
### 2.1 Control-Inventar und die neue Layout-Regel
16 Fenster/UserControls, ~8.300 LOC. Aus den Designer-Dateien:
| WinForms-Control | Anzahl | Avalonia-Ersatz | Bewertung |
|---|---:|---|---|
| `DataGridViewTextBoxColumn` | 69 | `DataGridTextColumn` | mechanisch, viel Fleißarbeit |
| `Label` | 49 | `TextBlock` | trivial |
| `ToolStripButton` | 37 | `Button` in `StackPanel` | Layout neu |
| `TabPage` / `TabControl` | 22 / 7 | `TabItem` / `TabControl` | trivial |
| `DataGridView` | 20 | `DataGrid` | siehe unten |
| `ToolStrip` | 16 | `StackPanel`/`DockPanel` | Layout neu |
| `Panel` / `Button` | 16 / 16 | direkt | trivial |
| `TextBox` / `ComboBox` | 9 / 9 | direkt | trivial |
| **`PropertyGrid`** | **6** | **handgebaute Formulare** (Entscheidung 4) | **teuerster Punkt, siehe 2.2** |
| `DateTimePicker` | 5 | `CalendarDatePicker` + `TimePicker` (getrennt!) | kleine Nacharbeit |
| `GroupBox` | 4 | `HeaderedContentControl`/`Border` | trivial |
| **`RichTextBox`** | **3** | kein Äquivalent, siehe 2.3 | mittel |
| `PictureBox` | 3 | `CartesianChart` (LiveCharts2) | siehe 2.4 |
| `SplitContainer` | 2 | `GridSplitter` | Layout neu |
| `MenuStrip` / `StatusStrip` | 1 / 1 | `Menu` / `DockPanel` | `ToolStripItemAlignment.Right` gibt es nicht → `DockPanel` |
**Entwarnung bei den DataGrids:** Die Nutzung ist erfreulich schlicht 25× `DataSource`-Binding, 35× `DefaultCellStyle`, 8× `AutoGenerateColumns`, nur 4 `SelectionChanged`, 3 `CellContentClick`, 1 `CellDoubleClick`. **Kein Virtual Mode, kein Custom Painting, keine `CellFormatting`.** Direkt auf `ItemsSource` + `Binding StringFormat` abbildbar.
Die Zeilenfärbung (`ClosedTradesView.cs`, `OpenTradesView.cs` über `DataBindingComplete``row.DefaultCellStyle.BackColor`) wird zu einem `IValueConverter` auf `DataGrid.LoadingRow` sauberer als heute.
> ### 📐 Die neue Layout-Regel (Entscheidung 2)
>
> Die bisherige Vorgabe „alle WinForms-Controls designerfähig (partial + `.Designer.cs`), nie code-only" wird ersetzt durch:
>
> **„Jede View besteht aus `View.axaml` (vollständiges Layout, deklarativ) und `View.axaml.cs` (nur Verdrahtung und Datenlogik). Steuerelemente und Layout werden NICHT zur Laufzeit im Code erzeugt."**
>
> Nutzen bleibt identisch: Layout ist vollständig inspizierbar, diffbar und an einer Stelle. Ein WYSIWYG-Designer existiert in Avalonia nicht stattdessen der **XAML-Previewer** (VS-/Rider-Extension) mit Live-Vorschau beim Tippen.
>
> **Praktische Konsequenz für die Portierung:** `WindowMenu.Populate()` baut die Menüleiste heute komplett zur Laufzeit auf (`menu.Items.Clear()` + dynamisch erzeugte `ToolStripMenuItem`). Das verstößt gegen die neue Regel und wird zu einem deklarativen `Menu` mit `ItemsSource`-Bindung an eine `ObservableCollection<WindowMenuEntry>` Layout im XAML, nur die Daten kommen aus dem Code. Gleiches gilt für die dynamisch angehängte Menüleiste in `ShellUiHost.AttachWindowMenu()`.
### 2.2 `PropertyGrid` → normale Steuerelemente (Entscheidung 4)
Betroffen sind **4 Fenster mit 6 Grid-Instanzen**:
| Datei | Instanz | Gebundenes Objekt |
|---|---|---|
| `Ui/Views/SettingsView.Designer.cs:21,33` | `propertyGrid`, `pgAccount` | `ServerSettings`, Account-Daten |
| `…CopyTrading/Ui/AccountSettingsView.Designer.cs:25` | `pgSettings` | `CopyTradingAccountSettings` |
| `…CopyTrading/Ui/MasterTradersView.Designer.cs:41` | `pgDetail` | `TrackedTrader` |
| `…ResolutionFarming/Ui/ResolutionFarmingMainForm.Designer.cs:38` | `pgSettings` | Farming-Settings |
**Vorarbeit, die sich jetzt auszahlt:** Die Modelle tragen bereits `[Description]`-Attribute als Inline-Erklärungen (`CopyTradingAccountSettings.cs:11`, `TrackedTrader.cs:10`) und `[Browsable(false)]` zum Ausblenden von Geheimnissen (`ServerSettings.cs:107,146` Watchdog-Token und Lizenzschlüssel). **Diese Attribute sind die Spezifikation der neuen Formulare** Beschriftung, Tooltip und Sichtbarkeit jedes Feldes stehen schon im Code. Das reduziert die Denkarbeit auf Layout und Validierung.
**Was die handgebauten Formulare besser können als das `PropertyGrid`:**
- **Eingabevalidierung vor dem Speichern** statt erst beim Commit der Zelle (bei Risiko-Limits und Preisgrenzen relevant)
- **Einheiten direkt am Feld** (USDC, %, bps, Minuten) statt im Beschreibungstext
- **Fachliche Gruppierung** statt alphabetischer Reflection-Reihenfolge
- **Abhängige Felder ausgrauen** (z. B. Ladder-Parameter nur bei aktivierter Sell-Ladder)
**Aufwand:** ~810 PT statt ~2 PT mit Fremdbibliothek. Der Aufschlag ist bewusst und kauft dauerhafte Wartbarkeit ohne Fremdabhängigkeit.
### 2.3 `RichTextBox` → Terminal-Fenster
`TerminalView` (378 + 195 LOC) und `SupervisorMainForm` (Chat) nutzen `RichTextBox` mit **Farbausgabe pro Log-Zeile**. Avalonia hat kein `RichTextBox`.
**Empfehlung:** `ItemsControl` über eine `ObservableCollection<LogLine>` mit Farb-Converter virtualisiert, deklarativ (passt zur neuen Layout-Regel), und schneller als heute. `TerminalView.cs:186` implementiert bereits ein Auto-Clear bei RAM-Limit; der Performance-Druck ist bekannt, ein virtualisiertes `ItemsControl` löst ihn an der Wurzel.
### 2.4 Charts: ScottPlot → LiveCharts2 (Entscheidung 8)
Nur **3 Charts**, alle in `DashboardView.RenderCharts()` (Equity-Kurve, PnL je Modul, PnL je Tag). Die Auswertungslogik liegt bereits pur in `TradeAnalytics` und bleibt **komplett unverändert**.
Der heutige Weg (`plot.GetImage(w,h).GetImageBytes()``System.Drawing.Bitmap``PictureBox`, `DashboardView.cs:207-227`) entfällt und wird zu drei deklarativen `CartesianChart`-Elementen im XAML was der neuen Layout-Regel ohnehin besser entspricht als das heutige Rendern im Code.
> **Zur Einordnung deiner Sorge:** Der Unterschied ist kleiner als befürchtet. `ScottPlot.Avalonia` wäre ~1 PT günstiger gewesen, nicht mehr bei **drei** Charts fällt der Integrationsvorteil kaum ins Gewicht. LiveCharts2 bringt dafür Animationen, Tooltips und Zoom out of the box, die du bei ScottPlot nachbauen müsstest. **Die Entscheidung für LiveCharts2 ist bei dieser Chart-Anzahl gut begründet** du zahlst dafür keinen nennenswerten Aufpreis.
### 2.5 `System.Drawing.Common` muss raus
Seit **.NET 7 ist `System.Drawing.Common` Windows-only** und wirft auf Linux `PlatformNotSupportedException`. Betroffen:
- `ModuleUi.cs:32` `System.Drawing.Image? Icon``Avalonia.Media.IImage`
- `TradeRowColoring.cs:11-13` `Color.FromArgb(...)` → eigenes Farbtupel oder `Avalonia.Media.Color`
- `WindowMenu.cs` `Font`, `FontStyle.Bold`
- `Models/DashboardRow.cs` `using System.Drawing` (vermutlich verwaist, prüfen)
- `DashboardView.cs:222-225` entfällt mit LiveCharts2
`TradeRowColoringTests.cs` testet die Farben direkt → zieht mit (klein).
### 2.6 PDF-Export: `PDFsharp-MigraDoc-GDI` → Core-Build
`Modules.Accounting` referenziert `PDFsharp-MigraDoc-GDI` 6.2.4 der **GDI+-Build ist Windows-only**. Ersatz: `PDFsharp-MigraDoc` (Core-Build), API-kompatibel.
**Aber:** Der Core-Build findet auf Linux keine Systemschriften. `PdfExporter.cs:34` setzt `style.Font.Name = "Segoe UI"` existiert auf Linux nicht. Nötig ist ein eigener **`IFontResolver`** plus eine als `EmbeddedResource` mitgelieferte Schrift (DejaVu Sans oder Open Sans, beide frei lizenziert). Einziger nennenswerter Umbau am Accounting-Modul.
`AccountingReportTests.cs:131` rendert PDFs headless im Test → zieht mit.
---
## 3. Zeitzonen ⏰
### 3.1 Der harte Punkt
`Ui/Views/TerminalView.cs:150`:
```csharp
TimeZoneInfo berlinTz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
```
Eine **Windows-Zeitzonen-ID**. Seit .NET 6 mappt `FindSystemTimeZoneById` Windows-IDs auf Linux über ICU funktioniert also *meistens*. Es bricht, sobald `InvariantGlobalization=true` gesetzt ist oder die Distro kein vollständiges ICU mitbringt (Alpine ohne `icu-libs`). Dann fliegt beim Öffnen des Terminal-Fensters eine `TimeZoneNotFoundException`.
Mit Entscheidung 6 verschwindet diese Stelle ohnehin die Zeitzone kommt künftig aus der Konfiguration, nicht mehr hartcodiert.
### 3.2 Die Now/UtcNow-Mischung
**122× `DateTime.UtcNow` gegen 23× `DateTime.Now`.** Konsistent, solange der Rechner auf `Europe/Berlin` steht. **Linux-Server laufen praktisch immer auf UTC.** Betroffene Stellen:
| Stelle | Auswirkung bei Server-TZ = UTC |
|---|---|
| `TerminalLogger.cs:23` `Timestamp = DateTime.Now` | Log-Zeitstempel 12 h versetzt |
| `TerminalLogger.cs:122`, `LauncherWidgetsPanel.cs:112` Logdatei `{DateTime.Now:yyyy-MM-dd}.jsonl` | **Tagesgrenze der Logdateien verschiebt sich** |
| `DossierService.cs:81`, `SupervisorTools.cs:144` suchen Logdateien per Datum | müssen dieselbe Konvention nutzen, sonst leere Dossiers |
| `DailyReportService.cs:61` | Tagesbericht schneidet an anderer Stelle |
| `AccountingMainForm.cs:57` Periodenvorbelegung | **steuerlich relevant** Monats-/Quartalsgrenzen |
| `PdfExporter.cs:55` „Erstellt: …" | Belegdatum im Steuer-PDF |
| `MasterTraderAnalyticsJob.cs` | mischt `Now` (Anzeige) und `UtcNow` (Cutoffs) funktioniert, ist aber verwirrend |
### 3.3 Anforderung: konfigurierbare Zeitzone (Entscheidung 6)
> **Ziel (präzisiert):** Die Zeitzone wird **einmalig bei der Installation** festgelegt — wie in einem Setup — und im laufenden Betrieb normalerweise nicht mehr gewechselt.
**Vorgeschlagene Umsetzung:**
1. **Neue Einstellung** `ServerSettings.ApplicationTimeZoneId` (Default `"Europe/Berlin"`). IANA-IDs verwenden .NET akzeptiert sie seit Version 6 auf **beiden** Plattformen, Windows-IDs dagegen nur eingeschränkt auf Linux.
2. **Zentraler Zugriffspunkt** statt verstreuter `DateTime.Now`-Aufrufe:
```
IAppClock
├─ DateTime UtcNow → für alle Domänen-/Persistenzwerte
├─ DateTime Local → für Anzeige und Logdatei-Namen
└─ TimeZoneInfo TimeZone → aufgelöst aus ApplicationTimeZoneId, mit Fallback auf UTC + Warnung
```
Damit ist die Zeitzone an **einer** Stelle wirksam statt an 23.
3. **Festlegung bei der Erstinstallation**, danach schreibgeschützt-mit-Bestätigung:
- Beim **ersten Start ohne gesetzte Zeitzone** wird sie abgefragt (UI-Dialog bzw. CLI-Schalter `--set-timezone` für den headless Betrieb) und in `server_settings.xml` festgeschrieben.
- Eine **spätere Änderung** ist möglich, aber bewusst unbequem: Bestätigungsdialog mit Klartext-Folgen („Logdatei-Tagesgrenzen verschieben sich um X Stunden. Bereits geschriebene Logs und Buchungen bleiben unverändert und sind danach in der neuen Zeitzone zu lesen.") und **Wirksamkeit erst nach Neustart**.
- **Protokolleintrag** mit alter und neuer Zeitzone, damit die Verschiebung später in den Logs nachvollziehbar ist.
Weil der Regelfall die einmalige Festlegung ist, entfällt jede Komplexität für einen Wechsel *zur Laufzeit* — kein Neu-Verdrahten laufender Jobs, kein Umschalten offener Fenster. Das ist der eigentliche Gewinn dieser Entscheidung.
4. **Zusätzlich** `TZ`-Umgebungsvariable in der systemd-Unit setzen, damit auch die Ausgaben *außerhalb* der Anwendung (Logrotate, Cron, Systemlogs) zusammenpassen.
**Offene fachliche Frage:** Soll die Zeitzone im Accounting **mitwandern** oder dort fix auf der steuerlich maßgeblichen Zeitzone stehen bleiben? Ein Wechsel des Betriebsstandorts ändert nicht zwingend den steuerlichen Bezugsrahmen. Empfehlung: **zwei getrennte Einstellungen** `ApplicationTimeZoneId` (Anzeige, Logs, Jobs) und `AccountingTimeZoneId` (Buchungsperioden, Steuerbelege), Default identisch. Kostet fast nichts, verhindert aber, dass ein Standortwechsel rückwirkend die Buchungsperioden verschiebt.
**Aufwand:** ~2 PT (Einstellung, `IAppClock`, 23 Aufrufstellen umziehen, Erstinstallations-Abfrage, Tests). Durch die Beschränkung auf die Installationszeit ~1 PT günstiger als in Revision 2 angesetzt.
### 3.4 MySQL
Pomelo speichert `DateTime` als `DATETIME` ohne Zeitzoneninformation. Bestehende Daten ändern sich beim Plattformwechsel nicht **aber die Interpretation neuer Werte** ändert sich. Prüfen: `SELECT @@global.time_zone, @@session.time_zone;` auf dem Ziel-Server. Bei gemischt geschriebenen Daten entstehen im Accounting stumme Lücken/Überlappungen an der Tagesgrenze. Der `IAppClock` aus 3.3 löst das, indem er die Persistenz konsequent auf UTC festlegt.
---
## 4. String- und Zahlenformatierung
### 4.1 ERLEDIGT der aktive Bug ist behoben ✅
`TraderMonitorService.cs` parste API-Preise kulturabhängig. **Empirisch verifiziert:**
```
de-DE "0.53" -> 53 "1234.5" -> 12345
Invariant "0.53" -> 0,53 "1234.5" -> 1234,5
```
Ein **Faktor-100-Fehler** im Einstandspreis der Master-Trader-Auswertung.
**Behoben am 06.08.2026:**
- `TraderMonitorService.cs:1276-1280` nutzt jetzt den bereits vorhandenen, korrekten Helper `ParseDecimal` statt duplizierter Inline-Logik. Der Helper ist `internal static` und dokumentiert.
- `PolymarketClobClient.cs:770, 784, 796, 820, 822, 827` alle `TryParse` auf `InvariantCulture` (Regex-Gruppen sind zwar ziffernrein und waren damit faktisch sicher, aber die Klasse Fehler soll nirgends mehr im Code stehen).
- `MasterTraderAnalyticsJob.cs:140` Unix-Timestamp-String invariant.
- **Neuer Regressionstest** `tests/PolyTrader.Tests/ApiNumberParsingTests.cs` 10 Fälle, die unter erzwungener **de-DE-Kultur** laufen und fehlschlagen, sobald jemand die Invariant-Angabe entfernt.
**Verifikation:** Build fehlerfrei, **448 Tests grün** (438 vorher + 10 neue).
### 4.2 Ausgabeformatierung (offen)
35× `DefaultCellStyle.Format`, 19× `ToString("…")`, dazu zahlreiche `:N2`/`:F2`/`:F3` in Log- und UI-Strings alle nach `CultureInfo.CurrentCulture`.
Auf Windows ist das `de-DE`. Auf Linux ist es **`LANG`/`LC_ALL`** unter systemd oft **gar nicht gesetzt** → .NET fällt auf **Invariant** zurück → aus `1.234,56 USDC` wird `1,234.56 USDC`. Kein Datenfehler, aber sofort sichtbar.
**Empfehlung:**
1. **Anzeige-Kultur beim Start explizit pinnen**: `CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("de-DE")` in `Program.Main`. Damit ist die Darstellung unabhängig von `LANG`. (Sinnvollerweise ebenfalls konfigurierbar, analog zur Zeitzone dieselbe Begründung: Betrieb im Ausland.)
2. **Globalisierungs-Analyzer aktivieren** (`CA1304`, `CA1305`, `CA1307`, `CA1310`). Sie hätten den Bug aus 4.1 gefunden.
### 4.3 Weitere Kulturthemen
- **ICU vs. NLS:** .NET nutzt seit Version 5 auf beiden Plattformen ICU Sortier- und Vergleichssemantik bleibt gleich. Voraussetzung: **kein `InvariantGlobalization=true`** im Publish-Profil, `libicu` auf dem Zielsystem (Debian/Ubuntu Standard, Alpine nachinstallieren).
- **String-Vergleiche:** durchgängig `StringComparison.OrdinalIgnoreCase` vorbildlich, keine Nacharbeit.
---
## 5. Weitere plattformspezifische Punkte
### 5.1 Dateisystem erfreulich sauber
| Prüfpunkt | Befund |
|---|---|
| `Path.Combine` | **17 von 17** korrekt, durchgängig über `AppContext.BaseDirectory` |
| `Environment.SpecialFolder` | **0 Treffer** |
| Harte Windows-Pfade | **genau 1**: `ServerSettings.cs:73` `MullvadCliPath` (konfigurierbar, nur der Default ist Windows) |
| `DllImport` / `LibraryImport` | **0 Treffer** |
| Registry / WMI / DPAPI / EventLog | **0 Treffer** im eigenen Code |
**Groß-/Kleinschreibung:** Linux ist case-sensitive. Kritisch: `server_settings.xml`, `appsettings.Local.json`, `master.key`, `openrouter.key`, `Logs/`, `favicon.ico`, die 24 PNGs in `Resources/`. Im Code konsistent trotzdem beim ersten Linux-Build gezielt prüfen, ebenso die `\`-Pfade in den `.csproj` (MSBuild normalisiert sie, aber es ist eine bekannte Stolperstelle).
**Dateirechte:** `master.key` und `openrouter.key` liegen als Klartext neben der Anwendung (`SettingsView.cs:301,356`, `Program.cs:393`, `OpenRouterClient.cs:46`). Auf Linux erzeugt `File.WriteAllText` mit Standard-umask **`644` world-readable**. Nach jedem Schreiben `File.SetUnixFileMode(path, UserRead | UserWrite)` (.NET 7+). Ergänzt Befund F1 des Sicherheitskonzepts.
> Das Deploymentcenter macht es beim Lizenz-Cache bereits richtig (`chmod 600 state.dat`, siehe LICENSE_INTEGRATION_GUIDE Abschnitt 3) dieselbe Konvention für die eigenen Schlüsseldateien übernehmen.
**Zeilenenden:** `Environment.NewLine` wechselt von `\r\n` auf `\n`. `CsvExporter.cs` nutzt bereits explizites `'\n'` korrekt.
### 5.2 Lizenz & Watchdog jetzt über das Deploymentcenter (Entscheidung 5)
**Das war in Revision 1 ein Blocker (R4) er ist gelöst.** Aus `J:\Softwareprojekte\Deploymentcenter\docs`:
| Thema | Alt (`LicenseLabrador.Client` 1.0.0) | Neu (Deploymentcenter, HWID v2) |
|---|---|---|
| Hardware-ID | Windows: Registry `MachineGuid`; Linux: `/etc/machine-id`**plattformabhängig verschieden** | `2:<plattform>:<sha256>` mit dokumentierter Quellen-Priorisierung je Plattform |
| Container | jede Instanz eine neue ID → Lizenz invalidiert bei jedem Neustart | **`DEPLOYMENTCENTER_HWID`-Override** gewinnt plattformweit |
| Rechnername im Hash | — | **bewusst nicht enthalten**, Umbenennen kostet keinen Aktivierungsplatz |
| Migration | — | Client sendet `hardware_id` (v2) **und** `legacy_hardware_id` (v1); Server zieht die Aktivierung lautlos um, **ohne zusätzlichen Platz** |
| Headless | modaler Dialog blockiert den Start | CLI-Schalter `--license-status`, `--license-set-key`, `--license-deactivate` **plus** `ILicensePrompt`/`ConsoleLicensePrompt` als UI-Abstraktion |
| Cache | — | `LLS2`-Envelope, AES-256-GCM, HKDF aus HWID+Slug, Linux `0600`, kein Klartext-Rückfall |
| SDK | `netstandard2.0` | `Deploymentcenter.Client`, Multi-Targeting `netstandard2.0;net8.0` |
**Damit entfallen zwei Risiken vollständig:** die Neubindung der Lizenz beim Plattformwechsel (Migration v1→v2 ist serverseitig gelöst) und der Container-Neustart-Effekt.
**Verbleibende Arbeit** ein Umbau, kein Blocker:
1. **Lizenz:** `Licensing/LicenseGate.cs` und `Ui/LicenseDialog.cs` von `LicenseLabrador.Client` auf `Deploymentcenter.Client` umstellen. Die API unterscheidet sich: `LicenseResult`/`LicenseState`/`GetHardwareId()``LicenseValidationResult` (`IsValid`, `Status`, `IsCached`, `Message`) / `HardwareId.GetHardwareId(slug)` / `ValidateAsync(slug, key, serverUrl)`. **`ILicensePrompt` implementieren** statt den Dialog direkt aufzurufen das ist genau die Naht, die den headless-Betrieb und später die Avalonia-UI trägt. Das lokale NuGet-Paket in `lib/nuget/` wird ersetzt.
2. **Watchdog:** `WatchdogHeartbeatService` sendet bereits `source`, `instance`, `interval` **das Payload-Schema passt schon**. Zu ändern sind nur Header (`X-Watchdog-Key``X-Agent-Token`) und Ziel-URL (`watchdog.mhdf.de``dc.mhdf.de/api/watchdog/v1/ping`). Ergänzen: `os`- und `group`-Feld, sowie der im Guide empfohlene Abschluss-Ping beim geordneten Herunterfahren.
**Aufwand:** ~23 PT. **Empfehlung: in Stufe L1 mitnehmen**, nicht später der headless-Betrieb (L2) hängt an `ILicensePrompt`.
> **Sicherheitshinweis am Rande:** `server_settings.xml` enthält den Watchdog-Token und den Lizenzschlüssel im Klartext (`WatchdogToken`, `LicenseKey`). Beim Umbau auf das Deploymentcenter ist der passende Moment, beide über die `SecretProtection` laufen zu lassen oder auf Umgebungsvariablen umzustellen passend zur bereits offenen Auflage, die Watchdog-Secrets zu rotieren.
### 5.3 Threema entfernt ✅ (Entscheidung 7)
**Am 06.08.2026 umgesetzt.** Nachfolgekanäle werden **RocketChat und Telegram**.
**Was entfernt wurde:**
| Entfernt | Umfang |
|---|---|
| `src/PolyTrader.Core/Services/ThreemaService.cs` | 1 Datei (`BackgroundService` + Webhook-Listener) |
| `libs/Threema-MsgApi-Net-Core` (vendorte Fremdbibliothek) | 35 Dateien, aus Solution und Git entfernt (History bleibt) |
| Projektreferenz in `PolyTrader.Core.csproj` | 1 |
| Threema-Block in `ServerSettings` (7 Properties, `[Category("Threema Notifications")]`) | 36 Zeilen |
| DI-Registrierungen in `Program.cs` (Singleton + HostedService + View-Injektion) | 3 |
| `ThreemaService`-Verdrahtung in `SettingsView` | 3 Stellen |
**Was an die Stelle getreten ist — `INotificationSink`:**
```
PolyTrader.Core/Notifications/
├─ INotificationSink.cs Schnittstelle + NotificationSeverity (Info/Warning/Critical)
└─ LogNotificationSink.cs Übergangs-Implementierung: schreibt ins Terminal-Log
```
Die drei Fachdienste kennen jetzt nur noch die Schnittstelle:
| Dienst | Meldung | Severity |
|---|---|---|
| `MasterTraderAnalyticsJob` | Auto-Pause eines Master-Traders | `Warning` |
| `SellLadderService` | SELL-Floor ohne Fill erreicht, Position wird gehalten | `Critical` |
| `DailyReportService` | täglicher Supervisor-Kurzbericht | `Info` |
**Zwei Entwurfsentscheidungen, die sich später auszahlen:**
1. **No-Throw-Vertrag.** `SendAsync` wirft laut Schnittstellen-Doku **niemals** — Kanalfehler werden intern abgefangen und protokolliert. Das ist genau die Best-Practice aus dem Watchdog-Guide („Fange Netzwerkfehler stets stumm ab, damit der Ausfall des Monitoring-Servers niemals den Hauptanwendungsfluss unterbricht"). Die Aufrufer verzichten deshalb bewusst auf eigene `try/catch`-Blöcke — der Code wurde dabei kürzer, nicht länger. **Bindend für die kommenden RocketChat- und Telegram-Implementierungen.**
2. **`NotificationSeverity`** ist von Anfang an dabei, obwohl die Log-Implementierung sie kaum nutzt. Bei zwei Kanälen (RocketChat für alles, Telegram als Fallback für Kritisches — oder umgekehrt) ist die Einstufung die natürliche Routing-Information. Sie nachträglich einzuziehen hätte alle Aufrufstellen erneut angefasst.
**Nebeneffekte — vier Linux-Themen sind mit weggefallen:**
| Entfallen | War |
|---|---|
| `libsodium` 1.0.16 (Baujahr 2018) | Risiko R7: `.so` gegen alte glibc gelinkt, Ladefehler auf Ubuntu 24.04 möglich |
| `Microsoft.Win32.Registry` (`E2EHelper.cs`, `#if CoreWinOnly`) | **die einzige Registry-Nutzung im gesamten Build** |
| `HttpListener` auf `http://*:{port}/` (Webhook) | Port-Bindung + `netsh`-Fehlermeldung mit Windows-Semantik |
| `System.Web.HttpUtility` | — |
**Verifikation:** Clean-Build fehlerfrei, **448 Tests grün**, Compiler-Warnungen von **17 auf 9** gesunken (die 8 Warnungen der Threema-Bibliothek — 4× `SYSLIB0014`, 3× `CA1416`, 1× `CS0472` — sind weg).
**Testverbesserung nebenbei:** Die drei Tests, die bisher `new ThreemaService(logger, new JobManager())` als Wegwerf-Instanz injizierten, nutzen jetzt `FakeNotificationSink` — damit ist **prüfbar, ob und mit welcher Severity gemeldet wurde**. Vorher war das nicht beobachtbar.
**Offen für später:** `RocketChatNotificationSink` und `TelegramNotificationSink` implementieren und in `Program.cs` registrieren — eine Klasse je Kanal, keine Änderung an der Fachlogik. Bei mehreren aktiven Kanälen bietet sich ein `CompositeNotificationSink` an (leitet an alle weiter, jeder Kanal fängt seine Fehler selbst ab).
### 5.4 Verbleibende Netzwerkdienste
- **`HttpListener`** nach dem Threema-Ausbau nur noch der MCP-Light-Server (`127.0.0.1:{port}/mcp`). Läuft auf Linux über die managed Implementierung, kein `netsh` nötig. Unkritisch.
- **`MullvadVpnService`**: Der CLI-Aufruf ist portabel (`UseShellExecute = false`, sauber). Auf Linux heißt die CLI `mullvad` (typisch `/usr/bin/mullvad`). **Achtung:** `HealthCheckAsync()` prüft `status.Contains("Connected")` der Wortlaut der Linux-CLI weicht ab. **Muss gegen die echte CLI verifiziert werden**, sonst reconnectet der Dienst dauerhaft ins Leere und meldet nach 3 Fehlversuchen „unrecoverable".
- **Nethereum 6.1.0, Pomelo/EF Core 8, `HttpClient`, `ClientWebSocket`, `System.Text.Json`, AES-GCM** (OpenSSL): plattformneutral, **keine Anpassung nötig**. Das ist der gesamte Trading-Pfad.
### 5.5 Windows-UI als Fallback: Variante 2 (Entscheidung 3)
Heute laufen Generic Host und `Application.Run` im **selben Prozess**; darunter **19 `BackgroundService`-Instanzen**. Auf Linux gibt es kein „WinExe" eine Avalonia-App braucht X11/Wayland, ein Trading-Server hat typischerweise kein Display.
**Entschieden: Variante 2 eingefrorener Stand.** Die WinForms-UI bleibt auf dem heutigen Stand und wird nicht weiterentwickelt; später verschwindet sie **vollständig aus dem Build**, damit keine Altlast mitgeschleppt wird.
**Was das konkret heißt:**
| | |
|---|---|
| **Einfrieren** | Vor Beginn von L3 einen Git-Tag setzen (Vorschlag: `winforms-final`) und im Repo dokumentieren. Ab da keine Feature-Arbeit mehr an `Ui/` und den `Ui/`-Ordnern der Module. |
| **Fallback im Ernstfall** | Tag auschecken, bauen, starten. Greift auf dieselbe MySQL zu — Anzeige und Einstellungen bleiben voll brauchbar. |
| **Weiterentwicklung** | Läuft unverändert weiter — nur eben im **Trading-Kern**, der ab L2 headless auf Linux läuft. Eingefroren ist die *Oberfläche*, nicht die Funktion. |
| **Endgültiger Ausbau** | ✅ **Vollzogen am 22.08.2026.** `Ui/`, `PolyTrader.App.csproj`, alle `.Designer.cs`/`.resx`, `UseWindowsForms`, `WinExe`, `favicon.ico` und `ApplicationConfiguration.Initialize()` sind entfernt; das Projekt ist aus `PolyTraderSharp.sln` genommen. Die PNG-Symbole sind **nicht** mitgelöscht, sondern nach `src/PolyTrader.App.Avalonia/Assets/` gewandert (der `avares://`-Pfad blieb gleich). |
**Wichtig für die Reihenfolge:** Damit der eingefrorene Stand überhaupt lauffähig bleibt, darf **P1 die WinForms-UI nicht kaputtmachen**. Beim Entkoppeln von Core und Modulen (Zielframework `net8.0`, `System.Drawing` raus) muss der WinForms-Zweig bis zum Tag baubar bleiben — praktisch heisst das: **erst taggen, dann P1 durchziehen**. Sonst ist der Fallback genau in dem Moment weg, in dem man ihn braucht.
> **Konkreter Vorschlag zur Absicherung:** Vor P1 einmal `dotnet publish -c Release` des heutigen Stands erzeugen und das Ergebnis zusammen mit dem Tag archivieren. Ein fertig gebautes Verzeichnis ist im Ernstfall schneller einsatzbereit als ein Checkout, der erst wieder kompiliert werden muss — und beweist nebenbei, dass der eingefrorene Stand tatsächlich läuft.
**Unabhängig davon nötig:** headless-fähiger Einstiegspunkt für den Linux-Betrieb, systemd-Unit mit `Restart=always` und `Environment=TZ=…`, Master-Key über `EnvironmentFile` mit `chmod 600`, Logrotate für `Logs/*.jsonl`.
### 5.6 Tests und CI
- Testprojekt auf `net8.0` (fällt mit den Referenzen).
- `WindowMenuTests.cs` einziger UI-Test, neu schreiben (~0,5 PT).
- `TradeRowColoringTests.cs` zieht mit dem Farbtyp-Wechsel mit (~0,2 PT).
- `AccountingReportTests.cs:131` braucht den neuen FontResolver.
- **`--smoke-ui`** (`Program.cs:500-570`): konstruiert alle Views headless und hat laut Kommentar bereits Designer-Regressionen gefangen. **Unbedingt erhalten** Avalonia bietet `Avalonia.Headless`, das Muster überträgt sich gut.
- **Es gibt keine CI.** Die Portierung ist der richtige Anlass, `dotnet build` + `dotnet test` auf einem Linux-Runner einzurichten sonst schleicht sich die Windows-Abhängigkeit unbemerkt zurück.
---
## 6. Aufwandsschätzung (Revision 2)
| # | Paket | Änderung ggü. Rev. 1 | Aufwand |
|---|---|---|---:|
| ~~L0~~ | ~~Kultur-Bug~~ | **erledigt** ✅ | ~~0,5 PT~~ |
| **P0** | Zielbild, Avalonia-Spike an einer echten View, Layout-Konventionen festzurren | — | **23 PT** |
| **P1** | Core/Module von WinForms entkoppeln, `TargetFramework` auf `net8.0`, `System.Drawing` raus | — | **23 PT** |
| **P2** | Kultur & Zeit: `IAppClock`, Zeitzone bei Installation festlegen, Kultur pinnen, Analyzer | **1 PT** (Entscheidung 6 vereinfacht) | **23 PT** |
| **P3** | Plattform-Kleinkram: Mullvad-Pfad + Status-Parsing, Unix-Dateirechte | **0,5 PT** (Threema entfällt) | **1 PT** |
| ~~**P3b**~~ | ~~Threema-Ausbau + `INotificationSink`~~ | **erledigt 06.08.2026** ✅ | ~~2 PT~~ |
| **P3c** | **Lizenz/Watchdog auf Deploymentcenter umstellen** | **neu** (Entscheidung 5) | **23 PT** |
| **P4** | PDF-Export: PDFsharp Core + `IFontResolver` + eingebettete Schrift | — | **12 PT** |
| ~~**P5**~~ | ~~Charts auf LiveCharts2~~ | — | ✅ **erledigt** (`0b8728b`) |
| ~~**P6**~~ | ~~UI-Shell: Launcher, Fenstermenü (deklarativ!), `ShellUiHost`, Shutdown-Dialog, Widgets~~ | — | ✅ **erledigt** (`cd59e5c`, `7f0b05e`). Lizenzdialog **bewusst ausgenommen** → kommt mit P3c |
| ~~**P7**~~ | ~~Core-Views: Dashboard, Settings, Jobs, Terminal~~ | — | ✅ **erledigt** (`0b8728b`, `4ba8149`, `929fa20`) PropertyGrid durch kategorisierten `SettingsEditor` ersetzt |
| ~~**P8**~~ | ~~Modul-Fenster: CopyTrading, ResolutionFarming, Supervisor, Accounting~~ | — | ✅ **erledigt** (`669676a`, `7f0b05e`) |
| **P9** | Tests ✅ (450 grün), `--smoke-ui` auf `Avalonia.Headless`, **Linux-CI fehlt vollständig** | — | **23 PT** |
| **P10** | Deployment: systemd, TZ ✅, Logrotate, Feldtest im Zielland | — | **23 PT** |
| ~~**P11**~~ | ~~**WinForms endgültig ausbauen**: `Ui/`, Designer-Dateien, `UseWindowsForms`, `WinExe`~~ | — | ✅ **erledigt** (22.08.2026) |
| **P3c** | **Deploymentcenter**: Lizenzverwaltung, Watchdog, Error-Reporting neu — D-0 bis D-5 code-seitig erledigt, **Live-Abnahme offen** | — | **Rest: Abnahme** |
| | **Summe (offen)** | | **49 PT** |
*Personentage entwickelnder Arbeit. Mit Agentenunterstützung sinkt die Kalenderzeit deutlich; der Review- und Feldtest-Anteil (P9, P10) bleibt weitgehend gleich bei einer Anwendung, die echtes Geld bewegt, ist das die richtige Reihenfolge.*
---
## 7. Fahrplan
### Die zentrale Erkenntnis — eingetreten, dann überholt
Ursprünglich galt: **P1P4 brauchen kein Avalonia**, und mit einem headless-Einstiegspunkt hätte man
für ~15 PT von rund 50 den eigentlichen Nutzen — Bot auf Linux, WinForms-UI als eingefrorener
Fallback.
**So ist es nicht gekommen, und das ist gut so.** Die Portierung lief weiter, bis die Oberfläche
vollständig unter Avalonia stand. Der Fallback wurde damit vom Dauerzustand zur reinen
Rückversicherung im Tag `winforms-final`. Der aktuelle Engpass ist nicht mehr die UI, sondern die
**Betriebsreife**: Deploymentcenter, CI, systemd und der Feldtest im Zielland.
| Stufe | Inhalt | Aufwand | Ergebnis |
|---|---|---:|---|
| ~~**L0**~~ | ~~Kultur-Bug~~ | — | ✅ **erledigt** Faktor-100-Preisfehler behoben, 10 Regressionstests |
| ~~**L0b**~~ | ~~Threema-Ausbau + `INotificationSink`~~ | — | ✅ **erledigt** 36 Dateien raus, 448 Tests grün, 8 Warnungen weniger |
| ~~**L1a**~~ | ~~Tag `winforms-final` + P1 (Core/Module entkoppeln) + P4 (PDF)~~ | — | ✅ **erledigt** Core, 4 Module und Testprojekt sind `net8.0`; alle sechs publishen für linux-x64 ohne Windows-Pakete; 442 Tests grün |
| **L1b** | P0 ✅ + P2 ✅ + P3 (Mullvad, Dateirechte) + **P3c (Deploymentcenter)** | 24 PT | P0/P2 erledigt (Spike wurde zur vollen Portierung; Zeitzone konfigurierbar). **Offen: P3c — steht als Nächstes an** und ersetzt Lizenzverwaltung, Watchdog und Error-Reporting grundlegend. |
| **L2** | ~~Headless-Einstiegspunkt~~ ✅ + systemd + Feldtest im Zielland | 24 PT | `--headless` existiert ([`App.Avalonia/Program.cs:41`](../../src/PolyTrader.App.Avalonia/Program.cs)). Offen: systemd-Unit + Feldtest. **Trading läuft headless auf Linux.** ⭐ Meilenstein |
| ~~**L3**~~ | ~~P5 + P6 + P7 (Charts, Shell, Core-Views)~~ | — | ✅ **erledigt** Avalonia-Grundgerüst (`cd59e5c`), Dashboard mit LiveCharts2 (`0b8728b`), Settings (`4ba8149`), Terminal (`929fa20`), Dunkelmodus (`487ea44`) |
| **L4** | ~~P8 (Modul-Fenster)~~ ✅ + P9 (**CI**) | 24 PT | Modul-Fenster portiert (`669676a`), Restlücken A1A4 geschlossen (`7f0b05e`), 450 Tests grün. **Offen: es gibt keinerlei CI-Konfiguration** weder `.github/workflows` noch `.gitea/workflows`. |
| ~~**L5**~~ | ~~P11 WinForms endgültig aus dem Build entfernen~~ | — | ✅ **erledigt am 22.08.2026** (Frühjahrsputz). Vorgezogen vor die A5-Abnahme, weil der Rückfall-Tag `winforms-final` auf dem Server gesichert ist und A5 auch gegen einen Worktree dieses Tags geprüft werden kann. Ergebnis: 91 Dateien und ~4.500 LOC Designer-Code weg, Build-Warnungen von 23 auf 15 gesunken, `LicenseLabrador.Client` mit ausgebaut (D-6). |
### Reihenfolge-Begründung
- **Der Tag `winforms-final` kommt VOR P1.** P1 entkoppelt Core und Module von WinForms — danach ist der alte Stand nicht mehr aus dem Hauptzweig baubar. Erst taggen (und einmal `dotnet publish` archivieren), dann umbauen. Sonst ist der Fallback genau dann weg, wenn man ihn braucht.
- **P3c (Deploymentcenter) gehört nach L1, nicht später** der headless-Betrieb in L2 hängt an der `ILicensePrompt`-Abstraktion. Ohne sie blockiert der Lizenzdialog den Start auf einem Server ohne Display.
- **P2 vor L2** die Zeitzone muss feststehen, bevor der erste Linux-Server Logs und Buchungen schreibt. Nachträglich umzustellen bedeutet gemischte Daten an der Tagesgrenze.
### Noch offene Punkte
1. **Accounting-Zeitzone getrennt von der Anwendungszeitzone?** ([3.3](#33-anforderung-konfigurierbare-zeitzone-entscheidung-6)) Empfehlung ja, verhindert rückwirkend verschobene Buchungsperioden bei einem Standortwechsel.
2. **Anzeige-Kultur ebenfalls bei der Installation festlegbar?** ([4.2](#42-ausgabeformatierung-offen)) naheliegend, da dieselbe Begründung wie bei der Zeitzone (Auslandsbetrieb).
3. **RocketChat und Telegram:** Wann sollen die echten `INotificationSink`-Implementierungen kommen? Bis dahin landen Auto-Pause- und Eskalationsmeldungen nur im Terminal-Log. Für den unbeaufsichtigten Linux-Betrieb (L2) wäre mindestens **ein** echter Kanal wichtig — sonst bemerkst du eine Auto-Pause erst beim nächsten Blick ins Log.
---
## 8. Risikoliste (Revision 3)
| # | Risiko | Status | Wirkung / Gegenmaßnahme |
|---|---|---|---|
| ~~R1~~ | Kulturabhängiger Parse, Faktor-100-Preisfehler | ✅ **behoben** | gefixt + 10 Regressionstests unter de-DE |
| ~~R4~~ | Lizenz-Hardware-ID ändert sich, in Containern instabil | ✅ **gelöst** | Deploymentcenter HWID v2 mit `DEPLOYMENTCENTER_HWID`-Override und v1→v2-Migration ohne Platzverlust |
| ~~R7~~ | libsodium 1.0.16 gegen moderne glibc | ✅ **entfallen** | mit dem Threema-Ausbau am 06.08.2026 |
| **R2** | Kein WYSIWYG-Designer; Layout-Regel ist neu | offen | Im Spike (P0) an einer echten View erproben, bevor 16 Views folgen |
| **R3** | `PropertyGrid`-Ersatz durch handgebaute Formulare | offen | Teuerster Posten (~10 PT). `[Description]`-Attribute liefern die Spezifikation |
| **R5** | Zeitzonen-Semantikwechsel trifft Logs, Tagesberichte, **Accounting-Perioden** | offen | P2 **vor** L2; `IAppClock`; Wirksamkeit erst nach Neustart |
| **R6** | Headless-Server ohne Display | offen | `ILicensePrompt`/`ConsoleLicensePrompt` aus dem Deploymentcenter-SDK; headless-Einstiegspunkt in L2 |
| **R8** | Anzeigeformate kippen auf Invariant (`LANG` nicht gesetzt) | offen | Kultur in `Program.Main` pinnen |
| **R9** | Keine CI Windows-Abhängigkeiten schleichen zurück | offen | Linux-Build in Gitea Actions ab L1 |
| **R10** | Mullvad-CLI meldet auf Linux anderen Statustext | offen | Gegen die echte CLI im Zielland verifizieren (5.4) |
| ~~R11~~ | Doppelpflege beider UIs | ✅ **entfällt** | Variante 2 gewählt (eingefrorener Stand) |
| ~~R12~~ | Benachrichtigungen fallen still | ✅ **entschärft** | `INotificationSink` eingezogen; Meldungen landen im Terminal-Log |
| **R13** | **Kein echter Benachrichtigungskanal** bis RocketChat/Telegram da sind | offen | Für den unbeaufsichtigten Betrieb (L2) mindestens einen Kanal implementieren — sonst bleibt eine Auto-Pause bis zum nächsten Log-Blick unbemerkt |
| **R14** | **Fallback-Stand nicht mehr baubar**, weil P1 vor dem Tag lief | offen | Tag `winforms-final` + archiviertes `dotnet publish` **vor** P1 (5.5) |
---
## 9. Was **nicht** angepasst werden muss
- **Der gesamte Trading-Pfad:** `PolymarketClobClient` (EIP-712 über Nethereum), `PolymarketApiService`, `PolymarketWssClient`, `AlchemyWssClient`, `CopyTradingEngine`, `SellLadderService`, `TraderMonitorService`
- **Persistenz:** EF Core 8 + Pomelo/MySQL, alle 15 Migrationen, `EncryptedStringConverter`
- **Sicherheit:** `SecretProtection` (AES-256-GCM läuft auf Linux über OpenSSL), Master-Key-Ableitung
- **Alle Analytics:** `TradeAnalytics`, `DossierBuilder`, Supervisor-Agent, OpenRouter-Client, MCP-Light-Server
- **Accounting-Logik:** Ingest, Klassifikation, BWA, CSV-Export (nur der PDF-Export braucht den FontResolver)
- **Watchdog-Payload-Schema** (`source`/`instance`/`interval` passen bereits zur Deploymentcenter-API), Modularitäts-Infrastruktur
- **438 von 448 Tests**
Das ist der Grund, warum die Schätzung trotz 24.000 LOC damals bei 4161 PT landete und nicht bei 150: **Zwei Drittel des Codes waren bereits plattformneutral, und die Trennung war sauber.** Rückblickend hat sich das bestätigt — die Portierung lief schneller als veranschlagt.
---
## 10. Anhang: verifizierte Fundstellen
### Behoben am 06.08.2026
| Thema | Datei:Zeile |
|---|---|
| Kulturabhängiger Preis-Parse (Faktor 100) | `…CopyTrading/Services/TraderMonitorService.cs:1276-1280` → nutzt jetzt `ParseDecimal` |
| `ParseDecimal` testbar + dokumentiert | `…CopyTrading/Services/TraderMonitorService.cs:1223-1234` |
| Kulturabhängige Parses (Regex-Gruppen) | `…Core/Services/PolymarketClobClient.cs:770, 784, 796, 820, 822, 827` |
| Kulturabhängiger Unix-Timestamp | `…CopyTrading/Services/MasterTraderAnalyticsJob.cs:140` |
| Neuer Regressionstest (10 Fälle, de-DE) | `tests/PolyTrader.Tests/ApiNumberParsingTests.cs` |
| Threema vollständig entfernt | `ThreemaService.cs`, `libs/Threema-MsgApi-Net-Core/` (35 Dateien), `ServerSettings` (7 Properties), `Program.cs`, `SettingsView.cs`, `PolyTrader.Core.csproj`, `PolyTraderSharp.sln` |
| Neutraler Benachrichtigungsausgang | `src/PolyTrader.Core/Notifications/INotificationSink.cs`, `LogNotificationSink.cs` |
| Fachdienste auf `INotificationSink` umgestellt | `MasterTraderAnalyticsJob.cs`, `SellLadderService.cs`, `DailyReportService.cs` |
| Testdouble mit Assertion-Möglichkeit | `tests/PolyTrader.Tests/Fakes/FakeNotificationSink.cs` |
### Offen
| Thema | Datei:Zeile |
|---|---|
| Windows-Zeitzonen-ID hartcodiert | `Ui/Views/TerminalView.cs:150` |
| Harter Windows-Pfad (Mullvad) | `src/PolyTrader.Core/Models/ServerSettings.cs:73` |
| Mullvad-Statusprüfung (Wortlaut) | `src/PolyTrader.Core/Services/MullvadVpnService.cs:HealthCheckAsync` |
| UI-Contract mit WinForms-Typen | `src/PolyTrader.Core/Modularity/ModuleUi.cs:32, 36` |
| Menü wird zur Laufzeit gebaut (neue Layout-Regel) | `src/PolyTrader.Core/Modularity/WindowMenu.cs:Populate`, `Ui/ShellUiHost.cs:AttachWindowMenu` |
| `System.Drawing.Color` in Modul-Logik | `…CopyTrading/Logic/TradeRowColoring.cs:11-13` |
| ScottPlot → `System.Drawing.Bitmap` | `Ui/Views/DashboardView.cs:207-227` |
| Windows-only Schrift im PDF | `…Accounting/Logic/PdfExporter.cs:34` |
| Schlüsseldateien ohne Rechteschutz | `Ui/Views/SettingsView.cs:301, 356`; `Program.cs:393` |
| Headless-Smoke-Test (erhaltenswert) | `Program.cs:500-570` |
| Lizenz-Gate mit modalem Dialog | `Licensing/LicenseGate.cs`, `Program.cs:98` |
| Secrets im Klartext | `server_settings.xml` (`WatchdogToken`, `LicenseKey`) |
### Referenzen
- `J:\Softwareprojekte\Deploymentcenter\docs\LICENSE_INTEGRATION_GUIDE.md` Hardware-ID v2, `LLS2`-Cache, CLI-Schalter, v1→v2-Migration
- `J:\Softwareprojekte\Deploymentcenter\docs\WATCHDOG_INTEGRATION_GUIDE.md` Ping-API, `X-Agent-Token`, Payload-Schema
- `J:\Softwareprojekte\Deploymentcenter\client-dotnet\Deploymentcenter.Client` SDK-Quellen (`LicenseClient`, `HardwareId`, `ILicensePrompt`, `StateStore`)
+61
View File
@@ -0,0 +1,61 @@
# Archiv
**Angelegt am 23.08.2026.** Hier liegen die Dokumente, aus denen
[ROADMAP.md](../ROADMAP.md) zusammengeführt wurde.
## Diese Dokumente sind nicht tot
Sie bleiben die **Bauanleitungen**. Wer ein Vorhaben aus der Roadmap umsetzt, findet hier die
Details, die in einer Roadmap keinen Platz haben: Code-Bezüge mit `datei:zeile`,
Akzeptanzkriterien je Phase, Risikotabellen mit Gegenmaßnahmen, Datenmodelle und vor allem die
**Begründungen** — warum eine Entscheidung so und nicht anders getroffen wurde.
Was hier **nicht** mehr gilt, ist der **Status**. Fortschrittsangaben, Häkchen und
„offen/erledigt"-Vermerke in diesen Dateien sind eingefroren. Maßgeblich ist allein die Roadmap.
> Das ist kein theoretisches Risiko: Bis zum 22.08.2026 wies der Modularisierungsplan die
> Phasen 36 als „IN ARBEIT" aus, obwohl die MySQL-Migration und die Modultrennung seit Wochen
> fertig waren. Genau deshalb gibt es jetzt nur noch **eine** Statusquelle.
## Inhalt
### `umsetzungsplaene/`
| Datei | Roadmap-Bezug |
|---|---|
| `UMSETZUNGSPLAN-Modularisierung.md` | ✅ abgeschlossen (Phasen 06) |
| `UMSETZUNGSPLAN-CopyTrading-Verbesserungen.md` | teils ✅, **Phase 1 ist B1** — der wichtigste offene Posten |
| `UMSETZUNGSPLAN-Fable-Review-Fixes.md` | ✅ abgeschlossen (Slices 06) |
| `UMSETZUNGSPLAN-Modul-ResolutionFarming.md` | Slices 05 ✅, Livegang ist **B3** |
| `UMSETZUNGSPLAN-Deploymentcenter-Integration.md` | D-0D-6 ✅ code-seitig, Live-Abnahme ist **A3** |
| `UMSETZUNGSPLAN-AutoRedeem.md` | **B4** |
| `UMSETZUNGSPLAN-Modul-MarketMaking.md` | **C1** |
| `UMSETZUNGSPLAN-Modul-BundleArbitrage.md` | **C2** |
| `UMSETZUNGSPLAN-StrategieDrift.md` | **C3** (zurückgestellt) |
| `UMSETZUNGSPLAN-AI-Aufloesequalitaet.md` | **C4** (zurückgestellt) |
| `UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration.md` | ⛔ **abgelöst** — nicht umsetzen |
### `konzepte/`
| Datei | Roadmap-Bezug |
|---|---|
| `KONZEPT-Modul-Accounting.md` | A-1/A-2/A-4 ✅, A-3 ist **B6**, A-5 ist **B7** |
| `KONZEPT-Modul-Supervisor.md` | S-0S-4 ✅, Live-Key-Test ist **B5** |
| `KONZEPT-Modul-DataDriven.md` | **D1** — nicht beschlossen, es gibt keinen Umsetzungsplan |
### Einzeldokument
`ANALYSE-Linux-Portierung.md` — Analyse und Fahrplan der Linux-Fähigkeit (P1P11, L1L5).
Fachlich weiterhin lesenswert: die Bestandsaufnahme, welcher Code portabel war und warum. Der
Fahrplan selbst ist abgearbeitet bis auf CI (**A2**) und systemd/Feldtest (**A4**).
## Ein Hinweis zu Dateiverweisen
Manche Dokumente verlinken auf Dateien, die es nicht mehr gibt — vor allem der abgelöste
Watchdog-Plan, der auf `Ui/`, `Program.cs` und `Licensing/` der WinForms-Anwendung zeigt. Diese
Verweise wurden entschärft (Klartext statt Link). Der zugehörige Code liegt im Git-Tag
`winforms-final`:
```bash
git worktree add ../polytrader-winforms winforms-final
```
@@ -0,0 +1,272 @@
# Konzept: Modul „Accounting" (Steuer-/Buchhaltungs-Reporting aller Live-Accounts)
> Stand: 2026-07-14
> Ziel: Vollständige, **unabhängig von unserer Trading-DB** erhobene, buchhalterisch korrekte
> Erfassung ALLER Transaktionen aller Live-Polymarket-Accounts. Periodische (meist monatliche),
> vor einer Steuerbehörde nachvollziehbare Abrechnungen — je einzelnem Account ODER über alle
> Accounts, für frei wählbare Zeiträume. BWA-artige Kennzahlen-Übersicht in der UI. Export als
> CSV und PDF.
> Reihenfolge: unabhängig, jederzeit baubar (kein Live-Trading nötig — rein lesende API-Abrufe).
> **Kein Handel; reines Ingest-/Reporting-Modul.**
>
> **Entscheidungen Richard (2026-07-14):**
> - Währung: **USDC nativ + USD + EUR** (jede Transaktion in allen drei ausgewiesen).
> - Abrechnung: neutrale prüfbare Aufstellung **UND** konkrete **US-Steuerberechnung** — Zielland
> **USA, Florida LLC** (Florida ohne State Income Tax → nur Federal). Als dokumentierte, vom CPA
> prüfbare Rechenschicht, **nicht als Steuerberatung** (Disclaimer, §4a).
> - Umfang v1: **inkl. On-Chain-Ein-/Auszahlungen** von Anfang an (vollständige Kapitalsicht).
---
## 0. Leitprinzipien
1. **Unabhängige Quelle = Polymarket, NICHT unsere DB.** Das Modul erhebt die Buchungsgrundlage
ausschließlich über eigene, regelmäßige Abrufe direkt bei Polymarket und speichert sie roh +
normalisiert in eigenen Tabellen. Unsere eigenen Trade-Logs (Copytrading/ResolutionFarming/Core)
werden **nur** für den optionalen Abgleich (§7) herangezogen, **nie** als Buchungsgrundlage.
2. **Nachvollziehbarkeit / Audit.** Jeder Buchungssatz führt auf einen konkreten, prüfbaren Nachweis
zurück (Transaktions-Hash, Activity-/Trade-ID, Abruf-Zeitpunkt, Rohdaten-Snapshot). Der Roh-Ingest
ist **unveränderlich (append-only)**; Abrechnungen sind daraus **reproduzierbar** ableitbar.
3. **Zwei Ebenen: neutraler Ledger + prüfbare US-Steuerschicht.** Unten liegt eine vollständige,
länderneutrale Transaktions-/Ledger-Aufstellung + Kennzahlen (belastbare Basis). Darauf setzt eine
**konfigurierbare, dokumentierte US-Steuer-Rechenschicht** (Florida LLC, §4a) — mit klaren, im
Export ausgewiesenen Annahmen, **die der US-CPA prüft/bestätigt**. Das Tool rechnet; es berät nicht
(Disclaimer, §4a). Der neutrale Ledger bleibt auch bei anderer steuerlicher Einordnung gültig.
4. **Lesend / idempotent.** Keine Orders, keine On-Chain-Writes. Überlappende Wiederholungs-Abrufe
dürfen **nichts doppelt buchen** (stabile Idempotenz-Schlüssel, Upsert statt Insert).
---
## 1. Architektur-Einbettung
Neues Projekt `src/PolyTrader.Modules.Accounting/` als `IPolyTraderModule`
(`Name = "Accounting"`, `DbPrefix = "acc_"`), Registrierung in `Program.cs`. Referenziert nur den
Core. Eigener `AccountingDbContext` (acc_-Tabellen), eigene UI (ein Fenster mit Tabs), eigene
Settings-Sektion.
- Nutzt den bestehenden `PolymarketApiService` (Data-API `/activity`, `/positions`,
`GetUsdcBalanceAsync(wallet)`) und für On-Chain-Ein-/Auszahlungen die vorhandene Alchemy-Anbindung.
- Betrifft ALLE Live-Accounts (aus `TradingState.Accounts` bzw. `IAccountRepository`; `!IsDemo`,
mit gesetzter `WalletAddress`).
- **Hinweis Ist-Stand:** `PolymarketApiService.GetTraderActivityAsync` filtert heute hart auf
`type=TRADE`. Fürs Accounting brauchen wir ALLE Activity-Typen → entweder den Filter parametrisieren
oder eine eigene, accounting-spezifische Abruf-Methode (schlanker, ohne Copytrading-Annahmen).
---
## 2. Datenbeschaffung (der Kern: der unabhängige Abruf)
### 2.1 Ledger-Quellen bei Polymarket
- **Data-API `/activity?user=<wallet>`** = primäres Kontobuch. Enthält (Feldnamen/Typen bei Umsetzung
zwingend aus https://docs.polymarket.com verifizieren) u. a.:
- `TRADE` (BUY/SELL-Fills): Preis, Size, USDC, Fee, Token/Market, txHash, Timestamp, Side.
- `REDEEM` (Resolution-Auszahlung: Gewinner-Shares → USDC).
- `SPLIT` / `MERGE` / `CONVERSION` (CTF-/NegRisk-Operationen; meist geldneutral, aber bestandsrelevant).
- `REWARD` (Liquidity Rewards) / Maker-Rebates — Einnahmen.
- **On-Chain USDC-Ein-/Auszahlungen** (Deposits/Withdrawals ins/aus dem Wallet): sind **nicht** Teil
der Trading-Activity. Quelle: Polygon/Alchemy (ERC-20-Transfer-Logs USDC ↔ Wallet).
- **Adresse ist vorhanden:** Polymarket nutzt **Gnosis-Safe-Proxy-Wallets** — genau die Adresse, die
wir bereits als `AccountState.WalletAddress` für Balance- und Activity-Abrufe verwenden. Sie ist
also die Watch-Adresse für USDC-Transfers (keine zusätzliche Beschaffung nötig).
- **Die eigentliche Arbeit ist die Klassifikation:** Die USDC-Transfers des Safes enthalten SOWOHL
trading-interne Bewegungen (Safe ↔ Polymarket-Contracts: CTF/Exchange/NegRisk — bereits in `/activity`
als Trades/Redeems erfasst) ALS AUCH echte Ein-/Auszahlungen (Safe ↔ EXTERNE Adressen). Nur letztere
sind Deposits/Withdrawals. Klassifikation über eine **Whitelist der Polymarket-System-Contracts**
(die CTF-Adresse `0x4D97…6045` liegt bereits im Code — Exchange/NegRisk-Adapter/USDC ergänzen, aus
docs verifizieren). Alternativ als Kreuzprobe: ΔSafe-Balance Σ(interne Activity) = externe Netto-Ein-/Auszahlung.
- ⚠️ Gnosis-Safe-Nuance: Transfers laufen ggf. als Safe-`execTransaction`; die ERC-20-Transfer-Logs
(from/to = Safe) bleiben aber die maßgebliche, prüfbare Quelle.
- **Balance-Anker**: `GetUsdcBalanceAsync(wallet)` je Abruf als Kontrollpunkt (End-Saldo Soll-Ist).
### 2.2 Vollständigkeit & Idempotenz
- **Backfill + Inkrementell**: Erstlauf lädt die volle Historie je Account (paginiert, `offset`/`limit`;
Rate-Limits beachten — Activity 1000/10 s, Positions 150/10 s, bereits im ApiService limitiert).
Danach nur neue Ereignisse ab dem letzten bekannten Zeitpunkt, mit Sicherheits-**Lookback-Überlappung**
gegen API-Lag.
- **Idempotenz-Schlüssel** je Buchungssatz: stabile Kombination `txHash + logIndex/assetId + type`
→ überlappende Abrufe buchen nicht doppelt (Unique-Constraint + Upsert).
- **Lückenerkennung**: Timestamp-Kontinuität + Seiten-Vollständigkeit prüfen; fehlende Bereiche gezielt
nachladen und markieren.
- **Rohdaten-Snapshot**: die JSON-Rohantwort je Abruf-Batch speichern (Nachweis + Reproduzierbarkeit),
zusätzlich zu den normalisierten Sätzen.
### 2.3 Abruf-Steuerung
- `AccountingIngestJob : BackgroundService` (JobManager-registriert, manuell triggerbar), Intervall
konfigurierbar (z. B. stündlich inkrementell, täglich Voll-Reconcile). Nutzt die bestehenden
Rate-Limiter des `PolymarketApiService`.
---
## 3. Persistenz (acc_-Tabellen, append-only Ledger)
| Tabelle | Inhalt |
|---|---|
| `acc_ledger` | Normalisierte, **unveränderliche** Buchungssätze: AccountId, EventType (TRADE_BUY/TRADE_SELL/REDEEM/REWARD/FEE/SPLIT/MERGE/DEPOSIT/WITHDRAWAL), Timestamp, TokenId/Market, Outcome, Size, PriceUsdc, GrossUsdc, FeeUsdc, NetUsdc, TxHash, LogIndex, Source, IngestBatchId, **IdempotencyKey (unique)** |
| `acc_ingest_runs` | Abruf-Protokoll: AccountId, Von/Bis, Start/Ende, #Sätze, Ok/Fehler, Balance-Anker (Soll-Ist) |
| `acc_raw` | Rohdaten-Snapshots (JSON) je Batch (Nachweis) |
| `acc_fx_rates` | (falls Fiat, §10.1) amtliche Tages-FX-Kurse (z. B. USD→EUR) je Datum |
| `acc_statements` (optional) | Erzeugte Periodenabrechnungen (Metadaten + Daten-Hash) für Versionierung/Reproduzierbarkeit |
Autoincrement-PKs (Lehre aus dem CopyTrading-`TradeId`-Problem), Unique-Index auf `IdempotencyKey`,
Indizes auf `(AccountId, Timestamp)`.
---
## 4. Buchhaltungs-Logik (pure, testbar) — `AccountingEngine`
Wie bei den anderen Modulen liegt die geldkritische Logik pur und unit-getestet in `Logic/`:
- **Klassifikation** roher Activity → Buchungssatz-Typ + Vorzeichen (Einnahme/Ausgabe). Rein/testbar.
- **Periodenabrechnung** (Zeitraum × Account bzw. alle): aggregiert die Ledger-Sätze zu:
Anfangssaldo, Einlagen, Entnahmen, Handelsvolumen, realisierte Gewinne/Verluste, Fees, Rewards,
Netto-Ergebnis, Endsaldo — mit **Soll-Ist gegen den Balance-Anker** (Abweichung = Vollständigkeits-Signal).
- **Realisierung / Kostenbasis**: Gewinn/Verlust wird bei SELL/REDEEM realisiert; Kostenbasis je
Position per **FIFO** (US-Norm für Krypto/Property; Specific-ID als Option, §10.5) — Lot-Matching der
BUYs zu jeder Veräußerung, inkl. Haltefristen (short/long term). Pure/testbar. Offene Positionen zum
Periodenende optional als **Mark-to-Market** ausweisen, aber getrennt vom realisierten Ergebnis.
- **Währung/FX**: jede Transaktion in **USDC (nativ), USD und EUR**. Umrechnung je Transaktionsdatum:
USDC→USD (Stablecoin, Annahme 1:1 als USD-Äquivalent — im Export dokumentiert; realer USDC/USD-Kurs
optional) und USD→EUR über amtliche Tageskurse (EZB), versioniert in `acc_fx_rates`.
---
## 4a. US-Steuer-Rechenschicht (Florida LLC) — konfigurierbar, CPA-prüfbar
> ⚠️ **Kein Steuerrat.** Diese Schicht rechnet Zahlen nach **dokumentierten, konfigurierbaren
> Annahmen** aus und weist diese im Export offen aus. Die steuerliche Einordnung von
> Prediction-Market-/Event-Contract-Erträgen in den USA ist **nicht eindeutig geklärt**. Der Output
> ist zur **Prüfung/Bestätigung durch einen US-CPA/Steuerberater** gedacht, nicht als endgültige
> Steuererklärung. Jede Annahme ist im PDF/CSV dokumentiert und in den Settings umstellbar.
**Rahmen (Defaults, alle in Settings umstellbar):**
- **Entität:** Florida LLC. Florida erhebt **keine State Income Tax** → nur **Federal**. LLC-Typ
(single-member = disregarded → Schedule C/D im 1040; vs. multi-member = Partnership 1065 + K-1)
bestimmt nur die Ziel-Formulare, **nicht** die Gewinnermittlung (§10.7).
- **Einordnung (Default-Annahme):** jede Position = **Property-Disposition** → realisierter Gewinn/
Verlust je Veräußerung (SELL/REDEEM) im **Form-8949/Schedule-D-Stil**: Proceeds (USD), Cost Basis
(USD), Holding Period (short/long), Gain/Loss. Alternative Einordnungen (ordinary income, gambling,
§1256) als umstellbare Annahme vorgesehen — Auswahl trifft der CPA (§10.4).
- **USDC:** als **USD-Äquivalent (1:1)** behandelt; USDC-Käufe/-Verkäufe gelten nicht separat als
steuerbares Krypto-Event (dokumentierte Vereinfachung; abschaltbar).
- **Kostenbasis:** **FIFO** (Default) oder Specific-ID; Lot-genau, mit Haltefristen.
- **Wash-Sale:** Default-Annahme „nicht anwendbar" auf Event-Contracts/Krypto (Stand 2026) — als
Schalter, da Rechtslage im Fluss.
**Output der Steuerschicht:**
- **Form-8949-artige Veräußerungsliste** je Account/Zeitraum (Description, Acquired, Sold, Proceeds,
Cost Basis, Gain/Loss, Short/Long) — CSV + PDF.
- **Schedule-D-artige Zusammenfassung** (kurz-/langfristig, Summen) je Account und aggregiert über die LLC.
- Vollständige **Methodik-/Annahmen-Seite** im Export (Reproduzierbarkeit + Prüfbarkeit).
Die Rechenlogik (FIFO-Lot-Matching, Haltefrist, Gain/Loss, FX-Umrechnung je Datum) liegt **pur und
unit-getestet** in `Logic/` (z. B. `UsTaxEngine`), getrennt vom neutralen Ledger.
---
## 5. UI (WinForms, ein Fenster mit Tabs — Muster wie ResolutionFarming)
- **Übersicht / BWA**: Kennzahlen-Kacheln für den gewählten Zeitraum + Account(s): Netto-Ergebnis,
Handelsvolumen, Fees, Rewards, Einlagen/Entnahmen, Endsaldo, realisiert vs. offen; Monats-/Perioden-Vergleich.
- **Ledger**: filterbare Transaktionsliste inkl. Nachweisspalten (txHash etc.).
- **Steuer (US)**: Form-8949-artige Veräußerungsliste + Schedule-D-Zusammenfassung je Account/alle,
mit ausgewiesenen Annahmen (FIFO, Einordnung, USDC=USD). Werte in USD (und EUR).
- **Abrechnungen / Export**: Zeitraum-Picker (Monats-Presets + frei), Account-Auswahl (einzeln / alle),
Währungswahl (USDC/USD/EUR), Buttons „CSV" und „PDF".
- **Abruf / Status**: Ingest-Läufe, Vollständigkeits-/Lücken-Status, Balance-Soll-Ist, manueller Trigger.
---
## 6. Export (CSV + PDF)
- **CSV**: vollständiger Ledger + Aggregat je Abrechnung (maschinen-/prüfbar).
- **PDF**: formatierte Abrechnung — Kopf (Account, Wallet, Zeitraum, Erstellungsdatum), Aggregat-Tabelle,
Transaktionsliste, Methodik-/Nachweis-Hinweis. Library **PDFsharp/MigraDoc (MIT)** — echte,
bedingungslose MIT-Lizenz ohne Umsatzschwelle; MigraDoc eignet sich für tabellarische
Abrechnungen/Berichte. (QuestPDF bewusst NICHT: dessen „Community"-Lizenz ist kostenlos nur unter
1 Mio USD Jahresumsatz — für eine handelnde LLC ein Lizenzrisiko. Siehe §10.1.)
- **Reproduzierbarkeit**: jeder Export trägt Zeitraum, Datenstand (letzter Ingest), Zeilenzahl und einen
Daten-Hash → gleiche Eingabe ⇒ identische Abrechnung (wichtig für die Prüfbarkeit).
---
## 7. Abgleich mit eigener Trade-DB (niedrige Priorität)
Gegenüberstellung Polymarket-Ledger ↔ unsere Modul-/Core-Trade-Logs je Account/Zeitraum: fehlende/
zusätzliche Trades, Preis-/Size-/Fee-Abweichungen, PnL-Differenzen. Rein analytisch — findet
Sync-/Buchungsfehler unserer Trading-Seite. Bericht in der UI + Export.
---
## 8. Phasen & Akzeptanzkriterien
- **A-1 Ingest (read-only), inkl. Ein-/Auszahlungen:** Modul + acc_-Persistenz + Activity-Ingest
(ALLE Typen) + On-Chain-USDC-Deposits/Withdrawals + Backfill/Inkrement + Idempotenz + Ingest-Status-UI.
**Akzeptanz:** volle Historie eines Live-Accounts vollständig & doppelfrei; Balance-Anker Soll-Ist ≈ 0
(inkl. Ein-/Auszahlungen).
**✅ UMGESETZT (2026-07-20, Commit folgt):** `PolyTrader.Modules.Accounting` (acc_-Präfix). Pure
`AccountingClassifier` (Typ/Vorzeichen/Idempotenz-Key; intern↔extern-Transfer-Trennung via
System-Contract-Whitelist). `AccountingDbContext` (acc_ledger append-only + Unique-Idempotency,
acc_ingest_runs, acc_raw; Migration `InitialAccounting` angewendet). `AccountingIngestService`
(BackgroundService, testbarer `IngestAccountAsync`: idempotenter Upsert + Balance-Anker-Δ +
Backfill/Inkrement mit Lookback). Quellen hinter Interfaces (`IActivitySource`/`ITransferSource`/
`IBalanceAnchorSource`) mit **Null-Stubs** — offline lauffähig; **Live-Abruf (Polymarket /activity
ALLE Typen, Alchemy-USDC-Transfers, GetUsdcBalanceAsync) + System-Contract-Whitelist ist Zielland-
Arbeit.** UI (designerfähig): Tabs Ledger + Abruf/Status mit manuellem Backfill/Inkrement. 12 Tests.
- **A-2 Abrechnung + Übersicht + FX:** `AccountingEngine` + BWA-UI + Periodenabrechnung je Account/alle,
Werte in USDC/USD/EUR (Tageskurse in `acc_fx_rates`). **Akzeptanz:** Monatsabrechnung stimmt gegen
Balance-Anker; Kennzahlen plausibel; FX nachvollziehbar.
**✅ UMGESETZT (2026-07-20):** `AccountingEngine` (pur): `BuildStatement` (Anfangs-/Endsaldo,
Ein-/Auszahlungen, Handelsvolumen, Redeems, Rewards, Fees, Netto-Handelsergebnis Cash-Basis exkl.
Ein-/Auszahlungen; Invariante EndsaldoAnfang = Ergebnis+Einz.Ausz.) + `BuildMonthlyBreakdown`
(verkettete Monats-Anfangssalden). `FxConverter` (pur, USDC≈USD-1:1-Annahme dokumentiert; USD→EUR
über `acc_fx_rates`, Nearest-on-or-before). `CsvExporter` (pur, RFC-4180, kulturinvariant).
`AccountingReportService` (Abrechnung + Währungs-View USDC/USD/EUR). UI-Tab „Übersicht/BWA"
(designerfähig): KPI-Kacheln + Monatsvergleich + Zeitraum-/Konto-/Währungswahl + CSV-Export. +6 Tests.
**Offen für Zielland:** EZB-Kurs-Ingest (acc_fx_rates füllen) → dann ist EUR verfügbar (USDC/USD sofort).
- **A-3 US-Steuerschicht:** `UsTaxEngine` (FIFO-Lot-Matching, Haltefristen, Gain/Loss) + Form-8949-/
Schedule-D-Ansicht + Annahmen-Dokumentation. **Akzeptanz:** Summe realisierter Gain/Loss stimmt gegen
die neutrale Abrechnung; Annahmen ausgewiesen.
- **A-4 Export:** CSV + PDF (Abrechnung, Ledger, Form-8949/Schedule-D) mit Zeitraum-/Account-/Währungswahl.
**Akzeptanz:** vollständig, reproduzierbar, prüfbar.
- **A-5 Reconciliation (niedrig):** Abgleich mit eigener DB.
Die geldkritische Logik (Klassifikation, Aggregation, Realisierung, FX) ist von Anfang an pur +
unit-getestet; Ingest/Export werden über Interfaces (Activity-Quelle, PDF-Writer) testbar gehalten —
wie bei ResolutionFarming die Live-Anbindung hinter Interfaces liegt.
---
## 9. Risiken & Gegenmaßnahmen
| Risiko | Gegenmaßnahme |
|---|---|
| Unvollständige API-Historie / Pagination-Lücken | Balance-Anker-Soll-Ist als Vollständigkeits-Wächter; Lückenerkennung + Nachlad; Rohdaten-Snapshots |
| API-Feld-/Endpoint-Änderungen | Rohdaten speichern; Normalisierung entkoppelt; Feldnamen bei Umsetzung aus docs verifizieren |
| Doppelbuchung bei überlappenden Abrufen | strikte Idempotenz-Schlüssel + Upsert |
| Steuerliche Fehlinterpretation | bewusst neutrale, vollständige Aufstellung statt Steuerberechnung; Methodik im Export dokumentiert |
| FX-Korrektheit | amtliche Tageskurse (z. B. EZB) versioniert in `acc_fx_rates` |
| Falsche Wallet-Adresse (Proxy vs. Signer) für Deposits | Adress-/Transfer-Semantik je Account verifizieren |
---
## 10. Entscheidungen & offene Punkte
**Entschieden (2026-07-14):**
-**Währung:** USDC nativ + USD + EUR (Tageskurse EZB, USDC≈USD dokumentiert).
-**Abrechnung:** neutraler Ledger **+** US-Steuerschicht (Florida LLC), als CPA-prüfbare Rechenschicht.
-**Umfang v1:** inkl. On-Chain-Ein-/Auszahlungen.
-**Kostenbasis:** FIFO (US-Norm), Specific-ID als Option.
**Noch offen / vom CPA zu bestätigen:**
1.**PDF-Library: PDFsharp/MigraDoc (MIT)** — nach Rückfrage bewusst statt QuestPDF: QuestPDFs
„Community MIT"-Lizenz ist nur unter 1 Mio USD Jahresumsatz kostenlos (darüber Professional/
Enterprise kostenpflichtig) → für eine handelnde LLC ein Lizenzrisiko. PDFsharp/MigraDoc ist echte
MIT ohne Schwelle.
2. **Steuerliche Einordnung (Default-Annahme):** Property-Disposition (Form 8949/Schedule D, Default) vs.
ordinary income vs. gambling vs. §1256 — vom US-CPA bestätigen lassen; im Tool umstellbar.
3. **LLC-Typ:** single-member (disregarded) vs. multi-member (Partnership 1065/K-1) — bestimmt nur die
Ziel-Formulare/Darstellung, nicht die Gewinnermittlung.
4. **USDC=USD-Annahme & Wash-Sale-Schalter:** Defaults gesetzt (1:1; Wash-Sale n/a) — vom CPA bestätigen.
5.**Proxy-Wallets: geklärt.** Der Gnosis-Safe (= vorhandene `WalletAddress`) ist die Watch-Adresse;
zu bauen ist die **Transfer-Klassifikation** intern (Polymarket-Contracts) vs. extern (Deposit/Withdrawal)
über eine System-Contract-Whitelist (CTF-Adresse bereits im Code; Exchange/NegRisk/USDC ergänzen).
6. **FX-Quelle & USDC/USD:** EZB-Tageskurse für USD→EUR; USDC→USD als 1:1 (Default) oder realer Kurs?
@@ -0,0 +1,129 @@
# Konzept: Eigenes datengetriebenes Trading-Modul („DataDriven")
> Stand: 2026-07-11
> Status: KONZEPT (noch kein Umsetzungsplan). Ziel: ein Strategiemodul, das eigene
> Handelsentscheidungen aus externen Datenquellen ableitet — je Marktkategorie eine
> eigene Datenquelle + ein eigenes Fair-Value-Modell. Die Datenanbindung wird so
> gebaut, dass das ResolutionFarming sie mitnutzen kann (State-aware-Filter, C-S2).
---
## 1. Grundidee
Alle bisherigen Module leiten Signale von ANDEREN ab (Master-Trades, Marktpreise).
DataDriven dreht das um: **Wir berechnen aus Rohdaten eine eigene faire
Wahrscheinlichkeit** und handeln nur, wenn der Marktpreis deutlich davon abweicht:
```
FairValue(Markt) aus Datenquelle → Vergleich mit Marktpreis (Bid/Ask)
→ |FairValue Preis| > MinEdge (nach Fees) → Order (Maker bevorzugt) → Halten bis Resolution/Ziel
```
Der Edge kommt nicht aus Geschwindigkeit, sondern daraus, die **Daten besser/
konsequenter auszuwerten als der Durchschnitts-Teilnehmer** der jeweiligen Kategorie.
Deshalb ist die Kategorien-Wahl die wichtigste Entscheidung dieses Moduls.
## 2. Architektur (fügt sich in die bestehende Modul-Landschaft)
```
PolyTrader.Modules.DataDriven (IPolyTraderModule, DbPrefix dd_)
├── Core-Beitrag (geteilt, auch für ResolutionFarming nutzbar):
│ IMarketStateProvider // je Kategorie: liefert konservative
│ { // Wahrscheinlichkeits-Schätzung + Zustand
│ bool Supports(MarketData m);
│ Task<MarketStateEstimate?> EstimateAsync(MarketData m, CancellationToken ct);
│ }
│ record MarketStateEstimate(decimal ProbLowerBound, decimal ProbUpperBound,
│ string StateSummary, DateTime AsOf, string Source);
├── Provider (je Kategorie ein Adapter, einzeln aktivierbar):
│ WeatherStateProvider // Open-Meteo/NOAA-Modelläufe
│ SportsScoreStateProvider // Live-Scores (z. B. API-Football)
│ CryptoStrikeStateProvider // Binance-Spot + realisierte Volatilität
│ MacroStateProvider // Nowcasts/Konsensdaten (CPI, Zinsen)
├── Services:
│ DdScannerService // Märkte je aktiver Kategorie laden, FairValue
│ // berechnen, Kandidaten mit Edge persistieren
│ DdExecutionService // Sizing/Limits (Muster: FarmingRiskEngine/
│ // Planner wiederverwenden!), Maker-first
│ DdPositionMonitorService // Re-Evaluation offener Positionen: dreht der
│ // FairValue, wird der Exit geprüft (Leiter-Muster)
└── Persistenz: dd_settings / dd_candidates / dd_positions / dd_closed_trades
(Kopie des bewährten rf_-Schemas; Autoincrement-IDs, Kalibrierungs-Historie)
```
**Wichtige Wiederverwendung:** Risk-Engine, Execution-Planner, Fill-Modell,
Resolution-Monitor und UI-Aufbau des ResolutionFarming sind fast 1:1 übertragbar —
DataDriven ist strukturell „ResolutionFarming mit eigener Signalquelle statt
Preisband-Scan". Der Unterschied: DataDriven darf auch UNTER 0.90 kaufen (überall,
wo FairValue ≫ Preis) und optional vor der Resolution verkaufen, wenn der Edge
realisiert ist (Preis hat FairValue erreicht → Kapitalumschlag).
**ResolutionFarming-Synergie (C-S2):** Sobald ein `IMarketStateProvider` für eine
Kategorie existiert, nutzt ihn auch der RF-Scanner: Ein Preis-Dip wird nicht mehr
pauschal gemieden (Momentum-Filter), sondern gegen `ProbLowerBound` geprüft —
Richards 5:1-in-Minute-80-Beispiel wird damit zur Kaufgelegenheit statt zum Reject.
## 3. Kategorien-Bewertung: Wo lohnt sich ein eigenes Modell?
Bewertung nach: Datenlage (frei/billig verfügbar?), Modell-Komplexität,
Bot-Konkurrenz (Stand 2026), Fee-Satz, Kapitalbindung.
| Kategorie | Datenquelle (Kosten) | Modell | Bot-Konkurrenz | Fee | Bindung | Urteil |
|---|---|---|---|---|---|---|
| **Wetter (Tages-Märkte: Temperatur/Niederschlag je Stadt)** | Open-Meteo/NOAA/ECMWF-Läufe (kostenlos) | Modell-Konsens vs. Marktpreis; Update-Lag nutzen | **moderat, wachsend** — Edges von ~10 Pp (2023) auf ~3 Pp (2026) komprimiert, aber vorhanden; dünne Bücher, wenig Retail | 1,25 % | StundenTage | ✅ **Bester Einstieg** |
| **Wetter (Saison: Hurrikane, Rekorde)** | wie oben + NHC | aufwendiger | gering (Kapital-Lockup schreckt ab) | 1,25 % | WochenMonate | ⚠️ später (Bindung) |
| **Sport pre-game (kleinere Ligen)** | API-Football o. ä. (~2030 $/Mon.) | Elo-/Quotenvergleich vs. Buchmacher-Konsens | groß in Top-Ligen, **moderat in Nebenligen** | 0,75 % | StundenTage | ✅ zweiter Kandidat |
| **Sport live (State-Provider)** | wie oben, Live-Scores | Score+Restzeit → P(Sieg), konservative Untergrenze | hoch (Latenz-Bots) — aber wir brauchen nur EINEN Fill unter FairValue, nicht den schnellsten | 0,75 % | MinutenStunden | ✅ als RF-Filter; als eigene Strategie nur eng begrenzt |
| **Krypto-Strikes (Wochen/Monat: „BTC über X am Y")** | Binance-WSS (kostenlos) | Distanz zum Strike + realisierte Vol → P | hoch bei 15-min/Stunde, **moderat bei Wochen-Strikes** | 1,8 % ⚠️ | TageWochen | ⚠️ Fee frisst viel; nur bei großem Modell-Edge |
| **Makro (CPI, Zinsentscheide)** | Cleveland-Fed-Nowcast, Konsens-Schätzungen (frei) | Nowcast vs. Marktpreis | moderat, aber informierte Gegenseite | 1,5 % | TageWochen | ⚠️ Nische, wenige Märkte |
| **Politik-Longtail (Nicht-Headline)** | Polls/Aggregatoren | Poll-Modell | gering im Long Tail | 1,0 % | Wochen+ | ⚠️ Bindung + Resolution-Risiko |
| **Kultur/Awards/Mentions** | Box-Office-Daten teils frei; sonst dünn | schwach | **gering** | 1,251,56 % | variabel | ❌ Resolution-Risiko (AI-Rater Pflicht), Datenlage schlecht |
| Krypto 15-min/1h Up-Down | Binance | Latenz | **extrem** (Sub-100-ms-Bots) | 1,8 % | Minuten | ❌ nicht unser Spiel |
**Antwort auf „nicht geflutete Kategorien":** Am wenigsten Bot-dominiert sind 2026
(a) **Wetter** — dünne Bücher, Nischenwissen (Stations-Regeln, Modell-Läufe), Retail
meidet die Kategorie; (b) **Long-Tail-/Nebenliga-Sport pre-game**; (c) **Makro-
Nischen** und (d) Long-Tail-Politik. Geflutet sind: Krypto-Kurzfrist, Top-Sport
in-play, Headline-Elections, Arbitrage/NegRisk-Rebalancing. Faustregel: Bots meiden
Kapitalbindung und Nischenwissen — genau dort liegt unser Fenster.
## 4. Empfohlener Aufbau-Pfad
1. **Phase DD-0:** `IMarketStateProvider`-Contract in Core + Modul-Skelett
(rf_-Schema kopieren). Kein Provider aktiv.
2. **Phase DD-1 (Wetter, read-only):** WeatherStateProvider (Open-Meteo, tägliche
Temperatur-Märkte 23 US-Städte). Scanner läuft wochenlang read-only:
FairValue vs. Marktpreis loggen → **misst den real verbliebenen Edge, bevor
irgendetwas gehandelt wird** (dasselbe Kalibrierungs-Gate-Prinzip wie RF).
3. **Phase DD-2:** Demo-Execution (Planner/Fill-Modell aus RF), 4 Wochen.
4. **Phase DD-3:** SportsScoreStateProvider — zuerst NUR als RF-Filter (C-S2:
Dip-Freigabe bei klarer Führung), erst danach als eigene DD-Strategie.
5. **Phase DD-4:** Live klein (eigener Account, wie bei RF), dann weitere Provider
nach gemessenem Edge.
## 5. Risiken dieses Moduls (ehrlich)
1. **Modell-Risiko ersetzt Master-Risiko:** Ein Bug/Bias im Fair-Value-Modell
produziert systematisch falsche Trades. Gegenmittel: read-only-Messphase je
Provider (DD-1-Prinzip), konservative Untergrenzen statt Punktschätzern.
2. **Edge-Kompression:** Der Wetter-Edge ist dokumentiert am Schrumpfen (10→3 Pp).
Read-only-Messung VOR jedem Livegang, und die Bereitschaft, eine Kategorie
wieder abzuschalten, wenn die Messung < MinEdge zeigt.
3. **Regel-Fallen:** Wetter-Märkte lösen nach exakten Stations-Regeln auf — der
AI-Auflösequalitäts-Rater (separater Plan) und das genaue Lesen der Regeln
je Markt-Serie sind Pflicht (welche Station, welche Rundung, welcher Zeitraum).
4. **Aufwand:** Jede Kategorie ist ein eigenes kleines Forschungsprojekt. Deshalb:
strikt eine Kategorie nach der anderen, jede mit eigenem Go/No-Go-Gate.
## 6. Quellen (Kategorien-/Konkurrenz-Einschätzung)
- Polymarket Weather/Climate-Kategorieübersichten (Volumen/Marktzahl):
https://polymarket.com/predictions/weather, https://polymarket.com/predictions/climate
- Weather-Bot-Funktionsweise & Edge-Kompression 2023→2026:
https://laikalabs.ai/prediction-markets/polymarket-weather-trading-bot
- Markt-Mikrostruktur/Tiefe Wetter-Märkte: https://polymart.app/blog/polymarket-weather-markets,
https://polymarkets.co.il/en/guide/weather-guide/
@@ -0,0 +1,206 @@
# Konzept: Modul „Supervisor" (KI-gestützte Handels-Analyse & Forensik)
> Stand: 2026-07-16
> Ziel: ALLES, was PolyTrader getan (und bewusst NICHT getan) hat, auf einfachem Wege detailliert
> analysierbar machen — Logs, DB-Einträge und das echte Geschehen auf der Plattform — 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. Die Analyse ist maximal so gut wie die Rekonstruierbarkeit unserer Entscheidungen.
---
## 0. Datenlage-Audit: Was haben wir, was fehlt?
### Vorhanden ✅
| Quelle | Inhalt | Qualität für Analyse |
|---|---|---|
| `core_trade_log` (TradeRecord) | abgeschlossene Trades aller Module | gut (strukturiert), aber nur das ERGEBNIS |
| Modul-Logs (`mod_copytrading_closed_trades`, `rf_*`) | Modul-Details inkl. Fees, SourceTrader, Cluster | gut |
| Terminal-/Datei-Logs (`/Logs`, Freitext) | Verlauf inkl. `TradeReasoning` (Begründungen) | schlecht maschinenlesbar: Freitext, deutsch, ohne IDs |
| `rf_candidates` | Scanner-Entscheidungen inkl. Reject-Grund | **Vorbild!** genau das Muster, das wir überall brauchen |
| geplant: `acc_ledger` (Accounting-Modul) | unabhängige Plattform-Ground-Truth je Wallet | schließt „echtes Geschehen auf der Plattform" |
### Fehlend ❌ (die eigentlichen Lücken)
1. **Entscheidungsjournal** — der größte Gap. Jede Engine-Entscheidung (BUY/SELL ausgeführt, abgelehnt,
übersprungen) existiert nur als Freitext-Log. Nicht abfragbar („zeig alle MaxBuyPrice-Rejects der
Woche"), nicht mit dem späteren Marktausgang verknüpfbar („was WÄRE aus den Rejects geworden?" —
das ist die halbe Strategie-Kalibrierung!).
2. **Order-Lifecycle nicht persistiert** — Platzierungsversuche, CLOB-Responses, Cancels, Leiter-Stufen
(Preis je Stufe, Wartezeiten, Floor-Erreichung) stehen nur im Log. Für „warum schlechter Fill?"
brauchen wir die Kette als Daten.
3. **Korrelation** — es gibt keine `SignalId`, die Signal → Entscheidung(en) → Order(s) → ClosedTrade
verbindet. Ohne sie ist jedes „Dossier" Handarbeit über Zeitstempel.
4. **Markt-Kontext zum Entscheidungszeitpunkt** — mindestens Signalpreis vs. erzielter Preis vs.
Zeitversatz (Latenz!); mit Phase 1 (Orderbuch) auch Spread/Tiefe. Ohne Kontext kann niemand
beurteilen, ob eine Entscheidung RICHTIG war — nur ob sie gut AUSGING.
5. **Strukturierte Logs** — zusätzlich zum Text ein JSONL-Sink (Timestamp, Level, Source, CorrelationId,
Message, Data), damit Logs filterbar/parsebar sind statt grep-über-Freitext.
---
## 1. Frage 1 — Aufbereitung: das „Trade-Dossier" als zentrale Einheit
**S-0: Datenfundament (Core, nützt sofort auch OHNE KI — z. B. beim Zielland-Debugging):**
- `core_decision_journal`: eine Zeile je Entscheidung. Felder: `SignalId`, Timestamp, Modul, AccountId,
TokenId, Side, Decision (Executed/Rejected/Skipped/Deferred), **ReasonCode (Enum!)** (z. B.
`MaxBuyPriceExceeded`, `PartialSellBelowThreshold`, `ExitPendingSkip`, `SpamBlock`, `BudgetExhausted`,
`BelowPolymarketMinimum`, …), Kontext-Zahlen (SignalPreis, Limitwert, verfügbares Budget, …) als
kompaktes JSON, Freitext wie bisher zusätzlich.
→ Die bestehenden ~20 `TradeReasoning`-Stellen bleiben, schreiben aber ZUSÄTZLICH strukturiert.
- `core_order_events`: Order-Lifecycle (Placed/Rejected/Cancelled/LadderStep/Filled) mit Preisen,
CLOB-Response, `SignalId`.
- `SignalId` (GUID) im `CopySignal`/RF-Flow erzeugen und bis in `ClosedTrade`/`TradeRecord` durchreichen.
- **JSONL-Log-Sink** (TerminalLogger erweitert): eine JSON-Zeile je Event (`ts`, `level`, `source`,
`correlationId`, `message`, optional `data`). JSONL statt JSON-Array: append-fähig, streambar,
zeilenweise filterbar — das KI-freundliche UND effiziente Format. Übergang: zunächst Dual-Sink
(Text + JSONL), Text-Sink später abschaltbar, sobald der Log Viewer etabliert ist.
- **Log Viewer im Terminal-Fenster** (zweiter Tab neben der Live-Anzeige): lädt die JSONL-Dateien und
bereitet sie menschenlesbar auf — Filter nach Datum/Level/Quelle/Text und **CorrelationId
(„zeig mir alles zu diesem Signal")**; Klick auf eine SignalId springt zur kompletten Kette. Damit
ist das effiziente Speicherformat für Menschen genauso zugänglich wie heute der Freitext.
**S-1: Dossier-Generator (pur, testbar):** Für einen Trade / ein abgelehntes Signal alles zusammensetzen:
Signal → Journal-Einträge → Order-Events → Fill/Leiter-Verlauf → ClosedTrade/RF-Position →
`acc_ledger`-Einträge (Plattform-Wahrheit!) → Log-Ausschnitt (Zeitfenster+CorrelationId) → Marktdaten.
Ausgabe als JSON (fürs Modell) und Markdown (für Menschen). Dazu Perioden-Reports über `TradeAnalytics`.
---
## 2. Frage 2 — WIE analysieren: OpenRouter-Agent mit Read-only-Tool-Registry
**v1: Agent IM Prozess (kein separater MCP-Server nötig).** Wir kontrollieren beide Seiten — ein
Function-Calling-Loop gegen OpenRouter (Chat Completions + Tools) ist in C# klein und ohne neue
Abhängigkeit machbar (HttpClient + System.Text.Json). Der Agent bekommt:
1. **System-Kontext:** ein kuratiertes, versioniertes Dokument `docs/analyse/ARCHITEKTUR-KONTEXT.md`
(„So funktioniert PolyTrader": Module, Entscheidungswege, Reason-Codes, Limits, Leiter-Mechanik,
bekannte Eigenheiten). KEIN Code-Dump — destilliertes Verhalten. Wird bei Änderungen am Geld-Pfad
mitgepflegt (Checkliste).
2. **Read-only-Tools** (Registry im Supervisor-Modul):
- `query_trades(filter)` — Core-Trade-Log
- `query_decisions(filter)` — Entscheidungsjournal (inkl. Rejects!)
- `get_dossier(signalId | tokenId+account)` — das komplette Dossier
- `read_logs(zeitraum, level, correlationId, textfilter)` — JSONL-Logs
- `get_ledger(account, zeitraum)` + `get_ledger_diff(...)` — Plattform vs. eigene DB (Accounting)
- `get_kpis(scope)` — TradeAnalytics
- `get_architecture_context()` — das Kontext-Dokument
- **Predictalytics-Tools** (`query_predictalytics_*`): historische Trade-Daten fremder Trader,
Master-Historien, Markt-Statistiken aus unserem Predictalytics-Tool (dessen API wir ohnehin
integrieren). Analytisch besonders wertvoll: **„schlechtes Signal" von „schlechter Ausführung"
trennen** — z. B. Master-Fill vs. unser Fill (Latenzkosten) oder unser Ergebnis vs. das anderer
Trader im selben Markt (Benchmark). Architektur: `IPredictalyticsClient` im **Core** (auch
Trading-Module nutzen ihn später, z. B. Master-Auswahl/AI-Rating); der Supervisor konsumiert ihn
nur read-only. Eigener Egress-Eintrag (unsere eigene API, aber dokumentiert).
**Hart: KEIN Tool kann handeln, canceln oder schreiben.** Der Supervisor ist Beobachter.
**Analyse-Modi:**
- **Einzeltrade-Forensik:** „Erkläre Trade X" → Dossier → Modell begründet mit Daten.
- **Batch-/Muster-Analyse:** „Alle Verlierer der letzten 14 Tage → gemeinsame Faktoren?" (Kategorie?
Uhrzeit? Master? Preisband? Latenz? Leiter-Floor-Fälle?)
- **Counterfactual:** „Was wurde abgelehnt und wie ist der Markt ausgegangen?" (Journal × Resolution).
- **Reconciliation-Anomalien:** DB ↔ Plattform-Differenzen erklären lassen.
- **Täglicher Supervisor-Bericht** (später): Kurzfassung via Threema.
**MCP-Server-Light: ja, aber als Phase S-4.** Dieselbe Tool-Registry zusätzlich über einen lokalen
MCP-Endpoint exponieren → dann können auch externe Clients (Claude Code/Desktop) direkt gegen die
laufende App analysieren. Architektonisch nur ein zweiter Transport über dieselben Tools — deshalb
lohnt es, die Registry von Anfang an transport-agnostisch zu bauen.
---
## 3. Frage 3 — Core- oder Modulebene? Hybrid.
- **Datenfundament = Core.** Journal, Order-Events, SignalId, JSONL sind Core-Contracts (wie der
generische Trade-Log heute); alle Module schreiben hinein (Dual-Write-Muster existiert bereits).
- **Analyse = modulübergreifend.** Profitabilität ist eine Frage über Module/Accounts hinweg — genau
wie das Dashboard.
- **Modul-Spezifisches via Contract:** Core definiert `IAnalysisContextSource` (liefert z. B.
RF-Kandidaten-Kontext oder Copytrading-Master-Kontext zu einem Dossier); Module registrieren
Implementierungen per DI; der Supervisor konsumiert alle. So bleibt „Module kennen einander nicht"
gewahrt (beide Seiten referenzieren nur den Core).
## 4. Frage 4 — Unterbringung: eigenes Modul „Supervisor"
`src/PolyTrader.Modules.Supervisor/` als `IPolyTraderModule` (`Name="Supervisor"`, `DbPrefix="sup_"`),
Registrierung in `Program.cs`, referenziert nur den Core. Eigenes Fenster (Muster RF/Copytrading):
- Tab **Analyse** (Chat mit dem Agenten, Tool-Aufrufe sichtbar/expandierbar — Nachvollziehbarkeit!)
- Tab **Dossier-Browser** (Trade/Signal auswählen → Dossier ansehen, auch ohne KI)
- Tab **Berichte** (gespeicherte Analysen/Tagesberichte, `sup_reports`)
- Tab **Settings** (API-Key, Modellwahl je Aufgabe, Kosten-/Tokenbudget, Bericht-Zeitplan)
Warum Modul statt Core-Fenster: passt ins etablierte Muster (eigene Persistenz `sup_reports`/
`sup_conversations`, eigene Settings, eigener Launcher-Button), hält die KI-Abhängigkeit aus dem Core
heraus und ist einzeln abschaltbar.
## 4a. Supervisor-„Team": Profile statt getrennter Agenten
Die Idee (technischer Supervisor + je Modul ein Strategie-Supervisor) ist richtig — aber als
**Profile über EINER gemeinsamen Infrastruktur**, nicht als getrennte Agenten/Fenster/Prozesse:
| Profil | Fokus | Tools (Subset) | Kontext |
|---|---|---|---|
| **Technik-Supervisor** | Fehler-/Warning-Muster in Logs, Job-Health, API-Ausfälle, Latenzen, Reconciliation-Differenzen — KEINE Strategie-Meinung | read_logs, get_ledger_diff, query_order_events | Architektur-Doku |
| **CopyTrading-Supervisor** | Master-Qualität vs. Ausführungsqualität, Leiter-Verhalten, Reject-Muster | query_decisions, get_dossier, Predictalytics (Master-Historie) | + CopyTrading-Kontextabschnitt |
| **ResolutionFarming-Supervisor** | Kalibrierung (Winrate je Preisband vs. Erwartung), Cluster-Risiken, Scanner-Rejects | query_decisions, rf-Kontext, get_kpis | + RF-Kontextabschnitt |
| *(später)* **Chef-Supervisor** | fasst die Einzelberichte zusammen | die Berichte der anderen | Gesamtsicht |
Ein Profil = System-Prompt + Tool-Subset + Zeitplan + Modellwahl. Gleiche Registry, gleicher Agent-
Runner, gleiche UI (Profil-Auswahl im Analyse-Tab; Berichte je Profil). Modul-Wissen kommt über die
`IAnalysisContextSource`-Registrierung der Module — ein neues Modul bringt seinen Supervisor-Kontext
selbst mit, ohne dass der Supervisor es kennt.
**Bewusst NICHT (v1):** Agent-zu-Agent-Orchestrierung/Diskussionen — teuer, schwer debugbar, wenig
Mehrwert. Profile laufen unabhängig (on-demand oder per Zeitplan); der „Chef" liest nur deren Berichte.
## 5. API-Key: ja, getrennt
**Separater OpenRouter-Key für den Supervisor** (getrennt von künftigen Trading-Modul-Keys wie dem
AI-Markt-Rating):
1. **Kostenzuordnung** — Forensik-Sessions können tokenintensiv werden; sauber getrennt sichtbar.
2. **Spend-Limits je Key** bei OpenRouter → ein Analyse-Amok kann nie das Trading-Budget fressen (und umgekehrt).
3. **Unabhängige Rotation/Sperrung** (Incident-Response, siehe Sicherheitskonzept).
4. **Modellwahl je Aufgabe:** Routineberichte mit günstigem Modell, Tiefen-Forensik mit starkem Modell — je Aufgabe konfigurierbar.
**Ablage:** beide Keys über die vorhandene `SecretProtection` (F1-Mechanik) verschlüsselt — nicht im
Klartext in DB/Config.
## 6. Sicherheit (Verzahnung mit docs/sicherheit)
- **OpenRouter = NEUER externer Datenempfänger.** Bewusste Erweiterung der Egress-Allowlist (§5.3 im
Sicherheitskonzept) + Dokumentation dort. Es verlassen uns: Trade-/Entscheidungs-/Log-Daten (Wallet-
Adressen sind ohnehin öffentlich on-chain). **Redaction-Schicht vor dem Versand:** niemals Secrets/
Keys/Connection-Strings in Tool-Antworten (Log-Reader filtert Muster; Secrets stehen per F6-Prüfung
ohnehin nicht in Logs — Doppelboden bleibt).
- **Read-only by design:** kein Order-/Schreib-Tool. Damit ist auch Prompt-Injection über Fremdtexte
(Marktfragen in Dossiers) auf „falsche Analyse" begrenzt, kann aber nie handeln.
- Tool-Aufrufe des Agenten werden geloggt (sup_-Tabelle) — der Supervisor ist selbst auditierbar.
## 7. Phasen
- **S-0 Datenfundament (Core):** `core_decision_journal` + ReasonCode-Enum + `SignalId`-Durchreichung +
`core_order_events` + JSONL-Sink + **Log Viewer im Terminal**. Engine/Leiter/Monitor/RF schreiben
strukturiert. Pure Logik + Tests; Migrationen offline. **Sofortnutzen ohne KI** (abfragbare Rejects,
Log-Forensik per CorrelationId, Zielland-Debugging).
- **S-1 Dossier:** Generator (pur, testbar) + Dossier-Browser-UI (Modul-Skelett Supervisor).
- **S-2 Agent:** OpenRouter-Client, Tool-Registry (read-only, transport-agnostisch), Analyse-Chat-Tab
mit **Profil-Auswahl** (zunächst 1 Profil „Allgemein"), Architektur-Kontext-Dokument.
- **S-3 Team & Berichte:** Technik-/Modul-Supervisor-Profile, Batch-Analysen, Counterfactual-Report,
täglicher Threema-Bericht; `IPredictalyticsClient` (Core) + Predictalytics-Tools.
- **S-4 MCP-Light (optional):** Tool-Registry zusätzlich als lokaler MCP-Server für externe Clients.
## 8. Offene Entscheidungen (Richard)
1. **Start mit S-0 sofort?** (Empfehlung: ja — nützt auch ohne KI und VOR dem Live-Start; alles
Weitere baut darauf.)
2. Counterfactual-Tracking („was wäre aus Rejects geworden") von Anfang an im Journal vorsehen
(Resolution-Nachverfolgung abgelehnter Signale) oder später?
3. Modellwahl-Defaults (günstig vs. stark) und Tokenbudget/Monat für den Supervisor.
4. Tagesbericht via Threema gewünscht (S-3)?
5. Text-Log-Sink nach Etablierung des Log Viewers abschalten (nur noch JSONL) oder dauerhaft dual?
6. Predictalytics-API: Auth/Key-Mechanik und welche Endpoints der Supervisor bekommt (read-only-Subset).
## Ergänzungen Richard (2026-07-16, eingearbeitet)
- ✅ Predictalytics-Daten als Analyse-Quelle (§2, `IPredictalyticsClient` im Core, S-3).
- ✅ Supervisor-„Team" — als Profile über einer Infrastruktur statt getrennter Agenten (§4a).
- ✅ Reasoning/Logs KI-freundlich als JSONL + menschenlesbarer Log Viewer im Terminal (§1, S-0).
@@ -0,0 +1,126 @@
# Umsetzungsplan: AI-Bewertung der Auflösequalität (C-S4)
> Stand: 2026-07-11
> Ziel: Ein LLM (via OpenRouter) bewertet je Markt das RESOLUTION-Risiko (subjektive
> Auflösequellen, Regeltext-Fallen, UMA-Dispute-Muster). Die Bewertung wird beim
> Markt-Import in der DB gespeichert und dient als Entry-Gate — zuerst im
> ResolutionFarming, perspektivisch auch im Copytrading.
> Einordnung: Core-Baustein (beide Module profitieren), kein eigenes Strategiemodul.
---
## 1. Abgrenzung (wichtig für die Umsetzung)
Das LLM prognostiziert NICHT den Markt-Ausgang. Es beantwortet ausschließlich:
**„Wie sauber/objektiv wird dieser Markt aufgelöst werden?"** — Input ist der
Regeltext, nicht das Weltgeschehen. Das hält die Aufgabe eng, billig und testbar.
## 2. Architektur
### 2.1 Core-Service `IMarketRiskRater` (PolyTrader.Core)
```csharp
public interface IMarketRiskRater
{
/// Liefert die (ggf. gecachte) Bewertung; null wenn (noch) keine vorliegt.
Task<MarketRiskRating?> GetOrRateAsync(MarketData market, CancellationToken ct);
}
public class MarketRiskRating // Tabelle core_market_risk_ratings
{
public string ConditionId { get; set; } // PK — Regeltexte ändern sich nicht → dauerhaft cachebar
public int Score { get; set; } // 0100 (100 = völlig objektiv auflösbar)
public string Flags { get; set; } // CSV: SUBJECTIVE_SOURCE, DEADLINE_AMBIGUITY,
// MENTIONS_TYPE, DISPUTE_PATTERN, MISSING_RULES
public string Reason { get; set; } // Einzeiler-Begründung des Modells
public string Model { get; set; } // verwendetes Modell (Nachvollziehbarkeit)
public DateTime RatedAt { get; set; }
}
```
### 2.2 `OpenRouterClient` (Core, dünn)
- HTTP-Client gegen https://openrouter.ai/api/v1/chat/completions, API-Key aus
appsettings (`OpenRouter:ApiKey`, gitignoriert wie andere Secrets).
- **Modellwahl:** günstiges Modell reicht (Haiku-Klasse, z. B.
`anthropic/claude-haiku-4.5` via OpenRouter; Modell-ID als Setting, nicht
hartkodieren). Kosten je Markt: Bruchteile eines Cents; mit Cache je ConditionId
einmalig pro Markt.
- **Structured Output:** Antwort als JSON erzwingen (Schema im Prompt + JSON-Mode);
bei Parse-Fehler 1 Retry, danach „kein Rating" (fail-closed, siehe 3.3).
- Timeout kurz (~15 s), Fehler loggen, NIE den aufrufenden Scanner blockieren.
### 2.3 Prompt (Kern, bei Umsetzung feinjustieren)
Input je Markt: `Question`, `Description`/Resolution-Kriterien (Gamma-API liefert
den Regeltext am Markt-Objekt — Feld bei Umsetzung verifizieren), Kategorie, EndDate.
Bewertungsauftrag an das Modell (sinngemäß):
1. Gibt es eine EINDEUTIGE, öffentlich prüfbare Auflösequelle (offizielles
Endergebnis, behördliche Zahl, On-Chain-Fakt)?
2. Sind Randfälle geregelt (Verschiebung, Abbruch, Unentschieden, Definitionsfragen
wie „offiziell angekündigt")?
3. Ähnelt der Markt bekannten Streit-Mustern (Mentions-/„sagt X"-Märkte, vage
Deadlines, Definitions-Ambiguität, mehrdeutige Quellen)?
Output: `{ "score": 0-100, "flags": [...], "reason": "…" }`.
## 3. Integration
### 3.1 Wann wird bewertet?
Beim Markt-Import bzw. beim ersten Kontakt: Der **RF-Scanner** ruft
`GetOrRateAsync` für jeden Kandidaten auf, der alle billigen Filter passiert hat
(NACH Preisband/Kategorie/Fenster, VOR dem Accept — kein LLM-Call für offensichtliche
Rejects). Cache macht Wiederholungs-Scans kostenlos.
### 3.2 Entry-Gate im ResolutionFarming
Neue `RfSettings`:
- `MinResolutionScore` (Default 70): Kandidaten mit Score darunter → Reject mit
Grund `"AI-Resolution-Score {score} < {min}: {reason}"` (landet wie alle Rejects
in rf_candidates → die Kalibrierung kann später prüfen, ob das Gate Geld spart!).
- `RequireRating` (Default true): fail-closed-Schalter (siehe 3.3).
### 3.3 Fail-Closed-Regeln (Sicherheitskern)
1. Kein Rating verfügbar (API down, Parse-Fehler, kein Regeltext) → Kandidat gilt
als riskant und wird abgelehnt — AUSSER die Kategorie steht auf einer
Objektiv-Whitelist (Default: Sports-Endergebnisse), dann Durchlass mit Log.
2. Das Rating **ersetzt die harte Blacklist nicht**: bekannte Giftmuster
(Mentions-Märkte etc.) bleiben in `BlacklistCsv` hart geblockt. Das LLM ist die
zweite Verteidigungslinie für den Long Tail, nicht die erste.
3. **Post-Mortem-Pflicht:** Jeder real erlebte Dispute → Muster in Blacklist/Prompt
aufnehmen. Dafür Flag-Feld in rf_closed_trades (`ResolutionDisputed`) vorsehen.
### 3.4 Copytrading (zweiter Schritt, optional)
Gleicher Service: Vor einem BUY den Score prüfen; unter Schwelle → Trade
verwerfen mit TradeReasoning-Log. Als Per-Account-Setting (`MinResolutionScore`,
0 = aus), Default zunächst AUS, um das Copy-Verhalten nicht zu verändern, bis der
Rater validiert ist.
## 4. Validierung VOR dem Scharfschalten (Pflicht-Phase)
1. **Retrospektiv-Test:** ~1520 öffentlich bekannte, umstrittene UMA-Resolutions
(Recherche-Aufgabe: bekannte Dispute-Fälle) + ~30 unstrittig aufgelöste
Vergleichsmärkte durch den Rater schicken. Messen: erwischt er die Streitfälle
(niedriger Score), ohne die sauberen zu blockieren?
2. **Akzeptanz:** ≥ 80 % der Streitfälle unter der Schwelle, ≤ 10 % der sauberen
fälschlich blockiert. Sonst Prompt/Schwelle iterieren.
3. Ergebnisse als Testdaten einfrieren (Golden-File-Test, läuft ohne API gegen
gespeicherte Antworten — kein LLM-Call in der CI).
## 5. Phasen
| Phase | Inhalt | Akzeptanz |
|---|---|---|
| AR-1 | OpenRouterClient + IMarketRiskRater + Tabelle/Migration + Unit-Tests (Parsing, Fail-Closed) | Build grün; Rater liefert für einen Beispiel-Regeltext strukturiertes Rating |
| AR-2 | Retrospektiv-Validierung (4.) | Akzeptanzquoten erreicht, dokumentiert |
| AR-3 | RF-Integration (3.1/3.2/3.3) + UI-Spalte (Score in Kandidaten-Tab) | Rejects mit AI-Grund sichtbar; Kalibrierung kann Gate-Wirkung auswerten |
| AR-4 | Copytrading-Integration (3.4), Default aus | Setting vorhanden, dokumentiert |
## 6. Leitplanken
- API-Key nie committen; Kosten-Deckel (max. Ratings/Tag als Setting, Default 500).
- Der Rater beeinflusst NIE Exits, nur Entries (keine Panik-Verkäufe durch LLM).
- Score/Reason immer mitloggen — Entscheidungen müssen im Log nachvollziehbar sein.
@@ -0,0 +1,117 @@
# Umsetzungsplan: On-Chain-Auto-Redeem (modulweise schaltbar)
> Stand: 2026-07-11
> Ziel: Gewonnene Positionen automatisch on-chain einlösen (Shares → USDC), als
> **Core-Baustein** mit **Aktivierung je Modul**. Richards Anforderung: In Testphasen
> neuer Module soll Auto-Redeem gezielt AUS bleiben können, um die Performance der
> Entwicklung manuell nachvollziehen zu können — ohne dass andere, etablierte Module
> ihren Automatismus verlieren.
> ⚠️ Höchste clob.md-Kritikalität: On-Chain-Signing mit echten Private Keys.
> Jede Phase: Backup/Commit vorher, Testmarkt/Kleinstbetrag zuerst.
---
## 1. Architektur: Queue statt Direktaufruf
Module redeemen nie selbst. Sie melden einlösbare Positionen in eine zentrale
Queue; ein Core-Worker arbeitet sie ab — nur für Module, deren Auto-Redeem aktiv ist.
```
Modul (RF-Monitor, CT-Sync, später DD) PolyTrader.Core
│ erkennt „Position gewonnen & aufgelöst" │
├── IRedeemQueue.Enqueue(RedeemRequest) ─────────────▶│ Tabelle core_redeem_queue
│ (immer! unabhängig vom Schalter) │
│ ▼
│ OnChainRedeemWorker (BackgroundService)
│ • nur Requests von Modulen mit AutoRedeemEnabled
│ • Rest bleibt als "Manual" sichtbar (UI-Liste)
▼ • CTF redeemPositions / NegRiskAdapter
UI je Modul: Pending-Redeems-Ansicht • Verifikation über USDC-Balance-Delta
```
**Warum immer enqueuen:** Auch bei deaktiviertem Auto-Redeem entsteht so eine
vollständige, je Modul gefilterte „zum Redeem bereit"-Liste (Dashboard/UI) — genau
die Übersicht, die Richard in Testphasen für die manuelle Betreuung will. Der
Schalter entscheidet nur, ob der Worker sie abarbeitet.
### 1.1 Datenmodell (`core_redeem_queue`)
| Feld | Inhalt |
|---|---|
| Id (auto) | PK |
| ModuleName | "ResolutionFarming" / "CopyTrading" / … |
| AccountId, TokenId, ConditionId | Ziel der Einlösung (ConditionId zwingend — für den Contract-Call) |
| IsNegRisk | Adapter-Wahl |
| SizeShares, ExpectedUsd | erwartete Auszahlung (Shares × 1.00) |
| Status | Pending / Processing / Done / Failed / Manual |
| Attempts, LastError, EnqueuedAt, CompletedAt, TxHash | Betrieb/Nachvollziehbarkeit |
### 1.2 Schalter
- **Je Modul:** `core_module_settings` (oder appsettings-Sektion) —
`AutoRedeem:ResolutionFarming = false`, `AutoRedeem:CopyTrading = false`
(Default IMMER aus; bewusstes Einschalten je Modul).
- **Global-Not-Aus:** `AutoRedeemGlobalEnabled` (analog GlobalTradingPaused,
Quickbar-Toggle) — schlägt alle Modul-Schalter.
- UI: Schalter je Modul in dessen Settings-Tab + Anzeige im Core-Dashboard,
welche Module aktiv sind.
## 2. Der On-Chain-Teil (`OnChainCtfService`, Core)
1. **Bibliothek:** Nethereum (bereits als Abhängigkeit im Projekt für EIP-712-Signing
vorhanden) — Contract-Calls über die bestehende Alchemy-RPC-Anbindung (Polygon).
2. **Aufrufe:** Standard-Markt: ConditionalTokens `redeemPositions(collateral,
parentCollectionId=0x0, conditionId, indexSets)`; NegRisk-Markt: über den
NegRisk-Adapter. **Contract-Adressen + ABI + indexSets-Ermittlung bei Umsetzung
zwingend aus https://docs.polymarket.com (Developer/CTF) verifizieren — nicht aus
dem Gedächtnis kodieren.** Die USDC-/CTF-Adressen als Konstanten mit Quellenangabe.
3. **Gas:** Wallet braucht POL. Vor jedem Call Balance-Check; unter Schwelle
(Setting, z. B. 0.5 POL) → Request auf `Manual` + Threema-Warnung „POL nachfüllen".
Gas-Preis: Standard-Estimation, Cap als Setting.
4. **Verifikation = Wahrheit:** Ein Redeem gilt erst als `Done`, wenn (a) die Tx
bestätigt ist UND (b) das USDC-Balance-Delta ≈ ExpectedUsd (Toleranz) gemessen
wurde. Sonst `Failed` mit Fehlertext.
5. **Retry:** max. 3 Versuche mit Backoff (1/10/60 min), danach `Manual` + Threema.
Idempotenz beachten: vor jedem Versuch prüfen, ob die Position on-chain überhaupt
noch einlösbar ist (bereits redeemte Shares → als Done werten, nicht als Fehler).
## 3. Modul-Integration
### 3.1 ResolutionFarming (erster Nutzer)
`FarmingResolutionMonitorService` setzt heute `RedeemStatus = "Pending"` am
RfClosedTrade. Ergänzung: beim Schließen eines Gewinners → `IRedeemQueue.Enqueue`.
Worker-Callback (oder Status-Poll) aktualisiert `RedeemStatus` (Pending → Redeemed/
Manual/Failed) → sichtbar im Historie-Tab. **Wichtig für Richards Testphasen-
Anforderung:** Der realisierte PnL ist bereits bei Resolution gebucht; der Redeem
ändert nur die Kapitalverfügbarkeit. Die Performance-Auswertung bleibt also mit und
ohne Auto-Redeem identisch — nur die Bankroll-Rotation unterscheidet sich.
### 3.2 CopyTrading (zweiter Nutzer)
Im `TraderMonitorService` existiert die Stelle bereits: der auskommentierte
Python-Redeem-Block im Resolution-Fallback („bereit für manuellen Redeem")
— dort `Enqueue` statt Kommentar. Gleicher PnL-Hinweis wie oben.
### 3.3 Künftige Module
Contract: Ein Modul, das Positionen bis Resolution hält, ruft bei „gewonnen &
aufgelöst" genau einmal `Enqueue` und liest optional den Status zurück. Mehr nicht.
## 4. Phasen & Akzeptanzkriterien
| Phase | Inhalt | Akzeptanz |
|---|---|---|
| RD-1 | Queue-Tabelle + IRedeemQueue + Modul-Schalter + UI-Pending-Liste (noch KEIN On-Chain-Code) | Module enqueuen; Liste zeigt je Modul „bereit zum Redeem"; Schalter sichtbar; 100 % ohne Live-API testbar |
| RD-2 | `OnChainCtfService` gegen Polygon: zuerst READ-only (Balance, Einlösbarkeits-Check) | Balance-/Zustandsabfragen stimmen gegen Polygonscan |
| RD-3 | Erster echter Redeem: EIN Testmarkt, Kleinstbetrag, manuell getriggert (Button an der Pending-Liste) | Tx bestätigt, USDC-Delta verifiziert, Status Done |
| RD-4 | Worker-Automatik scharf für RF (Schalter an), CT folgt nach Beobachtung | 1 Woche fehlerfreier Betrieb, Failed-Quote < 5 %, POL-Warnung getestet |
## 5. Leitplanken
1. `.agents/rules/clob.md` gilt verschärft: Private-Key-Nutzung außerhalb des
erprobten Order-Signing-Pfads. Jede Phase einzeln committen; RD-3 nie
überspringen.
2. Der Worker fasst NUR Queue-Einträge an — er scannt nie selbst Positionen
(klare Verantwortung: Module erkennen, Core löst ein).
3. Alle Beträge/TxHashes loggen; Threema-Tageszusammenfassung „X Redeems, Y USDC
freigesetzt, Z manuell offen".
4. Manuelle Redeems (über die Website) müssen erkannt werden: Einlösbarkeits-Check
in 2.5 markiert extern eingelöste Einträge als Done statt Failed.
@@ -0,0 +1,314 @@
# Umsetzungsplan: Copytrading-Modul — Rentabilitäts-Verbesserungen
> Stand: 2026-07-06
> Ziel: Bekannte Verlustquellen im Copytrading-Modul beseitigen und die
> Rentabilität durch datengetriebene Trader-Auswahl, echte Fill-Daten und
> besseres SELL-Handling steigern.
> Reihenfolge: **Dieser Plan zuerst.** Phase 1 (Marktdaten-Fundament) ist
> Voraussetzung für die Strategiemodule MarketMaking und BundleArbitrage.
---
## 0. Kontext & Hintergrund (für die Umsetzung ohne Vorwissen)
Das Copytrading-Modul (`src/PolyTrader.Modules.CopyTrading/`) kopiert Trades von
Master-Tradern auf Polymarket. Signalkette:
1. `AlchemyWebsocketService` erkennt On-Chain-Events der Master-Wallets (WSS).
2. `TraderMonitorService.TriggerFastBlockchainPoll()` parst die Transaktion direkt
(Fast Track, ~34 s hinter dem Master) oder fällt auf Data-API-Polling zurück.
3. Signale (`CopySignal`) laufen über einen Channel in die `CopyTradingEngine`.
4. Die Engine prüft Risiko-Limits (`CopyTradingAccountSettings`: PerMarketLimit,
PerMasterLimit, Zeitfenster-Limits, MaxBuyPrice) und platziert CLOB-Orders
über `PolymarketClobClient` (Core).
**Historischer Kontext (wichtig!):** Eine Verlustanalyse im April 2026
(`agentspace/prompts/AnalyzingOvernightTradingLosses.md`) hat als Hauptursache
für Overnight-Verluste identifiziert, dass der Bot SELLs der Master mit
Market-Orders ins leergeräumte Orderbuch kopiert und so zur „Exit-Liquidity"
wird (Beispiel: Entry 0.51, Master-Exit 0.99, unser Exit 0.49). Der damalige
Fix (GTD-Limit-Sells) ist **im aktuellen Modul-Code nicht mehr vorhanden**
vermutlich bei der Modularisierung verloren gegangen.
**Seit März 2026 erhebt Polymarket Taker-Fees** (Sports ~0,75 %, Politik/Finanzen
~1,0 %, Krypto ~1,8 %, am 50-¢-Preis am höchsten, Richtung 1 ¢/99 ¢ abnehmend;
Maker zahlen nichts und erhalten Rebates). Der Bot ist heute fast immer Taker.
Quelle: https://docs.polymarket.com/trading/fees — **bei Umsetzung aktuellen
Stand verifizieren.**
### Leitplanken (gelten für alle Phasen)
1. `.agents/rules/clob.md` beachten: Änderungen an der CLOB-Integration sind
hochkritisch. Vor jeder Änderung Backup/Commit der alten Version, jede
Änderung mehrfach prüfen.
2. Jede Phase lässt die App baubar und lauffähig zurück (Debug-Build grün).
3. Entscheidungslogik als testbare, pure Funktionen extrahieren und in
`PolyTrader.Tests` (xUnit, existiert bereits) abdecken.
4. Kein Livegang einer Phase ohne mehrtägige Beobachtung auf dem Server.
---
## Phase 0 — Sofortmaßnahmen: Blutung stoppen
### 0.1 🔴 SELL-Exit-Liquidity-Regression beheben (höchste Priorität)
**Befund:** In `CopyTradingEngine.cs` (Live-SELL-Pfad, aktuell ~Zeile 694735)
werden SELLs als `"MARKET"` mit Fallback-Limit `0.01m` gesendet:
```csharp
decimal sellLimit = 0.01m; // Market Order Fallback Limit
...
var result = await _clob.PlaceOrderAsync(account, signal.TokenId, signal.Side,
expectedUsdc, sellLimit, "MARKET", _state.DebugOrderPayloadLog, isNegRisk);
```
Das ist exakt das Verhalten, das die April-Verluste verursacht hat.
**Ziel-Design: Eskalationsleiter statt Market-Order**
1. Referenzpreis = `signal.Price` (Exit-Preis des Masters).
2. Erste Order: GTD-Limit.
- HF-Trader (`trader.Category == "HF"`): `signal.Price - 0.005m`.
- Sonst: `signal.Price * (1 - settings.MaxPriceDifference / 100m)`.
3. Neuer Setting-Wert `SellFloorPct` in `CopyTradingAccountSettings`
(Default z. B. 15 %): absolute Untergrenze = `signal.Price * (1 - SellFloorPct/100)`.
4. Hintergrund-Loop (Erweiterung von `CleanupStaleOpenOrdersAsync` in
`TraderMonitorService` oder eigener Loop): Order nach T Sekunden ohne Fill
(HF: ~20 s, sonst: ~120 s) canceln und eine Stufe tiefer neu platzieren
(Schrittweite z. B. 2 ¢ oder 3 % relativ), bis zum Floor.
5. Floor erreicht und kein Fill → Position halten, **Threema-Benachrichtigung**
senden (`ThreemaService` im Core existiert) und Position als „ExitPending"
markieren.
6. Position darf **nicht mehr optimistisch** aus `account.OpenPositions`
entfernt werden. Stattdessen Flag `ExitPending` (neues Property auf
`Position` oder Tracking-Dictionary im `CopyTradingState`), damit Limits
weiterhin korrekt rechnen und kein Doppel-SELL entsteht. Entfernen erst,
wenn der Fill über Sync/User-Channel (Phase 1) bestätigt ist.
**Preis-/Stufenlogik als pure statische Funktion** implementieren (z. B.
`SellLadder.NextPrice(referencePrice, step, floor, attempt)`) und mit
Unit-Tests abdecken.
**Akzeptanzkriterien:**
- Kein Code-Pfad sendet mehr `"MARKET"`-SELLs mit 0.01-Limit.
- Unit-Tests für Ladder-Preise (HF/normal, Floor-Clamping, 0.01/0.99-Grenzen).
- Log zeigt pro SELL: Referenzpreis, gewähltes Limit, Stufe.
### 0.2 Fee-Modell einführen
1. Fee-Rate je Markt beschaffen: Die CLOB-/Gamma-API liefert Fee-Informationen
am Markt-Objekt (Feldname bei Umsetzung anhand
https://docs.polymarket.com/trading/fees verifizieren, z. B. `fee_rate_bps`).
Fallback: statische Kategorie-Tabelle (Sports 0.75 %, Politics/Finance 1.0 %,
Crypto 1.8 %, Geopolitics 0 %).
2. `MarketData` (Core) um `TakerFeeBps` erweitern (EF-Migration Core),
Befüllung über `MarketSyncService` bzw. beim Markt-Fetch.
3. Risk-Check in `CopyTradingEngine`: erwartete Fee vom verfügbaren Edge
abziehen; Mikro-Trades, deren Fee den erwartbaren Gewinn frisst, verwerfen
(Logging mit Begründung wie bei den bestehenden Checks).
4. PnL-Berechnung (Demo **und** Live-Anzeige) um Fees korrigieren.
**Akzeptanz:** Fee erscheint im TradeReasoning-Log jedes BUY; Demo-PnL weist
Fees aus.
### 0.3 `ProfitTarget` implementieren oder entfernen
`CopyTradingAccountSettings.ProfitTarget` (Default 50.0) existiert in Settings,
DB und UI, wird aber **nirgends ausgewertet** (toter Knopf).
**Empfehlung: implementieren** als optionaler Take-Profit:
- Semantik: `0` = deaktiviert; sonst Prozent-Gewinnschwelle.
- Prüfung im 30-s-Live-Sync (`PollLiveAccountsAsync`): wenn
`CurrentPrice >= EntryPrice * (1 + ProfitTarget/100)` → Verkauf über die
Eskalationsleiter aus 0.1 (Startlimit = CurrentPrice), ExitReason
`"Profit Target"`.
- Zusammenspiel mit `PreRedeemLimit` beachten (beide können feuern —
PreRedeem hat Vorrang, da näher an 1.00).
### 0.4 Kleinere Konsistenz-Fixes
1. **20-Sekunden-Spam-Blockade** (`PendingOrderTimestamps`-Check am Anfang des
SELL-Pfads): blockiert aktuell auch legitime SELLs, wenn der Master < 20 s
nach dem Kauf aussteigt. Fix: Blockade nur für gleichgerichtete Orders
(BUY nach BUY), SELL nach BUY zulassen.
2. **`_state.GlobalPnl`**: wird in `PollLiveAccountsAsync`-Close-Pfaden addiert,
in `PollClosedAccountsAsync` nicht → Anzeige driftet. Vereinheitlichen.
3. **Demo-`ClosedTrade` ohne `TokenId`**: Im Demo-SELL-Pfad wird `TokenId` nicht
gesetzt (Preload von `_processedClosures` filtert auf `TokenId`). Setzen.
---
## Phase 1 — Marktdaten-Fundament (Core-Infrastruktur)
> Diese Phase gehört in **PolyTrader.Core** (`src/PolyTrader.Core/Streaming/`),
> nicht ins Modul — MarketMaking- und BundleArbitrage-Modul (separate Pläne)
> setzen sie voraus.
### 1.1 CLOB User-Channel (echte Fills in Echtzeit)
Polymarket bietet einen authentifizierten WSS-User-Channel, der Order-Events
(Platzierung, Teil-/Voll-Fill, Cancel) der eigenen Accounts pusht.
Endpoint/Protokoll bei Umsetzung verifizieren:
https://docs.polymarket.com (CLOB WSS, `user` channel; Auth via API-Key/
Secret/Passphrase — liegen je Account in `AccountState`).
Neuer Core-Service `ClobUserChannelService : BackgroundService`:
- Verbindet pro Live-Account, Auto-Reconnect mit Backoff (Muster von
`AlchemyWssClient` übernehmen).
- Publiziert Fill-Events intern (Event oder Channel), z. B.
`record OrderFillEvent(int AccountId, string TokenId, string OrderId, string Side, decimal Price, decimal Size, DateTime Ts)`.
Konsumenten im Copytrading-Modul:
- `Position.EntryPrice`/`Size` mit **echten Fill-Daten** aktualisieren
(heute: Limit-Preis als EntryPrice, Korrektur erst im 30-s-REST-Sync).
- SELL-Eskalationsleiter (Phase 0.1): Fill-Bestätigung beendet die Leiter.
- Neue Tabelle `ct_fill_log` (EF-Migration im Modul): SignalPrice, OrderPrice,
FillPrice, Latenz (Signal→Fill in ms), TraderId, AccountId, TokenId, Side.
→ Grundlage für Slippage-Statistik in Phase 3.
### 1.2 CLOB Market-Channel (Orderbücher live)
Neuer Core-Service `ClobMarketDataService`:
- Abonniert den öffentlichen `market`-Channel für eine dynamische Token-Liste
(Subscribe/Unsubscribe zur Laufzeit).
- Hält `OrderBookCache` (Best-Bid/Ask, Tiefe der obersten N Level, Timestamp).
- Interface für Konsumenten: `IOrderBookProvider.TryGetBook(tokenId, maxAgeMs)`.
- REST-Fallback `GET /book` über `PolymarketClobClient`, wenn kein Stream aktiv.
**Hinweis:** Im Modul existiert bereits ein `PolymarketWssClient` (Auto-Redeem).
Nicht verschieben/umbauen (Regression-Risiko), sondern den neuen Core-Service
parallel aufbauen; spätere Konsolidierung als separater Schritt.
### 1.3 Pre-Trade-Orderbuch-Check in der Engine
Vor jedem Live-BUY in `CopyTradingEngine.ProcessAccountOrderAsync`:
1. Buch holen (`IOrderBookProvider`, Fallback REST, Timeout ~150 ms —
bei Timeout Verhalten wie heute, nicht blockieren).
2. Checks (neue Settings in `CopyTradingAccountSettings`):
- `MaxSpreadPct` (Default z. B. 5 %): Spread größer → Skip mit Log.
- Tiefen-Check: liegt an unserem Limit-Preis genug Ask-Size für
`exactShares`? Wenn nein → Skip („Sniping-Verdacht: Liquidität bereits
konsumiert") statt teuer ins dünne Buch zu laufen.
**Akzeptanz Phase 1:** Fill-Log füllt sich mit echten Fills; TradeReasoning
zeigt Spread/Tiefe-Entscheidungen; kein messbarer Latenz-Nachteil im Hot-Path
(> 200 ms Zusatz wäre Regression).
---
## Phase 2 — SELL-Verfeinerung: Proportionalität
Heute (Proportionalitätsfilter in `CopyTradingEngine`, SELL-Pre-Flight):
verkauft der Master < 30 % seines Bestands → ignorieren; ≥ 30 % → **wir
verkaufen alles**. Information über gestaffelte Exits geht verloren.
**Ziel:** Verkaufsquote spiegeln.
1. Beim Öffnen einer Position den Master-Bestand zum Einstiegszeitpunkt
festhalten (`MasterSharesAtEntry`, im `CopyTradingState.MasterTraderPositions`
bzw. auf der Position persistieren).
2. Bei SELL-Signal: `sellRatio = signal.Size / masterSharesVorVerkauf` (wie
heute berechnet). Statt Voll-Exit: `sharesToSell = ourShares * sellRatio`.
3. Untergrenzen beachten: bleibt danach < Polymarket-Minimum (56 Shares) übrig
→ Voll-Exit statt Rest-Dust.
4. Kleiner Teilverkauf (< 10 %) weiterhin ignorieren (Rauschen von Day-Tradern),
Schwelle konfigurierbar (`MinSellRatioPct`).
5. Verkauf läuft immer über die Eskalationsleiter aus Phase 0.1.
Akzeptanz: Unit-Tests für die Ratio-Logik inkl. Dust-Grenzen; Logs zeigen
„Teilverkauf x % gespiegelt".
---
## Phase 3 — Trader-Intelligence (Auswahl automatisieren)
> Beim Copytrading entscheidet die Master-Auswahl über den Großteil des
> Ergebnisses. Diese Phase macht sie messbar und selbstkorrigierend.
### 3.1 Copy-PnL-Score („Kopierbarkeit")
Der `MasterTraderAnalyticsJob` misst heute den PnL des **Masters**. Relevanter
ist, was **wir** mit ihm verdient haben — inkl. unserer Slippage und Fees.
1. Neue Kennzahlen je Master aus `ct_`-Closed-Trades (`ICopyTradeLogRepository`,
Filter `SourceTraderId`, letzte 30 Tage):
- `CopyPnl30d`, `CopyProfitFactor` (Bruttogewinn/Bruttoverlust),
`CopyAvgPnlPerTrade`, `CopyTradeCount30d`.
- `AvgSlippagePct` aus `ct_fill_log` (Phase 1.1): Ø(FillPriceSignalPrice)/SignalPrice.
2. Felder auf `TrackedTrader` ergänzen (+ EF-Migration `mod_copytrading_trackers`),
Berechnung im `MasterTraderAnalyticsJob`, Anzeige in `MastersTradersView`.
3. **Achtung Metrik-Falle:** Winrate allein ist irreführend (Favoriten-Käufer
haben 95 % Winrate und können trotzdem negativ sein). Profit-Faktor und
Ø-PnL/Trade als primäre Sortierung in der UI.
### 3.2 Sniper-/Verhaltens-Metriken in den Analytics-Job
Portierung der Logik aus `analyze_snipers.py` (liegt im Projektroot) nach C#
in den `MasterTraderAnalyticsJob`:
1. Data-API-Activity je Master über volle 3 Tage paginieren (das Skript zeigt
das Pagination-Muster; API-Limit je Request beachten).
2. Kennzahlen: `MedianHoldMinutes`, `SellWithin5MinPct` (Anteil SELLs < 5 min
nach zugehörigem BUY), `SellCount3d`.
3. Schwellen (konfigurierbar): `SellWithin5MinPct > 50 %` → Master als Sniper
flaggen: Warn-Status in UI + Threema-Hinweis. Optional Auto-Pause (siehe 3.3).
### 3.3 Automatischer Kill-Switch je Master
Neue Modul-Settings (global, z. B. in `CopyTradingState` + Persistenz):
`AutoPauseEnabled`, `AutoPauseMinTrades` (z. B. 10), `AutoPauseDrawdownUsd`
oder `-Pct`.
Regel im Analytics-Job (läuft 2×/Tag — zusätzlich stündlicher Light-Check
sinnvoll): Copy-PnL der letzten N Trades unter Schwelle → `IsActive = false`,
`Reasoning` mit Begründung + Zeitstempel befüllen, Threema-Notification.
Reaktivierung bewusst nur manuell.
**Akzeptanz Phase 3:** UI zeigt Copy-Score-Spalten; ein simulierter
Verlust-Master wird automatisch pausiert (Test mit Demo-Daten).
---
## Phase 4 — Maker-Mode & Demo-Realismus
### 4.1 Maker-Einstieg für langsame Master
Für Master mit Haltedauern von Stunden/Tagen (SwissTony/RN1-Typ) ist der
3-Sekunden-Taker-Fill unnötig teuer (Fees + Spread). Neues Verhalten
(Flag je Trader, z. B. `Category == "HOLDER"` oder eigenes Bool `MakerEntry`):
1. BUY als GTC-Limit **auf** Best-Bid (oder Mid 1 Tick) statt über dem Ask.
2. Kein Fill nach T Minuten (konfigurierbar, z. B. 10) und Signal-Markt noch
im Preisband → auf Taker-Verhalten eskalieren oder verwerfen (Setting).
3. Fees: Maker zahlt 0 und sammelt ggf. Rebates — im Fee-Modell (0.2) abbilden.
### 4.2 Demo-Modus realistisch machen
Demo füllt heute zum Signalpreis ohne Slippage/Fees → Demo-Ergebnisse sind
systematisch geschönt und als Validierung neuer Master unbrauchbar.
Fill-Modell im Demo-Pfad der Engine:
`FillPreis = Signalpreis + halber Spread (aus IOrderBookProvider, Fallback
+1 ¢) `, Fee der Marktkategorie abziehen, beides im `ClosedTrade` ausweisen.
**Akzeptanz:** Demo- und Live-PnL desselben Masters weichen über 2 Wochen um
< 20 % relativ ab (grobe Plausibilität statt heutiger Systematik-Lücke).
---
## Offene Entscheidungen (vor Umsetzung mit Richard klären)
1. `SellFloorPct`-Default und Stufen-Timing der Eskalationsleiter (0.1).
2. `ProfitTarget`: implementieren (Empfehlung) oder Feld entfernen?
3. Auto-Pause: nur benachrichtigen oder hart deaktivieren? (Empfehlung: hart,
nachts passiert sonst genau das Falsche.)
4. Maker-Mode: als Trader-Flag oder automatisch aus `MedianHoldMinutes`
ableiten? (Empfehlung: automatisch ab z. B. Median > 60 min, manuell
überschreibbar.)
## Reihenfolge & Abhängigkeiten
```
Phase 0 (sofort, unabhängig)
└── Phase 1 (Core-Infra; parallel zu 0 möglich, Livegang nach 0)
├── Phase 2 (braucht 0.1-Leiter)
├── Phase 3 (braucht 1.1-Fill-Log für Slippage; Rest unabhängig)
└── Phase 4 (braucht 1.2-Orderbuch)
```
@@ -0,0 +1,827 @@
# Umsetzungsplan: Deploymentcenter in PolyTrader einbinden (P3c)
> Stand: 14.08.2026 · Grundlage: `J:\Softwareprojekte\Deploymentcenter\docs` (Fassung 2.4) und die
> SDK-Quellen unter `client-dotnet\Deploymentcenter.Client`.
> Löst ab: [UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration.md](./UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration.md)
> (Watchdog + LicenseLabrador als Einzeldienste).
> Ersetzt in `ANALYSE-Linux-Portierung.md` den Punkt **P3c** und präzisiert dort §5.2.
---
## 0. Was sich gegenüber der bisherigen Planung geändert hat
Das Deploymentcenter ist nicht mehr „Lizenz + Watchdog an einem Ort", sondern deckt inzwischen
**vier** Bereiche ab. Zwei davon standen in unserer Planung bisher gar nicht:
| Bereich | Zustand bei uns |
|---|---|
| **Lizenz** | vorhanden, aber gegen LicenseLabrador gebaut (`LicenseGate`, `Ui/LicenseDialog`) — **und in der Avalonia-App gar nicht verdrahtet**, dort läuft derzeit alles ungeprüft |
| **Watchdog** | vorhanden (`WatchdogHeartbeatService`), aber gegen die **alte** Watchdog-API gebaut — URL, Header und Payload passen nicht |
| **Error-Reporting / Bugtracker** | **existiert bei uns nicht**. Es gibt im gesamten Repo weder `AppDomain.UnhandledException` noch `TaskScheduler.UnobservedTaskException` |
| **UpdateService + Erstinstallation** | **existiert bei uns nicht**. Kein Paket, kein Release-Weg, keine Versionsdisziplin (`<Version>` steht in keiner einzigen `.csproj`) |
Die Schätzung aus der Analyse (24 PT) bezog sich auf die ersten beiden Punkte. Mit
Error-Reporting, Auslieferung und Erstinstallation liegt der Umfang bei **47 PT**.
---
## 1. Befunde — Probleme, Bugs und Konzeptfehler
Sortiert nach Wirkung auf uns. Spalte „Wo" trennt, was **wir** lösen und was im
**Deploymentcenter** gelöst werden müsste.
### D-1 · Der Update-Weg beendet die Anwendung hart — für einen Trading-Bot unzulässig · **hoch** · Wo: wir + DC
`UpdateClient.LaunchUpdateAgent` ruft `Environment.Exit(0)`
(`client-dotnet/Deploymentcenter.Client/UpdateClient.cs:401`), und `exitCurrentApp: true` ist die
**Vorgabe**. Genau das haben wir bei der Lizenz schon einmal ausgeschlossen: Eine Anwendung mit
offenen Positionen, laufenden CLOB-Aufrufen und einem 10-Sekunden-Bestätigungsdialog darf nicht
mitten im Lauf abgeschossen werden.
Lösbar auf unserer Seite (`exitCurrentApp: false` und danach der bestehende geordnete
Shutdown-Pfad). **Nicht lösbar auf unserer Seite:** Der Agent wartet über `--wait-for-pid` auf das
Ende der Anwendung, aber nur `--wait-timeout` Sekunden — Vorgabe **60**
(`Deploymentcenter.UpdateAgent/Program.cs:70`). `LaunchUpdateAgent` hat für diesen Wert
**keinen Parameter** (`UpdateClient.cs:330-342`). Unser Herunterfahren nimmt sich allein für
`host.StopAsync` bis zu 30 s, dazu Avalonia-Teardown und die Modul-Stopps. Reißt die Frist, tut der
Agent **nichts** — immerhin sicher, aber ohne Rückmeldung und ohne Stellschraube.
**Wunsch an DC:** `waitTimeoutSeconds` in `LaunchUpdateAgent` durchreichen.
**Bis dahin bei uns:** den Agenten selbst starten (eigener `ProcessStartInfo`) statt über
`LaunchUpdateAgent` — dann setzen wir `--wait-timeout` selbst.
### D-2 · Der Lizenzschlüssel wandert in die Kommandozeile · **hoch** · Wo: DC
Der Agent nimmt die Zugangsdaten der Release-Ablage ausschließlich als Argument entgegen:
`--license-key` bzw. `--download-user/--download-password`
(`Deploymentcenter.UpdateAgent/Program.cs:70-79`), und `UpdateClient.LaunchUpdateAgent` hängt sie
ebenso an (`UpdateClient.cs:366-369`). Umgebungsvariablen liest der Agent **an keiner Stelle**.
Auf einem Mehrbenutzersystem zeigt `ps` (bzw. der Task-Manager) die Argumente jedes laufenden
Prozesses. Das ist wörtlich dieselbe Begründung, mit der `UPGRADE.md §5` den `curl`-Aufruf im
Crontab verwirft — an dieser Stelle wird sie nicht angewandt. Für uns kommt hinzu: „keine Secrets
in argv" ist bei PolyTrader ein bereits **umgesetzter** Sicherheitsbefund; diese Integration würde
ihn zurücknehmen.
**Wunsch an DC:** Zugangsdaten zusätzlich über `DC_LICENSE_KEY` / `DC_DOWNLOAD_PASSWORD` oder über
stdin annehmen. Sauberer Workaround auf unserer Seite gibt es nicht.
### D-3 · Das Lizenzurteil ist nicht signiert — Rückschritt gegenüber LicenseLabrador · **mittelhoch** · Wo: DC
`LicenseClient` liest den Status aus reinem JSON (`LicenseClient.cs:191-237`). Der mitgeschickte
`nonce` (`:113`) wird in der Antwort **nie geprüft**, eine Signatur gibt es nicht. LicenseLabrador
verifizierte die Serverantwort per Ed25519 gegen einen einkompilierten Public Key und reflektierte
die Nonce — deshalb konnte niemand ohne den privaten Schlüssel ein „valid" erfinden.
Jetzt trägt allein TLS. Wer DNS oder die `hosts`-Datei umbiegt und ein eigenes Zertifikat in den
Trust Store legt — also der Besitzer der Maschine — kann sich ein `{"status":"valid"}` selbst
ausstellen. Für unser erklärtes Bedrohungsmodell („Schutz gegen einfaches Kopieren auf ein fremdes
System") reicht das; sobald Dritte die Software bekommen, ist es die schwächste Stelle der Kette.
Bemerkenswert: Für **Release-Pakete** führt `UPDATESERVICE_INTEGRATION_GUIDE §6` eine ausführliche
und richtige Begründung, warum dort asymmetrisch signiert werden muss („ein Update wird auf dem
Zielsystem geprüft") — beim Lizenzurteil gilt dasselbe Argument und fehlt die Umsetzung.
Zusatz: Der Client erzwingt **kein** `https://` (`serverBaseUrl` wird ungeprüft übernommen,
`LicenseClient.cs:118`). Das prüfen wir wie beim Heartbeat selbst.
### D-4 · `ILicensePrompt` ist eine tote Naht · **mittel** · Wo: DC (Doku) + unsere Analyse
`LicenseClient` nimmt ein `ILicensePrompt` entgegen und legt es in `_prompt` ab
(`LicenseClient.cs:62,69-72`) — **benutzt wird es nirgends**, im ganzen SDK gibt es keine weitere
Fundstelle. Unsere `ANALYSE-Linux-Portierung.md` (§5.2 und Risiko R6) baut die headless-Story
darauf auf, dass diese Abstraktion den Start auf einem Server ohne Display trägt. Sie tut nichts.
Kein Beinbruch — die Ablauflogik gehört ohnehin zu uns (`LicenseGate` hat sie bereits) —, aber die
Analyse muss korrigiert werden, und die Abstraktion bauen wir selbst.
### D-5 · Reihenfolgefalle beim Zugangsschutz trifft uns in der scharfen Variante · **mittel** · Wo: wir
`UPGRADE.md §16.1` beschreibt für bestehende Produkte „erst ausliefern, dann scharfschalten". Für
**neue** Produkte gilt das ausdrücklich nicht: `/releases/polytrader/` entsteht erst mit dem ersten
Upload und wird beim nächsten `cli/tick.php`-Lauf geschützt. Es gibt also kein Zeitfenster, in dem
sich ein noch ungeschütztes Paket ziehen ließe.
**Der allererste ausgelieferte Build muss die Zugangsdaten bereits mitbringen.** Veröffentlichen wir
zuerst ein Release, dessen Anwendung `credentials` noch nicht übergibt, fällt die Tür hinter ihm zu
und jede Erstinstallation bekommt 401.
Zweitens: Der Schutz beruht auf `.htaccess` und trägt nur unter Apache mit `AllowOverride`. Die
Anzeige „GESCHÜTZT" beweist nichts — nur der **Selbsttest** tut es. Gehört in unsere Abnahme, nicht
in die Zuversicht.
### D-6 · Der Packager würde unsere Secrets mitveröffentlichen · **hoch** · Wo: wir
`pack-and-deploy` verpackt das Publish-Verzeichnis. Bei uns liegt dort:
| Datei | Inhalt | Wie sie dorthin kommt |
|---|---|---|
| `appsettings.Local.json` | **MySQL-Verbindung mit Passwort** | `CopyToOutputDirectory` in `PolyTrader.App.Avalonia.csproj` |
| `master.key` | AES-Master-Key aller at-rest-Secrets | wird neben der Anwendung erwartet (`Program.cs:309`) |
| `openrouter.key`, `server_settings.xml`, `data.db`, `Logs/` | API-Schlüssel, Watchdog-Token, Lizenzschlüssel, Betriebsdaten | Betrieb |
Ohne strikte `excludePatterns` landet all das im Paket und damit auf dem Server. Der Zugangsschutz
macht es nicht harmlos: `UPDATESERVICE §5A/Grenzen` sagt es selbst — dann ist es eben statt
öffentlich „für jeden Lizenznehmer sichtbar". Der Packager warnt inzwischen bei
zugangsdatenverdächtigen Dateien, aber die Warnung ist **keine Sperre**.
Gegenmaßnahme: eigenes Publish-Profil in ein sauberes Verzeichnis, explizite `excludePatterns`,
und als Abnahmekriterium ein Blick in das fertige Archiv.
### D-7 · `BuildInfo.targets` kommt bei unserer Auslieferungsart nicht mit · **mittel** · Wo: DC + wir
`Deploymentcenter.Client.csproj` setzt weder `PackageId` noch `Version` und packt die
`Deploymentcenter.BuildInfo.targets` **nicht** ins NuGet-Paket (kein
`<None Pack="true" PackagePath="build/">`). Der Guide empfiehlt stattdessen einen relativen
`<Import>` ins Nachbar-Repo — genau die Kopplung, die wir bei LicenseLabrador bewusst aufgegeben
haben (Begründung in `lib/nuget/README.md`: PolyTrader muss ohne das Schwester-Repo bauen).
**Wunsch an DC:** `PackageId`/`Version` setzen und die Targets als `build/Deploymentcenter.Client.targets`
mitpacken — dann greift sie beim Paketverweis von selbst.
**Bis dahin bei uns:** die Datei einmalig nach `build/` kopieren, mit Herkunfts- und
Aktualisierungsvermerk.
### D-8 · Wir haben keine Versionsdisziplin · **mittel** · Wo: wir
In keiner `.csproj` steht ein `<Version>`, eine `Directory.Build.props` gibt es nicht. Damit trägt
jede Assembly `1.0.0.0`.
Der Packager prüft `--version` gegen die Hauptassembly und bricht bei Abweichung ab — das ist gut
so, denn der Fehler dahinter ist übel: Wird `1.0.1` als `1.0.2` veröffentlicht, aktualisieren alle
Installationen, melden weiterhin `1.0.1`, halten das Release erneut für neu und aktualisieren bei
**jedem Start** wieder. Ohne zentrale Version ist ein Release entweder unmöglich oder gefährlich.
Muss **vor** dem ersten Paket stehen.
### D-9 · `setup.json` kann unsere Konfiguration nicht schreiben · **mittel** · Wo: wir
Der Installer beherrscht die Formate `json` und `env` (`SETUP_INTEGRATION_GUIDE §3/§4`). Unsere
Betriebseinstellungen liegen in `server_settings.xml` — eigenes XML-Format, das er nicht kennt.
Konsequenz: Entweder verzichtet die Erstinstallation auf jede Vorbelegung, oder der
Deploymentcenter-Block (BaseUrl, Token, Kanal, Source, Lizenzschlüssel) zieht nach
`appsettings.json`. Zweiteres ist ohnehin sauberer — die Anwendung liest bereits
`appsettings.Local.json` über die `IConfiguration`, und nur so kann die Erstinstallation das
Anwendungstoken per `source: "provision"` selbst ziehen, ohne dass jemand ein Token abtippt.
### D-10 · Die Erstinstallation kann den Master-Key nicht lösen · **mittel** · Wo: wir
Der Installer schreibt ausdrücklich **Klartext** und verschlüsselt nichts
(`SETUP_INTEGRATION_GUIDE §3/Dateirechte`). Einen `POLYTRADER_MASTER_KEY` kann er nicht erzeugen.
Eine frisch installierte Instanz hätte also alle Secrets unverschlüsselt liegen, bis jemand von
Hand nachhilft — auf einem Zielsystem im Ausland der wahrscheinlichste Ort für „später".
Optionen: (a) Master-Key als `type: "secret"`-Feld abfragen, (b) PolyTrader erzeugt beim ersten
Start selbst einen und legt ihn mit `0600` ab, (c) über die systemd-Unit als Umgebungsvariable.
**Empfehlung: (b) + (c)**, (a) wäre eine weitere Stelle, an der jemand ein Geheimnis abtippt.
Positiv: Die im Guide geforderte Weiche „Entschlüsselung reicht Klartext unverändert durch" haben
wir bereits (`SecretProtection.Unprotect`). Der Guide warnt zu Recht, sie nicht eines Tages als
tote Altlast zu entfernen — sie gehört als **aktiv genutzt** dokumentiert.
### D-11 · Pfad-Inkonsistenz, die erst beim Installieren auffällt · **klein** · Wo: wir
`ServerSettings.Load("server_settings.xml")` ist **relativ zum Arbeitsverzeichnis**, `master.key`
wird dagegen aus `AppContext.BaseDirectory` gelesen (`Program.cs:309`). Solange man aus dem
Projektordner startet, fällt das nicht auf. Nach einer Installation nach `/opt/polytrader` mit
einer systemd-Unit, deren `WorkingDirectory` woanders zeigt, liest die Anwendung eine andere Datei,
als der Installer geschrieben hat. Vor D-6/L2 klären.
### D-12 · Der bestehende Heartbeat passt nicht — mehr als „Header und URL" · **klein** · Wo: wir
`WatchdogHeartbeatService` sendet an `{base}/api/heartbeat` mit `X-Watchdog-Key`
(`WatchdogHeartbeatService.cs:280,428`). Das Deploymentcenter erwartet
`{base}/api/watchdog/v1/ping` mit `Authorization: Bearer` (alternativ `X-Agent-Token`), Events
gehen nach `/api/watchdog/v1/event`.
`ANALYSE-Linux-Portierung.md §5.2` nennt das „nur Header und Ziel-URL". Tatsächlich kommen dazu:
`version` (sonst lässt sich ein Ausfall keinem Rollout zuordnen), `os`, `group`, das ganze
`checks`-Konzept, `metrics` — und `status: "stopped"`, das die Fehlalarm-Vermeidung beim geordneten
Beenden übernimmt. Unser `metadata`-Feld liest der Server dagegen nicht; es entfällt.
### D-13 · Error-Reporting braucht einen Handler, den es bei uns nicht gibt · **mittel** · Wo: wir
Weder `AppDomain.CurrentDomain.UnhandledException` noch `TaskScheduler.UnobservedTaskException`
kommen im Repo vor. Beim Bau lauern zwei Fallen:
1. **Rückkopplung.** Der Reporter meldet einen Fehlschlag über `TerminalLogger.Error` — was den
nächsten Report auslöst. Der Reporter darf seine eigenen Fehler niemals über den
eskalierenden Pfad melden. (DC drosselt bei 60 Reports/Minute/IP; darauf verlassen wir uns nicht.)
2. **Datenabfluss.** Unsere Fehlermeldungen enthalten Wallet-Adressen, Marktkennungen und Beträge,
der DC-Server steht beim Hoster. Das vorhandene `Sanitize()` des Heartbeats muss auch auf
Meldung **und Stacktrace** des Error-Reports laufen.
### D-14 · Kein Update ohne gültige Lizenz — auch nicht das reparierende · **klein, bewusst** · Wo: Entscheidung
Der Zugangsschutz koppelt Updates an eine gültige Lizenz. Eine Installation mit abgelaufener Lizenz
kann sich also nicht mehr selbst reparieren. Für eigene Systeme belanglos; sobald Kunden im Spiel
sind, ist es eine Entscheidung, keine Nebenwirkung.
### D-16 · `environment` im Fehler-Report ist heimlich ein Auth-Gate · **kleinmittel, überraschend** · Wo: DC (Doku + Fehlermeldung)
Beim Live-Test von D-3 lieferte `/api/errors/v1/report` mit einem funktionierenden,
`bugtracker:report`-berechtigten Token beharrlich `401 unauthorized: "Erwartet wird ein Token mit
dem Recht \"bugtracker:report\""` — obwohl der Token genau dieses Recht trug (per
`/api/tokens/v1/provision` selbst bestätigt).
Ursache: `report.php` liest `environment` aus dem **Request-Body** und reicht es an
`ApiAuth::requireScope($db, 'bugtracker:report', $environment, false)` durch;
`TokenManager::validateToken` lehnt danach ab, wenn `token['environment'] !== 'all' &&
token['environment'] !== $environment` (`TokenManager.php:317`). Unser Token war bei der
Provisionierung auf `environment: "development"` festgelegt, der Report schickte
`"environment": "production"` — Mismatch, Ablehnung.
Zwei Dinge daran sind problematisch:
1. **Kein Dokument erwähnt diese Kopplung.** `bugtracker.md §3` führt `environment` als reine
Metadatum-Angabe für `/report` (Bugtracker), nicht als Zugriffs-Gate. Für `/api/errors/v1/report`
steht es nirgends. Andere Endpunkte (`watchdog:ping`, `watchdog:read/evaluate`,
`license:deactivate`) übergeben `$environment` gar nicht erst an `requireScope` — die Kopplung
gilt also inkonsistent nur für diesen einen Endpunkt.
2. **Die Fehlermeldung nennt die falsche Ursache.** `401` mit `"Token mit dem Recht
bugtracker:report"` legt einen fehlenden Scope nahe. Der tatsächliche Grund (Environment-
Mismatch) taucht in der Antwort nirgends auf — von der Client-Seite aus ist das ohne Blick in den
Serverquelltext nicht diagnostizierbar.
**Für uns gelöst:** Token mit `environment: "all"` neu provisioniert (`dc_sub_109c1a03…`), gilt für
jeden gemeldeten Umgebungswert. **Wunsch an DC:** entweder den wahren Grund in der Fehlerantwort
nennen (z.B. eigener Code `environment_mismatch`), oder die Kopplung ganz aus `/errors/v1/report`
entfernen und `environment` wie bei den anderen Endpunkten rein deskriptiv behandeln.
### D-15 · `Unauthorized` wird nur im statischen Zweig erkannt · **klein** · Wo: DC
`CheckForUpdateAsync` setzt `result.Unauthorized` nur, wenn die statische `latest.json` mit 401
antwortet (`UpdateClient.cs:105-111`). Fehlt die `latest.json`, fällt die Prüfung auf die
**offene** API zurück — eine tote Lizenz sieht dann aus wie „kein Update vorhanden". Für die
Fehlersuche im Feld relevant.
---
## 2. Leitentscheidungen
1. **Das SDK kommt als lokales NuGet-Paket** in `lib/nuget`, wie schon `LicenseLabrador.Client` —
keine Projektreferenz ins Nachbar-Repo. Begründung unverändert: PolyTrader muss ohne das
Schwester-Repo bauen (Build-Server, Zielland-System).
2. **Der Heartbeat bleibt unsere eigene Implementierung.** Der Service ist erprobt (Live-Reload,
Entprellung, TLS-Zwang, No-Throw-Vertrag, 21 Tests); umgestellt werden Endpunkt, Auth und
Payload — nicht die Mechanik.
3. **Kein `Environment.Exit`, nirgends.** Weder bei ungültiger Lizenz (Entscheidung vom 31.07.2026)
noch beim Update (D-1). Ein Update läuft über denselben geordneten Weg wie das Beenden über die
Menüleiste, inklusive Bestätigungsdialog.
4. **Updates werden nie automatisch installiert.** Die Anwendung prüft beim Start, meldet im
Launcher und installiert erst auf ausdrückliche Bestätigung. Auch ein `is_critical`-Release
erzwingt nichts — offene Positionen schlagen jedes Sicherheitsargument.
5. **Revidiert bei der Umsetzung (14.08.2026):** Ursprünglich geplant war, dass der gesamte
Deploymentcenter-Block nach `appsettings.json` zieht. Für Watchdog-Felder (`WatchdogUrl`,
`WatchdogToken`, `WatchdogSource`, `WatchdogInstance`, `WatchdogIntervalSeconds`) blieb es bei
`server_settings.xml`/`ServerSettings` — die bestehende UI (maskierte Eingabe, Live-Reload) deckt
den Bedarf bereits ab, ein zweites Konfigurationssystem dafür wäre unnötig gewesen. Nur Werte
**ohne** bestehende UI-Heimat (ProjectSlug, Channel, Feature-Schalter für D-3/D-4) liegen in
`appsettings.json`/`DeploymentcenterOptions`; Secrets darin (Token, LicenseKey für D-2) in der
gitignorierten `appsettings.Local.json`, zusätzlich über `SecretProtection` verschlüsselbar wo
die bestehende Ablage (`ServerSettings.LicenseKey`) das schon leistet.
6. **Ausgeliefert wird ausschließlich `PolyTrader.App.Avalonia`**, für `win-x64` und `linux-x64`
(Entscheidung Richard, 14.08.2026: WinForms verschwindet demnächst vollständig, L5).
**Folge für diesen Plan:** Lizenzdialog und Startgate werden **nur noch in Avalonia** gebaut,
nicht in beiden Anwendungen. `Licensing/LicenseGate.cs` und `Ui/LicenseDialog.cs` im
WinForms-Projekt werden nicht portiert, sondern ersetzt und gelöscht. Das spart eine komplette
zweite Verdrahtung — und die WinForms-Fassung wäre ohnehin die einzige mit Lizenzprüfung
gewesen, während die tatsächlich benutzte Anwendung ungeprüft läuft.
7. **Watchdog und LicenseLabrador werden vollständig ausgebaut** (Entscheidung Richard,
14.08.2026). Kein Parallelbetrieb, keine Rückfallebene: Nach D-1/D-2 gibt es genau einen
Gegenüber, das Deploymentcenter. Siehe Schnitt **D-6**.
8. **Ein Anwendungstoken je Installation**, per `source: "provision"` beim Setup gezogen und auf
`watchdog:ping` + `bugtracker:report` beschränkt. Ein Master-Token wird **nie** ausgeliefert.
9. **Produkt-Slug ist `polytrader`** (Entscheidung Richard, 14.08.2026). Das alte `pt` aus dem
LicenseLabrador-Admin entfällt mit diesem Umzug ersatzlos.
10. **Der Master-Key wird bei der Erstinstallation erzeugt** (Entscheidung Richard, 14.08.2026),
nicht abgefragt — Einzelheiten in **D-5**.
---
## 3. Umsetzung in Schnitten
### D-0 · Fundament (ohne Serverkontakt) ✅ ERLEDIGT (14.08.2026)
Umgesetzt wie geplant: `Directory.Build.props` mit `<Version>0.1.0</Version>` für alle Projekte,
`Deploymentcenter.Client` 2.5.0 als lokales NuGet-Paket in `lib/nuget` (PackageReference in
`PolyTrader.Core.csproj`, `BuildInfo.targets` kommt jetzt automatisch über `build/` aus dem Paket
D-7 war zu diesem Zeitpunkt bereits vom Deploymentcenter-Team behoben, keine manuelle Übernahme
nötig), `nuget.config`-Mapping um `Deploymentcenter.*` ergänzt, `DeploymentcenterOptions` +
`Deploymentcenter`-Sektion in `appsettings.json` (Secrets in `appsettings.Local.json`, gitignored).
`PolyTrader.Core.BuildInfo.Version` erzeugt sich korrekt bei jedem Build. LicenseLabrador-Paket
bewusst noch nicht entfernt (das ist D-6, erst nach Live-Abnahme).
### D-0-Rest · Zwischenverifikation der Befunde (14.08.2026)
Vor dem Start Richards Hinweis geprüft, dass die Befunde behoben sein sollten: **5 von 6 bestätigt
behoben** (Commit `1967b49`, Deploymentcenter-Team) D-1 (Exit/`--wait-timeout`), D-2 (argv-Key,
jetzt `DC_LICENSE_KEY`-Umgebungsvariable), D-4 (`ILicensePrompt` jetzt über `EnsureLicensedAsync()`
genutzt), D-6 (Packager bricht bei Secrets ab statt nur zu warnen), D-7 (Paket jetzt `IsPackable`,
Version 2.5.0). Als Bonus zusätzlich behoben: D-15 (`Unauthorized` jetzt auch im API-Rückfallzweig
erkannt). **D-3 (unsigniertes Lizenzurteil) bleibt bestätigt offen** vom Deploymentcenter-Team
selbst so benannt („bewusst nicht Teil dieses Commits"), deckt sich mit unserer Einordnung in §6.
### D-1 · Watchdog auf die Deploymentcenter-API umstellen ✅ ERLEDIGT (14.08.2026)
Umgesetzt in [WatchdogHeartbeatService.cs](../../../src/PolyTrader.Core/Services/WatchdogHeartbeatService.cs).
Abweichung von der ursprünglichen Planung: **kein neues Konfigurationsschema.** `ServerSettings`
(XML, Live-Reload, maskierte Token-Eingabe bereits erprobte Mechanik) bleibt die Quelle für
`WatchdogUrl`/`WatchdogToken`/`WatchdogSource`/`WatchdogInstance`/`WatchdogIntervalSeconds`; nur
ihre Bedeutung ändert sich (zeigen jetzt auf das Deploymentcenter statt auf watchdog.mhdf.de).
`DeploymentcenterOptions`/`appsettings.json` wäre hier ein zweites, unnötiges Konfigurationssystem
gewesen die bestehende UI/Masking/Live-Reload-Maschinerie deckt genau denselben Bedarf ab.
`DeploymentcenterOptions` bleibt für D-2/D-3/D-4 reserviert (Werte ohne bestehende UI-Heimat:
ProjectSlug, Channel, Feature-Schalter).
- Endpunkte `…/api/watchdog/v1/ping` und `…/api/watchdog/v1/event`, Header `Authorization: Bearer`
statt `X-Watchdog-Key`. `TryBuildEndpoint` nimmt jetzt einen relativen Pfad statt eines
Aktionsnamens (`PingPath`/`EventPath`-Konstanten).
- Payload ergänzt: `version` (aus `PolyTrader.Core.BuildInfo.Version`, nicht mehr aus der
Assembly-Version stimmt jetzt mit `Directory.Build.props` überein), `os`
(`RuntimeInformation.OSDescription`). Das bisherige `metadata`-Feld entfällt (der Server liest es
nicht, siehe Befund D-12).
- **`checks`-Mechanik neu:** `RegisterCheck(name, Func<(bool ok, string? message)>)` /
`UnregisterCheck(name)` Composition Root registriert die konkreten Prüfungen, der Dienst selbst
kennt ihre Bedeutung nicht. Eine werfende Prüfung meldet sich selbst als fehlgeschlagen statt den
Heartbeat zu verhindern. In `Program.cs` (Avalonia) verdrahtet: `vpn`-Prüfung gegen
`MullvadVpnService.IsConnected`. **`db`/`market_feed`-Prüfungen sind bewusst noch nicht verdrahtet**
die jeweiligen Dienste geben aktuell kein Gesundheitssignal nach außen; das ist ein Fast-Follow,
kein offener Blocker.
- Beim Herunterfahren sendet `StopAsync` jetzt **zwei** Dinge (vorher nur das Event): einen
Heartbeat mit `status:"stopped"` (das ist, was der Evaluator tatsächlich auswertet) **und**
weiterhin das `stopped_graceful`-Event ins Ereignisprotokoll.
- Tests: 8 neue/geänderte Fälle (Endpunkte, Bearer-Header, `checks`-Payload inkl. Fehlerfall),
gesamt 454 Tests grün.
**Live-Abnahme (14.08.2026, gegen den echten Server, Dev-Sub-Token):**
`POST /api/watchdog/v1/ping` mit dem neuen Payload → `state: "up"`, `app_version` korrekt übernommen.
`POST /api/watchdog/v1/event` (`stopped_graceful`) → angenommen. Erneuter Ping mit
`status:"stopped"` → Monitor wechselt zu `state: "stopped"`, `state_changed: true`. Test-Monitor
`polytrader-dev-probe` bleibt im Deploymentcenter stehen (Zustand `stopped`, alarmiert nicht).
### D-2 · Lizenz auf das Deploymentcenter umstellen ✅ ERLEDIGT (15.08.2026)
Umgesetzt in [Licensing/LicenseGate.cs](../../../src/PolyTrader.App.Avalonia/Licensing/LicenseGate.cs),
[Licensing/LicenseCli.cs](../../../src/PolyTrader.App.Avalonia/Licensing/LicenseCli.cs) und den neuen
Lizenz-Schaltflächen in [SettingsWindow.axaml(.cs)](../../../src/PolyTrader.App.Avalonia/Views/SettingsWindow.axaml).
Zwei Abweichungen von der ursprünglichen Planung, beide vereinfachend:
- **Kein eigenes `ILicensePrompt`, kein eigener modaler Lizenzdialog.** Für die GUI wird die
bereits vorhandene, erprobte Settings-Oberfläche wiederverwendet — dieselbe maskierte
Prompt/Save-Mechanik wie beim Watchdog-Token (`btnLicenseKey` = „Lizenzschlüssel setzen …",
speichert verschlüsselbar in `server_settings.xml`) plus ein neuer `btnCheckLicense` = „Lizenz
jetzt prüfen", der den gespeicherten Schlüssel gegen den Server validiert und Status + Hardware-ID
in einem Info-Dialog anzeigt. Ein zusätzliches, eigenes Dialogfenster nur für die Lizenz wäre eine
zweite, redundante Eingabe-Oberfläche gewesen. Für `--headless`/CLI genügt die vom SDK
mitgelieferte `ConsoleLicensePrompt` (wird hier gar nicht gebraucht, siehe nächster Punkt) —
eine eigene Fassung hätte keinen Mehrwert geboten.
Grund für den Verzicht auf einen echten Avalonia-Dialog **vor** dem Hauptfenster: Avalonias
Startreihenfolge (`AppBuilder.Setup()` darf pro Prozess nur einmal laufen) macht einen zweiten,
vorgeschalteten Dialog vor `StartWithClassicDesktopLifetime` unnötig fragil; ein im laufenden
Fenster erreichbarer Button ist die robustere Lösung und deckt denselben Bedarf.
- **Modulentscheidung läuft still, ohne Dialog, vor dem Host-Aufbau.** `LicenseGate.CheckUsable`
validiert den in `server_settings.xml` gespeicherten Schlüssel synchron (wie einst die
WinForms-Fassung) und entscheidet, welche Module überhaupt registriert werden **nicht** über
`EnsureLicensedAsync`/`ILicensePrompt` des SDK (die interaktive Korrektur läuft über die
Settings-UI, s.o., nicht über eine erneute Abfrage an dieser Stelle). Eine per Dialog neu
eingetragene Lizenz wirkt für Module deshalb **erst beim nächsten Start** bewusst, konsistent
mit der Entscheidung vom 31.07.2026 (keine Laufzeit-Revalidierung).
- `DeploymentcenterOptions` wird jetzt **vor** dem Host-Aufbau eigenständig aus
`appsettings.json`/`appsettings.Local.json` geladen (`Program.LoadDeploymentcenterOptions`), weil
die Modulliste schon vor dem eigentlichen `IHost` feststehen muss. Dieselbe Instanz wird
anschließend auch reguär in die DI registriert.
- **`--smoke-ui` bewusst ausgenommen:** `enforceLicense: !smoke` — die Konstruktionsprüfung soll auf
einem (nie lizenzierten) Build-Server weiterhin ALLE Modul-Fenster testen, nicht nur die
Core-Shell. Verifiziert: `--smoke-ui` konstruiert weiterhin alle 4 Modul-Fenster plus das
Einstellungsfenster mit den neuen Lizenz-Feldern (7 statt 6 Settings-Abschnitte).
- TLS wird — anders als im SDK (Befund D-3) — im Gate selbst erzwungen, exakt wie beim Watchdog.
- CLI-Schalter `--license-status`, `--license-set-key <schlüssel>`, `--license-deactivate` nach der
Konvention aus LICENSE_INTEGRATION_GUIDE §4. `--license-deactivate` verlangt `DC_SHARED_KEY` aus
der Umgebung (Administrationsrechner) und verweigert sich mit klarer Meldung ohne ihn — der
Shared Key gehört nicht in die ausgelieferte Anwendung.
- **Keine eigenen Unit-Tests angelegt** (wie schon bei der WinForms-Fassung dokumentiert
begründet): `PolyTrader.Tests` referenziert `PolyTrader.App.Avalonia` nicht, der Gate ist dünner
Leim über dem SDK. Abdeckung über `--smoke-ui` (Konstruktion) und die Live-Verifikation unten.
**Live-Verifikation (15.08.2026, echter Server, Dev-Lizenz mit 9999 Aktivierungen von Richard):**
```
> PolyTrader.App.Avalonia.exe --license-set-key LLAB2-12345-67890-ABCDE-FGHIJ
valid: License is valid
Lizenz gültig und in server_settings.xml gespeichert (Klartext).
> PolyTrader.App.Avalonia.exe --license-status
HWID v2: 2:win:a765bd47c2458fd518a7a1ad8a9d1d4b654c91085dca4667f33e7cad4427bf93 (machine-guid)
Produkt: polytrader
Server: https://dc.mhdf.de
Cache: valid
Offline noch bis: 2026-08-18 07:56:54Z
Gespeicherter Schlüssel (server_settings.xml): gesetzt (Klartext)
```
Damit ist derselbe Codepfad geprüft, den `LicenseGate.CheckUsable` beim Start benutzt
(`LicenseClient.ValidateAsync`) nicht nur ein isolierter Testaufruf. Die volle GUI/`--headless`
wurde **bewusst nicht** gestartet, um keine echten Handelsverbindungen (Polymarket-WSS,
Alchemy-Listener) aus dieser Sitzung heraus zu öffnen; das bleibt Richards manuelle Abnahme.
`max_activations` war bereits großzügig gesetzt (Richards Hinweis), keine Admin-Änderung nötig.
### D-3 · Fehler-Reporting ✅ ERLEDIGT (17.08.2026)
Umgesetzt in [DeploymentcenterErrorReporter.cs](../../../src/PolyTrader.Core/Services/DeploymentcenterErrorReporter.cs)
und [SecretRedactor.cs](../../../src/PolyTrader.Core/Security/SecretRedactor.cs) (die
Secret-Muster-Prüfung aus `WatchdogHeartbeatService.Sanitize` wurde dorthin ausgelagert, damit
Heartbeat-Meldung — kurz, einzeilig — und Stacktrace — lang, mehrzeilig — dieselbe Musterprüfung,
aber unterschiedliche Kürzungsregeln bekommen, ohne Code zu verdoppeln).
- Drei Quellen wie geplant: `AppDomain.UnhandledException` (`level: "fatal"`, synchron mit 3-s-Frist
gesendet, da der Prozess unmittelbar danach stirbt), `TaskScheduler.UnobservedTaskException`
(`"warning"`, `SetObserved()` verhindert einen Prozessabbruch durch die schon behandelte Ausnahme),
`TerminalLogger`-Fehler (`"error"`, Fire-and-forget wie beim Heartbeat).
- **No-Throw + keine Rückkopplung** wie gefordert: eigene Sendefehler laufen ausschließlich über
`TerminalLogger.Warning` (niemals `.Error` sonst würde der Reporter sich selbst erneut auslösen,
da er genau auf `.Error` hört).
- Eigene Ratenbegrenzung: 20 Meldungen/Minute (gleitendes Fenster, kein Hintergrund-Timer), deutlich
unter dem Server-Limit von 60/Minute/IP Absicht ist, lokal zu drosseln, bevor eine
Fehlerschleife den Server oder andere Dienste auf derselben IP überhaupt erreicht.
- `Sanitize` (Nachricht, ≤200 Zeichen, einzeilig) und die neue `SanitizeStackTrace` (≤4000 Zeichen,
**mehrzeilig bleibt erhalten** ein einzeiliger Stacktrace wäre zum Debuggen wertlos) laufen
beide vor jedem Versand.
- **Korrektur gegenüber der Planung:** kein `client_ref`/Idempotenz-Schlüssel implementiert anders
als beim Bugtracker-`/report` dedupliziert `/api/errors/v1/report` bereits serverseitig über einen
inhaltsbasierten Gruppenschlüssel (bestätigt im Live-Test unten, `occurrence_count` stieg ohne
jedes Zutun). Ein zusätzlicher `client_ref` hätte hier keinen Zweck gehabt.
- Registrierung als `BackgroundService` wie `WatchdogHeartbeatService` (rein ereignisgesteuert,
`ExecuteAsync` wartet nur auf Abbruch) konsistent mit dem etablierten Muster, obwohl kein
eigener Takt gebraucht wird.
- Konfiguration über `DeploymentcenterOptions.Token`/`.ErrorReportingEnabled` (aus
`appsettings.Local.json`) anders als beim Watchdog gibt es hierfür keine bestehende
UI-Heimat in `ServerSettings`, passt also zur in D-1 begründeten Aufteilung.
- 14 neue Tests (Payload, Auth-Header, Secrets aus Nachricht UND Stacktrace, Ratenbegrenzung,
alle drei Quellen inkl. `internal` gemachter Handler-Methoden für direkte Testbarkeit ohne echte
Prozessabstürze zu provozieren). Gesamt 468 Tests grün.
**Live-Verifikation (17.08.2026, echter Server):** Zwei identische Meldungen über
`/api/errors/v1/report` mit dem exakten Payload-Format des neuen Codes → erste Meldung
`is_new: true, occurrence_count: 1`, zweite `is_new: false, occurrence_count: 2` serverseitige
Deduplizierung bestätigt.
**Nebenbefund beim Live-Test: D-16** (neu, siehe §1) das `environment`-Feld im Report-Body ist
ein undokumentiertes Auth-Gate gegen die Provisionierung des Tokens; die Fehlermeldung bei einem
Mismatch nennt fälschlich einen fehlenden Scope. Für uns gelöst durch Neuprovisionierung mit
`environment: "all"` (Token in `appsettings.Local.json` aktualisiert, betrifft auch den
Watchdog-Token-Kommentar dort).
---
### D-0 · Fundament (ohne Serverkontakt) — ursprüngliche Planung
- `Directory.Build.props` mit **einer** `<Version>` für alle Projekte (D-8).
- `Deploymentcenter.BuildInfo.targets` nach `build/` übernehmen und einbinden; `BuildInfo.Version`
ersetzt das heutige `Assembly.GetName().Version` im Heartbeat (D-7).
- SDK paketieren: `dotnet pack Deploymentcenter.Client -c Release -o lib/nuget`; `nuget.config` um
das Quellen-Mapping `Deploymentcenter.*` → `local` ergänzen; `LicenseLabrador.Client` samt
`.nupkg` entfernen.
- Neuer Konfigurationsabschnitt `Deploymentcenter` in `appsettings.json` + `DeploymentcenterOptions`
(BaseUrl, ProjectSlug, Channel, Token, LicenseKey, Watchdog-Source/-Instance/-Intervall,
Schalter für Heartbeat / Fehlermeldungen / Update-Prüfung).
**Abnahme:** Solution baut, Tests grün, `BuildInfo.Version` stimmt mit `<Version>` überein,
`lib/nuget` enthält nur noch das DC-Paket.
### D-1 · Watchdog auf die Deploymentcenter-API umstellen — ursprüngliche Planung
- Endpunkte `…/api/watchdog/v1/ping` und `…/api/watchdog/v1/event`, Header
`Authorization: Bearer`. TLS-Zwang und Loopback-Ausnahme bleiben.
- Payload ergänzen: `version` (aus `BuildInfo`), `os`, `group`.
- `checks` füllen — das ist der eigentliche Gewinn gegenüber heute: `db` (MySQL erreichbar),
`market_feed` (Alter des letzten Ticks), `vpn` (Mullvad-Zustand), `modules` (laufen die aktiven
Module). Ein `ok`-Heartbeat mit fehlschlagender Prüfung wird serverseitig auf `warning` gestuft.
- `metrics` schlank halten (Jobs in der Warteschlange, offene Orders als **Anzahl**). Weiterhin
**keine** Kontostände, Wallet-Adressen oder Trader-Namen.
- Beim Herunterfahren `status: "stopped"` senden (zusätzlich zum bestehenden
`stopped_graceful`-Event) — das ist der vom Evaluator ausgewertete Weg.
- Konfiguration aus `DeploymentcenterOptions`; die alten `Watchdog*`-Felder aus `ServerSettings`
entfernen (mit einmaliger Übernahme beim Start, damit niemand neu tippt).
- `WatchdogHeartbeatTests` nachziehen.
**Abnahme:** Monitor erscheint auf `dc.mhdf.de` mit korrekter `version`; Beenden über die
Menüleiste → Monitor `stopped`, **kein** Alarm; Prozess abschießen → nach 4× Intervall genau **ein**
Down-Alarm. Voraussetzung, die vorher zu prüfen ist: `cli/tick.php` läuft dort tatsächlich als
Cron — ohne ihn sind alle Monitorzustände wertlos (`/api/health` → `checks.evaluator.ok`).
### D-2 · Lizenz auf das Deploymentcenter umstellen — ursprüngliche Planung
- `LicenseGate` auf `Deploymentcenter.Client.LicenseClient` umbauen:
`ValidateAsync(slug, key, baseUrl, appVersion)`, Ergebnis `LicenseValidationResult`.
`LicenseClient.DefaultAppVersion = BuildInfo.Version` einmal beim Start.
- Statusabbildung: `IsValid` → voller Start; **`IsTransient` → weiterlaufen und nur warnen**
(Netzstörung ist kein Urteil); alles andere → eingeschränkter Modus ohne Module.
`https://` selbst erzwingen (D-3).
- Eigene Prompt-Abstraktion bauen (D-4): Avalonia-Dialog für den Desktop, Konsolenvariante für
`--headless`. Der Dialog zeigt Hardware-ID (kopierbar), Status und Offline-Restlaufzeit.
- **Gate und Dialog entstehen im Avalonia-Projekt** — dort läuft heute gar keine Prüfung, und die
WinForms-Fassung wird nicht mitgezogen (Leitentscheidung 6). `Licensing/LicenseGate.cs` und
`Ui/LicenseDialog.cs` werden mit D-6 gelöscht, nicht portiert.
- CLI-Schalter `--license-status`, `--license-set-key`, `--license-deactivate` nach der Konvention
des Guides.
**Abnahme:** gültige Lizenz → alle Module; Netz getrennt → `valid_offline` mit sichtbarer
Restlaufzeit; Schlüssel entfernt → nur Core-Shell, kein Absturz; `--headless` ohne Display
blockiert nicht.
**Vor dem ersten echten Start im DC einstellen:** Produkt `polytrader` mit großzügigem
`max_activations` (ein Windows-Neuinstall erzeugt eine neue MachineGuid = neuer Platz) und
`default_cache_ttl_hours` als Offline-Kulanz für eigene Maschinen.
> **Ein Aktivierungsplatz wird beim ersten echten Start verbraucht.** Die Prüfung deshalb nicht
> „mal eben" von einer Entwicklermaschine aus ausprobieren, sondern dort, wo die Installation
> hingehört — so wie beim letzten Mal gehandhabt.
### D-3 · Fehler-Reporting — ursprüngliche Planung
- `DeploymentcenterErrorReporter` in `PolyTrader.Core`: `POST /api/errors/v1/report`, Scope
`bugtracker:report`.
- Quellen: `AppDomain.UnhandledException` (`level: "fatal"`),
`TaskScheduler.UnobservedTaskException` (`warning`), `TerminalLogger`-Fehler (`error`).
- Pflichtmerkmale: No-Throw, keine Rückkopplung (D-13), eigene Ratenbegrenzung,
`Sanitize()` auf Meldung **und** Stacktrace, abschaltbar über die Konfiguration.
- `client_ref` je Prozessstart+Fehlergruppe setzen, damit ein Neuversuch kein Duplikat anlegt.
- Serverseitig Ignore-Regeln für bekanntes Rauschen anlegen (Polymarket-429, VPN-Reconnect) —
jeweils **mit** Alarmschwelle, denn der Zähler ist der eigentliche Zweck.
**Abnahme:** erzwungener Testfehler erscheint als Gruppe; zweites Auftreten erhöht
`occurrence_count`, statt ein zweites Item anzulegen; bei nicht erreichbarem DC läuft PolyTrader
unverändert weiter und erzeugt **keine** Fehlerschleife.
### D-4 · Auslieferung (UpdateService) ✅ VOLLSTÄNDIG ERLEDIGT (20.08.2026)
**Umgesetzt (Schritte 1 und 3 der ursprünglichen Planung):**
- Update-Prüfung beim Start in [LauncherWindow.axaml(.cs)](../../../src/PolyTrader.App.Avalonia/Views/LauncherWindow.axaml):
fire-and-forget, nur aktiv bei `Deploymentcenter:UpdateCheckEnabled` (Vorgabe `false`), nutzt
`ReleaseCredentials.FromLicenseKey(...)` mit dem entschlüsselten `ServerSettings.LicenseKey`.
Ein gefundenes Update erscheint als Banner (Version, Changelog, `is_critical`-Hinweis) mit
„Jetzt installieren …" und „Später". **Installiert nie automatisch** — deckt sich mit
Leitentscheidung 4. `result.Unauthorized` wird erkannt und bewusst still behandelt (die
Lizenzprüfung aus D-2 hat eine tote Lizenz bereits gemeldet, ein zweiter Hinweis wäre Rauschen).
- Installation nach Bestätigungsdialog über `UpdateClient.LaunchUpdateAgent(..., exitCurrentApp:
false, waitForCurrentProcess: true, waitTimeoutSeconds: 40, licenseKey: ...)`, danach
`_uiHost.RequestShutdown()` — **derselbe** geordnete Weg wie „Beenden" über die Menüleiste,
inklusive der 10-Sekunden-Sicherheitsabfrage. Kein Sonderpfad, der die Bestätigung umgeht.
- `deploy/packager.config.example.json` (Schritt 1) mit den `excludePatterns`/`preservePatterns`
aus der Planung, plus Nachtrag `*.pfx/*.key/*.pem/*.p12` (der Packager selbst prüft seit
Version 2.5.0 zusätzlich Dateiname UND Inhalt, siehe D-6 der Befunde — diese Liste ist die
erste, nicht die einzige Verteidigungslinie). `deploy/packager.config.json` (echte FTP-Zugangsdaten
+ `updateservice:publish`-Token) neu in `.gitignore`.
- **Live geprüft (HTTP-Vertrag, per curl):**
`GET /api/updateservice/v1/check?product=polytrader&version=0.1.0&channel=prod` →
`update_available: false, latest_version: "0.1.0"` — bestätigt, dass für `polytrader` aktuell
**kein** Release auf dem Server liegt (die `2.0.1`-Zeile in `sql/schema.sql` ist Beispiel-Seed-Daten
aus dem Deploymentcenter-Repo, nicht live). Der Code-Pfad selbst nutzt dieselbe SDK-Methode, die
bereits in D-1/D-2/D-3 erprobt ist.
**Nachgezogen (20.08.2026, auf Richards ausdrücklichen Wunsch — „lass uns einen Win und Linux
Release erstellen und hochladen"):**
- Signierschlüssel serverseitig bestätigt: `GET /api/updateservice/v1/pubkey` liefert einen
RSA-Key (Fingerprint `ed99a38d…`) — die zuvor offene Checkliste-Voraussetzung war in Wahrheit
schon erfüllt.
- `dotnet publish -c Release --self-contained true` für `win-x64` (124 MB) und `linux-x64`
(120 MB) in saubere `/tmp`-Verzeichnisse, **nicht** das Debug-Output. Self-contained bewusst
gewählt, weil der Zweck genau der Erstinstallations-Test auf einem frischen System ist, das
nicht zwingend eine .NET-Laufzeit mitbringt.
- **Fund dabei:** `appsettings.Local.json` (mein echtes lokales MySQL-Passwort + Dev-Token) landete
im Publish-Output — die `CopyToOutputDirectory`-Regel in der `.csproj`, die genau für diesen
Fall in D-6 der Befunde und im eigenen `packager.config.example.json`-Kommentar dokumentiert ist.
Vor jedem weiteren Schritt von Hand entfernt, dann zusätzlich mit einem eigenen Grep nach
gefüllten `Password=`-Feldern, `dc_master_…`/`dc_sub_…`-Tokens und privaten Schlüsseln über beide
Verzeichnisse geprüft (leer). Der Packager selbst hätte es beim `--allow-secrets`-losen Lauf
ohnehin blockiert — das war die zweite, nicht die einzige Verteidigungslinie.
- **`deploy/packager.config.json`** (gitignored) angelegt. FTP-Zugangsdaten kamen auf Richards
Anweisung aus `Deploymentcenter/scripts/deploy_config.json` (dieselbe Hosting-Zugangsdaten, mit
denen auch das Deploymentcenter selbst deployt wird). **Remote-Pfad per read-only FTPS-Listing
verifiziert statt geraten:** `/releases/` liegt als eigenständiges Verzeichnis auf FTP-Root-Ebene
(nicht unter `/public_html/releases/`, wie das Beispiel im UPDATESERVICE-Guide nahelegt) —
bestätigt durch die dort bereits vorhandenen Verzeichnisse `clawddotnet/` und `predictalytics/`
echter Nachbarprojekte. `ftpRemoteBaseDir: "/releases"` entsprechend gesetzt.
- Ein `updateservice:publish`-Token separat provisioniert (`environment: "all"`, wie schon bei D-3
gelernt) — nicht das Dev-Token aus `appsettings.Local.json` wiederverwendet, das hat nur
`watchdog:ping`/`bugtracker:report`.
- **Beide Releases veröffentlicht** über `pack-and-deploy`: `polytrader 0.1.0 (prod, win-x64)` und
`polytrader 0.1.0 (prod, linux-x64)`. Versionsgegenprobe lief automatisch durch (Assembly-Version
stimmte mit `--version` überein). `appsettings.json` korrekt als „GESCHÜTZT" (preservePatterns)
markiert, `*.pdb` je Projekt korrekt ausgeschlossen.
**Live-Abnahme (20.08.2026):**
- [x] `GET /api/updateservice/v1/check?product=polytrader&version=0.0.0&channel=prod&platform=win-x64`
**und** `...&platform=linux-x64` → beide `update_available: true, latest_version: "0.1.0"`.
- [x] Beide Antworten enthalten ein gefülltes `manifest_signature`-Feld — signiert.
- [x] Zugangsschutz: **nicht sofort aktiv** — `curl -I .../package.tar.gz` lieferte direkt nach dem
Publish noch `200` statt `401`. Deckt sich mit der Dokumentation: für ein *neues* Produkt
entsteht `/releases/polytrader/` erst mit dem ersten Upload, der Schutz erst mit dem
nächsten `cli/tick.php`-Durchlauf. Per Hintergrund-Poll auf `401` gewartet statt einfach
anzunehmen, dass er greift — nach rund zwei Minuten aktiv geworden.
- [x] **Vollständige Selbsttest-Sequenz danach grün:** ohne Zugangsdaten → `401` (beide
Plattformen); mit dem Dev-Lizenzschlüssel als Basic-Auth (`lic_<sha256-präfix>:<schlüssel>`)
→ `200`; `.htpasswd` direkt abgerufen → `403`.
Reihenfolge ist hier bindend — siehe D-5.
**Ursprüngliche Planung (jetzt umgesetzt, s.o.):**
1. `packager.config.json` mit strikten `excludePatterns` (`appsettings.Local.json`, `master.key`,
`openrouter.key`, `server_settings.xml`, `*.db`, `Logs/**`, `*.pdb`) und `preservePatterns`
(`appsettings.json`).
2. Sauberes Publish-Verzeichnis je Plattform (`win-x64`, `linux-x64`), **nicht** das Debug-Output.
3. In der Anwendung: Update-Prüfung beim Start mit
`credentials: ReleaseCredentials.FromLicenseKey(...)`, Anzeige im Launcher, Installation nur auf
Bestätigung und über den geordneten Shutdown-Pfad (D-1).
4. **Erst danach** das erste Release veröffentlichen.
5. Zugangsschutz erzeugen und **Selbsttest** laufen lassen; zusätzlich von Hand:
`curl -I …/releases/polytrader/prod/win-x64/<version>/package.tar.gz` → 401.
**Abnahme:** Update auf einem Testsystem behält `appsettings.Local.json`, `server_settings.xml`,
`master.key` und `data.db`; eine im neuen Release entfallene DLL ist verschwunden; ein Abbruch
mitten im Anwenden hinterlässt eine lauffähige Installation (Rollback); und der Blick ins Archiv:
```bash
tar -tzf package.tar.gz | grep -Ei 'local|master\.key|openrouter|settings\.xml|\.db$'
```
Diese Ausgabe muss **leer** sein.
### D-5 · Erstinstallation (`setup.json`) ✅ Code-seitig erledigt (17.08.2026), Live-Abnahme offen
**Umgesetzt:**
- [setup.json](../../../src/PolyTrader.App.Avalonia/setup.json) im App-Projekt (per `CopyToOutputDirectory`
im Publish-Paket) mit zwei Zielen: `appsettings.json` (Deploymentcenter-Block: `BaseUrl` via
`detect:baseurl`, `ProjectSlug` via `detect:project`, `Channel`, `Token` via `provision` mit den
Scopes `watchdog:ping`+`bugtracker:report`) und `appsettings.Local.json` (`Database:MySqlConnectionString`
als `secret`).
**Korrektur gegenüber der ursprünglichen Planung:** kein `Watchdog:Source`-Feld — die
Watchdog-Konfiguration liegt seit D-1 bewusst in `server_settings.xml` (maskierte UI,
Live-Reload), und `setup.json` kann nur `json`/`env`-Ziele schreiben, kein XML. Aktivierung von
Watchdog **und** Eintragen des Lizenzschlüssels bleiben deshalb bewusste Post-Install-Schritte
über das Einstellungsfenster — beide sind seit D-1/D-2 genau dafür gebaut (maskierte
Eingabe-Buttons, sofort wirksam ohne Neustart). Im `setup.json`-Kommentar dokumentiert, damit das
nicht als vergessenes Feld missverstanden wird.
- **Master-Key-Erzeugung (D-10)** in [MasterKeyResolver.cs](../../../src/PolyTrader.Core/Security/MasterKeyResolver.cs)
(neu, testbar in `PolyTrader.Core` statt direkt in `Program.cs` verdrahtet) + gemeinsamer
[FilePermissions.cs](../../../src/PolyTrader.Core/Security/FilePermissions.cs)-Helfer (ersetzt die
bisher in `SettingsWindow.axaml.cs` duplizierte `RestrictToOwner`-Logik). Reihenfolge:
Umgebungsvariable → vorhandene Datei → **nur wenn beides fehlt** neu erzeugen. 8 Tests, u.a.
explizit „ein zweiter/dritter Aufruf erzeugt niemals neu" und „ein fehlgeschlagener Schreibversuch
wirft nie, sondern warnt". Live geprüft: frisches Verzeichnis ohne `master.key` →
`--smoke-ui`-Lauf erzeugt die Datei mit der erwarteten Log-Meldung, `SecretProtection` aktiviert
sich sofort danach in derselben Sitzung.
- [polytrader.service](../../../deploy/polytrader.service) (systemd-Unit-Vorlage) löst **D-11**: setzt
`WorkingDirectory` ausdrücklich auf das Installationsverzeichnis, mit Begründung im Kommentar
(sonst laden `server_settings.xml`, relativ zum Arbeitsverzeichnis, und `master.key`, relativ zu
`AppContext.BaseDirectory`, aus unterschiedlichen Orten). `SIGTERM` mit 45 s Frist für den
geordneten Shutdown-Pfad statt `SIGKILL`.
**Bewusst nicht gemacht:**
- **Installationskonto im DC anlegen** — das Anlegen eines Kontos mit Zugangsdaten ist eine
Handlung, die grundsätzlich beim Menschen bleibt, nicht beim Agenten. Richard legt es im WebUI
unter *System → 👤 Benutzer* an (Rolle `installer`, siehe SETUP_INTEGRATION_GUIDE §2).
- **Volle Live-Abnahme** (`update-agent --action install` auf einem echten System) — braucht ein
veröffentlichtes Release (D-4, noch offen, siehe dort) und das Installationskonto von oben. Ohne
beides lässt sich nur die App-seitige Hälfte prüfen, was hiermit geschehen ist.
Ursprünglich geplant, jetzt überholt:
- **Master-Key beim ersten Start selbst erzeugen** (Entscheidung Richard, 14.08.2026):
`ConfigureSecretProtection` legt, wenn weder `POLYTRADER_MASTER_KEY` noch `master.key` vorliegen,
einen kryptografisch zufälligen Schlüssel an und schreibt ihn mit `File.SetUnixFileMode(…,
UserRead | UserWrite)` — sonst entsteht er unter Linux mit `644`, also world-readable.
Drei Punkte, die dabei nicht untergehen dürfen:
- **Nur erzeugen, wenn wirklich keiner da ist.** Ein versehentliches Überschreiben macht jedes
bereits verschlüsselte Secret in der Datenbank unlesbar — das ist der einzige wirklich
unumkehrbare Fehler in diesem ganzen Plan. Erzeugung deshalb ausschließlich beim Nichtvorhandensein
der Datei, mit deutlichem Log-Eintrag und einem Hinweis, den Schlüssel zu sichern.
- **Ort ist `AppContext.BaseDirectory`** — dieselbe Stelle, an der er heute gelesen wird. Zusammen
mit D-11 klären, dass die Anwendung nach der Installation dieselbe Datei findet.
- **`master.key` gehört in `excludePatterns`** (D-6) und darf nie in ein Paket geraten.
- Installationskonto im DC anlegen (Rolle `installer`, **kein** Admin).
- systemd-Unit schreiben — der Installer legt bewusst keinen Dienst an. Dabei D-11 auflösen
(`WorkingDirectory` = Installationsverzeichnis oder absolute Pfade).
**Abnahme:** `update-agent --action install` auf einem frischen Linux-System führt zu einer
lauffähigen Installation, ohne dass eine Datei von Hand bearbeitet wird; `master.key` entsteht mit
`600`; ein zweiter Start erzeugt **keinen** neuen Schlüssel.
### D-6 · Watchdog und LicenseLabrador ausbauen ✅ Code-seitig erledigt (22.08.2026), Serverabschaltung offen
Entscheidung Richard (14.08.2026): kein Parallelbetrieb. Läuft **nach** D-1 und D-2, damit zu
keinem Zeitpunkt beides fehlt. Umgesetzt im Frühjahrsputz vom 22.08.2026, gemeinsam mit dem
WinForms-Ausbau (P11/L5) — beide Schnitte betrafen dieselben Dateien.
- [x] `LicenseLabrador.Client` aus `lib/nuget` entfernt; `PolyTrader.App.csproj` existiert nicht
mehr. Quellen-Mapping in `NuGet.Config` trägt nur noch `Deploymentcenter.*`, `lib/nuget/README.md`
beschreibt jetzt das Deploymentcenter-SDK.
- [x] `Licensing/LicenseGate.cs` und `Ui/LicenseDialog.cs` gelöscht — ersetzt durch die
Avalonia-Fassung aus D-2.
- [x] Beide Altpläne als **abgelöst** gekennzeichnet;
[ANALYSE-Linux-Portierung.md](../ANALYSE-Linux-Portierung.md) auf Revision 6 gezogen (P11/L5
erledigt), Leitfaden-Abschnitte B und C aufgelöst.
- [x] **Abweichung vom Plan, bewusst:** Die `Watchdog*`-Felder in `ServerSettings` bleiben stehen.
Die Planzeile „alte Watchdog-Felder entfernen" stammt aus der Zeit vor D-1 — D-1 hat die Felder
jedoch **umgewidmet** (sie zeigen jetzt auf die Deploymentcenter-API) statt sie zu ersetzen. Sie
werden von `WatchdogHeartbeatService`, `DeploymentcenterOptions`, dem Einstellungsfenster und den
Tests aktiv benutzt; ein Entfernen wäre ein Rückschritt gewesen.
- [ ] **Serverseitig offen:** `watchdog.mhdf.de` und `license.mhdf.de` abschalten, sobald die
Live-Abnahme steht. Damit erledigen sich die beiden offenen Auflagen aus dem Altplan (Secrets
rotieren, UTC/`NOW()`) von selbst — **nicht** vorher abschalten, sonst fehlt die Rückfallebene
während der Abnahme.
**Abnahme-Kriterium erfüllt:** Die Solution baut ohne LicenseLabrador-Paket (0 Fehler, 476 Tests
grün), und `grep -ri licenselabrador` findet im Code nur noch historische Erwähnungen in
Kommentaren und Plandokumenten.
**Abnahme:** Die Solution baut ohne LicenseLabrador-Paket, `grep -ri licenselabrador` findet im
Code nur noch historische Erwähnungen in Plandokumenten, und `server_settings.xml` enthält keine
Watchdog-Felder mehr.
> **Reihenfolge:** Die alten Dienste erst abschalten, wenn D-1 und D-2 **live** abgenommen sind —
> nicht, wenn sie gebaut sind.
---
## 4. Reihenfolge, Abhängigkeiten, Aufwand
| Schritt | Inhalt | hängt an | Aufwand | Stand |
|---|---|---|---|---|
| D-0 | Fundament: Version, SDK-Paket, Optionen | — | 0,5 PT | ✅ 14.08.2026 |
| D-1 | Watchdog umstellen | D-0 | 0,51 PT | ✅ 14.08.2026, live verifiziert |
| D-2 | Lizenz umstellen + Prompt-Naht (nur Avalonia) | D-0 | 1 PT | ✅ 15.08.2026, live verifiziert |
| D-3 | Fehler-Reporting | D-0 | 0,51 PT | ✅ 17.08.2026, live verifiziert |
| D-4 | Auslieferung | D-0, D-2 (Schlüssel für den Paketabruf) | 12 PT | ✅ 20.08.2026, `polytrader 0.1.0` live für win-x64+linux-x64, signiert, live geprüft |
| D-5 | Erstinstallation + Master-Key-Erzeugung | D-4 | 0,51 PT | Code ✅ 17.08.2026; Release liegt jetzt vor — Live-Install fehlt nur noch das Installationskonto (Richard) |
| D-6 | Watchdog/LicenseLabrador ausbauen | Live-Abnahme von D-1 und D-2 | 0,5 PT | offen — beide Vorbedingungen live ✅, wartet auf Richards manuelle GUI-Abnahme |
**Summe 47 PT.** D-1, D-2 und D-3 sind untereinander unabhängig und können in beliebiger
Reihenfolge laufen. Zwei Reihenfolgen sind dagegen bindend:
- **D-2 vor D-4** — ohne Lizenzschlüssel im Update-Weg sperrt uns der Zugangsschutz mit dem ersten
Release aus (Befund D-5).
- **D-6 nach der Live-Abnahme**, nicht nach dem Bau.
### Serverseitige Voraussetzungen (vor D-1)
- [x] Projekt `polytrader` im Deploymentcenter angelegt (Slug festgelegt, id 2, aus dem Seed-Bestand)
- [x] `cli/tick.php` läuft als Cron — `/api/health` meldet `checks.evaluator.ok: true` (14.08.2026 geprüft)
- [x] Master-Token vorhanden; Dev-Sub-Token für `polytrader` provisioniert (Scopes `watchdog:ping`,
`bugtracker:report`) — für den Produktivbetrieb ein eigenes Anwendungstoken je Installation
nachziehen, siehe Leitentscheidung 8
- [ ] Release-Signierschlüssel hinterlegt (`/api/updateservice/v1/pubkey` antwortet) — vor D-4
- [x] `max_activations` für `polytrader` großzügig gesetzt (Richard, vor 15.08.2026: Dev-Lizenz mit
9999 Aktivierungen) — `default_cache_ttl_hours` ungeprüft übernommen (Live-Test zeigte
plausible ~72h Offline-Kulanz), bei Bedarf im Admin nachjustieren
- [ ] Installationskonto (Rolle `installer`) angelegt — vor D-5
---
## 5. Entscheidungen (Richard, 14.08.2026)
| Frage | Entscheidung | Wirkt auf |
|---|---|---|
| Produkt-Slug | **`polytrader`**, `pt` entfällt | D-2, D-4, Serverseite |
| Zielarchitektur | **Alles auf das Deploymentcenter.** Watchdog und LicenseLabrador verschwinden, kein Parallelbetrieb | D-6 |
| Oberfläche | **Nur noch Avalonia**, WinForms wird demnächst vollständig ausgebaut | Leitentscheidung 6, D-2, D-4 |
| Master-Key | **Bei der Erstinstallation erzeugen** | D-5, D-10 |
## 6. Weiterhin offen
**Stand 17.08.2026:** Von den ursprünglich zwei DC-seitigen Wünschen ist einer erledigt
(Zugangsdaten aus argv → `DC_LICENSE_KEY`-Umgebungsvariable, `waitTimeoutSeconds` einstellbar,
`BuildInfo.targets` jetzt im Paket — alles Commit `1967b49`, siehe §0). **Offen bleibt nur D-3 der
Befunde** (unsigniertes Lizenzurteil) — vom Deploymentcenter-Team selbst bestätigt und bewusst
zurückgestellt. Trägt für den Eigenbetrieb, wird relevant, sobald Dritte die Software bekommen.
Kein Blocker für diesen Plan.
Dazu neu **D-16** (§1): das `environment`-Feld in `/api/errors/v1/report` ist ein undokumentiertes
Auth-Gate mit irreführender Fehlermeldung — für uns durch Neuprovisionierung gelöst (Token mit
`environment: "all"`), als Wunsch an DC offen (entweder die echte Ursache in der Fehlerantwort
nennen, oder die Kopplung entfernen).
### Entscheidung nötig: erstes Release veröffentlichen (D-4)
D-4 ist bis auf die eigentliche Auslieferung fertig — Update-Prüfung, Installations-Bestätigung und
der geordnete Neustart-Weg sind gebaut und laufen über denselben Pfad wie „Beenden". **Nicht
durchgeführt:** ein Paket schnüren und veröffentlichen. Drei Dinge sprechen dafür, das nicht
nebenbei zu tun, sondern mit Richard abzustimmen:
1. Ein veröffentlichtes Release ist auf einem **von mehreren Projekten geteilten** Server
persistent, nicht so folgenlos rückgängig zu machen wie ein lokaler Build.
2. Der Zugangsschutz (§5A des UpdateService-Guides) verändert `.htaccess`/`.htpasswd` in
`/releases/` — einem Verzeichnisbaum, den auch andere Produkte nutzen.
3. Der **Release-Signierschlüssel ist serverseitig noch nicht bestätigt** (offener Punkt in der
Checkliste §4). Ein erstes, unsigniertes Release wäre vermeidbar.
**Sobald Richard grünes Licht gibt:** `deploy/packager.config.json` aus der Vorlage anlegen (echtes
`updateservice:publish`-Token, FTP-Zugangsdaten), `dotnet publish` für `win-x64` und `linux-x64` in
saubere Verzeichnisse (nicht das Debug-Output), `pack-and-deploy` je Plattform aufrufen, danach
Zugangsschutz + Selbsttest prüfen (§4B der Planung).
@@ -0,0 +1,163 @@
# Umsetzungsplan: Fable-Code-Review-Fixes (Copytrading)
> Basis: Fable-5-Review nach Umsetzung des Rentabilitätsplans (Stand 2026-07-08).
> Ausgangslage: 207 Tests grün, Build/Smoke grün. Die Logic/-Klassen sind laut Review
> sauber; die Lücken liegen im **Zusammenspiel** von SELL-Leiter, Engine und den
> Hintergrund-Services (TraderMonitorService).
## Fortschritt
-**Slice 0** IClobClient-Seam + FakeClobClient (verhaltensneutral).
-**Slice 1** K1/H2/H1: atomarer Claim, Cleanup+Engine schonen Leitern, Floor-Robustheit. 8 Tests.
-**Slice 2** K2: Startup-Reconciliation (GetOpenOrders ohne assetId = alle). 3 Tests.
-**Slice 3** K3 (System-SELL vom Ownership-Check ausgenommen + Resolved-Cache) + M5 (Demo-Score-Anzeige, stündl. Auto-Pause). 5 Tests.
-**Slice 4** H4 (RoundToTick + Dust-Abbruch), M1 (GlobalPnl im Guard), M2 (TokenId), M3-min (serverseitiges Max + lauter Fehlschlag), M4 (Parser 9999), M6 (Fees in Orders), Doku. 10 Tests.
-**Slice 5** H3: BUY-Skip während ExitPending (Entscheidung A).
-**Slice 6** SnapshotService entfernt, Demo-Balance/PnL-Reconciliation, Settings-Validierung (IsLadderConfigInverted + Load-Warnung). 3 Tests.
**Stand: 244 Tests grün, Build/Smoke grün.**
### Nachgelagerte Testabdeckung (nach dem K3-Fund)
- **Engine-Integrationstests** (`CopyTradingEngineTests`, gemockter CLOB): H3 BUY-Skip, Doppel-SELL-Guard, K3 System-Close, Fremd-Trader-Reject, H2 Cleanup-schont-Leiter (+Kontrast). Engine `_clob``IClobClient`, `ProcessAccountOrderAsync` internal.
- **⚠️ K3-Korrektur:** Der Slice-3-Fix sass am falschen Ort (downstream ~Z.643). Der echte Ownership-Check ist der frühe `inPortfolio`-Lookup (~Z.437, `p.SourceTraderId == signal.TraderId`), der System-Signale schon vorher mit early return abwies. Jetzt am richtigen Ort via `IsAuthorizedSell` **vom Engine-Test aufgedeckt**.
- **K1a-Test** (`TraderMonitorServiceTests`): Cleanup cancelt Leiter-Order nicht (aktive Leiter) bzw. cancelt sie ohne Leiter. `_clob``IClobClient`, `CleanupStaleOpenOrdersAsync` internal.
**Alle 3 kritischen + 4 hohen Bugs sind jetzt durch Tests abgesichert** (K1a/K1b/K2/K3/H1/H2/H3/H4).
### Bewusst aufgeschobene Follow-ups (Live-Verifikation/Risiko)
- **M3 Autoincrement-Migration**: `TradeId` auf DB-Autoincrement umstellen Schema-Änderung an der Trade-Persistenz, erst im Zielland live verifizieren. (M3-Minimum ist umgesetzt.)
- **PersistenceService-Dedup-Zeitfenster**: `Exists(AccountId,TokenId)` blockt legit Re-Entries; robuster Fix (z.B. OpenedAt-basiert) braucht Live-Daten Duplikat-Schutz nicht unverifiziert brechen.
- **Perf**: `UpsertLive`-Dirty-Check (Schreib-Amplifikation) und Leiter-Parallelität laut Fable bei aktueller Größe unkritisch.
- **M6/K2**: fee-signierte Orders bzw. `/data/orders` ohne asset_id sind API-gated → im Zielland verifizieren.
## Arbeitsgrundsätze (für jeden Slice)
1. **`.agents/rules/clob.md`:** vor jedem CLOB-nahen Slice ein Commit als Rollback-Punkt;
Preis-/Zustandslogik pur in `SellLogic`/`CopyTradingRisk` + neue Tests; Service-Interaktionen
(Cleanup überspringt Leiter etc.) mit kleinem **Integrationstest über gemockten CLOB-Client**.
2. Nach jedem Slice: `dotnet build` + `dotnet test` (alle grün) + `--smoke-ui` grün, dann commit+push.
3. Ein Slice = eine kohärente Einheit = ein Commit. Reihenfolge unten folgt Fables Empfehlung.
---
## Slice 0 (Prereq): Testbarkeit — `IClobClient`-Interface
**Warum zuerst:** K1/H2/K2 brauchen Integrationstests mit gemocktem CLOB. `PolymarketClobClient`
ist heute eine konkrete Klasse ohne Interface → nicht mockbar.
- Interface `IClobClient` (Core) mit den von Leiter/Reconciliation genutzten Methoden:
`PlaceOrderAsync`, `CancelOrderAsync`, `GetOpenOrdersAsync`, `CancelConflictingOrdersAsync`.
- `PolymarketClobClient : IClobClient`. DI zusätzlich `IClobClient → PolymarketClobClient`.
- `SellLadderService`/Reconciliation gegen `IClobClient` typisieren (Engine kann vorerst konkret bleiben).
- **Verhaltensneutral, keine Logikänderung.** Ermöglicht `FakeClobClient` im Testprojekt.
- Tests: keine neuen fachlichen; Build grün genügt.
---
## Slice 1: „Wer darf Leiter-Orders anfassen" (H1 + K1 + H2) 🔴🟠
Kernthema: Leiter-Order darf nur von der Leiter angefasst/gecancelt werden.
- **H1 — Atomarer Claim:** In `SellLadderService.StartLadderAsync` als ERSTES
`if (!_copyState.ExitLadders.TryAdd(key, placeholder)) return false;` → macht ALLE Aufrufer
(Engine-SELL + ProfitTarget) idempotent. Bei Fehlschlag der Order den Key wieder entfernen.
- **K1 — Cleanup überspringt Leitern:** In `TraderMonitorService.CleanupStaleOpenOrdersAsync`
Keys mit `_copyState.ExitLadders.ContainsKey(key)` überspringen (`continue`).
- **K1 — Floor-Robustheit:** In `SellLadderService.ProcessLadderAsync` am Floor NICHT dauerhaft
früh zurückkehren, sondern periodisch via `GetOpenOrdersAsync` prüfen, ob die Floor-Order noch
ruht; wenn nicht → am Floor neu platzieren (+ `PendingOrderTimestamps` refreshen).
- **H2 — Engine-Cancel schont Leiter:** Den Pre-Signal-`CancelConflictingOrdersAsync`-Aufruf der
Engine überspringen, wenn `_copyState.ExitLadders.ContainsKey(key)` (oder hinter den
ExitPending-Check verschieben).
- Tests: Integrationstest (FakeClob) — Cleanup cancelt KEINE Leiter-Order; zwei parallele
StartLadder-Aufrufe → nur eine Leiter; Floor-Order weg → Leiter platziert neu. Pure: ggf.
Floor-Recheck-Entscheidung.
---
## Slice 2: Neustart-Reconciliation (K2) 🔴
Ruhende GTC-Leiter-/Maker-Orders überleben Neustarts, der Verwaltungszustand nicht.
- Beim Modul-Start je **Live-Account** alle offenen CLOB-Orders via `GetOpenOrdersAsync` abrufen und
pauschal canceln (deterministisch; die Engine entscheidet danach sauber neu). Kein Leiter-Rebuild.
- Ort: eigener Startup-Schritt im Modul (z. B. in `TraderMonitorService`-Warmup oder als kurzer
`IHostedService`), NACH der State-Hydration, VOR dem ersten Signal-Processing.
- Umfangreiches Logging (welche Orders gecancelt).
- Tests: Integrationstest (FakeClob) — für jeden offenen Order-Eintrag wird Cancel gerufen.
---
## Slice 3: Demo-Resolution + Demo-Score (K3 + M5) 🔴🟡
Sonst ist die Demo-Validierungsphase (auf der die Zielland-Strategie beruht) wertlos.
- **K3 — System-Signale (TraderId==0) vom Ownership-Check ausnehmen:** In der Engine SELL-Pre-Flight
(`p.SourceTraderId == signal.TraderId`) den Fall `signal.TraderId == 0` zulassen (System-Close bei
Marktauflösung). Zusätzlich „bereits als resolved erkannt"-Cache, damit ein Markt nur einmal
verarbeitet wird (verhindert 30-s-Loop-Spam + API-Last).
- **M5 — Demo-Score & schnellerer Auto-Pause:** Copy-Score getrennt für Demo (Anzeige/Validierung)
und Live (Pausieren) berechnen; der Kill-Switch filtert weiterhin `!IsDemo`, aber die Demo-Kennzahlen
füllen die Spalten. Zusätzlich stündlicher Light-Check nur für die Pause-Regel (statt nur alle 12 h).
- Tests: Ownership-Ausnahme (Engine), Resolved-Cache (pure). Demo/Live-Score-Trennung ist Job-Logik.
---
## Slice 4: Kleine, klar umrissene Fixes (H4 + M1 + M2 + M3 + M4 + M6 + Doku) 🟠🟡🟢
Jeweils klein und abgegrenzt — in einem oder zwei Commits.
- **H4 — Dust-Reject-Schleife:** (a) Leiter-Preis vor der USDC-Berechnung auf Tick runden
(`Math.Round(next, 3)`, zentral in `SellLogic`); (b) Abbruch in `ProcessLadderAsync`:
`pos.Size < CopyTradingRisk.MinShares` → Leiter beenden, `ExitPending=false`, Dust loggen.
Pure Tests für Rundung + Abbruch.
- **M1 — GlobalPnl-Doppelzählung:** In `TraderMonitorService` (~Z.910 und ~Z.961) das
`GlobalPnl += realizedPnl` INNERHALB des `_processedClosures`-Guards buchen (wie in
`PollClosedAccountsAsync` bereits korrekt).
- **M2 — TokenId in Live-Close-Records:** In beiden Live-Close-Records (~Z.922-940 und ~Z.972-990)
`TokenId = removedPos.TokenId` setzen (der 0.4-Fix erwischte nur den Demo-Pfad).
- **M3 — TradeId robust:** `ClosedTrade.TradeId` auf DB-Autoincrement (`ValueGeneratedOnAdd`)
umstellen + Code-Vergabe (`GetNextTradeId`) entfernen + Migration. Eliminiert die stille
PK-Kollisions-Fehlerklasse und den teuren Full-Table-`Max()`-Startup in `Program.cs`.
(Alternative/Minimum: serverseitiges `Max()` + lauter Fehlschlag statt `catch {}`.)
- **M4 — Parser-Default:** `MongoExportParser` ProfitTarget-Fallback `50m → 9999m` (sonst schaltet
ein erneuter `--migrate-json`-Lauf Take-Profit unbeabsichtigt scharf). Pure Test.
- **M6 — Fee in signierte Orders:** An den Callsites (Engine-BUY, Leiter, PreRedeem)
`actualFeeBps` aus `FeeModel`/`MarketData.TakerFeeBps` an `PlaceOrderAsync` durchreichen.
Verifikation im Zielland, aber die Verdrahtung jetzt.
- **Doku — Stale [Description]:** `SellFloorPct` ist verdrahtet (nicht „Phase 0.1 offen");
`ProfitTarget`-Text nicht mehr „folgt in Phase 0.3". Texte aktualisieren (Richard verlässt sich drauf).
---
## Slice 5: H3 — BUY während ExitPending 🟠
Re-buyt der Master, während unsere Leiter verkauft, kauft die Engine normal zu → die Leiter verkauft
danach `pos.Size` inkl. neuer Shares zum alten Floor.
**ENTSCHEIDUNG (Richard, 2026-07-08): Variante A — BUYs skippen, solange `ExitPending`.**
Während des Ausstiegs keine Zukäufe; die Leiter verkauft die Position sauber zu Ende.
- Umsetzung: In der Engine BUY-Pre-Flight früh prüfen —
`if (account.OpenPositions.TryGetValue(signal.TokenId, out var p) && p.ExitPending) { log + return; }`.
(Spiegelt die bestehende Double-Sell-Guard-Logik, nur für den BUY-Pfad.)
- Umfangreiches Logging (verworfener BUY während aktivem Exit inkl. TokenId/TraderId).
- Tests: Engine-BUY-Pfad überspringt, solange `ExitPending`; nach Leiter-Ende (ExitPending=false)
wird ein neuer BUY wieder normal ausgeführt.
---
## Slice 6: Rest nach Gelegenheit (🟢 Perf/Doku)
- **PersistenceService-Dedup:** `Exists(AccountId, TokenId)` blockt legitime Re-Entries (HF-Alltag) →
Dedup-Schlüssel um Zeitfenster ergänzen. (Copy-Score untererfasst sonst.)
- **Demo-Balance vs. PnL:** Balance sollte `exitUsd ExitFee` gutschreiben (und der BUY die Entry-Fee
abziehen), damit Σ(Balance-Änderungen) = Σ(PnL). Aktuell driftet es um die Fees.
- **Settings-Validierung:** `MaxPriceDifference% > SellFloorPct` → Leiter startet unter dem Floor
(sofortige „Floor erreicht"-Notification). UI-Warnung/Validierung.
- **Perf — Schreib-Amplifikation:** `PollLiveAccountsAsync` `UpsertLive` je Position alle 30 s →
Dirty-Check (nur bei Änderung) oder Batch.
- **Perf — Leitern seriell:** `ProcessLadderAsync` pro Tick seriell → begrenzte Parallelität + Timeout.
- **Totcode:** `services/SnapshotService.cs` entfernen (nirgends registriert) oder bewusst reaktivieren.
---
## Empfohlene Reihenfolge (Fable)
`Slice 0` (Test-Infra) → `Slice 1` (K1+H2+H1) → `Slice 2` (K2) → `Slice 3` (K3+M5) →
`Slice 4` (H4/M1/M2/M3/M4/M6/Doku) → `Slice 5` (H3, nach Entscheidung) → `Slice 6` (Rest).
**Als korrekt bestätigt (nicht anfassen):** Logic/-Klassen sauber/verhaltenstreu; ExitPending
EF-ignoriert; TotalFees gemappt; closed_trades-Indizes vorhanden; SellLadderService als
Singleton+Hosted (eine Instanz); Copy-Score-Find serverseitig; SELL-Spam-Blockade seitensensitiv.
@@ -0,0 +1,187 @@
# Umsetzungsplan: Modul „BundleArbitrage" (Intra-Market- & NegRisk-Arbitrage)
> Stand: 2026-07-06
> Ziel: Neues Strategiemodul, das Preissummen-Anomalien innerhalb von
> Polymarket erkennt und handelt: YES + NO < $1.00 (binäre Märkte) und
> Summen-Verletzungen in NegRisk-Multi-Outcome-Märkten.
> Reihenfolge: Nach/parallel zu MarketMaking — nutzt dieselbe Orderbuch-
> Infrastruktur. **Harte Voraussetzung:** Phase 1 (Marktdaten-Fundament) aus
> `UMSETZUNGSPLAN-CopyTrading-Verbesserungen.md`.
> **Wichtig:** Dieses Modul startet bewusst als reines Mess-Modul
> (Detection-only). Ob Execution gebaut wird, entscheidet die Messphase.
---
## 0. Strategie-Hintergrund & ehrliche Einordnung
**Mechanik:**
- **Binär:** Kostet YES + NO zusammen < $1.00 (beide zum Ask kaufbar),
ist der Kauf beider Seiten ein garantierter Gewinn: Das Paar zahlt bei
Resolution sicher $1.00 aus — oder kann on-chain sofort zu $1.00 USDC
zusammengelegt werden (CTF `mergePositions`).
- **NegRisk (Multi-Outcome, genau ein Gewinner):** Summe aller YES-Asks < $1.00
→ alle YES kaufen (eines zahlt aus). Komplementär: Überteuerte Summen über
die NO-Seite bzw. NegRisk-Konvertierungen handeln.
**Ehrliche Einordnung (Stand 2026):** Auf den großen Märkten ist das ein
HFT-Spiel — Fenster von Sekunden, dominiert von spezialisierten Bots; die
Taker-Fees seit März 2026 haben viele kleine Anomalien zusätzlich unprofitabel
gemacht. **Die Chance liegt im Long Tail** (kleine/neue Märkte, auf die die
großen Bots nicht schauen) und als **Beifang** der ohnehin laufenden
Orderbuch-Streams des MarketMaking-Moduls. Deshalb: erst messen, dann bauen.
**Fee-Beachtung:** Als Taker fallen je Leg Fees an (kategorieabhängig,
01,8 %). Ein Bundle mit 2 ¢ Brutto-Marge kann nach Fees negativ sein.
Die Profitrechnung muss Fees je Leg von Anfang an enthalten. Maker-seitige
Ausführung (ein Leg ruht als Limit) ist fee-frei, aber nicht atomar.
---
## 1. Architektur-Einbettung
Neues Projekt `src/PolyTrader.Modules.BundleArbitrage/` als `IPolyTraderModule`
(`Name = "BundleArbitrage"`, `DbPrefix = "ba_"`), Registrierung in `Program.cs`.
**Eigener Polymarket-Account** (gleiche Begründung wie in den anderen
Modul-Plänen; kann sich in v1 den Account mit MarketMaking teilen, sofern
die Inventar-Buchführung getrennt bleibt — Empfehlung: eigener Account,
sobald Execution live geht).
### Persistenz
| Tabelle | Inhalt |
|---|---|
| `ba_opportunities` | Jede erkannte Anomalie: Zeitpunkt, Markt/Event, Legs mit Preisen & ausführbarer Size, Brutto-/Netto-Marge (nach Fees), Lebensdauer (wann verschwunden) |
| `ba_executions` | Ausgeführte Bundles: Legs, Fills, Slippage, Ergebnis |
| `ba_settings` | Schwellen, Size-Limits, Modus (Detect/Execute) |
Die Lebensdauer-Messung („wie lange war die Anomalie ausführbar?") ist der
wichtigste Datenpunkt der Messphase — sie entscheidet, ob unsere
Ausführungslatenz überhaupt konkurrenzfähig ist.
---
## 2. Komponenten
### 2.1 `ArbScannerService : BackgroundService` — Detection
Zwei Datenpfade:
1. **Hot Set (WSS):** Für die vom `ClobMarketDataService` (Core) ohnehin
gestreamten Bücher (MarketMaking-Märkte + Top-Volumen-Märkte) wird bei
jedem Book-Update die Summenprüfung getriggert (< 1 ms, pure Funktion).
2. **Long-Tail-Sweep (REST):** Zyklischer Scan über aktive Märkte
(Gamma-API-Liste, dann CLOB `GET /book` bzw. Batch-Preis-Endpoints —
verfügbare Batch-Endpoints bei Umsetzung in der Doku prüfen).
Rate-Limits respektieren (Batching + Delays wie im
`TraderMonitorService`-Muster); Sweep-Frequenz Setting (z. B. alle 60 s
für 500 Märkte, priorisiert nach Volumen/Neuheit).
**Prüf-Logik (pure, getestete Klasse `BundleMath`):**
- Binär: `bestAskYes + bestAskNo + FeeYes + FeeNo < 1.00 MinMarginPct`.
Ausführbare Size = min(AskSize beider Seiten), ggf. über mehrere Book-Level
kumuliert (Level-2-Sweep-Rechnung).
- NegRisk: `Σ bestAskYes_i + Σ Fees < 1.00 MinMarginPct` über alle Outcomes
eines NegRisk-Events (Event-Gruppierung über Gamma-API; `NegRisk`-Flag
existiert bereits in `MarketData`).
- Jede erkannte Anomalie → `ba_opportunities`; bei Verschwinden (nächstes
Update unterschreitet Schwelle) Lebensdauer nachtragen.
### 2.2 Mess-Auswertung (Phase BA-1, entscheidungsrelevant)
Report (UI-Tab + wöchentlicher Threema-Report):
- Anomalien/Tag nach Marge-Bucket (0,51 %, 12 %, > 2 % netto).
- Verteilung ausführbare Size und Lebensdauer.
- Erwarteter Monatsertrag bei angenommener Erfolgsquote X % =
Σ(Netto-Marge × min(Size, unser Limit)) über gefangene Fenster.
**Go/No-Go-Kriterium für Execution:** erwarteter Ertrag > Entwicklungs- und
Kapitalkosten; realistisch fangbare Fenster (Lebensdauer > unsere Latenz,
konservativ ≥ 23 s).
### 2.3 `ArbExecutionService` — nur nach Go-Entscheidung
1. **Beide Legs gleichzeitig** als IOC-artige Orders senden (CLOB-Ordertypen
FOK/FAK bei Umsetzung in der Doku verifizieren; `PolymarketClobClient`
ggf. erweitern). Preis = erkannter Ask + kleiner Puffer, Size = min-Leg.
2. **Single-Leg-Risiko** (ein Leg füllt, das andere nicht) ist das
Kernproblem — Behandlungsreihenfolge:
a) Sofortiger Retry des offenen Legs (bis Preis `1.00 Fees MinMargin/2`).
b) Kein Fill → offenes Leg als GTC-Maker-Order zum Break-even-Preis stellen.
c) Timeout (Setting, z. B. 10 min) → Leg über Eskalationsleiter abbauen
(Muster aus Copytrading-Plan Phase 0.1) und Verlust in `ba_executions`
verbuchen. `MaxSingleLegLossUsd`-Tageslimit als Kill-Switch.
3. Size-Limits: `MaxUsdPerBundle` (Start 1025), `MaxOpenBundles`,
Tagesbudget.
4. `.agents/rules/clob.md` beachten — jede CLOB-Client-Erweiterung mit
Backup/Commit und Mehrfach-Review.
### 2.4 Kapital-Recycling: CTF `mergePositions` (Phase BA-4)
Ohne Merge bindet jedes Bundle Kapital bis zur Resolution (bei kurzlaufenden
Märkten oft akzeptabel — Priorisierung im Scanner auf EndDate < 7 Tage
umgeht das Problem anfangs).
On-Chain-Merge: YES + NO gleicher Size → $1.00 USDC sofort, via
ConditionalTokens `mergePositions(...)`; NegRisk-Sets über den
NegRisk-Adapter. Implementierung teilt sich Infrastruktur mit dem
Auto-Redeem des ResolutionFarming-Moduls (Phase RF-4) — **gemeinsamen
Core-Baustein `OnChainCtfService` bauen**, nicht zweimal implementieren.
Contract-Adressen/ABI aus https://docs.polymarket.com (Developer/CTF)
verifizieren; Gas (POL) -Handling und Balance-Warnung wie im RF-Plan.
### 2.5 UI
- Tab „Live-Anomalien": aktuelle Opportunities mit Netto-Marge/Size.
- Tab „Messung": Statistik-Report aus 2.2.
- Tab „Executions": Bundles, Single-Leg-Vorfälle, PnL.
- Tab „Settings": Schwellen, Modus-Schalter Detect/Execute (Default: Detect).
---
## 3. Phasen & Akzeptanzkriterien
### Phase BA-1: Detection-only (24 Wochen Messung)
- Scanner (Hot Set + Long-Tail-Sweep), `BundleMath` mit Unit-Tests
(inkl. Fee-Rechnung, Level-2-Kumulation, NegRisk-Summen),
`ba_opportunities`-Logging, Mess-Report.
- Akzeptanz: App baut & läuft; Report nach 2 Wochen vollständig;
dokumentierte Go/No-Go-Empfehlung.
### Phase BA-2: Execution klein (nur bei Go)
- IOC-Doppel-Leg, Single-Leg-Behandlung, Size-Limits, Kill-Switch.
- Zunächst nur binäre Märkte (NegRisk-Execution ist komplexer → BA-3).
- Akzeptanz: ≥ 20 Bundles ausgeführt; Single-Leg-Quote < 20 %;
Netto-PnL nach Fees > 0.
### Phase BA-3: NegRisk-Execution
- Multi-Leg-Bundles (N Outcomes), strengere Size-/Slippage-Grenzen
(mehr Legs = mehr Single-Leg-Risiko).
### Phase BA-4: `OnChainCtfService` (Merge) — Kapital-Recycling
- Gemeinsam mit ResolutionFarming RF-4 (Redeem) als ein Core-Baustein.
- Testmarkt/Kleinstbetrag zuerst; Akzeptanz: Bundle → USDC ohne manuellen
Eingriff, USDC-Delta verifiziert.
---
## 4. Risiken & Gegenmaßnahmen
| Risiko | Gegenmaßnahme |
|---|---|
| Anomalien existieren, sind aber in < 1 s weg | Messphase BA-1 entscheidet VOR Entwicklungsaufwand für Execution |
| Single-Leg-Exposure | IOC-Orders, Retry-Kaskade, Tages-Verlustlimit, kleine Bundles |
| Fees fressen Marge | Netto-Rechnung inkl. Fees je Leg von Anfang an; `MinMarginPct` konservativ (Start ≥ 1 %) |
| Rate-Limits durch Long-Tail-Sweep | Batching, Priorisierung, Sweep-Frequenz drosseln; API-Fehlerquote überwachen |
| Stale-Book-Falsch-Signale | Max-Age-Check auf Book-Daten (`TryGetBook(maxAgeMs)`); Anomalie erst nach 2 aufeinanderfolgenden Bestätigungen |
| On-Chain-Merge-Fehler | Separater Baustein, Testmarkt, clob.md-Regeln, Balance-Verifikation |
## 5. Offene Entscheidungen
1. `MinMarginPct` (netto, nach Fees) für Detection-Logging (Empfehlung 0,5 %)
vs. Execution (Empfehlung ≥ 1 %).
2. Long-Tail-Sweep-Umfang (alle aktiven Märkte vs. Top-N + Neue) — abhängig
von beobachteten Rate-Limits.
3. Account-Frage: mit MarketMaking teilen oder eigener (Empfehlung: eigener,
sobald BA-2 startet).
4. Priorität von BA-4 (Merge): Bei Fokus auf kurzlaufende Märkte zunächst
verzichtbar — Kapitalbindung von Tagen ist bei kleinen Größen tragbar.
@@ -0,0 +1,200 @@
# Umsetzungsplan: Modul „MarketMaking" (Liquidity Rewards + Spread)
> Stand: 2026-07-06
> Ziel: Neues Strategiemodul, das beidseitige Limit-Orders in belohnungs-
> berechtigten Polymarket-Märkten stellt und drei Ertragsquellen kombiniert:
> tägliche Liquidity Rewards (USDC), Maker-Rebates und den Spread selbst.
> Reihenfolge: Nach ResolutionFarming. **Harte Voraussetzung:** Phase 1
> (Marktdaten-Fundament: `ClobMarketDataService`, `ClobUserChannelService`)
> aus `UMSETZUNGSPLAN-CopyTrading-Verbesserungen.md`.
---
## 0. Strategie-Hintergrund
Polymarket zahlt täglich (00:00 UTC) USDC-Rewards an Wallets, die kompetitive
Resting-Limit-Orders in berechtigten Märkten stellen. Der Reward-Pool liegt
2026 bei > $5 M/Monat (Sport-Peaks ~$8 M). Die Formel belohnt: Nähe zum
Midpoint (innerhalb eines markt-spezifischen Max-Spreads), Ordergröße
(Mindestgröße je Markt) und beidseitige Tiefe (einseitige Orders scoren
reduziert). Seit den Taker-Fees (März 2026) gibt es zusätzlich ein
**Maker-Rebate-Programm** (Anteil der Taker-Fees wird täglich an Maker
ausgeschüttet). Maker zahlen selbst keine Fees.
**Referenzen (bei Umsetzung Formel/Parameter aktuell verifizieren):**
- https://docs.polymarket.com/market-makers/liquidity-rewards
- https://docs.polymarket.com/trading/fees (Maker-Rebates)
- Reward-Parameter je Markt (Max-Spread, Min-Size, Tages-Pool) kommen aus der
Gamma-/CLOB-API am Markt-Objekt.
**Warum dieses Modul strategisch wertvoll ist:** Es ist die einzige Strategie,
bei der wir nicht gegen schnellere Bots um denselben Trade konkurrieren —
Anwesenheit wird bezahlt. Ertrag ist stetig statt direktional.
**Hauptrisiko: Adverse Selection.** Unsere Quotes werden bevorzugt dann
gefüllt, wenn jemand mit besserer Information (News-Bot, Live-Sport-Feed)
gegen uns handelt. Gegenmaßnahmen: Marktauswahl (ruhige, langlaufende Märkte;
anfangs KEINE Live-Sport- und KEINE Krypto-Kurzfrist-Märkte), Inventar-Limits,
Volatilitäts-Pause.
---
## 1. Architektur-Einbettung
Neues Projekt `src/PolyTrader.Modules.MarketMaking/` als `IPolyTraderModule`
(`Name = "MarketMaking"`, `DbPrefix = "mm_"`), Registrierung in `Program.cs`.
**Eigener Polymarket-Account zwingend** (gleiche Begründung wie im
ResolutionFarming-Plan, hier noch kritischer: Der Copytrading-
`TraderMonitorService` würde MM-Inventar als Positionen adoptieren und der
Copytrading-`CancelConflictingOrdersAsync`-Mechanismus würde unsere
Resting-Quotes canceln!).
### Persistenz
| Tabelle | Inhalt |
|---|---|
| `mm_settings` | Globale + je-Markt-Settings (Size, Spread-Ziel, Limits) |
| `mm_markets` | Kuratierte/gescorte Märkte (Reward-Parameter, Status) |
| `mm_quotes_log` | Quote-Historie (Preis, Size, Dauer, Cancel-Grund) — für Reward-Optimierung |
| `mm_fills` | Fills mit Seite, Preis, Inventar danach |
| `mm_daily_pnl` | Tagesabrechnung: Rewards, Rebates, Spread-PnL, Inventar-PnL |
---
## 2. Komponenten
### 2.1 `MarketSelectorJob` — Marktauswahl & Scoring
Täglich + manuell triggerbar:
1. Reward-berechtigte Märkte über Gamma-/CLOB-API listen (Felder: Reward-Pool/
Rate, `rewardsMaxSpread`, `rewardsMinSize` — Feldnamen verifizieren).
2. Score je Markt: `erwarteter Reward pro gequoteter $ ÷ Risiko-Proxy`.
- Reward-Schätzung: Tages-Pool des Markts ÷ beobachtete konkurrierende
Maker-Liquidität innerhalb des Max-Spreads (aus Orderbuch-Snapshots).
- Risiko-Proxy: realisierte Midpoint-Volatilität (Stddev der Mid-Bewegungen
über 24 h aus `ClobMarketDataService`-Daten), Zeit bis Resolution
(je näher, desto gefährlicher), Kategorie.
3. Harte Ausschlüsse (erste Ausbaustufe): Live-Sport (in-play), Krypto-
Kurzfrist-Märkte (15 min/1 h), Märkte < 7 Tage vor EndDate, Midpoint
außerhalb 0.100.90 (Extrempreise = asymmetrisches Inventarrisiko).
4. Output: Ranking in `mm_markets` + UI; Betreiber aktiviert Märkte manuell
(Whitelist-Prinzip — der Bot wählt in v1 nicht selbst).
### 2.2 `QuotingEngine : BackgroundService` — Kern des Moduls
Je aktivem Markt eine Quote-State-Machine:
1. **Zielquote:** Bid und Ask symmetrisch um den Midpoint, Abstand
`QuoteSpreadTicks` (Setting), immer **innerhalb** des Reward-Max-Spreads;
Size ≥ Reward-Min-Size (Setting `QuoteSizeUsd`, initial klein).
2. **Requote-Trigger:** Midpoint-Bewegung > Schwelle (z. B. 1 Tick), eigene
Order gefüllt, Reward-Fenster verletzt. Requote = Cancel + neue Order über
`PolymarketClobClient`.
3. **Churn-Begrenzung:** Mindest-Ruhezeit zwischen Requotes (z. B. 35 s),
Hysterese (nicht bei jedem Tick nachziehen) — API-Rate-Limits und
Order-Spam vermeiden.
4. **Fill-Verarbeitung:** über `ClobUserChannelService` (Echtzeit). Nach Fill:
Inventar aktualisieren, Gegenquote anpassen (siehe 2.3).
5. Alle Quotes/Cancels in `mm_quotes_log` (Grundlage für Optimierung).
Die Preis-/Requote-Logik als **pure, getestete Klasse** (`QuoteCalculator`)
implementieren — Input: Book-Snapshot, Inventar, Settings; Output: Ziel-Quotes.
Unit-Tests in `PolyTrader.Tests` (das ist die kritischste Logik des Moduls).
### 2.3 `InventoryManager` — Risikosteuerung
1. Inventar je Markt = Netto-Shares (YES-äquivalent) × Preis.
2. **Skew:** Bei wachsendem Inventar Quotes asymmetrisch verschieben
(Kaufseite weiter weg, Verkaufsseite näher/attraktiver), Faktor
proportional zu `Inventar / MaxInventoryUsd`.
3. **Limits (Settings je Markt + global):**
- `MaxInventoryUsd` je Markt (Default klein, z. B. 50).
- `MaxTotalInventoryUsd` über alle Märkte.
- Bei Limit-Bruch: Quoting nur noch auf der abbauenden Seite
(„Reduce-Only-Modus") bis Inventar < 50 % des Limits.
4. **Exit vor Resolution:** Ab `ExitHoursBeforeEnd` (Default 48 h) Reduce-Only,
ab 24 h aktiver Abbau (Maker-seitig, notfalls Taker mit Verlust-Deckel).
5. **Volatilitäts-Pause:** Midpoint-Sprung > X % in Y Sekunden → alle Quotes
des Markts canceln, Cooldown Z Minuten (News-Schutz). Global-Kill-Switch
analog `GlobalTradingPaused`.
### 2.4 `RewardTracker`
1. Tägliche Reward-/Rebate-Eingänge erkennen (USDC-Transfers auf die Wallet
via Data-API/Alchemy) und `mm_daily_pnl` zuordnen.
2. Tagesabrechnung: `Rewards + Rebates + SpreadPnL + InventarPnL(mark-to-mid)
Verluste = Netto`. Threema-Tagesreport.
3. Kennzahl je Markt: **Reward-ROI pro gequoteter $** → Feedback in den
`MarketSelectorJob` (schlechte Märkte deaktivieren).
### 2.5 UI
- Tab „Märkte": Kandidaten-Ranking, aktiv/inaktiv-Toggle, Reward-Parameter.
- Tab „Live": aktuelle Quotes, Inventar je Markt (Ampel), letzte Fills.
- Tab „Abrechnung": `mm_daily_pnl`-Historie, Reward-ROI je Markt.
- Tab „Settings": PropertyGrid.
---
## 3. Phasen & Akzeptanzkriterien
### Phase MM-1: Fundament-Verifikation + Selector (read-only)
- Voraussetzung prüfen: `ClobMarketDataService`/`ClobUserChannelService`
laufen stabil (mehrtägiger Soak-Test, Reconnect-Verhalten).
- `MarketSelectorJob` + UI-Ranking, keine Orders.
- Akzeptanz: Ranking plausibel; Orderbuch-Daten für Top-Märkte lückenlos
über 72 h (Basis für Volatilitäts-Proxy).
### Phase MM-2: Paper-Quoting (Messung Adverse Selection)
- QuotingEngine läuft vollständig, sendet aber **keine** Orders; simulierte
Fills: Quote gilt als gefüllt, wenn der Marktpreis durch unser Quote-Level
handelt (aus Market-Channel-Trades ableitbar).
- 2 Wochen laufen lassen. Messen: simulierter Spread-PnL, Inventarverläufe,
Wie oft wären wir „überfahren" worden (Fill unmittelbar vor großer
Gegenbewegung)?
- Akzeptanz/Go-Kriterium: simuliertes Inventar bleibt innerhalb der Limits;
Spread-PnL ≥ 0 (Rewards kommen on top und sind der eigentliche Ertrag).
- **Hinweis:** Rewards selbst lassen sich nicht simulieren — sie erfordern
echte Resting-Orders. Paper-Phase misst nur die Risikoseite.
### Phase MM-3: Live auf 12 ruhigen Märkten
- Eigener Account, kleines Kapital (z. B. 300500 USDC), `QuoteSizeUsd`
knapp über Reward-Min-Size, 12 langlaufende Politik-/Geopolitik-Märkte.
- Akzeptanz nach 24 Wochen: tägliche Rewards fließen nachweislich
(`mm_daily_pnl`); Netto (Rewards + Spread Inventarverluste) > 0;
keine Order-Leichen (Cancel-Fehler) im CLOB.
### Phase MM-4: Skalierung + Skew-Feintuning
- Mehr Märkte (Selector-getrieben), Inventar-Skew-Parameter aus Fill-Daten
optimieren, Size je Markt anhand Reward-ROI erhöhen.
### Phase MM-5 (optional): Reward-Optimierung
- Order-Laddering (mehrere Level innerhalb des Max-Spreads), dynamische
Spread-Wahl abhängig von Konkurrenz-Liquidität, Teilnahme an
Sponsor-/Sonder-Reward-Programmen (z. B. Sport-Events pre-game).
---
## 4. Risiken & Gegenmaßnahmen
| Risiko | Gegenmaßnahme |
|---|---|
| Adverse Selection durch News-/Latenz-Bots | Marktauswahl (keine Live-Events), Volatilitäts-Pause, kleine Size |
| Inventar läuft in Resolution | Exit-Regeln ab 48 h/24 h vor EndDate (2.3) |
| Order-Churn → Rate-Limits/Sperren | Requote-Hysterese, Mindest-Ruhezeit, Monitoring der API-Fehlerquote |
| Reward-Regeländerungen | Parameter täglich aus API lesen, nichts hartkodieren |
| WSS-Ausfall → blinde Quotes | Watchdog: keine Book-Updates > N s → alle Quotes canceln (Fail-Safe) |
| Konflikt mit Copytrading | Eigener Account (Abschnitt 1) |
Der Fail-Safe „bei Datenverlust alles canceln" ist Pflicht ab MM-3 und muss
getestet werden (WSS künstlich trennen).
## 5. Offene Entscheidungen
1. Startmärkte (Empfehlung: 12 langlaufende Politik-/Geopolitik-Märkte mit
mittlerem Volumen — genug Reward-Pool, wenig Newsflow).
2. `QuoteSizeUsd`/Kapital für MM-3.
3. Beidseitig quoten von Anfang an (voller Reward-Score) oder zunächst
einseitig konservativ? (Empfehlung: beidseitig, dafür kleine Size —
einseitig scored schlechter und halbiert den Lerneffekt.)
@@ -0,0 +1,212 @@
# Umsetzungsplan: Modul „ResolutionFarming" (Favoriten nahe Auflösung)
> Stand: 2026-07-06
> Ziel: Neues Strategiemodul, das systematisch unterbewertete Favoriten
> (~9098 ¢) in bald auflösenden Märkten kauft, bis zur Resolution hält und
> automatisch redeemt.
> Reihenfolge: **Erstes neues Strategiemodul** (geringster Infrastrukturbedarf,
> validiert die Modul-Architektur über Copytrading hinaus).
> Voraussetzung: Phase 0 + 0.2 (Fee-Modell) aus
> `UMSETZUNGSPLAN-CopyTrading-Verbesserungen.md`. Phase 1 (Orderbuch) ist
> hilfreich, aber nicht zwingend für den Start.
---
## 0. Strategie-Hintergrund (Warum das funktioniert)
Auswertungen der Polymarket-Handelsdaten zeigen ein **Favorite-Longshot-
Reversal**: Outcomes mit hoher Wahrscheinlichkeit sind systematisch
*unterbewertet* (Retail überschätzt Longshots und drückt damit den Favoriten-
Preis). Ein 95-¢-Favorit gewinnt im Schnitt öfter als in 95 % der Fälle.
25 % Marge in 2448 h ergibt hohe annualisierte Renditen — **sofern das
Tail-Risiko diszipliniert gemanagt wird**: Ein verlorener 95-¢-Trade
vernichtet ~19 gewonnene. Das Risikomodell IST die Strategie.
Interner Kontext: Die profitabelsten kopierten Master (Typ „SwissTony"/„RN1")
machen genau das — hunderte BUYs, nie SELLs, Auflösung abwarten. Dieses Modul
internalisiert die Strategie und eliminiert die Copy-Latenz und die
Fremdbestimmung der Marktauswahl.
**Fees (seit März 2026):** Taker-Fees je Kategorie (Sports ~0,75 %, Politik
~1,0 %, Krypto ~1,8 %, Geopolitik 0 %) — bei 25 % Brutto-Marge ist die
Kategorie-Wahl entscheidend. Maker-Einstieg (Limit ins Buch) zahlt 0 Fees.
---
## 1. Architektur-Einbettung
Neues Projekt `src/PolyTrader.Modules.ResolutionFarming/` (Class Library,
net8.0-windows), Registrierung als `IPolyTraderModule` analog
`CopyTradingModule` (`Name = "ResolutionFarming"`, `DbPrefix = "rf_"`),
Einbindung in `Program.cs` der App.
### ⚠️ Grundsatzentscheidung: Eigener Polymarket-Account je Strategiemodul
**Dringende Empfehlung:** Das Modul handelt über einen **eigenen Account**
(Multi-Account-Support existiert im Core / `AccountState`).
Begründung: Der `TraderMonitorService` des Copytrading-Moduls synct **alle**
Wallet-Positionen eines Accounts in `account.OpenPositions`, würde
ResolutionFarming-Positionen „adoptieren" (Master-Zuordnungs-Fallbacks),
in seine Limits (PerMarket/PerMaster/Zeitfenster) einrechnen und ggf.
Auto-Redeem-/Cleanup-Logik darauf anwenden. Saubere Trennung über getrennte
Wallets vermeidet diese gesamte Konfliktklasse zur Laufzeit **und**
buchhalterisch (PnL je Strategie sauber messbar).
In der UI/Settings des Moduls: Zuordnung `AccountId ↔ Modul` mit Warnung,
wenn derselbe Account auch im Copytrading aktiv ist.
### Persistenz (EF Core / Pomelo / MySQL, eigener DbContext analog `CopyTradingDbContext`)
| Tabelle | Inhalt |
|---|---|
| `rf_settings` | Modul-Settings je Account (Preisband, Budgets, Limits) |
| `rf_candidates` | Scanner-Ergebnisse (Markt, Preis, Score, Filtergründe) — auch abgelehnte, für spätere Kalibrierung |
| `rf_positions` | Offene Farming-Positionen (TokenId, Entry, Size, EndDate, ClusterKey, Status) |
| `rf_closed_trades` | Abgeschlossene Trades inkl. Fees, Redeem-Infos |
Zusätzlich schreibt das Modul in den generischen Core-Trade-Log
(modulübergreifendes Dashboard).
---
## 2. Komponenten
### 2.1 `MarketScannerJob : BackgroundService`
Alle 1015 Minuten (JobManager-Registrierung wie `MasterTraderAnalyticsJob`,
manuell triggerbar):
1. Gamma-API: aktive Märkte mit `endDate < now + MaxHoursToResolution`
(Default 48 h), nicht closed. Bestehenden `PolymarketApiService` erweitern
(Query-Parameter für endDate-Fenster; Endpoint-Details bei Umsetzung aus
https://docs.polymarket.com verifizieren).
2. Je Markt den Favoriten bestimmen (Outcome mit höchstem Preis). Preisquelle:
CLOB Midpoint/Book (REST `GET /book` bzw. `IOrderBookProvider`, falls
Phase 1 des Copytrading-Plans schon umgesetzt).
3. Filterkette (jeder Reject wird mit Grund in `rf_candidates` geloggt):
- Preisband: `MinPrice ≤ ask ≤ MaxPrice` (Default 0.900.98).
- Liquidität: Ask-Tiefe am Zielpreis ≥ geplante Ordergröße × Faktor;
zusätzlich Markt-Volumen/Liquiditätsfelder der Gamma-API als Grobfilter.
- Kategorie-Whitelist (Default: Sports, Geopolitik, Politik; **Krypto
ausschließen** — 1,8 % Fee frisst die Marge; keine 15-Min-/Stunden-Märkte).
- Netto-Edge-Check: `(1 ask) Fee(ask, Kategorie) ≥ MinEdgePct`
(Default z. B. 1,5 %).
- Blacklist-Mechanismus (Slugs/Tags), z. B. für Marktarten mit
Resolution-Streitigkeiten (UMA-Disputes).
4. Kandidaten mit Score in `rf_candidates` schreiben; Anzeige in der Modul-UI.
### 2.2 Risiko-Engine (pure, testbare Klasse `FarmingRiskEngine`)
Settings je Account (`rf_settings`):
| Setting | Default | Bedeutung |
|---|---|---|
| `MaxPerMarketUsd` | 25 | Max. Einsatz je Markt |
| `MaxPerClusterPct` | 10 % | Max. Anteil der Bankroll je **Ereignis-Cluster** |
| `MaxTotalExposurePct` | 60 % | Max. Gesamteinsatz in offenen Positionen |
| `MaxNewPositionsPerDay` | 20 | Drosselung |
| `MinEdgePct` | 1.5 % | Netto-Edge nach Fees |
| `DailyLossKillSwitchUsd` | konfig. | Tagesverlust → Modul pausiert + Threema |
**Cluster-Definition (kritisch!):** 20 Fußballspiele desselben Spieltags sind
keine 20 unabhängigen Wetten. ClusterKey ableiten aus Event-/Series-Slug der
Gamma-API (z. B. Liga+Datum, Turnier, Wahl-Event). Korrelierte Favoriten
(z. B. „Kandidat X gewinnt" + „Partei von X gewinnt") teilen einen Cluster.
Erste Version: Heuristik über Event-Slug; Verfeinerung später.
### 2.3 `FarmingExecutionService`
1. Einstieg **Maker-first**: GTC-Limit auf Best-Bid bzw. Mid 1 Tick
(0 Fees). Kein Fill nach T Minuten (Default 15) → Entscheidung per Setting:
Taker-Fill (wenn Netto-Edge auch mit Fee noch ≥ MinEdge) oder verwerfen.
2. Order-Verwaltung über `PolymarketClobClient` (Core), Tracking analog
`PendingOrderTimestamps`-Muster des Copytrading-Moduls.
3. Positionen in `rf_positions` + Core-Positions-Sync gegen die Wallet
(Data-API `positions` — Muster aus `TraderMonitorService.PollLiveAccountsAsync`
übernehmen, aber schlanker: kein Master-Mapping nötig).
### 2.4 `ResolutionMonitorJob` + `AutoRedeemService`
1. Monitor: prüft offene `rf_positions` gegen Resolution
(`PolymarketApiService.CheckMarketResolutionAsync` existiert bereits).
2. **On-Chain-Auto-Redeem** (heute im gesamten Projekt nur manuell — dieser
Baustein nützt auch dem Copytrading):
- Gewinner-Shares einlösen via ConditionalTokens-Contract auf Polygon
(`redeemPositions(...)`); für NegRisk-Märkte über den NegRisk-Adapter.
**Contract-Adressen und Aufrufparameter bei Umsetzung zwingend aus der
offiziellen Doku verifizieren** (https://docs.polymarket.com,
Developer-Sektion CTF/NegRisk).
- Implementierung: Nethereum-Paket ODER Raw-RPC über die vorhandene
Alchemy-Anbindung; Signing mit dem Account-PrivateKey (liegt in
`AccountState`).
- Gas: Wallet braucht POL; Balance-Check + Threema-Warnung bei Unterdeckung.
- Retry mit Backoff; Erfolg = USDC-Balance-Delta verifiziert.
- `.agents/rules/clob.md` gilt hier besonders: On-Chain-Signing ist
hochkritisch — zuerst mit Kleinstbetrag auf einem Testmarkt verifizieren.
3. Übergangslösung bis 2.4 fertig: PreRedeem-artiger Verkauf (GTC-Limit 0.99+)
oder manueller Redeem — das Modul funktioniert auch ohne On-Chain-Teil,
bindet dann nur Kapital länger.
### 2.5 UI (`RegisterUi`, ein Fenster mit Tabs analog `CopyTradingMainForm`)
- Tab „Kandidaten": aktueller Scan mit Filtergründen (auch Rejects).
- Tab „Positionen": offene Farming-Positionen, Cluster-Auslastung, Countdown.
- Tab „Historie/Statistik": Winrate je Preisband (Kalibrierung!), Netto-PnL,
Fees, Redeem-Status.
- Tab „Settings": PropertyGrid auf `rf_settings` (Muster `AccountSettingsView`).
---
## 3. Phasen & Akzeptanzkriterien
### Phase RF-1: Modul-Skelett + Scanner (read-only)
- Projekt, Modul-Registrierung, DbContext + Migration, Scanner-Job, UI-Tab
„Kandidaten". **Keine Order-Platzierung.**
- Akzeptanz: App baut & startet mit Modul; Scanner liefert plausible
Kandidaten; Rejects nachvollziehbar geloggt; 1 Woche Kandidaten-Sammlung.
### Phase RF-2: Demo-Betrieb (4 Wochen)
- Execution im Demo-Modus (realistisches Fill-Modell: Ask-Preis + Fee,
siehe Copytrading-Plan Phase 4.2). Risiko-Engine aktiv.
- Akzeptanz/Go-Kriterium für Live: Kalibrierungstabelle zeigt
`realisierte Winrate je Preisband > Preisband-Mitte` und
Netto-Edge nach Fees > 0 über ≥ 100 Demo-Trades.
### Phase RF-3: Live klein
- Eigener Account, kleines Budget (z. B. 200500 USDC), `MaxPerMarketUsd` 510.
- Kill-Switch + Threema-Reporting (Tageszusammenfassung) aktiv.
- Akzeptanz: 2 Wochen Live ohne Ausführungsfehler; Live-Ergebnis im Rahmen
der Demo-Erwartung.
### Phase RF-4: Auto-Redeem on-chain
- Wie 2.4; zuerst Testmarkt/Kleinstbetrag, dann aktivieren.
- Akzeptanz: Gewinner-Position wird ohne manuellen Eingriff zu USDC.
### Phase RF-5: Kalibrierung & Skalierung
- Scoring von Heuristik auf Daten umstellen: historische Winrate je
Preisband × Kategorie aus `rf_candidates`/`rf_closed_trades` (+ optional
öffentliche Polymarket-Historien-Datensätze) → nur Bänder/Kategorien mit
nachgewiesenem Edge handeln. Budget stufenweise erhöhen.
---
## 4. Risiken & Gegenmaßnahmen
| Risiko | Gegenmaßnahme |
|---|---|
| Tail-Event (Favorit verliert) | Cluster-Limits, MaxPerMarket, Diversifikation über Kategorien |
| Korrelierte Cluster falsch geschnitten | Konservative Cluster-Heuristik, Review der Cluster in der UI |
| Resolution-Disputes (UMA) | Blacklist strittiger Marktarten; nur klare, objektiv auflösbare Märkte |
| Fee-Änderungen | Fee je Trade persistieren, MinEdge-Check dynamisch |
| Konflikt mit Copytrading-Sync | Eigener Account (Abschnitt 1) |
| On-Chain-Redeem-Fehler | Separate Phase, Testmarkt zuerst, Balance-Verifikation, clob.md-Regeln |
## 5. Offene Entscheidungen
1. Eigener Account: neuer Polymarket-Account nötig — wer legt ihn an, wie viel
Startkapital?
2. Preisband-Default (0.900.98) und `MinEdgePct` — mit Demo-Daten validieren.
3. Nethereum vs. Raw-RPC für On-Chain-Calls (Empfehlung: Nethereum, weniger
Fehlerfläche beim ABI-Encoding).
4. Taker-Fallback beim Einstieg erlauben oder strikt Maker-only?
@@ -0,0 +1,380 @@
# Umsetzungsplan: Modularisierung PolyTraderSharp
> **Stand: 22.08.2026 — Ziel erreicht, Plan weitgehend abgearbeitet.**
> Ursprünglicher Plan vom 01.07.2026. Ziel war der Umbau des monolithischen
> WinForms-Copytraders in ein modulares System mit schlankem **Core** und unabhängigen
> **Modulen**. Erstes Modul: **Copytrading**.
>
> **Was daraus geworden ist:** Der Core steht, und es sind **vier** Module entstanden —
> CopyTrading, ResolutionFarming, Supervisor und Accounting. Die Phasen 06 sind
> abgeschlossen, der WinForms-Monolith ist am 22.08.2026 vollständig aus dem Repo entfernt.
> Offen sind nur noch zwei bewusst zurückgestellte Nacharbeiten aus Phase 7.
>
> ⚠️ **Die Phasen 36 waren bis zum 22.08.2026 fälschlich als „IN ARBEIT" bzw. offen
> markiert** — die Arbeit war längst getan, der Plan wurde nur nicht mitgezogen. Die
> Häkchen unten sind am Code nachgeprüft, nicht aus der Erinnerung gesetzt.
---
## 1. Leitprinzipien
1. **Core kennt keine Module.** Der Core stellt nur Basis-Infrastruktur bereit
(Host, DB/Persistenz, Settings, Jobs, Logging, API-Clients, Benachrichtigungen,
Modul-Contract). Er hat **keine** Referenz auf irgendein Modul.
2. **Module hängen nicht voneinander ab.** Jedes Modul referenziert nur den Core.
Ein Modul kennt kein anderes Modul. Dies wird durch getrennte Projekte
**zur Compile-Zeit erzwungen**.
3. **Jede Phase lässt die App lauffähig und baubar zurück.** Kein „Big Bang".
Nach jeder Phase: Debug-Build grün, App startet, Copytrading funktioniert.
4. **WinForms bleibt.** Die GUI-Anforderung ist fix. Module tragen ihre eigenen
UI-Tabs zur Shell bei.
5. **Sicherheit vor Geschwindigkeit beim Refactoring.** CLOB-Integration ist
hochkritisch (siehe `.agents/rules/clob.md`) bei Berührung besonders sorgfältig,
jede Änderung mehrfach prüfen. Rollback jederzeit über Git möglich.
---
## 2. Zielarchitektur
### 2.1 Solution-Struktur (Multi-Projekt)
```
PolyTraderSharp.sln
├── PolyTrader.Core (Class Library, net8.0-windows)
│ • Generic Host / Bootstrap-Infrastruktur
│ • Persistenz: Repository-Interfaces + Implementierung (EF Core)
│ • Settings (appsettings.json + IOptions) + Core-Settings-Sektion
│ • JobManager, Logging (TerminalLogger / ILogger-Sink)
│ • Polymarket-Infrastruktur: PolymarketApiService, PolymarketClobClient,
│ PolymarketWssClient, AlchemyWebsocketService
│ • Querschnitt: MullvadVpnService, ThreemaService
│ • Eigene Trading-Accounts (AccountState) — die Konten, mit denen WIR traden
│ • Generischer Trade-Log (modulübergreifend auswertbar)
│ • Gesamt-Dashboard (Overview über alle Module)
│ • Core-State (generisch): MarketCache, globale Betriebsschalter
│ • IPolyTraderModule-Contract + Modul-Registry
├── PolyTrader.Modules.CopyTrading (Class Library, net8.0-windows)
│ • TraderMonitorService (Signalquelle)
│ • CopyTradingEngine (Ausführung)
│ • MasterTraderAnalyticsJob, TraderAnalyticsJob
│ • Models: TrackedTrader (kopierte Master-Trader), CopySignal,
│ CopyTradeRecord, TraderAnalyticsResult, MasterTraderHistoryRecord
│ • Copytrading-State: Traders (Master), MasterTraderPositions,
│ PendingOrderTimestamps, TraderAnalyticsCache
│ • Channels: CopySignal, ClosedTrade
│ • Eigener Copytrading-Trade-Log (Detail-Auswertung kopierter Trades,
│ zusätzlich zum generischen Core-Log)
│ • Eigene UI-Tabs (Master/Slave-Verwaltung, Modul-Analyse, Closed Trades)
│ • Eigene Modul-Settings-Sektion
│ • CopyTradingModule : IPolyTraderModule
├── PolyTrader.App (WinForms .exe, net8.0-windows)
│ • Program.cs: Host-Bootstrap, lädt Core + registrierte Module
│ • Shell-Form (frm_main reduziert auf Rahmen: Terminal, Jobs, Settings-Tab)
│ • Referenziert Core + alle aktiven Module
└── PolyTrader.Tests (xUnit, optional — spätere Phase)
• Risk-/Entscheidungslogik des Copytrading-Moduls
```
### 2.2 Modul-Contract (Entwurf)
```csharp
public interface IPolyTraderModule
{
string Name { get; } // "CopyTrading"
string DbPrefix { get; } // Namespace für DB-Objekte, z.B. "ct_"
void RegisterServices(IServiceCollection services, IConfiguration config);
void RegisterUi(IModuleUiHost uiHost); // Modul hängt seine Tabs ein
Task StartAsync(CancellationToken ct); // läuft NACH Core-Hydration
Task StopAsync(CancellationToken ct);
}
```
- **Discovery:** Die App registriert Module explizit in `Program.cs`
(`services.AddPolyTraderModule<CopyTradingModule>()`). Kein Runtime-Assembly-Scanning
(bewusst einfach gehalten; kann später zum Plugin-System ausgebaut werden).
- **Feature-/Lizenz-Gating:** `IPolyTraderModule` ist die natürliche Schnittstelle,
um Module später per Lizenz zu aktivieren/deaktivieren (vgl. `lizenssystem.md`).
### 2.3 State-Aufteilung
`TradingState` wird zerlegt:
| Feld | Ziel |
|------|------|
| `MarketCache` | **Core** (generischer Markt-Cache) |
| `GlobalTradingPaused`, `LiveTradingMode`, `DemoTradingMode` | **Core** (globale Betriebsschalter) |
| `Accounts` (unsere eigenen Trading-Accounts, `AccountState`) | **Core** — die Konten, mit denen WIR traden; modulübergreifend nutzbar |
| `Traders` (kopierte Master-Trader, `TrackedTrader`) | **CopyTrading-Modul** |
| `MasterTraderPositions`, `PendingOrderTimestamps`, `TraderAnalyticsCache`, `TotalCopyTrades`, `GlobalPnl` | **CopyTrading-Modul** |
> Entschieden (2026-07-01): Eigene Trading-Accounts liegen im **Core** (auch künftige
> Module handeln über dieselben Konten). Die **kopierten** Master-Trader (`TrackedTrader`)
> sind ein Copytrading-Konzept und liegen im **Modul**.
### 2.4 Trade-Logging (zweistufig)
Zwei unabhängige, parallel geführte Logs:
1. **Generischer Core-Trade-Log** (`TradeRecord` + `ITradeLogRepository`):
modulneutrale Felder (ModulName, AccountId, Markt, Side, Entry/Exit, PnL, Zeiten,
ExitReason). Ermöglicht die **modulübergreifende** Gesamtauswertung. Jedes Modul,
das Trades ausführt, schreibt hier einen Eintrag.
2. **Copytrading-spezifischer Log** (`CopyTradeRecord`, im Modul): erweitert die
generischen Felder um Copytrading-Details (`SourceTraderId`, `SourceTraderName`,
Master-Adresse, Signal-Herkunft) für die **detaillierte** Copytrading-Analyse.
Beim Schließen eines kopierten Trades schreibt das Modul **beides**: einen generischen
Eintrag in den Core-Log und einen Detaileintrag in seinen eigenen Log.
### 2.5 Dashboard & Analyse
- **Core-Gesamt-Dashboard:** Overview über alle Module (aggregierte PnL, Kontostände,
offene Positionen, grobe Kennzahlen je Modul) — gespeist aus dem generischen Core-Log.
- **Modul-Analyse:** Jedes Modul liefert seine eigene Detailansicht (Copytrading:
Trader-Winrates, kopierte Trades, Master-Performance) — gespeist aus dem Modul-Log.
### 2.6 Settings
- **Core-Settings-Sektion:** globale/Infrastruktur-Einstellungen (DB, VPN, Threema,
Betriebsschalter).
- **Modul-Settings-Sektion:** jedes Modul trägt seine eigene Sektion zum Settings-Tab bei
(analog zu den UI-Tabs), registriert über den `IPolyTraderModule`-Contract.
- **API-Keys sind Modul-Settings:** Alchemy-/Polymarket-WSS-Keys wandern von der globalen
`ServerSettings` in die jeweilige Modul-Settings-Sektion (siehe 2.7).
### 2.7 Streaming / WebSocket-Architektur *(Entscheidung 2026-07-01)*
**Prinzip:** Der **Core stellt die WSS-Verbindungs-Klasse als wiederverwendbare Fähigkeit**
bereit — **keinen** geteilten Singleton-Stream. Jedes **Modul erzeugt seine eigene Instanz**
mit **eigenem API-Key und eigenem Filter**.
**Begründung:** Blockchain-/WSS-Streams werden modulspezifisch **gefiltert** (sonst viel zu
umfangreich). Ein einzelner, Core-gesteuerter Stream, auf den mehrere Module gleichzeitig
zugreifen, wäre für jedes einzelne Modul mit nutzlosen Informationen geflutet.
**Aufteilung des heutigen `AlchemyWebsocketService`:**
- **Core** (`PolyTrader.Core.Streaming`): Verbindungs-Mechanik — `ClientWebSocket`, `eth_subscribe`,
Empfangs-Loop, Decode, Reconnect/429-Backoff, Health. Parametrisiert über eine
`BlockchainWssSubscription` (Contract, Topics, Adress-Filter) + RPC-URL/Key. Als **Factory**
(`IBlockchainWssClientFactory.Create()`), damit jedes Modul eine eigene Instanz bekommt.
- **Modul** (`CopyTrading`): `CopyTradingBlockchainListener : BackgroundService`, der eine
Core-WSS-Instanz mit dem Copytrading-Filter (Wallets der getrackten Master-Trader) + dem
Modul-eigenen Alchemy-Key betreibt, den gefilterten Substream konsumiert und selbst reagiert
(TraderMonitor-Poll, Re-Subscribe bei Trader-Listen-Änderung).
Analog für den Polymarket-User/Market-WSS (`PolymarketWssClient` → Core-Verbindungsklasse +
Modul-Listener). `IsAlchemyHealthy` (heute im Core-State) wird zum Health-Signal der jeweiligen
Modul-Instanz.
---
## 3. Persistenz-Strategie
- **Zielrichtung: Wechsel auf MySQL** via **EF Core + Pomelo.EntityFrameworkCore.MySql**,
gekapselt hinter Repository-Interfaces im Core.
- **Begründung:** DB liegt off-hot-path (Live-Pfad ist RAM-only) → kein Performance-Nachteil.
Gewinn: saubere relationale Tabellen statt Collection-per-Account + Shim, ACID,
EF-Migrations, Standard-Backups.
- **Risikoarm durch Reihenfolge:** Zuerst Repository-Abstraktion einziehen (Phase 3),
MySQL-Umstieg als eigene späte Phase (Phase 6). Die Modularisierung ist davon
entkoppelt und nicht blockiert.
- **Aufräumen:** LiteDB-Paket, `data.db` und `MongoDbLiteDBShim` entfallen nach der Migration.
- **ORM: Entity Framework Core** (entschieden) — Migrations + wenig Boilerplate.
---
## 4. Phasenplan
> Jede Phase endet mit grünem Debug-Build + lauffähiger App + Git-Commit.
### Phase 0 — Fundament: Versionskontrolle & Aufräumen *(ABGESCHLOSSEN 2026-07-01)*
- [x] `git init` (Branch `main`), `.gitignore` (bin/, obj/, .vs/, *.user, *.db, server_settings.xml, agentspace/antigravity/, .claude/settings.local.json).
- [x] Alle 11 `.bak*`-Dateien entfernt (per `-f` im Baseline-Commit `475d396` archiviert, danach entfernt → rekonstruierbar).
- [x] Tote Stubs entfernt: `services/database.cs`, `services/settings.cs`, `polymarket/*.cs`.
- [x] Threema-Lib unter `libs/` vendored (nested `.git` entfernt).
- [x] Baseline-Commit `475d396` + Cleanup-Commit `f76ad73`; Debug-Build 0 Fehler verifiziert.
### Phase 1 — Multi-Projekt-Gerüst anlegen *(ABGESCHLOSSEN 2026-07-01, Commit `4f130ff`)*
- [x] Drei Projekte: `PolyTrader.App` (umbenanntes WinForms-Projekt, Root),
`src/PolyTrader.Core`, `src/PolyTrader.Modules.CopyTrading` (net8.0-windows).
- [x] Referenzen: App → Core + CopyTrading; CopyTrading → Core; Core → nichts.
- [x] App-csproj: `src\**` vom Globbing ausgeschlossen (keine Glob-Kollision);
RootNamespace auf `PolyTraderSharp` gepinnt (schützt .resx/Namespaces).
- [x] Threema-Lib-Referenz bleibt im App-Projekt (wandert in Phase 4 zu Bedarf in Core).
- [x] **Ergebnis:** Solution-Build 0 Fehler, Code liegt weiterhin im App-Projekt.
- [ ] *Offen für spätere Phasen:* NuGet-Pakete beim Code-Umzug auf Core/Modul verteilen.
### Phase 2 — Konfiguration externalisieren *(ABGESCHLOSSEN 2026-07-01, Commit `e312fbb`)*
- [x] `appsettings.json` eingeführt (Mongo-Connection + DB-Name), Copy-to-Output.
- [x] `DatabaseOptions` im Core, via `IOptions<T>` gebunden; hart codierte Strings
aus `Program.cs` entfernt.
- [x] Startup-Cleanup-Hack aus `Main()` entfernt und gekapselt nach Host-Build über
die konfigurierte DB neu verankert.
- [ ] *Offen (bewusst später):* Alchemy-Key / Mullvad-Account / Threema bleiben vorerst
im GUI-editierbaren `server_settings.xml` (kein Konflikt mit Settings-Tab).
### Phase 3 — Persistenz-Abstraktion *(ABGESCHLOSSEN — nachgetragen 22.08.2026)*
- [x] **3a** (`8b3264f`): Core-Modelle `AccountState`/`Position`/`MarketData` in den Core
verschoben (Namespace `PolyTraderSharp.Models` beibehalten), MongoDB.Driver-Paket im Core.
- [x] **3b** (`ed5d6e3`): Repository-Interfaces + Mongo-Implementierungen im Core
(`IAccountRepository`, `IMarketRepository`, `IPositionRepository`), `AddCorePersistence()`.
- [x] **3c** (`7197f9b`): Unkritische Call-Sites migriert (MarketSyncService, PolymarketWssClient).
- [x] **3d — Hot-Path** (`a0e367e`, `0c6fc6a`): TraderMonitorService + CopyTradingEngine
auf `IPositionRepository`/`IMarketRepository`/`IAccountRepository` umgestellt.
`_db` aus CopyTradingEngine komplett entfernt; Verhalten unverändert.
- [x] **frm_main-UI:** erledigt — `frm_main` existiert nicht mehr (Avalonia-Portierung,
Rest mit dem WinForms-Ausbau am 22.08.2026 gelöscht).
- [x] Generisches `ITradeLogRepository` (Core) + `ICopyTradeLogRepository` + `ITrackedTraderRepository`
(Modul) sind gebaut und in Gebrauch.
- [x] **Ergebnis erreicht:** Kein direkter DB-Zugriff mehr außerhalb der Repository-Schicht.
### Phase 4 — Core herauslösen *(ABGESCHLOSSEN — nachgetragen 22.08.2026)*
- [x] **4.1** (`9039af8`): TerminalLogger, JobManager, JobStatusRow → Core;
toten Stub `logging.cs` gelöscht.
- [x] **4.2** (`eebe992`): PolymarketClobClient → Core (+ Nethereum.Web3), reines Verschieben.
- [x] **4.3** (`8c126cd`): ServerSettings → Core.
- [x] **4.4** (`a1ce3fc`): `IPolyTraderModule`-Contract im Core (UI-Teil auf Phase 5 vertagt).
- [x] **4.5** (`c88eac5`): Startup-Reihenfolge-Fix — `StartupHydrationService` (IHostedService,
als erster registriert) hydriert Accounts/Trader vor den Trading-Services;
`frm_main.LoadDatabaseAndState` entfernt.
- [x] **4.6a** (`9c068b4`): CopySignal + PolymarketApiService → Core.
- [x] **4.7** (`0ffc041`): MullvadVpnService + ThreemaService (+ Threema-Lib-Ref) → Core;
toter Stub `mullvad.cs` gelöscht.
- [x] **Ehemals blockiert durch den TradingState-Split:** aufgelöst. `MarketSyncService` und die
Streaming-Infrastruktur liegen im Core (`src/PolyTrader.Core/Streaming/`); `PolymarketWssClient`
ist bewusst im CopyTrading-Modul geblieben, weil er modulspezifisch filtert.
- [x] **Ergebnis:** Core baut eigenständig und enthält jetzt: Modelle (Account/Position/
Market/CopySignal/JobStatusRow/ServerSettings), Repository-Schicht, Config, Logging,
JobManager, CLOB-Client, API-Service, Mullvad, Threema, IPolyTraderModule.
**Stand nach Phase 4:** Der Core ist substanziell und eigenständig. Was noch in der App liegt:
Modul-Services (TraderMonitor, CopyTradingEngine, Analytics-Jobs), die TradingState-abhängige
Infra (MarketSync, Alchemy, WSS, Snapshot), PersistenceService, StartupHydrationService,
der Shim, `TradingState`, `frm_main` und die Modul-Modelle (TrackedTrader, TraderAnalyticsResult,
MasterTraderHistoryRecord, ClosedTrade, DashboardRow). Der **TradingState-Split** ist der
Dreh- und Angelpunkt für Phase 5.
> **Entscheidung (2026-07-01):** `CopySignal` wird ein **Core**-Typ (generisches Markt-Trade-
> Signal). Das entkoppelt die Polymarket-Infrastruktur sauber in den Core. Der Channel/Workflow
> bleibt Copytrading. Umbenennung zu `TradeSignal` optional/später.
### Phase 5 — CopyTrading-Modul herauslösen *(ABGESCHLOSSEN — nachgetragen 22.08.2026)*
- [x] **5.1** (`55050a1`): Modul-Modelle (TrackedTrader, TraderAnalyticsResult,
MasterTraderHistoryRecord) ins Modul verschoben.
- [x] **5.2** (`f8d395b`): **TradingState-Split** — Core `TradingState` (globale Schalter,
Accounts, MarketCache, GlobalPnl) vs. `CopyTradingState` im Modul (Traders,
MasterTraderPositions, TraderAnalyticsCache, TotalCopyTrades, PendingOrderTimestamps,
SixSharesMinimum); 10 Konsumenten umgestellt. Modulgrenze auf State-Ebene gezogen.
- [x] **5.3a** (`f5e7eaf`): MarketSyncService → Core (nur Core-State).
- [x] **5.3-WSS 1/2** (`3be75c0`): Core-WSS-Verbindungsklasse extrahiert
(`IBlockchainWssClient` + Factory + Modelle in `PolyTrader.Core.Streaming`);
`AlchemyWebsocketService` nutzt sie via Factory. Verhaltensneutral.
- [x] **ClosedTrade-Migration** (`88fc982`): `ClosedTrade` → Modul, `ICopyTradeLogRepository`
(+ Mongo-Impl) im Modul; `closed_trades`-Zugriffe der Services (TraderMonitor,
Persistence, WSS, TraderAnalytics) auf das Repo umgestellt. TraderMonitor nutzt kein
`_db` mehr. frm_main/Program.cs bleiben auf `_db`.
- [x] **5.3b:** erledigt — `TraderMonitorService`, `CopyTradingEngine`, `TraderAnalyticsJob`,
`MasterTraderAnalyticsJob` und `PersistenceService` liegen unter
`src/PolyTrader.Modules.CopyTrading/Services/`.
- [x] **5.3-WSS 2/2:** erledigt — `PolymarketWssClient` liegt im Modul, die generische
Verbindungsschicht (`IBlockchainWssClient` + Factory) im Core.
- **UI-Trennung (Launcher-Modell, entschieden 2026-07-01):** Hauptfenster = schlanke
Startleiste; jede Ansicht öffnet als eigenständiges Fenster. Module liefern designbare
`UserControl`s via `IPolyTraderModule.RegisterUi` / `IModuleUiHost`. `frm_main` bleibt
übergangsweise als „Legacy-UI" per Button erreichbar, bis alle Views extrahiert sind.
- [x] UI-Contract im Core (`IModuleUiHost`, `ModuleView`, `RegisterUi`) — `26dd68a`.
- [x] Proof-of-Pattern: `LauncherForm` + `ViewHostForm` + `ShellUiHost` + erste View
`TerminalView` (designbar); App startet Launcher — `3503bbb`.
- [x] Alle Views extrahiert. Das Launcher-Modell wurde inzwischen selbst wieder abgelöst:
seit dem UI-Redesign gibt es **eine** Shell mit Seitenleiste
(`src/PolyTrader.App.Avalonia/Views/ShellWindow.axaml`) statt vieler Einzelfenster.
- [x] `frm_main` (Legacy) entfernt.
- [x] `CopyTradingModule : IPolyTraderModule` ist implementiert — ebenso
`ResolutionFarmingModule`, `SupervisorModule` und `AccountingModule`.
- [x] Dualer Trade-Log verdrahtet (Core-Log + Copytrading-Log).
- [x] Collection-Namensbug (`traders` vs. `trackers`) mit der MySQL-Migration hinfällig —
relationale Tabellen mit festem Schema statt Mongo-Collections.
- [x] **Ergebnis erreicht:** Copytrading ist ein eigenständiges, entfernbares Modul; drei
weitere Module sind nach demselben Muster entstanden.
### Phase 6 — MySQL-Migration *(ABGESCHLOSSEN — nachgetragen 22.08.2026)*
- [x] EF Core + Pomelo eingerichtet, relationales Schema modelliert; Tabellen mit den Präfixen
`core_` und `mod_`, Migrationen je Projekt unter `Persistence/Ef/Migrations/`.
- [x] Repository-Implementierungen auf EF Core hinter den bestehenden Interfaces.
- [x] Migration der Altdaten über die CLI-Schalter `--migrate-json` und `--verify-mysql`
(statt eines Skripts in `agentspace/scripts`).
- [x] Mongo vollständig entfernt: kein `MongoDB.Driver`- oder `LiteDB`-Paket mehr in irgendeiner
`.csproj`, kein Shim. `data.db` und die `MongoDB/`-Exporte sind am 22.08.2026 auch lokal
gelöscht worden.
### Phase 7 — Nacharbeiten *(teilweise erledigt — Stand 22.08.2026)*
- [x] Test-Projekt: Risk-/Entscheidungslogik als reine Funktionen extrahiert und getestet —
**476 Tests** grün.
- [x] Leere `catch {}` beseitigt — in `src/` findet sich kein einziger leerer Catch-Block mehr.
- [x] Secrets-Verschlüsselung umgesetzt, allerdings **nicht mit DPAPI**: DPAPI ist Windows-only und
hätte die Linux-Portierung blockiert. Stattdessen AES-GCM at-rest über
`Security/SecretProtection.cs` + `EncryptedStringConverter`, Schlüssel aus
`POLYTRADER_MASTER_KEY` (siehe `Security/MasterKeyResolver.cs`).
- [ ] **Offen:** God-Methoden splitten (`PollLiveAccountsAsync`, `ProcessAccountOrderAsync`);
duplizierte Closed-Trade-Erzeugung zentralisieren.
- [ ] **Offen:** `TerminalLogger` auf `Microsoft.Extensions.Logging` + UI-Sink umstellen. Hängt mit
dem Zeitzonen-Punkt zusammen (Abschnitt C im Avalonia-Leitfaden): der Logger stempelt mit
`DateTime.Now` statt der konfigurierten `AppTimeZone`.
---
## 5. Datei-→-Ziel-Zuordnung (Referenz)
| Aktuell | Ziel |
|---------|------|
| `Program.cs` | PolyTrader.App |
| `frm_main.*` | PolyTrader.App (Shell) + Copytrading-Tabs → Modul |
| `frm_analytics.*` | PolyTrader.Modules.CopyTrading |
| `TradingState.cs` | aufgeteilt: Core + Modul |
| `services/PolymarketApiService.cs` | Core |
| `services/PolymarketClobClient.cs` | Core |
| `services/PolymarketWssClient.cs` | Core |
| `services/AlchemyWebsocketService.cs` | Core |
| `services/MullvadVpnService.cs`, `mullvad.cs` | Core |
| `services/ThreemaService.cs` | Core |
| `services/JobManager.cs`, `TerminalLogger.cs`, `logging.cs` | Core |
| `services/PersistenceService.cs` | Core (generischer Trade-Log-Writer); Copytrading-Detail-Writer → Modul |
| `services/MarketSyncService.cs`, `SnapshotService.cs` | Core |
| `Extensions/MongoDbLiteDBShim.cs` | Core (temporär), entfällt in Phase 6 |
| `services/CopyTradingEngine.cs` | Modul |
| `services/TraderMonitorService.cs` | Modul |
| `services/MasterTraderAnalyticsJob.cs`, `TraderAnalyticsJob.cs` | Modul |
| `Models/AccountState.cs`, `Position.cs`, `MarketData.cs`, `ServerSettings.cs`, `JobStatusRow.cs`, `DashboardRow.cs` | Core |
| `Models/ClosedTrade.cs` | aufgeteilt: generischer `TradeRecord` → Core, `CopyTradeRecord` (mit SourceTrader-Feldern) → Modul |
| `Models/TrackedTrader.cs`, `CopySignal.cs`, `TraderAnalyticsResult.cs`, `MasterTraderHistoryRecord.cs` | Modul |
| `services/database.cs`, `settings.cs`, `polymarket/*.cs` | löschen (Phase 0) |
| `*.bak*` | löschen (Phase 0) |
---
## 6. Getroffene Entscheidungen (2026-07-01)
1. **Eigene Trading-Accounts → Core**, **kopierte Master-Trader → Copytrading-Modul.**
2. **Zweistufiges Trade-Logging:** generischer Core-Log (modulübergreifend) **und**
zusätzlicher Copytrading-Detail-Log im Modul (siehe 2.4).
3. **ORM: Entity Framework Core.**
4. **Dashboard:** Core liefert Gesamt-Overview über alle Module; Module liefern
eigene Detail-Analysen (siehe 2.5).
5. **Settings:** getrennte Core- und Modul-Settings-Sektionen (siehe 2.6).
---
## 7. Risiken & Gegenmaßnahmen
- **CLOB-Regression:** Höchstes Risiko. Gegenmaßnahme: CLOB-Client möglichst unverändert
in den Core verschieben (nur Namespace/Referenzen), keine Logikänderung in der
Umstrukturierungsphase.
- **Startup-Race weiterhin aktiv, bis Phase 4:** Bis der Startup-Fix greift, bleibt das
bestehende Verhalten kein neues Risiko, aber früh angehen.
- **Datenmigration (Phase 6):** Server läuft produktiv. Migration mit Read-Only-Export +
Verifikation vor Umschaltung; Rollback-Pfad (Mongo bleibt bis Verifikation bestehen).
@@ -0,0 +1,98 @@
# Umsetzungsplan: Strategie-Drift-Erkennung für Master-Trader (B-S2)
> Stand: 2026-07-11
> Ziel: Verhaltens-Änderungen eines Masters erkennen, BEVOR sie sich im Copy-PnL
> niederschlagen. Die bestehende Auto-Pause (Copy-PnL-basiert) ist ein nachlaufender
> Indikator — bei 95-¢-Tradern sieht man den Schaden erst nach mehreren Verlusten.
> Verhalten läuft dem PnL voraus: Ein Wetter-Bot, der plötzlich Politik-Longshots
> kauft, hat die Strategie gewechselt, lange bevor die Verluste messbar sind.
> Modul: PolyTrader.Modules.CopyTrading (baut auf vorhandenem MasterTraderAnalyticsJob auf).
---
## 1. Der Verhaltens-Fingerprint
Je Master werden zwei Fenster verglichen: **Referenz** (30 Tage bzw. die von
Predictalytics gelieferte Baseline) vs. **aktuell** (7 Tage). Datenquelle: die
Activity-/History-Daten, die der `MasterTraderAnalyticsJob` bereits lädt
(`mod_copytrading_mt_history` + Data-API-Activity; für Preisband/Größe die
Activity-Items — Felder existieren in den bereits geparsten JSONs).
Fingerprint-Metriken (pure Klasse `Logic/TraderFingerprint.cs`, voll unit-getestet):
| Metrik | Definition | Drift-Beispiel |
|---|---|---|
| `TradesPerWeek` | Trade-Frequenz | Bot-Betreiber wechselt von 40 auf 400/Woche |
| `CategoryMix` | Einsatz-Anteil je Kategorie (Vektor) | Wetter-Bot kauft plötzlich Politik |
| `PriceBandMix` | Einsatz-Anteil je Einstiegs-Preisband (10-¢-Bänder) | Favoriten-Halter kauft Longshots |
| `MedianHoldHours` | Median Haltedauer (Kauf→Close/Resolution) | Halter wird Day-Trader |
| `SellRatio` | Anteil aktiv verkaufter Positionen | „Stur-Halter" beginnt zu verkaufen |
| `SizeP90Rel` | 90. Perzentil Positionsgröße relativ zur Referenz | Martingale-/Tilt-Muster |
### Drift-Score
Pro Metrik eine normierte Abweichung (für Anteils-Vektoren: L1-Distanz / 2 → 0..1;
für Skalare: `|akt ref| / max(ref, ε)` gekappt auf 1). Gesamt:
```
DriftScore = gewichtete Summe (Default-Gewichte: CategoryMix 0.3, PriceBandMix 0.25,
SellRatio 0.2, TradesPerWeek 0.1, MedianHoldHours 0.1, SizeP90Rel 0.05)
```
Schwellen (global in `CopyTradingState`, per PropertyGrid änderbar, mit
[Description]): `DriftWarnScore` (Default 0.25) und `DriftPauseScore` (Default 0.5).
**Mindeststichprobe:** unter 10 Trades im 7-Tage-Fenster keine Bewertung (Rauschen).
## 2. Aktionen bei Drift
| Stufe | Bedingung | Aktion |
|---|---|---|
| Beobachten | Score < Warn | nichts; Score in UI-Spalte sichtbar |
| **Warnen** | Warn ≤ Score < Pause | Threema-Meldung mit den 2 größten Abweichungen („Kategorie-Mix: Wetter 90→40 %, Politik 0→45 %"); Master in UI gelb |
| **Neu-Trades pausieren** | Score ≥ Pause UND `AutoPauseEnabled` | NEUE BUYs dieses Masters aussetzen (`DriftPaused`-Flag auf TrackedTrader, Engine-Check im BUY-Pfad analog ExitPending); offene Positionen + SELL-Handling laufen normal weiter; Threema; Reaktivierung manuell |
Bewusst: Drift pausiert nur **Neu-Käufe** — es verkauft nichts. Bestehende
Positionen sind Sache der normalen Exit-Mechanik (Halter: Resolution).
## 3. Umsetzung
### Slice D-1: Pure Logik + Persistenz
- `Logic/TraderFingerprint.cs`: `Compute(IEnumerable<TradeObservation>)`
Fingerprint; `Drift(reference, current)` → Score + Top-Abweichungen. Unit-Tests
(Vektor-Distanzen, Mindeststichprobe, Rand: leere Referenz).
- `TrackedTrader`: Felder `FingerprintBaselineJson` (Referenz, von Predictalytics
importierbar ODER selbst aus 30 Tagen berechnet), `DriftScore`, `DriftPaused`,
`DriftDetail` (Kurztext) + Migration.
- `MasterTraderHistoryRecord` erweitern um die dafür nötigen Felder (EntryPrice-Band,
Kategorie, Size, Haltedauer), sofern noch nicht vorhanden — beim History-Download
mit befüllen (Daten sind in den API-Antworten enthalten).
### Slice D-2: Job-Integration
- Im `MasterTraderAnalyticsJob` nach dem History-Download: Fingerprint aktuell (7 T)
vs. Referenz (30 T bzw. BaselineJson) → Score, Aktionen gemäß Tabelle.
- **Frequenz:** Der Job läuft 12-stündlich — für Drift zu träge. Leichten
Stunden-Tick ergänzen (nur Fingerprint-Neuberechnung aus bereits geladenen
History-Daten, KEINE zusätzlichen API-Calls; die 12-h-Läufe aktualisieren die
Rohdaten).
- Referenz-Handhabung: Baseline wird NICHT automatisch nachgezogen, solange eine
Warnung/Pause aktiv ist (sonst „lernt" die Referenz die Drift). Nach manueller
Entwarnung: Baseline auf aktuelles 30-T-Fenster zurücksetzen (Button in UI).
### Slice D-3: Engine + UI
- Engine-BUY-Pfad: `DriftPaused`-Check (analog `IsActive`), TradeReasoning-Log.
- `MastersTradersView`: Spalten DriftScore (mit Ampelfarbe) + DriftDetail;
Kontextmenü „Drift entwarnen + Baseline zurücksetzen".
## 4. Akzeptanzkriterien
1. Unit-Tests: konstruierte Drift-Szenarien (Kategorie-Wechsel, Frequenz-Explosion,
Longshot-Umstieg) erzeugen erwartete Scores; stabile Master bleiben < Warn.
2. Simulierter Kategorie-Wechsel in Testdaten führt zu `DriftPaused` + Engine
verweigert Neu-BUY mit nachvollziehbarem Log.
3. Kein zusätzlicher Data-API-Traffic durch den Stunden-Tick (nur DB/RAM).
4. Threema-Meldungen enthalten die konkreten Top-Abweichungen, nicht nur den Score.
## 5. Abgrenzung
- Ersetzt NICHT die PnL-Auto-Pause (Phase 3.3, bleibt) — Drift ist das Frühwarnsystem,
PnL-Pause das Sicherheitsnetz.
- Sniper-Metriken (Plan Phase 3.2, Median-Haltezeit via Activity-Pagination) sind ein
Spezialfall dieses Fingerprints — bei Umsetzung zusammenlegen statt doppelt bauen.
@@ -0,0 +1,455 @@
# Umsetzungsplan: Watchdog & LicenseLabrador in PolyTrader einbinden
> # ⛔ ABGELÖST — nicht mehr umsetzen
>
> **Dieser Plan wird nicht ausgeführt.** Watchdog und LicenseLabrador als getrennte Dienste
> sind durch das **Deploymentcenter** ersetzt, das Lizenz, Monitoring, Fehler-Reporting und
> Updates in einem Dienst zusammenfasst.
>
> **Maßgeblich ist stattdessen:**
> [UMSETZUNGSPLAN-Deploymentcenter-Integration.md](./UMSETZUNGSPLAN-Deploymentcenter-Integration.md)
>
> **Vollzogen am 22.08.2026** (Schnitt D-6): `LicenseLabrador.Client` ist aus `lib/nuget` und
> aus dem Quellen-Mapping in `NuGet.Config` entfernt, `Licensing/LicenseGate.cs` und
> `Ui/LicenseDialog.cs` sind mit dem WinForms-Ausbau gelöscht. Im Code findet sich
> LicenseLabrador nur noch als historische Erwähnung in Kommentaren und Plandokumenten.
> Die `Watchdog*`-Felder in `ServerSettings` sind **bewusst geblieben**: Schnitt D-1 hat sie
> auf die Deploymentcenter-API **umgewidmet** statt sie zu ersetzen — sie sind in Gebrauch.
>
> **Serverseitig noch offen:** `watchdog.mhdf.de` und `license.mhdf.de` abschalten. Das darf
> erst geschehen, wenn die Live-Abnahme des Deploymentcenters steht — vorher fehlt die
> Rückfallebene. Mit dem Abschalten erledigen sich auch die beiden Auflagen aus diesem Plan
> (Watchdog-Secrets rotieren, UTC/`NOW()`-Mix).
>
> Der Text unten bleibt als **Verlaufsdokument** stehen. Die Dateiverweise darin zeigen auf
> inzwischen gelöschte Pfade; ihr Inhalt liegt im Git-Tag `winforms-final`.
---
> Stand: 2026-07-28 · Grundlage: Review von `J:\Softwareprojekte\WatchDog` und
> `J:\Softwareprojekte\LicenseLabrador`.
> **Reihenfolge war bindend:** Teil A (Blocker in den Fremdprojekten) vor Teil B (Einbindung).
> Wer B ohne A baut, bindet bekannte Lücken fest in den Produktivbetrieb ein.
---
## 0. Kurzfassung der Bewertung
**LicenseLabrador (Server)** ist handwerklich gut: keine Secrets im Repo, Argon2id, CSRF in allen
Admin-Formularen, durchgängig Prepared Statements, Ed25519 über den Base64-Payload (umgeht das
Kanonikalisierungsproblem elegant), Nonce-Reflexion, Rate-Limit plus Failed-Key-Sperre, Audit-Log.
Der Server kann so bleiben. Die Schwächen liegen im **C#-Client**.
**Watchdog** ist funktional weit, hat aber Produktiv-Secrets im Git, ein hartkodiertes
Admin-Passwort und eine Autorisierung, die jeden Agent-Token wie einen Admin behandelt. Vor der
Anbindung von PolyTrader muss dort aufgeräumt werden.
Kritischster Einzelbefund für **unseren** Betrieb: die Hardware-ID von LicenseLabrador ist auf
einem Rechner mit Mullvad-VPN nicht stabil (siehe L-1). Ohne Fix sperrt PolyTrader sich selbst aus.
---
## 0a. Nachprüfung vom 29.07.2026
Beide Projekte wurden nach dem ersten Review nachgebessert. Stand jetzt:
**LicenseLabrador — integrationsbereit.** Die vier Blocker sind behoben (Änderungen liegen noch
uncommitted im Arbeitsverzeichnis, sollten also committet werden):
- **L-1 erledigt.** `HardwareId` nutzt jetzt `MachineGuid` (bzw. `/etc/machine-id`) als
Primärquelle, `ProcessorCount` ist raus, der MAC-Fallback filtert virtuelle Adapter
(Mullvad, WireGuard, Docker, Hyper-V, WSL) und sortiert deterministisch. Damit ändert ein
VPN-Wechsel die Hardware-ID nicht mehr — die Selbst-Aussperrung ist vom Tisch.
- **L-2 erledigt.** Der gecachte Envelope wird gegen die lokale `hardware_id` geprüft; kopierte
`state.dat`-Dateien werden verworfen.
- **L-3 erledigt.** `signatureTampered` liefert jetzt `TamperSuspected` statt still in die
Offline-Kulanz zu fallen.
- **L-5 erledigt.** Das HMAC-Secret wird aus der Hardware-ID abgeleitet, und `docs/SECURITY.md`
führt es korrekt als Laufzeit-Schutz gegen Property-Patching, nicht als kryptografische
Sicherheit.
- Das SDK baut fehlerfrei (eine `CS8604`-Warnung in `LicenseClient.cs:47`).
- **Offen:** L-4 (gecachte Endpoints haben weiter Vorrang vor der Konfiguration, `http://` wird
weiter akzeptiert) sowie L-6 bis L-13. L-4 entschärfen wir auf unserer Seite, indem wir
ausschließlich `https://`-Endpoints konfigurieren — da die Antwort signaturgeprüft ist, kann ein
untergeschobener Endpoint keine gültige Lizenz erfinden, sondern uns höchstens in die Offline-
Kulanz drängen. Kein Blocker mehr, aber Nachtrag wert.
**Watchdog — integrationsbereit mit zwei Auflagen.** Commits `1e453f8` und `404e184` haben die
Blocker abgearbeitet; nachgeprüft am 29.07.2026:
- **Erledigt in `1e453f8`:** W-2 (Backdoor entfernt, nur noch `password_verify`), W-3 (CSRF in
`admin.php` und `index.php`), W-4 (alle XSS-Vektoren escaped, inklusive `e.message`), **W-5**
(Token-Scoping greift: `validateToken()` wird benutzt, `$requireAdmin()` schützt `monitor/edit`,
`monitor/delete`, `icon/upload`), W-6 (Status-API nicht mehr offen), **W-13** (Alarm nur noch
bei Zustandswechsel, zusätzlich 120 s Dämpfung pro Quelle und Art).
- **W-14 erledigt in `404e184`.** `Evaluator.php` setzt einen Monitor nur noch auf `up`, wenn
`last_status === 'ok'` **und** die Metriken unauffällig sind. Zustandsmaschine
durchgespielt: dauerhaftes `status=error` bleibt auf `error` (ein Alarm, danach Ruhe);
Rückkehr auf `ok` erzeugt genau eine Entwarnung; Absturz → genau ein Down-Alarm;
`down` + Rückkehr mit `status=error` → ein `hard_error`. Die Flapping-Schleife ist weg.
- **W-22 erledigt.** `escapeHtml` liegt jetzt auf Dateiebene (`index.php:630`) vor allen
Verwendungen. Monitore ohne Metriken — unser Fall — brechen im Detail-Fenster nicht mehr ab.
- **W-23 erledigt.** `session_start()` steht am Anfang von `api.php`; der Admin-Session-Zweig
funktioniert.
- **W-24 erledigt.** `display_errors` und Stack-Traces hängen an `WATCHDOG_DEBUG=1`; API-Fehler
liefern sauberes JSON ohne interne Details.
- **W-1 teilweise erledigt — Auflage 1.** Aus dem getrackten `config/config.php` sind die echten
Werte raus (Platzhalter), `config.example.php` ist ergänzt, `config.local.php` war schon
gitignoriert und die Merge-Logik ist intakt. **Aber die Secrets stehen weiter in der
Git-History:** `git show 1e453f8:config/config.php` liefert DB-Passwort, `shared_key` und
`setup_token` im Klartext. Das lässt sich durch Bearbeiten von HEAD nicht heilen — **alle drei
müssen rotiert werden** (das FTP-Passwort aus `deploy_config.json` gleich mit, es ist dasselbe).
- **Auflage 2 — W-16 (UTC vs. `NOW()`).** Unverändert: `cli/tick.php:39,44,106`,
`ProxmoxPoller.php:56,117` und `admin.php:115,157` mischen UTC-Strings aus PHP mit dem
`NOW()` der DB-Session. Steht die MySQL-Zeitzone nicht auf UTC, verschieben sich
Job-Fälligkeit, Lock-Leases und der Log-Cleanup um den Offset. Entweder in der DB-Session
`SET time_zone = '+00:00'` setzen oder `NOW()` durchgängig durch UTC-Parameter ersetzen.
Nicht integrationsblockend, aber vor dem Produktivbetrieb zu klären.
- **Weiterhin offen, ohne Einfluss auf unsere Anbindung:** W-11 (`api.php:157` gibt im eigenen
`catch` weiter `$t->getMessage()` heraus — der neue Handler in `Autoload.php` macht es richtig,
wird hier aber umgangen), W-15 (`intervalSeconds` der PS-/Bash-Agents wird weiter ignoriert;
unser C#-Client sendet `interval` und ist korrekt), **W-17** (`self_ping` ist weiter ein
`echo`-Stub — es gibt also keinen Alarm, wenn der Watchdog selbst stirbt), W-7 bis W-10, W-12,
W-18 bis W-21.
**Konsequenz für die Reihenfolge:** Schritt 1 und 2 sind erledigt. **Teil B kann vollständig
beginnen** — B.1 bis B.3 (Heartbeat) und B.4/B.5 (Lizenz) sind nicht mehr blockiert. Parallel
abzuarbeiten: Secrets rotieren, W-16, W-17.
---
## Teil A — Blocker in den Fremdprojekten
### A.1 Watchdog — Sicherheit
| # | Befund | Datei | Maßnahme |
|---|---|---|---|
| W-1 | DB-Passwort, `shared_key`, `setup_token` als Defaults im **getrackten** `config/config.php` (auch in der Git-History) | `config/config.php:15,19,20` | Datei nach `config.php.example` umbenennen (Muster von LicenseLabrador), aus dem Index nehmen, **alle drei Secrets rotieren** |
| W-2 | Hartkodiertes Admin-Passwort, vor der DB-Prüfung ausgewertet | `public/admin.php:31` | Ersatzlos entfernen; nur noch `users`-Tabelle + `password_verify` |
| W-3 | Kein CSRF-Schutz im Admin — Monitor löschen, Proxmox-Target anlegen, Token widerrufen sind cross-site auslösbar | `public/admin.php` (alle POST-Zweige) | `Csrf`-Klasse aus LicenseLabrador übernehmen (1:1 portierbar) |
| W-4 | Stored XSS: Heartbeat-`message` und Disk-Namen landen ungefiltert per `innerHTML` im Dashboard | `public/index.php:707,731,744,770` | `textContent` statt `innerHTML`, oder clientseitig escapen |
| W-5 | Jeder gültige Agent-Token autorisiert **jeden** Endpunkt — auch `/api/monitor/delete`, `/api/monitor/edit`, `/api/link`, `/api/icon/upload` | `public/api.php:66-77` | Zwei Berechtigungsstufen trennen: Agent-Token nur für `heartbeat`/`event`; verwaltende Routen nur mit Admin-Session oder `shared_key`. `TokenManager::validateToken()` (prüft die Source-Bindung) wird derzeit **nirgends** aufgerufen — einbinden |
| W-6 | `/api/status`, `/api/events`, `/api/lineage` sind bewusst unauthentifiziert und zusätzlich von der Basic-Auth ausgenommen | `public/api.php:60`, `.htaccess:6` | Für uns nicht akzeptabel: „polytrader ist seit 14:03 down" ist eine verwertbare Information. Lesezugriff hinter Session **oder** eigenen Read-Token legen |
| W-7 | Installer-Download läuft **vor** dem Login-Check; `{{TOKEN}}`/`{{SOURCE}}` werden ungefiltert in ein PowerShell-Skript interpoliert, das als SYSTEM ausgeführt wird | `public/admin.php:58-84` | Hinter `$isLoggedIn` verschieben, Platzhalter strikt validieren (`[A-Za-z0-9_-]`), Host nicht aus `HTTP_HOST` ableiten |
| W-8 | Agent-Token als **maschinenweite** Umgebungsvariable, Default-URL `http://` | `agents/windows/install.ps1.tmpl:104`, `agents/*/watchdog-agent.*:9,4` | Token in eine ACL-geschützte Datei unter `C:\ProgramData\Watchdog`; HTTPS erzwingen |
| W-9 | SVG-Upload in denselben Origin, `mkdir(0777)` | `public/api.php:284-296`, `admin.php:240` | SVG entfernen oder als `Content-Disposition: attachment` ausliefern; `0755` |
| W-10 | Proxmox: `allowSelfSigned = true` als Default, `cert_fingerprint` nie benutzt, Token-Secret im Klartext in der DB | `src/ProxmoxClient.php:18,76`, `schema:87` | Fingerprint-Pinning umsetzen (steht so in HANDOFF §8), Secret verschlüsseln |
| W-11 | `500`-Antwort gibt `$t->getMessage()` an den Client (DB-Fehlerdetails) | `public/api.php:141-144` | Generische Meldung, Details ins `event_log` |
| W-12 | FTP-Deploy-Passwort im Klartext, identisch mit DB- und Admin-Passwort | `scripts/deploy_config.json` | Eigenes Passwort, rotieren |
### A.2 Watchdog — funktionale Fehler
| # | Befund | Wirkung |
|---|---|---|
| W-13 | `api.php:120` alarmiert bei **jedem** Heartbeat mit `status=error`/`warning` sofort | Ein dauerhaft fehlerhafter Dienst im 30-s-Takt erzeugt 120 Telegram-Nachrichten pro Stunde. Widerspricht §6 des Umsetzungsplans („nur bei Zustandswechsel") |
| W-14 | `upsertHeartbeat` erzwingt `state = IF(state="down","up",…)`, der Evaluator setzt bei frischem Heartbeat zusätzlich `error → up` („recovered") | Ein Dienst, der dauerhaft `status=error` meldet, pendelt im Minutentakt zwischen „recovered" und „hard_error" — mit Alarm bei jedem Wechsel |
| W-15 | Agents senden `intervalSeconds`, die API liest nur `interval`/`expected_interval_sec` | Der gemeldete Takt wird still verworfen, es bleibt bei 60 s. Der C#-Client sendet `interval` und ist zufällig korrekt |
| W-16 | PHP schreibt UTC-Strings, MySQL vergleicht mit `NOW()` (Session-Zeitzone) | Job-Fälligkeit, Lock-Leases und Log-Cleanup verschieben sich um den UTC-Offset, sofern die DB nicht auf UTC steht. Betrifft `cli/tick.php:39,44,106` und `admin.php:108` |
| W-17 | `self_ping` ist ein `echo`-Stub, `proxmox_smart` hat keinen Handler, `first_contact_deadline_utc` / `expect_running` / `ack_until_utc` werden nie ausgewertet | Der „Wer wacht über den Wächter"-Mechanismus fehlt komplett; der Nie-Start-Alarm ebenfalls. Beides sind Abnahmekriterien aus dem Plan |
| W-18 | `warning_digest` schickt HTML durch `notifyImmediate`, das per `htmlspecialchars` escaped | Im Telegram erscheinen `<b>`-Tags als Text |
| W-19 | Nur der Heartbeat-Zweig liegt im `try/catch`, danach läuft der Code ohne Fehlerbehandlung weiter | Uneinheitliche Fehlerbilder |
| W-20 | Kein Rate-Limit auf `/api/heartbeat` (Plan §14 fordert es) | Ein durchdrehender Client flutet die DB |
| W-21 | Keine Tests im gesamten Projekt (Plan §13 fordert Unit-Tests der Zustandsmaschine) | W-13/W-14 wären mit einem Test aufgefallen |
**Mindestumfang vor der Einbindung:** W-1, W-2, W-5, W-13, W-14, W-16. Ohne W-13/W-14 ist der
Watchdog für einen produktiven Trading-Bot unbrauchbar, weil jeder echte Fehler in einer
Nachrichtenflut untergeht.
### A.3 LicenseLabrador — Client
| # | Befund | Datei | Maßnahme |
|---|---|---|---|
| **L-1** | **Hardware-ID ist instabil.** Die „erste" aktive Netzwerkkarte wird per `FirstOrDefault` ohne stabile Sortierung gewählt, dazu `ProcessorCount` | `HardwareId.cs:47`, `:40` | **Blocker für uns.** Mullvad, Hyper-V und Docker legen virtuelle Adapter an und wieder ab — jede Änderung erzeugt eine neue HWID, damit eine neue Aktivierung, damit `max_activations` (Default 2) erschöpft: PolyTrader sperrt sich selbst aus. Fix: nur `MachineGuid` als Primärquelle; MAC nur als Fallback, wenn die Registry nichts liefert, und dann sortiert und auf physische Adapter gefiltert; `ProcessorCount` entfernen |
| L-2 | Offline-Cache ist maschinen-portabel: geprüft wird nur `payload.license_key == licenseKey`, **nicht** `hardware_id` und nicht `product` | `LicenseClient.cs:195` | Wer `state.dat` von einer aktivierten Maschine kopiert und das Netz blockiert, läuft bis zu 168 h auf beliebig vielen Rechnern. Der wirksamste Angriff auf das gesamte System — und mit zwei Vergleichen behoben |
| L-3 | Erkannte Signaturmanipulation (`signatureTampered`) wird verworfen und fällt still in die Offline-Kulanz | `LicenseClient.cs:132-135` | `LicenseState.TamperSuspected` zurückgeben und protokollieren |
| L-4 | Kein TLS-Zwang; Endpoints aus dem **Cache** haben Vorrang vor der Konfiguration | `LicenseClient.cs:276-292`, `:342` | `http://`-Endpoints ablehnen; konfigurierte Endpoints zuerst versuchen, gecachte nur als Ergänzung |
| L-5 | HMAC-„Härtung" mit Klartext-Konstante im Assembly, geprüft mit demselben Prozess-Secret | `LicenseClient.cs:23,172` | Schützt gegen nichts, was ein Angreifer nicht in derselben Sitzung mitpatcht. Als Obfuskation behalten, aber in `docs/SECURITY.md` nicht mehr als Schutzmaßnahme führen |
| L-6 | Offline-`.lic` ohne Hardware-Bindung bzw. ohne Key gilt für **jede** Maschine bzw. **jeden** Key | `LicenseClient.cs:232,242`, `offline_license.php:62` | Im Admin-Dialog `hardware_id` verpflichtend machen (Ausnahme nur mit expliziter Warnung); ein geleaktes Notfall-File ist sonst ein Universal-Crack bis `valid_until` |
| L-7 | Session-Cookie ohne `secure`-Flag | `server/src/Auth.php:11` | `'secure' => true` + HTTPS erzwingen |
| L-8 | `key_id` wird signiert und übertragen, vom Client aber ignoriert | `Signature.cs` | Ohne Auswertung ist keine Schlüsselrotation möglich, ohne alle Clients gleichzeitig zu tauschen. Für uns momentan verschmerzbar, aber notieren |
| L-9 | `LicenseClient` erzeugt einen `HttpClient` pro Instanz und ist nicht `IDisposable` | `LicenseClient.cs:28` | Socket-Leak bei wiederholter Instanziierung; `HttpClient` injizierbar machen |
| L-10 | `app_version` hartkodiert `"1.0.0"` | `LicenseClient.cs:325` | Aus `LicenseConfig` befüllen — der Server pflegt die Spalte bereits |
| L-11 | `strtotime()` liefert bei ungültigem Datum `false``valid_until: false` im JSON → Deserialisierung wirft → stilles „keine Lizenz" | `offline_license.php:55` | Datum validieren |
| L-12 | Nur 5 Unit-Tests; `EvaluateOfflineFallback`, Statusabbildung und Grace-Ablauf sind ungetestet | `ClientTests.cs` | Tests für L-1/L-2 mitliefern |
| L-13 | `create_admin.php`: Minimum 6 Zeichen, Passwort wird im Klartext angezeigt | `server/tools/create_admin.php` | Minimum 12, Eingabe verdecken |
**Mindestumfang vor der Einbindung:** L-1, L-2, L-3, L-4.
---
## Teil B — Einbindung in PolyTrader
### B.0 Leitentscheidungen
1. **Eigene Client-Implementierung für den Watchdog.** Der mitgelieferte `WatchdogTestClient`
deaktiviert die Zertifikatsprüfung vollständig (`WatchdogClient.cs:42`). Wir schreiben einen
schlanken `WatchdogHeartbeatService` in `PolyTrader.Core` — der HTTP-Vertrag ist trivial
(ein POST mit JSON und einem Header) und wir behalten die Kontrolle über TLS, Timeouts und
Fehlerverhalten.
2. **LicenseLabrador.Client als Projektreferenz**, nicht als kopierter Code — dann fließen
Fixes aus Teil A.3 automatisch ein. Voraussetzung: L-1 bis L-4 sind dort behoben.
3. **Kein `Environment.Exit` bei ungültiger Lizenz.** `docs/INTEGRATION.md` schlägt das vor; für
einen Trading-Bot mit offenen Positionen ist ein harter Abbruch gefährlich. Stattdessen:
Trading-Module gar nicht erst starten (Startfall) bzw. geordnet stoppen (Laufzeitfall).
4. **Secrets über `SecretProtection`.** Watchdog-Token und Lizenzschlüssel werden mit dem
bestehenden AES-GCM-Master-Key (`POLYTRADER_MASTER_KEY`) verschlüsselt abgelegt — dieselbe
Mechanik wie für Wallet-Keys.
5. **Heartbeat-Metriken bleiben schlank.** Version, aktive Module, Uptime. **Keine** Kontostände,
Positionen, Wallet-Adressen oder Trader-Namen — der Watchdog-Server liegt beim Hoster und
seine Status-API war bis W-6 öffentlich lesbar.
### B.1 Slice W-A — Heartbeat-Grundgerüst ✅ ERLEDIGT (29.07.2026)
Umgesetzt in [WatchdogHeartbeatService.cs](../../../src/PolyTrader.Core/Services/WatchdogHeartbeatService.cs)
mit 21 Tests in [WatchdogHeartbeatTests.cs](../../../tests/PolyTrader.Tests/WatchdogHeartbeatTests.cs).
Abweichungen und Ergänzungen gegenüber der Planung unten:
- **TLS-Zwang implementiert:** `TryBuildEndpoint` akzeptiert nur `https://`; `http://` ausschließlich
für Loopback-Adressen (lokales Testen). Damit kann der Agent-Token nicht versehentlich im
Klartext übertragen werden — der Grund, warum der mitgelieferte Testclient nicht verwendet wird.
- **Intervall wird nach unten begrenzt** (10 s), damit ein Fehlkonfigurierter Client den Watchdog
nicht flutet (W-20 ist serverseitig weiter offen).
- **Log-Entprellung:** nur der erste Fehlversuch und die Erholung werden protokolliert, nicht
jeder Tick. Sonst hätte ein längerer Watchdog-Ausfall das Terminal geflutet.
- **Settings-Quelle injizierbar** (`Func<ServerSettings>`), damit der Dienst ohne Datei testbar ist.
- **Token darf verschlüsselt sein:** `SecretProtection.Unprotect` reicht Klartext unverändert
durch und entschlüsselt `enc:v1:…`, wenn ein Master-Key gesetzt ist. Kein neuer Fehlerpfad,
wenn kein Master-Key existiert.
- **UI — vollständig über das Settings-Fenster bedienbar:** Enabled, URL, Source, Instance und
Intervall erscheinen automatisch im bestehenden `PropertyGrid` (Kategorie „Watchdog Monitoring").
Der **Agent-Token** ist bewusst `[Browsable(false)]` und wird über den Designer-Button
„Watchdog-Token setzen …" mit **maskierter Eingabe** gepflegt — kein Secret offen im Fenster und
kein Handanlegen an der `server_settings.xml`. Bei gesetztem Master-Key wird er über
`SecretProtection.Protect` verschlüsselt abgelegt, sonst mit deutlichem Hinweis im Klartext.
Die schreibgeschützte Anzeige `WatchdogTokenStatus` zeigt „nicht gesetzt" /
„gesetzt (verschlüsselt)" / „gesetzt (Klartext)". Zweiter Button: „Test-Heartbeat senden" —
nutzt die **gespeicherten** Werte, damit ein Erfolg keine Konfiguration bestätigt, die nicht
auf der Platte liegt.
- **Änderungen greifen ohne Neustart.** `ExecuteAsync` wertet die Konfiguration in jedem Durchlauf
neu aus (1-s-Poll, Sendezeitpunkt über eine Fälligkeit, der Takt bleibt also exakt) statt einen
`PeriodicTimer` mit fest verdrahtetem Intervall zu benutzen. Ein-/Ausschalten, Takt-, URL- und
Token-Änderungen wirken sofort nach dem Speichern; Zustandswechsel werden je Ursache genau
einmal geloggt. Zwei Tests fixieren das Live-Ein- und -Ausschalten.
Ursprüngliche Planung:
**Neu:** `src/PolyTrader.Core/Services/WatchdogHeartbeatService.cs` (`BackgroundService`)
- `ExecuteAsync`: `PeriodicTimer` im konfigurierten Takt (Default 60 s). **Jeder** Sendeversuch in
`try/catch` — ein nicht erreichbarer Watchdog darf PolyTrader niemals beeinträchtigen.
- Payload: `source`, `instance`, `type: "heartbeat"`, `status`, `message`, `interval`
(**nicht** `intervalSeconds`, siehe W-15), `group`, `metadata`.
- Header `X-Watchdog-Key` mit dem entschlüsselten Agent-Token.
- `HttpClient` aus `SocketsHttpHandler` wie die übrigen Clients in `Program.cs`, mit
Standard-Zertifikatsprüfung und 10 s Timeout.
- Registrierung in `Program.cs` neben `MarketSyncService`:
`services.AddHostedService<WatchdogHeartbeatService>();`
**Konfiguration** in `ServerSettings` (`src/PolyTrader.Core/Models/ServerSettings.cs`), analog zu
den Threema-/Mullvad-Feldern:
```
WatchdogEnabled bool = false
WatchdogUrl string = "" // https:// erzwungen
WatchdogSource string = "polytrader"
WatchdogInstance string = "prod-1"
WatchdogIntervalSeconds int = 60
WatchdogToken string = "" // via SecretProtection verschlüsselt
```
Bedienung über die bestehende `SettingsView` (Designer-fähig, partial + `.Designer.cs` — siehe
UI-Regel), mit einem „Test-Heartbeat senden"-Button.
**Abnahme:** Monitor erscheint im Watchdog-Dashboard, `last_seen_utc` läuft mit. PolyTrader startet
und arbeitet unverändert, wenn `WatchdogUrl` falsch oder der Server aus ist.
### B.2 Slice W-B — Status aus dem Log ableiten ✅ ERLEDIGT (31.07.2026)
Umgesetzt im `WatchdogHeartbeatService`. Abweichungen gegenüber der Planung:
- **Nur `Error` eskaliert**, keine Warnungs-Eskalation: App-Warnungen (VPN-Reconnect, „kein
Master-Key" …) sind zu häufig, um daraus einen Monitor-Alarm zu rechtfertigen. `Warning`/`Info`
lassen den Status auf `ok`. Das vermeidet auch eine Rückkopplung, wenn der Dienst selbst warnt.
- **Entprellung über ein Nachwirkfenster** (`ErrorStickyWindow = 5 min`): Nach einem `Error` meldet
der Heartbeat so lange `status=error`, bis 5 Minuten ohne neuen Fehler vergangen sind — dann
wieder `ok`. Kein Flackern error→ok→error, Erholung wird genau einmal gemeldet. (Der 30-Minuten-
Notbehelf aus der Planung entfällt, weil W-13 im Watchdog behoben ist und ohnehin nur bei
Zustandswechsel alarmiert wird.)
- **`Sanitize()`** entfernt Wallet-Adressen/Keys (`0x…`-Hex) und verschlüsselte Secrets
(`enc:v1:…`) und kürzt auf 200 Zeichen, bevor eine Fehlermeldung an den (extern gehosteten)
Watchdog geht. Beträge werden bewusst nicht gestrippt (würde die Meldung entwerten; der Kanal ist
nur für den Betreiber).
- Angedockt über `TerminalLogger.OnLogMessage` (synchron), Abmeldung in `Dispose`.
Ursprüngliche Planung: Der Heartbeat soll `status=error` melden, wenn PolyTrader in Not ist.
- `TerminalLogger.OnLogMessage` abonnieren, Fehler der letzten `n` Intervalle zählen.
- Ableitung: `Error` im Fenster → `status=error` mit der letzten Fehlermeldung als `message`;
`Warning``status=warning`; sonst `ok`.
- **Entprellen ist Pflicht**, solange W-13 im Watchdog nicht behoben ist: sonst löst jeder
Heartbeat eine eigene Telegram-Nachricht aus. Bis dahin `status=error` frühestens alle 30 Minuten
erneut senden.
- Meldungstexte kürzen (≤ 200 Zeichen) und **keine** Wallet-Adressen, Keys oder Beträge
durchreichen. Ein Filter über die bekannten Secret-Präfixe gehört dazu.
### B.3 Slice W-C — Lifecycle-Events ✅ ERLEDIGT (31.07.2026)
Umgesetzt im `WatchdogHeartbeatService` (`SendEventAsync``POST /api/event`):
- **Start:** `kind: "started"` mit App-Version, genau einmal, sobald der Dienst aktiv wird
(in der Sendeschleife, nach `AppHost.Start()`), vor dem ersten Heartbeat.
- **Geplantes Beenden:** `StopAsync` sendet `kind: "stopped_graceful"` (best effort, 3-s-Timeout,
blockiert das Herunterfahren nie). Läuft über den bestehenden Pfad `AppHost.StopAsync()` nach
`Application.Run` + den 10-Sekunden-Bestätigungsdialog.
- Damit unterscheidet der Watchdog „sauber beendet" (Monitor → `stopped`, kein Alarm) von
„abgestürzt" (Staleness → genau ein Down-Alarm) — der eigentliche Mehrwert.
- Tests decken „started"→„stopped_graceful" über den Dienst-Lebenszyklus sowie den
`/api/event`-Vertrag ab.
**Abnahme (manuell, live):** PolyTrader über die Menüleiste beenden → Monitor geht auf `stopped`,
**kein** Alarm. PolyTrader per Task-Manager abschießen → nach `Interval × Faktor + Grace` genau
**ein** Down-Alarm.
---
**Stand 31.07.2026:** Watchdog-Seite (B.1/B.2/B.3) und Lizenz-Seite (B.4, B.5 vereinfacht)
umgesetzt; 438 Tests grün, `--smoke-ui` ok. Offen nur noch die **live**-Abnahme (echter Server)
und die Fremdprojekt-Auflagen (Watchdog-Secrets rotieren, UTC/NOW(), `self_ping`-Stub).
### B.4 Slice L-A — Lizenzprüfung beim Start ✅ ERLEDIGT (31.07.2026)
Umgesetzt in `Licensing/LicenseGate.cs`, Startdialog in
`Ui/LicenseDialog.cs` (+`.Designer.cs`), Verdrahtung in
`Program.cs`, Schlüssel-/Statusfeld in
[ServerSettings.cs](../../../src/PolyTrader.Core/Models/ServerSettings.cs). Abweichungen und
Ergänzungen gegenüber der ursprünglichen Planung unten:
- **Ort:** `LicenseGate` liegt im **App-Projekt** (`PolyTraderSharp.Licensing`), nicht in Core.
Grund: `Program.Main` (App) ist der einzige Aufrufer, und so bleibt die (cross-repo)
SDK-Referenz auf `PolyTrader.App` beschränkt statt in Core alle Module zu belasten.
- **SDK-Einbindung:** `LicenseLabrador.Client` als **Cross-Repo-Projektreferenz** von
`PolyTrader.App` auf `..\..\LicenseLabrador\...` (Fixes am SDK fließen ohne Kopie ein). Baut
sauber, kein BouncyCastle-Konflikt mit Nethereum.
- **Slug `pt`** (nicht `polytrader`), Endpoint **`https://license.mhdf.de`**, eingebetteter
Public-Key. **End-to-End verifiziert** (ohne Aktivierung zu verbrauchen): `validate` mit
bewusst ungültigem Schlüssel liefert signiertes `not_found`/„Invalid license key" (⇒ Slug `pt`
bestätigt, Produkt existiert), Nonce wird reflektiert, und die Server-**Signatur verifiziert
gegen den eingebetteten Public-Key** (openssl Ed25519 „Verified Successfully"). Der Client wird
echte Antworten also akzeptieren.
- **Startablauf:** Master-Key wird jetzt **vor** dem Host-Build geladen (`ConfigureSecretProtection(bootLog)`),
dann der Gate. Bei `IsUsable == false` → modaler Dialog (Schlüssel eingeben, **Hardware-ID
anzeigen + kopieren**, „eingeschränkt starten"); bleibt es ungültig, wird die Modulliste
**leer** gebaut → nur die Core-Shell (Terminal/Settings) startet. **Kein `Environment.Exit`.**
`TamperSuspected` wird als nicht nutzbar behandelt und im Dialog benannt.
- **Konfiguration wie beim Watchdog-Token:** `ServerSettings.LicenseKey` (`[Browsable(false)]`,
bei Master-Key verschlüsselt) + Statusanzeige. Der Schlüssel `9KHEZ-…` ist bereits in
`server_settings.xml` hinterlegt → beim ersten echten Start validiert der Gate automatisch und
aktiviert auf **dieser** Maschine (kein Dialog nötig, falls gültig).
- **Ein Dialog für beide Wege (31.07.2026):** Der Settings-Toolstrip-Button „Lizenz prüfen /
setzen …" öffnet **denselben** `LicenseDialog` wie der Start, nur im Verwalten-Modus
(`startupContext:false`, Button „Schließen" statt „eingeschränkt starten"). Damit validiert man
auch aus den laufenden Einstellungen gegen den Server (Hardware-ID sichtbar/kopierbar); der
frühere einfache Text-Prompt entfällt. Ein gültig validierter Schlüssel wird verschlüsselt
gespeichert und gilt ab dem nächsten Start.
- **Verifikation:** Solution baut fehlerfrei, 430 Tests grün, `--smoke-ui` konstruiert zusätzlich
`license.dialog`. Eigene Unit-Tests für den Gate wurden bewusst **nicht** angelegt: er ist
dünner Leim über dem SDK (das seine eigenen Krypto-Tests mitbringt), und ein Test bräuchte eine
Referenz vom Testprojekt auf die WinExe. Stattdessen deckt der Smoke-Test Konstruktion/Compile ab.
- **Offen (bewusst):** L-4 bleibt serverseitige Hygiene — sicherstellen, dass die `endpoints`-
Einstellung im Lizenz-Admin **https oder leer** ist (gecachte Endpoints hätten sonst im SDK
Vorrang). Und die eigentliche **erfolgreiche** Aktivierung mit dem echten Schlüssel passiert
erst beim realen Start aus PolyTrader (mit dessen Hardware-ID) — von hier aus bewusst nicht
getestet, um keinen Aktivierungs-Slot auf der falschen Maschine zu verbrennen.
Ursprüngliche Planung:
**Neu:** `src/PolyTrader.Core/Security/LicenseGate.cs`
- Wird in `Program.Main` **nach** `ConfigureSecretProtection` und **vor** `AppHost.Start()`
aufgerufen — der Lizenzschlüssel liegt verschlüsselt und braucht den Master-Key.
- `LicenseConfig`: `ProductSlug = "polytrader"`, eingebetteter `PublicKeyBase64`,
Endpoints ausschließlich `https://`.
- Ergebnis:
- `IsUsable == true` → normaler Start. Bei `ValidOffline` zusätzlich eine sichtbare
Restlaufzeit im Launcher.
- `IsUsable == false` → Lizenzdialog (Schlüssel eingeben, Hardware-ID anzeigen und kopierbar
machen), Trading-Module werden **nicht** registriert. Der Rest der Shell darf starten, damit
Terminal und Settings erreichbar bleiben.
- `TamperSuspected` (nach L-3) wird deutlich angezeigt und wie „nicht nutzbar" behandelt.
**Wichtig:** Der Schlüssel gehört in die verschlüsselte Ablage, nicht in `appsettings.json`.
### B.5 Slice L-B — ~~Revalidierung im Betrieb~~ VERWORFEN (Entscheidung 31.07.2026)
**Es gibt bewusst KEINE Laufzeit-Revalidierung.** Richards Vorgabe: im laufenden Betrieb darf
lizenzseitig **gar nichts** passieren — die Software darf niemals mitten in einer Sitzung aufhören
zu arbeiten (offene Positionen, laufende API-Aufrufe). Die einzige Durchsetzung ist der
**Startup-Gate (B.4)**: beim nächsten Start einer ungültigen/widerrufenen/kopierten Lizenz startet
die Software nicht mehr voll, sondern zeigt die Aufforderung, eine (neue) gültige Lizenz anzugeben.
Kein `BackgroundService`, kein 12-h-Timer, kein Netzwerkaufruf während der Sitzung.
**Warum das reicht (Bedrohungsmodell = Schutz vor einfachem Kopieren auf ein fremdes System):**
Die Software läuft überwiegend auf eigenen Systemen. Geschützt werden soll nur gegen „jemand
kopiert die Software auf ein eigenes System und macht dort weiter". Das leistet der Startup-Gate
durch drei Mechanismen des SDK, **alle beim Start**:
1. **Hardware-gebundene Aktivierung** — ein fremdes System hat eine andere `MachineGuid` ⇒ andere
Hardware-ID ⇒ braucht einen eigenen Aktivierungs-Slot.
2. **`max_activations`** — sind alle Slots belegt, liefert der Server `activation_limit` ⇒ die
Kopie startet nicht (nur die Aufforderung erscheint).
3. **Hardware-gebundener Offline-Cache (L-2-Fix)** — ein mitkopierter `state.dat` gehört zu einer
fremden Hardware-ID und wird offline verworfen ⇒ die Kopie kann sich auch ohne Netz nicht
„durchmogeln".
**Zwei Stellschrauben im Lizenz-Admin (Produkt `pt`):**
- **`max_activations`** großzügig auf die Zahl der eigenen Maschinen + Reserve setzen (ein
Windows-Neuinstall erzeugt eine neue `MachineGuid` = neuer Slot; alte Aktivierungen im Admin
freigeben). Sonst sperrt man sich bei einem Reinstall selbst aus.
- **`default_cache_ttl_hours`** (Offline-Kulanz, Default 168 h/7 Tage) = wie lange eine **eigene**,
bereits aktivierte Maschine ohne Serverkontakt neu starten darf. Für rein eigene Systeme darf
das gern länger sein. Die Kopie profitiert davon nicht (Punkt 3).
**Verhalten bei fehlender/ungültiger Lizenz beim Start (aktuell umgesetzt):** modaler Dialog
(Schlüssel eingeben / Hardware-ID kopieren / „eingeschränkt starten"). Ohne gültige Lizenz startet
nur die Core-Shell (Terminal + Einstellungen), **keine Module** — die Software „arbeitet" also
nicht, bleibt aber bedienbar genug, um eine Lizenz einzutragen. Wer stattdessen ein hartes
Beenden statt des eingeschränkten Modus will, ist eine Ein-Zeilen-Änderung im Dialog (offen).
### B.6 Slice L-C — Predictalytics
Beide Dienste sind mandantenfähig ausgelegt: ein zweites Produkt `predictalytics` im
LicenseLabrador-Admin, ein zweiter Monitor im Watchdog. Der Heartbeat-Service ist bewusst
projektunabhängig geschrieben und lässt sich unverändert übernehmen. Fällt an, sobald
Predictalytics produktiv geht.
---
## Reihenfolge und Aufwand
| Schritt | Inhalt | Abhängigkeit |
|---|---|---|
| 1 | Watchdog A.1 (W-1, W-2, W-5) + A.2 (W-13, W-14, W-16) | — |
| 2 | LicenseLabrador A.3 (L-1 bis L-4) + Tests | — |
| 3 | B.1 Heartbeat-Grundgerüst + Settings-UI | 1 |
| 4 | B.2 Status aus dem Log | 3 |
| 5 | B.3 Lifecycle-Events | 3 |
| 6 | B.4 Lizenzprüfung beim Start | 2 |
| 7 | B.5 Revalidierung | 6 |
| 8 | Restliche Befunde aus A.1/A.2/A.3 | — |
| 9 | B.6 Predictalytics | 3, 6 |
Schritte 1 und 2 sind unabhängig voneinander und können parallel laufen.
---
## Offene Entscheidungen für Richard
1. **Wo läuft der Watchdog?** Aktuell ein Shared-Webserver bei derselben Hosting-Firma. Für den
Zweck („Außensicht auf einen Server-Crash") reicht das, solange es nicht dieselbe Maschine ist.
Der `self_ping` an healthchecks.io (W-17) fehlt aber noch komplett — bis dahin gibt es keinen
Alarm, wenn der Watchdog selbst stirbt.
2. **Lizenzierung für uns selbst oder für Kunden?** Wenn PolyTrader vorerst nur auf unseren
eigenen Maschinen läuft, ist die Offline-Kulanz von 168 h großzügig und L-2 entsprechend
entspannter. Sobald Dritte die Software bekommen, sind L-1, L-2 und L-6 harte Blocker.
3. **`max_activations`** sollte für unsere eigene Lizenz großzügig gesetzt werden (5+), solange
L-1 nicht behoben ist — sonst reicht ein VPN-Wechsel für eine Aussperrung.