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,157 @@
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Data;
|
||||
|
||||
public class AppDbContext : DbContext
|
||||
{
|
||||
public DbSet<Trader> Traders => Set<Trader>();
|
||||
public DbSet<Trade> Trades => Set<Trade>();
|
||||
public DbSet<Market> Markets => Set<Market>();
|
||||
public DbSet<MarketOutcome> MarketOutcomes => Set<MarketOutcome>();
|
||||
public DbSet<TraderScore> TraderScores => Set<TraderScore>();
|
||||
public DbSet<WatchlistEntry> WatchlistEntries => Set<WatchlistEntry>();
|
||||
public DbSet<Alert> Alerts => Set<Alert>();
|
||||
public DbSet<PlatformConfig> PlatformConfigs => Set<PlatformConfig>();
|
||||
public DbSet<TraderAnalytics> TraderAnalytics => Set<TraderAnalytics>();
|
||||
public DbSet<MarketAnalytics> MarketAnalytics => Set<MarketAnalytics>();
|
||||
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder mb)
|
||||
{
|
||||
// Trader
|
||||
mb.Entity<Trader>(e =>
|
||||
{
|
||||
e.HasKey(t => t.Id);
|
||||
e.HasIndex(t => new { t.Platform, t.PlatformUserId }).IsUnique();
|
||||
e.Property(t => t.PlatformUserId).HasMaxLength(128);
|
||||
e.Property(t => t.DisplayName).HasMaxLength(256);
|
||||
e.Property(t => t.TotalPnl).HasPrecision(18, 4);
|
||||
e.Property(t => t.WinRate).HasPrecision(8, 4);
|
||||
e.HasOne(t => t.CurrentScore).WithOne(s => s.Trader)
|
||||
.HasForeignKey<TraderScore>(s => s.TraderId).OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// Trade
|
||||
mb.Entity<Trade>(e =>
|
||||
{
|
||||
e.HasKey(t => t.Id);
|
||||
e.HasIndex(t => new { t.Platform, t.PlatformTradeId }).IsUnique();
|
||||
e.HasIndex(t => t.TraderId);
|
||||
e.HasIndex(t => t.ExecutedAt);
|
||||
e.HasIndex(t => t.AssetId);
|
||||
e.HasIndex(t => t.DbMarketId);
|
||||
// PlatformTradeId: legacy data up to 256 chars; new trades use shorter format
|
||||
e.Property(t => t.PlatformTradeId).HasMaxLength(256);
|
||||
// MarketId: Polymarket ConditionId is always 66 hex chars
|
||||
e.Property(t => t.MarketId).HasMaxLength(66);
|
||||
// AssetId: clobTokenId; Polymarket uses decimal strings up to 78 chars
|
||||
e.Property(t => t.AssetId).HasMaxLength(80);
|
||||
// Outcome: labels can be long (e.g. anime titles or sports match descriptions)
|
||||
e.Property(t => t.Outcome).HasMaxLength(128);
|
||||
// Price: 0.00–1.00 on prediction markets, 6 decimals sufficient
|
||||
e.Property(t => t.Price).HasPrecision(10, 6);
|
||||
// Size: number of shares, needs more integer digits
|
||||
e.Property(t => t.Size).HasPrecision(14, 6);
|
||||
e.Property(t => t.Amount).HasPrecision(18, 4);
|
||||
// TransactionHash: 0x + 64 hex = 66 chars
|
||||
e.Property(t => t.TransactionHash).HasMaxLength(66);
|
||||
e.HasOne(t => t.Trader).WithMany(tr => tr.Trades).HasForeignKey(t => t.TraderId);
|
||||
e.HasOne(t => t.MarketOutcome).WithMany().HasForeignKey(t => t.MarketOutcomeId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
e.HasOne(t => t.DbMarket).WithMany().HasForeignKey(t => t.DbMarketId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
// 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.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.Liquidity).HasPrecision(18, 4);
|
||||
e.HasMany(m => m.Outcomes).WithOne(o => o.Market).HasForeignKey(o => o.MarketId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// MarketOutcome
|
||||
mb.Entity<MarketOutcome>(e =>
|
||||
{
|
||||
e.HasKey(o => o.Id);
|
||||
e.HasIndex(o => o.TokenId);
|
||||
e.HasIndex(o => new { o.MarketId, o.OutcomeIndex }).IsUnique();
|
||||
e.Property(o => o.Label).HasMaxLength(256);
|
||||
e.Property(o => o.TokenId).HasMaxLength(256);
|
||||
e.Property(o => o.CurrentPrice).HasPrecision(18, 8);
|
||||
});
|
||||
|
||||
// TraderScore
|
||||
mb.Entity<TraderScore>(e =>
|
||||
{
|
||||
e.HasKey(s => s.Id);
|
||||
e.Property(s => s.ActivityScore).HasPrecision(8, 4);
|
||||
e.Property(s => s.QualityScore).HasPrecision(8, 4);
|
||||
e.Property(s => s.CombinedScore).HasPrecision(8, 4);
|
||||
e.Property(s => s.VolumeScore).HasPrecision(8, 4);
|
||||
e.Property(s => s.TimingScore).HasPrecision(8, 4);
|
||||
});
|
||||
|
||||
// WatchlistEntry
|
||||
mb.Entity<WatchlistEntry>(e =>
|
||||
{
|
||||
e.HasKey(w => w.Id);
|
||||
e.HasIndex(w => w.TraderId).IsUnique();
|
||||
e.Property(w => w.Label).HasMaxLength(256);
|
||||
e.HasOne(w => w.Trader).WithMany(t => t.WatchlistEntries).HasForeignKey(w => w.TraderId);
|
||||
});
|
||||
|
||||
// Alert
|
||||
mb.Entity<Alert>(e =>
|
||||
{
|
||||
e.HasKey(a => a.Id);
|
||||
e.HasIndex(a => a.CreatedAt);
|
||||
e.Property(a => a.Title).HasMaxLength(512);
|
||||
e.Property(a => a.Message).HasMaxLength(4096);
|
||||
e.HasOne(a => a.Trader).WithMany().HasForeignKey(a => a.TraderId).OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
// PlatformConfig
|
||||
mb.Entity<PlatformConfig>(e =>
|
||||
{
|
||||
e.HasKey(p => p.Id);
|
||||
e.Property(p => p.Name).HasMaxLength(128);
|
||||
e.Property(p => p.DisplayName).HasMaxLength(256);
|
||||
e.Property(p => p.BaseUrl).HasMaxLength(1024);
|
||||
});
|
||||
|
||||
// TraderAnalytics
|
||||
mb.Entity<TraderAnalytics>(e =>
|
||||
{
|
||||
e.HasKey(a => a.TraderId);
|
||||
e.Property(a => a.OverallPnL).HasPrecision(18, 4);
|
||||
e.Property(a => a.OverallWinRate).HasPrecision(8, 4);
|
||||
e.Property(a => a.PnL30d).HasPrecision(18, 4);
|
||||
e.Property(a => a.WinRate30d).HasPrecision(8, 4);
|
||||
e.Property(a => a.PnL7d).HasPrecision(18, 4);
|
||||
e.Property(a => a.WinRate7d).HasPrecision(8, 4);
|
||||
e.Property(a => a.PnL24h).HasPrecision(18, 4);
|
||||
e.Property(a => a.WinRate24h).HasPrecision(8, 4);
|
||||
});
|
||||
|
||||
// MarketAnalytics
|
||||
mb.Entity<MarketAnalytics>(e =>
|
||||
{
|
||||
e.HasKey(a => a.MarketId);
|
||||
e.Property(a => a.BotActivityScore).HasPrecision(8, 4);
|
||||
e.Property(a => a.AverageTradeSize).HasPrecision(18, 4);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.IO;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Data;
|
||||
|
||||
public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
|
||||
{
|
||||
public AppDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
|
||||
// Fallback for local migrations
|
||||
var connectionString = "Server=localhost;Database=Predictalytics;User=root;Password=;";
|
||||
|
||||
optionsBuilder.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 31)));
|
||||
|
||||
return new AppDbContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Data.Repositories;
|
||||
|
||||
public class AlertRepository : IAlertRepository
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public AlertRepository(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<IReadOnlyList<Alert>> GetRecentAsync(int count = 50, bool unreadOnly = false, CancellationToken ct = default)
|
||||
{
|
||||
var q = _db.Alerts.AsQueryable();
|
||||
if (unreadOnly) q = q.Where(a => !a.IsRead);
|
||||
return await q.OrderByDescending(a => a.CreatedAt).Take(count).ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task AddAsync(Alert alert, CancellationToken ct = default)
|
||||
{ _db.Alerts.Add(alert); await _db.SaveChangesAsync(ct); }
|
||||
|
||||
public async Task MarkAsReadAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
var a = await _db.Alerts.FindAsync(new object[] { id }, ct);
|
||||
if (a != null) { a.IsRead = true; await _db.SaveChangesAsync(ct); }
|
||||
}
|
||||
|
||||
public async Task<int> GetUnreadCountAsync(CancellationToken ct = default)
|
||||
=> await _db.Alerts.CountAsync(a => !a.IsRead, ct);
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using System.Threading;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Data.Repositories;
|
||||
|
||||
public class MarketRepository : IMarketRepository
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private static readonly SemaphoreSlim _syncSemaphore = new(1, 1);
|
||||
|
||||
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);
|
||||
|
||||
public async Task<MarketOutcome?> GetOutcomeByTokenIdAsync(string tokenId, CancellationToken ct = default)
|
||||
=> await _db.MarketOutcomes.Include(o => o.Market)
|
||||
.FirstOrDefaultAsync(o => o.TokenId == tokenId, ct);
|
||||
|
||||
public async Task<IReadOnlyList<MarketOutcome>> GetOutcomesByTokenIdsAsync(IEnumerable<string> tokenIds, CancellationToken ct = default)
|
||||
=> await _db.MarketOutcomes.Include(o => o.Market)
|
||||
.Where(o => tokenIds.Contains(o.TokenId))
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task AddOrUpdateAsync(Market market, CancellationToken ct = default)
|
||||
{
|
||||
TruncateMarketStrings(market);
|
||||
|
||||
var existing = await _db.Markets.Include(m => m.Outcomes)
|
||||
.FirstOrDefaultAsync(m => m.Platform == market.Platform && m.PlatformMarketId == market.PlatformMarketId, ct);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
UpdateMarketFields(existing, market);
|
||||
}
|
||||
else
|
||||
{
|
||||
_db.Markets.Add(market);
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task AddOrUpdateRangeAsync(IEnumerable<Market> markets, CancellationToken ct = default)
|
||||
{
|
||||
// Deduplicate input by PlatformMarketId to avoid processing the same ID twice in one call
|
||||
var marketList = markets
|
||||
.GroupBy(m => new { m.Platform, m.PlatformMarketId })
|
||||
.Select(g => g.First())
|
||||
.ToList();
|
||||
|
||||
if (!marketList.Any()) return;
|
||||
|
||||
await _syncSemaphore.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
// Process in sub-batches to avoid too large SQL queries
|
||||
const int subBatchSize = 500;
|
||||
for (int i = 0; i < marketList.Count; i += subBatchSize)
|
||||
{
|
||||
var currentBatch = marketList.Skip(i).Take(subBatchSize).ToList();
|
||||
var platform = currentBatch.First().Platform;
|
||||
var ids = currentBatch.Select(m => m.PlatformMarketId).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))
|
||||
.ToListAsync(ct);
|
||||
|
||||
var existingMap = existingMarkets.ToDictionary(m => m.PlatformMarketId);
|
||||
|
||||
foreach (var market in currentBatch)
|
||||
{
|
||||
TruncateMarketStrings(market);
|
||||
|
||||
if (existingMap.TryGetValue(market.PlatformMarketId, out var existing))
|
||||
{
|
||||
UpdateMarketFields(existing, market);
|
||||
}
|
||||
else
|
||||
{
|
||||
_db.Markets.Add(market);
|
||||
}
|
||||
}
|
||||
|
||||
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.Description = updated.Description;
|
||||
existing.ImageUrl = updated.ImageUrl;
|
||||
existing.Category = updated.Category;
|
||||
existing.Volume = updated.Volume;
|
||||
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.LastUpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Upsert outcomes
|
||||
foreach (var newOutcome in updated.Outcomes)
|
||||
{
|
||||
var existingOutcome = existing.Outcomes
|
||||
.FirstOrDefault(o => o.OutcomeIndex == newOutcome.OutcomeIndex);
|
||||
|
||||
if (existingOutcome != null)
|
||||
{
|
||||
existingOutcome.Label = newOutcome.Label;
|
||||
existingOutcome.TokenId = newOutcome.TokenId;
|
||||
existingOutcome.CurrentPrice = newOutcome.CurrentPrice;
|
||||
}
|
||||
else
|
||||
{
|
||||
newOutcome.MarketId = existing.Id;
|
||||
existing.Outcomes.Add(newOutcome);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TruncateMarketStrings(Market market)
|
||||
{
|
||||
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) ?? "";
|
||||
|
||||
foreach (var o in market.Outcomes)
|
||||
{
|
||||
o.Label = StringHelper.Truncate(o.Label, 256) ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Market>> GetActiveAsync(int count = 50, CancellationToken ct = default)
|
||||
=> await _db.Markets.Include(m => m.Outcomes)
|
||||
.Where(m => !m.IsResolved)
|
||||
.OrderByDescending(m => m.Volume)
|
||||
.Take(count)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<int> GetCountAsync(CancellationToken ct = default)
|
||||
=> await _db.Markets.CountAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Market>> GetMarketsDueForTradeUpdateAsync(int cooldownHours, int limit, CancellationToken ct = default)
|
||||
{
|
||||
var cutoff = DateTime.UtcNow.AddHours(-cooldownHours);
|
||||
return await _db.Markets
|
||||
.Where(m => !m.IsResolved && (m.LastTradesUpdatedAt == null || m.LastTradesUpdatedAt < cutoff))
|
||||
.OrderBy(m => m.LastTradesUpdatedAt ?? DateTime.MinValue)
|
||||
.Take(limit)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Market market, CancellationToken ct = default)
|
||||
{
|
||||
TruncateMarketStrings(market);
|
||||
_db.Markets.Update(market);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<Market?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
=> await _db.Markets.Include(m => m.Outcomes).FirstOrDefaultAsync(m => m.Id == id, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Market>> SearchAsync(string query, int take = 20, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query)) return Array.Empty<Market>();
|
||||
|
||||
return await _db.Markets.Include(m => m.Outcomes)
|
||||
.Where(m => m.Question.Contains(query) ||
|
||||
m.PlatformMarketId.Contains(query) ||
|
||||
m.Id.ToString() == query)
|
||||
.OrderByDescending(m => m.Volume)
|
||||
.Take(take)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Data.Repositories;
|
||||
|
||||
public class TradeRepository : ITradeRepository
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public TradeRepository(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<Trade?> GetByPlatformTradeIdAsync(PlatformType platform, string platformTradeId, CancellationToken ct = default)
|
||||
=> await _db.Trades.FirstOrDefaultAsync(t => t.Platform == platform && t.PlatformTradeId == platformTradeId, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Trade>> GetByTraderIdAsync(int traderId, int skip = 0, int take = 50, CancellationToken ct = default)
|
||||
=> await _db.Trades.Include(t => t.Trader).Where(t => t.TraderId == traderId)
|
||||
.OrderByDescending(t => t.ExecutedAt).Skip(skip).Take(take).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Trade>> GetByDbMarketIdAsync(int dbMarketId, int skip = 0, int take = 50, CancellationToken ct = default)
|
||||
=> await _db.Trades.Include(t => t.Trader).Where(t => t.DbMarketId == dbMarketId)
|
||||
.OrderByDescending(t => t.ExecutedAt).Skip(skip).Take(take).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Trade>> GetByMarketIdAsync(string platformMarketId, int skip = 0, int take = 50, CancellationToken ct = default)
|
||||
=> await _db.Trades.Include(t => t.Trader).Where(t => t.MarketId == platformMarketId)
|
||||
.OrderByDescending(t => t.ExecutedAt).Skip(skip).Take(take).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Trade>> GetRecentAsync(int count = 50, PlatformType? platform = null, CancellationToken ct = default)
|
||||
{
|
||||
var q = _db.Trades.Include(t => t.Trader).AsQueryable();
|
||||
if (platform.HasValue) q = q.Where(t => t.Platform == platform.Value);
|
||||
return await q.OrderByDescending(t => t.ExecutedAt).Take(count).ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Trade>> GetLargestAsync(int count = 5, DateTime? since = null, CancellationToken ct = default)
|
||||
{
|
||||
var q = _db.Trades.Include(t => t.Trader).AsQueryable();
|
||||
if (since.HasValue) q = q.Where(t => t.ExecutedAt >= since.Value);
|
||||
return await q.OrderByDescending(t => t.Amount).Take(count).ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<int> GetCountAsync(int? traderId = null, CancellationToken ct = default)
|
||||
{
|
||||
var q = _db.Trades.AsQueryable();
|
||||
if (traderId.HasValue) q = q.Where(t => t.TraderId == traderId.Value);
|
||||
return await q.CountAsync(ct);
|
||||
}
|
||||
|
||||
public async Task AddRangeAsync(IEnumerable<Trade> trades, CancellationToken ct = default)
|
||||
{
|
||||
foreach (var t in trades)
|
||||
{
|
||||
t.Outcome = StringHelper.Truncate(t.Outcome, 128) ?? "";
|
||||
t.PlatformTradeId = StringHelper.Truncate(t.PlatformTradeId, 256) ?? "";
|
||||
t.MarketId = StringHelper.Truncate(t.MarketId, 66) ?? "";
|
||||
t.AssetId = StringHelper.Truncate(t.AssetId, 80) ?? "";
|
||||
if (t.TransactionHash != null)
|
||||
t.TransactionHash = StringHelper.Truncate(t.TransactionHash, 66);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_db.Trades.AddRange(trades);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
foreach (var t in trades)
|
||||
{
|
||||
try { _db.Entry(t).State = EntityState.Detached; } catch { }
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<decimal> GetTotalVolumeAsync(DateTime? since = null, CancellationToken ct = default)
|
||||
{
|
||||
var q = _db.Trades.AsQueryable();
|
||||
if (since.HasValue) q = q.Where(t => t.ExecutedAt >= since.Value);
|
||||
return await q.SumAsync(t => t.Amount, ct);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Trade>> GetOrphanedTradesAsync(int limit, CancellationToken ct = default)
|
||||
{
|
||||
return await _db.Trades
|
||||
.Where(t => t.MarketOutcomeId == null && !string.IsNullOrEmpty(t.AssetId))
|
||||
.OrderByDescending(t => t.ExecutedAt)
|
||||
.Take(limit)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<HashSet<string>> GetKnownPlatformTradeIdsAsync(PlatformType platform, int traderId, CancellationToken ct = default)
|
||||
{
|
||||
var ids = await _db.Trades
|
||||
.Where(t => t.Platform == platform && t.TraderId == traderId)
|
||||
.Select(t => t.PlatformTradeId)
|
||||
.ToListAsync(ct);
|
||||
return new HashSet<string>(ids);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Trade trade, CancellationToken ct = default)
|
||||
{
|
||||
trade.Outcome = StringHelper.Truncate(trade.Outcome, 128) ?? "";
|
||||
trade.PlatformTradeId = StringHelper.Truncate(trade.PlatformTradeId, 256) ?? "";
|
||||
trade.MarketId = StringHelper.Truncate(trade.MarketId, 66) ?? "";
|
||||
trade.AssetId = StringHelper.Truncate(trade.AssetId, 80) ?? "";
|
||||
|
||||
_db.Trades.Update(trade);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Data.Repositories;
|
||||
|
||||
public class WatchlistRepository : IWatchlistRepository
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public WatchlistRepository(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<IReadOnlyList<WatchlistEntry>> GetAllAsync(CancellationToken ct = default)
|
||||
=> await _db.WatchlistEntries.Include(w => w.Trader).ToListAsync(ct);
|
||||
|
||||
public async Task<WatchlistEntry?> GetByTraderIdAsync(int traderId, CancellationToken ct = default)
|
||||
=> await _db.WatchlistEntries.FirstOrDefaultAsync(w => w.TraderId == traderId, ct);
|
||||
|
||||
public async Task AddAsync(WatchlistEntry entry, CancellationToken ct = default)
|
||||
{ _db.WatchlistEntries.Add(entry); await _db.SaveChangesAsync(ct); }
|
||||
|
||||
public async Task RemoveAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
var e = await _db.WatchlistEntries.FindAsync(new object[] { id }, ct);
|
||||
if (e != null) { _db.WatchlistEntries.Remove(e); await _db.SaveChangesAsync(ct); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Predictalytics.Infrastructure.Data;
|
||||
|
||||
public static class StringHelper
|
||||
{
|
||||
public static string? Truncate(string? value, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return value;
|
||||
return value.Length <= maxLength ? value : value[..maxLength];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using Predictalytics.Application.Interfaces;
|
||||
using Predictalytics.Application.Services;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Predictalytics.Infrastructure.Data;
|
||||
using Predictalytics.Infrastructure.Data.Repositories;
|
||||
using Predictalytics.Infrastructure.Providers.Azuro;
|
||||
using Predictalytics.Infrastructure.Providers.Limitless;
|
||||
using Predictalytics.Infrastructure.Providers.Polymarket;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Predictalytics.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddPredictalytics(this IServiceCollection services, IConfiguration configuration, string? connectionStringOverride = null, bool dbDebug = false)
|
||||
{
|
||||
if (dbDebug) Serilog.Log.Warning(">>> INFRASTRUCTURE: AddPredictalytics STARTING");
|
||||
|
||||
// MySQL / EF Core
|
||||
var connectionString = connectionStringOverride;
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
connectionString = configuration.GetConnectionString("DefaultConnection")
|
||||
?? "Server=localhost;Database=Predictalytics_dev;User=root;Password=;";
|
||||
}
|
||||
|
||||
// Use MySqlConnectionStringBuilder to ensure valid format and parse components
|
||||
var csBuilder = new MySqlConnector.MySqlConnectionStringBuilder(connectionString);
|
||||
|
||||
// Final safety check
|
||||
if (string.IsNullOrWhiteSpace(csBuilder.Database))
|
||||
{
|
||||
Serilog.Log.Error("❌ INVALID CONNECTION STRING: Database name is empty! (Input length: {Length})", connectionString.Length);
|
||||
throw new InvalidOperationException("The connection string is missing a valid 'Database' parameter.");
|
||||
}
|
||||
|
||||
var maskedCs = csBuilder.ConnectionString.Replace(csBuilder.Password, "****");
|
||||
if (dbDebug)
|
||||
{
|
||||
Serilog.Log.Warning("🗄️ Initializing database connection: {ConnectionString}", maskedCs);
|
||||
Serilog.Log.Warning("🗄️ Target Server: {Server}, Database: {Database}", csBuilder.Server, csBuilder.Database);
|
||||
}
|
||||
|
||||
// Explicitly register the connection string so we can use it elsewhere if needed
|
||||
services.AddSingleton(csBuilder.ConnectionString);
|
||||
|
||||
services.AddDbContext<AppDbContext>(options =>
|
||||
{
|
||||
// Log exactly what is being used at the moment of configuration
|
||||
if (dbDebug) Serilog.Log.Warning("🛠️ EF: Configuring AppDbContext. Target DB: '{Database}'", csBuilder.Database);
|
||||
|
||||
options.UseMySql(csBuilder.ConnectionString, new MySqlServerVersion(new Version(8, 0, 31)),
|
||||
mysql => mysql.EnableRetryOnFailure(3, TimeSpan.FromSeconds(10), null));
|
||||
});
|
||||
|
||||
// Repositories
|
||||
services.AddScoped<ITraderRepository, TraderRepository>();
|
||||
services.AddScoped<ITradeRepository, TradeRepository>();
|
||||
services.AddScoped<IMarketRepository, MarketRepository>();
|
||||
services.AddScoped<IWatchlistRepository, WatchlistRepository>();
|
||||
services.AddScoped<IAlertRepository, AlertRepository>();
|
||||
|
||||
// Application Services
|
||||
services.AddScoped<IScoringService, ScoringService>();
|
||||
services.AddScoped<IDiscoveryService, DiscoveryService>();
|
||||
services.AddScoped<IAlertService, AlertService>();
|
||||
services.AddScoped<IAnalyticsService, AnalyticsService>();
|
||||
services.AddScoped<WatchlistService>();
|
||||
services.AddSingleton<IRateLimiter, RateLimiterService>();
|
||||
services.AddSingleton<IPlatformStatisticsService, PlatformStatisticsService>();
|
||||
|
||||
// Platform Providers
|
||||
services.AddHttpClient();
|
||||
services.AddHttpClient("LimitlessApi", c =>
|
||||
{
|
||||
c.BaseAddress = new Uri("https://api.limitless.exchange/");
|
||||
c.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
c.Timeout = TimeSpan.FromSeconds(60);
|
||||
});
|
||||
|
||||
services.AddSingleton<PolymarketApiClient>();
|
||||
services.AddSingleton<LimitlessApiClient>();
|
||||
services.AddSingleton<IPlatformProvider, PolymarketProvider>();
|
||||
services.AddSingleton<IPlatformProvider, LimitlessProvider>();
|
||||
services.AddSingleton<IPlatformProvider, AzuroProvider>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies pending EF Core migrations and seeds default platform rows.
|
||||
/// Requires the target database to already be "stamped" with the InitialBaseline
|
||||
/// migration in __EFMigrationsHistory (see UMSETZUNGSPLAN.md, section B1) if it was
|
||||
/// previously created via the old EnsureCreated + manual ALTER approach.
|
||||
/// </summary>
|
||||
public static async Task EnsureDatabaseAsync(IServiceProvider services, bool dbDebug = false)
|
||||
{
|
||||
using var scope = services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
try
|
||||
{
|
||||
if (dbDebug)
|
||||
{
|
||||
var maskedConnStr = System.Text.RegularExpressions.Regex.Replace(
|
||||
db.Database.GetDbConnection().ConnectionString ?? "NULL", "Password=[^;]+", "Password=****");
|
||||
Serilog.Log.Warning("🔍 DEBUG: Applying EF Core migrations. ConnectionString: {CS}", maskedConnStr);
|
||||
}
|
||||
|
||||
await db.Database.MigrateAsync();
|
||||
|
||||
if (dbDebug) Serilog.Log.Warning("✅ DEBUG: Migrations applied successfully.");
|
||||
|
||||
// Seed default platform rows (idempotent)
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open) await conn.OpenAsync();
|
||||
|
||||
using var seedPlatform = conn.CreateCommand();
|
||||
seedPlatform.CommandText = @"
|
||||
INSERT IGNORE INTO `PlatformConfigs` (`Id`, `Name`, `DisplayName`, `IsActive`, `CreatedAt`, `UpdatedAt`) VALUES
|
||||
(0, 'Unknown', 'Unknown Platform', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP()),
|
||||
(1, 'Polymarket', 'Polymarket', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP()),
|
||||
(2, 'Azuro', 'Azuro', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP()),
|
||||
(3, 'Limitless', 'Limitless', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP());";
|
||||
await seedPlatform.ExecuteNonQueryAsync();
|
||||
|
||||
await conn.CloseAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Warning("⚠️ Could not connect to database or apply migrations: {Message}. Background workers will retry connection automatically.", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Serilog.Context;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Helper to push platform context into Serilog LogContext for platform-filtered file sinks.
|
||||
/// Usage: using (PlatformLogContext.Push("Polymarket")) { ... }
|
||||
/// </summary>
|
||||
public static class PlatformLogContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Pushes the platform name to the Serilog LogContext.
|
||||
/// Dispose the returned IDisposable to remove it.
|
||||
/// </summary>
|
||||
public static IDisposable Push(string platformName)
|
||||
{
|
||||
return LogContext.PushProperty("Platform", platformName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Custom Serilog sink that delegates log writes to a provided action.
|
||||
/// The action is responsible for marshaling to the correct thread (e.g. UI thread).
|
||||
/// </summary>
|
||||
public class RichTextBoxSink : ILogEventSink
|
||||
{
|
||||
private readonly Action<string, LogEventLevel> _writeAction;
|
||||
|
||||
public RichTextBoxSink(Action<string, LogEventLevel> writeAction)
|
||||
{
|
||||
_writeAction = writeAction;
|
||||
}
|
||||
|
||||
public void Emit(LogEvent logEvent)
|
||||
{
|
||||
var message = $"[{logEvent.Timestamp:HH:mm:ss}] [{logEvent.Level.ToString()[..3].ToUpper()}] {logEvent.RenderMessage()}";
|
||||
if (logEvent.Exception != null)
|
||||
message += $"\n ⚠ {logEvent.Exception.Message}";
|
||||
|
||||
_writeAction(message + "\n", logEvent.Level);
|
||||
}
|
||||
}
|
||||
+641
@@ -0,0 +1,641 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Predictalytics.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260701102311_InitialBaseline")]
|
||||
partial class InitialBaseline
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("IsRead")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("varchar(4096)");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Severity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("varchar(512)");
|
||||
|
||||
b.Property<int?>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("TraderId");
|
||||
|
||||
b.ToTable("Alerts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("DbCreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("varchar(4096)");
|
||||
|
||||
b.Property<DateTime?>("EndDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("EventSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("varchar(512)");
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime?>("LastTradesUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime?>("LastUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("Liquidity")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("MarketSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("varchar(512)");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("PlatformMarketId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<string>("Question")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<string>("ResolutionOutcome")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime?>("StartDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("Volume")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Platform", "PlatformMarketId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Markets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
|
||||
{
|
||||
b.Property<int>("MarketId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("AverageTradeSize")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("BotActivityScore")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<DateTime>("LastCalculatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("UniqueTradersCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("MarketId");
|
||||
|
||||
b.ToTable("MarketAnalytics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("CurrentPrice")
|
||||
.HasPrecision(18, 8)
|
||||
.HasColumnType("decimal(18,8)");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<int>("MarketId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("OutcomeIndex")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TokenId");
|
||||
|
||||
b.HasIndex("MarketId", "OutcomeIndex")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MarketOutcomes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.PlatformConfig", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("BaseUrl")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<string>("SettingsJson")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PlatformConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("AssetId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("varchar(80)");
|
||||
|
||||
b.Property<int?>("DbMarketId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("ExecutedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("MarketId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(66)
|
||||
.HasColumnType("varchar(66)");
|
||||
|
||||
b.Property<int?>("MarketOutcomeId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Outcome")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("PlatformTradeId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasPrecision(10, 6)
|
||||
.HasColumnType("decimal(10,6)");
|
||||
|
||||
b.Property<int>("Side")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(14, 6)
|
||||
.HasColumnType("decimal(14,6)");
|
||||
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TransactionHash")
|
||||
.HasMaxLength(66)
|
||||
.HasColumnType("varchar(66)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("DbMarketId");
|
||||
|
||||
b.HasIndex("ExecutedAt");
|
||||
|
||||
b.HasIndex("MarketOutcomeId");
|
||||
|
||||
b.HasIndex("TraderId");
|
||||
|
||||
b.HasIndex("Platform", "PlatformTradeId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Trades");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<bool>("IsAutoDiscovered")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsInitialImportComplete")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsSuspectedBot")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime?>("LastApiErrorAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime?>("LastPolledAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime?>("LastTradesUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int?>("ManualPriorityOverride")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("PlatformUserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<int>("Strategy")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Tier")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("TotalPnl")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<int>("TotalTrades")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("WinRate")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Platform", "PlatformUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Traders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
|
||||
{
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("LastCalculatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("OverallPnL")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("OverallWinRate")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<decimal>("PnL24h")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("PnL30d")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("PnL7d")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("WinRate24h")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<decimal>("WinRate30d")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<decimal>("WinRate7d")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.HasKey("TraderId");
|
||||
|
||||
b.ToTable("TraderAnalytics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("ActivityScore")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<DateTime>("CalculatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("CombinedScore")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<decimal>("QualityScore")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<int>("Rank")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("TimingScore")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("VolumeScore")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TraderId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TraderScores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("AddedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("AlertsEnabled")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TraderId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("WatchlistEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||
.WithMany()
|
||||
.HasForeignKey("TraderId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
|
||||
.WithOne("Analytics")
|
||||
.HasForeignKey("Predictalytics.Domain.Entities.MarketAnalytics", "MarketId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Market");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
|
||||
.WithMany("Outcomes")
|
||||
.HasForeignKey("MarketId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Market");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Market", "DbMarket")
|
||||
.WithMany()
|
||||
.HasForeignKey("DbMarketId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
|
||||
.WithMany()
|
||||
.HasForeignKey("MarketOutcomeId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||
.WithMany("Trades")
|
||||
.HasForeignKey("TraderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("DbMarket");
|
||||
|
||||
b.Navigation("MarketOutcome");
|
||||
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||
.WithOne("Analytics")
|
||||
.HasForeignKey("Predictalytics.Domain.Entities.TraderAnalytics", "TraderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||
.WithOne("CurrentScore")
|
||||
.HasForeignKey("Predictalytics.Domain.Entities.TraderScore", "TraderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||
.WithMany("WatchlistEntries")
|
||||
.HasForeignKey("TraderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||
{
|
||||
b.Navigation("Analytics");
|
||||
|
||||
b.Navigation("Outcomes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
|
||||
{
|
||||
b.Navigation("Analytics");
|
||||
|
||||
b.Navigation("CurrentScore");
|
||||
|
||||
b.Navigation("Trades");
|
||||
|
||||
b.Navigation("WatchlistEntries");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialBaseline : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Markets",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Platform = table.Column<int>(type: "int", nullable: false),
|
||||
PlatformMarketId = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
MarketSlug = table.Column<string>(type: "varchar(512)", maxLength: 512, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
EventSlug = table.Column<string>(type: "varchar(512)", maxLength: 512, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Description = table.Column<string>(type: "varchar(4096)", maxLength: 4096, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ImageUrl = table.Column<string>(type: "varchar(1024)", maxLength: 1024, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Question = table.Column<string>(type: "varchar(1024)", maxLength: 1024, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Category = table.Column<string>(type: "varchar(128)", maxLength: 128, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Volume = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
Liquidity = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
StartDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
EndDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
IsResolved = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
ResolutionOutcome = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
DbCreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
LastUpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
LastTradesUpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Markets", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PlatformConfigs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false),
|
||||
Name = table.Column<string>(type: "varchar(128)", maxLength: 128, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
DisplayName = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
BaseUrl = table.Column<string>(type: "varchar(1024)", maxLength: 1024, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
SettingsJson = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PlatformConfigs", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Traders",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Platform = table.Column<int>(type: "int", nullable: false),
|
||||
PlatformUserId = table.Column<string>(type: "varchar(128)", maxLength: 128, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
DisplayName = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Notes = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
IsAutoDiscovered = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
Tier = table.Column<int>(type: "int", nullable: false),
|
||||
Strategy = table.Column<int>(type: "int", nullable: false),
|
||||
IsSuspectedBot = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
ManualPriorityOverride = table.Column<int>(type: "int", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
LastPolledAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
LastTradesUpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
IsInitialImportComplete = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
LastApiErrorAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
TotalPnl = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
WinRate = table.Column<decimal>(type: "decimal(8,4)", precision: 8, scale: 4, nullable: false),
|
||||
TotalTrades = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Traders", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MarketAnalytics",
|
||||
columns: table => new
|
||||
{
|
||||
MarketId = table.Column<int>(type: "int", nullable: false),
|
||||
LastCalculatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
BotActivityScore = table.Column<decimal>(type: "decimal(8,4)", precision: 8, scale: 4, nullable: false),
|
||||
UniqueTradersCount = table.Column<int>(type: "int", nullable: false),
|
||||
AverageTradeSize = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MarketAnalytics", x => x.MarketId);
|
||||
table.ForeignKey(
|
||||
name: "FK_MarketAnalytics_Markets_MarketId",
|
||||
column: x => x.MarketId,
|
||||
principalTable: "Markets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MarketOutcomes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
MarketId = table.Column<int>(type: "int", nullable: false),
|
||||
Label = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
OutcomeIndex = table.Column<int>(type: "int", nullable: false),
|
||||
TokenId = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CurrentPrice = table.Column<decimal>(type: "decimal(18,8)", precision: 18, scale: 8, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MarketOutcomes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MarketOutcomes_Markets_MarketId",
|
||||
column: x => x.MarketId,
|
||||
principalTable: "Markets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Alerts",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Type = table.Column<int>(type: "int", nullable: false),
|
||||
Platform = table.Column<int>(type: "int", nullable: false),
|
||||
TraderId = table.Column<int>(type: "int", nullable: true),
|
||||
Title = table.Column<string>(type: "varchar(512)", maxLength: 512, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Message = table.Column<string>(type: "varchar(4096)", maxLength: 4096, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Severity = table.Column<int>(type: "int", nullable: false),
|
||||
IsRead = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Alerts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Alerts_Traders_TraderId",
|
||||
column: x => x.TraderId,
|
||||
principalTable: "Traders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TraderAnalytics",
|
||||
columns: table => new
|
||||
{
|
||||
TraderId = table.Column<int>(type: "int", nullable: false),
|
||||
LastCalculatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
OverallPnL = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
OverallWinRate = table.Column<decimal>(type: "decimal(8,4)", precision: 8, scale: 4, nullable: false),
|
||||
PnL30d = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
WinRate30d = table.Column<decimal>(type: "decimal(8,4)", precision: 8, scale: 4, nullable: false),
|
||||
PnL7d = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
WinRate7d = table.Column<decimal>(type: "decimal(8,4)", precision: 8, scale: 4, nullable: false),
|
||||
PnL24h = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
WinRate24h = table.Column<decimal>(type: "decimal(8,4)", precision: 8, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TraderAnalytics", x => x.TraderId);
|
||||
table.ForeignKey(
|
||||
name: "FK_TraderAnalytics_Traders_TraderId",
|
||||
column: x => x.TraderId,
|
||||
principalTable: "Traders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TraderScores",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
TraderId = table.Column<int>(type: "int", nullable: false),
|
||||
ActivityScore = table.Column<decimal>(type: "decimal(8,4)", precision: 8, scale: 4, nullable: false),
|
||||
QualityScore = table.Column<decimal>(type: "decimal(8,4)", precision: 8, scale: 4, nullable: false),
|
||||
CombinedScore = table.Column<decimal>(type: "decimal(8,4)", precision: 8, scale: 4, nullable: false),
|
||||
VolumeScore = table.Column<decimal>(type: "decimal(8,4)", precision: 8, scale: 4, nullable: false),
|
||||
TimingScore = table.Column<decimal>(type: "decimal(8,4)", precision: 8, scale: 4, nullable: false),
|
||||
Rank = table.Column<int>(type: "int", nullable: false),
|
||||
CalculatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TraderScores", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_TraderScores_Traders_TraderId",
|
||||
column: x => x.TraderId,
|
||||
principalTable: "Traders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WatchlistEntries",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
TraderId = table.Column<int>(type: "int", nullable: false),
|
||||
Label = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Notes = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
AlertsEnabled = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
AddedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WatchlistEntries", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_WatchlistEntries_Traders_TraderId",
|
||||
column: x => x.TraderId,
|
||||
principalTable: "Traders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Trades",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
TraderId = table.Column<int>(type: "int", nullable: false),
|
||||
Platform = table.Column<int>(type: "int", nullable: false),
|
||||
PlatformTradeId = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
MarketId = table.Column<string>(type: "varchar(66)", maxLength: 66, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
DbMarketId = table.Column<int>(type: "int", nullable: true),
|
||||
AssetId = table.Column<string>(type: "varchar(80)", maxLength: 80, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
MarketOutcomeId = table.Column<int>(type: "int", nullable: true),
|
||||
Outcome = table.Column<string>(type: "varchar(128)", maxLength: 128, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Side = table.Column<int>(type: "int", nullable: false),
|
||||
Price = table.Column<decimal>(type: "decimal(10,6)", precision: 10, scale: 6, nullable: false),
|
||||
Size = table.Column<decimal>(type: "decimal(14,6)", precision: 14, scale: 6, nullable: false),
|
||||
Amount = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
ExecutedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
TransactionHash = table.Column<string>(type: "varchar(66)", maxLength: 66, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Trades", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Trades_MarketOutcomes_MarketOutcomeId",
|
||||
column: x => x.MarketOutcomeId,
|
||||
principalTable: "MarketOutcomes",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_Trades_Markets_DbMarketId",
|
||||
column: x => x.DbMarketId,
|
||||
principalTable: "Markets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_Trades_Traders_TraderId",
|
||||
column: x => x.TraderId,
|
||||
principalTable: "Traders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Alerts_CreatedAt",
|
||||
table: "Alerts",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Alerts_TraderId",
|
||||
table: "Alerts",
|
||||
column: "TraderId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MarketOutcomes_MarketId_OutcomeIndex",
|
||||
table: "MarketOutcomes",
|
||||
columns: new[] { "MarketId", "OutcomeIndex" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MarketOutcomes_TokenId",
|
||||
table: "MarketOutcomes",
|
||||
column: "TokenId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Markets_Platform_PlatformMarketId",
|
||||
table: "Markets",
|
||||
columns: new[] { "Platform", "PlatformMarketId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Traders_Platform_PlatformUserId",
|
||||
table: "Traders",
|
||||
columns: new[] { "Platform", "PlatformUserId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TraderScores_TraderId",
|
||||
table: "TraderScores",
|
||||
column: "TraderId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Trades_AssetId",
|
||||
table: "Trades",
|
||||
column: "AssetId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Trades_DbMarketId",
|
||||
table: "Trades",
|
||||
column: "DbMarketId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Trades_ExecutedAt",
|
||||
table: "Trades",
|
||||
column: "ExecutedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Trades_MarketOutcomeId",
|
||||
table: "Trades",
|
||||
column: "MarketOutcomeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Trades_Platform_PlatformTradeId",
|
||||
table: "Trades",
|
||||
columns: new[] { "Platform", "PlatformTradeId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Trades_TraderId",
|
||||
table: "Trades",
|
||||
column: "TraderId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WatchlistEntries_TraderId",
|
||||
table: "WatchlistEntries",
|
||||
column: "TraderId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Alerts");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MarketAnalytics");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PlatformConfigs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TraderAnalytics");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TraderScores");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Trades");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "WatchlistEntries");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MarketOutcomes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Traders");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Markets");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Predictalytics.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("IsRead")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("varchar(4096)");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Severity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("varchar(512)");
|
||||
|
||||
b.Property<int?>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("TraderId");
|
||||
|
||||
b.ToTable("Alerts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("DbCreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("varchar(4096)");
|
||||
|
||||
b.Property<DateTime?>("EndDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("EventSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("varchar(512)");
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime?>("LastTradesUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime?>("LastUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("Liquidity")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("MarketSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("varchar(512)");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("PlatformMarketId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<string>("Question")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<string>("ResolutionOutcome")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime?>("StartDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("Volume")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Platform", "PlatformMarketId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Markets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
|
||||
{
|
||||
b.Property<int>("MarketId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("AverageTradeSize")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("BotActivityScore")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<DateTime>("LastCalculatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("UniqueTradersCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("MarketId");
|
||||
|
||||
b.ToTable("MarketAnalytics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("CurrentPrice")
|
||||
.HasPrecision(18, 8)
|
||||
.HasColumnType("decimal(18,8)");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<int>("MarketId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("OutcomeIndex")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TokenId");
|
||||
|
||||
b.HasIndex("MarketId", "OutcomeIndex")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MarketOutcomes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.PlatformConfig", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("BaseUrl")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<string>("SettingsJson")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PlatformConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("AssetId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("varchar(80)");
|
||||
|
||||
b.Property<int?>("DbMarketId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("ExecutedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("MarketId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(66)
|
||||
.HasColumnType("varchar(66)");
|
||||
|
||||
b.Property<int?>("MarketOutcomeId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Outcome")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("PlatformTradeId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasPrecision(10, 6)
|
||||
.HasColumnType("decimal(10,6)");
|
||||
|
||||
b.Property<int>("Side")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(14, 6)
|
||||
.HasColumnType("decimal(14,6)");
|
||||
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TransactionHash")
|
||||
.HasMaxLength(66)
|
||||
.HasColumnType("varchar(66)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("DbMarketId");
|
||||
|
||||
b.HasIndex("ExecutedAt");
|
||||
|
||||
b.HasIndex("MarketOutcomeId");
|
||||
|
||||
b.HasIndex("TraderId");
|
||||
|
||||
b.HasIndex("Platform", "PlatformTradeId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Trades");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<bool>("IsAutoDiscovered")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsInitialImportComplete")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsSuspectedBot")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime?>("LastApiErrorAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime?>("LastPolledAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime?>("LastTradesUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int?>("ManualPriorityOverride")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("PlatformUserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<int>("Strategy")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Tier")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("TotalPnl")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<int>("TotalTrades")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("WinRate")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Platform", "PlatformUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Traders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
|
||||
{
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("LastCalculatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("OverallPnL")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("OverallWinRate")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<decimal>("PnL24h")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("PnL30d")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("PnL7d")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("WinRate24h")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<decimal>("WinRate30d")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<decimal>("WinRate7d")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.HasKey("TraderId");
|
||||
|
||||
b.ToTable("TraderAnalytics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("ActivityScore")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<DateTime>("CalculatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("CombinedScore")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<decimal>("QualityScore")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<int>("Rank")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("TimingScore")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("VolumeScore")
|
||||
.HasPrecision(8, 4)
|
||||
.HasColumnType("decimal(8,4)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TraderId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TraderScores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("AddedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("AlertsEnabled")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TraderId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("WatchlistEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||
.WithMany()
|
||||
.HasForeignKey("TraderId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
|
||||
.WithOne("Analytics")
|
||||
.HasForeignKey("Predictalytics.Domain.Entities.MarketAnalytics", "MarketId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Market");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
|
||||
.WithMany("Outcomes")
|
||||
.HasForeignKey("MarketId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Market");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Market", "DbMarket")
|
||||
.WithMany()
|
||||
.HasForeignKey("DbMarketId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
|
||||
.WithMany()
|
||||
.HasForeignKey("MarketOutcomeId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||
.WithMany("Trades")
|
||||
.HasForeignKey("TraderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("DbMarket");
|
||||
|
||||
b.Navigation("MarketOutcome");
|
||||
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||
.WithOne("Analytics")
|
||||
.HasForeignKey("Predictalytics.Domain.Entities.TraderAnalytics", "TraderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||
.WithOne("CurrentScore")
|
||||
.HasForeignKey("Predictalytics.Domain.Entities.TraderScore", "TraderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||
.WithMany("WatchlistEntries")
|
||||
.HasForeignKey("TraderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||
{
|
||||
b.Navigation("Analytics");
|
||||
|
||||
b.Navigation("Outcomes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
|
||||
{
|
||||
b.Navigation("Analytics");
|
||||
|
||||
b.Navigation("CurrentScore");
|
||||
|
||||
b.Navigation("Trades");
|
||||
|
||||
b.Navigation("WatchlistEntries");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>Predictalytics.Infrastructure</RootNamespace>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Predictalytics.Domain\Predictalytics.Domain.csproj" />
|
||||
<ProjectReference Include="..\Predictalytics.Application\Predictalytics.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,42 @@
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Predictalytics.Infrastructure.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Providers.Azuro;
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder provider for Azuro prediction market.
|
||||
/// All log entries include Platform=Azuro for log-file routing.
|
||||
/// </summary>
|
||||
public class AzuroProvider : IPlatformProvider
|
||||
{
|
||||
private readonly ILogger<AzuroProvider> _logger;
|
||||
public AzuroProvider(ILogger<AzuroProvider> logger) => _logger = logger;
|
||||
|
||||
public PlatformType Platform => PlatformType.Azuro;
|
||||
public string PlatformName => "Azuro";
|
||||
public bool IsImplemented => false;
|
||||
|
||||
public Task<IReadOnlyList<Trade>> GetTraderTradesAsync(string platformUserId, int limit = 50, CancellationToken ct = default)
|
||||
{ using var _ = PlatformLogContext.Push(PlatformName); _logger.LogWarning("Provider not yet implemented"); return Task.FromResult<IReadOnlyList<Trade>>(Array.Empty<Trade>()); }
|
||||
|
||||
public Task<IReadOnlyList<TraderPositionInfo>> GetTraderPositionsAsync(string platformUserId, CancellationToken ct = default)
|
||||
{ using var _ = PlatformLogContext.Push(PlatformName); _logger.LogWarning("Provider not yet implemented"); return Task.FromResult<IReadOnlyList<TraderPositionInfo>>(Array.Empty<TraderPositionInfo>()); }
|
||||
|
||||
public Task<IReadOnlyList<DiscoveredTrader>> DiscoverTradersAsync(int limit = 20, CancellationToken ct = default)
|
||||
{ using var _ = PlatformLogContext.Push(PlatformName); _logger.LogWarning("Provider not yet implemented"); return Task.FromResult<IReadOnlyList<DiscoveredTrader>>(Array.Empty<DiscoveredTrader>()); }
|
||||
|
||||
public Task<Market?> GetMarketAsync(string platformMarketId, CancellationToken ct = default)
|
||||
{ using var _ = PlatformLogContext.Push(PlatformName); _logger.LogWarning("Provider not yet implemented"); return Task.FromResult<Market?>(null); }
|
||||
|
||||
public Task<IReadOnlyList<Market>> GetMarketsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default)
|
||||
{ using var _ = PlatformLogContext.Push(PlatformName); return Task.FromResult<IReadOnlyList<Market>>(Array.Empty<Market>()); }
|
||||
|
||||
public Task<IReadOnlyList<DiscoveredTrader>> GetTopHoldersAsync(string platformMarketId, int limit = 20, CancellationToken ct = default)
|
||||
{ using var _ = PlatformLogContext.Push(PlatformName); return Task.FromResult<IReadOnlyList<DiscoveredTrader>>(Array.Empty<DiscoveredTrader>()); }
|
||||
|
||||
public Task<IReadOnlyList<Trade>> GetMarketTradesAsync(string platformMarketId, int limit = 50, CancellationToken ct = default)
|
||||
{ using var _ = PlatformLogContext.Push(PlatformName); return Task.FromResult<IReadOnlyList<Trade>>(Array.Empty<Trade>()); }
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Providers.Limitless;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for Limitless API.
|
||||
/// Base URL: https://api.limitless.exchange
|
||||
/// </summary>
|
||||
public class LimitlessApiClient
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly ILogger<LimitlessApiClient> _logger;
|
||||
|
||||
public LimitlessApiClient(IHttpClientFactory httpFactory, ILogger<LimitlessApiClient> logger)
|
||||
{
|
||||
_client = httpFactory.CreateClient("LimitlessApi");
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<List<LimitlessMarketResponse>> GetActiveMarketsAsync(int limit = 100, int offset = 0, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"markets/active?limit={Math.Min(limit, 25)}"; // Offset is not supported by this endpoint, limit max 25
|
||||
try
|
||||
{
|
||||
var response = await _client.GetAsync(url, ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync(ct);
|
||||
_logger.LogError("Limitless API 400/Error for {Url}: {Error}", url, error);
|
||||
return [];
|
||||
}
|
||||
var result = await response.Content.ReadFromJsonAsync<LimitlessActiveMarketsResponse>(cancellationToken: ct);
|
||||
return result?.Data ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch active markets from Limitless via {Url}", url);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LimitlessMarketResponse?> GetMarketAsync(string addressOrSlug, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"markets/{addressOrSlug}";
|
||||
try
|
||||
{
|
||||
var response = await _client.GetAsync(url, ct);
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
return await response.Content.ReadFromJsonAsync<LimitlessMarketResponse>(cancellationToken: ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch Limitless market: {Url}", url);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LimitlessPortfolioResponse?> GetPositionsAsync(string walletAddress, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"portfolio/{walletAddress}/positions";
|
||||
try
|
||||
{
|
||||
var response = await _client.GetAsync(url, ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync(ct);
|
||||
_logger.LogError("Limitless API Error for {Url}: {Error}", url, error);
|
||||
return null;
|
||||
}
|
||||
return await response.Content.ReadFromJsonAsync<LimitlessPortfolioResponse>(cancellationToken: ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch Limitless positions for {Url}", url);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<LimitlessEventResponse>> GetMarketEventsAsync(string slug, int limit = 50, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"markets/{slug}/events?limit={limit}";
|
||||
try
|
||||
{
|
||||
var response = await _client.GetAsync(url, ct);
|
||||
if (!response.IsSuccessStatusCode) return [];
|
||||
var result = await response.Content.ReadFromJsonAsync<LimitlessEventsResponse>(cancellationToken: ct);
|
||||
return result?.Events ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch events for Limitless market {Url}", url);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Providers.Limitless;
|
||||
|
||||
public class LimitlessActiveMarketsResponse
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public List<LimitlessMarketResponse>? Data { get; set; }
|
||||
|
||||
[JsonPropertyName("totalMarketsCount")]
|
||||
public int TotalMarketsCount { get; set; }
|
||||
}
|
||||
|
||||
public class LimitlessEventsResponse
|
||||
{
|
||||
[JsonPropertyName("events")]
|
||||
public List<LimitlessEventResponse>? Events { get; set; }
|
||||
}
|
||||
|
||||
public class LimitlessPortfolioResponse
|
||||
{
|
||||
[JsonPropertyName("clob")]
|
||||
public List<LimitlessClobItem>? Clob { get; set; }
|
||||
|
||||
[JsonPropertyName("amm")]
|
||||
public List<LimitlessClobItem>? Amm { get; set; }
|
||||
|
||||
[JsonPropertyName("group")]
|
||||
public List<LimitlessClobItem>? Group { get; set; }
|
||||
}
|
||||
|
||||
public class LimitlessClobItem
|
||||
{
|
||||
[JsonPropertyName("market")]
|
||||
public LimitlessMarketResponse? Market { get; set; }
|
||||
|
||||
[JsonPropertyName("positions")]
|
||||
public LimitlessPositionsContainer? Positions { get; set; }
|
||||
}
|
||||
|
||||
public class LimitlessPositionsContainer
|
||||
{
|
||||
[JsonPropertyName("yes")]
|
||||
public LimitlessPositionDetails? Yes { get; set; }
|
||||
|
||||
[JsonPropertyName("no")]
|
||||
public LimitlessPositionDetails? No { get; set; }
|
||||
}
|
||||
|
||||
public class LimitlessPositionDetails
|
||||
{
|
||||
[JsonPropertyName("size")]
|
||||
public string? Size { get; set; } // API uses strings for numbers here
|
||||
|
||||
[JsonPropertyName("fillPrice")]
|
||||
public string? FillPrice { get; set; }
|
||||
|
||||
[JsonPropertyName("marketValue")]
|
||||
public string? MarketValue { get; set; }
|
||||
|
||||
[JsonPropertyName("realisedPnl")]
|
||||
public string? RealisedPnl { get; set; }
|
||||
|
||||
[JsonPropertyName("unrealizedPnl")]
|
||||
public string? UnrealizedPnl { get; set; }
|
||||
|
||||
[JsonPropertyName("latestTrade")]
|
||||
public LimitlessTradeInfo? LatestTrade { get; set; }
|
||||
}
|
||||
|
||||
public class LimitlessMarketResponse
|
||||
{
|
||||
[JsonPropertyName("address")]
|
||||
public string? Address { get; set; }
|
||||
|
||||
[JsonPropertyName("slug")]
|
||||
public string? Slug { get; set; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[JsonPropertyName("categories")]
|
||||
public List<string>? Categories { get; set; }
|
||||
|
||||
[JsonPropertyName("imageUrl")]
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("expirationDate")]
|
||||
public string? ExpirationDate { get; set; }
|
||||
|
||||
[JsonPropertyName("expirationTimestamp")]
|
||||
public long? ExpirationTimestamp { get; set; }
|
||||
|
||||
[JsonPropertyName("active")]
|
||||
public bool Active { get; set; }
|
||||
|
||||
[JsonPropertyName("closed")]
|
||||
public bool Closed { get; set; }
|
||||
|
||||
[JsonPropertyName("volumeFormatted")]
|
||||
public string? VolumeFormatted { get; set; }
|
||||
|
||||
[JsonPropertyName("liquidity")]
|
||||
public double? Liquidity { get; set; }
|
||||
|
||||
[JsonPropertyName("prices")]
|
||||
public List<double>? Prices { get; set; }
|
||||
|
||||
[JsonPropertyName("tokens")]
|
||||
public LimitlessTokens? Tokens { get; set; }
|
||||
}
|
||||
|
||||
public class LimitlessTokens
|
||||
{
|
||||
[JsonPropertyName("yes")]
|
||||
public string? Yes { get; set; }
|
||||
|
||||
[JsonPropertyName("no")]
|
||||
public string? No { get; set; }
|
||||
}
|
||||
|
||||
public class LimitlessTradeInfo
|
||||
{
|
||||
[JsonPropertyName("timestamp")]
|
||||
public long Timestamp { get; set; }
|
||||
|
||||
[JsonPropertyName("price")]
|
||||
public string? Price { get; set; } // Might be string in this context
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public string? Size { get; set; }
|
||||
|
||||
[JsonPropertyName("side")]
|
||||
public object? Side { get; set; }
|
||||
|
||||
[JsonPropertyName("transactionHash")]
|
||||
public string? TransactionHash { get; set; }
|
||||
}
|
||||
|
||||
public class LimitlessEventResponse
|
||||
{
|
||||
[JsonPropertyName("txHash")]
|
||||
public string? TxHash { get; set; }
|
||||
|
||||
[JsonPropertyName("side")]
|
||||
public object? Side { get; set; }
|
||||
|
||||
[JsonPropertyName("price")]
|
||||
public double? Price { get; set; }
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public double? Size { get; set; }
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public string? CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("profile")]
|
||||
public LimitlessProfile? Profile { get; set; }
|
||||
|
||||
[JsonPropertyName("asset")]
|
||||
public string? Asset { get; set; }
|
||||
}
|
||||
|
||||
public class LimitlessProfile
|
||||
{
|
||||
[JsonPropertyName("account")]
|
||||
public string? Account { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Predictalytics.Infrastructure.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Providers.Limitless;
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder provider for Limitless prediction market.
|
||||
/// All log entries include Platform=Limitless for log-file routing.
|
||||
/// </summary>
|
||||
public class LimitlessProvider : IPlatformProvider
|
||||
{
|
||||
private readonly LimitlessApiClient _api;
|
||||
private readonly ILogger<LimitlessProvider> _logger;
|
||||
|
||||
public PlatformType Platform => PlatformType.Limitless;
|
||||
public string PlatformName => "Limitless";
|
||||
public bool IsImplemented => true;
|
||||
|
||||
public LimitlessProvider(LimitlessApiClient api, ILogger<LimitlessProvider> logger)
|
||||
{
|
||||
_api = api;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Trade>> GetTraderTradesAsync(string platformUserId, int limit = 50, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
_logger.LogDebug("Fetching latest trade activity via positions for {Wallet}", platformUserId);
|
||||
|
||||
var portfolio = await _api.GetPositionsAsync(platformUserId, ct);
|
||||
if (portfolio == null) return [];
|
||||
|
||||
var items = (portfolio.Clob ?? []).Concat(portfolio.Amm ?? []).Concat(portfolio.Group ?? []);
|
||||
var trades = new List<Trade>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Positions == null || item.Market == null) continue;
|
||||
|
||||
// Check Yes and No positions for latest trades
|
||||
var posList = new[] {
|
||||
(Details: item.Positions.Yes, Outcome: "Yes", TokenId: item.Market.Tokens?.Yes),
|
||||
(Details: item.Positions.No, Outcome: "No", TokenId: item.Market.Tokens?.No)
|
||||
};
|
||||
|
||||
foreach (var pos in posList)
|
||||
{
|
||||
if (pos.Details?.LatestTrade == null) continue;
|
||||
|
||||
var lt = pos.Details.LatestTrade;
|
||||
decimal.TryParse(lt.Price, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var price);
|
||||
decimal.TryParse(lt.Size, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var size);
|
||||
|
||||
trades.Add(new Trade
|
||||
{
|
||||
Platform = PlatformType.Limitless,
|
||||
// Compact format: {txHash}_{assetId} — wallet via TransientWallet
|
||||
PlatformTradeId = lt.TransactionHash != null
|
||||
? $"{lt.TransactionHash}_{pos.TokenId}"
|
||||
: $"{lt.Timestamp}_{pos.TokenId}",
|
||||
MarketId = item.Market.Address ?? item.Market.Slug ?? "",
|
||||
AssetId = pos.TokenId ?? "",
|
||||
Outcome = pos.Outcome,
|
||||
Side = ParseSide(lt.Side),
|
||||
Price = price,
|
||||
Size = size,
|
||||
Amount = price * size,
|
||||
ExecutedAt = (lt.Timestamp > 0 && lt.Timestamp < 253402300799)
|
||||
? DateTimeOffset.FromUnixTimeSeconds(lt.Timestamp).UtcDateTime
|
||||
: DateTime.UtcNow,
|
||||
TransactionHash = lt.TransactionHash,
|
||||
TransientWallet = platformUserId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return trades.OrderByDescending(t => t.ExecutedAt).Take(limit).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<TraderPositionInfo>> GetTraderPositionsAsync(string platformUserId, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
var portfolio = await _api.GetPositionsAsync(platformUserId, ct);
|
||||
if (portfolio == null) return [];
|
||||
|
||||
var items = (portfolio.Clob ?? []).Concat(portfolio.Amm ?? []).Concat(portfolio.Group ?? []);
|
||||
var result = new List<TraderPositionInfo>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Positions == null || item.Market == null) continue;
|
||||
|
||||
var posList = new[] {
|
||||
(Details: item.Positions.Yes, Outcome: "Yes", TokenId: item.Market.Tokens?.Yes),
|
||||
(Details: item.Positions.No, Outcome: "No", TokenId: item.Market.Tokens?.No)
|
||||
};
|
||||
|
||||
foreach (var pos in posList)
|
||||
{
|
||||
if (pos.Details == null) continue;
|
||||
|
||||
decimal.TryParse(pos.Details.Size, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var size);
|
||||
if (size == 0) continue;
|
||||
|
||||
decimal.TryParse(pos.Details.FillPrice, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var avgPrice);
|
||||
decimal.TryParse(pos.Details.MarketValue, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var val);
|
||||
decimal.TryParse(pos.Details.UnrealizedPnl, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var pnl);
|
||||
|
||||
result.Add(new TraderPositionInfo(
|
||||
platformUserId,
|
||||
item.Market.Address ?? item.Market.Slug ?? "",
|
||||
item.Market.Title ?? "",
|
||||
pos.Outcome,
|
||||
size,
|
||||
avgPrice,
|
||||
val,
|
||||
0 // PercentPnl not directly available as decimal in this view
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DiscoveredTrader>> DiscoverTradersAsync(int limit = 20, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
_logger.LogInformation("Trader discovery for Limitless via active markets...");
|
||||
|
||||
var markets = await _api.GetActiveMarketsAsync(10, 0, ct);
|
||||
var traders = new List<DiscoveredTrader>();
|
||||
|
||||
foreach (var m in markets.Take(5))
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
var events = await _api.GetMarketEventsAsync(m.Slug ?? m.Address ?? "", 20, ct);
|
||||
|
||||
foreach (var e in events.Where(ev => ev.Profile?.Account != null))
|
||||
{
|
||||
traders.Add(new DiscoveredTrader(
|
||||
e.Profile!.Account!,
|
||||
e.Profile.Account![..10] + "...",
|
||||
(decimal)(e.Price * e.Size ?? 0),
|
||||
1, 0
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return traders.GroupBy(t => t.PlatformUserId)
|
||||
.Select(g => g.First())
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<Market?> GetMarketAsync(string platformMarketId, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
var raw = await _api.GetMarketAsync(platformMarketId, ct);
|
||||
if (raw == null) return null;
|
||||
|
||||
return MapLimitlessMarket(raw);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Market>> GetMarketsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
int.TryParse(cursor, out var offset);
|
||||
|
||||
// Since Limitless /markets/active doesn't support offset, we only return the first page.
|
||||
// Returning data for offset > 0 would cause an infinite loop in MarketSyncWorker.
|
||||
if (offset > 0) return [];
|
||||
|
||||
var raw = await _api.GetActiveMarketsAsync(limit, offset, ct);
|
||||
return raw.Select(MapLimitlessMarket).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DiscoveredTrader>> GetTopHoldersAsync(string platformMarketId, int limit = 20, CancellationToken ct = default)
|
||||
{
|
||||
var events = await _api.GetMarketEventsAsync(platformMarketId, limit * 2, ct);
|
||||
return events
|
||||
.Where(e => e.Profile?.Account != null)
|
||||
.GroupBy(e => e.Profile!.Account)
|
||||
.Select(g => new DiscoveredTrader(g.Key!, g.Key![..10] + "...", 0, g.Count(), 0))
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Trade>> GetMarketTradesAsync(string platformMarketId, int limit = 50, CancellationToken ct = default)
|
||||
{
|
||||
var events = await _api.GetMarketEventsAsync(platformMarketId, limit, ct);
|
||||
return events
|
||||
.Where(e => e.Profile?.Account != null)
|
||||
.Select(e =>
|
||||
{
|
||||
var wallet = e.Profile!.Account!;
|
||||
var asset = e.Asset ?? "";
|
||||
var side = e.Side ?? "";
|
||||
// Compact format: {txHash}_{assetId} — wallet via TransientWallet
|
||||
var tradeId = e.TxHash != null
|
||||
? $"{e.TxHash}_{asset}"
|
||||
: $"{e.CreatedAt}_{asset}_{side}";
|
||||
|
||||
return new Trade
|
||||
{
|
||||
Platform = PlatformType.Limitless,
|
||||
PlatformTradeId = tradeId,
|
||||
MarketId = platformMarketId,
|
||||
AssetId = asset,
|
||||
Outcome = "",
|
||||
Side = ParseSide(side),
|
||||
Price = (decimal)(e.Price ?? 0),
|
||||
Size = (decimal)(e.Size ?? 0),
|
||||
Amount = (decimal)(e.Price * e.Size ?? 0),
|
||||
ExecutedAt = DateTime.TryParse(e.CreatedAt, out var dt) ? dt : DateTime.UtcNow,
|
||||
TransactionHash = e.TxHash,
|
||||
TransientWallet = wallet,
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private Market MapLimitlessMarket(LimitlessMarketResponse raw)
|
||||
{
|
||||
var market = new Market
|
||||
{
|
||||
Platform = PlatformType.Limitless,
|
||||
PlatformMarketId = raw.Address ?? raw.Slug ?? "",
|
||||
MarketSlug = raw.Slug ?? "",
|
||||
EventSlug = "", // Limitless doesn't seem to have a clear Event/Market split in this model
|
||||
Question = raw.Title ?? "",
|
||||
Description = raw.Description ?? "",
|
||||
Category = raw.Categories?.FirstOrDefault() ?? "",
|
||||
ImageUrl = raw.ImageUrl ?? "",
|
||||
Volume = decimal.TryParse(raw.VolumeFormatted?.Replace(" USDC", ""), out var vol) ? vol : 0,
|
||||
Liquidity = (decimal)(raw.Liquidity ?? 0),
|
||||
EndDate = (raw.ExpirationTimestamp.HasValue && raw.ExpirationTimestamp.Value > 0 && raw.ExpirationTimestamp.Value < 253402300799)
|
||||
? DateTimeOffset.FromUnixTimeSeconds(raw.ExpirationTimestamp.Value).UtcDateTime
|
||||
: (DateTime.TryParse(raw.ExpirationDate, out var ed) ? ed : null),
|
||||
IsResolved = raw.Closed,
|
||||
LastUpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Map Outcomes from tokens object and prices array
|
||||
if (raw.Tokens != null)
|
||||
{
|
||||
// Yes Outcome
|
||||
market.Outcomes.Add(new MarketOutcome
|
||||
{
|
||||
Label = "Yes",
|
||||
OutcomeIndex = 0,
|
||||
TokenId = raw.Tokens.Yes ?? "",
|
||||
CurrentPrice = (decimal)(raw.Prices != null && raw.Prices.Count > 0 ? raw.Prices[0] : 0)
|
||||
});
|
||||
|
||||
// No Outcome
|
||||
market.Outcomes.Add(new MarketOutcome
|
||||
{
|
||||
Label = "No",
|
||||
OutcomeIndex = 1,
|
||||
TokenId = raw.Tokens.No ?? "",
|
||||
CurrentPrice = (decimal)(raw.Prices != null && raw.Prices.Count > 1 ? raw.Prices[1] : 0)
|
||||
});
|
||||
}
|
||||
|
||||
return market;
|
||||
}
|
||||
|
||||
private TradeSide ParseSide(object? sideObj)
|
||||
{
|
||||
var sideStr = sideObj?.ToString()?.ToUpper();
|
||||
if (sideStr == "0" || sideStr == "BUY") return TradeSide.Buy;
|
||||
return TradeSide.Sell;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Predictalytics.Application.Interfaces;
|
||||
using Predictalytics.Domain.Enums;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Providers.Polymarket;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for Polymarket Data API.
|
||||
/// All endpoints use https://data-api.polymarket.com
|
||||
/// </summary>
|
||||
public class PolymarketApiClient
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly HttpClient _gammaClient;
|
||||
private readonly IRateLimiter _rateLimiter;
|
||||
private readonly ILogger<PolymarketApiClient> _logger;
|
||||
|
||||
private const string DataApiBase = "https://data-api.polymarket.com";
|
||||
private const string GammaApiBase = "https://gamma-api.polymarket.com";
|
||||
|
||||
public PolymarketApiClient(IHttpClientFactory httpFactory, IRateLimiter rateLimiter, ILogger<PolymarketApiClient> logger)
|
||||
{
|
||||
_client = httpFactory.CreateClient("PolymarketData");
|
||||
_client.BaseAddress = new Uri(DataApiBase);
|
||||
_client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
|
||||
_gammaClient = httpFactory.CreateClient("PolymarketGamma");
|
||||
_gammaClient.BaseAddress = new Uri(GammaApiBase);
|
||||
_gammaClient.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
|
||||
_rateLimiter = rateLimiter;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<List<PolymarketTradeResponse>> GetTradesAsync(string walletAddress, int limit = 1000, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/activity?user={walletAddress}&limit={limit}";
|
||||
return await ExecuteWithRetryAsync<List<PolymarketTradeResponse>>(_client, url, ct) ?? [];
|
||||
}
|
||||
|
||||
public async Task<List<PolymarketTradeResponse>> GetMarketTradesAsync(string conditionId, int limit = 1000, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/trades?condition_id={conditionId}&limit={limit}";
|
||||
return await ExecuteWithRetryAsync<List<PolymarketTradeResponse>>(_client, url, ct) ?? [];
|
||||
}
|
||||
|
||||
public async Task<List<PolymarketPositionResponse>> GetPositionsAsync(string walletAddress, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/positions?user={walletAddress}&sizeThreshold=0.1&sortBy=CURRENT&sortOrder=DESC";
|
||||
return await ExecuteWithRetryAsync<List<PolymarketPositionResponse>>(_client, url, ct) ?? [];
|
||||
}
|
||||
|
||||
public async Task<GammaMarketResponse?> GetMarketAsync(string conditionId, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/markets?condition_id={conditionId}";
|
||||
var results = await ExecuteWithRetryAsync<List<GammaMarketResponse>>(_gammaClient, url, ct);
|
||||
return results?.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetch a batch of markets from the Gamma API with pagination.
|
||||
/// Supports offset-based pagination via the offset parameter.
|
||||
/// </summary>
|
||||
public async Task<List<GammaMarketResponse>> GetMarketsAsync(int limit = 1000, int offset = 0, bool includeClosed = false, CancellationToken ct = default)
|
||||
{
|
||||
var activeOnly = !includeClosed;
|
||||
var url = $"/markets?limit={limit}&offset={offset}&active={activeOnly.ToString().ToLower()}&closed={includeClosed.ToString().ToLower()}";
|
||||
_logger.LogDebug("Fetching markets: {Url}", url);
|
||||
var result = await ExecuteWithRetryAsync<List<GammaMarketResponse>>(_gammaClient, url, ct);
|
||||
_logger.LogInformation("Fetched {Count} markets (offset={Offset}, closed={Closed})", result?.Count ?? 0, offset, includeClosed);
|
||||
return result ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetch top holders for a specific market (conditionId) from the Data API.
|
||||
/// Returns holders grouped by token (outcome).
|
||||
/// </summary>
|
||||
public async Task<List<HoldersResponse>> GetHoldersAsync(string conditionId, int limit = 20, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/holders?market={conditionId}&limit={limit}";
|
||||
_logger.LogDebug("Fetching holders: {Url}", url);
|
||||
var result = await ExecuteWithRetryAsync<List<HoldersResponse>>(_client, url, ct);
|
||||
_logger.LogInformation("Fetched holders for {Market}: {Count} token groups",
|
||||
conditionId.Length > 12 ? conditionId[..12] + "..." : conditionId, result?.Count ?? 0);
|
||||
return result ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get leaderboard from the official Polymarket Data API v1.
|
||||
/// Endpoint: GET https://data-api.polymarket.com/v1/leaderboard
|
||||
/// </summary>
|
||||
public async Task<List<LeaderboardEntry>> GetLeaderboardAsync(
|
||||
int limit = 50,
|
||||
string timePeriod = "ALL",
|
||||
string orderBy = "PNL",
|
||||
string category = "OVERALL",
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/v1/leaderboard?limit={Math.Min(limit, 50)}&time_period={timePeriod}&order_by={orderBy}&category={category}";
|
||||
_logger.LogDebug("Fetching leaderboard: {Url}", url);
|
||||
var result = await ExecuteWithRetryAsync<List<LeaderboardEntry>>(_client, url, ct);
|
||||
_logger.LogInformation("Leaderboard returned {Count} entries", result?.Count ?? 0);
|
||||
return result ?? [];
|
||||
}
|
||||
|
||||
private async Task<T?> ExecuteWithRetryAsync<T>(HttpClient client, string url, CancellationToken ct, int attempt = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await client.GetAsync(url, ct);
|
||||
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
TimeSpan? retryAfter = null;
|
||||
if (response.Headers.RetryAfter != null)
|
||||
{
|
||||
retryAfter = response.Headers.RetryAfter.Delta ??
|
||||
(response.Headers.RetryAfter.Date.HasValue
|
||||
? response.Headers.RetryAfter.Date.Value - DateTimeOffset.UtcNow
|
||||
: null);
|
||||
}
|
||||
|
||||
var waitTime = retryAfter ?? TimeSpan.FromSeconds(30);
|
||||
if (waitTime.TotalSeconds < 5)
|
||||
{
|
||||
_logger.LogWarning("Got 429 but Retry-After was {RawWait}s. Enforcing 30s minimum.", waitTime.TotalSeconds);
|
||||
waitTime = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
_logger.LogWarning("⚠️ Rate limit exceeded (429) for Polymarket. Pausing for {WaitTime}s...", (int)waitTime.TotalSeconds);
|
||||
|
||||
_rateLimiter.ReportRateLimitExceeded(PlatformType.Polymarket, waitTime);
|
||||
|
||||
if (attempt < 3)
|
||||
{
|
||||
await _rateLimiter.WaitAsync(PlatformType.Polymarket, ct);
|
||||
_logger.LogWarning("Retrying {Url} (attempt {NextAttempt})...", url, attempt + 1);
|
||||
return await ExecuteWithRetryAsync<T>(client, url, ct, attempt + 1);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
if ((int)response.StatusCode == 422)
|
||||
{
|
||||
_logger.LogInformation("End of data reached (422) for {Url}. Stopping pagination.", url);
|
||||
return default;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<T>(cancellationToken: ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is HttpRequestException hex && hex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_logger.LogCritical("Unhandled 429 in PolymarketApiClient for {Url}. This should have been caught by the status code check.", url);
|
||||
}
|
||||
_logger.LogError(ex, "Failed to fetch from {Url} (attempt {Attempt})", url, attempt);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Providers.Polymarket;
|
||||
|
||||
/// <summary>
|
||||
/// Converter that handles JSON values that may be either a number or a string.
|
||||
/// Polymarket API is inconsistent — some fields are numbers in one endpoint and strings in another.
|
||||
/// </summary>
|
||||
public class FlexibleDoubleConverter : JsonConverter<double>
|
||||
{
|
||||
public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.Number => reader.GetDouble(),
|
||||
JsonTokenType.String => double.TryParse(reader.GetString(), System.Globalization.NumberStyles.Any,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var v) ? v : 0,
|
||||
JsonTokenType.Null => 0,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options)
|
||||
=> writer.WriteNumberValue(value);
|
||||
}
|
||||
|
||||
public class FlexibleLongConverter : JsonConverter<long>
|
||||
{
|
||||
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.Number => reader.GetInt64(),
|
||||
JsonTokenType.String => long.TryParse(reader.GetString(), out var v) ? v : 0,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
|
||||
=> writer.WriteNumberValue(value);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Polymarket Data API response models
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
public class PolymarketTradeResponse
|
||||
{
|
||||
[JsonPropertyName("conditionId")] public string ConditionId { get; set; } = "";
|
||||
[JsonPropertyName("asset")] public string Asset { get; set; } = "";
|
||||
[JsonPropertyName("side")] public string Side { get; set; } = "";
|
||||
[JsonPropertyName("action")] public string Action { get; set; } = "";
|
||||
[JsonPropertyName("type")] public string Type { get; set; } = "";
|
||||
[JsonPropertyName("user")] public string? User { get; set; }
|
||||
[JsonPropertyName("proxyWallet")] public string? ProxyWallet { get; set; }
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Size { get; set; }
|
||||
|
||||
[JsonPropertyName("price")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Price { get; set; }
|
||||
|
||||
[JsonPropertyName("outcome")] public string Outcome { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
[JsonConverter(typeof(FlexibleLongConverter))]
|
||||
public long Timestamp { get; set; }
|
||||
|
||||
[JsonPropertyName("transactionHash")] public string? TransactionHash { get; set; }
|
||||
}
|
||||
|
||||
public class PolymarketPositionResponse
|
||||
{
|
||||
[JsonPropertyName("asset_id")] public string AssetId { get; set; } = "";
|
||||
[JsonPropertyName("market")] public string Market { get; set; } = "";
|
||||
[JsonPropertyName("outcome")] public string Outcome { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Size { get; set; }
|
||||
|
||||
[JsonPropertyName("avgPrice")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double AvgPrice { get; set; }
|
||||
|
||||
[JsonPropertyName("currentValue")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double CurrentValue { get; set; }
|
||||
|
||||
[JsonPropertyName("cashPnl")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double CashPnl { get; set; }
|
||||
|
||||
[JsonPropertyName("percentPnl")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double PercentPnl { get; set; }
|
||||
|
||||
[JsonPropertyName("question")] public string Question { get; set; } = "";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Gamma API — Market metadata (full market response)
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
public class GammaMarketResponse
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = "";
|
||||
[JsonPropertyName("conditionId")] public string ConditionId { get; set; } = "";
|
||||
[JsonPropertyName("question")] public string Question { get; set; } = "";
|
||||
[JsonPropertyName("slug")] public string Slug { get; set; } = "";
|
||||
[JsonPropertyName("description")] public string? Description { get; set; }
|
||||
[JsonPropertyName("image")] public string? Image { get; set; }
|
||||
[JsonPropertyName("category")] public string Category { get; set; } = "";
|
||||
[JsonPropertyName("groupItemTitle")] public string? GroupItemTitle { get; set; }
|
||||
[JsonPropertyName("events")] public List<GammaEventResponse>? Events { get; set; }
|
||||
|
||||
[JsonPropertyName("volumeNum")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Volume { get; set; }
|
||||
|
||||
[JsonPropertyName("liquidityNum")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Liquidity { get; set; }
|
||||
|
||||
[JsonPropertyName("endDateIso")] public string? EndDate { get; set; }
|
||||
[JsonPropertyName("startDate")] public string? StartDate { get; set; }
|
||||
[JsonPropertyName("createdAt")] public string? CreatedAt { get; set; }
|
||||
[JsonPropertyName("closed")] public bool Closed { get; set; }
|
||||
[JsonPropertyName("active")] public bool Active { get; set; }
|
||||
[JsonPropertyName("resolved")] public bool Resolved { get; set; }
|
||||
[JsonPropertyName("resolution_outcome")] public string? ResolutionOutcome { get; set; }
|
||||
|
||||
/// <summary>JSON string of outcomes, e.g. "[\"Yes\", \"No\"]"</summary>
|
||||
[JsonPropertyName("outcomes")] public string? Outcomes { get; set; }
|
||||
|
||||
/// <summary>JSON string of outcome prices, e.g. "[\"0.55\", \"0.45\"]"</summary>
|
||||
[JsonPropertyName("outcomePrices")] public string? OutcomePrices { get; set; }
|
||||
|
||||
/// <summary>JSON string of CLOB token IDs, e.g. "[\"12345...\", \"67890...\"]"</summary>
|
||||
[JsonPropertyName("clobTokenIds")] public string? ClobTokenIds { get; set; }
|
||||
}
|
||||
|
||||
public class GammaEventResponse
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = "";
|
||||
[JsonPropertyName("slug")] public string Slug { get; set; } = "";
|
||||
[JsonPropertyName("title")] public string Title { get; set; } = "";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Data API — Holders response
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
public class HoldersResponse
|
||||
{
|
||||
[JsonPropertyName("token")] public string Token { get; set; } = "";
|
||||
[JsonPropertyName("holders")] public List<HolderEntry> Holders { get; set; } = [];
|
||||
}
|
||||
|
||||
public class HolderEntry
|
||||
{
|
||||
[JsonPropertyName("proxyWallet")] public string ProxyWallet { get; set; } = "";
|
||||
[JsonPropertyName("name")] public string Name { get; set; } = "";
|
||||
[JsonPropertyName("pseudonym")] public string Pseudonym { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("amount")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Amount { get; set; }
|
||||
|
||||
[JsonPropertyName("outcomeIndex")] public int OutcomeIndex { get; set; }
|
||||
[JsonPropertyName("profileImage")] public string? ProfileImage { get; set; }
|
||||
[JsonPropertyName("verified")] public bool Verified { get; set; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Polymarket Data API v1 Leaderboard response
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
public class LeaderboardEntry
|
||||
{
|
||||
[JsonPropertyName("rank")] public string Rank { get; set; } = "";
|
||||
[JsonPropertyName("proxyWallet")] public string ProxyWallet { get; set; } = "";
|
||||
[JsonPropertyName("userName")] public string UserName { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("vol")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Vol { get; set; }
|
||||
|
||||
[JsonPropertyName("pnl")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Pnl { get; set; }
|
||||
|
||||
[JsonPropertyName("profileImage")] public string? ProfileImage { get; set; }
|
||||
[JsonPropertyName("xUsername")] public string? XUsername { get; set; }
|
||||
[JsonPropertyName("verifiedBadge")] public bool VerifiedBadge { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
using System.Text.Json;
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Predictalytics.Infrastructure.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Providers.Polymarket;
|
||||
|
||||
/// <summary>
|
||||
/// Full implementation of IPlatformProvider for Polymarket.
|
||||
/// All log entries include Platform=Polymarket for log-file routing.
|
||||
/// </summary>
|
||||
public class PolymarketProvider : IPlatformProvider
|
||||
{
|
||||
private readonly PolymarketApiClient _api;
|
||||
private readonly ILogger<PolymarketProvider> _logger;
|
||||
|
||||
public PlatformType Platform => PlatformType.Polymarket;
|
||||
public string PlatformName => "Polymarket";
|
||||
public bool IsImplemented => true;
|
||||
|
||||
public PolymarketProvider(PolymarketApiClient api, ILogger<PolymarketProvider> logger)
|
||||
{ _api = api; _logger = logger; }
|
||||
|
||||
public async Task<IReadOnlyList<Trade>> GetTraderTradesAsync(string platformUserId, int limit = 1000, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
_logger.LogDebug("Fetching trades for {Wallet} (limit={Limit})", platformUserId, limit);
|
||||
var raw = await _api.GetTradesAsync(platformUserId, limit, ct);
|
||||
_logger.LogInformation("Fetched {Count} trades for {Wallet}", raw.Count, platformUserId);
|
||||
|
||||
var mappedTrades = raw.Select(r =>
|
||||
{
|
||||
var wallet = r.User ?? r.ProxyWallet ?? "";
|
||||
var side = MapTradeSide(r);
|
||||
var sideStr = side.ToString().ToUpperInvariant();
|
||||
// Compact format: {txHash}_{assetId}_{side} — no wallet in ID to reduce index size.
|
||||
// Wallet passed transiently via TransientWallet [NotMapped] for MarketHistoryWorker.
|
||||
return new Trade
|
||||
{
|
||||
Platform = PlatformType.Polymarket,
|
||||
PlatformTradeId = string.IsNullOrEmpty(r.TransactionHash)
|
||||
? $"{r.Timestamp}_{r.Asset}_{sideStr}"
|
||||
: $"{r.TransactionHash}_{r.Asset}_{sideStr}",
|
||||
MarketId = r.ConditionId ?? "",
|
||||
AssetId = r.Asset ?? "",
|
||||
Outcome = r.Outcome ?? "",
|
||||
Side = side,
|
||||
Price = (decimal)r.Price,
|
||||
Size = (decimal)r.Size,
|
||||
Amount = (decimal)(r.Price * r.Size),
|
||||
ExecutedAt = DateTimeOffset.FromUnixTimeSeconds(r.Timestamp).UtcDateTime,
|
||||
TransactionHash = r.TransactionHash,
|
||||
TraderId = 0,
|
||||
TransientWallet = wallet,
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return mappedTrades.GroupBy(t => t.PlatformTradeId).Select(g => g.First()).ToList();
|
||||
}
|
||||
|
||||
|
||||
public async Task<IReadOnlyList<Trade>> GetMarketTradesAsync(string platformMarketId, int limit = 1000, CancellationToken ct = default)
|
||||
{
|
||||
var raw = await _api.GetMarketTradesAsync(platformMarketId, limit, ct);
|
||||
_logger.LogInformation("Fetched {Count} trades for Market {Market} (limit={Limit})", raw.Count, platformMarketId, limit);
|
||||
|
||||
var mappedTrades = raw.Select(r =>
|
||||
{
|
||||
var wallet = !string.IsNullOrEmpty(r.User) ? r.User : (r.ProxyWallet ?? "");
|
||||
var side = MapTradeSide(r);
|
||||
var sideStr = side.ToString().ToUpperInvariant();
|
||||
return new Trade
|
||||
{
|
||||
Platform = PlatformType.Polymarket,
|
||||
PlatformTradeId = string.IsNullOrEmpty(r.TransactionHash)
|
||||
? $"{r.Timestamp}_{r.Asset}_{sideStr}"
|
||||
: $"{r.TransactionHash}_{r.Asset}_{sideStr}",
|
||||
MarketId = r.ConditionId ?? "",
|
||||
AssetId = r.Asset ?? "",
|
||||
Outcome = r.Outcome ?? "",
|
||||
Side = side,
|
||||
Price = (decimal)r.Price,
|
||||
Size = (decimal)r.Size,
|
||||
Amount = (decimal)(r.Price * r.Size),
|
||||
ExecutedAt = DateTimeOffset.FromUnixTimeSeconds(r.Timestamp).UtcDateTime,
|
||||
TransactionHash = r.TransactionHash,
|
||||
TraderId = 0,
|
||||
TransientWallet = wallet,
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return mappedTrades.GroupBy(t => t.PlatformTradeId).Select(g => g.First()).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<TraderPositionInfo>> GetTraderPositionsAsync(string platformUserId, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
_logger.LogDebug("Fetching positions for {Wallet}", platformUserId);
|
||||
var raw = await _api.GetPositionsAsync(platformUserId, ct);
|
||||
_logger.LogInformation("Fetched {Count} positions for {Wallet}", raw.Count, platformUserId);
|
||||
|
||||
return raw.Select(r => new TraderPositionInfo(
|
||||
platformUserId, r.Market, r.Question, r.Outcome,
|
||||
(decimal)r.Size, (decimal)r.AvgPrice,
|
||||
(decimal)r.CurrentValue, (decimal)r.PercentPnl
|
||||
)).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DiscoveredTrader>> DiscoverTradersAsync(int limit = 50, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
_logger.LogInformation("Running trader discovery via v1/leaderboard (limit={Limit})...", limit);
|
||||
var leaderboard = await _api.GetLeaderboardAsync(limit, ct: ct);
|
||||
_logger.LogInformation("Discovery returned {Count} traders from leaderboard", leaderboard.Count);
|
||||
|
||||
return leaderboard.Select(e => new DiscoveredTrader(
|
||||
e.ProxyWallet,
|
||||
string.IsNullOrEmpty(e.UserName) ? e.ProxyWallet[..10] + "..." : e.UserName,
|
||||
(decimal)e.Vol,
|
||||
0, // trade count not in leaderboard API
|
||||
0 // win rate computed later from trades
|
||||
)).ToList();
|
||||
}
|
||||
|
||||
public async Task<Market?> GetMarketAsync(string platformMarketId, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
_logger.LogDebug("Fetching market {MarketId}", platformMarketId);
|
||||
var raw = await _api.GetMarketAsync(platformMarketId, ct);
|
||||
if (raw == null)
|
||||
{
|
||||
_logger.LogWarning("Market {MarketId} not found", platformMarketId);
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Fetched market: {Question}", raw.Question);
|
||||
return MapGammaMarket(raw);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Market>> GetMarketsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
int offset = 0;
|
||||
if (!string.IsNullOrEmpty(cursor) && int.TryParse(cursor, out var parsed))
|
||||
offset = parsed;
|
||||
|
||||
_logger.LogInformation("Fetching markets batch (limit={Limit}, offset={Offset}, includeClosed={Closed})", limit, offset, includeClosed);
|
||||
var raw = await _api.GetMarketsAsync(limit, offset, includeClosed, ct);
|
||||
_logger.LogInformation("Fetched {Count} markets from Gamma API", raw.Count);
|
||||
|
||||
return raw
|
||||
.Where(m => !string.IsNullOrEmpty(m.ConditionId) && !string.IsNullOrEmpty(m.ClobTokenIds))
|
||||
.Select(MapGammaMarket)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DiscoveredTrader>> GetTopHoldersAsync(string platformMarketId, int limit = 20, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
_logger.LogInformation("Fetching top holders for market {MarketId}", platformMarketId[..12] + "...");
|
||||
var holdersGroups = await _api.GetHoldersAsync(platformMarketId, limit, ct);
|
||||
|
||||
// Flatten all holders across token groups, deduplicate by wallet
|
||||
var uniqueHolders = holdersGroups
|
||||
.SelectMany(g => g.Holders)
|
||||
.GroupBy(h => h.ProxyWallet)
|
||||
.Select(g =>
|
||||
{
|
||||
var first = g.First();
|
||||
var totalAmount = g.Sum(h => h.Amount);
|
||||
var displayName = !string.IsNullOrEmpty(first.Name) ? first.Name
|
||||
: !string.IsNullOrEmpty(first.Pseudonym) ? first.Pseudonym
|
||||
: first.ProxyWallet[..10] + "...";
|
||||
|
||||
return new DiscoveredTrader(
|
||||
first.ProxyWallet,
|
||||
displayName,
|
||||
(decimal)totalAmount,
|
||||
0, 0
|
||||
);
|
||||
})
|
||||
.OrderByDescending(d => d.Volume24h)
|
||||
.ToList();
|
||||
|
||||
_logger.LogInformation("Discovered {Count} unique holders from market {MarketId}",
|
||||
uniqueHolders.Count, platformMarketId[..12] + "...");
|
||||
|
||||
return uniqueHolders;
|
||||
}
|
||||
|
||||
// ── Private helpers ──────────────────────────────────────────
|
||||
|
||||
private Market MapGammaMarket(GammaMarketResponse raw)
|
||||
{
|
||||
var eventSlug = "";
|
||||
if (raw.Events != null && raw.Events.Count > 0 && !string.IsNullOrEmpty(raw.Events[0].Slug))
|
||||
{
|
||||
eventSlug = raw.Events[0].Slug;
|
||||
}
|
||||
|
||||
var market = new Market
|
||||
{
|
||||
Platform = PlatformType.Polymarket,
|
||||
PlatformMarketId = raw.ConditionId,
|
||||
MarketSlug = raw.Slug,
|
||||
EventSlug = eventSlug,
|
||||
Description = raw.Description,
|
||||
ImageUrl = raw.Image,
|
||||
Question = raw.Question,
|
||||
Category = raw.Category,
|
||||
Volume = (decimal)raw.Volume,
|
||||
Liquidity = (decimal)raw.Liquidity,
|
||||
StartDate = DateTime.TryParse(raw.StartDate, out var sd) ? sd : null,
|
||||
EndDate = DateTime.TryParse(raw.EndDate, out var ed) ? ed : null,
|
||||
CreatedAt = DateTime.TryParse(raw.CreatedAt, out var cd) ? cd : DateTime.UtcNow,
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
IsResolved = raw.Resolved || raw.Closed, // Prefer resolved flag
|
||||
ResolutionOutcome = raw.ResolutionOutcome,
|
||||
LastUpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Parse outcomes, prices, and token IDs from JSON strings
|
||||
var outcomeLabels = ParseJsonStringArray(raw.Outcomes);
|
||||
var outcomePrices = ParseJsonStringArray(raw.OutcomePrices);
|
||||
var tokenIds = ParseJsonStringArray(raw.ClobTokenIds);
|
||||
|
||||
for (int i = 0; i < outcomeLabels.Count; i++)
|
||||
{
|
||||
decimal price = 0;
|
||||
if (i < outcomePrices.Count)
|
||||
decimal.TryParse(outcomePrices[i], System.Globalization.NumberStyles.Any,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out price);
|
||||
|
||||
string tokenId = i < tokenIds.Count ? tokenIds[i] : "";
|
||||
|
||||
var label = outcomeLabels[i];
|
||||
if ((label.Equals("Yes", StringComparison.OrdinalIgnoreCase) || label.Equals("No", StringComparison.OrdinalIgnoreCase))
|
||||
&& !string.IsNullOrEmpty(raw.GroupItemTitle))
|
||||
{
|
||||
label = $"{raw.GroupItemTitle} - {label}";
|
||||
}
|
||||
|
||||
market.Outcomes.Add(new MarketOutcome
|
||||
{
|
||||
Label = label,
|
||||
OutcomeIndex = i,
|
||||
TokenId = tokenId,
|
||||
CurrentPrice = price
|
||||
});
|
||||
}
|
||||
|
||||
return market;
|
||||
}
|
||||
|
||||
private static List<string> ParseJsonStringArray(string? json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json)) return [];
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<string>>(json) ?? [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static TradeSide MapTradeSide(PolymarketTradeResponse r)
|
||||
{
|
||||
// Check Action/Type field first for special operations
|
||||
var typeOrAction = !string.IsNullOrEmpty(r.Action) ? r.Action
|
||||
: !string.IsNullOrEmpty(r.Type) ? r.Type : "";
|
||||
|
||||
if (!string.IsNullOrEmpty(typeOrAction))
|
||||
{
|
||||
if (typeOrAction.Equals("SPLIT", StringComparison.OrdinalIgnoreCase)) return TradeSide.Split;
|
||||
if (typeOrAction.Equals("MERGE", StringComparison.OrdinalIgnoreCase)) return TradeSide.Merge;
|
||||
if (typeOrAction.Equals("REDEEM", StringComparison.OrdinalIgnoreCase)) return TradeSide.Redeem;
|
||||
if (typeOrAction.Equals("ADD_LIQUIDITY", StringComparison.OrdinalIgnoreCase)) return TradeSide.AddLiquidity;
|
||||
if (typeOrAction.Equals("REMOVE_LIQUIDITY", StringComparison.OrdinalIgnoreCase)) return TradeSide.RemoveLiquidity;
|
||||
// Type field can also contain BUY/SELL directly
|
||||
if (typeOrAction.Equals("BUY", StringComparison.OrdinalIgnoreCase)) return TradeSide.Buy;
|
||||
if (typeOrAction.Equals("SELL", StringComparison.OrdinalIgnoreCase)) return TradeSide.Sell;
|
||||
}
|
||||
|
||||
// Side field (explicit buy/sell direction)
|
||||
if (!string.IsNullOrEmpty(r.Side))
|
||||
{
|
||||
if (r.Side.Equals("BUY", StringComparison.OrdinalIgnoreCase)) return TradeSide.Buy;
|
||||
if (r.Side.Equals("SELL", StringComparison.OrdinalIgnoreCase)) return TradeSide.Sell;
|
||||
}
|
||||
|
||||
return TradeSide.Unknown;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user