- 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>
144 lines
6.8 KiB
C#
144 lines
6.8 KiB
C#
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 });
|
|
});
|
|
}
|
|
}
|