diff --git a/src/Predictalytics.Api/wwwroot/index.html b/src/Predictalytics.Api/wwwroot/index.html index 9fb5860..c7d85d7 100644 --- a/src/Predictalytics.Api/wwwroot/index.html +++ b/src/Predictalytics.Api/wwwroot/index.html @@ -546,6 +546,7 @@ Win Rate PnL Volumen + Avg. Return Trades diff --git a/src/Predictalytics.Api/wwwroot/js/app.js b/src/Predictalytics.Api/wwwroot/js/app.js index b1d184a..16b7501 100644 --- a/src/Predictalytics.Api/wwwroot/js/app.js +++ b/src/Predictalytics.Api/wwwroot/js/app.js @@ -658,11 +658,12 @@ async function viewTrader(id) { ${fmt.pct(c.winRate)} ${fmt.pnl(c.totalPnL)} ${fmt.usd(c.totalVolume)} + ${(c.avgReturnPct || 0).toFixed(1)}% ${fmt.num(c.totalTrades)} `).join(''); } else { - catBody.innerHTML = 'No category data available'; + catBody.innerHTML = 'No category data available'; } const tbody = document.getElementById('td-tradesBody'); diff --git a/src/Predictalytics.Application.Tests/Services/PositionPnLEngineTests.cs b/src/Predictalytics.Application.Tests/Services/PositionPnLEngineTests.cs index a099585..3b273ee 100644 --- a/src/Predictalytics.Application.Tests/Services/PositionPnLEngineTests.cs +++ b/src/Predictalytics.Application.Tests/Services/PositionPnLEngineTests.cs @@ -881,4 +881,61 @@ public class PositionPnLEngineTests Assert.Equal(6, analytics.Trades30d); } } + + [Fact] + public async Task RecalculateTraderPositionsAsync_CalculatesCategoryTotalInvestedAndROI() + { + // Arrange + var dbName = Guid.NewGuid().ToString(); + using (var db = CreateDbContext(dbName)) + { + var trader = new Trader { Id = 1, PlatformUserId = "0x1", DisplayName = "Trader 1" }; + var ev = new Event { Id = 2, Platform = PlatformType.Polymarket, Slug = "e2", Title = "E2", Tags = "Crypto" }; + db.Set().Add(ev); + + var market = new Market { Id = 20, EventId = 2, PlatformMarketId = 2L, Question = "BTC to 100k?", Category = MarketCategory.Crypto, Subcategory = "Bitcoin", IsResolved = true }; + var outcome = new MarketOutcome { Id = 200, MarketId = 20, Label = "Yes", TokenId = "t200", CurrentPrice = 1.00m }; + market.Outcomes.Add(outcome); + + db.Traders.Add(trader); + db.Markets.Add(market); + + // Buy trade (Invested = 100 USD) + db.Trades.Add(new Trade + { + Id = 601, TraderId = 1, DbMarketId = 20, MarketOutcomeId = 200, + Side = TradeSide.Buy, Price = 0.50m, Size = 200m, Amount = 100m, + ExecutedAt = DateTime.UtcNow.AddMinutes(-10), + MarketOutcome = outcome + }); + + // Sell trade (Payout = 200 USD -> PnL = 100 USD) + db.Trades.Add(new Trade + { + Id = 602, TraderId = 1, DbMarketId = 20, MarketOutcomeId = 200, + Side = TradeSide.Sell, Price = 1.00m, Size = 200m, Amount = 200m, + ExecutedAt = DateTime.UtcNow, + MarketOutcome = outcome + }); + + await db.SaveChangesAsync(); + } + + // Act + using (var db = CreateDbContext(dbName)) + { + var pnlEngine = new PositionPnLEngine(db, NullLogger.Instance); + await pnlEngine.RecalculateTraderPositionsAsync(1); + } + + // Assert + using (var db = CreateDbContext(dbName)) + { + var perf = await db.TraderCategoryPerformances.FirstOrDefaultAsync(p => p.TraderId == 1 && p.Category == MarketCategory.Crypto); + Assert.NotNull(perf); + Assert.Equal(100m, perf.TotalInvested); + Assert.Equal(100m, perf.TotalPnL); + Assert.Equal(100m, perf.AvgReturnPct); // (100 / 100) * 100 = 100% ROI + } + } } diff --git a/src/Predictalytics.Application.Tests/Services/TradeRepositoryTests.cs b/src/Predictalytics.Application.Tests/Services/TradeRepositoryTests.cs new file mode 100644 index 0000000..853d640 --- /dev/null +++ b/src/Predictalytics.Application.Tests/Services/TradeRepositoryTests.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Predictalytics.Domain.Entities; +using Predictalytics.Domain.Enums; +using Predictalytics.Infrastructure.Data; +using Predictalytics.Infrastructure.Data.Repositories; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Predictalytics.Application.Tests.Services; + +public class TradeRepositoryTests +{ + [Fact] + public async Task AddRangeAsync_GeneratesCorrectSqlAndParameters() + { + using var connection = new SqliteConnection("DataSource=:memory:"); + connection.Open(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + using var db = new AppDbContext(options); + db.Database.EnsureCreated(); + + var repo = new TradeRepository(db, NullLogger.Instance); + + var trade = new Trade + { + PlatformTradeId = "test_tx_usdc", + MarketId = "cond_1", + AssetId = "asset_1", + Outcome = "Yes", + Side = TradeSide.Buy, + Price = 0.50m, + Size = 100m, + Amount = 50m, + ExecutedAt = DateTime.UtcNow, + UsdcSize = 123.45m, + OutcomeIndex = 1 + }; + + // SQLite will throw due to MySql specific "ON DUPLICATE KEY UPDATE" syntax. + // We catch it and verify that it failed at execution rather than mapping/parameter building. + var exception = await Assert.ThrowsAnyAsync(() => repo.AddRangeAsync(new[] { trade })); + + // Assert that the exception is a syntax error near "DUPLICATE", which verifies the query was correctly built and sent + Assert.Contains("DUPLICATE", exception.Message, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/Predictalytics.Application/DTOs/TraderDto.cs b/src/Predictalytics.Application/DTOs/TraderDto.cs index 7a7d580..249ec1b 100644 --- a/src/Predictalytics.Application/DTOs/TraderDto.cs +++ b/src/Predictalytics.Application/DTOs/TraderDto.cs @@ -85,7 +85,8 @@ public record TraderCategoryPerformanceDto( decimal TotalPnL, int TotalTrades, int WinningTrades, - decimal WinRate + decimal WinRate, + decimal AvgReturnPct ); public record TraderPositionDto( diff --git a/src/Predictalytics.Application/Services/AnalyticsService.cs b/src/Predictalytics.Application/Services/AnalyticsService.cs index 87b9fb4..7d6b808 100644 --- a/src/Predictalytics.Application/Services/AnalyticsService.cs +++ b/src/Predictalytics.Application/Services/AnalyticsService.cs @@ -243,7 +243,8 @@ public class AnalyticsService : IAnalyticsService p.TotalPnL, p.TotalTrades, p.WinningTrades, - p.WinRate)).ToList(); + p.WinRate, + p.AvgReturnPct)).ToList(); return new TraderDetailDto(trader.Id, trader.Platform.ToString(), trader.PlatformUserId, trader.DisplayName, trader.Notes, trader.Tier.ToString(), trader.Strategy.ToString(), trader.IsSuspectedBot, trader.ManualPriorityOverride, diff --git a/src/Predictalytics.Domain/Entities/Trade.cs b/src/Predictalytics.Domain/Entities/Trade.cs index 2e94fcb..9757d9a 100644 --- a/src/Predictalytics.Domain/Entities/Trade.cs +++ b/src/Predictalytics.Domain/Entities/Trade.cs @@ -66,6 +66,13 @@ public class Trade /// Transaction hash (for blockchain-based platforms). public string? TransactionHash { get; set; } + /// USDC size of the trade (raw currency size traded). + [Column(TypeName = "decimal(18,6)")] + public decimal? UsdcSize { get; set; } + + /// Outcome index traded. + public int? OutcomeIndex { get; set; } + // ── Context Enrichment (AI Strategy Detection) ─────────── /// Market price 1 minute before trade execution. diff --git a/src/Predictalytics.Domain/Entities/TraderCategoryPerformance.cs b/src/Predictalytics.Domain/Entities/TraderCategoryPerformance.cs index e1c0b97..b453e2d 100644 --- a/src/Predictalytics.Domain/Entities/TraderCategoryPerformance.cs +++ b/src/Predictalytics.Domain/Entities/TraderCategoryPerformance.cs @@ -20,6 +20,9 @@ public class TraderCategoryPerformance /// Total Profit/Loss in this category (USD). public decimal TotalPnL { get; set; } + /// Total invested amount in this category (USD). + public decimal TotalInvested { get; set; } + /// Number of trades in this category. public int TotalTrades { get; set; } @@ -28,4 +31,7 @@ public class TraderCategoryPerformance /// Calculated win rate for this category (0.0 - 1.0). public decimal WinRate => TotalTrades > 0 ? (decimal)WinningTrades / TotalTrades : 0; + + /// Calculated ROI percentage (Total PnL / Total Invested * 100). + public decimal AvgReturnPct => TotalInvested > 0 ? (TotalPnL / TotalInvested) * 100m : 0; } diff --git a/src/Predictalytics.Infrastructure/Data/Repositories/TradeRepository.cs b/src/Predictalytics.Infrastructure/Data/Repositories/TradeRepository.cs index c194ce8..7422d5d 100644 --- a/src/Predictalytics.Infrastructure/Data/Repositories/TradeRepository.cs +++ b/src/Predictalytics.Infrastructure/Data/Repositories/TradeRepository.cs @@ -70,14 +70,14 @@ public class TradeRepository : ITradeRepository foreach (var chunk in tradeList.Chunk(500)) { - var sb = new System.Text.StringBuilder("INSERT INTO Trades (PlatformTradeId, MarketId, AssetId, Outcome, Side, Price, Size, Amount, ExecutedAt, TransactionHash, TraderId, MarketOutcomeId, DbMarketId, Platform, IsContextEnriched, AggregatedCount) VALUES "); + var sb = new System.Text.StringBuilder("INSERT INTO Trades (PlatformTradeId, MarketId, AssetId, Outcome, Side, Price, Size, Amount, ExecutedAt, TransactionHash, TraderId, MarketOutcomeId, DbMarketId, Platform, IsContextEnriched, AggregatedCount, UsdcSize, OutcomeIndex) VALUES "); var parameters = new List(); for (int i = 0; i < chunk.Length; i++) { var t = chunk[i]; - int pIdx = i * 16; - sb.Append($"({{{pIdx}}}, {{{pIdx + 1}}}, {{{pIdx + 2}}}, {{{pIdx + 3}}}, {{{pIdx + 4}}}, {{{pIdx + 5}}}, {{{pIdx + 6}}}, {{{pIdx + 7}}}, {{{pIdx + 8}}}, {{{pIdx + 9}}}, {{{pIdx + 10}}}, {{{pIdx + 11}}}, {{{pIdx + 12}}}, {{{pIdx + 13}}}, {{{pIdx + 14}}}, {{{pIdx + 15}}})"); + int pIdx = i * 18; + sb.Append($"({{{pIdx}}}, {{{pIdx + 1}}}, {{{pIdx + 2}}}, {{{pIdx + 3}}}, {{{pIdx + 4}}}, {{{pIdx + 5}}}, {{{pIdx + 6}}}, {{{pIdx + 7}}}, {{{pIdx + 8}}}, {{{pIdx + 9}}}, {{{pIdx + 10}}}, {{{pIdx + 11}}}, {{{pIdx + 12}}}, {{{pIdx + 13}}}, {{{pIdx + 14}}}, {{{pIdx + 15}}}, {{{pIdx + 16}}}, {{{pIdx + 17}}})"); if (i < chunk.Length - 1) sb.Append(", "); @@ -98,10 +98,12 @@ public class TradeRepository : ITradeRepository parameters.Add((int)t.Platform); parameters.Add(t.IsContextEnriched); parameters.Add(t.AggregatedCount ?? (object?)null); + parameters.Add(t.UsdcSize ?? (object?)null); + parameters.Add(t.OutcomeIndex ?? (object?)null); } // For Aggregated Trades, we want UPSERT logic to update size, amount and VWAP - sb.Append(" ON DUPLICATE KEY UPDATE Price=VALUES(Price), Size=VALUES(Size), Amount=VALUES(Amount), AggregatedCount=VALUES(AggregatedCount);"); + sb.Append(" ON DUPLICATE KEY UPDATE Price=VALUES(Price), Size=VALUES(Size), Amount=VALUES(Amount), AggregatedCount=VALUES(AggregatedCount), UsdcSize=VALUES(UsdcSize), OutcomeIndex=VALUES(OutcomeIndex);"); int maxRetries = 3; var backoffs = new[] { 250, 500, 1000 }; diff --git a/src/Predictalytics.Infrastructure/Migrations/20260719090559_AddTradeUsdcOutcomeIndexAndCategoryTotalInvested.Designer.cs b/src/Predictalytics.Infrastructure/Migrations/20260719090559_AddTradeUsdcOutcomeIndexAndCategoryTotalInvested.Designer.cs new file mode 100644 index 0000000..2863c16 --- /dev/null +++ b/src/Predictalytics.Infrastructure/Migrations/20260719090559_AddTradeUsdcOutcomeIndexAndCategoryTotalInvested.Designer.cs @@ -0,0 +1,1275 @@ +// +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("20260719090559_AddTradeUsdcOutcomeIndexAndCategoryTotalInvested")] + partial class AddTradeUsdcOutcomeIndexAndCategoryTotalInvested + { + /// + 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("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("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/20260719090559_AddTradeUsdcOutcomeIndexAndCategoryTotalInvested.cs b/src/Predictalytics.Infrastructure/Migrations/20260719090559_AddTradeUsdcOutcomeIndexAndCategoryTotalInvested.cs new file mode 100644 index 0000000..7ad8e0f --- /dev/null +++ b/src/Predictalytics.Infrastructure/Migrations/20260719090559_AddTradeUsdcOutcomeIndexAndCategoryTotalInvested.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Predictalytics.Infrastructure.Migrations +{ + /// + public partial class AddTradeUsdcOutcomeIndexAndCategoryTotalInvested : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "OutcomeIndex", + table: "Trades", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "UsdcSize", + table: "Trades", + type: "decimal(18,6)", + nullable: true); + + migrationBuilder.AddColumn( + name: "TotalInvested", + table: "TraderCategoryPerformances", + type: "decimal(65,30)", + nullable: false, + defaultValue: 0m); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "OutcomeIndex", + table: "Trades"); + + migrationBuilder.DropColumn( + name: "UsdcSize", + table: "Trades"); + + migrationBuilder.DropColumn( + name: "TotalInvested", + table: "TraderCategoryPerformances"); + } + } +} diff --git a/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs index ad93167..fc0128b 100644 --- a/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -453,6 +453,9 @@ namespace Predictalytics.Infrastructure.Migrations .HasMaxLength(128) .HasColumnType("varchar(128)"); + b.Property("OutcomeIndex") + .HasColumnType("int"); + b.Property("Platform") .HasColumnType("int"); @@ -485,6 +488,9 @@ namespace Predictalytics.Infrastructure.Migrations .HasMaxLength(66) .HasColumnType("varchar(66)"); + b.Property("UsdcSize") + .HasColumnType("decimal(18,6)"); + b.HasKey("Id"); b.HasIndex("AssetId"); @@ -765,6 +771,9 @@ namespace Predictalytics.Infrastructure.Migrations .HasMaxLength(128) .HasColumnType("varchar(128)"); + b.Property("TotalInvested") + .HasColumnType("decimal(65,30)"); + b.Property("TotalPnL") .HasPrecision(18, 4) .HasColumnType("decimal(18,4)"); diff --git a/src/Predictalytics.Infrastructure/Providers/Polymarket/PolymarketProvider.cs b/src/Predictalytics.Infrastructure/Providers/Polymarket/PolymarketProvider.cs index ce672ff..1a05433 100644 --- a/src/Predictalytics.Infrastructure/Providers/Polymarket/PolymarketProvider.cs +++ b/src/Predictalytics.Infrastructure/Providers/Polymarket/PolymarketProvider.cs @@ -56,7 +56,9 @@ public class PolymarketProvider : IPlatformProvider TransactionHash = r.TransactionHash?.ToLowerInvariant(), TraderId = 0, TransientWallet = wallet, - TransientDisplayName = !string.IsNullOrEmpty(r.Name) ? r.Name : r.Pseudonym + TransientDisplayName = !string.IsNullOrEmpty(r.Name) ? r.Name : r.Pseudonym, + UsdcSize = (decimal)r.UsdcSize, + OutcomeIndex = r.OutcomeIndex }; }).ToList(); @@ -94,7 +96,9 @@ public class PolymarketProvider : IPlatformProvider TransactionHash = r.TransactionHash?.ToLowerInvariant(), TraderId = 0, TransientWallet = wallet, - TransientDisplayName = !string.IsNullOrEmpty(r.Name) ? r.Name : r.Pseudonym + TransientDisplayName = !string.IsNullOrEmpty(r.Name) ? r.Name : r.Pseudonym, + UsdcSize = (decimal)r.UsdcSize, + OutcomeIndex = r.OutcomeIndex }; }).ToList(); @@ -127,7 +131,9 @@ public class PolymarketProvider : IPlatformProvider TransactionHash = r.TransactionHash, TraderId = 0, TransientWallet = wallet, - TransientDisplayName = !string.IsNullOrEmpty(r.Name) ? r.Name : r.Pseudonym + TransientDisplayName = !string.IsNullOrEmpty(r.Name) ? r.Name : r.Pseudonym, + UsdcSize = (decimal)r.UsdcSize, + OutcomeIndex = r.OutcomeIndex }; }).ToList(); diff --git a/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs b/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs index 9ca6c26..9c64f84 100644 --- a/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs +++ b/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs @@ -398,6 +398,7 @@ public class PositionPnLEngine : IPositionPnLEngine { existing.TotalVolume = kvp.Value.TotalVolume; existing.TotalPnL = kvp.Value.TotalPnL; + existing.TotalInvested = kvp.Value.TotalInvested; existing.TotalTrades = kvp.Value.TotalTrades; existing.WinningTrades = kvp.Value.WinningTrades; _db.TraderCategoryPerformances.Update(existing); @@ -603,6 +604,11 @@ public class PositionPnLEngine : IPositionPnLEngine } } + decimal marketBuyAmount = marketGroup + .Where(t => t.Side == TradeSide.Buy) + .Sum(t => t.Amount); + + perf.TotalInvested += marketBuyAmount; perf.TotalPnL += marketPnl; perf.TotalTrades += 1; if (marketPnl > 0)