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
@@ -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);