Rework insider feed: system-level InsiderWatch + dedicated view (no watchlist writes)

Watchlists will become per-user once the product is offered commercially, so the
system must not auto-add/remove traders there. Decouple insider tracking entirely:

- New system-owned entity InsiderWatch (TraderId unique, FirstDetectedAt,
  LastAlertedTradeAt) + IInsiderWatchRepository; migration AddInsiderWatch.
- AlertService.EvaluateInsiderWatchAsync now maintains InsiderWatch (not the
  watchlist): registers each possible_insider wallet, seeds the high-water mark
  at detection time, and fires one InsiderActivity alert per new trade. Dedup via
  LastAlertedTradeAt.
- Dedicated "Insider" view: GET /api/traders/insiders + InsiderDto + a new
  Insider-Radar page (sorted by market-surprise). Read-only, separate from watchlist.
- Revert the WatchlistEntry.LastInsiderAlertAt field + its migration (unapplied);
  drop the now-unused IWatchlistRepository.UpdateAsync.
- Tests updated to assert InsiderWatch registry (and that no WatchlistEntry is created).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
Richard
2026-07-23 20:52:37 +02:00
parent 8b9b34342f
commit 2bec11d0a9
19 changed files with 342 additions and 82 deletions
@@ -25,6 +25,7 @@ public class AppDbContext : DbContext
public DbSet<BackgroundJob> BackgroundJobs => Set<BackgroundJob>();
public DbSet<TraderTrait> TraderTraits => Set<TraderTrait>();
public DbSet<TraderWindowMetrics> TraderWindowMetrics => Set<TraderWindowMetrics>();
public DbSet<InsiderWatch> InsiderWatches => Set<InsiderWatch>();
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<InsiderWatch>(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<Alert>(e =>
{