using FluentAssertions; using IBKRTrader.Core.Trading; namespace IBKRTrader.Tests.Trading; /// /// Fortschreibung einer Position durch eine Ausführung. Die Long-Fälle bildeten vorher der /// PortfolioService ab; die drei Short-Fälle konnten bis zur Optionserweiterung gar nicht auftreten. /// [Trait("cat", "unit")] public class PositionMathTests { [Fact] public void Buy_FromFlat_SetsTheCostBasis() { var r = PositionMath.Apply(0, 0m, TradeSide.Buy, 10, 100m); r.Quantity.Should().Be(10); r.AvgPrice.Should().Be(100m); r.ClosedQuantity.Should().Be(0); } [Fact] public void Buy_More_AveragesTheCostBasis() { var r = PositionMath.Apply(10, 100m, TradeSide.Buy, 10, 120m); r.Quantity.Should().Be(20); r.AvgPrice.Should().Be(110m); } [Fact] public void Sell_Partial_LeavesTheCostBasisAlone() { // Das Ergebnis des geschlossenen Teils ist realisiert; es gehoert in die Trade-Historie und // darf den Einstand der Restposition nicht verschieben. var r = PositionMath.Apply(10, 100m, TradeSide.Sell, 4, 130m); r.Quantity.Should().Be(6); r.AvgPrice.Should().Be(100m); r.ClosedQuantity.Should().Be(4); } [Fact] public void Sell_Everything_ClosesThePosition() { var r = PositionMath.Apply(10, 100m, TradeSide.Sell, 10, 130m); r.Quantity.Should().Be(0); r.ClosedQuantity.Should().Be(10); } // ─── Was es vor den Optionen nicht gab ─────────────────────────────────────── [Fact] public void SellToOpen_FromFlat_CreatesAShortPosition() { var r = PositionMath.Apply(0, 0m, TradeSide.Sell, 1, 2.50m); r.Quantity.Should().Be(-1); r.AvgPrice.Should().Be(2.50m); // vereinnahmte Praemie je Kontrakt r.ClosedQuantity.Should().Be(0); } [Fact] public void SellToOpen_Again_AveragesThePremiumReceived() { var r = PositionMath.Apply(-1, 2.00m, TradeSide.Sell, 1, 3.00m); r.Quantity.Should().Be(-2); r.AvgPrice.Should().Be(2.50m); } [Fact] public void BuyToClose_RemovesTheShort() { var r = PositionMath.Apply(-2, 2.50m, TradeSide.Buy, 2, 0.40m); r.Quantity.Should().Be(0); r.ClosedQuantity.Should().Be(2); } [Fact] public void BuyToClose_Partial_KeepsThePremiumBasis() { var r = PositionMath.Apply(-3, 2.50m, TradeSide.Buy, 1, 0.40m); r.Quantity.Should().Be(-2); r.AvgPrice.Should().Be(2.50m); r.ClosedQuantity.Should().Be(1); } [Fact] public void CrossingZero_ClosesTheOldPosition_AndOpensTheOppositeAtTheFillPrice() { // Short 1 Kontrakt, 3 gekauft: 1 geschlossen, 2 neu long – und der Einstand der neuen // Position ist der Ausfuehrungspreis, nicht die alte Praemie. var r = PositionMath.Apply(-1, 2.50m, TradeSide.Buy, 3, 0.40m); r.Quantity.Should().Be(2); r.AvgPrice.Should().Be(0.40m); r.ClosedQuantity.Should().Be(1); } [Fact] public void ZeroFill_ChangesNothing() { var r = PositionMath.Apply(5, 100m, TradeSide.Buy, 0, 120m); r.Quantity.Should().Be(5); r.AvgPrice.Should().Be(100m); } }