using FluentAssertions; using IBKRTrader.Core.Trading; using IBKRTrader.Modules.OptionsWheel.Logic; namespace IBKRTrader.Tests.Modules.OptionsWheel; /// /// Der Zustandsautomat je Basiswert. Er wird aus den Positionen abgeleitet und nicht /// fortgeschrieben – Zuteilung und Verfall ändern Positionen ohne Order von uns, ein mitgeführter /// Zustand liefe zwangsläufig aus dem Tritt. /// [Trait("cat", "unit")] public class WheelStateMachineTests { private static readonly DateOnly Expiry = new(2026, 9, 18); private static Position Shares(int qty) => new("OW", InstrumentRef.ForStock("AAPL"), qty, 140m); private static Position Contract(OptionRight right, int qty, decimal strike = 150m) => new("OW", InstrumentRef.ForOption(new OptionSpec("AAPL", Expiry, strike, right)), qty, 2.50m); [Fact] public void NoPositions_MeansIdle_AndTheNextStepIsAPut() { var state = WheelStateMachine.Derive([]); state.Should().Be(WheelState.Idle); WheelStateMachine.NextAction(state).Should().Be(WheelStateMachine.WheelAction.SellPut); } [Fact] public void AWrittenPut_MeansShortPut() { WheelStateMachine.Derive([Contract(OptionRight.Put, -1)]) .Should().Be(WheelState.ShortPut); } [Fact] public void SharesWithoutACall_MeanStock_AndTheNextStepIsACall() { // Der Zustand nach einer Zuteilung. var state = WheelStateMachine.Derive([Shares(100)]); state.Should().Be(WheelState.Stock); WheelStateMachine.NextAction(state).Should().Be(WheelStateMachine.WheelAction.SellCall); } [Fact] public void SharesWithAWrittenCall_MeanShortCall() { WheelStateMachine.Derive([Shares(100), Contract(OptionRight.Call, -1)]) .Should().Be(WheelState.ShortCall); } [Fact] public void BoughtOptions_DoNotCreateAWheelState() { // Eine gekaufte Option ist keine geschriebene; sie gehoert nicht zum Wheel. WheelStateMachine.Derive([Contract(OptionRight.Put, +2)]) .Should().Be(WheelState.Idle); } [Fact] public void APendingOrder_OutweighsThePositions() { // Sonst bekaeme derselbe Basiswert ein zweites Leg, waehrend das erste noch im Buch liegt. WheelStateMachine.Derive([Shares(100)], hasPendingOrder: true) .Should().Be(WheelState.PendingOrder); } [Fact] public void HaltedOutweighsEverything_AndStopsAnyAction() { var state = WheelStateMachine.Derive([Shares(100)], hasPendingOrder: true, halted: true); state.Should().Be(WheelState.Halted); WheelStateMachine.NextAction(state).Should().Be(WheelStateMachine.WheelAction.None); } [Fact] public void OpenLegs_AreManaged_NotReopened() { WheelStateMachine.NextAction(WheelState.ShortPut) .Should().Be(WheelStateMachine.WheelAction.ManageOpenLeg); WheelStateMachine.NextAction(WheelState.ShortCall) .Should().Be(WheelStateMachine.WheelAction.ManageOpenLeg); } [Fact] public void AssignmentIsARegularPath_NotAFault() { // Amerikanische Optionen koennen jederzeit ausgeuebt werden. Ein Automat, der Zuteilung als // Stoerfall behandelte, waere im Normalbetrieb staendig gestoert. WheelStateMachine.DescribeTransition(WheelState.ShortPut, WheelState.Stock) .Should().Contain("zugeteilt"); WheelStateMachine.DescribeTransition(WheelState.ShortCall, WheelState.Idle) .Should().Contain("abgerufen"); } }