Files
RichardandClaude Opus 5 6975720dc6 Eine Roadmap statt neun Plandokumente; alte Plaene ins Archiv
Die offenen Punkte lagen ueber neun Dokumente verstreut, teils widersprechend,
teils mit Punkten, die laengst umgesetzt waren. ROADMAP.md fuehrt sie zusammen:
sechs Stufen, jeder Punkt gegen den Code geprueft.

Markierung ueber eine Legende, damit Konzepte nicht mit Aufgaben verwechselt
werden: dringend, eingeplant, Backlog, Konzept (durchdacht, aber bewusst nicht
eingeplant), liegt beim Nutzer, verworfen.

Stufen: 0 Sofort (OpenRouter-Key) - 1 Aufraeumen abschliessen (Merge nach main,
Nullable-Warnungen, Startup-Backfill) - 2 Portierung abschliessen (Linux-Erstlauf
und Verifikation, dann Deployment/CI) - 3 Analytik schaerfen (Strategie-
Klassifikation, Backtest-Harness, Track-Record im Score, Ranking) - 4 Daten-
haushalt (SQL des Nutzers) - 5 Ingest skalieren - 6 Monetarisierung vorbereiten.

Ein Anhang haelt fest, was geprueft und bewusst NICHT auf die Roadmap kam, damit
es nicht versehentlich wieder als Aufgabe auftaucht (Azuro/Limitless, die
Marketing-Seiten, Kaltarchiv, EF Core 10).

Archiv: FIXPLAN-DONE, FIXPLAN-G-Speicher, FIXPLAN-TODO, FIXPLAN-UI-Ranglisten,
UMSETZUNGSPLAN sowie ANALYSE-Linux-Portierung, PLAN-Linux-Portierung,
PLAN-Architektur-WebUI-Backend und PLAN-DatenIngest-Skalierung liegen jetzt
unter docs/archiv/ mit einer README, die jedes Dokument einordnet. Sie bleiben
als Begruendungs- und Detailquelle - die Roadmap nennt jeden Punkt knapp, die
Herleitung steht dort.

Dabei aufgefallen: die beiden nie gepflegten Plaene (Architektur, DatenIngest)
zeigten 40 offene Punkte, von denen die Haelfte umgesetzt war - Read/Control-
Split, /api/capabilities, CORS-Whitelist, Egress-Kanaele mit Cooldown und
Per-Kanal-Limiter, Ingest-Tiering. Nachgeprueft, abgehakt und mit Statusblock
eingeordnet, sonst waere das Archiv selbst eine Fehlerquelle.

Was dabei nur teilweise umgesetzt war, ist als Roadmap 5.3 aufgenommen
(Health-Statistik je Kanal, multi-homed pruefen, DB-Guard, Plausibilitaets-
pruefung), ebenso der nie durchgefuehrte Audit auf versteckte Writes in
Read-Endpunkten (6.1). Header-Rotation ist als verworfen markiert statt offen
zu bleiben: Verschleierung gegenueber Polymarket riskiert genau den Zugang, auf
dem das Projekt aufsetzt.

STATUS.md beschreibt jetzt nur noch den Ist-Stand und verweist fuer die offenen
Punkte auf die Roadmap, damit nichts doppelt gepflegt wird. CLAUDE.md nennt
beide Einstiege.

Build gruen, 126 Tests gruen, keine toten Links.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 18:46:00 +02:00

11 KiB
Raw Permalink Blame History

Fix- und Datenreparatur-Plan (Stand 2026-07-09, Übergabe an Gemini)

Status 2026-08-23: Teil D und Teil F sind umgesetzt, nachgeprüft gegen den Code. Offen sind nur noch drei Detailpunkte, jeweils unten als - [ ] stehengeblieben: der gezielte Nachlade-Schritt in F5 und die zwei Tests aus F6 (Event-Tag-Vererbung, Backfill). Belege:

  • D3IngestMode (Domain/Enums/IngestMode.cs), Einstufung in TradeHistoryWorker.cs:174-177 inklusive der geforderten Hysterese (Rückstufung erst unter halber Schwelle, Richtung Full zusätzlich erst nach 7 Tagen), wöchentliche Biopsie in TradeHistoryWorker.cs:127, Aggregations-Buckets in TradeAggregation.cs, Trait not_copyable_hf in TraderTraitCalculator.cs. Tests: IngestModeTests.cs.
  • F1/F2CanonicalTags und BlacklistTags/IsNoiseTag in Infrastructure/Helpers/MarketCategoryMapper.cs.
  • F3/F4 — Tag-Nachladen in PolymarketProvider.cs:187-196, Offline-Backfill aus Event-Tags in PredictalyticsHost.cs:114-135.

Abnahmekriterium für alle Code-Änderungen: dotnet test muss grün bleiben. Die damals notierte Zahl (16 grün + 1 übersprungen) ist überholt — Stand 2026-08-23 sind es 126 grün + 1 übersprungen (der Skip CheckpointResetAndReplay_DoesNotDoubleCountBalance ist weiterhin 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.