# Fix- und Datenreparatur-Plan (Stand 2026-07-09, Übergabe an Gemini) > **Abnahmekriterium für alle Code-Änderungen:** `dotnet test src/Predictalytics.Application.Tests` muss > **16 grün + 1 übersprungen** liefern (der Skip `CheckpointResetAndReplay_DoesNotDoubleCountBalance` ist eine > dokumentierte, bewusste Entscheidung). Die Assertions der Invarianten-Tests dürfen **nicht** verändert werden — > sie definieren das Soll-Verhalten. Wenn ein Test rot wird, ist der Code falsch, nicht der Test. ## Hintergrund Die Engine-Fixes vom 09.07. sind korrekt (Tests grün). Die im WebUI sichtbaren Probleme haben drei andere Ursachen: 1. Die Buttons der **Trader-Detailseite** nutzen alte, Job-lose Endpoints (die Listen-Buttons nutzen bereits das Job-System). 2. Die **abgeleiteten Daten in der DB stammen aus der Bug-Ära** (Snapshots/Positionen wurden von den alten, fehlerhaften Engine-Versionen berechnet). Beispiel aus dem Live-System: `PnL30d = 244,0K` bei `TotalPnL = 158,5K`, weil der Basis-Snapshot `-85,5K` enthält (korrupter Altwert). Kein Code-Fix ändert das — die Daten müssen einmalig repariert werden. 3. **Deadlocks + Shutdown-Fehlerkaskaden** in den Workern (unbatchtes Reconciliation-UPDATE, fehlende Cancellation-Behandlung). **Ein DB-Reset ist NICHT nötig.** Die Rohdaten (`Trades`) sind größtenteils intakt; Positionen, Analytics, Snapshots und Scores sind abgeleitet und lokal neu berechenbar. Nur Trader, deren Alt-Trades die Retention bereits gelöscht/kompaktiert hat, brauchen einen gezielten API-Re-Import (kleine Teilmenge, siehe Teil B). --- ## Teil D — Ausbaustufe: Merkmals-Tags & HF-Trader-Tiering (ergänzt 2026-07-11) > **Bereits direkt erledigt (nicht Teil dieses Auftrags):** `TotalTrades` wird jetzt von der Engine aus dem > echten Row-Count gesetzt; der Kategorie-Mapper klassifiziert zusätzlich über den Frage-Text und matcht kurze > Tokens nur an Wortgrenzen; `UpdateMarketFields` überschreibt gute Kategorien nicht mehr mit "Other". > Teststand: **32 grün + 1 Skip** — das ist die neue Basis, Assertions unverändert lassen. ### D3. Trader-Tiering (`IngestMode`) — Umgang mit Ultra-HF-Tradern (RN1, Swisstony) **Hintergrund:** Ultra-HF-Trader werden heute schon NICHT vollständig erfasst (PollingWorker: 100 Trades/60 s gegen 300+/min) — das Trade-Replay-PnL ist für diese Klasse bereits falsch und frisst nur Speicher. - Enum `IngestMode { Full = 0, Aggregated = 1, SnapshotOnly = 2 }` + Spalte auf `Trader` (Default Full), Migration. - **Klassifizierung** im `TradeHistoryWorker` nach jedem Fetch: Zeitspanne der letzten 500 Trades → Trades/Tag-Schätzung. > 5.000/Tag → SnapshotOnly; > 100/Tag → Aggregated. Hysterese: Rückstufung Richtung Full erst nach 7 Tagen unter der halben Schwelle (kein Flattern). - **SnapshotOnly (Tier C):** - Polling/History-Worker überspringen den Trade-Import komplett. - Stündlich: `GetTraderPositionsAsync` (der ungenutzte `/positions`-Endpoint!) → `TraderPositions` upserten (size→SharesHeld, avgPrice→AvgCost, cashPnl→RealizedPnl); `OverallPnL` aus Positions + `GetLeaderboardAsync`-PnL für die Zeitfenster; `TraderDailySnapshot` weiter schreiben (Equity-Kurve bleibt). - Wöchentliche „Biopsie": einmal 500 Trades via /activity ziehen, NUR durch den `TraderTraitCalculator` schicken, NICHT persistieren. - Engine überspringt Trade-Replay für SnapshotOnly; Estimator/Enrichment überspringen; CopytradingScore = 0 mit Trait `not_copyable_hf`. - **Aggregated (Tier B):** Aggregation beim Import statt nachträglicher Kompaktierung: Bucket (TraderId, MarketOutcomeId, Side, Stunde) mit VWAP-Preis, Summen-Size/-Amount, `AggregatedCount`; gespeichert als normale Trade-Zeile mit `PlatformTradeId = "AGG_{traderId}_{outcomeId}_{side}_{yyyyMMddHH}"`, laufende Stunde per Upsert aktualisieren. Average-Cost-Engine bleibt damit verlustfrei. - Danach: `RetentionDays` für Full-Trader auf 180 erhöhen (Config) — die Bots stellen nicht mehr die Masse, und längerer Track-Record nützt genau den kopierbaren Tradern. - **Tests:** Klassifizierungs-Schwellen + Hysterese als pure Funktion; PollingWorker importiert für SnapshotOnly-Trader nichts; Aggregations-Upsert ist idempotent (2× dieselbe Stunde → 1 Zeile, korrekte Summen und `AggregatedCount`). ### D4. Abnahme Teil D 1. `dotnet test`: alle bestehenden **32 + 1 Skip** bleiben grün (Assertions unverändert) + die neuen D-Tests. 2. RN1/Swisstony stehen nach der Einstufung auf SnapshotOnly: PnL gefüllt (aus /positions/Leaderboard), Traits gesetzt, **keine neuen Trade-Zeilen** mehr in der DB. 3. Detailseite zeigt Trait-Chips; Traders-Liste filterbar nach Trait. 3b. Detailseite zeigt Median-Win/-Loss-Rendite und Profit Factor; die D2c-Werte sind für Trader mit abgeschlossenen Märkten gefüllt. 4. Tägliches DB-Wachstum sichtbar reduziert (DB-Size-Anzeige im WinForms-Statusbar beobachten). Reihenfolge: **D1 → D2/D2b/D2c → D3** (D2c ist klein und gehört in denselben Engine-Durchlauf wie die Winrate; bei D3 zuerst Tier C, dann Tier B). --- --- ## Teil F — Kategorie-Erkennung strukturell reparieren (ergänzt 2026-07-13) > Nutzer meldet: Markt-Kategorien und die Kategorien, in denen sich ein Trader bewegt, stimmen > weiterhin nicht. Ursache **live gegen die Gamma-API verifiziert** — es ist strukturell, nicht der Mapper. ### F0. Root Cause (verifiziert) - Die **`/events`-Liste** (MarketSyncWorker-Pfad) liefert **reichhaltige Tags** (z. B. `['Sports','Soccer','FIFA World Cup',...]`). - Der **`/markets?condition_id=`-Pfad** (On-Demand, `PolymarketProvider.GetMarketAsync`, aufgerufen vom `PollingWorker` für jeden noch unbekannten Markt) liefert **weder `category` noch Event-Tags** (`raw.Events[0].Tags` ist leer, auch mit `include_tag=true`). → Solche Märkte werden nur per Frage-Text klassifiziert und landen sonst auf **`Other`**. - **Genau die vom Trader gehandelten Märkte entstehen überwiegend on-demand** → viele `Other`. - Verschärfend: **geschlossene/aufgelöste** Märkte deckt der aktive Events-Sync nicht laufend ab → historische Märkte (die für die Analyse zählen) bleiben ohne Tags = `Other`. - Zwei Folgefehler: (a) `GetSubcategory` nimmt **`tags[0]`** — das ist oft Müll (`"Ethiopia"`, `"Hide From New"`, `"exchange"`), nicht die Kategorie; (b) die Tag-**Reihenfolge** ist unzuverlässig (bei „Next PM of Ethiopia" steht der kanonische Tag `Politics` **zuletzt**). ### F1. Mapper: kanonischen Tag zuerst, dann Heuristik - [ ] In `MarketCategoryMapper.Map`: **zuerst** prüfen, ob **irgendein Tag exakt** einer bekannten Kategorie entspricht (Polymarkets Tag-Vokabular enthält fast immer den kanonischen Top-Level-Tag: `Sports`, `Politics`, `Crypto`, `Business`/`Economy`, `Pop Culture`, `Science`, ...). Mapping-Tabelle Tag→`MarketCategory` (inkl. Synonyme: `Finance`/`Business`→Economy, `Pop Culture`→PopCulture). Erst wenn **kein** kanonischer Tag matcht, die bestehende Keyword-Heuristik auf Frage+Tags anwenden. - [ ] **Kategorie über die gesamte Tag-Menge** bestimmen, nie über `tags[0]`. ### F2. Subcategory: Noise filtern, sinnvoll wählen, leer normalisieren - [ ] Organisations-/Müll-Tags herausfiltern (Blacklist: `Hide From New`, `Tournament Futures`, `Main Election`, `Recurring`, Jahres-Tags wie `2025 Predictions`, `2026 FIFA World Cup`→ok als Sub?, …). - [ ] Subcategory = spezifischster **verbleibender** Tag, der **nicht** die Kategorie selbst ist (bei World-Cup-Tags → `Soccer`, nicht `Sports`). Kein passender → leerer String. - [ ] **NULL/`""` einheitlich als `""`** speichern (behebt die doppelten „Sports/-"-Zeilen: heute entstehen zwei Gruppen-Keys aus NULL vs. ""). ### F3. On-Demand-Markt: Tags nachladen statt `Other` zu speichern - [ ] In `GetMarketAsync`: wenn `parentTags` leer ist, aber ein Event mit Id vorhanden ist → **`/events?id=` nachladen** (liefert Tags, 1 Extra-Call pro neuem Markt, cachebar) und die Tags fürs Mapping verwenden. Über den `IRateLimiter` drosseln. - [ ] Alternativ/zusätzlich: existiert das Parent-Event bereits in unserer DB (aus dem Events-Sync, `Event.Tags` gefüllt) → **Tags von dort erben**, ganz ohne API-Call. ### F4. Kategorie aus Event-Tags ableiten + Offline-Backfill (der große Hebel) - [ ] Markt-Kategorie primär aus den **Event-Tags** (`market.Event.Tags`) ableiten, nicht aus den (leeren) Markt-Feldern. On-Demand-Märkte erben so die Kategorie ihres Events. - [ ] **Einmaliger Offline-Backfill** (keine API-Calls!): über alle Märkte iterieren, deren Event Tags hat, und Kategorie/Subcategory aus `Event.Tags` neu ableiten (mit F1/F2). Als Methode in `RunRecalculateAllTradersAsync` einhängen ODER eigener Dev-Endpoint. Danach `TraderCategoryPerformance` neu rechnen (passiert durch die ohnehin folgende Trader-Neuberechnung). ### F5. Coverage geschlossener Märkte - [ ] Sicherstellen, dass der Events-Sync **geschlossene** Events (mit Tags) ausreichend abdeckt — mindestens für Märkte, die getrackte Trader gehandelt haben (gezielter „fehlende Event-Tags nachladen"-Schritt in der Reconciliation). ### F6. Tests - [ ] Mapper: kanonischer Tag gewinnt über Reihenfolge (`"Ethiopia, Elections, ..., Politics"` → Politics; `"Sports, Soccer, ..."` → Sports, Subcategory `Soccer`). - [ ] Subcategory: Müll-Tags werden gefiltert; NULL und `""` erzeugen denselben Gruppen-Key (kein Duplikat mehr). - [ ] Event-Tag-Vererbung: Markt ohne eigene Tags, Event mit `['Crypto',...]` → Markt wird Crypto. - [ ] Backfill: Markt in DB als `Other`, Event.Tags = `['Politics',...]` → nach Backfill Politics, ohne API-Call. ### F7. Reihenfolge F1+F2 (reiner Mapper, sofort, testbar) → F4-Backfill (heilt Bestand offline) → F3 (On-Demand-Tags für neue Märkte) → F5 (Coverage). F1/F2/F4 bringen den Großteil, ohne nennenswerte API-Last.