feat: prioritize trader analytics via LastAnalyzedAt and add force-analyze endpoint
This commit is contained in:
@@ -36,6 +36,12 @@ public static class TraderEndpoints
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
group.MapPost("/{id:int}/force-analyze", async (int id, IAnalyticsService svc, CancellationToken ct) =>
|
||||
{
|
||||
await svc.ForceAnalyzeTraderAsync(id, 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);
|
||||
|
||||
@@ -29,6 +29,9 @@ public interface IAnalyticsService
|
||||
/// <summary>Manually trigger a trade history sync for a specific trader.</summary>
|
||||
Task TriggerTradeSyncAsync(int traderId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Manually trigger a deep-dive analysis (PnL recalculation) for a specific trader.</summary>
|
||||
Task ForceAnalyzeTraderAsync(int traderId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Manually add a trader by platform and wallet address.</summary>
|
||||
Task<int> AddTraderAsync(string platform, string walletAddress, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -17,15 +17,22 @@ public class AnalyticsService : IAnalyticsService
|
||||
private readonly IMarketRepository _marketRepo;
|
||||
private readonly IDiscoveryService _discovery;
|
||||
private readonly IEnumerable<IPlatformProvider> _providers;
|
||||
private readonly IPositionPnLEngine _pnlEngine;
|
||||
private readonly ILogger<AnalyticsService> _logger;
|
||||
|
||||
public AnalyticsService(ITraderRepository traderRepo, ITradeRepository tradeRepo,
|
||||
IAlertRepository alertRepo, IWatchlistRepository watchlistRepo, IMarketRepository marketRepo,
|
||||
IDiscoveryService discovery, IEnumerable<IPlatformProvider> providers, ILogger<AnalyticsService> logger)
|
||||
IDiscoveryService discovery, IEnumerable<IPlatformProvider> providers, IPositionPnLEngine pnlEngine, ILogger<AnalyticsService> logger)
|
||||
{
|
||||
_traderRepo = traderRepo; _tradeRepo = tradeRepo;
|
||||
_alertRepo = alertRepo; _watchlistRepo = watchlistRepo; _marketRepo = marketRepo;
|
||||
_discovery = discovery; _providers = providers; _logger = logger;
|
||||
_traderRepo = traderRepo;
|
||||
_tradeRepo = tradeRepo;
|
||||
_alertRepo = alertRepo;
|
||||
_watchlistRepo = watchlistRepo;
|
||||
_marketRepo = marketRepo;
|
||||
_discovery = discovery;
|
||||
_providers = providers;
|
||||
_pnlEngine = pnlEngine;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<DashboardDto> GetDashboardAsync(CancellationToken ct = default)
|
||||
@@ -419,6 +426,12 @@ public class AnalyticsService : IAnalyticsService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ForceAnalyzeTraderAsync(int traderId, CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogInformation("Manually forcing deep analysis for trader {TraderId}", traderId);
|
||||
await _pnlEngine.RecalculateTraderPositionsAsync(traderId, ct);
|
||||
}
|
||||
|
||||
public async Task<int> AddTraderAsync(string platform, string walletAddress, CancellationToken ct = default)
|
||||
{
|
||||
if (!Enum.TryParse<PlatformType>(platform, true, out var pType))
|
||||
|
||||
@@ -224,11 +224,12 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
// Sync back to Trader record for quick sorting / UI display
|
||||
trader.TotalPnl = overallPnl;
|
||||
trader.WinRate = winRateOverall;
|
||||
trader.LastAnalyzedAt = DateTime.UtcNow;
|
||||
|
||||
// Calculate Category Performance
|
||||
var existingCatPerf = await _db.TraderCategoryPerformances
|
||||
.Where(tcp => tcp.TraderId == traderId)
|
||||
.ToDictionaryAsync(tcp => tcp.Category, ct);
|
||||
.ToDictionaryAsync(tcp => (tcp.Category, tcp.Subcategory), ct);
|
||||
|
||||
var newCatPerf = CalculateCategoryPerformances(trades, tempPositions);
|
||||
|
||||
@@ -348,11 +349,11 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Dictionary<MarketCategory, TraderCategoryPerformance> CalculateCategoryPerformances(
|
||||
private static Dictionary<(MarketCategory, string), TraderCategoryPerformance> CalculateCategoryPerformances(
|
||||
List<Trade> trades,
|
||||
Dictionary<int, TraderPosition> finalPositions)
|
||||
{
|
||||
var result = new Dictionary<MarketCategory, TraderCategoryPerformance>();
|
||||
var result = new Dictionary<(MarketCategory, string), TraderCategoryPerformance>();
|
||||
|
||||
var tradesByMarket = trades
|
||||
.Where(t => t.MarketOutcome?.Market != null)
|
||||
@@ -362,11 +363,14 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
{
|
||||
var market = marketGroup.Key;
|
||||
var category = market.Category;
|
||||
var subcat = market.Subcategory ?? "";
|
||||
|
||||
if (!result.TryGetValue(category, out var perf))
|
||||
var key = (category, subcat);
|
||||
|
||||
if (!result.TryGetValue(key, out var perf))
|
||||
{
|
||||
perf = new TraderCategoryPerformance { Category = category };
|
||||
result[category] = perf;
|
||||
perf = new TraderCategoryPerformance { Category = category, Subcategory = subcat };
|
||||
result[key] = perf;
|
||||
}
|
||||
|
||||
// Add volume
|
||||
|
||||
@@ -40,21 +40,23 @@ public class TraderAnalyticsWorker : BackgroundService
|
||||
|
||||
private async Task RunAnalyticsAsync(CancellationToken ct)
|
||||
{
|
||||
var cutoff30d = DateTime.UtcNow.AddDays(-30);
|
||||
List<int> traderIds;
|
||||
|
||||
using (var scope = _services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
// Find traders active in the last 30 days
|
||||
traderIds = await db.Trades
|
||||
.Where(t => t.ExecutedAt >= cutoff30d)
|
||||
.Select(t => t.TraderId)
|
||||
.Distinct()
|
||||
// Find traders who have never been analyzed, or whose last analysis was before their latest trade.
|
||||
// Prioritize never-analyzed traders.
|
||||
traderIds = await db.Traders
|
||||
.Where(t => t.LastAnalyzedAt == null || t.Trades.Any(tr => tr.ExecutedAt > t.LastAnalyzedAt))
|
||||
.OrderBy(t => t.LastAnalyzedAt == null ? 0 : 1)
|
||||
.ThenBy(t => t.LastAnalyzedAt)
|
||||
.Select(t => t.Id)
|
||||
.Take(500) // Limit batch size to prevent long-running loops without save
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Found {Count} active traders to analyze", traderIds.Count);
|
||||
_logger.LogInformation("Found {Count} active or unanalyzed traders to update", traderIds.Count);
|
||||
|
||||
foreach (var id in traderIds)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user