@
#3 Strategy-drift alarm: fire + surface drift off the fingerprint history Turns the drift foundation into an actual alarm that protects copiers: - New AlertType.StrategyDrift (severity 3). - FingerprintSnapshotService.GetDriftedTradersAsync lists every master whose latest fingerprint drifted from its baseline. - AlertService.EvaluateStrategyDriftAsync (run from EvaluateAlertsAsync) fires a StrategyDrift alert per drifted master, summarizing the changed dimensions. Drift is slow-moving, so alerts are de-duplicated per trader over a 7-day cooldown via new IAlertRepository.ExistsRecentAsync. - UI: 📉 icon in the alert feed + a drift banner on the trader detail page (fetches /fingerprint-drift, lists the drifted dimensions). - Test: drift alert fires once then dedups within the cooldown. No schema change (reuses TraderFingerprintSnapshots + Alerts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
@@ -5,6 +5,7 @@ using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Infrastructure.Data;
|
||||
using Predictalytics.Infrastructure.Data.Repositories;
|
||||
using Predictalytics.Infrastructure.Services;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -24,6 +25,7 @@ public class AlertServiceTests
|
||||
new TradeRepository(db, NullLogger<TradeRepository>.Instance),
|
||||
new TraderRepository(db),
|
||||
new InsiderWatchRepository(db),
|
||||
new FingerprintSnapshotService(db, NullLogger<FingerprintSnapshotService>.Instance),
|
||||
NullLogger<AlertService>.Instance);
|
||||
|
||||
[Fact]
|
||||
@@ -91,4 +93,33 @@ public class AlertServiceTests
|
||||
await svc.EvaluateInsiderWatchAsync();
|
||||
Assert.Single(db.Alerts.Where(a => a.Type == AlertType.InsiderActivity));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StrategyDrift_AlertsOnce_ThenDedupsWithinCooldown()
|
||||
{
|
||||
using var db = CreateDbContext();
|
||||
db.Traders.Add(new Trader { Id = 9, PlatformUserId = "0xD", DisplayName = "DriftMaster" });
|
||||
// Baseline 20 days ago (score 78) vs latest now (score 40) -> clear score-drop drift.
|
||||
db.TraderFingerprintSnapshots.Add(new TraderFingerprintSnapshot
|
||||
{
|
||||
TraderId = 9, CapturedAt = DateTime.UtcNow.AddDays(-20), CopytradingScore = 78m
|
||||
});
|
||||
db.TraderFingerprintSnapshots.Add(new TraderFingerprintSnapshot
|
||||
{
|
||||
TraderId = 9, CapturedAt = DateTime.UtcNow, CopytradingScore = 40m
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var svc = CreateService(db);
|
||||
await svc.EvaluateStrategyDriftAsync();
|
||||
|
||||
var alerts = db.Alerts.Where(a => a.Type == AlertType.StrategyDrift).ToList();
|
||||
Assert.Single(alerts);
|
||||
Assert.Equal(9, alerts[0].TraderId);
|
||||
Assert.Equal(3, alerts[0].Severity);
|
||||
|
||||
// Within the cooldown, a second evaluation must not re-alert.
|
||||
await svc.EvaluateStrategyDriftAsync();
|
||||
Assert.Single(db.Alerts.Where(a => a.Type == AlertType.StrategyDrift));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user