${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