@
R3 Slice 2: Trading-Buchführung auf EF Core (+ InMemory-Tests) - BudgetService, TradeHistoryService, PortfolioService von Dapper/DatabaseService auf IDbContextFactory<CoreDbContext> (EF Core) umgestellt - Positionen/Budget/Trade-Historie ueber CoreDbContext (core_position/core_budget/core_trade_history) - Tests: EF-InMemory-Provider im Testprojekt; 5 PortfolioService-Tests (Buy/Sell/Avg/Exposure/Isolation) - 35/35 Tests + Build + smoke-ui gruen; WorkerBase-Log/Modul/Dapper-Entfernung folgen Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// EF-gestützte Buchführung gegen die EF-InMemory-Datenbank (kein externer DB-Zugriff,
|
||||
/// deterministisch – zählt als Unit-Test).
|
||||
/// </summary>
|
||||
[Trait("cat", "unit")]
|
||||
public class PortfolioServiceTests
|
||||
{
|
||||
private sealed class InMemoryFactory : IDbContextFactory<CoreDbContext>
|
||||
{
|
||||
private readonly DbContextOptions<CoreDbContext> _options;
|
||||
public InMemoryFactory(DbContextOptions<CoreDbContext> options) => _options = options;
|
||||
public CoreDbContext CreateDbContext() => new(_options);
|
||||
}
|
||||
|
||||
private static PortfolioService CreateSut(out IDbContextFactory<CoreDbContext> factory)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<CoreDbContext>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user