From 39bfbccd44e1f10c7233a9be987187d88863c168 Mon Sep 17 00:00:00 2001 From: Louis Szeto <56447733+LouisSzeto@users.noreply.github.com> Date: Mon, 3 Jun 2024 23:38:52 +0800 Subject: [PATCH] Add Protective Collar to Option Strategies (#8025) * Add Protective Collar Option Strategy * Add buying power model for Protective Collar * Add unit test for Protective Collar * Add regression test for Protective Collar * Add unit test on position group buying power * allow same strike for later abstraction * minor bug fix * Set up conversion option strategy * add margin requirement * add unit tests * Add regression test * add reverse conversion definition * add reverse conversion and refactor conversion/collar margin model * added/modified unit tests for conversion/reverse conversion * minor bug fix on unit test, add regression test * Address peer review * update new set of IB testing data --- ...tionEquityConversionRegressionAlgorithm.cs | 122 +++++++++++ ...uityProtectiveCollarRegressionAlgorithm.cs | 123 +++++++++++ ...ityReverseConversionRegressionAlgorithm.cs | 122 +++++++++++ Common/Securities/Option/OptionStrategies.cs | 57 +++++ ...onStrategyPositionGroupBuyingPowerModel.cs | 78 +++++++ .../OptionStrategyDefinitions.cs | 36 +++ ...ategyPositionGroupBuyingPowerModelTests.cs | 207 ++++++++++++++++++ .../Options/OptionStrategiesTests.cs | 100 +++++++++ 8 files changed, 845 insertions(+) create mode 100644 Algorithm.CSharp/OptionEquityConversionRegressionAlgorithm.cs create mode 100644 Algorithm.CSharp/OptionEquityProtectiveCollarRegressionAlgorithm.cs create mode 100644 Algorithm.CSharp/OptionEquityReverseConversionRegressionAlgorithm.cs diff --git a/Algorithm.CSharp/OptionEquityConversionRegressionAlgorithm.cs b/Algorithm.CSharp/OptionEquityConversionRegressionAlgorithm.cs new file mode 100644 index 000000000000..09ddbf70ad9e --- /dev/null +++ b/Algorithm.CSharp/OptionEquityConversionRegressionAlgorithm.cs @@ -0,0 +1,122 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +using System; +using System.Linq; +using QuantConnect.Data; +using QuantConnect.Data.Market; +using System.Collections.Generic; +using QuantConnect.Securities.Option.StrategyMatcher; +using QuantConnect.Securities.Option; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm exercising an equity Conversion option strategy and asserting it's being detected by Lean and works as expected + /// + public class OptionEquityConversionRegressionAlgorithm : OptionEquityBaseStrategyRegressionAlgorithm + { + /// + /// OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. + /// + /// Slice object keyed by symbol containing the stock data + public override void OnData(Slice slice) + { + if (!Portfolio.Invested) + { + OptionChain chain; + if (IsMarketOpen(_optionSymbol) && slice.OptionChains.TryGetValue(_optionSymbol, out chain)) + { + var contracts = chain + .OrderByDescending(x => x.Expiry) + .ThenBy(x => x.Strike) + .ToList(); + + var call = contracts.Last(contract => contract.Right == OptionRight.Call); + var put = contracts.Single(contract => contract.Right == OptionRight.Put && contract.Expiry == call.Expiry + && contract.Strike == call.Strike); + var underlying = call.Symbol.Underlying; + + var initialMargin = Portfolio.MarginRemaining; + MarketOrder(underlying, 100); + MarketOrder(call.Symbol, -1); + MarketOrder(put.Symbol, 1); + var freeMarginPostTrade = Portfolio.MarginRemaining; + AssertOptionStrategyIsPresent(OptionStrategyDefinitions.Conversion.Name, 1); + + var callInTheMoneyAmount = ((Option)Securities[call.Symbol]).GetIntrinsicValue(Securities[underlying].Price); + var expectedMarginUsage = (callInTheMoneyAmount + 0.1m * call.Strike) * 100; + + if (expectedMarginUsage != Portfolio.TotalMarginUsed) + { + throw new Exception("Unexpect margin used!"); + } + + // we payed the ask and value using the assets price + var priceSpreadDifference = GetPriceSpreadDifference(call.Symbol, put.Symbol, underlying); + if (initialMargin != (freeMarginPostTrade + expectedMarginUsage + _paidFees - priceSpreadDifference)) + { + throw new Exception("Unexpect margin remaining!"); + } + } + } + } + + /// + /// Data Points count of all timeslices of algorithm + /// + public override long DataPoints => 471135; + + /// + /// Data Points count of the algorithm history + /// + public override int AlgorithmHistoryDataPoints => 0; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public override Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "3"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "200000"}, + {"End Equity", "199859"}, + {"Net Profit", "0%"}, + {"Sharpe Ratio", "0"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0"}, + {"Beta", "0"}, + {"Annual Standard Deviation", "0"}, + {"Annual Variance", "0"}, + {"Information Ratio", "0"}, + {"Tracking Error", "0"}, + {"Treynor Ratio", "0"}, + {"Total Fees", "$3.00"}, + {"Estimated Strategy Capacity", "$1600000.00"}, + {"Lowest Capacity Asset", "GOOCV W78ZFMML01JA|GOOCV VP83T1ZUHROL"}, + {"Portfolio Turnover", "38.88%"}, + {"OrderListHash", "8d8b71fdb1faafde96e301b4b2f9ca7d"} + }; + } +} diff --git a/Algorithm.CSharp/OptionEquityProtectiveCollarRegressionAlgorithm.cs b/Algorithm.CSharp/OptionEquityProtectiveCollarRegressionAlgorithm.cs new file mode 100644 index 000000000000..faacaef2b660 --- /dev/null +++ b/Algorithm.CSharp/OptionEquityProtectiveCollarRegressionAlgorithm.cs @@ -0,0 +1,123 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +using System; +using System.Linq; +using QuantConnect.Data; +using QuantConnect.Securities; +using QuantConnect.Data.Market; +using System.Collections.Generic; +using QuantConnect.Securities.Option.StrategyMatcher; +using QuantConnect.Securities.Option; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm exercising an equity Protective Collar option strategy and asserting it's being detected by Lean and works as expected + /// + public class OptionEquityProtectiveCollarRegressionAlgorithm : OptionEquityBaseStrategyRegressionAlgorithm + { + /// + /// OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. + /// + /// Slice object keyed by symbol containing the stock data + public override void OnData(Slice slice) + { + if (!Portfolio.Invested) + { + OptionChain chain; + if (IsMarketOpen(_optionSymbol) && slice.OptionChains.TryGetValue(_optionSymbol, out chain)) + { + var contracts = chain + .OrderByDescending(x => x.Expiry) + .ThenBy(x => x.Strike) + .ToList(); + + var call = contracts.Last(contract => contract.Right == OptionRight.Call); + var put = contracts.First(contract => contract.Right == OptionRight.Put && contract.Expiry == call.Expiry + && contract.Strike < call.Strike); + var underlying = call.Symbol.Underlying; + + var initialMargin = Portfolio.MarginRemaining; + MarketOrder(underlying, 100); + MarketOrder(call.Symbol, -1); + MarketOrder(put.Symbol, 1); + var freeMarginPostTrade = Portfolio.MarginRemaining; + AssertOptionStrategyIsPresent(OptionStrategyDefinitions.ProtectiveCollar.Name, 1); + + var putOutOfTheMoneyAmount = ((Option)Securities[put.Symbol]).OutOfTheMoneyAmount(Securities[underlying].Price); + var expectedMarginUsage = Math.Min(putOutOfTheMoneyAmount + 0.1m * put.Strike, 0.25m * call.Strike) * 100; + + if (expectedMarginUsage != Portfolio.TotalMarginUsed) + { + throw new Exception("Unexpect margin used!"); + } + + // we payed the ask and value using the assets price + var priceSpreadDifference = GetPriceSpreadDifference(call.Symbol, put.Symbol, underlying); + if (initialMargin != (freeMarginPostTrade + expectedMarginUsage + _paidFees - priceSpreadDifference)) + { + throw new Exception("Unexpect margin remaining!"); + } + } + } + } + + /// + /// Data Points count of all timeslices of algorithm + /// + public override long DataPoints => 471135; + + /// + /// Data Points count of the algorithm history + /// + public override int AlgorithmHistoryDataPoints => 0; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public override Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "3"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "200000"}, + {"End Equity", "199859"}, + {"Net Profit", "0%"}, + {"Sharpe Ratio", "0"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0"}, + {"Beta", "0"}, + {"Annual Standard Deviation", "0"}, + {"Annual Variance", "0"}, + {"Information Ratio", "0"}, + {"Tracking Error", "0"}, + {"Treynor Ratio", "0"}, + {"Total Fees", "$3.00"}, + {"Estimated Strategy Capacity", "$1600000.00"}, + {"Lowest Capacity Asset", "GOOCV W78ZFMML01JA|GOOCV VP83T1ZUHROL"}, + {"Portfolio Turnover", "38.71%"}, + {"OrderListHash", "74791244fa3c7fbefd47dd99c3cd6fa7"} + }; + } +} diff --git a/Algorithm.CSharp/OptionEquityReverseConversionRegressionAlgorithm.cs b/Algorithm.CSharp/OptionEquityReverseConversionRegressionAlgorithm.cs new file mode 100644 index 000000000000..8477d95b62a6 --- /dev/null +++ b/Algorithm.CSharp/OptionEquityReverseConversionRegressionAlgorithm.cs @@ -0,0 +1,122 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +using System; +using System.Linq; +using QuantConnect.Data; +using QuantConnect.Data.Market; +using System.Collections.Generic; +using QuantConnect.Securities.Option.StrategyMatcher; +using QuantConnect.Securities.Option; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm exercising an equity Reverse Conversion option strategy and asserting it's being detected by Lean and works as expected + /// + public class OptionEquityReverseConversionRegressionAlgorithm : OptionEquityBaseStrategyRegressionAlgorithm + { + /// + /// OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. + /// + /// Slice object keyed by symbol containing the stock data + public override void OnData(Slice slice) + { + if (!Portfolio.Invested) + { + OptionChain chain; + if (IsMarketOpen(_optionSymbol) && slice.OptionChains.TryGetValue(_optionSymbol, out chain)) + { + var contracts = chain + .OrderByDescending(x => x.Expiry) + .ThenBy(x => x.Strike) + .ToList(); + + var call = contracts.Last(contract => contract.Right == OptionRight.Call); + var put = contracts.Single(contract => contract.Right == OptionRight.Put && contract.Expiry == call.Expiry + && contract.Strike == call.Strike); + var underlying = call.Symbol.Underlying; + + var initialMargin = Portfolio.MarginRemaining; + MarketOrder(underlying, -100); + MarketOrder(call.Symbol, 1); + MarketOrder(put.Symbol, -1); + var freeMarginPostTrade = Portfolio.MarginRemaining; + AssertOptionStrategyIsPresent(OptionStrategyDefinitions.ReverseConversion.Name, 1); + + var putInTheMoneyAmount = ((Option)Securities[put.Symbol]).GetIntrinsicValue(Securities[underlying].Price); + var expectedMarginUsage = (putInTheMoneyAmount + 0.1m * call.Strike) * 100; + + if (expectedMarginUsage != Portfolio.TotalMarginUsed) + { + throw new Exception("Unexpect margin used!"); + } + + // we payed the ask and value using the assets price + var priceSpreadDifference = GetPriceSpreadDifference(call.Symbol, put.Symbol, underlying); + if (initialMargin != (freeMarginPostTrade + expectedMarginUsage + _paidFees - priceSpreadDifference)) + { + throw new Exception("Unexpect margin remaining!"); + } + } + } + } + + /// + /// Data Points count of all timeslices of algorithm + /// + public override long DataPoints => 471135; + + /// + /// Data Points count of the algorithm history + /// + public override int AlgorithmHistoryDataPoints => 0; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public override Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "3"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "200000"}, + {"End Equity", "199801"}, + {"Net Profit", "0%"}, + {"Sharpe Ratio", "0"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0"}, + {"Beta", "0"}, + {"Annual Standard Deviation", "0"}, + {"Annual Variance", "0"}, + {"Information Ratio", "0"}, + {"Tracking Error", "0"}, + {"Treynor Ratio", "0"}, + {"Total Fees", "$3.00"}, + {"Estimated Strategy Capacity", "$7500000.00"}, + {"Lowest Capacity Asset", "GOOCV W78ZFMML01JA|GOOCV VP83T1ZUHROL"}, + {"Portfolio Turnover", "38.84%"}, + {"OrderListHash", "722e8214812becc745646ff31fcbce1b"} + }; + } +} diff --git a/Common/Securities/Option/OptionStrategies.cs b/Common/Securities/Option/OptionStrategies.cs index 5263429e0cb7..235182831536 100644 --- a/Common/Securities/Option/OptionStrategies.cs +++ b/Common/Securities/Option/OptionStrategies.cs @@ -135,6 +135,63 @@ public static OptionStrategy ProtectivePut(Symbol canonicalOption, decimal strik return InvertStrategy(CoveredPut(canonicalOption, strike, expiration), OptionStrategyDefinitions.ProtectivePut.Name); } + /// + /// Creates a Protective Collar strategy that consists of buying 1 put contract and 1 lot of the underlying. + /// + /// Option symbol + /// The strike price for the call option contract + /// The strike price for the put option contract + /// Option expiration date + /// Option strategy specification + public static OptionStrategy ProtectiveCollar(Symbol canonicalOption, decimal callStrike, decimal putStrike, DateTime expiration) + { + if (callStrike < putStrike) + { + throw new ArgumentException("ProtectiveCollar: callStrike must be greater than putStrike", $"{nameof(callStrike)}, {nameof(putStrike)}"); + } + + // Since a protective collar is a combination of protective put and covered call + var coveredCall = CoveredCall(canonicalOption, callStrike, expiration); + var protectivePut = ProtectivePut(canonicalOption, putStrike, expiration); + + return new OptionStrategy + { + Name = OptionStrategyDefinitions.ProtectiveCollar.Name, + Underlying = canonicalOption.Underlying, + CanonicalOption = canonicalOption, + OptionLegs = coveredCall.OptionLegs.Concat(protectivePut.OptionLegs).ToList(), + UnderlyingLegs = coveredCall.UnderlyingLegs // only 1 lot of long stock position + }; + } + + /// + /// Creates a Conversion strategy that consists of buying 1 put contract, 1 lot of the underlying and selling 1 call contract. + /// Put and call must have the same expiration date, underlying (multiplier), and strike price. + /// + /// Option symbol + /// The strike price for the call and put option contract + /// Option expiration date + /// Option strategy specification + public static OptionStrategy Conversion(Symbol canonicalOption, decimal strike, DateTime expiration) + { + var strategy = ProtectiveCollar(canonicalOption, strike, strike, expiration); + strategy.Name = OptionStrategyDefinitions.Conversion.Name; + return strategy; + } + + /// + /// Creates a Reverse Conversion strategy that consists of buying 1 put contract and 1 lot of the underlying. + /// + /// Option symbol + /// The strike price for the put option contract + /// Option expiration date + /// Option strategy specification + public static OptionStrategy ReverseConversion(Symbol canonicalOption, decimal strike, DateTime expiration) + { + // Since a reverse conversion is an inverted conversion, we can just use the Conversion method and invert the legs + return InvertStrategy(Conversion(canonicalOption, strike, expiration), OptionStrategyDefinitions.ReverseConversion.Name); + } + /// /// Creates a Naked Call strategy that consists of selling 1 call contract. /// diff --git a/Common/Securities/Option/OptionStrategyPositionGroupBuyingPowerModel.cs b/Common/Securities/Option/OptionStrategyPositionGroupBuyingPowerModel.cs index 880bf426d4f1..e6c6760783c4 100644 --- a/Common/Securities/Option/OptionStrategyPositionGroupBuyingPowerModel.cs +++ b/Common/Securities/Option/OptionStrategyPositionGroupBuyingPowerModel.cs @@ -120,6 +120,36 @@ public override MaintenanceMargin GetMaintenanceMargin(PositionGroupMaintenanceM return new MaintenanceMargin(inAccountCurrency); } + else if (_optionStrategy.Name == OptionStrategyDefinitions.ProtectiveCollar.Name) + { + // Minimum (((10% * Put Strike Price) + Put Out of the Money Amount), (25% * Call Strike Price)) + var putPosition = parameters.PositionGroup.Positions.Single(position => + position.Symbol.SecurityType.IsOption() && position.Symbol.ID.OptionRight == OptionRight.Put); + var callPosition = parameters.PositionGroup.Positions.Single(position => + position.Symbol.SecurityType.IsOption() && position.Symbol.ID.OptionRight == OptionRight.Call); + var underlyingPosition = parameters.PositionGroup.Positions.FirstOrDefault(position => !position.Symbol.SecurityType.IsOption()); + var putSecurity = (Option)parameters.Portfolio.Securities[putPosition.Symbol]; + var callSecurity = (Option)parameters.Portfolio.Securities[callPosition.Symbol]; + var underlyingSecurity = parameters.Portfolio.Securities[underlyingPosition.Symbol]; + + var putMarginRequirement = 0.1m * putSecurity.StrikePrice + putSecurity.OutOfTheMoneyAmount(underlyingSecurity.Price); + var callMarginRequirement = 0.25m * callSecurity.StrikePrice; + + // call and put has the exact same number of contracts + var contractUnits = Math.Abs(putPosition.Quantity) * putSecurity.ContractUnitOfTrade; + var result = Math.Min(putMarginRequirement, callMarginRequirement) * contractUnits; + var inAccountCurrency = parameters.Portfolio.CashBook.ConvertToAccountCurrency(result, underlyingSecurity.QuoteCurrency.Symbol); + + return new MaintenanceMargin(inAccountCurrency); + } + else if (_optionStrategy.Name == OptionStrategyDefinitions.Conversion.Name) + { + return GetConversionMaintenanceMargin(parameters.PositionGroup, parameters.Portfolio, OptionRight.Call); + } + else if (_optionStrategy.Name == OptionStrategyDefinitions.ReverseConversion.Name) + { + return GetConversionMaintenanceMargin(parameters.PositionGroup, parameters.Portfolio, OptionRight.Put); + } else if (_optionStrategy.Name == OptionStrategyDefinitions.NakedCall.Name || _optionStrategy.Name == OptionStrategyDefinitions.NakedPut.Name) { @@ -262,6 +292,14 @@ public override InitialMargin GetInitialMarginRequirement(PositionGroupInitialMa // Initial Stock Margin Requirement + In the Money Amount result = GetMaintenanceMargin(new PositionGroupMaintenanceMarginParameters(parameters.Portfolio, parameters.PositionGroup)); } + else if (_optionStrategy.Name == OptionStrategyDefinitions.ProtectiveCollar.Name || _optionStrategy.Name == OptionStrategyDefinitions.Conversion.Name) + { + result = GetCollarConversionInitialMargin(parameters.PositionGroup, parameters.Portfolio, OptionRight.Call); + } + else if (_optionStrategy.Name == OptionStrategyDefinitions.ReverseConversion.Name) + { + result = GetCollarConversionInitialMargin(parameters.PositionGroup, parameters.Portfolio, OptionRight.Put); + } else if (_optionStrategy.Name == OptionStrategyDefinitions.NakedCall.Name || _optionStrategy.Name == OptionStrategyDefinitions.NakedPut.Name) { @@ -489,5 +527,45 @@ private static decimal GetShortStraddleStrangleMargin(IPositionGroup positionGro return result; } + + /// + /// Returns the maintenance margin for a conversion or reverse conversion. + /// + private static decimal GetConversionMaintenanceMargin(IPositionGroup positionGroup, SecurityPortfolioManager portfolio, OptionRight optionRight) + { + // 10% * Strike Price + Call/Put In the Money Amount + var optionPosition = positionGroup.Positions.Single(position => + position.Symbol.SecurityType.IsOption() && position.Symbol.ID.OptionRight == optionRight); + var underlyingPosition = positionGroup.Positions.FirstOrDefault(position => !position.Symbol.SecurityType.IsOption()); + var optionSecurity = (Option)portfolio.Securities[optionPosition.Symbol]; + var underlyingSecurity = portfolio.Securities[underlyingPosition.Symbol]; + + var marginRequirement = 0.1m * optionSecurity.StrikePrice + optionSecurity.GetIntrinsicValue(underlyingSecurity.Price); + var result = marginRequirement * Math.Abs(optionPosition.Quantity) * optionSecurity.ContractUnitOfTrade; + var inAccountCurrency = portfolio.CashBook.ConvertToAccountCurrency(result, underlyingSecurity.QuoteCurrency.Symbol); + + return new MaintenanceMargin(inAccountCurrency); + } + + /// + /// Returns the initial margin requirement for a collar, conversion, or reverse conversion. + /// + private static decimal GetCollarConversionInitialMargin(IPositionGroup positionGroup, SecurityPortfolioManager portfolio, OptionRight optionRight) + { + // Initial Stock Margin Requirement + In the Money Call/Put Amount + var optionPosition = positionGroup.Positions.Single(position => + position.Symbol.SecurityType.IsOption() && position.Symbol.ID.OptionRight == optionRight); + var underlyingPosition = positionGroup.Positions.Single(position => !position.Symbol.SecurityType.IsOption()); + var optionSecurity = (Option)portfolio.Securities[optionPosition.Symbol]; + var underlyingSecurity = portfolio.Securities[underlyingPosition.Symbol]; + + var intrinsicValue = optionSecurity.GetIntrinsicValue(underlyingSecurity.Price); + var inTheMoneyAmount = intrinsicValue * optionSecurity.ContractUnitOfTrade * Math.Abs(optionPosition.Quantity); + + var initialMarginRequirement = underlyingSecurity.BuyingPowerModel.GetInitialMarginRequirement(underlyingSecurity, underlyingPosition.Quantity); + + var result = Math.Abs(initialMarginRequirement) + inTheMoneyAmount; + return portfolio.CashBook.ConvertToAccountCurrency(result, optionSecurity.QuoteCurrency.Symbol); + } } } diff --git a/Common/Securities/Option/StrategyMatcher/OptionStrategyDefinitions.cs b/Common/Securities/Option/StrategyMatcher/OptionStrategyDefinitions.cs index 15206b6b0314..92f40e28f84d 100644 --- a/Common/Securities/Option/StrategyMatcher/OptionStrategyDefinitions.cs +++ b/Common/Securities/Option/StrategyMatcher/OptionStrategyDefinitions.cs @@ -104,6 +104,42 @@ public static ImmutableList AllDefinitions OptionStrategyDefinition.PutLeg(1) ); + /// + /// Hold 1 lot of the underlying, sell 1 call contract and buy 1 put contract. + /// The strike price of the short call is below the strike of the long put with the same expiration. + /// + /// Combination of and + public static OptionStrategyDefinition ProtectiveCollar { get; } + = OptionStrategyDefinition.Create("Protective Collar", 1, + OptionStrategyDefinition.CallLeg(-1), + OptionStrategyDefinition.PutLeg(1, (legs, p) => p.Strike < legs[0].Strike, + (legs, p) => p.Expiration == legs[0].Expiration) + ); + + /// + /// Hold 1 lot of the underlying, sell 1 call contract and buy 1 put contract. + /// The strike price of the call and put are the same, with the same expiration. + /// + /// A special case of + public static OptionStrategyDefinition Conversion { get; } + = OptionStrategyDefinition.Create("Conversion", 1, + OptionStrategyDefinition.CallLeg(-1), + OptionStrategyDefinition.PutLeg(1, (legs, p) => p.Strike == legs[0].Strike, + (legs, p) => p.Expiration == legs[0].Expiration) + ); + + /// + /// Hold 1 lot of the underlying, sell 1 call contract and buy 1 put contract. + /// The strike price of the call and put are the same, with the same expiration. + /// + /// Inverse of + public static OptionStrategyDefinition ReverseConversion { get; } + = OptionStrategyDefinition.Create("Reverse Conversion", -1, + OptionStrategyDefinition.CallLeg(1), + OptionStrategyDefinition.PutLeg(-1, (legs, p) => p.Strike == legs[0].Strike, + (legs, p) => p.Expiration == legs[0].Expiration) + ); + /// /// Sell 1 call contract without holding the underlying /// diff --git a/Tests/Common/Securities/OptionStrategyPositionGroupBuyingPowerModelTests.cs b/Tests/Common/Securities/OptionStrategyPositionGroupBuyingPowerModelTests.cs index 74a9bf55327e..c20a074e099c 100644 --- a/Tests/Common/Securities/OptionStrategyPositionGroupBuyingPowerModelTests.cs +++ b/Tests/Common/Securities/OptionStrategyPositionGroupBuyingPowerModelTests.cs @@ -159,6 +159,51 @@ public void Setup() new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -20, 20, true), // -20 to 0 new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -20, -(1000000 - 20 * 10250) / (10250 + 0), true), // -20 to max short new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -20, -(1000000 - 20 * 10250) / (10250 + 0) - 1, false), // -20 to max short + 1 + // Initial margin requirement|premium for ProtectiveCollar with quantities 1 and -1 are 26231|0 and 26231|1 respectively + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 0, (1000000 - 0 * 26231) / (26231 + 0), true), // 0 to max long + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 0, (1000000 - 0 * 26231) / (26231 + 0) + 1, false), // 0 to max long + 1 + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 0, -(1000000 + 0 * 26231) / (26231 + 1), true), // 0 to max short + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 0, -(1000000 + 0 * 26231) / (26231 + 1) - 1, false), // 0 to max short + 1 + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 20, (1000000 - 20 * 26231) / (26231 + 0), true), // 20 to max long + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 20, (1000000 - 20 * 26231) / (26231 + 0) + 1, false), // 20 to max long + 1 + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 20, -20, true), // 20 to 0 + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 20, -(1000000 + 20 * 26231) / (26231 + 1), true), // 20 to max short + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 20, -(1000000 + 20 * 26231) / (26231 + 1) - 1, false), // 20 to max short + 1 + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -20, (1000000 + 20 * 26231) / (26231 + 0), true), // -20 to max long + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -20, (1000000 + 20 * 26231) / (26231 + 0) + 1, false), // -20 to max long + 1 + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -20, 20, true), // -20 to 0 + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -20, -(1000000 - 20 * 26231) / (26231 + 1), true), // -20 to max short + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -20, -(1000000 - 20 * 26231) / (26231 + 1) - 1, false), // -20 to max short + 1 + // Initial margin requirement|premium for Conversion with quantities 1 and -1 are 26295|0 and 26231|146 respectively + new TestCaseData(OptionStrategyDefinitions.Conversion, 0, (1000000 - 0 * 26295) / (26295 + 0), true), // 0 to max long + new TestCaseData(OptionStrategyDefinitions.Conversion, 0, (1000000 - 0 * 26295) / (26295 + 0) + 1, false), // 0 to max long + 1 + new TestCaseData(OptionStrategyDefinitions.Conversion, 0, -(1000000 + 0 * 26231) / (26231 + 146), true), // 0 to max short + new TestCaseData(OptionStrategyDefinitions.Conversion, 0, -(1000000 + 0 * 26231) / (26231 + 146) - 1, false), // 0 to max short + 1 + new TestCaseData(OptionStrategyDefinitions.Conversion, 20, (1000000 - 20 * 26295) / (26295 + 0), true), // 20 to max long + new TestCaseData(OptionStrategyDefinitions.Conversion, 20, (1000000 - 20 * 26295) / (26295 + 0) + 1, false), // 20 to max long + 1 + new TestCaseData(OptionStrategyDefinitions.Conversion, 20, -20, true), // 20 to 0 + new TestCaseData(OptionStrategyDefinitions.Conversion, 20, -(1000000 + 20 * 26231) / (26231 + 146), true), // 20 to max short + new TestCaseData(OptionStrategyDefinitions.Conversion, 20, -(1000000 + 20 * 26231) / (26231 + 146) - 1, false), // 20 to max short + 1 + new TestCaseData(OptionStrategyDefinitions.Conversion, -20, (1000000 + 20 * 26295) / (26295 + 0), true), // -20 to max long + new TestCaseData(OptionStrategyDefinitions.Conversion, -20, (1000000 + 20 * 26295) / (26295 + 0) + 1, false), // -20 to max long + 1 + new TestCaseData(OptionStrategyDefinitions.Conversion, -20, 20, true), // -20 to 0 + new TestCaseData(OptionStrategyDefinitions.Conversion, -20, -(1000000 - 20 * 26231) / (26231 + 146), true), // -20 to max short + new TestCaseData(OptionStrategyDefinitions.Conversion, -20, -(1000000 - 20 * 26231) / (26231 + 146) - 1, false), // -20 to max short + 1 + // Initial margin requirement|premium for ReverseConversion with quantities 1 and -1 are 26231|146 and 26295|0 respectively + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 0, (1000000 - 0 * 26231) / (26231 + 146), true), // 0 to max long + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 0, (1000000 - 0 * 26231) / (26231 + 146) + 1, false), // 0 to max long + 1 + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 0, -(1000000 + 0 * 26295) / (26295 + 0), true), // 0 to max short + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 0, -(1000000 + 0 * 26295) / (26295 + 0) - 1, false), // 0 to max short + 1 + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 20, (1000000 - 20 * 26231) / (26231 + 146), true), // 20 to max long + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 20, (1000000 - 20 * 26231) / (26231 + 146) + 1, false), // 20 to max long + 1 + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 20, -20, true), // 20 to 0 + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 20, -(1000000 + 20 * 26295) / (26295 + 0), true), // 20 to max short + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 20, -(1000000 + 20 * 26295) / (26295 + 0) - 1, false), // 20 to max short + 1 + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -20, (1000000 + 20 * 26231) / (26231 + 146), true), // -20 to max long + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -20, (1000000 + 20 * 26231) / (26231 + 146) + 1, false), // -20 to max long + 1 + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -20, 20, true), // -20 to 0 + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -20, -(1000000 - 20 * 26295) / (26295 + 0), true), // -20 to max short + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -20, -(1000000 - 20 * 26295) / (26295 + 0) - 1, false), // -20 to max short + 1 // Initial margin requirement|premium for BearCallSpread with quantities 1 and -1 are 1000|0 and 0|1200 respectively new TestCaseData(OptionStrategyDefinitions.BearCallSpread, 0, (1000000 - 0 * 1000) / (1000 + 0), true), // 0 to max long new TestCaseData(OptionStrategyDefinitions.BearCallSpread, 0, (1000000 - 0 * 1000) / (1000 + 0) + 1, false), // 0 to max long + 1 @@ -648,6 +693,12 @@ public void OrderQuantityCalculation(int initialHoldingsQuantity, decimal target new TestCaseData(OptionStrategyDefinitions.CoveredPut, -1, 10000m), // IB: 10276.15 new TestCaseData(OptionStrategyDefinitions.ProtectivePut, 1, 10000m), // IB: inverted covered put new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -1, 12000m), // IB: covered put + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 1, 26231m), // IB: 26231 + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -1, 26231m), // IB: same as long + new TestCaseData(OptionStrategyDefinitions.Conversion, 1, 26295m), // IB: 26295 + new TestCaseData(OptionStrategyDefinitions.Conversion, -1, 26230m), // IB: reverse conversion + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 1, 26231m), // IB: 26231 + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -1, 26295m), // IB: conversion new TestCaseData(OptionStrategyDefinitions.BearCallSpread, 1, 1000m), // IB: 1000 new TestCaseData(OptionStrategyDefinitions.BearCallSpread, -1, 0m), // IB: 0 new TestCaseData(OptionStrategyDefinitions.BearPutSpread, 1, 0m), // IB: 0 @@ -746,6 +797,12 @@ public void CoveredCallInitialMarginRequirement(OptionStrategyDefinition optionS new TestCaseData(OptionStrategyDefinitions.CoveredPut, -1, 10000m), // IB: 10276 new TestCaseData(OptionStrategyDefinitions.ProtectivePut, 1, 10000m), // IB: inverted covered Put new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -1, 10250m), // IB: covered Put + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 1, 6202m), // IB: 6202 + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -1, 6202m), // IB: same as long + new TestCaseData(OptionStrategyDefinitions.Conversion, 1, 5303m), // IB: 5303 + new TestCaseData(OptionStrategyDefinitions.Conversion, -1, 5240m), // IB: reverse conversion + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 1, 5240m), // IB: 5240 + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -1, 5303m), // IB: conversion new TestCaseData(OptionStrategyDefinitions.BearCallSpread, 1, 1000m), // IB: 10000 new TestCaseData(OptionStrategyDefinitions.BearCallSpread, -1, 0m), // IB: 0 new TestCaseData(OptionStrategyDefinitions.BearPutSpread, 1, 0m), // IB: 0 @@ -857,6 +914,32 @@ public void GetsMaintenanceMargin(OptionStrategyDefinition optionStrategyDefinit new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -10, -102500m / 10, -1), new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -10, -102500m, -10), new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -10, -102500m - 102520m, -20), + // Initial margin requirement (including premium) for ProtectiveCollar with quantity 10 and -10 is 262310 and 262318 respectively + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, 262310m / 10, +1), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, -262310m / 10, -1), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, -262310m, -10), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, -262310m - 262318m, -20), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, 262318m / 10, +1), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, -262318m / 10, -1), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, -262318m - 262310m, -20), + // Initial margin requirement (including premium) for Conversion with quantity 10 and -10 is 262945 and 263778 respectively + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, 262945m / 10, +1), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, -262945m / 10, -1), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, -262945m, -10), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, -262945m - 263778m, -20), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, 263778m / 10, +1), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, -263778m / 10, -1), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, -263778m, -10), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, -263778m - 262945m, -20), + // Initial margin requirement (including premium) for ReverseConversion with quantity 10 and -10 is 263768 and 262915 respectively + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, 263768m / 10, +1), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, -263768m / 10, -1), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, -263768m, -10), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, -263768m - 262915m, -20), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, 262915m / 10, +1), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, -262915m / 10, -1), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, -262915m, -10), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, -262915m - 263768m, -20), // Initial margin requirement (including premium) for BearCallSpread with quantity 10 and -10 is 10000 and 12000 respectively new TestCaseData(OptionStrategyDefinitions.BearCallSpread, 10, 10000m / 10, +1), new TestCaseData(OptionStrategyDefinitions.BearCallSpread, 10, -10000m / 10, -1), @@ -1199,6 +1282,33 @@ public void PositionGroupOrderQuantityCalculationForDeltaBuyingPowerWithCustomPo new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -10, 102500m * 9 / 10, -1), new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -10, 0m, -10), new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -10, -102520m, -20), + // Initial margin requirement (including premium) for ProtectiveCollar with quantity 10 and -10 is 262310m and 262318m respectively + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, 262310m * 11 / 10, +1), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, 262310m * 9 / 10, -1), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, 0m, -10), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, -262318m, -20), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, 262318m * 11 / 10, +1), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, 262318m * 9 / 10, -1), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, 0m, -10), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, -262310m, -20), + // Initial margin requirement (including premium) for Conversion with quantity 10 and -10 is 262945m and 263778m respectively + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, 262945m * 11 / 10, +1), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, 262945m * 9 / 10, -1), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, 0m, -10), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, -263778m, -20), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, 263778m * 11 / 10, +1), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, 263778m * 9 / 10, -1), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, 0m, -10), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, -262945m, -20), + // Initial margin requirement (including premium) for ReverseConversion with quantity 10 and -10 is 263768m and 262915m respectively + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, 263768m * 11 / 10, +1), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, 263768m * 9 / 10, -1), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, 0m, -10), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, -262915m, -20), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, 262915m * 11 / 10, +1), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, 262915m * 9 / 10, -1), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, 0m, -10), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, -263768m, -20), // Initial margin requirement (including premium) for BearCallSpread with quantity 10 and -10 is 10000 and 12000 respectively new TestCaseData(OptionStrategyDefinitions.BearCallSpread, 10, 10000m * 11 / 10, +1), new TestCaseData(OptionStrategyDefinitions.BearCallSpread, 10, 10000m * 9 / 10, -1), @@ -1491,6 +1601,30 @@ public void PositionGroupOrderQuantityCalculationForTargetBuyingPowerWithCustomP new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -10, 1, (1000000m - 102500m) + 102500m + 102500m), new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -10, 10, (1000000m - 102500m) + 102500m + 102500m), new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -10, 20, (1000000m - 102500m) + 102500m + 102500m), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, 1, 1000000m - 62020m), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, -1, (1000000m - 62020m) + 62020m + 262318m), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, -10, (1000000m - 62020m) + 62020m + 262318m), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, -20, (1000000m - 62020m) + 62020m + 262318m), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, -1, 1000000m - 62020m), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, 1, (1000000m - 62020m) + 62020m + 262310m), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, 10, (1000000m - 62020m) + 62020m + 262310m), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, 20, (1000000m - 62020m) + 62020m + 262310m), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, 1, 1000000m - 53030m), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, -1, (1000000m - 53030m) + 53030m + 262945m), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, -10, (1000000m - 53030m) + 53030m + 262945m), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, -20, (1000000m - 53030m) + 53030m + 262945m), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, -1, 1000000m - 52400m), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, 1, (1000000m - 52400m) + 52400m + 263778m), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, 10, (1000000m - 52400m) + 52400m + 263778m), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, 20, (1000000m - 52400m) + 52400m + 263778m), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, 1, 1000000m - 52400m), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, -1, (1000000m - 52400m) + 52400m + 263768m), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, -10, (1000000m - 52400m) + 52400m + 263768m), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, -20, (1000000m - 52400m) + 52400m + 263768m), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, -1, 1000000m - 53010m), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, 1, (1000000m - 53010m) + 53010m + 262915m), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, 10, (1000000m - 53010m) + 53010m + 262915m), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, 20, (1000000m - 53010m) + 53010m + 262915m), new TestCaseData(OptionStrategyDefinitions.NakedCall, 10, +1, 1000000m - 194000m), new TestCaseData(OptionStrategyDefinitions.NakedCall, 10, -1, (1000000m - 194000m) + 194000m + 194000m), new TestCaseData(OptionStrategyDefinitions.NakedCall, 10, -10, (1000000m - 194000m) + 194000m + 194000m), @@ -1940,6 +2074,30 @@ public void BuyingPowerForOptionStartingFromStrategyWithALegInTheOppositeDirecti new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -10, 1), new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -10, 10), new TestCaseData(OptionStrategyDefinitions.ProtectivePut, -10, 20), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, 1), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, -1), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, -10), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, 10, -20), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, -1), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, 1), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, 10), + new TestCaseData(OptionStrategyDefinitions.ProtectiveCollar, -10, 20), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, 1), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, -1), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, -10), + new TestCaseData(OptionStrategyDefinitions.Conversion, 10, -20), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, -1), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, 1), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, 10), + new TestCaseData(OptionStrategyDefinitions.Conversion, -10, 20), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, 1), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, -1), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, -10), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, 10, -20), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, -1), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, 1), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, 10), + new TestCaseData(OptionStrategyDefinitions.ReverseConversion, -10, 20), new TestCaseData(OptionStrategyDefinitions.BearCallSpread, 10, 1), new TestCaseData(OptionStrategyDefinitions.BearCallSpread, 10, -1), new TestCaseData(OptionStrategyDefinitions.BearCallSpread, 10, -10), @@ -2261,6 +2419,18 @@ private IPositionGroup SetUpOptionStrategy(OptionStrategyDefinition optionStrate var spyMay17_300Put = _algorithm.AddOptionContract(Symbols.CreateOptionSymbol("SPY", OptionRight.Put, 300, may172023)); spyMay17_300Put.SetMarketPrice(new Tick { Value = 0.01m }); + var jun212024 = new DateTime(2024, 06, 21); + + var spyJun21_534Call = _algorithm.AddOptionContract(Symbols.CreateOptionSymbol("SPY", OptionRight.Call, 534, jun212024)); + spyJun21_534Call.SetMarketPrice(new Tick { Value = 0.01m }); + var spyJun21_524Call = _algorithm.AddOptionContract(Symbols.CreateOptionSymbol("SPY", OptionRight.Call, 524, jun212024)); + spyJun21_524Call.SetMarketPrice(new Tick { Value = 2.29m }); + + var spyJun21_524Put = _algorithm.AddOptionContract(Symbols.CreateOptionSymbol("SPY", OptionRight.Put, 524, jun212024)); + spyJun21_524Put.SetMarketPrice(new Tick { Value = 0.827m }); + var spyJun21_514Put = _algorithm.AddOptionContract(Symbols.CreateOptionSymbol("SPY", OptionRight.Put, 514, jun212024)); + spyJun21_514Put.SetMarketPrice(new Tick { Value = 0.018m }); + _equity.SetMarketPrice(new Tick { Value = 410m }); _equity.SetLeverage(4); @@ -2337,6 +2507,43 @@ private IPositionGroup SetUpOptionStrategy(OptionStrategyDefinition optionStrate expectedPositionGroupBPMStrategy = OptionStrategyDefinitions.CoveredPut.Name; } } + else if (optionStrategyDefinition.Name == OptionStrategyDefinitions.ProtectiveCollar.Name) + { + _equity.SetMarketPrice(new Tick { Value = 524.62m }); + _equity.SetLeverage(2); + + _equity.Holdings.SetHoldings(_equity.Price, initialHoldingsQuantity * _putOption.ContractMultiplier); + spyJun21_534Call.Holdings.SetHoldings(spyJun21_534Call.Price, -initialHoldingsQuantity); + spyJun21_514Put.Holdings.SetHoldings(spyJun21_514Put.Price, initialHoldingsQuantity); + } + else if (optionStrategyDefinition.Name == OptionStrategyDefinitions.Conversion.Name) + { + _equity.SetMarketPrice(new Tick { Value = 524.63m }); + _equity.SetLeverage(2); + + _equity.Holdings.SetHoldings(_equity.Price, initialHoldingsQuantity * _putOption.ContractMultiplier); + spyJun21_524Call.Holdings.SetHoldings(spyJun21_524Call.Price, -initialHoldingsQuantity); + spyJun21_524Put.Holdings.SetHoldings(spyJun21_524Put.Price, initialHoldingsQuantity); + + if (initialHoldingsQuantity < 0) + { + expectedPositionGroupBPMStrategy = OptionStrategyDefinitions.ReverseConversion.Name; + } + } + else if (optionStrategyDefinition.Name == OptionStrategyDefinitions.ReverseConversion.Name) + { + _equity.SetMarketPrice(new Tick { Value = 524.61m }); + _equity.SetLeverage(2); + + _equity.Holdings.SetHoldings(_equity.Price, -initialHoldingsQuantity * _putOption.ContractMultiplier); + spyJun21_524Call.Holdings.SetHoldings(spyJun21_524Call.Price, initialHoldingsQuantity); + spyJun21_524Put.Holdings.SetHoldings(spyJun21_524Put.Price, -initialHoldingsQuantity); + + if (initialHoldingsQuantity < 0) + { + expectedPositionGroupBPMStrategy = OptionStrategyDefinitions.Conversion.Name; + } + } else if (optionStrategyDefinition.Name == OptionStrategyDefinitions.BearCallSpread.Name) { var shortCallOption = spyMay19_300Call; diff --git a/Tests/Common/Securities/Options/OptionStrategiesTests.cs b/Tests/Common/Securities/Options/OptionStrategiesTests.cs index e29bc591f223..66548e317a2d 100644 --- a/Tests/Common/Securities/Options/OptionStrategiesTests.cs +++ b/Tests/Common/Securities/Options/OptionStrategiesTests.cs @@ -133,6 +133,106 @@ public void BuildsProtectivePutStrategy() Assert.AreEqual(100, underlyingLeg.Quantity); } + [Test] + public void BuildsProtectiveCollarStrategy() + { + var canonicalOptionSymbol = Symbols.SPY_Option_Chain; + var underlying = Symbols.SPY; + var callStrike = 350m; + var putStrike = 300m; + var expiration = new DateTime(2023, 08, 18); + + var strategy = OptionStrategies.ProtectiveCollar(canonicalOptionSymbol, callStrike, putStrike, expiration); + + Assert.AreEqual(OptionStrategyDefinitions.ProtectiveCollar.Name, strategy.Name); + Assert.AreEqual(underlying, strategy.Underlying); + Assert.AreEqual(canonicalOptionSymbol, strategy.CanonicalOption); + Assert.AreEqual(2, strategy.OptionLegs.Count); + + var callOptionLeg = strategy.OptionLegs[0]; + Assert.AreEqual(OptionRight.Call, callOptionLeg.Right); + Assert.AreEqual(callStrike, callOptionLeg.Strike); + Assert.AreEqual(expiration, callOptionLeg.Expiration); + Assert.AreEqual(-1, callOptionLeg.Quantity); + + var putOptionLeg = strategy.OptionLegs[1]; + Assert.AreEqual(OptionRight.Put, putOptionLeg.Right); + Assert.AreEqual(putStrike, putOptionLeg.Strike); + Assert.AreEqual(expiration, putOptionLeg.Expiration); + Assert.AreEqual(1, putOptionLeg.Quantity); + + Assert.AreEqual(1, strategy.UnderlyingLegs.Count); + var underlyingLeg = strategy.UnderlyingLegs[0]; + Assert.AreEqual(underlying, underlyingLeg.Symbol); + Assert.AreEqual(100, underlyingLeg.Quantity); + } + + [Test] + public void BuildsConversionStrategy() + { + var canonicalOptionSymbol = Symbols.SPY_Option_Chain; + var underlying = Symbols.SPY; + var strike = 350m; + var expiration = new DateTime(2023, 08, 18); + + var strategy = OptionStrategies.Conversion(canonicalOptionSymbol, strike, expiration); + + Assert.AreEqual(OptionStrategyDefinitions.Conversion.Name, strategy.Name); + Assert.AreEqual(underlying, strategy.Underlying); + Assert.AreEqual(canonicalOptionSymbol, strategy.CanonicalOption); + Assert.AreEqual(2, strategy.OptionLegs.Count); + + var callOptionLeg = strategy.OptionLegs[0]; + Assert.AreEqual(OptionRight.Call, callOptionLeg.Right); + Assert.AreEqual(strike, callOptionLeg.Strike); + Assert.AreEqual(expiration, callOptionLeg.Expiration); + Assert.AreEqual(-1, callOptionLeg.Quantity); + + var putOptionLeg = strategy.OptionLegs[1]; + Assert.AreEqual(OptionRight.Put, putOptionLeg.Right); + Assert.AreEqual(strike, putOptionLeg.Strike); + Assert.AreEqual(expiration, putOptionLeg.Expiration); + Assert.AreEqual(1, putOptionLeg.Quantity); + + Assert.AreEqual(1, strategy.UnderlyingLegs.Count); + var underlyingLeg = strategy.UnderlyingLegs[0]; + Assert.AreEqual(underlying, underlyingLeg.Symbol); + Assert.AreEqual(100, underlyingLeg.Quantity); + } + + [Test] + public void BuildsReverseConversionStrategy() + { + var canonicalOptionSymbol = Symbols.SPY_Option_Chain; + var underlying = Symbols.SPY; + var strike = 350m; + var expiration = new DateTime(2023, 08, 18); + + var strategy = OptionStrategies.ReverseConversion(canonicalOptionSymbol, strike, expiration); + + Assert.AreEqual(OptionStrategyDefinitions.ReverseConversion.Name, strategy.Name); + Assert.AreEqual(underlying, strategy.Underlying); + Assert.AreEqual(canonicalOptionSymbol, strategy.CanonicalOption); + Assert.AreEqual(2, strategy.OptionLegs.Count); + + var callOptionLeg = strategy.OptionLegs[0]; + Assert.AreEqual(OptionRight.Call, callOptionLeg.Right); + Assert.AreEqual(strike, callOptionLeg.Strike); + Assert.AreEqual(expiration, callOptionLeg.Expiration); + Assert.AreEqual(1, callOptionLeg.Quantity); + + var putOptionLeg = strategy.OptionLegs[1]; + Assert.AreEqual(OptionRight.Put, putOptionLeg.Right); + Assert.AreEqual(strike, putOptionLeg.Strike); + Assert.AreEqual(expiration, putOptionLeg.Expiration); + Assert.AreEqual(-1, putOptionLeg.Quantity); + + Assert.AreEqual(1, strategy.UnderlyingLegs.Count); + var underlyingLeg = strategy.UnderlyingLegs[0]; + Assert.AreEqual(underlying, underlyingLeg.Symbol); + Assert.AreEqual(-100, underlyingLeg.Quantity); + } + [Test] public void BuildsNakedCallStrategy() {