@
UI-U3: server-side min-winrate/min-copyability filters Add optional minWinRate/minCopyability params to GetTradersAsync so the leaderboard min-value filters see the full pool instead of being applied client-side to only the first page of results. Covered by a new AnalyticsService test (three traders, both filters). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
@@ -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) =>
|
||||
|
||||
@@ -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<TradeRepository>.Instance);
|
||||
var discoveryMock = new Mock<IDiscoveryService>();
|
||||
var pnlEngineMock = new Mock<IPositionPnLEngine>();
|
||||
var alertRepoMock = new Mock<IAlertRepository>();
|
||||
var watchlistRepoMock = new Mock<IWatchlistRepository>();
|
||||
watchlistRepoMock.Setup(w => w.GetAllAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<WatchlistEntry>());
|
||||
var marketRepo = new MarketRepository(db);
|
||||
var providers = new List<IPlatformProvider>();
|
||||
|
||||
var svc = new AnalyticsService(
|
||||
traderRepo, tradeRepo, alertRepoMock.Object, watchlistRepoMock.Object,
|
||||
marketRepo, discoveryMock.Object, providers, pnlEngineMock.Object,
|
||||
NullLogger<AnalyticsService>.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<int> ImportTraderAsync(PlatformType platform, string platformUserId, string displayName, bool isAutoDiscovered = true, CancellationToken ct = default)
|
||||
|
||||
@@ -10,7 +10,7 @@ public interface IAnalyticsService
|
||||
|
||||
/// <summary>Perform deep-dive analysis on a specific trader.</summary>
|
||||
Task<TraderDeepDiveDto?> GetTraderDeepDiveAsync(int traderId, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<TraderDto>> GetTradersAsync(int skip = 0, int take = 50, string? platform = null, bool highlyCopyable = false, string? traitFilter = null, string? sort = null, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<TraderDto>> 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);
|
||||
|
||||
/// <summary>Curated dashboard showcase sections (copy-ready, smooth operators, rising stars, ...).</summary>
|
||||
Task<IReadOnlyList<Services.ShowcaseSection>> GetShowcasesAsync(CancellationToken ct = default);
|
||||
|
||||
@@ -162,7 +162,7 @@ public class AnalyticsService : IAnalyticsService
|
||||
)).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<TraderDto>> 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<IReadOnlyList<TraderDto>> 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<PlatformType>(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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user