diff --git a/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs b/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs index 5ce4c6a..39443c4 100644 --- a/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs +++ b/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs @@ -13,8 +13,8 @@ public static class TraderEndpoints { 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))); + group.MapGet("/", async (IAnalyticsService svc, int? skip, int? take, string? platform, bool? highlyCopyable, string? trait, string? sort, decimal? minWinRate, decimal? minCopyability, CancellationToken ct) => + Results.Ok(await svc.GetTradersAsync(skip ?? 0, take ?? 50, platform, highlyCopyable ?? false, trait, sort, minWinRate, minCopyability, ct))); // Curated dashboard showcases (leaderboards). Read-only, persisted data only. group.MapGet("/showcases", async (IAnalyticsService svc, CancellationToken ct) => diff --git a/src/Predictalytics.Application.Tests/Services/AnalyticsServiceTests.cs b/src/Predictalytics.Application.Tests/Services/AnalyticsServiceTests.cs index 6a3fbc4..d5fcd88 100644 --- a/src/Predictalytics.Application.Tests/Services/AnalyticsServiceTests.cs +++ b/src/Predictalytics.Application.Tests/Services/AnalyticsServiceTests.cs @@ -101,6 +101,51 @@ public class AnalyticsServiceTests Assert.Equal(87.5m, deepDive.EntryQuality); } + [Fact] + public async Task GetTradersAsync_MinFilters_ExcludeBelowThreshold() + { + // Arrange + using var db = CreateDbContext(); + var traderRepo = new TraderRepository(db); + var tradeRepo = new TradeRepository(db, NullLogger.Instance); + var discoveryMock = new Mock(); + var pnlEngineMock = new Mock(); + var alertRepoMock = new Mock(); + var watchlistRepoMock = new Mock(); + watchlistRepoMock.Setup(w => w.GetAllAsync(It.IsAny())) + .ReturnsAsync(new List()); + var marketRepo = new MarketRepository(db); + var providers = new List(); + + var svc = new AnalyticsService( + traderRepo, tradeRepo, alertRepoMock.Object, watchlistRepoMock.Object, + marketRepo, discoveryMock.Object, providers, pnlEngineMock.Object, + NullLogger.Instance); + + // Three traders: WinRate 40 / 60 / 80, Copyability 30 / 55 / 90. + db.Traders.Add(new Trader { Id = 1, PlatformUserId = "0xA", DisplayName = "Low", WinRate = 40m, + Analytics = new TraderAnalytics { TraderId = 1, CopytradingCopyabilityScore = 30m } }); + db.Traders.Add(new Trader { Id = 2, PlatformUserId = "0xB", DisplayName = "Mid", WinRate = 60m, + Analytics = new TraderAnalytics { TraderId = 2, CopytradingCopyabilityScore = 55m } }); + db.Traders.Add(new Trader { Id = 3, PlatformUserId = "0xC", DisplayName = "High", WinRate = 80m, + Analytics = new TraderAnalytics { TraderId = 3, CopytradingCopyabilityScore = 90m } }); + await db.SaveChangesAsync(); + + // Act — minWinRate 55 keeps Mid + High. + var byWinRate = await svc.GetTradersAsync(minWinRate: 55m); + // minCopyability 60 keeps only High. + var byCopyability = await svc.GetTradersAsync(minCopyability: 60m); + // No filter keeps all three. + var unfiltered = await svc.GetTradersAsync(); + + // Assert + Assert.Equal(2, byWinRate.Count); + Assert.DoesNotContain(byWinRate, t => t.DisplayName == "Low"); + Assert.Single(byCopyability); + Assert.Equal("High", byCopyability[0].DisplayName); + Assert.Equal(3, unfiltered.Count); + } + private class MockDiscoveryService : IDiscoveryService { public Task ImportTraderAsync(PlatformType platform, string platformUserId, string displayName, bool isAutoDiscovered = true, CancellationToken ct = default) diff --git a/src/Predictalytics.Application/Interfaces/IAnalyticsService.cs b/src/Predictalytics.Application/Interfaces/IAnalyticsService.cs index 2e3aeba..b856e5a 100644 --- a/src/Predictalytics.Application/Interfaces/IAnalyticsService.cs +++ b/src/Predictalytics.Application/Interfaces/IAnalyticsService.cs @@ -10,7 +10,7 @@ public interface IAnalyticsService /// Perform deep-dive analysis on a specific trader. Task GetTraderDeepDiveAsync(int traderId, CancellationToken ct = default); - Task> GetTradersAsync(int skip = 0, int take = 50, string? platform = null, bool highlyCopyable = false, string? traitFilter = null, string? sort = null, CancellationToken ct = default); + Task> GetTradersAsync(int skip = 0, int take = 50, string? platform = null, bool highlyCopyable = false, string? traitFilter = null, string? sort = null, decimal? minWinRate = null, decimal? minCopyability = null, CancellationToken ct = default); /// Curated dashboard showcase sections (copy-ready, smooth operators, rising stars, ...). Task> GetShowcasesAsync(CancellationToken ct = default); diff --git a/src/Predictalytics.Application/Services/AnalyticsService.cs b/src/Predictalytics.Application/Services/AnalyticsService.cs index c5cd823..c19805d 100644 --- a/src/Predictalytics.Application/Services/AnalyticsService.cs +++ b/src/Predictalytics.Application/Services/AnalyticsService.cs @@ -162,7 +162,7 @@ public class AnalyticsService : IAnalyticsService )).ToList(); } - public async Task> GetTradersAsync(int skip = 0, int take = 50, string? platform = null, bool highlyCopyable = false, string? traitFilter = null, string? sort = null, CancellationToken ct = default) + public async Task> GetTradersAsync(int skip = 0, int take = 50, string? platform = null, bool highlyCopyable = false, string? traitFilter = null, string? sort = null, decimal? minWinRate = null, decimal? minCopyability = null, CancellationToken ct = default) { PlatformType? pType = null; if (!string.IsNullOrEmpty(platform) && platform != "All" && Enum.TryParse(platform, true, out var pt)) @@ -180,6 +180,17 @@ public class AnalyticsService : IAnalyticsService traders = traders.Where(t => t.Traits.Any(tr => tr.Trait == traitFilter)).ToList(); } + // Min-value filters applied server-side so they see the full pool, not just the first page. + if (minWinRate is > 0) + { + traders = traders.Where(t => t.WinRate >= minWinRate.Value).ToList(); + } + + if (minCopyability is > 0) + { + traders = traders.Where(t => (t.Analytics?.CopytradingCopyabilityScore ?? 0) >= minCopyability.Value).ToList(); + } + // Leaderboard sort keys (in memory — traders are already materialized above). traders = (sort?.ToLowerInvariant()) switch {