Add showcase leaderboards endpoint + sortable trader list
- GET /api/traders/showcases: curated dashboard sections (copy-ready, smooth operators, rising stars, high conviction, specialists, insider watch, red flags), each encoding a selection funnel over the persisted analytics/traits. Pure ShowcaseBuilder holds the ranking logic (+6 unit tests). - GET /api/traders?sort=: leaderboard sort keys (pnl, pnl30d, winrate, copytrading, calmar, conviction, profitfactor) over the loaded set. Read-only, persisted-data-only (public-tier safe). 76 tests, 1 skip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
eaccdadddf
commit
02c5a4d6f4
@@ -0,0 +1,113 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Predictalytics.Application.DTOs;
|
||||
using Predictalytics.Application.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Predictalytics.Application.Tests.Services;
|
||||
|
||||
public class ShowcaseBuilderTests
|
||||
{
|
||||
private static TraderDto Dto(int id) => new(
|
||||
id, "Polymarket", "0x" + id, "T" + id, "Bronze", "Unknown",
|
||||
CombinedScore: 0, CopytradingScore: 0, CopytradingQualityScore: 0, CopytradingCopyabilityScore: 0,
|
||||
WinRate: 0, TotalPnl: 0, TotalTrades: 0, Trades30d: 0, PnL30d: 0, WinRate30d: 0, EstimatedBankroll: 0,
|
||||
IsOnWatchlist: false, IsSuspectedBot: false, LastPolledAt: null, Traits: null);
|
||||
|
||||
private static ShowcaseCandidate Cand(
|
||||
int id,
|
||||
decimal copytrading = 0, decimal totalPnl = 0, decimal pnl30d = 0,
|
||||
decimal? profitFactor = null, decimal? calmar = null, decimal maxDd = 0,
|
||||
decimal? conviction = null, decimal concentration = 0,
|
||||
bool bot = false, bool snapshot = false, int totalTrades = 0,
|
||||
bool insider = false, bool farmer = false)
|
||||
=> new(Dto(id), copytrading, totalPnl, pnl30d, profitFactor, calmar, maxDd,
|
||||
conviction, concentration, bot, snapshot, totalTrades, insider, farmer);
|
||||
|
||||
private static List<int> Ids(IReadOnlyList<ShowcaseSection> s, string key)
|
||||
=> s.FirstOrDefault(x => x.Key == key)?.Traders.Select(t => t.Id).ToList() ?? new List<int>();
|
||||
|
||||
[Fact]
|
||||
public void CopyReady_IncludesQualified_ExcludesBotAndThinProfit()
|
||||
{
|
||||
var s = ShowcaseBuilder.Build(new[]
|
||||
{
|
||||
Cand(1, copytrading: 70, totalTrades: 50, profitFactor: 2.0m), // qualifies
|
||||
Cand(2, copytrading: 70, totalTrades: 50, profitFactor: 2.0m, bot: true), // bot -> out
|
||||
Cand(3, copytrading: 70, totalTrades: 50, profitFactor: 1.1m), // thin profit -> out
|
||||
Cand(4, copytrading: 40, totalTrades: 50, profitFactor: 2.0m), // low score -> out
|
||||
});
|
||||
|
||||
var ids = Ids(s, "copy_ready");
|
||||
Assert.Contains(1, ids);
|
||||
Assert.DoesNotContain(2, ids);
|
||||
Assert.DoesNotContain(3, ids);
|
||||
Assert.DoesNotContain(4, ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmoothOperators_RanksByCalmar_RequiresDrawdown()
|
||||
{
|
||||
var s = ShowcaseBuilder.Build(new[]
|
||||
{
|
||||
Cand(1, totalPnl: 500, calmar: 5m, maxDd: 100m),
|
||||
Cand(2, totalPnl: 500, calmar: 9m, maxDd: 50m),
|
||||
Cand(3, totalPnl: 500, calmar: null, maxDd: 0m), // no drawdown recorded -> excluded
|
||||
});
|
||||
|
||||
var ids = Ids(s, "smooth_operators");
|
||||
Assert.Equal(new[] { 2, 1 }, ids); // higher calmar first
|
||||
Assert.DoesNotContain(3, ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HighConviction_OnlyPositive()
|
||||
{
|
||||
var s = ShowcaseBuilder.Build(new[]
|
||||
{
|
||||
Cand(1, conviction: 25m),
|
||||
Cand(2, conviction: -10m), // overbets losers -> out of conviction
|
||||
Cand(3, conviction: null),
|
||||
});
|
||||
|
||||
Assert.Equal(new[] { 1 }, Ids(s, "high_conviction"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RedFlags_FlagsHighPnlWithNegativeConvictionOrFarming()
|
||||
{
|
||||
var s = ShowcaseBuilder.Build(new[]
|
||||
{
|
||||
Cand(1, totalPnl: 10000, conviction: -5m), // negative conviction
|
||||
Cand(2, totalPnl: 20000, profitFactor: 1.05m), // thin profit factor
|
||||
Cand(3, totalPnl: 30000, farmer: true), // resolution farmer
|
||||
Cand(4, totalPnl: 50000, conviction: 10m, profitFactor: 3m), // clean whale -> not flagged
|
||||
});
|
||||
|
||||
var ids = Ids(s, "red_flags");
|
||||
Assert.Contains(1, ids);
|
||||
Assert.Contains(2, ids);
|
||||
Assert.Contains(3, ids);
|
||||
Assert.DoesNotContain(4, ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsiderWatch_OnlyPossibleInsiders()
|
||||
{
|
||||
var s = ShowcaseBuilder.Build(new[]
|
||||
{
|
||||
Cand(1, totalPnl: 5000, insider: true),
|
||||
Cand(2, totalPnl: 9000, insider: false),
|
||||
});
|
||||
|
||||
Assert.Equal(new[] { 1 }, Ids(s, "insider_watch"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptySections_AreOmitted()
|
||||
{
|
||||
// No candidate qualifies for anything -> no sections at all.
|
||||
var s = ShowcaseBuilder.Build(new[] { Cand(1) });
|
||||
Assert.Empty(s);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user