diff --git a/src/Predictalytics.Application.Tests/Services/StrategyMetricsCalculatorTests.cs b/src/Predictalytics.Application.Tests/Services/StrategyMetricsCalculatorTests.cs
new file mode 100644
index 0000000..bbce8bb
--- /dev/null
+++ b/src/Predictalytics.Application.Tests/Services/StrategyMetricsCalculatorTests.cs
@@ -0,0 +1,74 @@
+using System.Collections.Generic;
+using Predictalytics.Application.Services;
+using Xunit;
+
+namespace Predictalytics.Application.Tests.Services;
+
+public class StrategyMetricsCalculatorTests
+{
+ // ── Concentration (Herfindahl) ────────────────────────────────────────────
+ [Fact]
+ public void Concentration_SingleCategory_IsOne()
+ {
+ Assert.Equal(1.0m, StrategyMetricsCalculator.ComputeConcentration(new[] { 500m }));
+ }
+
+ [Fact]
+ public void Concentration_TwoEqualCategories_IsHalf()
+ {
+ // shares 0.5, 0.5 -> 0.25 + 0.25 = 0.5
+ Assert.Equal(0.5m, StrategyMetricsCalculator.ComputeConcentration(new[] { 100m, 100m }));
+ }
+
+ [Fact]
+ public void Concentration_SpecialistScoresHigherThanGeneralist()
+ {
+ var specialist = StrategyMetricsCalculator.ComputeConcentration(new[] { 900m, 50m, 50m });
+ var generalist = StrategyMetricsCalculator.ComputeConcentration(new[] { 100m, 100m, 100m, 100m });
+ Assert.True(specialist > generalist);
+ }
+
+ [Fact]
+ public void Concentration_NoVolume_IsZero()
+ {
+ Assert.Equal(0m, StrategyMetricsCalculator.ComputeConcentration(new[] { 0m, 0m }));
+ }
+
+ // ── Conviction / sizing edge ──────────────────────────────────────────────
+ [Fact]
+ public void Conviction_TooFewMarkets_IsNull()
+ {
+ var markets = new List<(decimal, decimal)> { (10m, 5m), (20m, 5m), (30m, 5m) };
+ Assert.Null(StrategyMetricsCalculator.ComputeConvictionEdge(markets));
+ }
+
+ [Fact]
+ public void Conviction_BigBetsWinMore_IsPositive()
+ {
+ // small bets (invested 10-30) return ~0%, big bets (invested 100-120) return ~+40%
+ var markets = new List<(decimal Invested, decimal ReturnPct)>
+ {
+ (10m, 0m), (20m, 2m), (30m, -2m),
+ (50m, 5m), (60m, 3m), (70m, 4m),
+ (100m, 40m), (110m, 38m), (120m, 42m),
+ };
+ var edge = StrategyMetricsCalculator.ComputeConvictionEdge(markets);
+ Assert.NotNull(edge);
+ Assert.True(edge > 30m); // big third avg ~40 minus small third avg ~0
+ }
+
+ [Fact]
+ public void Conviction_OverbetsLosers_IsNegative()
+ {
+ // big bets LOSE, small bets win -> negative conviction (a red flag)
+ var markets = new List<(decimal Invested, decimal ReturnPct)>
+ {
+ (10m, 20m), (20m, 25m), (30m, 22m),
+ (50m, 5m), (60m, 3m), (70m, 4m),
+ (100m, -30m), (110m, -35m), (120m, -28m),
+ };
+ var edge = StrategyMetricsCalculator.ComputeConvictionEdge(markets);
+ Assert.NotNull(edge);
+ Assert.True(edge < 0m);
+ }
+}
diff --git a/src/Predictalytics.Application/DTOs/TraderDto.cs b/src/Predictalytics.Application/DTOs/TraderDto.cs
index 8833d43..a98666a 100644
--- a/src/Predictalytics.Application/DTOs/TraderDto.cs
+++ b/src/Predictalytics.Application/DTOs/TraderDto.cs
@@ -70,6 +70,10 @@ public record TraderDetailDto(
int LongestLosingStreakDays,
decimal? ReturnOverMaxDrawdown,
+ // H2 Strategy fingerprint
+ decimal CategoryConcentration,
+ decimal? ConvictionEdgePct,
+
int Rank,
bool IsOnWatchlist,
DateTime CreatedAt,
diff --git a/src/Predictalytics.Application/Services/AnalyticsService.cs b/src/Predictalytics.Application/Services/AnalyticsService.cs
index ceff508..5757743 100644
--- a/src/Predictalytics.Application/Services/AnalyticsService.cs
+++ b/src/Predictalytics.Application/Services/AnalyticsService.cs
@@ -256,6 +256,7 @@ public class AnalyticsService : IAnalyticsService
s?.CombinedScore ?? 0, a?.CopytradingScore ?? 0, a?.CopytradingQualityScore ?? 0, a?.CopytradingCopyabilityScore ?? 0,
a?.MedianWinReturnPct ?? 0, a?.AvgWinReturnPct ?? 0, a?.MedianLossReturnPct ?? 0, a?.AvgLossReturnPct ?? 0, a?.ProfitFactor,
a?.MaxDrawdownUsd ?? 0, a?.PnlVolatilityUsd ?? 0, a?.LongestLosingStreakDays ?? 0, a?.ReturnOverMaxDrawdown,
+ a?.CategoryConcentration ?? 0, a?.ConvictionEdgePct,
s?.Rank ?? 0, wl != null, trader.CreatedAt, trader.LastPolledAt,
trader.AiStrategySummary,
trades.Select(MapTradeDto).ToList(),
diff --git a/src/Predictalytics.Application/Services/StrategyMetricsCalculator.cs b/src/Predictalytics.Application/Services/StrategyMetricsCalculator.cs
new file mode 100644
index 0000000..de7805d
--- /dev/null
+++ b/src/Predictalytics.Application/Services/StrategyMetricsCalculator.cs
@@ -0,0 +1,51 @@
+namespace Predictalytics.Application.Services;
+
+///
+/// Deeper strategy-fingerprint metrics computed purely from a trader's closed markets and
+/// category mix — no new data collection required.
+///
+public static class StrategyMetricsCalculator
+{
+ ///
+ /// Category concentration via the Herfindahl–Hirschman Index over volume shares.
+ /// 1.0 = everything in a single category (specialist); approaches 1/n for an even spread
+ /// (generalist). Returns 0 when there is no volume. A specialist's edge is often more
+ /// trustworthy inside their niche and more suspect outside it.
+ ///
+ public static decimal ComputeConcentration(IEnumerable categoryVolumes)
+ {
+ var vols = categoryVolumes.Where(v => v > 0).ToList();
+ var total = vols.Sum();
+ if (total <= 0) return 0m;
+
+ decimal hhi = 0m;
+ foreach (var v in vols)
+ {
+ var share = v / total;
+ hhi += share * share;
+ }
+ return System.Math.Round(hhi, 4);
+ }
+
+ ///
+ /// Conviction / sizing edge: do the trader's BIGGEST bets outperform their smallest?
+ /// Splits closed markets into the top and bottom third by invested capital and returns
+ /// (avg return% of the big-bet third) − (avg return% of the small-bet third).
+ /// Positive ⇒ their sizing carries information (bigger conviction → better outcome), so a
+ /// copier should size-weight them; ≈0 ⇒ size is noise, copy flat; negative ⇒ they overbet
+ /// their losers (a red flag). Needs ≥ 6 closed markets; returns null otherwise.
+ ///
+ public static decimal? ComputeConvictionEdge(IReadOnlyList<(decimal Invested, decimal ReturnPct)> closedMarkets)
+ {
+ var valid = closedMarkets.Where(m => m.Invested > 0).OrderBy(m => m.Invested).ToList();
+ if (valid.Count < 6) return null;
+
+ int third = valid.Count / 3;
+ var smallBets = valid.Take(third).ToList();
+ var bigBets = valid.Skip(valid.Count - third).ToList();
+
+ var smallAvg = smallBets.Average(m => m.ReturnPct);
+ var bigAvg = bigBets.Average(m => m.ReturnPct);
+ return System.Math.Round(bigAvg - smallAvg, 2);
+ }
+}
diff --git a/src/Predictalytics.Domain/Entities/AnalyticsEntities.cs b/src/Predictalytics.Domain/Entities/AnalyticsEntities.cs
index bb6ac5e..14ad9b4 100644
--- a/src/Predictalytics.Domain/Entities/AnalyticsEntities.cs
+++ b/src/Predictalytics.Domain/Entities/AnalyticsEntities.cs
@@ -60,6 +60,12 @@ public class TraderAnalytics
public decimal PnlVolatilityUsd { get; set; }
public int LongestLosingStreakDays { get; set; }
+ // H2 Strategy fingerprint
+ /// Herfindahl index of category volume shares (0..1; 1 = single-category specialist).
+ public decimal CategoryConcentration { get; set; }
+ /// Return% of the biggest-bet third minus the smallest-bet third. Null = too few closed markets.
+ public decimal? ConvictionEdgePct { get; set; }
+
/// Calmar-like: profit per unit of worst drawdown. Null when there was no drawdown.
public decimal? ReturnOverMaxDrawdown =>
MaxDrawdownUsd > 0 ? System.Math.Round(OverallPnL / MaxDrawdownUsd, 2) : null;
diff --git a/src/Predictalytics.Infrastructure/Migrations/20260723071430_AddStrategyFingerprintMetrics.Designer.cs b/src/Predictalytics.Infrastructure/Migrations/20260723071430_AddStrategyFingerprintMetrics.Designer.cs
new file mode 100644
index 0000000..3ff8d64
--- /dev/null
+++ b/src/Predictalytics.Infrastructure/Migrations/20260723071430_AddStrategyFingerprintMetrics.Designer.cs
@@ -0,0 +1,1290 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Predictalytics.Infrastructure.Data;
+
+#nullable disable
+
+namespace Predictalytics.Infrastructure.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260723071430_AddStrategyFingerprintMetrics")]
+ partial class AddStrategyFingerprintMetrics
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("IsRead")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("varchar(4096)");
+
+ b.Property("Platform")
+ .HasColumnType("int");
+
+ b.Property("Severity")
+ .HasColumnType("int");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("varchar(512)");
+
+ b.Property("TraderId")
+ .HasColumnType("int");
+
+ b.Property("Type")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("TraderId");
+
+ b.ToTable("Alerts");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.BackgroundJob", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("CompletedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(4096)
+ .HasColumnType("varchar(4096)");
+
+ b.Property("JobType")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("varchar(64)");
+
+ b.Property("StartedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("varchar(64)");
+
+ b.Property("TraderId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("JobType");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("TraderId");
+
+ b.ToTable("BackgroundJobs");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.Event", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("DbCreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Description")
+ .HasMaxLength(4096)
+ .HasColumnType("varchar(4096)");
+
+ b.Property("EndDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("ImageUrl")
+ .HasMaxLength(1024)
+ .HasColumnType("varchar(1024)");
+
+ b.Property("IsActive")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("IsClosed")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("LastUpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Platform")
+ .HasColumnType("int");
+
+ b.Property("PlatformEventId")
+ .HasColumnType("bigint");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("varchar(512)");
+
+ b.Property("StartDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Tags")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("varchar(1024)");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("varchar(1024)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Platform", "PlatformEventId")
+ .IsUnique();
+
+ b.ToTable("Events");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Category")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("varchar(64)");
+
+ b.Property("ClosedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("ConditionId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("DbCreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Description")
+ .HasMaxLength(4096)
+ .HasColumnType("varchar(4096)");
+
+ b.Property("EndDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("EventId")
+ .HasColumnType("int");
+
+ b.Property("FeeRateBps")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("ImageUrl")
+ .HasMaxLength(1024)
+ .HasColumnType("varchar(1024)");
+
+ b.Property("IsNegRisk")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("IsResolved")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("LastTradesUpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("LastUpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Liquidity")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("MarketSlug")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("varchar(512)");
+
+ b.Property("Platform")
+ .HasColumnType("int");
+
+ b.Property("PlatformMarketId")
+ .HasColumnType("bigint");
+
+ b.Property("Question")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("varchar(1024)");
+
+ b.Property("QuestionId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("ResolutionOutcome")
+ .HasColumnType("longtext");
+
+ b.Property("StartDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Subcategory")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("varchar(128)");
+
+ b.Property("Volume")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("Volume24h")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EventId");
+
+ b.HasIndex("Platform", "PlatformMarketId")
+ .IsUnique();
+
+ b.ToTable("Markets");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
+ {
+ b.Property("MarketId")
+ .HasColumnType("int");
+
+ b.Property("AverageTradeSize")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("BotActivityScore")
+ .HasPrecision(8, 4)
+ .HasColumnType("decimal(8,4)");
+
+ b.Property("LastCalculatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("UniqueTradersCount")
+ .HasColumnType("int");
+
+ b.HasKey("MarketId");
+
+ b.ToTable("MarketAnalytics");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("CurrentPrice")
+ .HasPrecision(18, 8)
+ .HasColumnType("decimal(18,8)");
+
+ b.Property("Label")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("MarketId")
+ .HasColumnType("int");
+
+ b.Property("OutcomeIndex")
+ .HasColumnType("int");
+
+ b.Property("TokenId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TokenId");
+
+ b.HasIndex("MarketId", "OutcomeIndex")
+ .IsUnique();
+
+ b.ToTable("MarketOutcomes");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcomePriceSnapshot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("MarketOutcomeId")
+ .HasColumnType("int");
+
+ b.Property("Price")
+ .HasPrecision(10, 6)
+ .HasColumnType("decimal(10,6)");
+
+ b.Property("Timestamp")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MarketOutcomeId", "Timestamp");
+
+ b.ToTable("MarketOutcomePriceSnapshots");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.PlatformConfig", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("int");
+
+ b.Property("BaseUrl")
+ .HasMaxLength(1024)
+ .HasColumnType("varchar(1024)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("IsActive")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("varchar(128)");
+
+ b.Property("SettingsJson")
+ .HasColumnType("longtext");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("Id");
+
+ b.ToTable("PlatformConfigs");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("AggregatedCount")
+ .HasColumnType("int");
+
+ b.Property("Amount")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("AssetId")
+ .IsRequired()
+ .HasMaxLength(80)
+ .HasColumnType("varchar(80)");
+
+ b.Property("DbMarketId")
+ .HasColumnType("int");
+
+ b.Property("ExecutedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("IsContextEnriched")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("MarketId")
+ .IsRequired()
+ .HasMaxLength(66)
+ .HasColumnType("varchar(66)");
+
+ b.Property("MarketOutcomeId")
+ .HasColumnType("int");
+
+ b.Property("Outcome")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("varchar(128)");
+
+ b.Property("OutcomeIndex")
+ .HasColumnType("int");
+
+ b.Property("Platform")
+ .HasColumnType("int");
+
+ b.Property("PlatformTradeId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("PostTradePrice1m")
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("PreTradePrice1m")
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("Price")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("Side")
+ .HasColumnType("int");
+
+ b.Property("Size")
+ .HasPrecision(14, 6)
+ .HasColumnType("decimal(14,6)");
+
+ b.Property("TraderId")
+ .HasColumnType("int");
+
+ b.Property("TransactionHash")
+ .HasMaxLength(66)
+ .HasColumnType("varchar(66)");
+
+ b.Property("UsdcSize")
+ .HasColumnType("decimal(18,6)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AssetId");
+
+ b.HasIndex("DbMarketId");
+
+ b.HasIndex("ExecutedAt");
+
+ b.HasIndex("MarketOutcomeId");
+
+ b.HasIndex("TraderId");
+
+ b.HasIndex("Platform", "PlatformTradeId")
+ .IsUnique();
+
+ b.ToTable("Trades");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TradeContext", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("EstimatedOrderType")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("varchar(32)");
+
+ b.Property("EstimatedSlippage")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("FollowerFillPrice10s")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("FollowerFillPrice60s")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("PriceAfter1m")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("PriceBefore1m")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("TradeId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TradeId")
+ .IsUnique();
+
+ b.ToTable("TradeContexts");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("AiStrategySummary")
+ .HasColumnType("longtext");
+
+ b.Property("AiStrategyUpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("IngestMode")
+ .HasColumnType("int");
+
+ b.Property("IsAutoDiscovered")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("IsInitialImportComplete")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("IsSuspectedBot")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("LastAnalyzedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("LastApiErrorAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("LastPolledAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("LastTradesUpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("ManualPriorityOverride")
+ .HasColumnType("int");
+
+ b.Property("MasterStatus")
+ .HasColumnType("int");
+
+ b.Property("Notes")
+ .HasColumnType("longtext");
+
+ b.Property("Platform")
+ .HasColumnType("int");
+
+ b.Property("PlatformUserId")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("varchar(128)");
+
+ b.Property("Strategy")
+ .HasColumnType("int");
+
+ b.Property("Tier")
+ .HasColumnType("int");
+
+ b.Property("TotalPnl")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("TotalTrades")
+ .HasColumnType("int");
+
+ b.Property("WinRate")
+ .HasPrecision(8, 4)
+ .HasColumnType("decimal(8,4)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Platform", "PlatformUserId")
+ .IsUnique();
+
+ b.ToTable("Traders");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
+ {
+ b.Property("TraderId")
+ .HasColumnType("int");
+
+ b.Property("AvgLossReturnPct")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("AvgWinReturnPct")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("CategoryConcentration")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("ConvictionEdgePct")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("CopytradingCopyabilityScore")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("CopytradingQualityScore")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("CopytradingScore")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("CurrentBalance")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("EstimatedBankroll")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("LastCalculatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("LongestLosingStreakDays")
+ .HasColumnType("int");
+
+ b.Property("MaxDrawdownUsd")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("MedianHoldDurationHours")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("MedianLossReturnPct")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("MedianMarketVolumeUsd")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("MedianPostFillDriftPct")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("MedianWinReturnPct")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("NetEdgeAfterFeesPct")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("OverallPnL")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("OverallWinRate")
+ .HasPrecision(8, 4)
+ .HasColumnType("decimal(8,4)");
+
+ b.Property("P50PositionSize")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("P90PositionSize")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("PnL24h")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("PnL30d")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("PnL7d")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("PnlVolatilityUsd")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("PriceBandProfileJson")
+ .HasColumnType("longtext");
+
+ b.Property("ProfitFactor")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("Trades30d")
+ .HasColumnType("int");
+
+ b.Property("TradesPerWeek")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("WinRate24h")
+ .HasPrecision(8, 4)
+ .HasColumnType("decimal(8,4)");
+
+ b.Property("WinRate30d")
+ .HasPrecision(8, 4)
+ .HasColumnType("decimal(8,4)");
+
+ b.Property("WinRate7d")
+ .HasPrecision(8, 4)
+ .HasColumnType("decimal(8,4)");
+
+ b.HasKey("TraderId");
+
+ b.ToTable("TraderAnalytics");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderCategoryPerformance", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Category")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("varchar(64)");
+
+ b.Property("Subcategory")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("varchar(128)");
+
+ b.Property("TotalInvested")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("TotalPnL")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("TotalTrades")
+ .HasColumnType("int");
+
+ b.Property("TotalVolume")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("TraderId")
+ .HasColumnType("int");
+
+ b.Property("WinningTrades")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TraderId", "Category", "Subcategory")
+ .IsUnique();
+
+ b.ToTable("TraderCategoryPerformances");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderDailySnapshot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("CurrentBalance")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("Date")
+ .HasColumnType("datetime(6)");
+
+ b.Property("TotalPnl")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("TraderId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TraderId", "Date")
+ .IsUnique();
+
+ b.ToTable("TraderDailySnapshots");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("AvgCost")
+ .HasPrecision(10, 6)
+ .HasColumnType("decimal(10,6)");
+
+ b.Property("IsHistoryPruned")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("LastAppliedTradeId")
+ .HasColumnType("bigint");
+
+ b.Property("LastTradeExecutedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("LastUpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("MarketOutcomeId")
+ .HasColumnType("int");
+
+ b.Property("RealizedPnl")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("SharesHeld")
+ .HasPrecision(14, 6)
+ .HasColumnType("decimal(14,6)");
+
+ b.Property("TraderId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MarketOutcomeId");
+
+ b.HasIndex("TraderId", "MarketOutcomeId")
+ .IsUnique();
+
+ b.ToTable("TraderPositions");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("ActivityScore")
+ .HasPrecision(5, 2)
+ .HasColumnType("decimal(5,2)");
+
+ b.Property("CalculatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("CombinedScore")
+ .HasPrecision(5, 2)
+ .HasColumnType("decimal(5,2)");
+
+ b.Property("QualityScore")
+ .HasPrecision(5, 2)
+ .HasColumnType("decimal(5,2)");
+
+ b.Property("Rank")
+ .HasColumnType("int");
+
+ b.Property("TimingScore")
+ .HasPrecision(5, 2)
+ .HasColumnType("decimal(5,2)");
+
+ b.Property("TraderId")
+ .HasColumnType("int");
+
+ b.Property("VolumeScore")
+ .HasPrecision(5, 2)
+ .HasColumnType("decimal(5,2)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TraderId")
+ .IsUnique();
+
+ b.ToTable("TraderScores");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderTrait", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("ComputedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("TraderId")
+ .HasColumnType("int");
+
+ b.Property("Trait")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("varchar(64)");
+
+ b.Property("Value")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TraderId", "Trait")
+ .IsUnique();
+
+ b.ToTable("TraderTraits");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderWindowMetrics", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("AvgReturnPct")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("ClosedMarkets")
+ .HasColumnType("int");
+
+ b.Property("ComputedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("MedianLossReturnPct")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("MedianWinReturnPct")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("ProfitFactor")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("TraderId")
+ .HasColumnType("int");
+
+ b.Property("WinRate")
+ .HasPrecision(8, 4)
+ .HasColumnType("decimal(8,4)");
+
+ b.Property("WindowEnd")
+ .HasColumnType("datetime(6)");
+
+ b.Property("WindowStart")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TraderId", "WindowStart", "WindowEnd")
+ .IsUnique();
+
+ b.ToTable("TraderWindowMetrics");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("AddedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("AlertsEnabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Label")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("Notes")
+ .HasColumnType("longtext");
+
+ b.Property("TraderId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TraderId")
+ .IsUnique();
+
+ b.ToTable("WatchlistEntries");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
+ .WithMany()
+ .HasForeignKey("TraderId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.Navigation("Trader");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.BackgroundJob", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
+ .WithMany()
+ .HasForeignKey("TraderId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.Navigation("Trader");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Event", "Event")
+ .WithMany("Markets")
+ .HasForeignKey("EventId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Event");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
+ .WithOne("Analytics")
+ .HasForeignKey("Predictalytics.Domain.Entities.MarketAnalytics", "MarketId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Market");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
+ .WithMany("Outcomes")
+ .HasForeignKey("MarketId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Market");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcomePriceSnapshot", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
+ .WithMany()
+ .HasForeignKey("MarketOutcomeId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("MarketOutcome");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Market", "DbMarket")
+ .WithMany()
+ .HasForeignKey("DbMarketId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
+ .WithMany()
+ .HasForeignKey("MarketOutcomeId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
+ .WithMany("Trades")
+ .HasForeignKey("TraderId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("DbMarket");
+
+ b.Navigation("MarketOutcome");
+
+ b.Navigation("Trader");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TradeContext", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Trade", "Trade")
+ .WithOne("Context")
+ .HasForeignKey("Predictalytics.Domain.Entities.TradeContext", "TradeId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Trade");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
+ .WithOne("Analytics")
+ .HasForeignKey("Predictalytics.Domain.Entities.TraderAnalytics", "TraderId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Trader");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderCategoryPerformance", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
+ .WithMany("CategoryPerformances")
+ .HasForeignKey("TraderId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Trader");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderDailySnapshot", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
+ .WithMany()
+ .HasForeignKey("TraderId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Trader");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
+ .WithMany()
+ .HasForeignKey("MarketOutcomeId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
+ .WithMany("Positions")
+ .HasForeignKey("TraderId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("MarketOutcome");
+
+ b.Navigation("Trader");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
+ .WithOne("CurrentScore")
+ .HasForeignKey("Predictalytics.Domain.Entities.TraderScore", "TraderId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Trader");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderTrait", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
+ .WithMany("Traits")
+ .HasForeignKey("TraderId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Trader");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.TraderWindowMetrics", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
+ .WithMany()
+ .HasForeignKey("TraderId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Trader");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
+ {
+ b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
+ .WithMany("WatchlistEntries")
+ .HasForeignKey("TraderId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Trader");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.Event", b =>
+ {
+ b.Navigation("Markets");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
+ {
+ b.Navigation("Analytics");
+
+ b.Navigation("Outcomes");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
+ {
+ b.Navigation("Context");
+ });
+
+ modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
+ {
+ b.Navigation("Analytics");
+
+ b.Navigation("CategoryPerformances");
+
+ b.Navigation("CurrentScore");
+
+ b.Navigation("Positions");
+
+ b.Navigation("Trades");
+
+ b.Navigation("Traits");
+
+ b.Navigation("WatchlistEntries");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Predictalytics.Infrastructure/Migrations/20260723071430_AddStrategyFingerprintMetrics.cs b/src/Predictalytics.Infrastructure/Migrations/20260723071430_AddStrategyFingerprintMetrics.cs
new file mode 100644
index 0000000..3b521c9
--- /dev/null
+++ b/src/Predictalytics.Infrastructure/Migrations/20260723071430_AddStrategyFingerprintMetrics.cs
@@ -0,0 +1,39 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Predictalytics.Infrastructure.Migrations
+{
+ ///
+ public partial class AddStrategyFingerprintMetrics : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "CategoryConcentration",
+ table: "TraderAnalytics",
+ type: "decimal(65,30)",
+ nullable: false,
+ defaultValue: 0m);
+
+ migrationBuilder.AddColumn(
+ name: "ConvictionEdgePct",
+ table: "TraderAnalytics",
+ type: "decimal(65,30)",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "CategoryConcentration",
+ table: "TraderAnalytics");
+
+ migrationBuilder.DropColumn(
+ name: "ConvictionEdgePct",
+ table: "TraderAnalytics");
+ }
+ }
+}
diff --git a/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
index 4ac709f..26bc72c 100644
--- a/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
+++ b/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
@@ -652,6 +652,12 @@ namespace Predictalytics.Infrastructure.Migrations
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
+ b.Property("CategoryConcentration")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("ConvictionEdgePct")
+ .HasColumnType("decimal(65,30)");
+
b.Property("CopytradingCopyabilityScore")
.HasColumnType("decimal(65,30)");
diff --git a/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs b/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs
index ecc9216..4f871cf 100644
--- a/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs
+++ b/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs
@@ -376,20 +376,26 @@ public class PositionPnLEngine : IPositionPnLEngine
analytics.LongestLosingStreakDays = risk.LongestLosingStreakDays;
// Calculate Win Rate and Return Pcts on Market level
- var (winRateOverall, winRate30d, winRate7d, winRate24h,
- medianWin, avgWin, medianLoss, avgLoss, profitFactor) =
- CalculateMarketWinRates(trades, tempPositions, cutoff30d, cutoff7d, cutoff24h);
+ var (winRateOverall, winRate30d, winRate7d, winRate24h,
+ medianWin, avgWin, medianLoss, avgLoss, profitFactor) =
+ CalculateMarketWinRates(trades, tempPositions, cutoff30d, cutoff7d, cutoff24h,
+ out var closedMarketReturns);
analytics.OverallWinRate = winRateOverall;
analytics.WinRate30d = winRate30d;
analytics.WinRate7d = winRate7d;
analytics.WinRate24h = winRate24h;
-
+
analytics.MedianWinReturnPct = medianWin;
analytics.AvgWinReturnPct = avgWin;
analytics.MedianLossReturnPct = medianLoss;
analytics.AvgLossReturnPct = avgLoss;
analytics.ProfitFactor = profitFactor;
+
+ // H2: conviction/sizing edge — do the biggest bets outperform the smallest?
+ analytics.ConvictionEdgePct =
+ Predictalytics.Application.Services.StrategyMetricsCalculator.ComputeConvictionEdge(closedMarketReturns);
+
analytics.LastCalculatedAt = DateTime.UtcNow;
// Sync back to Trader record for quick sorting / UI display
@@ -411,7 +417,12 @@ public class PositionPnLEngine : IPositionPnLEngine
.ToDictionaryAsync(tcp => (tcp.Category, tcp.Subcategory), ct);
var newCatPerf = CalculateCategoryPerformances(trades, tempPositions);
-
+
+ // H2: how concentrated is the trader across categories (specialist vs generalist)?
+ analytics.CategoryConcentration =
+ Predictalytics.Application.Services.StrategyMetricsCalculator.ComputeConcentration(
+ newCatPerf.Values.Select(p => p.TotalVolume));
+
foreach (var kvp in newCatPerf)
{
if (existingCatPerf.TryGetValue(kvp.Key, out var existing))
@@ -446,8 +457,10 @@ public class PositionPnLEngine : IPositionPnLEngine
Dictionary finalPositions,
DateTime cutoff30d,
DateTime cutoff7d,
- DateTime cutoff24h)
+ DateTime cutoff24h,
+ out List<(decimal Invested, decimal ReturnPct)> closedMarketReturns)
{
+ closedMarketReturns = new List<(decimal, decimal)>();
// Group trades by Market
var tradesByMarket = trades
.Where(t => t.DbMarketId.HasValue || !string.IsNullOrEmpty(t.MarketId))
@@ -496,12 +509,13 @@ public class PositionPnLEngine : IPositionPnLEngine
if (invested > 0)
{
var returnPct = marketPnl / invested * 100m;
- if (returnPct > 0)
+ closedMarketReturns.Add((invested, returnPct));
+ if (returnPct > 0)
{
winReturns.Add(returnPct);
totalGrossWins += marketPnl;
}
- else if (returnPct < 0)
+ else if (returnPct < 0)
{
lossReturns.Add(returnPct);
totalGrossLosses += Math.Abs(marketPnl);