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();
|
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) =>
|
group.MapPost("/{id:int}/watchlist", async (int id, WatchlistService svc, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
await svc.AddAsync(id, "Watched via UI", null, 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>
|
/// <summary>Manually trigger a trade history sync for a specific trader.</summary>
|
||||||
Task TriggerTradeSyncAsync(int traderId, CancellationToken ct = default);
|
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>
|
/// <summary>Manually add a trader by platform and wallet address.</summary>
|
||||||
Task<int> AddTraderAsync(string platform, string walletAddress, CancellationToken ct = default);
|
Task<int> AddTraderAsync(string platform, string walletAddress, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,15 +17,22 @@ public class AnalyticsService : IAnalyticsService
|
|||||||
private readonly IMarketRepository _marketRepo;
|
private readonly IMarketRepository _marketRepo;
|
||||||
private readonly IDiscoveryService _discovery;
|
private readonly IDiscoveryService _discovery;
|
||||||
private readonly IEnumerable<IPlatformProvider> _providers;
|
private readonly IEnumerable<IPlatformProvider> _providers;
|
||||||
|
private readonly IPositionPnLEngine _pnlEngine;
|
||||||
private readonly ILogger<AnalyticsService> _logger;
|
private readonly ILogger<AnalyticsService> _logger;
|
||||||
|
|
||||||
public AnalyticsService(ITraderRepository traderRepo, ITradeRepository tradeRepo,
|
public AnalyticsService(ITraderRepository traderRepo, ITradeRepository tradeRepo,
|
||||||
IAlertRepository alertRepo, IWatchlistRepository watchlistRepo, IMarketRepository marketRepo,
|
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;
|
_traderRepo = traderRepo;
|
||||||
_alertRepo = alertRepo; _watchlistRepo = watchlistRepo; _marketRepo = marketRepo;
|
_tradeRepo = tradeRepo;
|
||||||
_discovery = discovery; _providers = providers; _logger = logger;
|
_alertRepo = alertRepo;
|
||||||
|
_watchlistRepo = watchlistRepo;
|
||||||
|
_marketRepo = marketRepo;
|
||||||
|
_discovery = discovery;
|
||||||
|
_providers = providers;
|
||||||
|
_pnlEngine = pnlEngine;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DashboardDto> GetDashboardAsync(CancellationToken ct = default)
|
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)
|
public async Task<int> AddTraderAsync(string platform, string walletAddress, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (!Enum.TryParse<PlatformType>(platform, true, out var pType))
|
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
|
// Sync back to Trader record for quick sorting / UI display
|
||||||
trader.TotalPnl = overallPnl;
|
trader.TotalPnl = overallPnl;
|
||||||
trader.WinRate = winRateOverall;
|
trader.WinRate = winRateOverall;
|
||||||
|
trader.LastAnalyzedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
// Calculate Category Performance
|
// Calculate Category Performance
|
||||||
var existingCatPerf = await _db.TraderCategoryPerformances
|
var existingCatPerf = await _db.TraderCategoryPerformances
|
||||||
.Where(tcp => tcp.TraderId == traderId)
|
.Where(tcp => tcp.TraderId == traderId)
|
||||||
.ToDictionaryAsync(tcp => tcp.Category, ct);
|
.ToDictionaryAsync(tcp => (tcp.Category, tcp.Subcategory), ct);
|
||||||
|
|
||||||
var newCatPerf = CalculateCategoryPerformances(trades, tempPositions);
|
var newCatPerf = CalculateCategoryPerformances(trades, tempPositions);
|
||||||
|
|
||||||
@@ -348,11 +349,11 @@ public class PositionPnLEngine : IPositionPnLEngine
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<MarketCategory, TraderCategoryPerformance> CalculateCategoryPerformances(
|
private static Dictionary<(MarketCategory, string), TraderCategoryPerformance> CalculateCategoryPerformances(
|
||||||
List<Trade> trades,
|
List<Trade> trades,
|
||||||
Dictionary<int, TraderPosition> finalPositions)
|
Dictionary<int, TraderPosition> finalPositions)
|
||||||
{
|
{
|
||||||
var result = new Dictionary<MarketCategory, TraderCategoryPerformance>();
|
var result = new Dictionary<(MarketCategory, string), TraderCategoryPerformance>();
|
||||||
|
|
||||||
var tradesByMarket = trades
|
var tradesByMarket = trades
|
||||||
.Where(t => t.MarketOutcome?.Market != null)
|
.Where(t => t.MarketOutcome?.Market != null)
|
||||||
@@ -362,11 +363,14 @@ public class PositionPnLEngine : IPositionPnLEngine
|
|||||||
{
|
{
|
||||||
var market = marketGroup.Key;
|
var market = marketGroup.Key;
|
||||||
var category = market.Category;
|
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 };
|
perf = new TraderCategoryPerformance { Category = category, Subcategory = subcat };
|
||||||
result[category] = perf;
|
result[key] = perf;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add volume
|
// Add volume
|
||||||
|
|||||||
@@ -40,21 +40,23 @@ public class TraderAnalyticsWorker : BackgroundService
|
|||||||
|
|
||||||
private async Task RunAnalyticsAsync(CancellationToken ct)
|
private async Task RunAnalyticsAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
var cutoff30d = DateTime.UtcNow.AddDays(-30);
|
|
||||||
List<int> traderIds;
|
List<int> traderIds;
|
||||||
|
|
||||||
using (var scope = _services.CreateScope())
|
using (var scope = _services.CreateScope())
|
||||||
{
|
{
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
// Find traders active in the last 30 days
|
// Find traders who have never been analyzed, or whose last analysis was before their latest trade.
|
||||||
traderIds = await db.Trades
|
// Prioritize never-analyzed traders.
|
||||||
.Where(t => t.ExecutedAt >= cutoff30d)
|
traderIds = await db.Traders
|
||||||
.Select(t => t.TraderId)
|
.Where(t => t.LastAnalyzedAt == null || t.Trades.Any(tr => tr.ExecutedAt > t.LastAnalyzedAt))
|
||||||
.Distinct()
|
.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);
|
.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)
|
foreach (var id in traderIds)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user