From 16431f38a5d2e08a97268702db637df0971f3938 Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 14 Jul 2026 09:04:31 +0200 Subject: [PATCH] feat: implement Part D and E from FIXPLAN - D1/D2/D2c: Added TraderTraits entity, TraderTraitCalculator, Market Return Metrics (MedianWin, AvgWin, etc.), and trait filters - D3: Implemented HF-Trader Tiering via IngestMode (Full, Aggregated, SnapshotOnly) and updated TradeHistoryWorker to respect tiers - E1-E5: Added MasterStatus to Trader, TraderWindowMetrics for rolling analytics, Fingerprint metrics (PriceBandProfile, P50/P90), Copyability aggregates (Volume, Drift, Edge) - E6: Implemented GET /api/traders/{id}/profile and GET /api/traders/correlation - Replaced FIXPLAN-2026-07-09.md with FIXPLAN-TODO.md and FIXPLAN-DONE.md - Cleaned up API docs and plan to use generic terms (removed hardcoded PolyTrader references) - Added respective EF Core Migrations --- FIXPLAN-2026-07-09.md => FIXPLAN-DONE.md | 58 +- FIXPLAN-TODO.md | 73 + docs/API.md | 13 +- .../Endpoints/TraderEndpoints.cs | 71 +- src/Predictalytics.Api/wwwroot/index.html | 11 + src/Predictalytics.Api/wwwroot/js/app.js | 50 +- .../Services/PositionPnLEngineTests.cs | 48 + .../DTOs/TraderDto.cs | 55 +- .../Interfaces/IAnalyticsService.cs | 5 +- .../Services/AiStrategyAnalysisService.cs | 8 + .../Services/AnalyticsService.cs | 23 +- .../Services/TraderTraitCalculator.cs | 329 +++++ .../Entities/AnalyticsEntities.cs | 19 + src/Predictalytics.Domain/Entities/Trade.cs | 14 +- src/Predictalytics.Domain/Entities/Trader.cs | 7 + .../Entities/TraderTrait.cs | 22 + .../Entities/TraderWindowMetrics.cs | 21 + src/Predictalytics.Domain/Enums/IngestMode.cs | 24 + .../Enums/MasterStatus.cs | 9 + .../Data/AppDbContext.cs | 42 + .../Data/Repositories/TradeRepository.cs | 10 +- .../Data/Repositories/TraderRepository.cs | 2 + ...0239_AddAggregatedCountToTrade.Designer.cs | 1103 ++++++++++++++ ...0260713090239_AddAggregatedCountToTrade.cs | 28 + ...20260713091529_AddTraderTraits.Designer.cs | 1147 +++++++++++++++ .../20260713091529_AddTraderTraits.cs | 53 + ...3093016_AddMarketReturnMetrics.Designer.cs | 1162 +++++++++++++++ .../20260713093016_AddMarketReturnMetrics.cs | 72 + ...0713105642_AddTraderIngestMode.Designer.cs | 1165 +++++++++++++++ .../20260713105642_AddTraderIngestMode.cs | 29 + ...ddMasterStatusAndWindowMetrics.Designer.cs | 1230 ++++++++++++++++ ...3130137_AddMasterStatusAndWindowMetrics.cs | 69 + ...82342_AddTraderAnalyticsFields.Designer.cs | 1266 +++++++++++++++++ ...20260713182342_AddTraderAnalyticsFields.cs | 222 +++ .../Migrations/AppDbContextModelSnapshot.cs | 166 +++ .../Services/PositionPnLEngine.cs | 75 +- .../Services/TradeHistoryWorker.cs | 266 +++- .../Services/TradeRetentionWorker.cs | 9 +- .../Services/TraderAnalyticsWorker.cs | 219 ++- 39 files changed, 9028 insertions(+), 167 deletions(-) rename FIXPLAN-2026-07-09.md => FIXPLAN-DONE.md (90%) create mode 100644 FIXPLAN-TODO.md create mode 100644 src/Predictalytics.Application/Services/TraderTraitCalculator.cs create mode 100644 src/Predictalytics.Domain/Entities/TraderTrait.cs create mode 100644 src/Predictalytics.Domain/Entities/TraderWindowMetrics.cs create mode 100644 src/Predictalytics.Domain/Enums/IngestMode.cs create mode 100644 src/Predictalytics.Domain/Enums/MasterStatus.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260713090239_AddAggregatedCountToTrade.Designer.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260713090239_AddAggregatedCountToTrade.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260713091529_AddTraderTraits.Designer.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260713091529_AddTraderTraits.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260713093016_AddMarketReturnMetrics.Designer.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260713093016_AddMarketReturnMetrics.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260713105642_AddTraderIngestMode.Designer.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260713105642_AddTraderIngestMode.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260713130137_AddMasterStatusAndWindowMetrics.Designer.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260713130137_AddMasterStatusAndWindowMetrics.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260713182342_AddTraderAnalyticsFields.Designer.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260713182342_AddTraderAnalyticsFields.cs 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 > + +