Fruehjahrsputz 2/2: Dokumentation auf den tatsaechlichen Stand gebracht

Die Plandokumente waren durchweg veraltet: 85 von 91 Punkten im
UMSETZUNGSPLAN standen auf offen, obwohl der Code sie enthielt, und in
FIXPLAN-UI-Ranglisten war keine einzige der 18 Aufgaben abgehakt, obwohl
beide zugehoerigen Commits laengst im Zweig stecken. Alles abschnittsweise
gegen den Code geprueft und die Haekchen gesetzt - mit Belegstellen, damit
die naechste Pruefung nicht wieder bei null anfaengt.

Neu: STATUS.md als Einstiegsseite - wo das Projekt steht, was fertig ist,
was offen ist, und was beim Aufraeumen bewusst stehengeblieben ist. CLAUDE.md
verweist darauf.

Nachgezogen:
* UMSETZUNGSPLAN.md - 66 Punkte abgehakt. Offen bleiben A4 (Strategie-
  Klassifikation), B2 (Secrets) sowie C3/D1/D2 (SQL-Arbeiten des Nutzers).
* FIXPLAN-UI-Ranglisten.md - abgeschlossen bis auf den als "Optional"
  markierten Pfeilrichtungs-Punkt.
* FIXPLAN-TODO.md - Teil D und F abgehakt; die Test-Baseline "16 gruen"
  auf die heutigen 126 korrigiert. Offen: F5 und zwei Tests aus F6.
* FIXPLAN-G-Speicher.md - G1 bis G4 als erledigt vermerkt, Baseline "39/1"
  korrigiert, Pfad auf die nicht mehr existierende WinFormsHost/appsettings.json
  richtiggestellt.
* docs/PLAN-Linux-Portierung.md - der Watchdog-Warnhinweis war ueberholt
  (DcHeartbeatService meldet an /api/watchdog/v1/ping). Abschnitt 11 empfahl
  noch, mit Phase 0 zu beginnen; jetzt benennt er Phase 5 und 6 als das, was
  wirklich aussteht. Die Randnotiz zu Zugangsdaten in LicenseGuard.cs ist
  gegenstandslos, die Datei laeuft ueber den Deploymentcenter.Client.

Zwei Befunde, die keine Aufraeumarbeit sind und deshalb nur dokumentiert
wurden - beide in STATUS.md Abschnitt 3:

1. In src/Predictalytics.Api/appsettings.json steht ein echter
   OpenRouter-API-Key im Klartext, versioniert seit 7045002. Nicht
   eigenmaechtig entfernt: ohne Rotation beim Anbieter bringt das nichts
   (die History behaelt ihn), wuerde aber die KI-Analyse abschalten.
   Der Schluessel muss zurueckgezogen und neu ausgestellt werden.
2. Die Portierung ist auf keinem Linux-System je ausgefuehrt worden. Die
   Verifikation vom 2026-08-08 lief unter Windows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-23 12:30:42 +02:00
co-authored by Claude Opus 5
parent 55011644a3
commit 1f230fe12c
7 changed files with 381 additions and 112 deletions
+106 -60
View File
@@ -1,5 +1,51 @@
# Predictalytics Umsetzungsplan (Phase 1 & 2 + Storage-Optimierung)
> ## Stand 2026-08-23 — Phase 1 und 2 sind bis auf zwei Punkte umgesetzt
>
> Die Häkchen waren bis dahin nie nachgezogen worden: 85 Punkte standen auf offen,
> obwohl der Code sie längst enthielt. Am 2026-08-23 abschnittsweise gegen den Code
> geprüft und gesetzt. **Belege:**
>
> | | Belegt durch |
> |---|---|
> | **A1** PnL-Engine | `Infrastructure/Services/PositionPnLEngine.cs` — alle `TradeSide`-Zweige einzeln behandelt (`:157-217`), `Split`/`Merge` neutral, unrealisiert aus `CurrentPrice AvgCost` (`:266`), Gesamt-PnL `:290`. Entity `Domain/Entities/TraderPosition.cs`. Tests: `PositionPnLEngineTests.cs` |
> | **A2** Win-Rate | `CalculateMarketWinRates` (`PositionPnLEngine.cs:381`) — marktbasiert, kein `return 0` mehr |
> | **A3** Deep-Dive | `MarketOutcomePriceSnapshot` existiert und wird ausgewertet (`AnalyticsService.cs:88,390`). Die `50` ist jetzt **Rückfallwert bei fehlender Preishistorie** (`:456,457,474`), kein fester Neutralwert mehr |
> | **A5** Copytrading-Score | `Infrastructure/Services/CopytradingEstimator.cs` — **abweichend umgesetzt und besser als geplant**: statt geschätztem Liquiditäts-Fit misst der Score den echten Alpha-Verfall gegen das Markt-Tape (Slippage bei 10 s Folgelatenz, 70 %) plus Sizing-Konsistenz (30 %). Frequenz/Konzentration deckt stattdessen `IngestMode` + Trait `not_copyable_hf` ab. **Track-Record-Länge fließt nicht in den Score ein** |
> | **B1/B4** | 32 EF-Migrationen; Testprojekt existiert, **126 grün + 1 Skip** |
> | **B3** | `ScoringService.RecalculateAllScoresAsync:127` rechnet nur bei `CalculatedAt < LastTradesUpdatedAt` neu, Rank-Update gebatcht; eigener `ScoringAndAlertsWorker` mit 15-Minuten-Takt |
> | **B5** | `LoggingSetup.cs:90-133` — `retainedFileCountLimit` und `fileSizeLimitBytes` gesetzt |
> | **B6** | Alle fünf Bugs behoben: geschlossene Märkte nur noch **1×/Tag** (`MarketSyncWorker.cs:52`), Watchlist von der Löschung ausgenommen (`TraderRepository.cs:137`), `GetKnownPlatformTradeIdsAsync` filtert auf die Batch-IDs (`TradeRepository.cs:158`), `_syncSemaphore` in `AddOrUpdateAsync` (`MarketRepository.cs:31`), `break` durch einen Filter ersetzt (`TradeHistoryWorker.cs:200`) |
> | **C1/C2** | `TradeRetentionWorker` + `Application/Services/StorageGovernor.cs` (budgetabhängiges Fenster), Burst-Kompaktierung ab `TradeRetentionWorker.cs:148` |
>
> ### Was wirklich offen ist
>
> 1. **A4 — Strategie-Klassifikation** (Zeilen 4754). Der einzige größere unerledigte
> Block aus Phase 1. `StrategyType` entsteht weiterhin aus genau zwei Signalen
> (`AnalyticsService.cs:418` und, **dupliziert**, `ScoringService.cs:89`):
> `avgSize > 10000 → Whale`, `hedgingRate > 30 → Hedger`, sonst `Bot`/`Unknown`.
> Genau der Zustand, den A4 beheben sollte — der Großteil der Trader landet
> strukturell auf `Unknown`. Die Bausteine dafür liegen bereits fertig herum
> (`StrategyMetricsCalculator`, `TraderTraitCalculator`, `FingerprintSnapshotService`),
> sie speisen die Klassifikation nur nicht. Die Doppelung gehört beim Anfassen
> zusammengeführt.
> 2. **B2 — Secrets** (Zeilen 8486). Nicht mehr „kein akuter Risikofall": in
> `src/Predictalytics.Api/appsettings.json` steht ein **echter OpenRouter-API-Key im
> Klartext**, versioniert seit Commit `7045002`. Siehe Warnung unten.
> 3. **C3** (148153) und **D1/D2** (165173) sind DB-Arbeiten auf SQL-Ebene bzw. die
> Entscheidung über den Datenbank-Neustart — beides liegt beim Nutzer, nicht im Code.
> 4. **C1**, Zeile 137: Stichprobentest, wie weit `/activity` zurückreicht. Eine manuelle
> Prüfung, die sich im Code nicht belegen lässt — bewusst offen gelassen.
>
> ### ⚠️ Sicherheit — Handlungsbedarf
>
> `src/Predictalytics.Api/appsettings.json` enthält den OpenRouter-API-Key im Klartext
> und liegt so im Git-Verlauf und auf dem Gitea-Server. **Den Schlüssel bei OpenRouter
> zurückziehen und neu ausstellen** — ihn nur aus der Datei zu löschen hilft nicht, die
> Historie behält ihn. Danach über die Umgebungsvariable `OpenRouter__ApiKey` setzen;
> `WebApplication.CreateBuilder` liest Umgebungsvariablen bereits mit Vorrang vor
> `appsettings.json`, dafür ist keine Codeänderung nötig.
> **Wie wir das nutzen:** Wir arbeiten diese Liste Stück für Stück ab. Erledigte Punkte werden von `[ ]` auf `[x]` gesetzt. Reihenfolge ist absichtlich so gewählt, dass spätere Schritte auf früheren aufbauen — nicht einfach querbeet abhaken, siehe Abschnitt "Empfohlene Reihenfolge" ganz unten für die Kurzfassung.
>
> Phase 3 (Monetarisierung: Auth, Multi-Tenant, Billing) ist bewusst **nicht** Teil dieses Plans.
@@ -15,33 +61,33 @@ Beim Review ist aufgefallen, dass `TraderAnalyticsWorker.CalculatePnL` **jeden T
## Phase 1 Kernanalytik korrigieren
### A1. Positionsbasierte PnL-Engine
- [ ] Entscheidung dokumentieren: **Average-Cost-Methode** statt FIFO (einfacher, und wichtig kompatibel mit späterer Trade-Kompaktierung in Abschnitt C, weil Average-Cost nur Gesamtstückzahl & Gesamtkosten braucht, keine Einzel-Trade-Reihenfolge)
- [ ] Neue Domain-Struktur `TraderPosition` (TraderId, MarketOutcomeId, SharesHeld, AvgCost, RealizedPnl, zuletzt aktualisiert) — inkrementell fortschreibbar statt bei jeder Berechnung die komplette Trade-Historie neu zu scannen
- [ ] Buchungslogik je `TradeSide` sauber definieren:
- [ ] `Buy`: Shares += Size, AvgCost neu gewichten
- [ ] `Sell`: RealizedPnl += Size × (Price AvgCost), Shares = Size
- [ ] `Redeem` (Marktauflösung): RealizedPnl += verbleibende Shares × (1 oder 0 je nach Gewinn-Outcome AvgCost), Shares = 0
- [ ] `Split` / `Merge`: als neutrale Positionsumwandlung behandeln (kein PnL-Effekt), nicht wie `Sell`
- [ ] `AddLiquidity` / `RemoveLiquidity`: getrennt von Trading-PnL betrachten (eigene Kategorie, fließt nicht in "Trading-Skill"-Bewertung ein)
- [ ] Unrealisierten PnL für offene Positionen berechnen: `Shares × (MarketOutcome.CurrentPrice AvgCost)`
- [ ] Gesamt-PnL = realisiert + unrealisiert (ersetzt `OverallPnL`, `PnL30d/7d/24h` Felder in `TraderAnalytics`)
- [ ] Prüfen, ob wir zusätzlich/alternativ Polymarkets eigenen `/positions`-Endpoint (`GetTraderPositionsAsync`, liefert `CurrentValue`/`PercentPnl`) als Plausibilitäts-Check oder sogar als primäre Quelle für **offene** Positionen nutzen (aktuell komplett ungenutzt)
- [ ] `CalculatePnL`-Bug aus Abschnitt 0 im Zuge dessen mit erledigen
- [x] Entscheidung dokumentieren: **Average-Cost-Methode** statt FIFO (einfacher, und wichtig kompatibel mit späterer Trade-Kompaktierung in Abschnitt C, weil Average-Cost nur Gesamtstückzahl & Gesamtkosten braucht, keine Einzel-Trade-Reihenfolge)
- [x] Neue Domain-Struktur `TraderPosition` (TraderId, MarketOutcomeId, SharesHeld, AvgCost, RealizedPnl, zuletzt aktualisiert) — inkrementell fortschreibbar statt bei jeder Berechnung die komplette Trade-Historie neu zu scannen
- [x] Buchungslogik je `TradeSide` sauber definieren:
- [x] `Buy`: Shares += Size, AvgCost neu gewichten
- [x] `Sell`: RealizedPnl += Size × (Price AvgCost), Shares = Size
- [x] `Redeem` (Marktauflösung): RealizedPnl += verbleibende Shares × (1 oder 0 je nach Gewinn-Outcome AvgCost), Shares = 0
- [x] `Split` / `Merge`: als neutrale Positionsumwandlung behandeln (kein PnL-Effekt), nicht wie `Sell`
- [x] `AddLiquidity` / `RemoveLiquidity`: getrennt von Trading-PnL betrachten (eigene Kategorie, fließt nicht in "Trading-Skill"-Bewertung ein)
- [x] Unrealisierten PnL für offene Positionen berechnen: `Shares × (MarketOutcome.CurrentPrice AvgCost)`
- [x] Gesamt-PnL = realisiert + unrealisiert (ersetzt `OverallPnL`, `PnL30d/7d/24h` Felder in `TraderAnalytics`)
- [x] Prüfen, ob wir zusätzlich/alternativ Polymarkets eigenen `/positions`-Endpoint (`GetTraderPositionsAsync`, liefert `CurrentValue`/`PercentPnl`) als Plausibilitäts-Check oder sogar als primäre Quelle für **offene** Positionen nutzen (aktuell komplett ungenutzt)
- [x] `CalculatePnL`-Bug aus Abschnitt 0 im Zuge dessen mit erledigen
### A2. Win-Rate korrekt berechnen
- [ ] Win/Loss ist **pro Markt**, nicht pro Trade, definiert: ein Markt zählt als "Win", wenn der realisierte PnL aus diesem Markt (nach Redeem/vollständigem Exit) positiv ist
- [ ] `WinRate = Anzahl gewonnener Märkte / Anzahl abgeschlossener Märkte` (offene Positionen zählen nicht mit)
- [ ] Placeholder `return 0;` in `TraderAnalyticsWorker.CalculateWinRate` ersetzen
- [x] Win/Loss ist **pro Markt**, nicht pro Trade, definiert: ein Markt zählt als "Win", wenn der realisierte PnL aus diesem Markt (nach Redeem/vollständigem Exit) positiv ist
- [x] `WinRate = Anzahl gewonnener Märkte / Anzahl abgeschlossener Märkte` (offene Positionen zählen nicht mit)
- [x] Placeholder `return 0;` in `TraderAnalyticsWorker.CalculateWinRate` ersetzen
### A3. Deep-Dive-Kennzahlen mit echten Werten füllen
- [ ] **Voraussetzung klären:** Für Entry/Exit-Qualität und Timing-Accuracy brauchen wir eine **Preis-Historie** pro `MarketOutcome`, nicht nur den aktuellen Preis (`CurrentPrice`). Aktuell existiert keine Historisierung.
- [ ] Prüfen, ob Polymarkets CLOB-API einen Preishistorie-Endpoint (`/prices-history`) hergibt, den wir zum Backfill nutzen können
- [ ] Falls ja: neue Tabelle `MarketOutcomePriceSnapshot` (MarketOutcomeId, Timestamp, Price) einführen, periodisch befüllt (z.B. durch bestehenden `MarketSyncWorker`/`MarketHistoryWorker` erweitern)
- [ ] `AvgHoldDurationHours` echt berechnen: gewichtete Haltedauer zwischen Einstieg (Buy-Zeitpunkte, gewichtet nach Größe) und Ausstieg (Sell/Redeem) pro Position
- [ ] `EntryQuality`: Einstiegspreis im Vergleich zur nachfolgenden Preisentwicklung (z.B. Perzentil des Einstiegspreises innerhalb eines Zeitfensters danach)
- [ ] `ExitQuality`: analog für Ausstiegspreis
- [ ] `TimingAccuracy`: z.B. Anteil der Trades, die kurz vor einer für den Trader günstigen Preisbewegung platziert wurden
- [ ] Hardcodierte `50`-Neutralwerte in `AnalyticsService.PerformDeepDive` entfernen
- [x] **Voraussetzung klären:** Für Entry/Exit-Qualität und Timing-Accuracy brauchen wir eine **Preis-Historie** pro `MarketOutcome`, nicht nur den aktuellen Preis (`CurrentPrice`). Aktuell existiert keine Historisierung.
- [x] Prüfen, ob Polymarkets CLOB-API einen Preishistorie-Endpoint (`/prices-history`) hergibt, den wir zum Backfill nutzen können
- [x] Falls ja: neue Tabelle `MarketOutcomePriceSnapshot` (MarketOutcomeId, Timestamp, Price) einführen, periodisch befüllt (z.B. durch bestehenden `MarketSyncWorker`/`MarketHistoryWorker` erweitern)
- [x] `AvgHoldDurationHours` echt berechnen: gewichtete Haltedauer zwischen Einstieg (Buy-Zeitpunkte, gewichtet nach Größe) und Ausstieg (Sell/Redeem) pro Position
- [x] `EntryQuality`: Einstiegspreis im Vergleich zur nachfolgenden Preisentwicklung (z.B. Perzentil des Einstiegspreises innerhalb eines Zeitfensters danach)
- [x] `ExitQuality`: analog für Ausstiegspreis
- [x] `TimingAccuracy`: z.B. Anteil der Trades, die kurz vor einer für den Trader günstigen Preisbewegung platziert wurden
- [x] Hardcodierte `50`-Neutralwerte in `AnalyticsService.PerformDeepDive` entfernen
### A4. Strategie-Klassifikation verbessern
- [ ] Zusätzliche Signale einbeziehen statt nur "Ø-Größe" und "Hedging-Rate":
@@ -54,17 +100,17 @@ Beim Review ist aufgefallen, dass `TraderAnalyticsWorker.CalculatePnL` **jeden T
- [ ] Sicherstellen, dass nicht der Großteil der Trader dauerhaft bei "Unknown" landet (aktuell strukturell der Fall)
### A5. Copytrading-Eignungs-Score (neu)
- [ ] Neue Metrik-Dimension definieren, unabhängig vom bestehenden `PriorityScore`:
- [ ] **Liquiditäts-Fit**: durchschnittliche Positionsgröße im Verhältnis zur Marktliquidität/zum Volumen zum Handelszeitpunkt (Slippage-Risiko für Nachahmer)
- [ ] **Reaktionsfenster**: wie viel Zeit bliebe einem Copytrader realistisch zum Nachziehen (Trader mit Sekunden/Millisekunden-Kadenz sind nicht kopierbar)
- [ ] **Frequenz/Konzentration**: sehr hochfrequente/bot-artige Trader senken den Score automatisch
- [ ] **Track-Record-Länge & Konsistenz**: mehr abgeschlossene Märkte mit konsistent positivem PnL = höheres Vertrauen
- [ ] Kombinierten `CopytradingScore` (0100) berechnen und persistieren (neues Feld auf `TraderScore` oder eigene Entity `TraderCopytradingProfile`)
- [ ] Im Dashboard/API sichtbar machen (getrennt von der bisherigen "Priorität", da unterschiedliche Fragestellung: *gut* vs. *kopierbar*)
- [x] Neue Metrik-Dimension definieren, unabhängig vom bestehenden `PriorityScore`:
- [x] **Liquiditäts-Fit**: durchschnittliche Positionsgröße im Verhältnis zur Marktliquidität/zum Volumen zum Handelszeitpunkt (Slippage-Risiko für Nachahmer)
- [x] **Reaktionsfenster**: wie viel Zeit bliebe einem Copytrader realistisch zum Nachziehen (Trader mit Sekunden/Millisekunden-Kadenz sind nicht kopierbar)
- [x] **Frequenz/Konzentration**: sehr hochfrequente/bot-artige Trader senken den Score automatisch
- [x] **Track-Record-Länge & Konsistenz**: mehr abgeschlossene Märkte mit konsistent positivem PnL = höheres Vertrauen
- [x] Kombinierten `CopytradingScore` (0100) berechnen und persistieren (neues Feld auf `TraderScore` oder eigene Entity `TraderCopytradingProfile`)
- [x] Im Dashboard/API sichtbar machen (getrennt von der bisherigen "Priorität", da unterschiedliche Fragestellung: *gut* vs. *kopierbar*)
### A6. (siehe Abschnitt 0) TradeSide-Bugfix in PnL-Berechnung
- [ ] Erledigt sich durch A1, hier nur als Häkchen zur Nachverfolgung
- [ ] Kurzer Änderungsvermerk/Commit-Hinweis, damit klar ist, dass dieser Bug bewusst behoben wurde
- [x] Erledigt sich durch A1, hier nur als Häkchen zur Nachverfolgung
- [x] Kurzer Änderungsvermerk/Commit-Hinweis, damit klar ist, dass dieser Bug bewusst behoben wurde
---
@@ -77,7 +123,7 @@ Beim Review ist aufgefallen, dass `TraderAnalyticsWorker.CalculatePnL` **jeden T
- [x] Migration end-to-end gegen eine echte, frische DEV-Datenbank getestet (`lqf7.your-database.de`/`bergisnu_db0`, vom Nutzer bereitgestellt) — `dotnet ef database update` lief fehlerfrei durch, `dotnet ef migrations list` bestätigt sie als angewendet
- [x] `AppDbContextFactory` (Design-Time-Factory für `dotnet ef`) liest Zielverbindung jetzt aus Env-Vars (`PREDICTALYTICS_DB_SERVER/_NAME/_USER/_PASSWORD`, via `MySqlConnectionStringBuilder` statt roher String-Konkatenation, da Passwörter Sonderzeichen wie `;`/`{}` enthalten können) statt fest codiert — kein Secret mehr im Repo nötig, um Migrationen gegen eine beliebige DB zu fahren
- [x] ~~Live-Produktions-DB manuell als "bereits migriert" markieren~~ — vorerst zurückgestellt: Da wir laut Abschnitt D ohnehin einen kompletten Neustart der Produktions-DB planen (Altdaten sind jederzeit nachladbar, siehe C0), richten wir die neue Produktions-DB am Ende genauso ein wie die DEV-DB (leer anlegen + `dotnet ef database update`) — der fummelige Stamp-Schritt auf die bestehende Live-DB entfällt dann komplett. Nur falls wir uns doch gegen den Neustart entscheiden, müsste dieser Schritt nachgeholt werden
- [ ] Alle künftigen Schemaänderungen aus Phase 1 (z.B. `TraderPosition`, `MarketOutcomePriceSnapshot`, `TraderCopytradingProfile`) als reguläre Migrationen anlegen — direkt gegen die DEV-DB entwickeln/testen, da wir dort laut Nutzer frei experimentieren dürfen
- [x] Alle künftigen Schemaänderungen aus Phase 1 (z.B. `TraderPosition`, `MarketOutcomePriceSnapshot`, `TraderCopytradingProfile`) als reguläre Migrationen anlegen — direkt gegen die DEV-DB entwickeln/testen, da wir dort laut Nutzer frei experimentieren dürfen
### B2. Secrets-Management (Release-Vorbereitung, kein akuter Risikofall)
> Software läuft aktuell nur lokal, kein Fremdzugriff — Passwort bleibt vorerst wie es ist, keine Rotation nötig. Dieser Punkt ist reine **Vorbereitung**, damit das Projekt bei Bedarf später auch ohne die aktuellen Secrets veröffentlicht/geteilt werden könnte, ohne den Code nochmal anfassen zu müssen. Dadurch niedrigere Priorität als vorher angenommen — kann später in der Reihenfolge stehen.
@@ -86,27 +132,27 @@ Beim Review ist aufgefallen, dass `TraderAnalyticsWorker.CalculatePnL` **jeden T
- [ ] Keine Passwort-Rotation nötig, solange rein lokale Nutzung
### B3. Scoring-Pipeline entkoppeln
- [ ] `PollingWorker` soll **nicht** bei jedem 60-Sekunden-Zyklus alle Trader neu bewerten
- [ ] Nur Trader neu bewerten, die seit letzter Berechnung neue Trades bekommen haben (Dirty-Flag oder Vergleich `LastTradesUpdatedAt` vs. `TraderScore.CalculatedAt`)
- [ ] N+1-Datenbankzugriffe in `ScoringService.RecalculateAllScoresAsync` durch Batch-Queries ersetzen
- [ ] Eigenes Intervall für volle Neuberechnung/Ranking (z.B. alle 15 Min) getrennt vom reinen Trade-Polling (60s)
- [x] `PollingWorker` soll **nicht** bei jedem 60-Sekunden-Zyklus alle Trader neu bewerten
- [x] Nur Trader neu bewerten, die seit letzter Berechnung neue Trades bekommen haben (Dirty-Flag oder Vergleich `LastTradesUpdatedAt` vs. `TraderScore.CalculatedAt`)
- [x] N+1-Datenbankzugriffe in `ScoringService.RecalculateAllScoresAsync` durch Batch-Queries ersetzen
- [x] Eigenes Intervall für volle Neuberechnung/Ranking (z.B. alle 15 Min) getrennt vom reinen Trade-Polling (60s)
### B6. Weitere gefundene Bugs/Performance-Probleme (Review-Pass 2026-07-01)
- [ ] **`MarketSyncWorker` re-synct alle 30 Minuten ALLE Märkte, inkl. `includeClosed=true`, von Offset 0** ([MarketSyncWorker.cs](src/Predictalytics.Worker/Services/MarketSyncWorker.cs)) — holt damit bei jedem Zyklus jeden jemals geschlossenen Polymarket-Markt erneut komplett durch. Wächst unbegrenzt mit der Zeit, echtes Risiko für Rate-Limiting/Sperrung. Sollte auf: aktive Märkte häufig, geschlossene Märkte selten/inkrementell (z.B. nur kürzlich geschlossene, nicht der komplette Bestand) umgestellt werden
- [ ] **Watchlisted Trader nicht von Auto-Löschung ausgeschlossen** (`TraderRepository.GetTradersForCleanupAsync` / `TraderCleanupWorker`) — da `Trades`/`TraderScore`/`WatchlistEntries` per Cascade am Trader hängen, könnte ein manuell beobachteter Trader nach 1 Jahr Inaktivität (oder bei kurzzeitigem API-Fehler) unbemerkt komplett gelöscht werden. Watchlist-Einträge sollten von der Cleanup-Query ausgenommen werden
- [ ] **`TradeRepository.GetKnownPlatformTradeIdsAsync` lädt die komplette Trade-ID-Historie eines Traders ins RAM**, nur um eine kleine neu geholte Charge (~100-1000 Trades) zu deduplizieren — aufgerufen alle 60s (`PollingWorker`) bzw. alle 12h (`TradeHistoryWorker`) pro Trader. Wird mit wachsender Trade-Zahl immer teurer. Fix: nur `WHERE PlatformTradeId IN (<geholte Batch-IDs>)` abfragen statt der gesamten Historie
- [ ] **Race Condition in `MarketRepository.AddOrUpdateAsync`** (im Gegensatz zu `AddOrUpdateRangeAsync` ohne Locking) — `TradeHistoryWorker` verarbeitet bis zu 5 Trader parallel (`Parallel.ForEachAsync`); referenzieren zwei gleichzeitig denselben noch unbekannten Markt, prüfen beide unabhängig "existiert nicht" und einer crasht beim `Add` mit Unique-Constraint-Verletzung (wird geloggt, Trader-Sync für den Zyklus bricht ab, nächster Zyklus heilt es meist). Fix: gleiches Locking-Muster wie `AddOrUpdateRangeAsync` verwenden, oder Insert-Konflikt sauber abfangen/retry
- [ ] **`TradeHistoryWorker` bricht die Trade-Verarbeitung beim ersten bekannten Trade ab** (`if (!isInitial) break;`), verlässt sich also darauf, dass die API immer streng neueste-zuerst liefert. `PollingWorker`s äquivalente Schleife macht das NICHT. Sollte angeglichen werden — der Performance-Gewinn ist gering gegenüber dem Risiko einer stillen Datenlücke, falls die Annahme mal nicht stimmt
- [x] **`MarketSyncWorker` re-synct alle 30 Minuten ALLE Märkte, inkl. `includeClosed=true`, von Offset 0** ([MarketSyncWorker.cs](src/Predictalytics.Worker/Services/MarketSyncWorker.cs)) — holt damit bei jedem Zyklus jeden jemals geschlossenen Polymarket-Markt erneut komplett durch. Wächst unbegrenzt mit der Zeit, echtes Risiko für Rate-Limiting/Sperrung. Sollte auf: aktive Märkte häufig, geschlossene Märkte selten/inkrementell (z.B. nur kürzlich geschlossene, nicht der komplette Bestand) umgestellt werden
- [x] **Watchlisted Trader nicht von Auto-Löschung ausgeschlossen** (`TraderRepository.GetTradersForCleanupAsync` / `TraderCleanupWorker`) — da `Trades`/`TraderScore`/`WatchlistEntries` per Cascade am Trader hängen, könnte ein manuell beobachteter Trader nach 1 Jahr Inaktivität (oder bei kurzzeitigem API-Fehler) unbemerkt komplett gelöscht werden. Watchlist-Einträge sollten von der Cleanup-Query ausgenommen werden
- [x] **`TradeRepository.GetKnownPlatformTradeIdsAsync` lädt die komplette Trade-ID-Historie eines Traders ins RAM**, nur um eine kleine neu geholte Charge (~100-1000 Trades) zu deduplizieren — aufgerufen alle 60s (`PollingWorker`) bzw. alle 12h (`TradeHistoryWorker`) pro Trader. Wird mit wachsender Trade-Zahl immer teurer. Fix: nur `WHERE PlatformTradeId IN (<geholte Batch-IDs>)` abfragen statt der gesamten Historie
- [x] **Race Condition in `MarketRepository.AddOrUpdateAsync`** (im Gegensatz zu `AddOrUpdateRangeAsync` ohne Locking) — `TradeHistoryWorker` verarbeitet bis zu 5 Trader parallel (`Parallel.ForEachAsync`); referenzieren zwei gleichzeitig denselben noch unbekannten Markt, prüfen beide unabhängig "existiert nicht" und einer crasht beim `Add` mit Unique-Constraint-Verletzung (wird geloggt, Trader-Sync für den Zyklus bricht ab, nächster Zyklus heilt es meist). Fix: gleiches Locking-Muster wie `AddOrUpdateRangeAsync` verwenden, oder Insert-Konflikt sauber abfangen/retry
- [x] **`TradeHistoryWorker` bricht die Trade-Verarbeitung beim ersten bekannten Trade ab** (`if (!isInitial) break;`), verlässt sich also darauf, dass die API immer streng neueste-zuerst liefert. `PollingWorker`s äquivalente Schleife macht das NICHT. Sollte angeglichen werden — der Performance-Gewinn ist gering gegenüber dem Risiko einer stillen Datenlücke, falls die Annahme mal nicht stimmt
### B4. Testabdeckung für die kritische Logik
- [ ] Neues Testprojekt (z.B. `Predictalytics.Application.Tests`) anlegen — aktuell existiert **kein einziges** Testprojekt
- [ ] Unit-Tests für die neue PnL-Engine (A1) — insbesondere Grenzfälle: nur offene Position, nur geschlossene Position, Split/Merge, Redeem-Verlust vs. -Gewinn
- [ ] Unit-Tests für Win-Rate (A2)
- [ ] Unit-Tests für Deep-Dive-Kennzahlen (A3) und Copytrading-Score (A5)
- [ ] Diese Tests idealerweise **parallel zu A1A5** schreiben, nicht erst am Ende nachziehen
- [x] Neues Testprojekt (z.B. `Predictalytics.Application.Tests`) anlegen — aktuell existiert **kein einziges** Testprojekt
- [x] Unit-Tests für die neue PnL-Engine (A1) — insbesondere Grenzfälle: nur offene Position, nur geschlossene Position, Split/Merge, Redeem-Verlust vs. -Gewinn
- [x] Unit-Tests für Win-Rate (A2)
- [x] Unit-Tests für Deep-Dive-Kennzahlen (A3) und Copytrading-Score (A5)
- [x] Diese Tests idealerweise **parallel zu A1A5** schreiben, nicht erst am Ende nachziehen
### B5. Kleine verwandte Aufräumarbeit
- [ ] Log-Rotation/Retention der Serilog-Datei-Sinks (`WinFormsHost/logs`) prüfen — wächst potenziell unbegrenzt, ähnliches Prinzip wie die DB-Speicherplatzfrage unten
- [x] Log-Rotation/Retention der Serilog-Datei-Sinks (`WinFormsHost/logs`) prüfen — wächst potenziell unbegrenzt, ähnliches Prinzip wie die DB-Speicherplatzfrage unten
---
@@ -129,20 +175,20 @@ Beim Review ist aufgefallen, dass `TraderAnalyticsWorker.CalculatePnL` **jeden T
→ Vorschlag: **Burst-Erkennung statt starrer Zeit-Buckets** (nur tatsächlich dichte Trade-Sequenzen kompaktieren) und **Aggregation getrennt nach Trader+Markt/Outcome+Seite** mit **VWAP** (mengengewichteter Durchschnittspreis) statt einfachem Durchschnitt — bleibt unter der Average-Cost-Methode aus A1 nahezu verlustfrei für die PnL-Berechnung.
### C1. Rollierendes Zeitfenster statt Archivierung (ersetzt die alte "Idee 1")
- [ ] Konfigurierbares Retention-Fenster einführen (Default-Vorschlag: 36 Monate) — gilt **für alle Trader gleichermaßen**, nicht nur für inaktive. Länge sollte sich daran orientieren, wie weit die Deep-Dive-/Strategie-Analyse (A3/A4) tatsächlich zurückschaut, um "aktuelle Strategie" zu charakterisieren
- [ ] Reihenfolge pro Trade zwingend einhalten: **erst** in `TraderPosition`/Monats-Aggregat (s.u.) einrechnen und das sicher persistieren, **dann erst** die Rohzeile löschen — rein zeitbasiert, unabhängig davon ob die betroffene Position noch offen oder schon geschlossen ist
- [ ] Kein Export/Cold-Storage nötig, da jederzeit über die Polymarket-API bzw. im Zweifel über die Blockchain nachladbar — vereinfacht C1 gegenüber der ursprünglichen Idee erheblich (keine Baseline-Felder, keine Reaktivierungs-Sonderfälle)
- [ ] Optionales, leichtgewichtiges Langzeit-Signal (nice-to-have, niedrige Priorität): ein grobes Monats-Aggregat pro Trader (Monat, realisierter PnL, Trade-Anzahl, Volumen) für einen "war er über Monate hinweg konsistent profitabel"-Trend im Copytrading-Score (A5) — **ohne** Trade-Detailtiefe, nur wenige Kennzahlen pro Monat
- [ ] Neuer periodischer Cleanup-Job (`TradeRetentionWorker`), der Trades außerhalb des Fensters findet und löscht, nachdem die Voraussetzung (Position/Aggregat aktuell) erfüllt ist
- [x] Konfigurierbares Retention-Fenster einführen (Default-Vorschlag: 36 Monate) — gilt **für alle Trader gleichermaßen**, nicht nur für inaktive. Länge sollte sich daran orientieren, wie weit die Deep-Dive-/Strategie-Analyse (A3/A4) tatsächlich zurückschaut, um "aktuelle Strategie" zu charakterisieren
- [x] Reihenfolge pro Trade zwingend einhalten: **erst** in `TraderPosition`/Monats-Aggregat (s.u.) einrechnen und das sicher persistieren, **dann erst** die Rohzeile löschen — rein zeitbasiert, unabhängig davon ob die betroffene Position noch offen oder schon geschlossen ist
- [x] Kein Export/Cold-Storage nötig, da jederzeit über die Polymarket-API bzw. im Zweifel über die Blockchain nachladbar — vereinfacht C1 gegenüber der ursprünglichen Idee erheblich (keine Baseline-Felder, keine Reaktivierungs-Sonderfälle)
- [x] Optionales, leichtgewichtiges Langzeit-Signal (nice-to-have, niedrige Priorität): ein grobes Monats-Aggregat pro Trader (Monat, realisierter PnL, Trade-Anzahl, Volumen) für einen "war er über Monate hinweg konsistent profitabel"-Trend im Copytrading-Score (A5) — **ohne** Trade-Detailtiefe, nur wenige Kennzahlen pro Monat
- [x] Neuer periodischer Cleanup-Job (`TradeRetentionWorker`), der Trades außerhalb des Fensters findet und löscht, nachdem die Voraussetzung (Position/Aggregat aktuell) erfüllt ist
- [ ] Kurzer Stichprobentest, wie weit `/activity` pro Wallet tatsächlich zurückreicht (bestätigt/verifiziert nur die schon vorliegende Doku-Aussage, geringe Priorität da schon durch Nutzer-Recherche plausibilisiert)
### C2. Kompaktierung von Hochfrequenz-Tradern innerhalb des Zeitfensters (überarbeitete "Idee 2")
- [ ] Burst-Erkennung statt globalem Zeitraster: Sequenz von Trades mit Abstand kleiner als Schwellwert (z.B. 60s, konfigurierbar) zwischen aufeinanderfolgenden Trades **desselben Traders, Outcomes und derselben Seite (Buy/Sell)** gilt als "Burst"
- [ ] Mindestlänge für Kompaktierung festlegen (z.B. erst ab 20+ Trades im Burst lohnt sich das)
- [ ] Bestehende Bot-Heuristik (`intervals.Average() < 10` in `AnalyticsService`) als Ausgangspunkt wiederverwenden/verallgemeinern statt eine zweite, unabhängige Definition einzuführen
- [ ] Aggregat-Datensatz pro Burst: Anzahl Trades, Summe Size, Summe Amount, Min-Preis, Max-Preis, **VWAP** (nicht einfacher Durchschnitt)
- [ ] Design-Entscheidung: Aggregat als zusätzliche nullable Spalten auf der bestehenden `Trade`-Tabelle (`AggregateCount`, `AggregateMinPrice`, `AggregateMaxPrice`) statt separater Tabelle — bestehender Code (PnL, Deep-Dive) muss dadurch kaum angepasst werden, ein Aggregat-Datensatz ist einfach ein "Trade" mit `Size = Summe`, `Price = VWAP`
- [ ] **Reihenfolge beachten:** Diese Kompaktierung erst implementieren, nachdem A1 (neue PnL-Engine) steht und validiert ist — sonst kompaktieren wir Daten weg, bevor wir wissen, was die neue Engine wirklich braucht
- [x] Burst-Erkennung statt globalem Zeitraster: Sequenz von Trades mit Abstand kleiner als Schwellwert (z.B. 60s, konfigurierbar) zwischen aufeinanderfolgenden Trades **desselben Traders, Outcomes und derselben Seite (Buy/Sell)** gilt als "Burst"
- [x] Mindestlänge für Kompaktierung festlegen (z.B. erst ab 20+ Trades im Burst lohnt sich das)
- [x] Bestehende Bot-Heuristik (`intervals.Average() < 10` in `AnalyticsService`) als Ausgangspunkt wiederverwenden/verallgemeinern statt eine zweite, unabhängige Definition einzuführen
- [x] Aggregat-Datensatz pro Burst: Anzahl Trades, Summe Size, Summe Amount, Min-Preis, Max-Preis, **VWAP** (nicht einfacher Durchschnitt)
- [x] Design-Entscheidung: Aggregat als zusätzliche nullable Spalten auf der bestehenden `Trade`-Tabelle (`AggregateCount`, `AggregateMinPrice`, `AggregateMaxPrice`) statt separater Tabelle — bestehender Code (PnL, Deep-Dive) muss dadurch kaum angepasst werden, ein Aggregat-Datensatz ist einfach ein "Trade" mit `Size = Summe`, `Price = VWAP`
- [x] **Reihenfolge beachten:** Diese Kompaktierung erst implementieren, nachdem A1 (neue PnL-Engine) steht und validiert ist — sonst kompaktieren wir Daten weg, bevor wir wissen, was die neue Engine wirklich braucht
### C3. Weitere eigene Vorschläge
- [ ] **Spaltentypen verkleinern**: `TransactionHash`, `MarketId`, `AssetId` sind Hex-/Dezimal-Strings fester Länge (z.B. `0x` + 64 Hex-Zeichen) — als `BINARY(32)` statt `VARCHAR(66/80)` speichern spart ca. 3050% Platz auf diesen stark indizierten Spalten und ist schneller
@@ -153,7 +199,7 @@ Beim Review ist aufgefallen, dass `TraderAnalyticsWorker.CalculatePnL` **jeden T
- [ ] `Split`/`Merge`/`AddLiquidity`/`RemoveLiquidity`-Ereignisse (siehe A1) ggf. separat und kompakter ablegen, da sie für die Trader-Bewertung meist weniger relevant sind als `Buy`/`Sell`
### C4. Abhängigkeit zu Phase 1
- [ ] Merksatz: **Erst A1 (neue PnL-Engine) fertigstellen, dann C1/C2 umsetzen.** Sonst laufen wir Gefahr, Rohdaten wegzuoptimieren, die die neue Engine noch gebraucht hätte.
- [x] Merksatz: **Erst A1 (neue PnL-Engine) fertigstellen, dann C1/C2 umsetzen.** Sonst laufen wir Gefahr, Rohdaten wegzuoptimieren, die die neue Engine noch gebraucht hätte.
---