Add unit tests for Engine and Scoring fixes
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user