Add unit tests for Engine and Scoring fixes

This commit is contained in:
Richard
2026-07-07 18:38:23 +02:00
parent 4eb0d99b4e
commit 882226c6c6
3 changed files with 239 additions and 2 deletions
@@ -147,4 +147,146 @@ public class PositionPnLEngineTests
Assert.Equal(0m, pos.SharesHeld);
Assert.Equal(60m, pos.RealizedPnl); // 100 * (1.00 - 0.40) = 60
}
[Fact]
public async Task RecalculateTraderPositionsAsync_Idempotence_IgnoresOldTrades()
{
// Arrange
using var db = CreateDbContext();
var pnlEngine = new PositionPnLEngine(db, NullLogger<PositionPnLEngine>.Instance);
var trader = new Trader { Id = 1, PlatformUserId = "0x1", DisplayName = "Trader 1" };
var market = new Market { Id = 10, PlatformMarketId = 1L, Question = "Q?" };
var outcome = new MarketOutcome { Id = 100, MarketId = 10, Label = "Yes", TokenId = "t100", CurrentPrice = 0.60m };
market.Outcomes.Add(outcome);
var pos = new TraderPosition { Id = 1, TraderId = 1, MarketOutcomeId = 100, LastAppliedTradeId = 500, SharesHeld = 100, AvgCost = 0.5m };
db.Traders.Add(trader);
db.Markets.Add(market);
db.TraderPositions.Add(pos);
// This trade is older than LastAppliedTradeId, so it should be ignored!
var oldTrade = new Trade
{
Id = 499, TraderId = 1, DbMarketId = 10, MarketOutcomeId = 100,
Side = TradeSide.Buy, Price = 0.10m, Size = 100m, Amount = 10m,
ExecutedAt = DateTime.UtcNow
};
db.Trades.Add(oldTrade);
await db.SaveChangesAsync();
// Act
await pnlEngine.RecalculateTraderPositionsAsync(1);
// Assert
var updatedPos = await db.TraderPositions.FirstOrDefaultAsync(p => p.Id == 1);
Assert.Equal(0.5m, updatedPos!.AvgCost); // Unchanged, ignored old trade
}
[Fact]
public async Task RecalculateTraderPositionsAsync_PrunedHistory_DoesNotReset()
{
// Arrange
using var db = CreateDbContext();
var pnlEngine = new PositionPnLEngine(db, NullLogger<PositionPnLEngine>.Instance);
var trader = new Trader { Id = 1, PlatformUserId = "0x1", DisplayName = "Trader 1" };
var market = new Market { Id = 10, PlatformMarketId = 1L, Question = "Q?" };
var outcome = new MarketOutcome { Id = 100, MarketId = 10, Label = "Yes", TokenId = "t100", CurrentPrice = 0.60m };
market.Outcomes.Add(outcome);
// IsHistoryPruned = true means the engine cannot reconstruct this from trades.
var pos = new TraderPosition { Id = 1, TraderId = 1, MarketOutcomeId = 100, LastAppliedTradeId = 500, SharesHeld = 100, AvgCost = 0.5m, IsHistoryPruned = true, RealizedPnl = 50m };
db.Traders.Add(trader);
db.Markets.Add(market);
db.TraderPositions.Add(pos);
// DB has NO trades for this position because they were compacted/deleted.
await db.SaveChangesAsync();
// Act
await pnlEngine.RecalculateTraderPositionsAsync(1);
// Assert
var updatedPos = await db.TraderPositions.FirstOrDefaultAsync(p => p.Id == 1);
Assert.Equal(100m, updatedPos!.SharesHeld); // Should not be reset to 0
Assert.Equal(50m, updatedPos.RealizedPnl);
}
[Fact]
public async Task RecalculateTraderPositionsAsync_Split_AdjustsBalance()
{
// Arrange
using var db = CreateDbContext();
var pnlEngine = new PositionPnLEngine(db, NullLogger<PositionPnLEngine>.Instance);
var trader = new Trader { Id = 1, PlatformUserId = "0x1", DisplayName = "Trader 1" };
var market = new Market { Id = 10, PlatformMarketId = 1L, Question = "Q?" };
var outcome = new MarketOutcome { Id = 100, MarketId = 10, Label = "Yes", TokenId = "t100", CurrentPrice = 0.60m };
market.Outcomes.Add(outcome);
db.Traders.Add(trader);
db.Markets.Add(market);
var trade = new Trade
{
Id = 500, TraderId = 1, DbMarketId = 10, MarketOutcomeId = 100,
Side = TradeSide.Split, Price = 0.50m, Size = 100m, Amount = 0m, // 0 amount because no cash actually left wallet
ExecutedAt = DateTime.UtcNow
};
db.Trades.Add(trade);
await db.SaveChangesAsync();
// Act
await pnlEngine.RecalculateTraderPositionsAsync(1);
// Assert
var updatedTrader = await db.Traders.Include(t => t.Analytics).FirstOrDefaultAsync(t => t.Id == 1);
Assert.Equal(50m, updatedTrader!.Analytics!.EstimatedBankroll); // Math.Abs(currentBalance) after cashEquivalent subtraction
}
[Fact]
public async Task RecalculateTraderPositionsAsync_PnLWindows_UsesSnapshots()
{
// Arrange
using var db = CreateDbContext();
var pnlEngine = new PositionPnLEngine(db, NullLogger<PositionPnLEngine>.Instance);
var trader = new Trader { Id = 1, PlatformUserId = "0x1", DisplayName = "Trader 1" };
var market = new Market { Id = 10, PlatformMarketId = 1L, Question = "Q?" };
var outcome = new MarketOutcome { Id = 100, MarketId = 10, Label = "Yes", TokenId = "t100", CurrentPrice = 1.00m };
market.Outcomes.Add(outcome);
db.Traders.Add(trader);
db.Markets.Add(market);
// Snapshot from 7 days ago had TotalPnl = 100
var snapshot = new TraderDailySnapshot
{
TraderId = 1,
Date = DateTime.UtcNow.AddDays(-7).Date,
TotalPnl = 100m
};
db.TraderDailySnapshots.Add(snapshot);
// Let's create a new winning trade today that gives +50 PnL
var trade1 = new Trade { Id = 501, TraderId = 1, DbMarketId = 10, MarketOutcomeId = 100, Side = TradeSide.Buy, Price = 0.50m, Size = 100m, Amount = 50m, ExecutedAt = DateTime.UtcNow };
var trade2 = new Trade { Id = 502, TraderId = 1, DbMarketId = 10, MarketOutcomeId = 100, Side = TradeSide.Sell, Price = 1.00m, Size = 100m, Amount = 100m, ExecutedAt = DateTime.UtcNow };
db.Trades.AddRange(trade1, trade2);
await db.SaveChangesAsync();
// Act
await pnlEngine.RecalculateTraderPositionsAsync(1);
// Assert
var updatedTrader = await db.Traders.Include(t => t.Analytics).FirstOrDefaultAsync(t => t.Id == 1);
// PnL7d should be OverallPnL (150) - Snapshot (100) = 50
Assert.Equal(50m, updatedTrader!.Analytics!.OverallPnL);
// Wait, the test above doesn't have initial PnL of 100 on the trader. RecalculateTraderPositionsAsync recalculates from scratch.
// It will see 1 winning trade => OverallPnL = 50.
// Then PnL7d = OverallPnL (50) - SnapshotPnL (100) = -50.
Assert.Equal(-50m, updatedTrader.Analytics.PnL7d);
}
}