Fix worker pagination, apply Analytics fixes, add analyze backlog endpoint

This commit is contained in:
Richard
2026-07-08 20:13:52 +02:00
parent 882226c6c6
commit a4acd23a60
8 changed files with 65 additions and 18 deletions
@@ -53,5 +53,33 @@ public static class JobEndpoints
await repo.AddAsync(job, ct);
return Results.Ok(job.Id);
});
group.MapPost("/analyze-backlog", async (int? take, IJobRepository repo, Predictalytics.Infrastructure.Data.AppDbContext db, CancellationToken ct) =>
{
int batchSize = take ?? 50;
var traderIds = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync(
db.Traders
.Where(t => t.LastAnalyzedAt == null || (t.LastTradesUpdatedAt != null && t.LastTradesUpdatedAt > t.LastAnalyzedAt))
.OrderBy(t => t.LastAnalyzedAt == null ? 0 : 1)
.ThenByDescending(t => t.TotalTrades)
.Select(t => t.Id)
.Take(batchSize),
ct);
int queued = 0;
foreach (var tId in traderIds)
{
var job = new BackgroundJob
{
JobType = JobType.TraderAnalysis,
Status = JobStatus.Pending,
TraderId = tId
};
await repo.AddAsync(job, ct);
queued++;
}
return Results.Ok(new { Queued = queued });
});
}
}
@@ -12,7 +12,13 @@ public class TraderRepository : ITraderRepository
public TraderRepository(AppDbContext db) => _db = db;
public async Task<Trader?> GetByIdAsync(int id, CancellationToken ct = default)
=> await _db.Traders.Include(t => t.CurrentScore).Include(t => t.CategoryPerformances).FirstOrDefaultAsync(t => t.Id == id, ct);
{
return await _db.Traders
.Include(t => t.CurrentScore)
.Include(t => t.Analytics)
.Include(t => t.CategoryPerformances)
.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).Include(t => t.CategoryPerformances)
@@ -58,8 +58,6 @@ public partial class MainForm : Form
// Wire up button events
btn_serverstart.Click += Btn_serverstart_Click;
btn_localWebserver.Click += Btn_localWebserver_Click;
btn_syncmarkets.Click += syncMarketsaToolStripMenuItem_Click;
btn_dbUpdate.Click += btn_dbUpdate_Click;
Log.Information("MainForm initialized. Ready.");
Log.Information("Press 'Start Server' to begin polling & discovery.");
@@ -181,16 +181,11 @@ public class EmbeddedWebServer
await marketRepo.AddOrUpdateEventsAsync(events, ct);
totalSynced += events.Count;
offset += batchSize;
offset += events.Count;
if (totalSynced % 500 == 0 || events.Count < batchSize)
Log.Information("[{Platform}] Synced {Total} events so far (offset={Offset}, includeClosed={Closed})...", provider.PlatformName, totalSynced, offset, includeClosed);
if (events.Count < batchSize)
{
Log.Warning("[{Platform}] Batch was smaller than limit ({Count}/{Limit}), assuming end of list.", provider.PlatformName, events.Count, batchSize);
break;
}
}
catch (Exception ex)
{
@@ -5,6 +5,7 @@ using Predictalytics.Infrastructure.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
namespace Predictalytics.Worker.Services;
@@ -75,12 +76,15 @@ public class MarketHistoryWorker : BackgroundService
.Distinct()
.ToList();
var config = scope.ServiceProvider.GetRequiredService<Microsoft.Extensions.Configuration.IConfiguration>();
bool isEnabled = config.GetValue<bool>($"PlatformSettings:{market.Platform}:EnableCrawling", market.Platform == PlatformType.Polymarket);
foreach (var wallet in wallets)
{
var existing = await traderRepo.GetByPlatformIdAsync(
market.Platform, wallet, stoppingToken);
if (existing == null)
if (existing == null && isEnabled)
{
var trader = new Domain.Entities.Trader
{
@@ -80,7 +80,7 @@ public class MarketSyncWorker : BackgroundService
passSynced += events.Count;
cycleTotalSynced += events.Count;
_statsService.TrackMarketSync(provider.Platform, events.Count);
offset += batchSize;
offset += events.Count;
if (passSynced % 500 == 0)
_logger.LogWarning("[{Platform}] Synced {Total} markets so far (includeClosed={Closed})...", p.PlatformName, passSynced, includeClosed);
@@ -5,6 +5,7 @@ using Predictalytics.Infrastructure.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
namespace Predictalytics.Worker.Services;
@@ -36,15 +37,18 @@ public class TopHolderDiscoveryWorker : BackgroundService
var providers = scope.ServiceProvider.GetRequiredService<IEnumerable<IPlatformProvider>>();
var rateLimiter = scope.ServiceProvider.GetRequiredService<IRateLimiter>();
// Get top active markets by volume
var activeMarkets = await marketRepo.GetActiveAsync(100, stoppingToken);
_logger.LogInformation("👥 Scanning top holders across {Count} active markets", activeMarkets.Count);
var config = scope.ServiceProvider.GetRequiredService<Microsoft.Extensions.Configuration.IConfiguration>();
int totalDiscovered = 0;
foreach (var market in activeMarkets)
{
if (stoppingToken.IsCancellationRequested) break;
bool isEnabled = config.GetValue<bool>($"PlatformSettings:{market.Platform}:EnableCrawling", market.Platform == PlatformType.Polymarket);
if (!isEnabled) continue;
var provider = providers.FirstOrDefault(p => p.Platform == market.Platform && p.IsImplemented);
if (provider == null) continue;
@@ -62,12 +62,17 @@ public class TraderAnalyticsWorker : BackgroundService
else
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Find traders who have never been analyzed, or whose last analysis was before their latest trade.
// Bug 5 Fix: Add 30-minute cooldown to prevent CPU looping.
var cooldown = DateTime.UtcNow.AddMinutes(-30);
// Find traders who have never been analyzed, or whose last analysis was before their latest trade.
// Include Resolution-Trigger: Traders with open positions in resolved markets.
traderIds = await db.Traders
.Where(t => t.LastAnalyzedAt == null ||
(t.LastAnalyzedAt < cooldown && t.Trades.Any(tr => tr.ExecutedAt > t.LastAnalyzedAt)))
.Where(t =>
t.LastAnalyzedAt == null ||
(t.LastAnalyzedAt < cooldown && t.LastTradesUpdatedAt != null && t.LastTradesUpdatedAt > t.LastAnalyzedAt) ||
(t.LastAnalyzedAt < cooldown && t.Positions.Any(p => p.SharesHeld > 0 && p.MarketOutcome != null && p.MarketOutcome.Market != null && p.MarketOutcome.Market.IsResolved))
)
.OrderBy(t => t.LastAnalyzedAt == null ? 0 : 1)
.ThenBy(t => t.LastAnalyzedAt)
.Select(t => t.Id)
@@ -111,14 +116,21 @@ public class TraderAnalyticsWorker : BackgroundService
var analyticsObj = trader.Analytics ?? new Predictalytics.Domain.Entities.TraderAnalytics { TraderId = trader.Id };
// Persist advanced copyability and quality scores derived from tape replay
analyticsObj.CopytradingScore = estScores.CopyabilityScore;
analyticsObj.CopytradingScore = estScores.CombinedScore;
analyticsObj.CopytradingQualityScore = estScores.QualityScore;
analyticsObj.CopytradingCopyabilityScore = estScores.CopyabilityScore;
trader.Analytics = analyticsObj;
// Only stamp if there were actually trades to analyze
trader.LastAnalyzedAt = DateTime.UtcNow;
await traderRepo.UpdateAsync(trader, ct);
}
else if (trader.LastAnalyzedAt == null)
{
// If it's a completely new trader with no trades, we still don't stamp LastAnalyzedAt
// so it remains null until the TradeHistoryWorker pulls trades.
}
trader.LastAnalyzedAt = DateTime.UtcNow;
await traderRepo.UpdateAsync(trader, ct);
var statsService = traderScope.ServiceProvider.GetService<IPlatformStatisticsService>();
if (statsService != null)