Initial commit: Predictalytics solution
Clean Architecture .NET 8 solution (Domain/Application/Infrastructure/Api/Worker/WinFormsHost) for analyzing Polymarket traders for copytrading/strategy-replication candidates. Includes EF Core InitialBaseline migration and DB secrets removed from source/config in preparation for version control.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Data.Repositories;
|
||||
|
||||
public class TraderRepository : ITraderRepository
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public TraderRepository(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<Trader?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
=> await _db.Traders.Include(t => t.CurrentScore).FirstOrDefaultAsync(t => t.Id == id, ct);
|
||||
|
||||
public async Task<Trader?> GetByPlatformIdAsync(PlatformType platform, string platformUserId, CancellationToken ct = default)
|
||||
=> await _db.Traders.Include(t => t.CurrentScore)
|
||||
.FirstOrDefaultAsync(t => t.Platform == platform && t.PlatformUserId == platformUserId, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Trader>> 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)
|
||||
.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<IReadOnlyList<Trader>> 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<IReadOnlyList<Trader>> 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<IReadOnlyList<Trader>> 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<int> 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 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<IReadOnlyList<Trader>> GetTradersDueForTradeUpdateAsync(int cooldownHours = 12, int take = 20, CancellationToken ct = default)
|
||||
{
|
||||
// Prioritize:
|
||||
// 1. Traders needing initial import (IsInitialImportComplete == false)
|
||||
// 2. Traders where LastTradesUpdatedAt < cutoff (cooldownHours)
|
||||
|
||||
var cutoff = DateTime.UtcNow.AddHours(-cooldownHours);
|
||||
|
||||
return await _db.Traders
|
||||
.Where(t => !t.IsInitialImportComplete || t.LastTradesUpdatedAt == null || t.LastTradesUpdatedAt < cutoff)
|
||||
.OrderBy(t => t.IsInitialImportComplete) // false (0) comes before true (1)
|
||||
.ThenBy(t => t.LastTradesUpdatedAt ?? DateTime.MinValue) // Oldest first
|
||||
.Take(take)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Trader>> GetTradersForCleanupAsync(DateTime inactiveSince, DateTime errorSince, int take = 50, CancellationToken ct = default)
|
||||
{
|
||||
return await _db.Traders
|
||||
.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<IReadOnlyList<Trader>> SearchAsync(string query, int take = 20, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query)) return Array.Empty<Trader>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user