From 8b9b34342f2b95408e5682d423769f32da2cdc01 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 23 Jul 2026 19:46:36 +0200 Subject: [PATCH] @ Insider-Follow feed: auto-watchlist possible-insider wallets + new-trade alerts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes the existing possible_insider trait actionable: - New AlertType.InsiderActivity (severity 4). - AlertService.EvaluateInsiderWatchAsync (run from EvaluateAlertsAsync every 15 min): auto-adds every possible_insider wallet to the watchlist, then fires one InsiderActivity alert per new trade one of them places. These wallets trade rarely, so a single new trade is the strongest copy signal. - Dedup + no history spam via WatchlistEntry.LastInsiderAlertAt (migration AddWatchlistLastInsiderAlertAt); a freshly auto-added wallet is baselined at AddedAt so backfilled trades never alert. - Repo support: ITraderRepository.GetByTraitAsync, IWatchlistRepository.UpdateAsync. - UI: distinct πŸ‘ icon for insider alerts (πŸ’° for large positions). - Tests: auto-add-without-history-alert, alert-on-new-trade-with-dedup. Migration auto-applies on startup (DependencyInjection.MigrateAsync). Co-Authored-By: Claude Opus 4.8 @ --- src/Predictalytics.Api/wwwroot/js/app.js | 3 +- .../Services/AlertServiceTests.cs | 91 ++ .../Services/AlertService.cs | 65 + .../Entities/WatchlistEntry.cs | 7 + src/Predictalytics.Domain/Enums/AlertType.cs | 4 +- .../Interfaces/ITraderRepository.cs | 3 + .../Interfaces/IWatchlistRepository.cs | 1 + .../Data/Repositories/TraderRepository.cs | 7 + .../Data/Repositories/WatchlistRepository.cs | 3 + ...AddWatchlistLastInsiderAlertAt.Designer.cs | 1293 +++++++++++++++++ ...23164157_AddWatchlistLastInsiderAlertAt.cs | 29 + .../Migrations/AppDbContextModelSnapshot.cs | 3 + 12 files changed, 1507 insertions(+), 2 deletions(-) create mode 100644 src/Predictalytics.Application.Tests/Services/AlertServiceTests.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260723164157_AddWatchlistLastInsiderAlertAt.Designer.cs create mode 100644 src/Predictalytics.Infrastructure/Migrations/20260723164157_AddWatchlistLastInsiderAlertAt.cs diff --git a/src/Predictalytics.Api/wwwroot/js/app.js b/src/Predictalytics.Api/wwwroot/js/app.js index 539f77c..ba7c2ed 100644 --- a/src/Predictalytics.Api/wwwroot/js/app.js +++ b/src/Predictalytics.Api/wwwroot/js/app.js @@ -558,9 +558,10 @@ async function loadAlerts() { const data = await api('/api/alerts?count=50'); const el = document.getElementById('alertsList'); if (!data || !data.length) { el.innerHTML = '

No alerts yet.

'; return; } + const alertIcon = t => t === 'InsiderActivity' ? 'πŸ‘' : t === 'LargePosition' ? 'πŸ’°' : 'πŸ””'; el.innerHTML = data.map(a => `
-
πŸ””
+
${alertIcon(a.type)}
${a.title}
${a.message}
diff --git a/src/Predictalytics.Application.Tests/Services/AlertServiceTests.cs b/src/Predictalytics.Application.Tests/Services/AlertServiceTests.cs new file mode 100644 index 0000000..6d04c29 --- /dev/null +++ b/src/Predictalytics.Application.Tests/Services/AlertServiceTests.cs @@ -0,0 +1,91 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Predictalytics.Application.Services; +using Predictalytics.Domain.Entities; +using Predictalytics.Domain.Enums; +using Predictalytics.Infrastructure.Data; +using Predictalytics.Infrastructure.Data.Repositories; +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Predictalytics.Application.Tests.Services; + +public class AlertServiceTests +{ + private static AppDbContext CreateDbContext() + => new(new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options); + + private static AlertService CreateService(AppDbContext db) => new( + new AlertRepository(db), + new TradeRepository(db, NullLogger.Instance), + new TraderRepository(db), + new WatchlistRepository(db), + NullLogger.Instance); + + [Fact] + public async Task InsiderWatch_AutoAddsInsider_WithoutAlertingOnHistory() + { + using var db = CreateDbContext(); + + var trader = new Trader { Id = 1, PlatformUserId = "0xI", DisplayName = "QuietWhale" }; + trader.Traits.Add(new TraderTrait { TraderId = 1, Trait = "possible_insider", Value = 4.2m }); + db.Traders.Add(trader); + // A historical trade (predates the auto-add) must NOT produce an alert. + db.Trades.Add(new Trade + { + Id = 10, TraderId = 1, DbMarketId = 100, Platform = PlatformType.Polymarket, + Side = TradeSide.Buy, Outcome = "Yes", Price = 0.08m, Amount = 900m, + ExecutedAt = DateTime.UtcNow.AddDays(-3) + }); + await db.SaveChangesAsync(); + + var svc = CreateService(db); + await svc.EvaluateInsiderWatchAsync(); + + Assert.Single(db.WatchlistEntries.Where(w => w.TraderId == 1)); + Assert.Empty(db.Alerts.Where(a => a.Type == AlertType.InsiderActivity)); + } + + [Fact] + public async Task InsiderWatch_AlertsOnNewTrade_AndDedupsOnRerun() + { + using var db = CreateDbContext(); + + var trader = new Trader { Id = 2, PlatformUserId = "0xJ", DisplayName = "Insider2" }; + trader.Traits.Add(new TraderTrait { TraderId = 2, Trait = "possible_insider", Value = 5m }); + db.Traders.Add(trader); + // Already watched, added an hour ago. + db.WatchlistEntries.Add(new WatchlistEntry + { + Id = 5, TraderId = 2, Label = "watched", AlertsEnabled = true, + AddedAt = DateTime.UtcNow.AddHours(-1) + }); + // A trade placed AFTER the entry was added -> should alert exactly once. + db.Trades.Add(new Trade + { + Id = 20, TraderId = 2, DbMarketId = 200, Platform = PlatformType.Polymarket, + Side = TradeSide.Buy, Outcome = "No", Price = 0.12m, Amount = 1500m, + ExecutedAt = DateTime.UtcNow.AddMinutes(-20) + }); + await db.SaveChangesAsync(); + + var svc = CreateService(db); + await svc.EvaluateInsiderWatchAsync(); + + var alerts = db.Alerts.Where(a => a.Type == AlertType.InsiderActivity).ToList(); + Assert.Single(alerts); + Assert.Equal(4, alerts[0].Severity); + Assert.Equal(2, alerts[0].TraderId); + + // High-water mark advanced; a second run must not re-alert. + var entry = db.WatchlistEntries.First(w => w.Id == 5); + Assert.NotNull(entry.LastInsiderAlertAt); + + await svc.EvaluateInsiderWatchAsync(); + Assert.Single(db.Alerts.Where(a => a.Type == AlertType.InsiderActivity)); + } +} diff --git a/src/Predictalytics.Application/Services/AlertService.cs b/src/Predictalytics.Application/Services/AlertService.cs index f694f2d..7e6e5cb 100644 --- a/src/Predictalytics.Application/Services/AlertService.cs +++ b/src/Predictalytics.Application/Services/AlertService.cs @@ -15,20 +15,26 @@ public class AlertService : IAlertService private readonly IAlertRepository _alertRepo; private readonly ITradeRepository _tradeRepo; private readonly ITraderRepository _traderRepo; + private readonly IWatchlistRepository _watchlistRepo; private readonly ILogger _logger; // Alert thresholds (configurable in future) private const decimal LargePositionThresholdUsd = 5000m; + /// Trait computed by TraderTraitCalculator for statistically improbable longshot winners. + private const string PossibleInsiderTrait = "possible_insider"; + public AlertService( IAlertRepository alertRepo, ITradeRepository tradeRepo, ITraderRepository traderRepo, + IWatchlistRepository watchlistRepo, ILogger logger) { _alertRepo = alertRepo; _tradeRepo = tradeRepo; _traderRepo = traderRepo; + _watchlistRepo = watchlistRepo; _logger = logger; } @@ -59,6 +65,65 @@ public class AlertService : IAlertService }, ct); } } + + await EvaluateInsiderWatchAsync(ct); + } + + /// + /// Insider-Follow feed: keeps every possible_insider wallet on the watchlist and fires a + /// high-severity alert for each new trade one of them places. These wallets trade rarely, so a + /// single new trade is the strongest copy signal we have. Dedup is via + /// ; a freshly auto-added wallet is baselined at + /// its so historical trades never trigger a backlog of alerts. + /// + public async Task EvaluateInsiderWatchAsync(CancellationToken ct = default) + { + var insiders = await _traderRepo.GetByTraitAsync(PossibleInsiderTrait, ct); + + foreach (var trader in insiders) + { + var entry = trader.WatchlistEntries.FirstOrDefault(); + + // Auto-add newly detected insiders; baseline at now so we don't alert on their history. + if (entry == null) + { + await _watchlistRepo.AddAsync(new WatchlistEntry + { + TraderId = trader.Id, + Label = "Auto: Possible Insider", + Notes = "Automatisch aufgenommen (possible_insider-Trait).", + AlertsEnabled = true + }, ct); + _logger.LogInformation("πŸ‘ Insider-Watch: auto-added {Trader} to watchlist", trader.DisplayName); + continue; + } + + if (!entry.AlertsEnabled) continue; + + var since = entry.LastInsiderAlertAt ?? entry.AddedAt; + var recent = await _tradeRepo.GetByTraderIdAsync(trader.Id, 0, 50, ct); + var newTrades = recent.Where(t => t.ExecutedAt > since).OrderBy(t => t.ExecutedAt).ToList(); + if (newTrades.Count == 0) continue; + + foreach (var trade in newTrades) + { + var marketRef = trade.DbMarketId.HasValue + ? $"Market #{trade.DbMarketId}" + : (!string.IsNullOrEmpty(trade.MarketId) ? $"Market {trade.MarketId[..Math.Min(12, trade.MarketId.Length)]}..." : "Unknown Market"); + await CreateAlertAsync(new Alert + { + Type = AlertType.InsiderActivity, + Platform = trade.Platform, + TraderId = trader.Id, + Title = $"Insider-Wallet aktiv: {trade.Side}", + Message = $"{trader.DisplayName} {trade.Side} ${trade.Amount:N0} auf {marketRef} ({trade.Outcome} @ {trade.Price:P0})", + Severity = 4 + }, ct); + } + + entry.LastInsiderAlertAt = newTrades.Max(t => t.ExecutedAt); + await _watchlistRepo.UpdateAsync(entry, ct); + } } public async Task CreateAlertAsync(Alert alert, CancellationToken ct = default) diff --git a/src/Predictalytics.Domain/Entities/WatchlistEntry.cs b/src/Predictalytics.Domain/Entities/WatchlistEntry.cs index 44aaf07..246e583 100644 --- a/src/Predictalytics.Domain/Entities/WatchlistEntry.cs +++ b/src/Predictalytics.Domain/Entities/WatchlistEntry.cs @@ -22,6 +22,13 @@ public class WatchlistEntry /// When this entry was added to the watchlist. public DateTime AddedAt { get; set; } = DateTime.UtcNow; + /// + /// High-water mark for insider-activity alerts: the ExecutedAt of the newest trade already + /// alerted on. Null until the first insider alert fires; new-trade detection uses + /// as the baseline so we never alert on backfilled history. + /// + public DateTime? LastInsiderAlertAt { get; set; } + // Navigation public Trader Trader { get; set; } = null!; } diff --git a/src/Predictalytics.Domain/Enums/AlertType.cs b/src/Predictalytics.Domain/Enums/AlertType.cs index f48f976..8ce7e00 100644 --- a/src/Predictalytics.Domain/Enums/AlertType.cs +++ b/src/Predictalytics.Domain/Enums/AlertType.cs @@ -13,5 +13,7 @@ public enum AlertType /// Trader exited a position completely PositionExit = 4, /// Custom user-defined alert - Custom = 5 + Custom = 5, + /// A watched possible-insider wallet placed a new trade (rare, high-signal). + InsiderActivity = 6 } diff --git a/src/Predictalytics.Domain/Interfaces/ITraderRepository.cs b/src/Predictalytics.Domain/Interfaces/ITraderRepository.cs index 0ac7123..bdcad4d 100644 --- a/src/Predictalytics.Domain/Interfaces/ITraderRepository.cs +++ b/src/Predictalytics.Domain/Interfaces/ITraderRepository.cs @@ -9,6 +9,9 @@ public interface ITraderRepository Task GetByPlatformIdAsync(PlatformType platform, string platformUserId, CancellationToken ct = default); Task> GetAllAsync(PlatformType? platform = null, int skip = 0, int take = 50, CancellationToken ct = default); Task> GetWatchlistedAsync(CancellationToken ct = default); + + /// Traders that carry a given trait, with their Traits and WatchlistEntries loaded. + Task> GetByTraitAsync(string trait, CancellationToken ct = default); Task> GetTopByScoreAsync(int count = 20, CancellationToken ct = default); Task> GetTopByPnLAsync(int count = 5, DateTime? since = null, CancellationToken ct = default); Task GetCountAsync(PlatformType? platform = null, CancellationToken ct = default); diff --git a/src/Predictalytics.Domain/Interfaces/IWatchlistRepository.cs b/src/Predictalytics.Domain/Interfaces/IWatchlistRepository.cs index f9af932..155c4d2 100644 --- a/src/Predictalytics.Domain/Interfaces/IWatchlistRepository.cs +++ b/src/Predictalytics.Domain/Interfaces/IWatchlistRepository.cs @@ -7,5 +7,6 @@ public interface IWatchlistRepository Task> GetAllAsync(CancellationToken ct = default); Task GetByTraderIdAsync(int traderId, CancellationToken ct = default); Task AddAsync(WatchlistEntry entry, CancellationToken ct = default); + Task UpdateAsync(WatchlistEntry entry, CancellationToken ct = default); Task RemoveAsync(int id, CancellationToken ct = default); } diff --git a/src/Predictalytics.Infrastructure/Data/Repositories/TraderRepository.cs b/src/Predictalytics.Infrastructure/Data/Repositories/TraderRepository.cs index 8b0fe07..56cc9d4 100644 --- a/src/Predictalytics.Infrastructure/Data/Repositories/TraderRepository.cs +++ b/src/Predictalytics.Infrastructure/Data/Repositories/TraderRepository.cs @@ -45,6 +45,13 @@ public class TraderRepository : ITraderRepository => await _db.Traders.Include(t => t.CurrentScore).Include(t => t.WatchlistEntries) .Where(t => t.WatchlistEntries.Any()).ToListAsync(ct); + public async Task> GetByTraitAsync(string trait, CancellationToken ct = default) + => await _db.Traders + .Include(t => t.Traits) + .Include(t => t.WatchlistEntries) + .Where(t => t.Traits.Any(tr => tr.Trait == trait)) + .ToListAsync(ct); + public async Task> 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); diff --git a/src/Predictalytics.Infrastructure/Data/Repositories/WatchlistRepository.cs b/src/Predictalytics.Infrastructure/Data/Repositories/WatchlistRepository.cs index 0b93763..24433a5 100644 --- a/src/Predictalytics.Infrastructure/Data/Repositories/WatchlistRepository.cs +++ b/src/Predictalytics.Infrastructure/Data/Repositories/WatchlistRepository.cs @@ -21,6 +21,9 @@ public class WatchlistRepository : IWatchlistRepository public async Task AddAsync(WatchlistEntry entry, CancellationToken ct = default) { _db.WatchlistEntries.Add(entry); await _db.SaveChangesAsync(ct); } + public async Task UpdateAsync(WatchlistEntry entry, CancellationToken ct = default) + { _db.WatchlistEntries.Update(entry); await _db.SaveChangesAsync(ct); } + public async Task RemoveAsync(int id, CancellationToken ct = default) { var e = await _db.WatchlistEntries.FindAsync(new object[] { id }, ct); diff --git a/src/Predictalytics.Infrastructure/Migrations/20260723164157_AddWatchlistLastInsiderAlertAt.Designer.cs b/src/Predictalytics.Infrastructure/Migrations/20260723164157_AddWatchlistLastInsiderAlertAt.Designer.cs new file mode 100644 index 0000000..9c280f7 --- /dev/null +++ b/src/Predictalytics.Infrastructure/Migrations/20260723164157_AddWatchlistLastInsiderAlertAt.Designer.cs @@ -0,0 +1,1293 @@ +ο»Ώ// +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("20260723164157_AddWatchlistLastInsiderAlertAt")] + partial class AddWatchlistLastInsiderAlertAt + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsRead") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("Platform") + .HasColumnType("int"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("TraderId"); + + b.ToTable("Alerts"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.BackgroundJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ErrorMessage") + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("JobType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("JobType"); + + b.HasIndex("Status"); + + b.HasIndex("TraderId"); + + b.ToTable("BackgroundJobs"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DbCreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsClosed") + .HasColumnType("tinyint(1)"); + + b.Property("LastUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Platform") + .HasColumnType("int"); + + b.Property("PlatformEventId") + .HasColumnType("bigint"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("Tags") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.HasKey("Id"); + + b.HasIndex("Platform", "PlatformEventId") + .IsUnique(); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("ConditionId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DbCreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("EventId") + .HasColumnType("int"); + + b.Property("FeeRateBps") + .HasColumnType("decimal(65,30)"); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("IsNegRisk") + .HasColumnType("tinyint(1)"); + + b.Property("IsResolved") + .HasColumnType("tinyint(1)"); + + b.Property("LastTradesUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("LastUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Liquidity") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("MarketSlug") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("Platform") + .HasColumnType("int"); + + b.Property("PlatformMarketId") + .HasColumnType("bigint"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("QuestionId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ResolutionOutcome") + .HasColumnType("longtext"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("Subcategory") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Volume") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Volume24h") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.HasIndex("Platform", "PlatformMarketId") + .IsUnique(); + + b.ToTable("Markets"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b => + { + b.Property("MarketId") + .HasColumnType("int"); + + b.Property("AverageTradeSize") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BotActivityScore") + .HasPrecision(8, 4) + .HasColumnType("decimal(8,4)"); + + b.Property("LastCalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UniqueTradersCount") + .HasColumnType("int"); + + b.HasKey("MarketId"); + + b.ToTable("MarketAnalytics"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CurrentPrice") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("MarketId") + .HasColumnType("int"); + + b.Property("OutcomeIndex") + .HasColumnType("int"); + + b.Property("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.MarketOutcomePriceSnapshot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("MarketOutcomeId") + .HasColumnType("int"); + + b.Property("Price") + .HasPrecision(10, 6) + .HasColumnType("decimal(10,6)"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("MarketOutcomeId", "Timestamp"); + + b.ToTable("MarketOutcomePriceSnapshots"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.PlatformConfig", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BaseUrl") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("SettingsJson") + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("PlatformConfigs"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregatedCount") + .HasColumnType("int"); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("AssetId") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("varchar(80)"); + + b.Property("DbMarketId") + .HasColumnType("int"); + + b.Property("ExecutedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsContextEnriched") + .HasColumnType("tinyint(1)"); + + b.Property("MarketId") + .IsRequired() + .HasMaxLength(66) + .HasColumnType("varchar(66)"); + + b.Property("MarketOutcomeId") + .HasColumnType("int"); + + b.Property("Outcome") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("OutcomeIndex") + .HasColumnType("int"); + + b.Property("Platform") + .HasColumnType("int"); + + b.Property("PlatformTradeId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PostTradePrice1m") + .HasColumnType("decimal(18,4)"); + + b.Property("PreTradePrice1m") + .HasColumnType("decimal(18,4)"); + + b.Property("Price") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("Side") + .HasColumnType("int"); + + b.Property("Size") + .HasPrecision(14, 6) + .HasColumnType("decimal(14,6)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.Property("TransactionHash") + .HasMaxLength(66) + .HasColumnType("varchar(66)"); + + b.Property("UsdcSize") + .HasColumnType("decimal(18,6)"); + + 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.TradeContext", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("EstimatedOrderType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("EstimatedSlippage") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("FollowerFillPrice10s") + .HasColumnType("decimal(65,30)"); + + b.Property("FollowerFillPrice60s") + .HasColumnType("decimal(65,30)"); + + b.Property("PriceAfter1m") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PriceBefore1m") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TradeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TradeId") + .IsUnique(); + + b.ToTable("TradeContexts"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AiStrategySummary") + .HasColumnType("longtext"); + + b.Property("AiStrategyUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IngestMode") + .HasColumnType("int"); + + b.Property("IsAutoDiscovered") + .HasColumnType("tinyint(1)"); + + b.Property("IsInitialImportComplete") + .HasColumnType("tinyint(1)"); + + b.Property("IsSuspectedBot") + .HasColumnType("tinyint(1)"); + + b.Property("LastAnalyzedAt") + .HasColumnType("datetime(6)"); + + b.Property("LastApiErrorAt") + .HasColumnType("datetime(6)"); + + b.Property("LastPolledAt") + .HasColumnType("datetime(6)"); + + b.Property("LastTradesUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ManualPriorityOverride") + .HasColumnType("int"); + + b.Property("MasterStatus") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("Platform") + .HasColumnType("int"); + + b.Property("PlatformUserId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Strategy") + .HasColumnType("int"); + + b.Property("Tier") + .HasColumnType("int"); + + b.Property("TotalPnl") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TotalTrades") + .HasColumnType("int"); + + b.Property("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("TraderId") + .HasColumnType("int"); + + b.Property("AvgLossReturnPct") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("AvgWinReturnPct") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("CategoryConcentration") + .HasColumnType("decimal(65,30)"); + + b.Property("ConvictionEdgePct") + .HasColumnType("decimal(65,30)"); + + b.Property("CopytradingCopyabilityScore") + .HasColumnType("decimal(65,30)"); + + b.Property("CopytradingQualityScore") + .HasColumnType("decimal(65,30)"); + + b.Property("CopytradingScore") + .HasColumnType("decimal(65,30)"); + + b.Property("CurrentBalance") + .HasColumnType("decimal(65,30)"); + + b.Property("EstimatedBankroll") + .HasColumnType("decimal(65,30)"); + + b.Property("LastCalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("LongestLosingStreakDays") + .HasColumnType("int"); + + b.Property("MaxDrawdownUsd") + .HasColumnType("decimal(65,30)"); + + b.Property("MedianHoldDurationHours") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("MedianLossReturnPct") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("MedianMarketVolumeUsd") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("MedianPostFillDriftPct") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("MedianWinReturnPct") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NetEdgeAfterFeesPct") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("OverallPnL") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("OverallWinRate") + .HasPrecision(8, 4) + .HasColumnType("decimal(8,4)"); + + b.Property("P50PositionSize") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("P90PositionSize") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PnL24h") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PnL30d") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PnL7d") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PnlVolatilityUsd") + .HasColumnType("decimal(65,30)"); + + b.Property("PriceBandProfileJson") + .HasColumnType("longtext"); + + b.Property("ProfitFactor") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Trades30d") + .HasColumnType("int"); + + b.Property("TradesPerWeek") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("WinRate24h") + .HasPrecision(8, 4) + .HasColumnType("decimal(8,4)"); + + b.Property("WinRate30d") + .HasPrecision(8, 4) + .HasColumnType("decimal(8,4)"); + + b.Property("WinRate7d") + .HasPrecision(8, 4) + .HasColumnType("decimal(8,4)"); + + b.HasKey("TraderId"); + + b.ToTable("TraderAnalytics"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.TraderCategoryPerformance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Subcategory") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("TotalInvested") + .HasColumnType("decimal(65,30)"); + + b.Property("TotalPnL") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TotalTrades") + .HasColumnType("int"); + + b.Property("TotalVolume") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.Property("WinningTrades") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TraderId", "Category", "Subcategory") + .IsUnique(); + + b.ToTable("TraderCategoryPerformances"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.TraderDailySnapshot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CurrentBalance") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("TotalPnl") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TraderId", "Date") + .IsUnique(); + + b.ToTable("TraderDailySnapshots"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AvgCost") + .HasPrecision(10, 6) + .HasColumnType("decimal(10,6)"); + + b.Property("IsHistoryPruned") + .HasColumnType("tinyint(1)"); + + b.Property("LastAppliedTradeId") + .HasColumnType("bigint"); + + b.Property("LastTradeExecutedAt") + .HasColumnType("datetime(6)"); + + b.Property("LastUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("MarketOutcomeId") + .HasColumnType("int"); + + b.Property("RealizedPnl") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("SharesHeld") + .HasPrecision(14, 6) + .HasColumnType("decimal(14,6)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("MarketOutcomeId"); + + b.HasIndex("TraderId", "MarketOutcomeId") + .IsUnique(); + + b.ToTable("TraderPositions"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ActivityScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CombinedScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("QualityScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("Rank") + .HasColumnType("int"); + + b.Property("TimingScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.Property("VolumeScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("TraderId") + .IsUnique(); + + b.ToTable("TraderScores"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.TraderTrait", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ComputedAt") + .HasColumnType("datetime(6)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.Property("Trait") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Value") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.HasKey("Id"); + + b.HasIndex("TraderId", "Trait") + .IsUnique(); + + b.ToTable("TraderTraits"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.TraderWindowMetrics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AvgReturnPct") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ClosedMarkets") + .HasColumnType("int"); + + b.Property("ComputedAt") + .HasColumnType("datetime(6)"); + + b.Property("MedianLossReturnPct") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("MedianWinReturnPct") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ProfitFactor") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.Property("WinRate") + .HasPrecision(8, 4) + .HasColumnType("decimal(8,4)"); + + b.Property("WindowEnd") + .HasColumnType("datetime(6)"); + + b.Property("WindowStart") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TraderId", "WindowStart", "WindowEnd") + .IsUnique(); + + b.ToTable("TraderWindowMetrics"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("AlertsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastInsiderAlertAt") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("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.BackgroundJob", b => + { + b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader") + .WithMany() + .HasForeignKey("TraderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Trader"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b => + { + b.HasOne("Predictalytics.Domain.Entities.Event", "Event") + .WithMany("Markets") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + 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.MarketOutcomePriceSnapshot", b => + { + b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome") + .WithMany() + .HasForeignKey("MarketOutcomeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MarketOutcome"); + }); + + 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.TradeContext", b => + { + b.HasOne("Predictalytics.Domain.Entities.Trade", "Trade") + .WithOne("Context") + .HasForeignKey("Predictalytics.Domain.Entities.TradeContext", "TradeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trade"); + }); + + 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.TraderCategoryPerformance", b => + { + b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader") + .WithMany("CategoryPerformances") + .HasForeignKey("TraderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trader"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.TraderDailySnapshot", b => + { + b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader") + .WithMany() + .HasForeignKey("TraderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trader"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b => + { + b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome") + .WithMany() + .HasForeignKey("MarketOutcomeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader") + .WithMany("Positions") + .HasForeignKey("TraderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MarketOutcome"); + + 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.TraderTrait", b => + { + b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader") + .WithMany("Traits") + .HasForeignKey("TraderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trader"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.TraderWindowMetrics", b => + { + b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader") + .WithMany() + .HasForeignKey("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.Event", b => + { + b.Navigation("Markets"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b => + { + b.Navigation("Analytics"); + + b.Navigation("Outcomes"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b => + { + b.Navigation("Context"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b => + { + b.Navigation("Analytics"); + + b.Navigation("CategoryPerformances"); + + b.Navigation("CurrentScore"); + + b.Navigation("Positions"); + + b.Navigation("Trades"); + + b.Navigation("Traits"); + + b.Navigation("WatchlistEntries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Predictalytics.Infrastructure/Migrations/20260723164157_AddWatchlistLastInsiderAlertAt.cs b/src/Predictalytics.Infrastructure/Migrations/20260723164157_AddWatchlistLastInsiderAlertAt.cs new file mode 100644 index 0000000..be26ca8 --- /dev/null +++ b/src/Predictalytics.Infrastructure/Migrations/20260723164157_AddWatchlistLastInsiderAlertAt.cs @@ -0,0 +1,29 @@ +ο»Ώusing System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Predictalytics.Infrastructure.Migrations +{ + /// + public partial class AddWatchlistLastInsiderAlertAt : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastInsiderAlertAt", + table: "WatchlistEntries", + type: "datetime(6)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastInsiderAlertAt", + table: "WatchlistEntries"); + } + } +} diff --git a/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs index 26bc72c..76f9dce 100644 --- a/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -1038,6 +1038,9 @@ namespace Predictalytics.Infrastructure.Migrations .HasMaxLength(256) .HasColumnType("varchar(256)"); + b.Property("LastInsiderAlertAt") + .HasColumnType("datetime(6)"); + b.Property("Notes") .HasColumnType("longtext");