using Predictalytics.Domain.Entities; using Predictalytics.Domain.Enums; using Predictalytics.Domain.Interfaces; using Microsoft.EntityFrameworkCore; using System.Text; namespace Predictalytics.Infrastructure.Data.Repositories; public class TraderRepository : ITraderRepository { private readonly AppDbContext _db; public TraderRepository(AppDbContext db) => _db = db; public async Task GetByIdAsync(int id, CancellationToken ct = default) { return await _db.Traders .Include(t => t.CurrentScore) .Include(t => t.Analytics) .Include(t => t.CategoryPerformances) .Include(t => t.Traits) .FirstOrDefaultAsync(t => t.Id == id, ct); } public async Task GetByPlatformIdAsync(PlatformType platform, string platformUserId, CancellationToken ct = default) => await _db.Traders.Include(t => t.CurrentScore).Include(t => t.CategoryPerformances) .FirstOrDefaultAsync(t => t.Platform == platform && t.PlatformUserId == platformUserId, ct); public async Task> GetAllAsync(PlatformType? platform = null, int skip = 0, int take = 50, CancellationToken ct = default) { 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); // Sort by CombinedScore, then by PnL as fallback return await q.OrderByDescending(t => t.CurrentScore != null ? t.CurrentScore.CombinedScore : 0) .ThenByDescending(t => t.TotalPnl) .Skip(skip).Take(take).ToListAsync(ct); } public async Task> GetWatchlistedAsync(CancellationToken ct = default) => await _db.Traders.Include(t => t.CurrentScore).Include(t => t.WatchlistEntries) .Where(t => t.WatchlistEntries.Any()).ToListAsync(ct); public async Task> GetTopByScoreAsync(int count = 20, CancellationToken ct = default) => await _db.Traders.Include(t => t.CurrentScore).Include(t => t.Analytics) .OrderByDescending(t => t.CurrentScore!.CombinedScore).Take(count).ToListAsync(ct); public async Task> GetTopByPnLAsync(int count = 5, DateTime? since = null, CancellationToken ct = default) { var q = _db.Traders.Include(t => t.CurrentScore).Include(t => t.Analytics).AsQueryable(); // If 'since' is 7 days ago, try to use PnL7d from Analytics if (since.HasValue && (DateTime.UtcNow - since.Value).TotalDays >= 6.9) { return await q.OrderByDescending(t => t.Analytics != null ? t.Analytics.PnL7d : t.TotalPnl) .Take(count).ToListAsync(ct); } return await q.OrderByDescending(t => t.TotalPnl).Take(count).ToListAsync(ct); } public async Task GetCountAsync(PlatformType? platform = null, CancellationToken ct = default) { var q = _db.Traders.AsQueryable(); if (platform.HasValue) q = q.Where(t => t.Platform == platform.Value); return await q.CountAsync(ct); } public async Task AddAsync(Trader trader, CancellationToken ct = default) { _db.Traders.Add(trader); await _db.SaveChangesAsync(ct); } public async Task UpdateAsync(Trader trader, CancellationToken ct = default) { _db.Traders.Update(trader); await _db.SaveChangesAsync(ct); } public async Task UpdateRanksAsync(IEnumerable<(int TraderId, int Rank)> ranks, CancellationToken ct = default) { // Batch update ranks using raw SQL to avoid N+1 and loading entities var sql = new StringBuilder(); sql.AppendLine("UPDATE TraderScores SET Rank = CASE TraderId"); var ids = new List(); foreach (var r in ranks) { sql.AppendLine($"WHEN {r.TraderId} THEN {r.Rank}"); ids.Add(r.TraderId); } sql.AppendLine("ELSE Rank END WHERE TraderId IN (" + string.Join(",", ids) + ");"); if (ids.Count > 0) { await _db.Database.ExecuteSqlRawAsync(sql.ToString(), ct); } } public async Task DeleteAsync(int id, CancellationToken ct = default) { var t = await _db.Traders.FindAsync(new object[] { id }, ct); if (t != null) { _db.Traders.Remove(t); await _db.SaveChangesAsync(ct); } } public async Task> GetTradersDueForTradeUpdateAsync(int cooldownHours = 12, int take = 20, CancellationToken ct = default) { var normalCutoff = DateTime.UtcNow.AddHours(-cooldownHours); var priorityCutoff = DateTime.UtcNow.AddHours(-1); // Sync priority traders more often, but not continuously return await _db.Traders .Include(t => t.WatchlistEntries) .Where(t => t.LastTradesUpdatedAt == null || (!t.IsInitialImportComplete) || ((!t.IsAutoDiscovered || t.WatchlistEntries.Any()) && t.LastTradesUpdatedAt < priorityCutoff) || (t.IsAutoDiscovered && t.LastTradesUpdatedAt < normalCutoff)) .OrderBy(t => t.IsAutoDiscovered) // Manual first (false = 0) .ThenByDescending(t => t.WatchlistEntries.Any()) // Watchlisted next (true = 1) .ThenBy(t => t.IsInitialImportComplete) // New ones next (false = 0) .ThenBy(t => t.LastTradesUpdatedAt ?? DateTime.MinValue) // Oldest first .Take(take) .ToListAsync(ct); } public async Task> GetTradersForCleanupAsync(DateTime inactiveSince, DateTime errorSince, int take = 50, CancellationToken ct = default) { return await _db.Traders .Where(t => t.IsAutoDiscovered && !t.WatchlistEntries.Any()) .Where(t => (t.LastPolledAt != null && t.LastPolledAt < inactiveSince) || (t.LastApiErrorAt != null && t.LastApiErrorAt < errorSince)) .OrderBy(t => t.LastApiErrorAt ?? DateTime.MaxValue) // Prioritize errors first .Take(take) .ToListAsync(ct); } public async Task> GetTradersForPollingAsync(int take, CancellationToken ct = default) { return await _db.Traders .OrderBy(t => t.LastPolledAt) .Take(take) .ToListAsync(ct); } public async Task> SearchAsync(string query, int take = 20, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(query)) return Array.Empty(); return await _db.Traders .Include(t => t.CurrentScore) .Include(t => t.Analytics) .Where(t => t.DisplayName.Contains(query) || t.PlatformUserId.Contains(query) || t.Id.ToString() == query) .OrderByDescending(t => t.CurrentScore != null ? t.CurrentScore.CombinedScore : 0) .ThenByDescending(t => t.TotalPnl) .Take(take) .ToListAsync(ct); } public async Task> GetPositionsAsync(int traderId, CancellationToken ct = default) { return await _db.TraderPositions .Include(p => p.MarketOutcome) .ThenInclude(o => o.Market) .Where(p => p.TraderId == traderId && (p.SharesHeld > 0 || p.RealizedPnl != 0)) .OrderByDescending(p => p.LastTradeExecutedAt ?? DateTime.MinValue) .ToListAsync(ct); } }