@
Insider-Follow feed: auto-watchlist possible-insider wallets + new-trade alerts 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 <noreply@anthropic.com> @
This commit is contained in:
@@ -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<AppDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options);
|
||||
|
||||
private static AlertService CreateService(AppDbContext db) => new(
|
||||
new AlertRepository(db),
|
||||
new TradeRepository(db, NullLogger<TradeRepository>.Instance),
|
||||
new TraderRepository(db),
|
||||
new WatchlistRepository(db),
|
||||
NullLogger<AlertService>.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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user