using FluentAssertions; using IBKRTrader.Core.Capital; using IBKRTrader.Core.Trading; namespace IBKRTrader.Tests.Capital; /// /// Welche Aktien und welches Bargeld sind frei? Der Baustein, ohne den „hat er die Aktien?" beim /// zweiten Call eine ungedeckte Option schreibt. /// [Trait("cat", "unit")] public class CoverageCalculatorTests { private static readonly DateOnly Expiry = new(2026, 9, 18); private static Position Stock(int qty, decimal price = 140m) => new("OW", InstrumentRef.ForStock("AAPL"), qty, price); private static Position Option(OptionRight right, decimal strike, int qty, decimal premium = 2.50m) => new("OW", InstrumentRef.ForOption(new OptionSpec("AAPL", Expiry, strike, right)), qty, premium); [Fact] public void SharesAlone_AreAllFree() { var c = CoverageCalculator.For([Stock(300)]); c.Shares.Should().Be(300); c.SharesBoundByShortCalls.Should().Be(0); c.FreeShares.Should().Be(300); } [Fact] public void EachWrittenCall_BindsOneRoundLot() { var c = CoverageCalculator.For([Stock(300), Option(OptionRight.Call, 150m, -2)]); c.SharesBoundByShortCalls.Should().Be(200); c.FreeShares.Should().Be(100); } [Fact] public void BoughtOptions_BindNothing() { // Eine gekaufte Option ist ein Recht, keine Verpflichtung - sie deckt nichts und bindet nichts. var c = CoverageCalculator.For([Stock(100), Option(OptionRight.Call, 150m, +3)]); c.FreeShares.Should().Be(100); c.CashBoundByShortPuts.Should().Be(0m); } [Fact] public void WrittenPuts_BindCash_AtStrikeTimesMultiplier() { var c = CoverageCalculator.For([Option(OptionRight.Put, 150m, -2)]); c.CashBoundByShortPuts.Should().Be(30_000m); } [Fact] public void MoreShortCallsThanShares_LeavesNoFreeShares_ButNeverNegative() { // Sollte durch die Deckungspruefung nicht entstehen koennen; entstuende es doch durch einen // Abgleich, darf daraus keine negative "Deckung" werden, die eine weitere Option traegt. var c = CoverageCalculator.For([Stock(100), Option(OptionRight.Call, 150m, -3)]); c.FreeShares.Should().Be(0); } [Fact] public void ShortStockPosition_DoesNotCountAsCoverage() { var c = CoverageCalculator.For([Stock(-100)]); c.Shares.Should().Be(0); c.FreeShares.Should().Be(0); } [Fact] public void MaxCoveredContracts_ForCalls_CountsFreeRoundLots() { var call = new OptionSpec("AAPL", Expiry, 150m, OptionRight.Call); CoverageCalculator.MaxCoveredContracts(call, new(250, 0, 0m), 0m).Should().Be(2); CoverageCalculator.MaxCoveredContracts(call, new(250, 200, 0m), 0m).Should().Be(0); } [Fact] public void MaxCoveredContracts_ForPuts_CountsAffordableObligations() { var put = new OptionSpec("AAPL", Expiry, 150m, OptionRight.Put); CoverageCalculator.MaxCoveredContracts(put, default, 45_000m).Should().Be(3); CoverageCalculator.MaxCoveredContracts(put, default, 14_999m).Should().Be(0); CoverageCalculator.MaxCoveredContracts(put, default, -1m).Should().Be(0); } }