using FluentAssertions; using IBKRTrader.Core.Analytics; using IBKRTrader.Core.Persistence.Entities; namespace IBKRTrader.Tests.Analytics; [Trait("cat", "unit")] public class TradeAnalyticsTests { private static CoreTrade Fill(string module, string action, decimal qty, decimal price, int minute, string symbol = "AAPL") => new() { Module = module, 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 EmptyInput_YieldsZeroKpis() { var k = TradeAnalytics.ComputeKpis(Array.Empty()); k.TradeCount.Should().Be(0); k.NetPnl.Should().Be(0m); k.WinRatePct.Should().Be(0d); } [Fact] public void ComputesWinRateAndProfitFactor() { var fills = new[] { Fill("CT", "BUY", 10, 100m, 0), Fill("CT", "SELL", 10, 130m, 1), // +300 Gewinner Fill("CT", "BUY", 10, 100m, 2, "MSFT"), Fill("CT", "SELL", 10, 90m, 3, "MSFT") // -100 Verlierer }; var k = TradeAnalytics.ComputeKpis(fills); k.TradeCount.Should().Be(2); k.NetPnl.Should().Be(200m); k.WinRatePct.Should().Be(50d); k.ProfitFactor.Should().Be(3d); // 300 / 100 } [Fact] public void PnlByModule_GroupsAndSorts() { var fills = new[] { Fill("A", "BUY", 10, 100m, 0), Fill("A", "SELL", 10, 130m, 1), // +300 Fill("B", "BUY", 10, 100m, 2, "MSFT"), Fill("B", "SELL", 10, 90m, 3, "MSFT") // -100 }; var buckets = TradeAnalytics.PnlByModule(fills); buckets.Should().HaveCount(2); buckets[0].Key.Should().Be("A"); buckets[0].Pnl.Should().Be(300m); buckets[1].Key.Should().Be("B"); } }