diff --git a/FIXPLAN-2026-07-09.md b/FIXPLAN-DONE.md
similarity index 90%
rename from FIXPLAN-2026-07-09.md
rename to FIXPLAN-DONE.md
index 17bfdd8..0ad008b 100644
--- a/FIXPLAN-2026-07-09.md
+++ b/FIXPLAN-DONE.md
@@ -320,14 +320,6 @@ zukünftiges Pruning ist damit verlustfrei im Sinne der PnL-Summen.
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).
@@ -467,54 +459,12 @@ daraus ableiten, wird nicht gespeichert.
- Resolution-Farmer-Profil (viele kleine Wins, seltene −100 %-Losses) liefert ProfitFactor nahe 1 —
der Fall, den die Winrate allein verschleiert.
-### 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 E — Master-Auswahl-Metriken & generischer Profil-Endpoint (ergänzt 2026-07-11)
-> Hintergrund: Ein externer Konsument (PolyTrader-Copytrading) braucht eine belastbare
+> Hintergrund: Ein externer Konsument (Copytrading-Client) braucht eine belastbare
> Master-Trader-Auswahl (Prüfplan liegt in `PREDICTALYTICS-PRUEFPLAN-Master-Auswahl.md` im
-> PolyTraderSharp-Repo). **Bewusste Architektur-Entscheidung: Die API bleibt generisch.**
-> Kein PolyTrader-spezifisches Format, kein „HOLDER/STOPLOSS"-Vokabular in der API —
+> externen Repo). **Bewusste Architektur-Entscheidung: Die API bleibt generisch.**
+> Kein kunden-spezifisches Format, kein „HOLDER/STOPLOSS"-Vokabular in der API —
> wir exponieren neutrale Metriken, der Konsument mappt selbst.
> Abgleich: M2/M8 existieren (D2c/CategoryPerformances), M3/M9 teilweise als Traits (D2),
> M1/M5/M6 teilweise; **komplett neu sind M4, M7, Out-of-Sample-Fenster, Kopierbarkeits-
@@ -604,7 +554,7 @@ Markt aus D2c.)
2. `GET /api/traders/{id}/profile` liefert für einen analysierten Trader alle Blöcke
gefüllt; Antwortzeit < 200 ms (nur persistierte Daten).
3. `docs/API.md` um Profile-/Correlation-Endpoint ergänzt.
-4. Kein PolyTrader-spezifisches Vokabular in API/DTOs.
+4. Kein kunden-spezifisches Vokabular in API/DTOs.
Reihenfolge: E1+E3 zuerst (Profil mit vorhandenen + Fingerprint-Daten), dann E2
(Fenster), E4/E5/E7 (Kennzahlen), E6 zuletzt. Teil E setzt D1/D2/D2c voraus.
diff --git a/FIXPLAN-TODO.md b/FIXPLAN-TODO.md
new file mode 100644
index 0000000..0c4c7fa
--- /dev/null
+++ b/FIXPLAN-TODO.md
@@ -0,0 +1,73 @@
+# 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).
+
+---
+
diff --git a/docs/API.md b/docs/API.md
index 102dc35..1482966 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -56,6 +56,17 @@ Offene und realisierte Positionen. Je **TraderPositionDto**: `marketId`,
`marketName`, `category`, `outcomeToken`, `sharesHeld`, `avgCost`, `realizedPnl`,
`unrealizedPnl`, `currentPrice`, `lastTradeExecutedAt`.
+### `GET /api/traders/{id}/profile`
+**TraderProfileDto**: Erweitertes Profil für die Master-Trader-Analyse. Enthält:
+`masterStatus` (`None`, `Candidate`, `Master`), `medianHoldDurationHours`, `p50PositionSize`,
+`p90PositionSize`, `tradesPerWeek`, `medianMarketVolumeUsd`, `medianPostFillDriftPct`,
+`netEdgeAfterFeesPct`, `priceBandProfileJson`, `windowMetrics` (Array von 60-/120-Tage Metriken).
+
+### `GET /api/traders/correlation?traderIdA=…&traderIdB=…`
+Vergleicht zwei Trader. **TraderCorrelationDto**: `commonMarketsCount`,
+`sameDirectionMarketsCount`, `intersectionRatioA`, `intersectionRatioB`,
+`agreementRatio` (wie oft in gemeinsamen Märkten die gleiche Richtung gehandelt wurde).
+
### `POST /api/traders?platform=Polymarket&wallet=0x…`
Trader manuell anlegen (Import + Discovery-Flag). Antwort: Trader-Id.
@@ -141,6 +152,6 @@ Jobs werden von den Workern abgearbeitet (Worker müssen laufen!). Status:
`medianLossReturnPct`, `avgLossReturnPct`, `profitFactor` (D2c).
- `GET /api/traders/{id}/profile` — vollständiges Analyse-Profil eines Traders in
einem Aufruf (Kennzahlen + Zeitfenster + Traits + Fingerprint-Verteilungen +
- Kopierbarkeits-Aggregate), gedacht für externe Konsumenten wie PolyTrader (Teil E).
+ Kopierbarkeits-Aggregate), gedacht für externe Konsumenten (Teil E).
- Out-of-Sample-Fenster (A/B), Preisband-Profil, Stop-Loss-Klassifikation,
Portfolio-/Korrelations-Sicht (Teil E).
diff --git a/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs b/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs
index a64cbf9..bc02031 100644
--- a/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs
+++ b/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs
@@ -1,5 +1,6 @@
using Predictalytics.Application.Interfaces;
using Predictalytics.Application.Services;
+using Microsoft.EntityFrameworkCore;
namespace Predictalytics.Api.Endpoints;
@@ -9,8 +10,11 @@ public static class TraderEndpoints
{
var group = app.MapGroup("/api/traders").WithTags("Traders");
- group.MapGet("/", async (IAnalyticsService svc, int? skip, int? take, string? platform, bool? highlyCopyable, CancellationToken ct) =>
- Results.Ok(await svc.GetTradersAsync(skip ?? 0, take ?? 50, platform, highlyCopyable ?? false, ct)));
+ group.MapGet("/", async (IAnalyticsService svc, int? skip, int? take, string? platform, bool? highlyCopyable, string? trait, CancellationToken ct) =>
+ Results.Ok(await svc.GetTradersAsync(skip ?? 0, take ?? 50, platform, highlyCopyable ?? false, trait, ct)));
+
+ group.MapGet("/traits", async (Predictalytics.Infrastructure.Data.AppDbContext db, CancellationToken ct) =>
+ Results.Ok(await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync(db.TraderTraits.Select(t => t.Trait).Distinct(), ct)));
group.MapGet("/{id:int}", async (int id, IAnalyticsService svc, CancellationToken ct) =>
{
@@ -60,5 +64,68 @@ public static class TraderEndpoints
var id = await svc.AddTraderAsync(platform, wallet, ct);
return Results.Ok(new { id });
});
+ group.MapGet("/{id:int}/profile", async (int id, Predictalytics.Infrastructure.Data.AppDbContext db, CancellationToken ct) =>
+ {
+ var trader = await db.Traders.Include(t => t.Analytics).FirstOrDefaultAsync(t => t.Id == id, ct);
+ if (trader == null || trader.Analytics == null) return Results.NotFound();
+
+ var windowMetrics = await db.TraderWindowMetrics
+ .Where(w => w.TraderId == id)
+ .OrderByDescending(w => w.WindowStart)
+ .Select(w => new Predictalytics.Application.DTOs.TraderWindowMetricsDto(
+ w.WindowStart, w.WindowEnd, w.ClosedMarkets, w.WinRate, w.AvgReturnPct,
+ w.MedianWinReturnPct, w.MedianLossReturnPct, w.ProfitFactor))
+ .ToListAsync(ct);
+
+ var profile = new Predictalytics.Application.DTOs.TraderProfileDto(
+ trader.Id,
+ trader.DisplayName ?? "",
+ trader.MasterStatus,
+ trader.Analytics.MedianHoldDurationHours,
+ trader.Analytics.P50PositionSize,
+ trader.Analytics.P90PositionSize,
+ trader.Analytics.TradesPerWeek,
+ trader.Analytics.MedianMarketVolumeUsd,
+ trader.Analytics.MedianPostFillDriftPct,
+ trader.Analytics.NetEdgeAfterFeesPct,
+ trader.Analytics.PriceBandProfileJson,
+ windowMetrics
+ );
+ return Results.Ok(profile);
+ });
+
+ group.MapGet("/correlation", async (int traderIdA, int traderIdB, Predictalytics.Infrastructure.Data.AppDbContext db, CancellationToken ct) =>
+ {
+ var positionsA = await db.TraderPositions
+ .Where(p => p.TraderId == traderIdA && p.MarketOutcome != null)
+ .Select(p => new { p.MarketOutcome!.MarketId, Direction = p.SharesHeld > 0 ? p.MarketOutcomeId : -p.MarketOutcomeId })
+ .ToListAsync(ct);
+
+ var positionsB = await db.TraderPositions
+ .Where(p => p.TraderId == traderIdB && p.MarketOutcome != null)
+ .Select(p => new { p.MarketOutcome!.MarketId, Direction = p.SharesHeld > 0 ? p.MarketOutcomeId : -p.MarketOutcomeId })
+ .ToListAsync(ct);
+
+ var marketsA = positionsA.Select(p => p.MarketId).Distinct().ToList();
+ var marketsB = positionsB.Select(p => p.MarketId).Distinct().ToList();
+
+ var commonMarkets = marketsA.Intersect(marketsB).ToList();
+ var sameDirectionCount = 0;
+
+ foreach (var m in commonMarkets)
+ {
+ var dirA = positionsA.Where(p => p.MarketId == m).Select(p => p.Direction).FirstOrDefault();
+ var dirB = positionsB.Where(p => p.MarketId == m).Select(p => p.Direction).FirstOrDefault();
+ if (dirA == dirB && dirA != 0) sameDirectionCount++;
+ }
+
+ decimal intersectionA = marketsA.Count > 0 ? (decimal)commonMarkets.Count / marketsA.Count : 0;
+ decimal intersectionB = marketsB.Count > 0 ? (decimal)commonMarkets.Count / marketsB.Count : 0;
+ decimal agreement = commonMarkets.Count > 0 ? (decimal)sameDirectionCount / commonMarkets.Count : 0;
+
+ return Results.Ok(new Predictalytics.Application.DTOs.TraderCorrelationDto(
+ traderIdA, traderIdB, commonMarkets.Count, sameDirectionCount, intersectionA, intersectionB, agreement
+ ));
+ });
}
}
diff --git a/src/Predictalytics.Api/wwwroot/index.html b/src/Predictalytics.Api/wwwroot/index.html
index e7441f2..dc3b4ce 100644
--- a/src/Predictalytics.Api/wwwroot/index.html
+++ b/src/Predictalytics.Api/wwwroot/index.html
@@ -155,6 +155,10 @@
Copyability >
+
+