Fingerprint-snapshot foundation + strategy-drift calculator (#3/#5 groundwork)

TraderAnalytics is one row per trader, overwritten every recalculation, so there
is no history to detect strategy drift (#3) or edge fade (#5) against. Add the
missing time series:

- TraderFingerprintSnapshot entity (score, category concentration, conviction,
  P50/P90 sizing, hold duration, trades/week, category-mix JSON, trait-set JSON)
  + migration AddFingerprintSnapshots (indexed by TraderId, CapturedAt).
- FingerprintSnapshotService (Infrastructure): CaptureDueAsync snapshots every
  copy-relevant trader (CopytradingScore >= 40) at most ~once/day; wired into
  ScoringAndAlertsWorker. GetDriftAsync reads latest-vs-baseline drift.
- FingerprintDriftCalculator (pure, Application): flags score drop, concentration
  shift, sizing jump, conviction sign-flip, category-mix TVD, trait-set change.
- GET /api/traders/{id}/fingerprint-drift?baselineDays=14 read endpoint.
- Tests: drift calculator (4 scenarios) + capture service (copy-relevance,
  throttle, drift read).

This is the shared foundation both #3 (drift alarm) and #5 (edge freshness) build on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
Richard
2026-07-23 21:21:21 +02:00
parent 2bec11d0a9
commit b21da1c2c7
13 changed files with 1957 additions and 0 deletions
@@ -26,6 +26,7 @@ public class AppDbContext : DbContext
public DbSet<TraderTrait> TraderTraits => Set<TraderTrait>();
public DbSet<TraderWindowMetrics> TraderWindowMetrics => Set<TraderWindowMetrics>();
public DbSet<InsiderWatch> InsiderWatches => Set<InsiderWatch>();
public DbSet<TraderFingerprintSnapshot> TraderFingerprintSnapshots => Set<TraderFingerprintSnapshot>();
private readonly bool _isReadOnly;
@@ -216,6 +217,14 @@ public class AppDbContext : DbContext
e.HasOne(i => i.Trader).WithMany().HasForeignKey(i => i.TraderId);
});
// TraderFingerprintSnapshot (time-series; many rows per trader)
mb.Entity<TraderFingerprintSnapshot>(e =>
{
e.HasKey(s => s.Id);
e.HasIndex(s => new { s.TraderId, s.CapturedAt });
e.HasOne(s => s.Trader).WithMany().HasForeignKey(s => s.TraderId);
});
// Alert
mb.Entity<Alert>(e =>
{