Fix category mapping and TotalTrades drift, plan traits/tiering (Teil D)
- MarketCategoryMapper: classify from question text (the Gamma /markets
endpoint delivers neither category nor event tags, so on-demand markets
had no signal at all), match short tokens on word boundaries ("eth" no
longer hits inside "whether", "pop" not inside "popular"), widen the
keyword lists across all categories.
- UpdateMarketFields: never overwrite a tag-derived category with an
uninformative "Other" from the on-demand path.
- PositionPnLEngine: sync Trader.TotalTrades to the actual replayed row
count — the worker-side increment counters drift (INSERT IGNORE,
deletions, historic imports) and produced Trades30d > TotalTrades.
- Tests: 14 new (mapper classification + word-boundary regression,
TotalTrades sync + Trades30d invariant, category update guard via
SQLite) — suite now 32 green + 1 skip.
- FIXPLAN Teil D for the larger rebuilds (AggregatedCount column,
TraderTraits heuristics, IngestMode tiering for ultra-HF traders).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
940a99fec2
commit
d2f3ec2bd0
@@ -318,3 +318,93 @@ zukünftiges Pruning ist damit verlustfrei im Sinne der PnL-Summen.
|
||||
7. Watchlist: Toggle auf der Detailseite wechselt sichtbar den Zustand; die neue Watchlist-Seite listet die
|
||||
beobachteten Trader; Remove funktioniert. *(A10)*
|
||||
8. „Run Deep Analysis" (KI) füllt die AI Strategy Analysis auf der Detailseite tatsächlich. *(A10)*
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
### D1. `AggregatedCount`-Spalte (Grundlage für D2/D3)
|
||||
|
||||
- Migration: `Trades.AggregatedCount INT NULL` (NULL = einzelner Roh-Trade).
|
||||
- Kompaktierung im `TradeRetentionWorker`: schreibt `AggregatedCount = Anzahl der ersetzten Trades`
|
||||
(heute geht die Original-Anzahl verloren!) und summiert beim erneuten Kompaktieren
|
||||
bestehende Aggregate (`Sum(t.AggregatedCount ?? 1)`).
|
||||
- Engine: `trader.TotalTrades = trades.Sum(t => t.AggregatedCount ?? 1)` (ersetzt `trades.Count`),
|
||||
`analytics.Trades30d` analog.
|
||||
- **Test:** Kompaktierung von 5 Trades → 1 Aggregat mit `AggregatedCount = 5`; `TotalTrades` bleibt nach
|
||||
Recalc 5, nicht 1.
|
||||
|
||||
### D2. TraderTraits — heuristische Strategie-Merkmale (ohne KI)
|
||||
|
||||
- Neue Tabelle `TraderTraits`: Id, TraderId (FK, Cascade), Trait (string ≤ 64), Value (decimal, Messwert),
|
||||
ComputedAt. Unique-Index (TraderId, Trait). Pro Analyse-Lauf upserten, nicht mehr zutreffende Traits löschen.
|
||||
- Berechnung als **pure Funktion** `TraderTraitCalculator.Compute(trader, trades, positions)` →
|
||||
Liste (Trait, Value); Aufruf im `TraderAnalyticsWorker` nach Engine + Estimator aus den **bereits geladenen**
|
||||
Daten — keine zusätzlichen API-Calls.
|
||||
- Traits v1 (Trait vergeben, wenn Bedingung erfüllt; Value = Messgröße):
|
||||
|
||||
| Trait | Regel |
|
||||
|---|---|
|
||||
| `sub_second_cadence` | Median-Intervall < 2 s bei ≥ 50 Trades (Value = Median in s) |
|
||||
| `always_on_24_7` | größte Inaktivitätslücke der letzten 7 Tage < 4 h bei ≥ 200 Trades/7d |
|
||||
| `uniform_sizes` | Variationskoeffizient der Size (letzte 200 Trades) < 0,1 |
|
||||
| `round_amounts` | > 60 % der Amounts ∈ {1,5,10,20,25,50,100,250,500,1000} ± 1 % |
|
||||
| `uses_split_merge` | Anteil Split+Merge > 10 % |
|
||||
| `both_sides_same_market` | Yes- UND No-Trades in > 20 % der Märkte |
|
||||
| `resolution_farming` | > 30 % der Buys mit Price ≥ 0,93 UND < 48 h vor `ClosedAt` (min. 10 Buys) |
|
||||
| `longshot_buyer` | > 30 % der Buys mit Price ≤ 0,10 |
|
||||
| `scalper` | mediane Haltedauer < 1 h |
|
||||
| `holds_to_resolution` | > 70 % der aufgelösten Positionen ohne vorherigen Sell |
|
||||
| `fresh_wallet` | erster Trade < 30 Tage (Value = Alter in Tagen) |
|
||||
| `stable_stake_fraction` | CV von Amount/EstimatedBankroll < 0,5 (nur bei Bankroll > 0) |
|
||||
|
||||
- Schwellwerte als Konstanten im Calculator (v1 hart kodiert ist ok).
|
||||
- API/UI: `TraderDto`/`TraderDetailDto` um `Traits` (string-Liste) erweitern; Detailseite zeigt Chips
|
||||
unter dem Strategy-Feld; Traders-Liste bekommt einen Trait-Filter.
|
||||
- KI-Integration: `AiStrategyAnalysisService`-Prompt bekommt die Merkmalsliste; Trade-Beispiele von 50 auf
|
||||
15 repräsentative reduzieren (5 größte, 5 jüngste, 5 zufällige) — die KI verifiziert Hypothesen statt zu raten.
|
||||
- **Tests:** pure-Function-Tests je Trait, mindestens Positiv- UND Negativfall für `resolution_farming`,
|
||||
`sub_second_cadence`, `uniform_sizes`, `both_sides_same_market`.
|
||||
|
||||
### 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.
|
||||
4. Tägliches DB-Wachstum sichtbar reduziert (DB-Size-Anzeige im WinForms-Statusbar beobachten).
|
||||
|
||||
Reihenfolge: **D1 → D2 → D3** (bei D3 zuerst Tier C, dann Tier B).
|
||||
|
||||
Reference in New Issue
Block a user