feat: implement Part D and E from FIXPLAN
- D1/D2/D2c: Added TraderTraits entity, TraderTraitCalculator, Market Return Metrics (MedianWin, AvgWin, etc.), and trait filters
- D3: Implemented HF-Trader Tiering via IngestMode (Full, Aggregated, SnapshotOnly) and updated TradeHistoryWorker to respect tiers
- E1-E5: Added MasterStatus to Trader, TraderWindowMetrics for rolling analytics, Fingerprint metrics (PriceBandProfile, P50/P90), Copyability aggregates (Volume, Drift, Edge)
- E6: Implemented GET /api/traders/{id}/profile and GET /api/traders/correlation
- Replaced FIXPLAN-2026-07-09.md with FIXPLAN-TODO.md and FIXPLAN-DONE.md
- Cleaned up API docs and plan to use generic terms (removed hardcoded PolyTrader references)
- Added respective EF Core Migrations
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Predictalytics.Application.Interfaces;
|
||||
using Predictalytics.Application.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Predictalytics.Api.Endpoints;
|
||||
|
||||
@@ -9,8 +10,11 @@ public static class TraderEndpoints
|
||||
{
|
||||
var group = app.MapGroup("/api/traders").WithTags("Traders");
|
||||
|
||||
group.MapGet("/", async (IAnalyticsService svc, int? skip, int? take, string? platform, bool? highlyCopyable, CancellationToken ct) =>
|
||||
Results.Ok(await svc.GetTradersAsync(skip ?? 0, take ?? 50, platform, highlyCopyable ?? false, ct)));
|
||||
group.MapGet("/", async (IAnalyticsService svc, int? skip, int? take, string? platform, bool? highlyCopyable, string? trait, CancellationToken ct) =>
|
||||
Results.Ok(await svc.GetTradersAsync(skip ?? 0, take ?? 50, platform, highlyCopyable ?? false, trait, 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) =>
|
||||
{
|
||||
@@ -60,5 +64,68 @@ public static class TraderEndpoints
|
||||
var id = await svc.AddTraderAsync(platform, wallet, ct);
|
||||
return Results.Ok(new { id });
|
||||
});
|
||||
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
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user