using FluentAssertions; using IBKRTrader.Core.Capital; using IBKRTrader.Core.Trading; namespace IBKRTrader.Tests.Capital; /// /// Die Sperren des Betreibers (#485/#486) als reine Rechnung. Sie gelten für Module – der /// manuelle Handel in TWS bleibt unberührt. /// [Trait("cat", "unit")] public class TradingRulesTests { private static readonly InstrumentRef Aapl = InstrumentRef.ForStock("AAPL"); // ─── Positionssperre ───────────────────────────────────────────────────────── [Fact] public void Free_LetsEverythingThrough() { TradingRules.CheckPositionLock(PositionLock.Free, Aapl, openingQuantity: 10, closingQuantity: 0) .Should().BeNull(); } [Fact] public void CloseOnly_AllowsReducing_ButNotBuilding() { // Reines Schließen: erlaubt. TradingRules.CheckPositionLock(PositionLock.CloseOnly, Aapl, 0, 10).Should().BeNull(); // Aufstocken: abgelehnt. var breach = TradingRules.CheckPositionLock(PositionLock.CloseOnly, Aapl, 10, 0); breach.Should().NotBeNull(); breach!.Kind.Should().Be(LimitKind.PositionLocked); breach.MaxQuantity.Should().Be(0, "es gibt nichts zu schließen, also geht nichts"); } [Fact] public void CloseOnly_OnAMixedOrder_ReportsTheClosingPartAsStillPossible() { // Ein Auftrag, der 4 schließt und 6 neu aufmacht: der schließende Teil bleibt möglich. var breach = TradingRules.CheckPositionLock(PositionLock.CloseOnly, Aapl, openingQuantity: 6, closingQuantity: 4); breach.Should().NotBeNull(); breach!.MaxQuantity.Should().Be(4, "so viel ginge jetzt noch – das kann das Modul nutzen"); } [Fact] public void OpenOnly_IsTheMirrorImage() { TradingRules.CheckPositionLock(PositionLock.OpenOnly, Aapl, 10, 0).Should().BeNull(); var breach = TradingRules.CheckPositionLock(PositionLock.OpenOnly, Aapl, 0, 10); breach.Should().NotBeNull(); breach!.Kind.Should().Be(LimitKind.PositionLocked); } [Fact] public void Locked_BlocksTheExitToo() { // Das ist die unbequeme, aber gewollte Eigenschaft dieser Stufe: Sie verhindert auch das // Schließen. Wer sie setzt, muss das wissen - deshalb steht es hier als Test und nicht // nur im Kommentar. TradingRules.CheckPositionLock(PositionLock.Locked, Aapl, 10, 0).Should().NotBeNull(); TradingRules.CheckPositionLock(PositionLock.Locked, Aapl, 0, 10).Should().NotBeNull(); TradingRules.CheckPositionLock(PositionLock.Locked, Aapl, 0, 10)! .MaxQuantity.Should().Be(0); } // ─── Instrumentenregeln ────────────────────────────────────────────────────── [Fact] public void NoRule_LetsEverythingThrough() { TradingRules.CheckInstrumentRule(InstrumentRule.None, Aapl, 100, 0).Should().BeNull(); } [Fact] public void BlockedInstrument_CannotBeOpened() { var breach = TradingRules.CheckInstrumentRule(new InstrumentRule(Blocked: true), Aapl, 1, 0); breach.Should().NotBeNull(); breach!.Kind.Should().Be(LimitKind.InstrumentBlocked); breach.MaxQuantity.Should().Be(0); } [Fact] public void BlockedInstrument_CanStillBeClosed() { // Der schließende Teil kommt hier gar nicht an - geprüft wird nur, was eröffnet. // Ein nachträgliches Verbot darf keine Position einsperren, die man loswerden will. TradingRules.CheckInstrumentRule(new InstrumentRule(Blocked: true), Aapl, openingQuantity: 0, heldTotal: 100).Should().BeNull(); } [Fact] public void MaxQuantity_CountsWhatIsAlreadyHeld() { var rule = new InstrumentRule(MaxQuantity: 100); // 60 im Bestand, 40 dazu: genau an der Grenze, erlaubt. TradingRules.CheckInstrumentRule(rule, Aapl, openingQuantity: 40, heldTotal: 60) .Should().BeNull(); // 60 im Bestand, 50 dazu: darüber. var breach = TradingRules.CheckInstrumentRule(rule, Aapl, openingQuantity: 50, heldTotal: 60); breach.Should().NotBeNull(); breach!.Kind.Should().Be(LimitKind.InstrumentQuantity); breach.Current.Should().Be(110m); breach.Limit.Should().Be(100m); breach.MaxQuantity.Should().Be(40, "so viele gingen noch"); } [Fact] public void MaxQuantity_CountsShortPositionsByTheirSize() { // Eine geschriebene Option steht mit negativem Vorzeichen im Buch. "Höchstens 5 Kontrakte" // meint die Größe der Verpflichtung, nicht ihre Richtung. var rule = new InstrumentRule(MaxQuantity: 5); TradingRules.CheckInstrumentRule(rule, Aapl, openingQuantity: 2, heldTotal: -4) .Should().NotBeNull("4 geschriebene plus 2 weitere sind 6"); } [Fact] public void AlreadyOverTheLimit_LeavesNoRoom() { var rule = new InstrumentRule(MaxQuantity: 100); var breach = TradingRules.CheckInstrumentRule(rule, Aapl, openingQuantity: 1, heldTotal: 150); breach.Should().NotBeNull(); breach!.MaxQuantity.Should().Be(0, "über der Grenze gibt es keinen Rest, auch keinen negativen"); } }