using IBKRTrader.Core.Persistence.Entities; namespace IBKRTrader.Core.Analytics; /// /// Ein geschlossener (realisierter) Teil-Trade: eine SELL-Menge, gegen ihre FIFO-gematchten BUY-Lots /// abgerechnet. Aus den Fills der core_trade_history rein rechnerisch abgeleitet. /// public sealed record RealizedTrade( string Module, string Symbol, decimal Quantity, decimal BuyPrice, decimal SellPrice, DateTime OpenedAt, DateTime ClosedAt) { /// Realisierte GuV dieser Menge (Erlös − Einstand); Fees sind hier nicht berücksichtigt. public decimal RealizedPnl => (SellPrice - BuyPrice) * Quantity; } /// /// Reines, seiteneffektfreies FIFO-Lot-Matching über Fills (BUY öffnet Lots, SELL realisiert gegen die /// ältesten offenen Lots) — je (Modul, Symbol). Grundlage sowohl für die Supervisor-KPIs /// () als auch für die realisierte GuV im Accounting-Modul (kein Duplikat). /// Geldkritisch → unit-getestet. Long-only-Sicht: SELL-Mengen ohne passendes offenes Lot werden /// ignoriert (kein Leerverkauf modelliert). /// public static class RealizedPnlEngine { private sealed class Lot { public decimal Quantity; public decimal Price; public DateTime OpenedAt; } /// Matcht alle Fills zu realisierten Teil-Trades (chronologisch, FIFO je Modul+Symbol). public static IReadOnlyList Match(IEnumerable fills) { var result = new List(); var groups = fills .GroupBy(f => (f.Module, f.Symbol)); foreach (var g in groups) { var open = new Queue(); foreach (var f in g.OrderBy(x => x.TradedAt).ThenBy(x => x.Id)) { bool isBuy = string.Equals(f.Action, "BUY", StringComparison.OrdinalIgnoreCase); if (isBuy) { open.Enqueue(new Lot { Quantity = f.Quantity, Price = f.Price, OpenedAt = f.TradedAt }); continue; } // SELL: gegen die ältesten offenen Lots abrechnen. decimal remaining = f.Quantity; while (remaining > 0 && open.Count > 0) { var lot = open.Peek(); decimal matched = Math.Min(remaining, lot.Quantity); result.Add(new RealizedTrade( g.Key.Module, g.Key.Symbol, matched, lot.Price, f.Price, lot.OpenedAt, f.TradedAt)); lot.Quantity -= matched; remaining -= matched; if (lot.Quantity <= 0) open.Dequeue(); } // Überschüssige SELL-Menge ohne offenes Lot: ignoriert (kein Short modelliert). } } return result; } /// Summe der realisierten GuV über alle gematchten Teil-Trades. public static decimal TotalRealized(IEnumerable fills) => Match(fills).Sum(t => t.RealizedPnl); }