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()
{