G3 & G4: Map UsdcSize, OutcomeIndex, compute Category ROI and add migration

This commit is contained in:
Richard
2026-07-19 12:35:03 +02:00
parent 8a39b912a6
commit 6fd7e563f4
14 changed files with 1484 additions and 10 deletions
@@ -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<Event>().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<PositionPnLEngine>.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
}
}
}
@@ -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<AppDbContext>()
.UseSqlite(connection)
.Options;
using var db = new AppDbContext(options);
db.Database.EnsureCreated();
var repo = new TradeRepository(db, NullLogger<TradeRepository>.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<Exception>(() => 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);
}
}