diff --git a/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs b/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs index 39443c4..567f9bc 100644 --- a/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs +++ b/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs @@ -20,6 +20,39 @@ public static class TraderEndpoints group.MapGet("/showcases", async (IAnalyticsService svc, CancellationToken ct) => Results.Ok(await svc.GetShowcasesAsync(ct))); + // Dedicated Insider view: system-level list of possible-insider wallets (NOT a user watchlist). + group.MapGet("/insiders", async (Predictalytics.Infrastructure.Data.AppDbContext db, CancellationToken ct) => + { + var traders = await db.Traders + .Include(t => t.Traits) + .Include(t => t.Analytics) + .Include(t => t.CurrentScore) + .Where(t => t.Traits.Any(tr => tr.Trait == "possible_insider")) + .ToListAsync(ct); + + var detectedAt = (await db.InsiderWatches.ToListAsync(ct)) + .ToDictionary(i => i.TraderId, i => i.FirstDetectedAt); + + var result = traders + .Select(t => new Predictalytics.Application.DTOs.InsiderDto( + t.Id, + t.Platform.ToString(), + t.DisplayName, + t.PlatformUserId, + t.CurrentScore?.CombinedScore ?? 0, + t.Analytics?.CopytradingCopyabilityScore ?? 0, + t.WinRate, + t.TotalPnl, + t.TotalTrades, + t.Traits.FirstOrDefault(tr => tr.Trait == "possible_insider")?.Value ?? 0, + detectedAt.TryGetValue(t.Id, out var d) ? d : (DateTime?)null, + t.LastPolledAt)) + .OrderByDescending(i => i.SurpriseValue) + .ToList(); + + return Results.Ok(result); + }); + group.MapGet("/traits", async (Predictalytics.Infrastructure.Data.AppDbContext db, CancellationToken ct) => Results.Ok(await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync(db.TraderTraits.Select(t => t.Trait).Distinct(), ct))); diff --git a/src/Predictalytics.Api/wwwroot/index.html b/src/Predictalytics.Api/wwwroot/index.html index 81e4db5..7db263d 100644 --- a/src/Predictalytics.Api/wwwroot/index.html +++ b/src/Predictalytics.Api/wwwroot/index.html @@ -36,6 +36,10 @@ Watchlist + + + Insider + Märkte @@ -321,6 +325,36 @@ + +
+
+
+

Insider-Radar

+
Wallets mit statistisch unplausibel guten Longshot-Treffern (possible_insider). Systemweit erkannt — unabhängig von deiner Watchlist.
+
+
+
+
+ + + + + + + + + + + + + + + +
TraderPlattformSurprise ↓ScoreCopyabilityWin RatePnLTradesErkannt
+
+
+
+
diff --git a/src/Predictalytics.Api/wwwroot/js/app.js b/src/Predictalytics.Api/wwwroot/js/app.js index ba7c2ed..c909b25 100644 --- a/src/Predictalytics.Api/wwwroot/js/app.js +++ b/src/Predictalytics.Api/wwwroot/js/app.js @@ -28,6 +28,7 @@ document.querySelectorAll('.nav-item[data-page]').forEach(item => { if (page === 'markets') loadMarkets(); if (page === 'jobs') loadJobs(); if (page === 'watchlist') loadWatchlist(); + if (page === 'insiders') loadInsiders(); }); }); @@ -514,6 +515,32 @@ async function loadTraders() { `).join(''); } +async function loadInsiders() { + const tbody = document.getElementById('insidersBody'); + const data = await api('/api/traders/insiders'); + if (!Array.isArray(data)) { + tbody.innerHTML = '⚠ Insider-Liste konnte nicht geladen werden (siehe Konsole / Server-Log).'; + return; + } + if (!data.length) { + tbody.innerHTML = '

Noch keine possible-insider Wallets erkannt.

'; + return; + } + tbody.innerHTML = data.map(t => ` + + ${t.displayName} + ${t.platform} + ${Number(t.surpriseValue).toFixed(1)} + ${Number(t.combinedScore).toFixed(1)} + ${Number(t.copytradingCopyabilityScore || 0).toFixed(1)} + ${fmt.pct(t.winRate)} + ${fmt.pnl(t.totalPnl)} + ${fmt.num(t.totalTrades)} + ${t.firstDetectedAt ? fmt.time(t.firstDetectedAt) : '—'} + + `).join(''); +} + async function loadWatchlist() { const tbody = document.getElementById('watchlistBody'); const data = await api(`/api/watchlist`); diff --git a/src/Predictalytics.Application.Tests/Services/AlertServiceTests.cs b/src/Predictalytics.Application.Tests/Services/AlertServiceTests.cs index 6d04c29..42d9d9d 100644 --- a/src/Predictalytics.Application.Tests/Services/AlertServiceTests.cs +++ b/src/Predictalytics.Application.Tests/Services/AlertServiceTests.cs @@ -23,18 +23,18 @@ public class AlertServiceTests new AlertRepository(db), new TradeRepository(db, NullLogger.Instance), new TraderRepository(db), - new WatchlistRepository(db), + new InsiderWatchRepository(db), NullLogger.Instance); [Fact] - public async Task InsiderWatch_AutoAddsInsider_WithoutAlertingOnHistory() + public async Task InsiderWatch_RegistersInsider_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. + // A historical trade (predates detection) must NOT produce an alert. db.Trades.Add(new Trade { Id = 10, TraderId = 1, DbMarketId = 100, Platform = PlatformType.Polymarket, @@ -46,7 +46,9 @@ public class AlertServiceTests var svc = CreateService(db); await svc.EvaluateInsiderWatchAsync(); - Assert.Single(db.WatchlistEntries.Where(w => w.TraderId == 1)); + // Registered in the system-level insider registry, NOT the user watchlist. + Assert.Single(db.InsiderWatches.Where(i => i.TraderId == 1)); + Assert.Empty(db.WatchlistEntries); Assert.Empty(db.Alerts.Where(a => a.Type == AlertType.InsiderActivity)); } @@ -58,13 +60,14 @@ public class AlertServiceTests 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 + // Already registered an hour ago; high-water mark predates the new trade. + db.InsiderWatches.Add(new InsiderWatch { - Id = 5, TraderId = 2, Label = "watched", AlertsEnabled = true, - AddedAt = DateTime.UtcNow.AddHours(-1) + Id = 3, TraderId = 2, + FirstDetectedAt = DateTime.UtcNow.AddHours(-1), + LastAlertedTradeAt = DateTime.UtcNow.AddHours(-1) }); - // A trade placed AFTER the entry was added -> should alert exactly once. + // A trade placed AFTER the high-water mark -> should alert exactly once. db.Trades.Add(new Trade { Id = 20, TraderId = 2, DbMarketId = 200, Platform = PlatformType.Polymarket, @@ -82,8 +85,8 @@ public class AlertServiceTests 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); + var watch = db.InsiderWatches.First(i => i.Id == 3); + Assert.True(watch.LastAlertedTradeAt > DateTime.UtcNow.AddMinutes(-25)); await svc.EvaluateInsiderWatchAsync(); Assert.Single(db.Alerts.Where(a => a.Type == AlertType.InsiderActivity)); diff --git a/src/Predictalytics.Application/DTOs/TraderDto.cs b/src/Predictalytics.Application/DTOs/TraderDto.cs index a98666a..173798e 100644 --- a/src/Predictalytics.Application/DTOs/TraderDto.cs +++ b/src/Predictalytics.Application/DTOs/TraderDto.cs @@ -147,3 +147,19 @@ public record TraderCorrelationDto( decimal IntersectionRatioB, decimal AgreementRatio ); + +/// One row of the dedicated Insider view (system-level, not a user watchlist). +public record InsiderDto( + int Id, + string Platform, + string DisplayName, + string PlatformUserId, + decimal CombinedScore, + decimal CopytradingCopyabilityScore, + decimal WinRate, + decimal TotalPnl, + int TotalTrades, + decimal SurpriseValue, + DateTime? FirstDetectedAt, + DateTime? LastPolledAt +); diff --git a/src/Predictalytics.Application/Services/AlertService.cs b/src/Predictalytics.Application/Services/AlertService.cs index 7e6e5cb..fa13043 100644 --- a/src/Predictalytics.Application/Services/AlertService.cs +++ b/src/Predictalytics.Application/Services/AlertService.cs @@ -15,7 +15,7 @@ public class AlertService : IAlertService private readonly IAlertRepository _alertRepo; private readonly ITradeRepository _tradeRepo; private readonly ITraderRepository _traderRepo; - private readonly IWatchlistRepository _watchlistRepo; + private readonly IInsiderWatchRepository _insiderRepo; private readonly ILogger _logger; // Alert thresholds (configurable in future) @@ -28,13 +28,13 @@ public class AlertService : IAlertService IAlertRepository alertRepo, ITradeRepository tradeRepo, ITraderRepository traderRepo, - IWatchlistRepository watchlistRepo, + IInsiderWatchRepository insiderRepo, ILogger logger) { _alertRepo = alertRepo; _tradeRepo = tradeRepo; _traderRepo = traderRepo; - _watchlistRepo = watchlistRepo; + _insiderRepo = insiderRepo; _logger = logger; } @@ -70,11 +70,12 @@ public class AlertService : IAlertService } /// - /// 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. + /// Insider-Follow feed: maintains the system-level registry for every + /// possible_insider wallet 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. + /// This is deliberately decoupled from (per-user) watchlists. Dedup is via + /// ; a freshly detected wallet is seeded at detection + /// time so historical (backfilled) trades never trigger a backlog of alerts. /// public async Task EvaluateInsiderWatchAsync(CancellationToken ct = default) { @@ -82,27 +83,24 @@ public class AlertService : IAlertService foreach (var trader in insiders) { - var entry = trader.WatchlistEntries.FirstOrDefault(); + var watch = await _insiderRepo.GetByTraderIdAsync(trader.Id, ct); - // Auto-add newly detected insiders; baseline at now so we don't alert on their history. - if (entry == null) + // Register newly detected insiders; seed the high-water mark at "now" so we don't alert + // on their history. No alerts on the detection cycle itself. + if (watch == null) { - await _watchlistRepo.AddAsync(new WatchlistEntry + await _insiderRepo.AddAsync(new InsiderWatch { TraderId = trader.Id, - Label = "Auto: Possible Insider", - Notes = "Automatisch aufgenommen (possible_insider-Trait).", - AlertsEnabled = true + FirstDetectedAt = DateTime.UtcNow, + LastAlertedTradeAt = DateTime.UtcNow }, ct); - _logger.LogInformation("👁 Insider-Watch: auto-added {Trader} to watchlist", trader.DisplayName); + _logger.LogInformation("👁 Insider-Watch: detected new possible-insider {Trader}", 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(); + var newTrades = recent.Where(t => t.ExecutedAt > watch.LastAlertedTradeAt).OrderBy(t => t.ExecutedAt).ToList(); if (newTrades.Count == 0) continue; foreach (var trade in newTrades) @@ -121,8 +119,8 @@ public class AlertService : IAlertService }, ct); } - entry.LastInsiderAlertAt = newTrades.Max(t => t.ExecutedAt); - await _watchlistRepo.UpdateAsync(entry, ct); + watch.LastAlertedTradeAt = newTrades.Max(t => t.ExecutedAt); + await _insiderRepo.UpdateAsync(watch, ct); } } diff --git a/src/Predictalytics.Domain/Entities/InsiderWatch.cs b/src/Predictalytics.Domain/Entities/InsiderWatch.cs new file mode 100644 index 0000000..3a3fd84 --- /dev/null +++ b/src/Predictalytics.Domain/Entities/InsiderWatch.cs @@ -0,0 +1,27 @@ +namespace Predictalytics.Domain.Entities; + +/// +/// System-level registry of wallets flagged with the possible_insider trait. This is a +/// global, system-owned list — deliberately separate from per-user +/// watchlists (which will become user-specific once the product is offered commercially). It backs +/// the dedicated "Insider" view and carries the high-water mark used to de-duplicate activity alerts. +/// +public class InsiderWatch +{ + public int Id { get; set; } + + /// Foreign key to the flagged trader (unique — one row per trader). + public int TraderId { get; set; } + + /// When this wallet was first detected as a possible insider. + public DateTime FirstDetectedAt { get; set; } = DateTime.UtcNow; + + /// + /// High-water mark for activity alerts: the ExecutedAt of the newest trade already alerted on. + /// Seeded at detection time so historical (backfilled) trades never trigger a backlog of alerts. + /// + public DateTime LastAlertedTradeAt { get; set; } + + // Navigation + public Trader Trader { get; set; } = null!; +} diff --git a/src/Predictalytics.Domain/Entities/WatchlistEntry.cs b/src/Predictalytics.Domain/Entities/WatchlistEntry.cs index 246e583..44aaf07 100644 --- a/src/Predictalytics.Domain/Entities/WatchlistEntry.cs +++ b/src/Predictalytics.Domain/Entities/WatchlistEntry.cs @@ -22,13 +22,6 @@ 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/Interfaces/IInsiderWatchRepository.cs b/src/Predictalytics.Domain/Interfaces/IInsiderWatchRepository.cs new file mode 100644 index 0000000..0f3cf1b --- /dev/null +++ b/src/Predictalytics.Domain/Interfaces/IInsiderWatchRepository.cs @@ -0,0 +1,11 @@ +using Predictalytics.Domain.Entities; + +namespace Predictalytics.Domain.Interfaces; + +public interface IInsiderWatchRepository +{ + Task> GetAllAsync(CancellationToken ct = default); + Task GetByTraderIdAsync(int traderId, CancellationToken ct = default); + Task AddAsync(InsiderWatch entry, CancellationToken ct = default); + Task UpdateAsync(InsiderWatch entry, CancellationToken ct = default); +} diff --git a/src/Predictalytics.Domain/Interfaces/IWatchlistRepository.cs b/src/Predictalytics.Domain/Interfaces/IWatchlistRepository.cs index 155c4d2..f9af932 100644 --- a/src/Predictalytics.Domain/Interfaces/IWatchlistRepository.cs +++ b/src/Predictalytics.Domain/Interfaces/IWatchlistRepository.cs @@ -7,6 +7,5 @@ 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/AppDbContext.cs b/src/Predictalytics.Infrastructure/Data/AppDbContext.cs index 25b8d49..6f85412 100644 --- a/src/Predictalytics.Infrastructure/Data/AppDbContext.cs +++ b/src/Predictalytics.Infrastructure/Data/AppDbContext.cs @@ -25,6 +25,7 @@ public class AppDbContext : DbContext public DbSet BackgroundJobs => Set(); public DbSet TraderTraits => Set(); public DbSet TraderWindowMetrics => Set(); + public DbSet InsiderWatches => Set(); private readonly bool _isReadOnly; @@ -207,6 +208,14 @@ public class AppDbContext : DbContext 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); + }); + // Alert mb.Entity(e => { diff --git a/src/Predictalytics.Infrastructure/Data/Repositories/InsiderWatchRepository.cs b/src/Predictalytics.Infrastructure/Data/Repositories/InsiderWatchRepository.cs new file mode 100644 index 0000000..9a5f894 --- /dev/null +++ b/src/Predictalytics.Infrastructure/Data/Repositories/InsiderWatchRepository.cs @@ -0,0 +1,23 @@ +using Predictalytics.Domain.Entities; +using Predictalytics.Domain.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace Predictalytics.Infrastructure.Data.Repositories; + +public class InsiderWatchRepository : IInsiderWatchRepository +{ + private readonly AppDbContext _db; + public InsiderWatchRepository(AppDbContext db) => _db = db; + + public async Task> GetAllAsync(CancellationToken ct = default) + => await _db.InsiderWatches.ToListAsync(ct); + + public async Task GetByTraderIdAsync(int traderId, CancellationToken ct = default) + => await _db.InsiderWatches.FirstOrDefaultAsync(i => i.TraderId == traderId, ct); + + public async Task AddAsync(InsiderWatch entry, CancellationToken ct = default) + { _db.InsiderWatches.Add(entry); await _db.SaveChangesAsync(ct); } + + public async Task UpdateAsync(InsiderWatch entry, CancellationToken ct = default) + { _db.InsiderWatches.Update(entry); await _db.SaveChangesAsync(ct); } +} diff --git a/src/Predictalytics.Infrastructure/Data/Repositories/TraderRepository.cs b/src/Predictalytics.Infrastructure/Data/Repositories/TraderRepository.cs index 56cc9d4..39b2989 100644 --- a/src/Predictalytics.Infrastructure/Data/Repositories/TraderRepository.cs +++ b/src/Predictalytics.Infrastructure/Data/Repositories/TraderRepository.cs @@ -48,7 +48,8 @@ public class TraderRepository : ITraderRepository public async Task> GetByTraitAsync(string trait, CancellationToken ct = default) => await _db.Traders .Include(t => t.Traits) - .Include(t => t.WatchlistEntries) + .Include(t => t.Analytics) + .Include(t => t.CurrentScore) .Where(t => t.Traits.Any(tr => tr.Trait == trait)) .ToListAsync(ct); diff --git a/src/Predictalytics.Infrastructure/Data/Repositories/WatchlistRepository.cs b/src/Predictalytics.Infrastructure/Data/Repositories/WatchlistRepository.cs index 24433a5..0b93763 100644 --- a/src/Predictalytics.Infrastructure/Data/Repositories/WatchlistRepository.cs +++ b/src/Predictalytics.Infrastructure/Data/Repositories/WatchlistRepository.cs @@ -21,9 +21,6 @@ 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/DependencyInjection.cs b/src/Predictalytics.Infrastructure/DependencyInjection.cs index c5a59ac..c5ffebb 100644 --- a/src/Predictalytics.Infrastructure/DependencyInjection.cs +++ b/src/Predictalytics.Infrastructure/DependencyInjection.cs @@ -66,6 +66,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Predictalytics.Infrastructure/Migrations/20260723164157_AddWatchlistLastInsiderAlertAt.cs b/src/Predictalytics.Infrastructure/Migrations/20260723164157_AddWatchlistLastInsiderAlertAt.cs deleted file mode 100644 index be26ca8..0000000 --- a/src/Predictalytics.Infrastructure/Migrations/20260723164157_AddWatchlistLastInsiderAlertAt.cs +++ /dev/null @@ -1,29 +0,0 @@ -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/20260723164157_AddWatchlistLastInsiderAlertAt.Designer.cs b/src/Predictalytics.Infrastructure/Migrations/20260723184034_AddInsiderWatch.Designer.cs similarity index 97% rename from src/Predictalytics.Infrastructure/Migrations/20260723164157_AddWatchlistLastInsiderAlertAt.Designer.cs rename to src/Predictalytics.Infrastructure/Migrations/20260723184034_AddInsiderWatch.Designer.cs index 9c280f7..bbddd53 100644 --- a/src/Predictalytics.Infrastructure/Migrations/20260723164157_AddWatchlistLastInsiderAlertAt.Designer.cs +++ b/src/Predictalytics.Infrastructure/Migrations/20260723184034_AddInsiderWatch.Designer.cs @@ -12,8 +12,8 @@ using Predictalytics.Infrastructure.Data; namespace Predictalytics.Infrastructure.Migrations { [DbContext(typeof(AppDbContext))] - [Migration("20260723164157_AddWatchlistLastInsiderAlertAt")] - partial class AddWatchlistLastInsiderAlertAt + [Migration("20260723184034_AddInsiderWatch")] + partial class AddInsiderWatch { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -181,6 +181,31 @@ namespace Predictalytics.Infrastructure.Migrations b.ToTable("Events"); }); + modelBuilder.Entity("Predictalytics.Domain.Entities.InsiderWatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("FirstDetectedAt") + .HasColumnType("datetime(6)"); + + b.Property("LastAlertedTradeAt") + .HasColumnType("datetime(6)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TraderId") + .IsUnique(); + + b.ToTable("InsiderWatches"); + }); + modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b => { b.Property("Id") @@ -1041,9 +1066,6 @@ namespace Predictalytics.Infrastructure.Migrations .HasMaxLength(256) .HasColumnType("varchar(256)"); - b.Property("LastInsiderAlertAt") - .HasColumnType("datetime(6)"); - b.Property("Notes") .HasColumnType("longtext"); @@ -1078,6 +1100,17 @@ namespace Predictalytics.Infrastructure.Migrations b.Navigation("Trader"); }); + modelBuilder.Entity("Predictalytics.Domain.Entities.InsiderWatch", b => + { + b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader") + .WithMany() + .HasForeignKey("TraderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trader"); + }); + modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b => { b.HasOne("Predictalytics.Domain.Entities.Event", "Event") diff --git a/src/Predictalytics.Infrastructure/Migrations/20260723184034_AddInsiderWatch.cs b/src/Predictalytics.Infrastructure/Migrations/20260723184034_AddInsiderWatch.cs new file mode 100644 index 0000000..65e8210 --- /dev/null +++ b/src/Predictalytics.Infrastructure/Migrations/20260723184034_AddInsiderWatch.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Predictalytics.Infrastructure.Migrations +{ + /// + public partial class AddInsiderWatch : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "InsiderWatches", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + TraderId = table.Column(type: "int", nullable: false), + FirstDetectedAt = table.Column(type: "datetime(6)", nullable: false), + LastAlertedTradeAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InsiderWatches", x => x.Id); + table.ForeignKey( + name: "FK_InsiderWatches_Traders_TraderId", + column: x => x.TraderId, + principalTable: "Traders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_InsiderWatches_TraderId", + table: "InsiderWatches", + column: "TraderId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "InsiderWatches"); + } + } +} diff --git a/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs index 76f9dce..05dd5b1 100644 --- a/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -178,6 +178,31 @@ namespace Predictalytics.Infrastructure.Migrations b.ToTable("Events"); }); + modelBuilder.Entity("Predictalytics.Domain.Entities.InsiderWatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("FirstDetectedAt") + .HasColumnType("datetime(6)"); + + b.Property("LastAlertedTradeAt") + .HasColumnType("datetime(6)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TraderId") + .IsUnique(); + + b.ToTable("InsiderWatches"); + }); + modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b => { b.Property("Id") @@ -1038,9 +1063,6 @@ namespace Predictalytics.Infrastructure.Migrations .HasMaxLength(256) .HasColumnType("varchar(256)"); - b.Property("LastInsiderAlertAt") - .HasColumnType("datetime(6)"); - b.Property("Notes") .HasColumnType("longtext"); @@ -1075,6 +1097,17 @@ namespace Predictalytics.Infrastructure.Migrations b.Navigation("Trader"); }); + modelBuilder.Entity("Predictalytics.Domain.Entities.InsiderWatch", b => + { + b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader") + .WithMany() + .HasForeignKey("TraderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trader"); + }); + modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b => { b.HasOne("Predictalytics.Domain.Entities.Event", "Event")