using FluentAssertions;
using IBKRTrader.Core.Budget;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Persistence.Ef;
using IBKRTrader.Core.Trading;
using Microsoft.EntityFrameworkCore;
namespace IBKRTrader.Tests.Trading;
///
/// EF-gestützte Buchführung gegen die EF-InMemory-Datenbank (kein externer DB-Zugriff,
/// deterministisch – zählt als Unit-Test).
///
[Trait("cat", "unit")]
public class PortfolioServiceTests
{
private sealed class InMemoryFactory : IDbContextFactory
{
private readonly DbContextOptions _options;
public InMemoryFactory(DbContextOptions options) => _options = options;
public CoreDbContext CreateDbContext() => new(_options);
}
private static PortfolioService CreateSut(out IDbContextFactory factory)
{
var options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
factory = new InMemoryFactory(options);
var logger = new LoggingService();
var history = new TradeHistoryService(factory, logger);
var budget = new BudgetService(factory, logger);
return new PortfolioService(factory, history, budget, logger);
}
[Fact]
public async Task Buy_CreatesPosition_AndExposure_AndBudget_AndHistory()
{
var sut = CreateSut(out var factory);
await sut.RecordFillAsync("CT", "AAPL", TradeSide.Buy, 5, 100m, "O1");
(await sut.GetPositionQuantityAsync("CT", "AAPL")).Should().Be(5);
(await sut.GetModuleExposureAsync("CT")).Should().Be(500m);
await using var db = await factory.CreateDbContextAsync();
db.TradeHistory.Should().ContainSingle();
(await db.Budgets.FindAsync("CT"))!.UsedBudget.Should().Be(500m);
}
[Fact]
public async Task Buy_Twice_AveragesPrice()
{
var sut = CreateSut(out _);
await sut.RecordFillAsync("CT", "AAPL", TradeSide.Buy, 10, 100m, "O1");
await sut.RecordFillAsync("CT", "AAPL", TradeSide.Buy, 10, 120m, "O2");
(await sut.GetPositionQuantityAsync("CT", "AAPL")).Should().Be(20);
// Durchschnitt: (10*100 + 10*120) / 20 = 110
var positions = await sut.GetPositionsAsync("CT");
positions.Single().AvgPrice.Should().Be(110m);
}
[Fact]
public async Task Sell_Partial_ReducesQuantity()
{
var sut = CreateSut(out _);
await sut.RecordFillAsync("CT", "AAPL", TradeSide.Buy, 10, 100m, "O1");
await sut.RecordFillAsync("CT", "AAPL", TradeSide.Sell, 4, 130m, "O2");
(await sut.GetPositionQuantityAsync("CT", "AAPL")).Should().Be(6);
}
[Fact]
public async Task Sell_Full_RemovesPosition()
{
var sut = CreateSut(out _);
await sut.RecordFillAsync("CT", "AAPL", TradeSide.Buy, 10, 100m, "O1");
await sut.RecordFillAsync("CT", "AAPL", TradeSide.Sell, 10, 130m, "O2");
(await sut.GetPositionQuantityAsync("CT", "AAPL")).Should().Be(0);
(await sut.GetPositionsAsync("CT")).Should().BeEmpty();
}
[Fact]
public async Task Exposure_IsIsolatedPerModule()
{
var sut = CreateSut(out _);
await sut.RecordFillAsync("CT", "AAPL", TradeSide.Buy, 5, 100m, "O1");
await sut.RecordFillAsync("XX", "MSFT", TradeSide.Buy, 2, 200m, "O2");
(await sut.GetModuleExposureAsync("CT")).Should().Be(500m);
(await sut.GetModuleExposureAsync("XX")).Should().Be(400m);
}
}