Files
Predictalytics/FIXPLAN-TODO.md
T

151 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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=<eventId>` 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.