using Predictalytics.Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; namespace Predictalytics.Infrastructure.Data; public class AppDbContext : DbContext { public DbSet Traders => Set(); public DbSet Trades => Set(); public DbSet Events => Set(); public DbSet Markets => Set(); public DbSet MarketOutcomes => Set(); public DbSet TraderScores => Set(); public DbSet WatchlistEntries => Set(); public DbSet Alerts => Set(); public DbSet PlatformConfigs => Set(); public DbSet TraderAnalytics => Set(); public DbSet MarketAnalytics => Set(); public DbSet TraderPositions => Set(); public DbSet MarketOutcomePriceSnapshots => Set(); public DbSet TraderDailySnapshots => Set(); public DbSet TraderCategoryPerformances => Set(); public DbSet TradeContexts => Set(); public DbSet BackgroundJobs => Set(); public DbSet TraderTraits => Set(); public DbSet TraderWindowMetrics => Set(); public DbSet InsiderWatches => Set(); public DbSet TraderFingerprintSnapshots => Set(); private readonly bool _isReadOnly; public AppDbContext(DbContextOptions options, Microsoft.Extensions.Configuration.IConfiguration? configuration = null) : base(options) { _isReadOnly = configuration?.GetValue("ApiSettings:ReadOnlyDatabase", false) ?? false; } public override int SaveChanges() { if (_isReadOnly) { throw new System.InvalidOperationException("Database is configured as Read-Only."); } return base.SaveChanges(); } public override int SaveChanges(bool acceptAllChangesOnSuccess) { if (_isReadOnly) { throw new System.InvalidOperationException("Database is configured as Read-Only."); } return base.SaveChanges(acceptAllChangesOnSuccess); } public override Task SaveChangesAsync(CancellationToken cancellationToken = default) { if (_isReadOnly) { throw new System.InvalidOperationException("Database is configured as Read-Only."); } return base.SaveChangesAsync(cancellationToken); } public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { if (_isReadOnly) { throw new System.InvalidOperationException("Database is configured as Read-Only."); } return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } protected override void OnModelCreating(ModelBuilder mb) { // Trader mb.Entity(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(s => s.TraderId).OnDelete(DeleteBehavior.Cascade); }); // TraderTrait mb.Entity(e => { e.HasKey(t => t.Id); e.HasIndex(t => new { t.TraderId, t.Trait }).IsUnique(); e.Property(t => t.Value).HasPrecision(18, 4); e.HasOne(t => t.Trader).WithMany(tr => tr.Traits) .HasForeignKey(t => t.TraderId).OnDelete(DeleteBehavior.Cascade); }); // TraderWindowMetrics mb.Entity(e => { e.HasKey(t => t.Id); e.HasIndex(t => new { t.TraderId, t.WindowStart, t.WindowEnd }).IsUnique(); e.Property(t => t.WinRate).HasPrecision(8, 4); e.Property(t => t.AvgReturnPct).HasPrecision(18, 4); e.Property(t => t.MedianWinReturnPct).HasPrecision(18, 4); e.Property(t => t.MedianLossReturnPct).HasPrecision(18, 4); e.Property(t => t.ProfitFactor).HasPrecision(18, 4); e.HasOne(t => t.Trader).WithMany() .HasForeignKey(t => t.TraderId).OnDelete(DeleteBehavior.Cascade); }); // Trade mb.Entity(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(18, 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); }); // Event mb.Entity(e => { e.HasKey(ev => ev.Id); e.HasIndex(ev => new { ev.Platform, ev.PlatformEventId }).IsUnique(); e.Property(ev => ev.Slug).HasMaxLength(512); e.Property(ev => ev.Title).HasMaxLength(1024); e.Property(ev => ev.Description).HasMaxLength(4096); e.Property(ev => ev.ImageUrl).HasMaxLength(1024); e.Property(ev => ev.Tags).HasMaxLength(1024); e.HasMany(ev => ev.Markets).WithOne(m => m.Event).HasForeignKey(m => m.EventId) .OnDelete(DeleteBehavior.Cascade); }); // Market mb.Entity(e => { e.HasKey(m => m.Id); e.HasIndex(m => new { m.Platform, m.PlatformMarketId }).IsUnique(); e.Property(m => m.ConditionId).HasMaxLength(256); e.Property(m => m.QuestionId).HasMaxLength(256); e.Property(m => m.MarketSlug).HasMaxLength(512); e.Property(m => m.Question).HasMaxLength(1024); e.Property(m => m.Description).HasMaxLength(4096); e.Property(m => m.ImageUrl).HasMaxLength(1024); e.Property(m => m.Category).HasConversion().HasMaxLength(64); e.Property(m => m.Subcategory).HasMaxLength(128); e.Property(m => m.Volume).HasPrecision(18, 4); e.Property(m => m.Volume24h).HasPrecision(18, 4); e.Property(m => m.Liquidity).HasPrecision(18, 4); e.HasMany(m => m.Outcomes).WithOne(o => o.Market).HasForeignKey(o => o.MarketId) .OnDelete(DeleteBehavior.Cascade); }); // MarketOutcome mb.Entity(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(e => { e.HasKey(s => s.Id); e.HasIndex(s => s.TraderId).IsUnique(); e.Property(s => s.ActivityScore).HasPrecision(5, 2); e.Property(s => s.QualityScore).HasPrecision(5, 2); e.Property(s => s.CombinedScore).HasPrecision(5, 2); e.Property(s => s.VolumeScore).HasPrecision(5, 2); e.Property(s => s.TimingScore).HasPrecision(5, 2); e.HasOne(s => s.Trader).WithOne(t => t.CurrentScore).HasForeignKey(s => s.TraderId).OnDelete(DeleteBehavior.Cascade); }); // WatchlistEntry mb.Entity(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); }); // InsiderWatch (system-level, one row per flagged trader) mb.Entity(e => { e.HasKey(i => i.Id); e.HasIndex(i => i.TraderId).IsUnique(); e.HasOne(i => i.Trader).WithMany().HasForeignKey(i => i.TraderId); }); // TraderFingerprintSnapshot (time-series; many rows per trader) mb.Entity(e => { e.HasKey(s => s.Id); e.HasIndex(s => new { s.TraderId, s.CapturedAt }); e.HasOne(s => s.Trader).WithMany().HasForeignKey(s => s.TraderId); }); // Alert mb.Entity(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); }); // TraderCategoryPerformance mb.Entity(e => { e.HasKey(tcp => tcp.Id); e.HasOne(tcp => tcp.Trader).WithMany(t => t.CategoryPerformances).HasForeignKey(tcp => tcp.TraderId).OnDelete(DeleteBehavior.Cascade); e.Property(tcp => tcp.Category).HasConversion().HasMaxLength(64); e.Property(tcp => tcp.Subcategory).HasMaxLength(128); e.Property(tcp => tcp.TotalVolume).HasPrecision(18, 4); e.Property(tcp => tcp.TotalPnL).HasPrecision(18, 4); e.HasIndex(tcp => new { tcp.TraderId, tcp.Category, tcp.Subcategory }).IsUnique(); }); // TradeContext mb.Entity(e => { e.HasKey(tc => tc.Id); e.HasOne(tc => tc.Trade).WithOne(t => t.Context).HasForeignKey(tc => tc.TradeId).OnDelete(DeleteBehavior.Cascade); e.Property(tc => tc.PriceBefore1m).HasPrecision(18, 4); e.Property(tc => tc.PriceAfter1m).HasPrecision(18, 4); e.Property(tc => tc.EstimatedSlippage).HasPrecision(18, 4); e.Property(tc => tc.EstimatedOrderType).HasConversion().HasMaxLength(32); }); // PlatformConfig mb.Entity(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(e => { e.HasKey(a => a.TraderId); e.HasOne(a => a.Trader).WithOne(t => t.Analytics).HasForeignKey(a => a.TraderId).OnDelete(DeleteBehavior.Cascade); 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); // D2c & E3 & E5 e.Property(a => a.MedianWinReturnPct).HasPrecision(18, 4); e.Property(a => a.AvgWinReturnPct).HasPrecision(18, 4); e.Property(a => a.MedianLossReturnPct).HasPrecision(18, 4); e.Property(a => a.AvgLossReturnPct).HasPrecision(18, 4); e.Property(a => a.ProfitFactor).HasPrecision(18, 4); e.Property(a => a.MedianHoldDurationHours).HasPrecision(18, 4); e.Property(a => a.P50PositionSize).HasPrecision(18, 4); e.Property(a => a.P90PositionSize).HasPrecision(18, 4); e.Property(a => a.TradesPerWeek).HasPrecision(18, 4); e.Property(a => a.MedianMarketVolumeUsd).HasPrecision(18, 4); e.Property(a => a.MedianPostFillDriftPct).HasPrecision(18, 4); e.Property(a => a.NetEdgeAfterFeesPct).HasPrecision(18, 4); }); // MarketAnalytics mb.Entity(e => { e.HasKey(a => a.MarketId); e.Property(a => a.BotActivityScore).HasPrecision(8, 4); e.Property(a => a.AverageTradeSize).HasPrecision(18, 4); }); // TraderPosition mb.Entity(e => { e.HasKey(tp => tp.Id); e.HasIndex(tp => new { tp.TraderId, tp.MarketOutcomeId }).IsUnique(); e.Property(tp => tp.SharesHeld).HasPrecision(14, 6); e.Property(tp => tp.AvgCost).HasPrecision(10, 6); e.Property(tp => tp.RealizedPnl).HasPrecision(18, 4); e.HasOne(tp => tp.Trader).WithMany(t => t.Positions).HasForeignKey(tp => tp.TraderId).OnDelete(DeleteBehavior.Cascade); e.HasOne(tp => tp.MarketOutcome).WithMany().HasForeignKey(tp => tp.MarketOutcomeId).OnDelete(DeleteBehavior.Cascade); }); // TraderDailySnapshot mb.Entity(e => { e.HasKey(s => s.Id); e.HasIndex(s => new { s.TraderId, s.Date }).IsUnique(); e.Property(s => s.TotalPnl).HasPrecision(18, 4); e.Property(s => s.CurrentBalance).HasPrecision(18, 4); e.HasOne(s => s.Trader).WithMany().HasForeignKey(s => s.TraderId).OnDelete(DeleteBehavior.Cascade); }); // MarketOutcomePriceSnapshot mb.Entity(e => { e.HasKey(ps => ps.Id); e.HasIndex(ps => new { ps.MarketOutcomeId, ps.Timestamp }); e.Property(ps => ps.Price).HasPrecision(10, 6); e.HasOne(ps => ps.MarketOutcome).WithMany().HasForeignKey(ps => ps.MarketOutcomeId).OnDelete(DeleteBehavior.Cascade); }); // BackgroundJob mb.Entity(e => { e.HasKey(j => j.Id); e.HasIndex(j => j.Status); e.HasIndex(j => j.JobType); e.Property(j => j.JobType).HasConversion().HasMaxLength(64); e.Property(j => j.Status).HasConversion().HasMaxLength(64); e.Property(j => j.ErrorMessage).HasMaxLength(4096); e.HasOne(j => j.Trader).WithMany().HasForeignKey(j => j.TraderId).OnDelete(DeleteBehavior.SetNull); }); } }