Enhance UI, add AI integration, improve logging and database stats

This commit is contained in:
Richard
2026-07-04 21:11:31 +02:00
parent 7a44914d9d
commit d102af2965
57 changed files with 4025 additions and 229 deletions
@@ -7,6 +7,7 @@ public class AppDbContext : DbContext
{
public DbSet<Trader> Traders => Set<Trader>();
public DbSet<Trade> Trades => Set<Trade>();
public DbSet<Event> Events => Set<Event>();
public DbSet<Market> Markets => Set<Market>();
public DbSet<MarketOutcome> MarketOutcomes => Set<MarketOutcome>();
public DbSet<TraderScore> TraderScores => Set<TraderScore>();
@@ -66,19 +67,34 @@ public class AppDbContext : DbContext
.OnDelete(DeleteBehavior.SetNull);
});
// Event
mb.Entity<Event>(e =>
{
e.HasKey(ev => ev.Id);
e.HasIndex(ev => new { ev.Platform, ev.PlatformEventId }).IsUnique();
e.Property(ev => ev.Slug).HasMaxLength(512);
e.Property(ev => ev.Title).HasMaxLength(1024);
e.Property(ev => ev.Description).HasMaxLength(4096);
e.Property(ev => ev.ImageUrl).HasMaxLength(1024);
e.Property(ev => ev.Tags).HasMaxLength(1024);
e.HasMany(ev => ev.Markets).WithOne(m => m.Event).HasForeignKey(m => m.EventId)
.OnDelete(DeleteBehavior.Cascade);
});
// Market
mb.Entity<Market>(e =>
{
e.HasKey(m => m.Id);
e.HasIndex(m => new { m.Platform, m.PlatformMarketId }).IsUnique();
e.Property(m => m.PlatformMarketId).HasMaxLength(256);
e.Property(m => m.ConditionId).HasMaxLength(256);
e.Property(m => m.QuestionId).HasMaxLength(256);
e.Property(m => m.MarketSlug).HasMaxLength(512);
e.Property(m => m.EventSlug).HasMaxLength(512);
e.Property(m => m.Question).HasMaxLength(1024);
e.Property(m => m.Description).HasMaxLength(4096);
e.Property(m => m.ImageUrl).HasMaxLength(1024);
e.Property(m => m.Category).HasMaxLength(128);
e.Property(m => m.Volume).HasPrecision(18, 4);
e.Property(m => m.Volume24h).HasPrecision(18, 4);
e.Property(m => m.Liquidity).HasPrecision(18, 4);
e.HasMany(m => m.Outcomes).WithOne(o => o.Market).HasForeignKey(o => o.MarketId)
.OnDelete(DeleteBehavior.Cascade);
@@ -14,8 +14,8 @@ public class MarketRepository : IMarketRepository
public MarketRepository(AppDbContext db) => _db = db;
public async Task<Market?> GetByPlatformIdAsync(PlatformType platform, string platformMarketId, CancellationToken ct = default)
=> await _db.Markets.Include(m => m.Outcomes)
.FirstOrDefaultAsync(m => m.Platform == platform && m.PlatformMarketId == platformMarketId, ct);
=> await _db.Markets.Include(m => m.Outcomes).Include(m => m.Event)
.FirstOrDefaultAsync(m => m.Platform == platform && m.ConditionId == platformMarketId, ct);
public async Task<MarketOutcome?> GetOutcomeByTokenIdAsync(string tokenId, CancellationToken ct = default)
=> await _db.MarketOutcomes.Include(o => o.Market)
@@ -34,7 +34,7 @@ public class MarketRepository : IMarketRepository
TruncateMarketStrings(market);
var existing = await _db.Markets.Include(m => m.Outcomes)
.FirstOrDefaultAsync(m => m.Platform == market.Platform && m.PlatformMarketId == market.PlatformMarketId, ct);
.FirstOrDefaultAsync(m => m.Platform == market.Platform && m.ConditionId == market.ConditionId, ct);
if (existing != null)
{
@@ -42,6 +42,11 @@ public class MarketRepository : IMarketRepository
}
else
{
if (market.Event == null && market.EventId == 0)
{
// Fallback to avoid foreign key exceptions if event is entirely missing
market.Event = new Event { Platform = market.Platform, PlatformEventId = market.PlatformMarketId, Slug = "unknown", Title = "Unknown" };
}
_db.Markets.Add(market);
}
@@ -55,9 +60,9 @@ public class MarketRepository : IMarketRepository
public async Task AddOrUpdateRangeAsync(IEnumerable<Market> markets, CancellationToken ct = default)
{
// Deduplicate input by PlatformMarketId to avoid processing the same ID twice in one call
// Deduplicate input by ConditionId to avoid processing the same ID twice in one call
var marketList = markets
.GroupBy(m => new { m.Platform, m.PlatformMarketId })
.GroupBy(m => new { m.Platform, m.ConditionId })
.Select(g => g.First())
.ToList();
@@ -72,25 +77,28 @@ public class MarketRepository : IMarketRepository
{
var currentBatch = marketList.Skip(i).Take(subBatchSize).ToList();
var platform = currentBatch.First().Platform;
var ids = currentBatch.Select(m => m.PlatformMarketId).ToList();
var ids = currentBatch.Select(m => m.ConditionId).ToList();
// Fetch all existing markets in this batch at once
var existingMarkets = await _db.Markets.Include(m => m.Outcomes)
.Where(m => m.Platform == platform && ids.Contains(m.PlatformMarketId))
.Where(m => m.Platform == platform && ids.Contains(m.ConditionId))
.ToListAsync(ct);
var existingMap = existingMarkets.ToDictionary(m => m.PlatformMarketId);
var existingMap = existingMarkets.ToDictionary(m => m.ConditionId);
foreach (var market in currentBatch)
{
TruncateMarketStrings(market);
if (existingMap.TryGetValue(market.PlatformMarketId, out var existing))
if (existingMap.TryGetValue(market.ConditionId, out var existing))
{
UpdateMarketFields(existing, market);
}
else
{
if (market.Event == null && market.EventId == 0)
{
market.Event = new Event { Platform = market.Platform, PlatformEventId = market.PlatformMarketId, Slug = "unknown", Title = "Unknown" };
}
_db.Markets.Add(market);
}
}
@@ -104,21 +112,96 @@ public class MarketRepository : IMarketRepository
}
}
public async Task AddOrUpdateEventsAsync(IEnumerable<Event> events, CancellationToken ct = default)
{
var eventList = events.GroupBy(e => new { e.Platform, e.PlatformEventId }).Select(g => g.First()).ToList();
if (!eventList.Any()) return;
await _syncSemaphore.WaitAsync(ct);
try
{
const int subBatchSize = 100;
for (int i = 0; i < eventList.Count; i += subBatchSize)
{
var currentBatch = eventList.Skip(i).Take(subBatchSize).ToList();
var platform = currentBatch.First().Platform;
var eventIds = currentBatch.Select(e => e.PlatformEventId).ToList();
var existingEvents = await _db.Events
.Include(e => e.Markets).ThenInclude(m => m.Outcomes)
.Where(e => e.Platform == platform && eventIds.Contains(e.PlatformEventId))
.ToListAsync(ct);
var existingEventsMap = existingEvents.ToDictionary(e => e.PlatformEventId);
foreach (var ev in currentBatch)
{
if (ev.Slug != null && ev.Slug.Length > 512) ev.Slug = ev.Slug[..512];
if (ev.Title != null && ev.Title.Length > 1024) ev.Title = ev.Title[..1024];
if (existingEventsMap.TryGetValue(ev.PlatformEventId, out var existing))
{
existing.Slug = ev.Slug;
existing.Title = ev.Title;
existing.Description = ev.Description;
existing.ImageUrl = ev.ImageUrl;
existing.Tags = ev.Tags;
existing.StartDate = ev.StartDate;
existing.EndDate = ev.EndDate;
existing.IsActive = ev.IsActive;
existing.IsClosed = ev.IsClosed;
existing.LastUpdatedAt = DateTime.UtcNow;
// Upsert markets inside event
foreach (var market in ev.Markets)
{
TruncateMarketStrings(market);
var existingMarket = existing.Markets.FirstOrDefault(m => m.ConditionId == market.ConditionId);
if (existingMarket != null)
{
UpdateMarketFields(existingMarket, market);
}
else
{
market.EventId = existing.Id;
market.Event = null; // Prevent EF tracking issue
existing.Markets.Add(market);
}
}
}
else
{
foreach (var m in ev.Markets) TruncateMarketStrings(m);
_db.Events.Add(ev);
}
}
await _db.SaveChangesAsync(ct);
}
}
finally
{
_syncSemaphore.Release();
}
}
private void UpdateMarketFields(Market existing, Market updated)
{
existing.Question = updated.Question;
existing.MarketSlug = updated.MarketSlug;
existing.EventSlug = updated.EventSlug;
existing.PlatformMarketId = updated.PlatformMarketId;
existing.QuestionId = updated.QuestionId;
existing.Description = updated.Description;
existing.ImageUrl = updated.ImageUrl;
existing.Category = updated.Category;
existing.Volume = updated.Volume;
existing.Volume24h = updated.Volume24h;
existing.Liquidity = updated.Liquidity;
existing.StartDate = updated.StartDate;
existing.EndDate = updated.EndDate;
existing.IsResolved = updated.IsResolved;
existing.ResolutionOutcome = updated.ResolutionOutcome;
existing.CreatedAt = updated.CreatedAt; // Platform creation date
existing.CreatedAt = updated.CreatedAt;
existing.LastUpdatedAt = DateTime.UtcNow;
// Upsert outcomes
@@ -146,7 +229,6 @@ public class MarketRepository : IMarketRepository
market.Question = StringHelper.Truncate(market.Question, 1024) ?? "";
market.Description = StringHelper.Truncate(market.Description, 4096);
market.MarketSlug = StringHelper.Truncate(market.MarketSlug, 512) ?? "";
market.EventSlug = StringHelper.Truncate(market.EventSlug, 512) ?? "";
market.ImageUrl = StringHelper.Truncate(market.ImageUrl, 1024);
market.Category = StringHelper.Truncate(market.Category, 128) ?? "";
@@ -192,7 +274,7 @@ public class MarketRepository : IMarketRepository
return await _db.Markets.Include(m => m.Outcomes)
.Where(m => m.Question.Contains(query) ||
m.PlatformMarketId.Contains(query) ||
m.ConditionId.Contains(query) ||
m.Id.ToString() == query)
.OrderByDescending(m => m.Volume)
.Take(take)
@@ -92,13 +92,13 @@ public class TradeRepository : ITradeRepository
public async Task<HashSet<string>> GetKnownPlatformTradeIdsAsync(PlatformType platform, int traderId, IEnumerable<string> platformTradeIds, CancellationToken ct = default)
{
var idList = platformTradeIds.ToList();
if (idList.Count == 0) return new HashSet<string>();
if (idList.Count == 0) return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var ids = await _db.Trades
.Where(t => t.Platform == platform && t.TraderId == traderId && idList.Contains(t.PlatformTradeId))
.Where(t => t.Platform == platform && idList.Contains(t.PlatformTradeId))
.Select(t => t.PlatformTradeId)
.ToListAsync(ct);
return new HashSet<string>(ids);
return new HashSet<string>(ids, StringComparer.OrdinalIgnoreCase);
}
public async Task UpdateAsync(Trade trade, CancellationToken ct = default)
@@ -111,4 +111,19 @@ public class TradeRepository : ITradeRepository
_db.Trades.Update(trade);
await _db.SaveChangesAsync(ct);
}
public async Task<IReadOnlyList<Trade>> GetTradesForContextEnrichmentAsync(int limit, CancellationToken ct = default)
{
return await _db.Trades
.Include(t => t.Trader)
.Include(t => t.Trader.CurrentScore)
.Include(t => t.Trader.WatchlistEntries)
.Where(t => !t.IsContextEnriched
&& t.Platform == PlatformType.Polymarket
&& t.AssetId != "")
.Where(t => t.Trader.WatchlistEntries.Any() || (t.Trader.CurrentScore != null && t.Trader.CurrentScore.CopytradingScore > 50))
.OrderByDescending(t => t.ExecutedAt)
.Take(limit)
.ToListAsync(ct);
}
}
@@ -100,6 +100,14 @@ public class TraderRepository : ITraderRepository
.ToListAsync(ct);
}
public async Task<IReadOnlyList<Trader>> GetTradersForPollingAsync(int take, CancellationToken ct = default)
{
return await _db.Traders
.OrderBy(t => t.LastPolledAt)
.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>();