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:
Richard
2026-07-14 09:04:31 +02:00
parent a1fcb4ace5
commit 16431f38a5
39 changed files with 9028 additions and 167 deletions
@@ -22,6 +22,8 @@ public class AppDbContext : DbContext
public DbSet<TraderCategoryPerformance> TraderCategoryPerformances => Set<TraderCategoryPerformance>();
public DbSet<TradeContext> TradeContexts => Set<TradeContext>();
public DbSet<BackgroundJob> BackgroundJobs => Set<BackgroundJob>();
public DbSet<TraderTrait> TraderTraits => Set<TraderTrait>();
public DbSet<TraderWindowMetrics> TraderWindowMetrics => Set<TraderWindowMetrics>();
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
@@ -40,6 +42,30 @@ public class AppDbContext : DbContext
.HasForeignKey<TraderScore>(s => s.TraderId).OnDelete(DeleteBehavior.Cascade);
});
// TraderTrait
mb.Entity<TraderTrait>(e =>
{
e.HasKey(t => t.Id);
e.HasIndex(t => new { t.TraderId, t.Trait }).IsUnique();
e.Property(t => t.Value).HasPrecision(18, 4);
e.HasOne(t => t.Trader).WithMany(tr => tr.Traits)
.HasForeignKey(t => t.TraderId).OnDelete(DeleteBehavior.Cascade);
});
// TraderWindowMetrics
mb.Entity<TraderWindowMetrics>(e =>
{
e.HasKey(t => t.Id);
e.HasIndex(t => new { t.TraderId, t.WindowStart, t.WindowEnd }).IsUnique();
e.Property(t => t.WinRate).HasPrecision(8, 4);
e.Property(t => t.AvgReturnPct).HasPrecision(18, 4);
e.Property(t => t.MedianWinReturnPct).HasPrecision(18, 4);
e.Property(t => t.MedianLossReturnPct).HasPrecision(18, 4);
e.Property(t => t.ProfitFactor).HasPrecision(18, 4);
e.HasOne(t => t.Trader).WithMany()
.HasForeignKey(t => t.TraderId).OnDelete(DeleteBehavior.Cascade);
});
// Trade
mb.Entity<Trade>(e =>
{
@@ -193,6 +219,22 @@ public class AppDbContext : DbContext
e.Property(a => a.WinRate7d).HasPrecision(8, 4);
e.Property(a => a.PnL24h).HasPrecision(18, 4);
e.Property(a => a.WinRate24h).HasPrecision(8, 4);
// D2c & E3 & E5
e.Property(a => a.MedianWinReturnPct).HasPrecision(18, 4);
e.Property(a => a.AvgWinReturnPct).HasPrecision(18, 4);
e.Property(a => a.MedianLossReturnPct).HasPrecision(18, 4);
e.Property(a => a.AvgLossReturnPct).HasPrecision(18, 4);
e.Property(a => a.ProfitFactor).HasPrecision(18, 4);
e.Property(a => a.MedianHoldDurationHours).HasPrecision(18, 4);
e.Property(a => a.P50PositionSize).HasPrecision(18, 4);
e.Property(a => a.P90PositionSize).HasPrecision(18, 4);
e.Property(a => a.TradesPerWeek).HasPrecision(18, 4);
e.Property(a => a.MedianMarketVolumeUsd).HasPrecision(18, 4);
e.Property(a => a.MedianPostFillDriftPct).HasPrecision(18, 4);
e.Property(a => a.NetEdgeAfterFeesPct).HasPrecision(18, 4);
});
// MarketAnalytics
@@ -70,14 +70,14 @@ public class TradeRepository : ITradeRepository
foreach (var chunk in tradeList.Chunk(500))
{
var sb = new System.Text.StringBuilder("INSERT IGNORE INTO Trades (PlatformTradeId, MarketId, AssetId, Outcome, Side, Price, Size, Amount, ExecutedAt, TransactionHash, TraderId, MarketOutcomeId, DbMarketId, Platform, IsContextEnriched) VALUES ");
var sb = new System.Text.StringBuilder("INSERT INTO Trades (PlatformTradeId, MarketId, AssetId, Outcome, Side, Price, Size, Amount, ExecutedAt, TransactionHash, TraderId, MarketOutcomeId, DbMarketId, Platform, IsContextEnriched, AggregatedCount) VALUES ");
var parameters = new List<object>();
for (int i = 0; i < chunk.Length; i++)
{
var t = chunk[i];
int pIdx = i * 15;
sb.Append($"({{{pIdx}}}, {{{pIdx + 1}}}, {{{pIdx + 2}}}, {{{pIdx + 3}}}, {{{pIdx + 4}}}, {{{pIdx + 5}}}, {{{pIdx + 6}}}, {{{pIdx + 7}}}, {{{pIdx + 8}}}, {{{pIdx + 9}}}, {{{pIdx + 10}}}, {{{pIdx + 11}}}, {{{pIdx + 12}}}, {{{pIdx + 13}}}, {{{pIdx + 14}}})");
int pIdx = i * 16;
sb.Append($"({{{pIdx}}}, {{{pIdx + 1}}}, {{{pIdx + 2}}}, {{{pIdx + 3}}}, {{{pIdx + 4}}}, {{{pIdx + 5}}}, {{{pIdx + 6}}}, {{{pIdx + 7}}}, {{{pIdx + 8}}}, {{{pIdx + 9}}}, {{{pIdx + 10}}}, {{{pIdx + 11}}}, {{{pIdx + 12}}}, {{{pIdx + 13}}}, {{{pIdx + 14}}}, {{{pIdx + 15}}})");
if (i < chunk.Length - 1)
sb.Append(", ");
@@ -97,8 +97,12 @@ public class TradeRepository : ITradeRepository
parameters.Add(t.DbMarketId ?? (object?)null);
parameters.Add((int)t.Platform);
parameters.Add(t.IsContextEnriched);
parameters.Add(t.AggregatedCount ?? (object?)null);
}
// For Aggregated Trades, we want UPSERT logic to update size, amount and VWAP
sb.Append(" ON DUPLICATE KEY UPDATE Price=VALUES(Price), Size=VALUES(Size), Amount=VALUES(Amount), AggregatedCount=VALUES(AggregatedCount);");
int maxRetries = 3;
var backoffs = new[] { 250, 500, 1000 };
for (int retry = 0; retry <= maxRetries; retry++)
@@ -17,6 +17,7 @@ public class TraderRepository : ITraderRepository
.Include(t => t.CurrentScore)
.Include(t => t.Analytics)
.Include(t => t.CategoryPerformances)
.Include(t => t.Traits)
.FirstOrDefaultAsync(t => t.Id == id, ct);
}
@@ -29,6 +30,7 @@ public class TraderRepository : ITraderRepository
var q = _db.Traders
.Include(t => t.CurrentScore)
.Include(t => t.Analytics)
.Include(t => t.Traits)
.AsQueryable();
if (platform.HasValue) q = q.Where(t => t.Platform == platform.Value);