using FluentAssertions; using IBKRTrader.Core.Analytics; using IBKRTrader.Core.Persistence.Entities; namespace IBKRTrader.Tests.Analytics; [Trait("cat", "unit")] public class RealizedPnlEngineTests { private static CoreTrade Fill(string action, decimal qty, decimal price, int minute, string symbol = "AAPL") => new() { Module = "CT", Symbol = symbol, Action = action, Quantity = qty, Price = price, TotalValue = qty * price, TradedAt = new DateTime(2026, 1, 1, 0, minute, 0, DateTimeKind.Utc) }; [Fact] public void BuyThenSellAll_RealizesFullPnl() { var fills = new[] { Fill("BUY", 10, 100m, 0), Fill("SELL", 10, 130m, 1) }; var realized = RealizedPnlEngine.Match(fills); realized.Should().HaveCount(1); realized[0].RealizedPnl.Should().Be(300m); // (130-100)*10 } [Fact] public void Sell_MatchesOldestLotsFirst_Fifo() { var fills = new[] { Fill("BUY", 10, 100m, 0), Fill("BUY", 10, 120m, 1), Fill("SELL", 15, 130m, 2) // 10 gegen 100er-Lot, 5 gegen 120er-Lot }; var realized = RealizedPnlEngine.Match(fills); realized.Should().HaveCount(2); realized[0].RealizedPnl.Should().Be((130m - 100m) * 10m); // 300 realized[1].RealizedPnl.Should().Be((130m - 120m) * 5m); // 50 RealizedPnlEngine.TotalRealized(fills).Should().Be(350m); } [Fact] public void PartialSell_LeavesRemainderOpen() { var fills = new[] { Fill("BUY", 10, 100m, 0), Fill("SELL", 4, 130m, 1) }; var realized = RealizedPnlEngine.Match(fills); realized.Should().HaveCount(1); realized[0].Quantity.Should().Be(4m); realized[0].RealizedPnl.Should().Be(120m); } [Fact] public void SellExceedingHoldings_IgnoresSurplus_NoShort() { var fills = new[] { Fill("BUY", 5, 100m, 0), Fill("SELL", 8, 130m, 1) }; var realized = RealizedPnlEngine.Match(fills); realized.Should().HaveCount(1); realized[0].Quantity.Should().Be(5m); // nur die gehaltenen 5 realisiert } [Fact] public void SeparatesBySymbol() { var fills = new[] { Fill("BUY", 10, 100m, 0, "AAPL"), Fill("BUY", 10, 50m, 1, "MSFT"), Fill("SELL", 10, 130m, 2, "AAPL") }; var realized = RealizedPnlEngine.Match(fills); realized.Should().HaveCount(1); realized[0].Symbol.Should().Be("AAPL"); } }