Implement B4: Add Predictalytics.Application.Tests project with unit tests for PositionPnLEngine and AnalyticsService
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Predictalytics.Application.Interfaces;
|
||||
using Predictalytics.Application.Services;
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Predictalytics.Infrastructure.Data;
|
||||
using Predictalytics.Infrastructure.Data.Repositories;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Predictalytics.Application.Tests.Services;
|
||||
|
||||
public class AnalyticsServiceTests
|
||||
{
|
||||
private AppDbContext CreateDbContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new AppDbContext(options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTraderDeepDiveAsync_CalculatesCorrectMetrics()
|
||||
{
|
||||
// Arrange
|
||||
using var db = CreateDbContext();
|
||||
var traderRepo = new TraderRepository(db);
|
||||
var tradeRepo = new TradeRepository(db);
|
||||
var marketRepo = new MarketRepository(db);
|
||||
|
||||
var discoveryMock = new MockDiscoveryService();
|
||||
var providers = new List<IPlatformProvider>();
|
||||
|
||||
var analyticsService = new AnalyticsService(
|
||||
traderRepo,
|
||||
tradeRepo,
|
||||
null!, // alertRepo (not used in GetTraderDeepDiveAsync)
|
||||
null!, // watchlistRepo (not used in GetTraderDeepDiveAsync)
|
||||
marketRepo,
|
||||
discoveryMock,
|
||||
providers,
|
||||
NullLogger<AnalyticsService>.Instance
|
||||
);
|
||||
|
||||
var trader = new Trader { Id = 1, PlatformUserId = "0x1", DisplayName = "Trader 1" };
|
||||
db.Traders.Add(trader);
|
||||
|
||||
var market = new Market { Id = 10, PlatformMarketId = "pm1", Question = "Q?" };
|
||||
var outcome = new MarketOutcome { Id = 100, MarketId = 10, Label = "Yes", TokenId = "t100", CurrentPrice = 0.50m };
|
||||
market.Outcomes.Add(outcome);
|
||||
db.Markets.Add(market);
|
||||
|
||||
var baseTime = DateTime.UtcNow.AddDays(-5);
|
||||
db.Trades.Add(new Trade
|
||||
{
|
||||
Id = 501, TraderId = 1, DbMarketId = 10, MarketOutcomeId = 100,
|
||||
Side = TradeSide.Buy, Price = 0.40m, Size = 100m, Amount = 40m,
|
||||
ExecutedAt = baseTime
|
||||
});
|
||||
|
||||
db.Trades.Add(new Trade
|
||||
{
|
||||
Id = 502, TraderId = 1, DbMarketId = 10, MarketOutcomeId = 100,
|
||||
Side = TradeSide.Sell, Price = 0.60m, Size = 100m, Amount = 60m,
|
||||
ExecutedAt = baseTime.AddHours(24) // 24 hours holding duration
|
||||
});
|
||||
|
||||
db.MarketOutcomePriceSnapshots.Add(new MarketOutcomePriceSnapshot
|
||||
{
|
||||
Id = 1, MarketOutcomeId = 100, Price = 0.50m, Timestamp = baseTime.AddHours(2)
|
||||
});
|
||||
db.MarketOutcomePriceSnapshots.Add(new MarketOutcomePriceSnapshot
|
||||
{
|
||||
Id = 2, MarketOutcomeId = 100, Price = 0.60m, Timestamp = baseTime.AddHours(12)
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var deepDive = await analyticsService.GetTraderDeepDiveAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(deepDive);
|
||||
Assert.Equal(24.0, (double)deepDive.AvgHoldDurationHours, 2);
|
||||
|
||||
// Entry Quality: Buy at 0.40, subsequent prices are 0.50 and 0.60 (avg 0.55).
|
||||
// Entry Quality = 50 + ((0.55 - 0.40) / 0.40) * 100 = 50 + 0.375 * 100 = 87.5
|
||||
Assert.Equal(87.5m, deepDive.EntryQuality);
|
||||
}
|
||||
|
||||
private class MockDiscoveryService : IDiscoveryService
|
||||
{
|
||||
public Task<int> ImportTraderAsync(PlatformType platform, string platformUserId, string displayName, CancellationToken ct) => Task.FromResult(0);
|
||||
public Task ScanTopHoldersAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
public Task<IReadOnlyList<DiscoveredTrader>> RunDiscoveryAsync(PlatformType platform, CancellationToken ct) => Task.FromResult<IReadOnlyList<DiscoveredTrader>>(new List<DiscoveredTrader>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Infrastructure.Data;
|
||||
using Predictalytics.Infrastructure.Services;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Predictalytics.Application.Tests.Services;
|
||||
|
||||
public class PositionPnLEngineTests
|
||||
{
|
||||
private AppDbContext CreateDbContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new AppDbContext(options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RecalculateTraderPositionsAsync_BuyTrade_CreatesTraderPosition()
|
||||
{
|
||||
// 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 = "pm1", 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 buyTrade = new Trade
|
||||
{
|
||||
Id = 500,
|
||||
TraderId = 1,
|
||||
DbMarketId = 10,
|
||||
MarketOutcomeId = 100,
|
||||
Side = TradeSide.Buy,
|
||||
Price = 0.50m,
|
||||
Size = 100m,
|
||||
Amount = 50m,
|
||||
ExecutedAt = DateTime.UtcNow
|
||||
};
|
||||
db.Trades.Add(buyTrade);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
await pnlEngine.RecalculateTraderPositionsAsync(1);
|
||||
|
||||
// Assert
|
||||
var pos = await db.TraderPositions.FirstOrDefaultAsync(p => p.TraderId == 1 && p.MarketOutcomeId == 100);
|
||||
Assert.NotNull(pos);
|
||||
Assert.Equal(100m, pos.SharesHeld);
|
||||
Assert.Equal(0.50m, pos.AvgCost);
|
||||
Assert.Equal(0m, pos.RealizedPnl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RecalculateTraderPositionsAsync_SellTrade_CalculatesRealizedPnL()
|
||||
{
|
||||
// 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 = "pm1", 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);
|
||||
|
||||
// Buy 100 shares at 0.40
|
||||
db.Trades.Add(new Trade
|
||||
{
|
||||
Id = 501, TraderId = 1, DbMarketId = 10, MarketOutcomeId = 100,
|
||||
Side = TradeSide.Buy, Price = 0.40m, Size = 100m, Amount = 40m,
|
||||
ExecutedAt = DateTime.UtcNow.AddMinutes(-10)
|
||||
});
|
||||
|
||||
// Sell 50 shares at 0.60
|
||||
db.Trades.Add(new Trade
|
||||
{
|
||||
Id = 502, TraderId = 1, DbMarketId = 10, MarketOutcomeId = 100,
|
||||
Side = TradeSide.Sell, Price = 0.60m, Size = 50m, Amount = 30m,
|
||||
ExecutedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
await pnlEngine.RecalculateTraderPositionsAsync(1);
|
||||
|
||||
// Assert
|
||||
var pos = await db.TraderPositions.FirstOrDefaultAsync(p => p.TraderId == 1 && p.MarketOutcomeId == 100);
|
||||
Assert.NotNull(pos);
|
||||
Assert.Equal(50m, pos.SharesHeld);
|
||||
Assert.Equal(0.40m, pos.AvgCost); // AvgCost stays at 0.40
|
||||
Assert.Equal(10m, pos.RealizedPnl); // 50 * (0.60 - 0.40) = 10
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RecalculateTraderPositionsAsync_RedeemTrade_CalculatesRedeemPnL()
|
||||
{
|
||||
// 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 = "pm1", Question = "Q?", IsResolved = true, ResolutionOutcome = "Yes" };
|
||||
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);
|
||||
|
||||
// Buy 100 shares at 0.40
|
||||
db.Trades.Add(new Trade
|
||||
{
|
||||
Id = 501, TraderId = 1, DbMarketId = 10, MarketOutcomeId = 100,
|
||||
Side = TradeSide.Buy, Price = 0.40m, Size = 100m, Amount = 40m,
|
||||
ExecutedAt = DateTime.UtcNow.AddMinutes(-10)
|
||||
});
|
||||
|
||||
// Redeem at 1.00 (Win)
|
||||
db.Trades.Add(new Trade
|
||||
{
|
||||
Id = 503, TraderId = 1, DbMarketId = 10, MarketOutcomeId = 100,
|
||||
Side = TradeSide.Redeem, Price = 1.00m, Size = 100m, Amount = 100m,
|
||||
ExecutedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
await pnlEngine.RecalculateTraderPositionsAsync(1);
|
||||
|
||||
// Assert
|
||||
var pos = await db.TraderPositions.FirstOrDefaultAsync(p => p.TraderId == 1 && p.MarketOutcomeId == 100);
|
||||
Assert.NotNull(pos);
|
||||
Assert.Equal(0m, pos.SharesHeld);
|
||||
Assert.Equal(60m, pos.RealizedPnl); // 100 * (1.00 - 0.40) = 60
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user