diff --git a/src/Predictalytics.Application.Tests/Services/RiskMetricsCalculatorTests.cs b/src/Predictalytics.Application.Tests/Services/RiskMetricsCalculatorTests.cs
new file mode 100644
index 0000000..5ce44d0
--- /dev/null
+++ b/src/Predictalytics.Application.Tests/Services/RiskMetricsCalculatorTests.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using Predictalytics.Application.Services;
+using Xunit;
+
+namespace Predictalytics.Application.Tests.Services;
+
+public class RiskMetricsCalculatorTests
+{
+ private static List<(DateTime, decimal)> Curve(params decimal[] pnls)
+ {
+ var list = new List<(DateTime, decimal)>();
+ var d = new DateTime(2026, 01, 01, 0, 0, 0, DateTimeKind.Utc);
+ foreach (var p in pnls) { list.Add((d, p)); d = d.AddDays(1); }
+ return list;
+ }
+
+ [Fact]
+ public void Empty_ReturnsZeros()
+ {
+ var m = RiskMetricsCalculator.Compute(new List<(DateTime, decimal)>());
+ Assert.Equal(0m, m.MaxDrawdownUsd);
+ Assert.Equal(0m, m.PnlVolatilityUsd);
+ Assert.Equal(0, m.LongestLosingStreakDays);
+ }
+
+ [Fact]
+ public void MonotonicUp_HasNoDrawdownNoStreak()
+ {
+ // Steady climb 0 -> 10 -> 20 -> 30: the ideal, most-copyable curve.
+ var m = RiskMetricsCalculator.Compute(Curve(0, 10, 20, 30));
+ Assert.Equal(0m, m.MaxDrawdownUsd);
+ Assert.Equal(0, m.LongestLosingStreakDays);
+ }
+
+ [Fact]
+ public void DipFromPeak_MeasuresPeakToTroughDrawdown()
+ {
+ // Peak 100, trough 40 -> max drawdown 60. Ends back at 90.
+ var m = RiskMetricsCalculator.Compute(Curve(0, 100, 70, 40, 90));
+ Assert.Equal(60m, m.MaxDrawdownUsd);
+ }
+
+ [Fact]
+ public void LongestLosingStreak_CountsConsecutiveDownDays()
+ {
+ // deltas: +100, -30, -30, -30, +50 -> 3 consecutive losing days
+ var m = RiskMetricsCalculator.Compute(Curve(0, 100, 70, 40, 10, 60));
+ Assert.Equal(3, m.LongestLosingStreakDays);
+ }
+
+ [Fact]
+ public void Volatility_IsZeroForConstantDailyGain_PositiveForBumpyPath()
+ {
+ // Constant +10/day -> zero volatility of daily returns.
+ var smooth = RiskMetricsCalculator.Compute(Curve(0, 10, 20, 30, 40));
+ Assert.Equal(0m, smooth.PnlVolatilityUsd);
+
+ // Same endpoint (+40) but a bumpy path -> positive volatility.
+ var bumpy = RiskMetricsCalculator.Compute(Curve(0, 50, 10, 60, 40));
+ Assert.True(bumpy.PnlVolatilityUsd > 0m);
+ }
+}
diff --git a/src/Predictalytics.Application/DTOs/TraderDto.cs b/src/Predictalytics.Application/DTOs/TraderDto.cs
index 249ec1b..8833d43 100644
--- a/src/Predictalytics.Application/DTOs/TraderDto.cs
+++ b/src/Predictalytics.Application/DTOs/TraderDto.cs
@@ -63,7 +63,13 @@ public record TraderDetailDto(
decimal MedianLossReturnPct,
decimal AvgLossReturnPct,
decimal? ProfitFactor,
-
+
+ // H1 Risk-adjusted return (equity-curve smoothness = copyability)
+ decimal MaxDrawdownUsd,
+ decimal PnlVolatilityUsd,
+ int LongestLosingStreakDays,
+ decimal? ReturnOverMaxDrawdown,
+
int Rank,
bool IsOnWatchlist,
DateTime CreatedAt,
diff --git a/src/Predictalytics.Application/Services/AnalyticsService.cs b/src/Predictalytics.Application/Services/AnalyticsService.cs
index 7d6b808..ceff508 100644
--- a/src/Predictalytics.Application/Services/AnalyticsService.cs
+++ b/src/Predictalytics.Application/Services/AnalyticsService.cs
@@ -255,6 +255,7 @@ public class AnalyticsService : IAnalyticsService
s?.ActivityScore ?? 0, s?.QualityScore ?? 0, s?.VolumeScore ?? 0, s?.TimingScore ?? 0,
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,
s?.Rank ?? 0, wl != null, trader.CreatedAt, trader.LastPolledAt,
trader.AiStrategySummary,
trades.Select(MapTradeDto).ToList(),
diff --git a/src/Predictalytics.Application/Services/RiskMetricsCalculator.cs b/src/Predictalytics.Application/Services/RiskMetricsCalculator.cs
new file mode 100644
index 0000000..bee8eb5
--- /dev/null
+++ b/src/Predictalytics.Application/Services/RiskMetricsCalculator.cs
@@ -0,0 +1,64 @@
+namespace Predictalytics.Application.Services;
+
+/// Risk/smoothness metrics derived from a trader's daily equity curve.
+public readonly record struct RiskMetrics(
+ decimal MaxDrawdownUsd,
+ decimal PnlVolatilityUsd,
+ int LongestLosingStreakDays);
+
+///
+/// Computes how SMOOTH a trader's path to their PnL was — a core copyability signal.
+/// Two traders with identical final PnL are very differently copyable if one got there
+/// steadily and the other via a violent up-and-down ride: the copier who joins mid-drawdown
+/// of the volatile trader may never recover. Pure function, no DB/API access.
+///
+public static class RiskMetricsCalculator
+{
+ /// Daily cumulative PnL points, ascending by date.
+ public static RiskMetrics Compute(IReadOnlyList<(DateTime Date, decimal TotalPnl)> snapshotsAsc)
+ {
+ if (snapshotsAsc is null || snapshotsAsc.Count == 0)
+ return new RiskMetrics(0m, 0m, 0);
+
+ // Max drawdown: largest peak-to-trough drop of the cumulative-PnL curve (in USD).
+ decimal peak = snapshotsAsc[0].TotalPnl;
+ decimal maxDrawdown = 0m;
+ foreach (var s in snapshotsAsc)
+ {
+ if (s.TotalPnl > peak) peak = s.TotalPnl;
+ var dd = peak - s.TotalPnl;
+ if (dd > maxDrawdown) maxDrawdown = dd;
+ }
+
+ // Day-over-day PnL deltas -> volatility (population stddev) + longest losing streak.
+ var deltas = new List(snapshotsAsc.Count);
+ int streak = 0, longestStreak = 0;
+ for (int i = 1; i < snapshotsAsc.Count; i++)
+ {
+ var d = snapshotsAsc[i].TotalPnl - snapshotsAsc[i - 1].TotalPnl;
+ deltas.Add(d);
+ if (d < 0)
+ {
+ streak++;
+ if (streak > longestStreak) longestStreak = streak;
+ }
+ else
+ {
+ streak = 0;
+ }
+ }
+
+ decimal volatility = 0m;
+ if (deltas.Count > 0)
+ {
+ var mean = deltas.Average();
+ var variance = deltas.Sum(x => (x - mean) * (x - mean)) / deltas.Count;
+ volatility = (decimal)System.Math.Sqrt((double)variance);
+ }
+
+ return new RiskMetrics(
+ System.Math.Round(maxDrawdown, 2),
+ System.Math.Round(volatility, 2),
+ longestStreak);
+ }
+}
diff --git a/src/Predictalytics.Domain/Entities/AnalyticsEntities.cs b/src/Predictalytics.Domain/Entities/AnalyticsEntities.cs
index 3e1888a..bb6ac5e 100644
--- a/src/Predictalytics.Domain/Entities/AnalyticsEntities.cs
+++ b/src/Predictalytics.Domain/Entities/AnalyticsEntities.cs
@@ -55,6 +55,15 @@ public class TraderAnalytics
public decimal MedianPostFillDriftPct { get; set; }
public decimal NetEdgeAfterFeesPct { get; set; }
+ // H1 Risk-adjusted return (from the daily equity curve; smoothness = copyability)
+ public decimal MaxDrawdownUsd { get; set; }
+ public decimal PnlVolatilityUsd { get; set; }
+ public int LongestLosingStreakDays { 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;
+
// Navigation
public virtual Trader Trader { get; set; } = null!;
}
diff --git a/src/Predictalytics.Infrastructure/Migrations/20260720071306_AddRiskAdjustedMetrics.Designer.cs b/src/Predictalytics.Infrastructure/Migrations/20260720071306_AddRiskAdjustedMetrics.Designer.cs
new file mode 100644
index 0000000..be64d97
--- /dev/null
+++ b/src/Predictalytics.Infrastructure/Migrations/20260720071306_AddRiskAdjustedMetrics.Designer.cs
@@ -0,0 +1,1284 @@
+//
+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("20260720071306_AddRiskAdjustedMetrics")]
+ partial class AddRiskAdjustedMetrics
+ {
+ ///
+ 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("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/20260720071306_AddRiskAdjustedMetrics.cs b/src/Predictalytics.Infrastructure/Migrations/20260720071306_AddRiskAdjustedMetrics.cs
new file mode 100644
index 0000000..bc34ae4
--- /dev/null
+++ b/src/Predictalytics.Infrastructure/Migrations/20260720071306_AddRiskAdjustedMetrics.cs
@@ -0,0 +1,51 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Predictalytics.Infrastructure.Migrations
+{
+ ///
+ public partial class AddRiskAdjustedMetrics : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "LongestLosingStreakDays",
+ table: "TraderAnalytics",
+ type: "int",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.AddColumn(
+ name: "MaxDrawdownUsd",
+ table: "TraderAnalytics",
+ type: "decimal(65,30)",
+ nullable: false,
+ defaultValue: 0m);
+
+ migrationBuilder.AddColumn(
+ name: "PnlVolatilityUsd",
+ table: "TraderAnalytics",
+ type: "decimal(65,30)",
+ nullable: false,
+ defaultValue: 0m);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "LongestLosingStreakDays",
+ table: "TraderAnalytics");
+
+ migrationBuilder.DropColumn(
+ name: "MaxDrawdownUsd",
+ table: "TraderAnalytics");
+
+ migrationBuilder.DropColumn(
+ name: "PnlVolatilityUsd",
+ table: "TraderAnalytics");
+ }
+ }
+}
diff --git a/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
index fc0128b..4ac709f 100644
--- a/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
+++ b/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
@@ -670,6 +670,12 @@ namespace Predictalytics.Infrastructure.Migrations
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)");
@@ -722,6 +728,9 @@ namespace Predictalytics.Infrastructure.Migrations
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
+ b.Property("PnlVolatilityUsd")
+ .HasColumnType("decimal(65,30)");
+
b.Property("PriceBandProfileJson")
.HasColumnType("longtext");
diff --git a/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs b/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs
index f2dd6ad..ecc9216 100644
--- a/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs
+++ b/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs
@@ -361,6 +361,20 @@ public class PositionPnLEngine : IPositionPnLEngine
// Count Trades30d
analytics.Trades30d = trades.Where(t => t.ExecutedAt >= cutoff30d).Sum(t => t.AggregatedCount ?? 1);
+ // H1: Risk-adjusted return from the daily equity curve (smoothness = copyability).
+ // Build the series from persisted history + today's freshly computed point.
+ var historicalSnapshots = await _db.TraderDailySnapshots
+ .Where(s => s.TraderId == traderId && s.Date < today)
+ .OrderBy(s => s.Date)
+ .Select(s => new { s.Date, s.TotalPnl })
+ .ToListAsync(ct);
+ var equitySeries = historicalSnapshots.Select(h => (h.Date, h.TotalPnl)).ToList();
+ equitySeries.Add((today, overallPnl));
+ var risk = Predictalytics.Application.Services.RiskMetricsCalculator.Compute(equitySeries);
+ analytics.MaxDrawdownUsd = risk.MaxDrawdownUsd;
+ analytics.PnlVolatilityUsd = risk.PnlVolatilityUsd;
+ analytics.LongestLosingStreakDays = risk.LongestLosingStreakDays;
+
// Calculate Win Rate and Return Pcts on Market level
var (winRateOverall, winRate30d, winRate7d, winRate24h,
medianWin, avgWin, medianLoss, avgLoss, profitFactor) =