using Predictalytics.Application.Interfaces; using Predictalytics.Application.Services; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace Predictalytics.Api.Endpoints; public static class TraderEndpoints { public static void MapTraderReadEndpoints(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/api/traders").WithTags("Traders"); group.MapGet("/", async (IAnalyticsService svc, int? skip, int? take, string? platform, bool? highlyCopyable, string? trait, string? sort, CancellationToken ct) => Results.Ok(await svc.GetTradersAsync(skip ?? 0, take ?? 50, platform, highlyCopyable ?? false, trait, sort, ct))); // Curated dashboard showcases (leaderboards). Read-only, persisted data only. group.MapGet("/showcases", async (IAnalyticsService svc, CancellationToken ct) => Results.Ok(await svc.GetShowcasesAsync(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) => { var detail = await svc.GetTraderDetailAsync(id, ct); return detail is not null ? Results.Ok(detail) : Results.NotFound(); }); group.MapGet("/{id:int}/positions", async (int id, IAnalyticsService svc, CancellationToken ct) => { var positions = await svc.GetTraderPositionsAsync(id, ct); return Results.Ok(positions); }); 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 )); }); } public static void MapTraderControlEndpoints(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/api/traders").WithTags("Traders"); group.MapGet("/{id:int}/deep-dive", async (int id, IAnalyticsService svc, CancellationToken ct) => { var dd = await svc.GetTraderDeepDiveAsync(id, ct); return dd is not null ? Results.Ok(dd) : Results.NotFound(); }); group.MapPost("/{id:int}/priority", async (int id, int? score, IScoringService svc, CancellationToken ct) => { await svc.SetManualOverrideAsync(id, score, ct); return Results.Ok(); }); group.MapPost("/{id:int}/watchlist", async (int id, WatchlistService svc, CancellationToken ct) => { await svc.AddAsync(id, "Watched via UI", null, ct); return Results.Ok(); }); group.MapDelete("/{id:int}/watchlist", async (int id, WatchlistService svc, CancellationToken ct) => { await svc.RemoveByTraderIdAsync(id, ct); return Results.Ok(); }); group.MapPost("/{id:int}/ai-analysis", async (int id, bool manual, IAiStrategyAnalysisService aiSvc, CancellationToken ct) => { var summary = await aiSvc.AnalyzeTraderStrategyAsync(id, manual, ct); return Results.Ok(new { summary }); }); group.MapPost("/", async (string platform, string wallet, IAnalyticsService svc, CancellationToken ct) => { var id = await svc.AddTraderAsync(platform, wallet, ct); return Results.Ok(new { id }); }); } }