9.8 KiB
Fix- und Datenreparatur-Plan (Stand 2026-07-09, Übergabe an Gemini)
Abnahmekriterium für alle Code-Änderungen:
dotnet test src/Predictalytics.Application.Testsmuss 16 grün + 1 übersprungen liefern (der SkipCheckpointResetAndReplay_DoesNotDoubleCountBalanceist 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:
- Die Buttons der Trader-Detailseite nutzen alte, Job-lose Endpoints (die Listen-Buttons nutzen bereits das Job-System).
- 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,0KbeiTotalPnL = 158,5K, weil der Basis-Snapshot-85,5Kenthält (korrupter Altwert). Kein Code-Fix ändert das — die Daten müssen einmalig repariert werden. - 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):
TotalTradeswird 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 aufTrader(Default Full), Migration. - Klassifizierung im
TradeHistoryWorkernach 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!) →TraderPositionsupserten (size→SharesHeld, avgPrice→AvgCost, cashPnl→RealizedPnl);OverallPnLaus Positions +GetLeaderboardAsync-PnL für die Zeitfenster;TraderDailySnapshotweiter schreiben (Equity-Kurve bleibt). - Wöchentliche „Biopsie": einmal 500 Trades via /activity ziehen, NUR durch den
TraderTraitCalculatorschicken, 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 mitPlatformTradeId = "AGG_{traderId}_{outcomeId}_{side}_{yyyyMMddHH}", laufende Stunde per Upsert aktualisieren. Average-Cost-Engine bleibt damit verlustfrei. - Danach:
RetentionDaysfü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
dotnet test: alle bestehenden 32 + 1 Skip bleiben grün (Assertions unverändert) + die neuen D-Tests.- RN1/Swisstony stehen nach der Einstufung auf SnapshotOnly: PnL gefüllt (aus /positions/Leaderboard), Traits gesetzt, keine neuen Trade-Zeilen mehr in der DB.
- 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.
- 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 vomPollingWorkerfür jeden noch unbekannten Markt) liefert wedercategorynoch Event-Tags (raw.Events[0].Tagsist leer, auch mitinclude_tag=true). → Solche Märkte werden nur per Frage-Text klassifiziert und landen sonst aufOther. - 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)
GetSubcategorynimmttags[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 TagPoliticszuletzt).
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 wie2025 Predictions,2026 FIFA World Cup→ok als Sub?, …). - Subcategory = spezifischster verbleibender Tag, der nicht die Kategorie selbst ist
(bei World-Cup-Tags →
Soccer, nichtSports). 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: wennparentTagsleer 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 denIRateLimiterdrosseln. - Alternativ/zusätzlich: existiert das Parent-Event bereits in unserer DB (aus dem Events-Sync,
Event.Tagsgefü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.Tagsneu ableiten (mit F1/F2). Als Methode inRunRecalculateAllTradersAsynceinhängen ODER eigener Dev-Endpoint. DanachTraderCategoryPerformanceneu 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, SubcategorySoccer). - 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.