diff --git a/Algorithm.CSharp/OptionChainFiltersRegressionAlgorithm.cs b/Algorithm.CSharp/OptionChainFiltersRegressionAlgorithm.cs new file mode 100644 index 000000000000..d73971b1fa41 --- /dev/null +++ b/Algorithm.CSharp/OptionChainFiltersRegressionAlgorithm.cs @@ -0,0 +1,195 @@ +/* + * 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.Collections.Generic; +using System.Linq; +using QuantConnect.Data; +using QuantConnect.Data.Market; +using QuantConnect.Interfaces; +using QuantConnect.Securities.Option; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm demonstrating that option chains can be filtered with the same filters used for + /// option universe selection, both on chains from + /// and on the chains delivered in the slice + /// + public class OptionChainFiltersRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private Symbol _option; + private bool _traded; + + public override void Initialize() + { + SetStartDate(2015, 12, 24); + SetEndDate(2015, 12, 24); + SetCash(100000); + + var option = AddOption("GOOG"); + _option = option.Symbol; + // The same words select the universe and, below, narrow down the chains + option.SetFilter(universe => universe.CallsOnly().Expiration(1, 10).Strikes(-2, 2)); + + var chain = OptionChain(_option); + if (chain.Count == 0) + { + throw new RegressionTestException("Expected a non empty option chain"); + } + // The relative strikes filter needs the underlying price, chains built from universe data must carry it + if (chain.Underlying.Price == 0) + { + throw new RegressionTestException("Expected the chain to carry the underlying price"); + } + + var totalContracts = chain.Count; + var filtered = chain.CallsOnly().Expiration(1, 10).Strikes(-2, 2); + // GOOG closed at 748.54 on 2015-12-23 and the only expiration 1 to 10 days out is 2015-12-31, + // so the two strikes below the spot and the two at or above it are 745, 747.5, 750 and 752.5 + AssertContracts(filtered, OptionRight.Call, new DateTime(2015, 12, 31), new[] { 745m, 747.5m, 750m, 752.5m }); + if (chain.Count != totalContracts) + { + throw new RegressionTestException("Filters must not modify the source chain"); + } + + // Front month is the nearest expiration, 2015-12-24 itself + AssertContracts(chain.PutsOnly().FrontMonth(), OptionRight.Put, new DateTime(2015, 12, 24)); + + // Standard contracts expire on the third Friday, weeklys do not + var standards = chain.StandardsOnly().FrontMonth(); + if (standards.Count == 0 || standards.Any(x => x.Expiry != new DateTime(2016, 1, 15))) + { + throw new RegressionTestException("Expected the standard front month to expire on 2016-01-15"); + } + var weeklys = chain.WeeklysOnly(); + if (weeklys.Count == 0 || weeklys.Any(x => OptionSymbol.IsStandard(x.Symbol))) + { + throw new RegressionTestException("Expected only weekly contracts"); + } + + // Greeks filters use the greeks the chain carries + var deltas = chain.Delta(0.5m, 0.6m); + var expectedDeltas = chain.Count(x => x.Greeks.Delta >= 0.5m && x.Greeks.Delta <= 0.6m); + if (deltas.Count == 0 || deltas.Count != expectedDeltas || deltas.Any(x => x.Greeks.Delta < 0.5m || x.Greeks.Delta > 0.6m)) + { + throw new RegressionTestException("Delta filter mismatch"); + } + } + + public override void OnData(Slice slice) + { + if (_traded || !slice.OptionChains.TryGetValue(_option, out var chain)) + { + return; + } + + // The universe only selected calls expiring 1 to 10 days out, so the chain filters agree with it + if (chain.CallsOnly().Expiration(1, 10).Count != chain.Count || chain.PutsOnly().Count != 0) + { + throw new RegressionTestException("Slice chain filters disagree with the universe filter"); + } + + // Buy the call at the first strike at or above the underlying price + var contract = chain.Strikes(0, 0).FirstOrDefault(); + if (contract != null) + { + MarketOrder(contract.Symbol, 1); + _traded = true; + } + } + + public override void OnEndOfAlgorithm() + { + if (!_traded) + { + throw new RegressionTestException("Expected to trade a contract selected from the slice option chain"); + } + } + + private static void AssertContracts(OptionChain chain, OptionRight right, DateTime expiry, decimal[] strikes = null) + { + if (chain.Count == 0 || chain.Any(x => x.Right != right || x.Expiry != expiry)) + { + throw new RegressionTestException($"Expected only {right} contracts expiring on {expiry:yyyy-MM-dd}"); + } + if (strikes != null && !chain.Select(x => x.Strike).OrderBy(x => x).SequenceEqual(strikes)) + { + throw new RegressionTestException($"Unexpected strikes: {string.Join(", ", chain.Select(x => x.Strike))}"); + } + } + + /// + /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. + /// + public bool CanRunLocally { get; } = true; + + /// + /// This is used by the regression test system to indicate which languages this algorithm is written in. + /// + public virtual List Languages { get; } = new() { Language.CSharp, Language.Python }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public long DataPoints => 7080; + + /// + /// Data Points count of the algorithm history + /// + public int AlgorithmHistoryDataPoints => 1; + + /// + /// Final status of the algorithm + /// + public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "1"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000"}, + {"End Equity", "99764"}, + {"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", "$1.00"}, + {"Estimated Strategy Capacity", "$36000.00"}, + {"Lowest Capacity Asset", "GOOCV W6U7P9WYPQVA|GOOCV VP83T1ZUHROL"}, + {"Portfolio Turnover", "0.73%"}, + {"Drawdown Recovery", "0"}, + {"OrderListHash", "d505d8b11141dfd2d54b74ac20e39268"} + }; + } +} diff --git a/Algorithm.CSharp/OptionChainStrategyFiltersRegressionAlgorithm.cs b/Algorithm.CSharp/OptionChainStrategyFiltersRegressionAlgorithm.cs new file mode 100644 index 000000000000..2dfcfda1f2b0 --- /dev/null +++ b/Algorithm.CSharp/OptionChainStrategyFiltersRegressionAlgorithm.cs @@ -0,0 +1,183 @@ +/* + * 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.Collections.Generic; +using System.Linq; +using QuantConnect.Data; +using QuantConnect.Data.Market; +using QuantConnect.Interfaces; +using QuantConnect.Securities.Option; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm demonstrating that the option strategy filters of the universe selection, like + /// or + /// , select the strategy legs + /// straight from an option chain too + /// + public class OptionChainStrategyFiltersRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private Symbol _option; + private bool _traded; + + public override void Initialize() + { + SetStartDate(2015, 12, 24); + SetEndDate(2015, 12, 24); + SetCash(100000); + + var option = AddOption("GOOG"); + _option = option.Symbol; + // The universe selects the straddle legs, the same filter picks them again from the slice chain below + option.SetFilter(universe => universe.Straddle(7)); + + var chain = OptionChain(_option); + var expiry = new DateTime(2015, 12, 31); + + // GOOG closed at 748.54 on 2015-12-23, the first expiry at least 7 days out is 2015-12-31 and the ATM strike is 747.50 + var straddle = chain.Straddle(7); + AssertLegs(straddle, expiry, (OptionRight.Call, 747.5m), (OptionRight.Put, 747.5m)); + + // Iron condor: near legs 5 away from the spot, far legs 10 away + var ironCondor = chain.IronCondor(7, 5, 10); + AssertLegs(ironCondor, expiry, (OptionRight.Put, 737.5m), (OptionRight.Put, 742.5m), (OptionRight.Call, 752.5m), (OptionRight.Call, 757.5m)); + + // Single contract and vertical spread pickers + AssertLegs(chain.NakedPut(7, -5), expiry, (OptionRight.Put, 742.5m)); + AssertLegs(chain.CallSpread(7, 5), expiry, (OptionRight.Call, 742.5m), (OptionRight.Call, 752.5m)); + + // Calendar spread: same strike, expiries at least 7 and 14 days out + var calendar = chain.CallCalendarSpread(0, 7, 14); + if (calendar.Count != 2 || calendar.Any(x => x.Right != OptionRight.Call || x.Strike != 747.5m) + || !calendar.Select(x => x.Expiry).OrderBy(x => x).SequenceEqual(new[] { expiry, new DateTime(2016, 1, 8) })) + { + throw new RegressionTestException($"Unexpected calendar spread legs: {string.Join(", ", calendar.Select(x => x.Symbol.Value))}"); + } + + // No match selects nothing instead of throwing + if (chain.Straddle(1000).Count != 0) + { + throw new RegressionTestException("Expected no legs for an expiry out of the chain"); + } + + // Invalid arguments are rejected like the universe filters do + try + { + chain.Strangle(7, -5, 5); + throw new RegressionTestException("Expected Strangle() to reject a negative call strike distance"); + } + catch (ArgumentException) + { + } + } + + public override void OnData(Slice slice) + { + if (_traded || !slice.OptionChains.TryGetValue(_option, out var chain)) + { + return; + } + + // The same filter that selected the universe picks the legs from the slice chain + var legs = chain.Straddle(7); + if (legs.Count == 2) + { + var leg = legs.First(); + Buy(OptionStrategies.Straddle(_option, leg.Strike, leg.Expiry), 1); + _traded = true; + } + } + + public override void OnEndOfAlgorithm() + { + if (!_traded) + { + throw new RegressionTestException("Expected to trade the straddle selected from the slice option chain"); + } + } + + private static void AssertLegs(OptionChain legs, DateTime expiry, params (OptionRight right, decimal strike)[] expected) + { + var actual = legs.Select(x => (x.Right, x.Strike)).OrderBy(x => x.Right).ThenBy(x => x.Strike).ToList(); + if (legs.Any(x => x.Expiry != expiry) || !actual.SequenceEqual(expected.OrderBy(x => x.right).ThenBy(x => x.strike))) + { + throw new RegressionTestException($"Unexpected legs: {string.Join(", ", legs.Select(x => x.Symbol.Value))}"); + } + } + + /// + /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. + /// + public bool CanRunLocally { get; } = true; + + /// + /// This is used by the regression test system to indicate which languages this algorithm is written in. + /// + public virtual List Languages { get; } = new() { Language.CSharp, Language.Python }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public long DataPoints => 5886; + + /// + /// Data Points count of the algorithm history + /// + public int AlgorithmHistoryDataPoints => 1; + + /// + /// Final status of the algorithm + /// + public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "2"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000"}, + {"End Equity", "99638"}, + {"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", "$2.00"}, + {"Estimated Strategy Capacity", "$23000.00"}, + {"Lowest Capacity Asset", "GOOCV 305Y7VNVZK3D2|GOOCV VP83T1ZUHROL"}, + {"Portfolio Turnover", "1.55%"}, + {"Drawdown Recovery", "0"}, + {"OrderListHash", "0918e55ec2074aaafad98475aa2fcc43"} + }; + } +} diff --git a/Algorithm.Python/OptionChainFiltersRegressionAlgorithm.py b/Algorithm.Python/OptionChainFiltersRegressionAlgorithm.py new file mode 100644 index 000000000000..3452d3235d80 --- /dev/null +++ b/Algorithm.Python/OptionChainFiltersRegressionAlgorithm.py @@ -0,0 +1,96 @@ +# 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. + +from AlgorithmImports import * + +### +### Regression algorithm demonstrating that option chains can be filtered with the same filters used for +### option universe selection, both on chains from QCAlgorithm.option_chain() and on the chains delivered in the slice +### +class OptionChainFiltersRegressionAlgorithm(QCAlgorithm): + + def initialize(self): + self.set_start_date(2015, 12, 24) + self.set_end_date(2015, 12, 24) + self.set_cash(100000) + + option = self.add_option("GOOG") + self._option = option.symbol + # The same words select the universe and, below, narrow down the chains + option.set_filter(lambda universe: universe.calls_only().expiration(1, 10).strikes(-2, 2)) + + chain = self.option_chain(self._option) + if chain.count == 0: + raise AssertionError("Expected a non empty option chain") + # The relative strikes filter needs the underlying price, chains built from universe data must carry it + if chain.underlying.price == 0: + raise AssertionError("Expected the chain to carry the underlying price") + + total_contracts = chain.count + filtered = chain.calls_only().expiration(1, 10).strikes(-2, 2) + # GOOG closed at 748.54 on 2015-12-23 and the only expiration 1 to 10 days out is 2015-12-31, + # so the two strikes below the spot and the two at or above it are 745, 747.5, 750 and 752.5 + self._assert_contracts(filtered, OptionRight.CALL, datetime(2015, 12, 31), [745, 747.5, 750, 752.5]) + if chain.count != total_contracts: + raise AssertionError("Filters must not modify the source chain") + + # Front month is the nearest expiration, 2015-12-24 itself + self._assert_contracts(chain.puts_only().front_month(), OptionRight.PUT, datetime(2015, 12, 24)) + + # Standard contracts expire on the third Friday, weeklys do not + standards = chain.standards_only().front_month() + if standards.count == 0 or any(x.expiry != datetime(2016, 1, 15) for x in standards): + raise AssertionError("Expected the standard front month to expire on 2016-01-15") + weeklys = chain.weeklys_only() + if weeklys.count == 0 or any(OptionSymbol.is_standard(x.symbol) for x in weeklys): + raise AssertionError("Expected only weekly contracts") + + # Greeks filters use the greeks the chain carries + deltas = chain.delta(0.5, 0.6) + expected_deltas = sum(1 for x in chain if 0.5 <= x.greeks.delta <= 0.6) + if deltas.count == 0 or deltas.count != expected_deltas or any(not 0.5 <= x.greeks.delta <= 0.6 for x in deltas): + raise AssertionError("Delta filter mismatch") + + # where() takes a predicate, like the universe filter does + high_open_interest = chain.where(lambda x: x.open_interest > 1000) + if high_open_interest.count == 0 or high_open_interest.count != sum(1 for x in chain if x.open_interest > 1000): + raise AssertionError("where() filter mismatch") + + self._traded = False + + def on_data(self, slice): + if self._traded: + return + chain = slice.option_chains.get(self._option) + if not chain: + return + + # The universe only selected calls expiring 1 to 10 days out, so the chain filters agree with it + if chain.calls_only().expiration(1, 10).count != chain.count or chain.puts_only().count != 0: + raise AssertionError("Slice chain filters disagree with the universe filter") + + # Buy the call at the first strike at or above the underlying price + contract = next(iter(chain.strikes(0, 0)), None) + if contract is not None: + self.market_order(contract.symbol, 1) + self._traded = True + + def on_end_of_algorithm(self): + if not self._traded: + raise AssertionError("Expected to trade a contract selected from the slice option chain") + + def _assert_contracts(self, chain, right, expiry, strikes=None): + if chain.count == 0 or any(x.right != right or x.expiry != expiry for x in chain): + raise AssertionError(f"Expected only {right} contracts expiring on {expiry:%Y-%m-%d}") + if strikes is not None and sorted(x.strike for x in chain) != strikes: + raise AssertionError(f"Unexpected strikes: {[x.strike for x in chain]}") diff --git a/Algorithm.Python/OptionChainStrategyFiltersRegressionAlgorithm.py b/Algorithm.Python/OptionChainStrategyFiltersRegressionAlgorithm.py new file mode 100644 index 000000000000..7521e005ca88 --- /dev/null +++ b/Algorithm.Python/OptionChainStrategyFiltersRegressionAlgorithm.py @@ -0,0 +1,86 @@ +# 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. + +from AlgorithmImports import * + +### +### Regression algorithm demonstrating that the option strategy filters of the universe selection, like straddle() +### or iron_condor(), select the strategy legs straight from an option chain too +### +class OptionChainStrategyFiltersRegressionAlgorithm(QCAlgorithm): + + def initialize(self): + self.set_start_date(2015, 12, 24) + self.set_end_date(2015, 12, 24) + self.set_cash(100000) + + option = self.add_option("GOOG") + self._option = option.symbol + # The universe selects the straddle legs, the same filter picks them again from the slice chain below + option.set_filter(lambda universe: universe.straddle(7)) + + chain = self.option_chain(self._option) + expiry = datetime(2015, 12, 31) + + # GOOG closed at 748.54 on 2015-12-23, the first expiry at least 7 days out is 2015-12-31 and the ATM strike is 747.50 + self._assert_legs(chain.straddle(7), expiry, [(OptionRight.CALL, 747.5), (OptionRight.PUT, 747.5)]) + + # Iron condor: near legs 5 away from the spot, far legs 10 away + self._assert_legs(chain.iron_condor(7, 5, 10), expiry, + [(OptionRight.CALL, 752.5), (OptionRight.CALL, 757.5), (OptionRight.PUT, 737.5), (OptionRight.PUT, 742.5)]) + + # Single contract and vertical spread pickers + self._assert_legs(chain.naked_put(7, -5), expiry, [(OptionRight.PUT, 742.5)]) + self._assert_legs(chain.call_spread(7, 5), expiry, [(OptionRight.CALL, 742.5), (OptionRight.CALL, 752.5)]) + + # Calendar spread: same strike, expiries at least 7 and 14 days out + calendar = chain.call_calendar_spread(0, 7, 14) + if (calendar.count != 2 or any(x.right != OptionRight.CALL or x.strike != 747.5 for x in calendar) + or sorted(x.expiry for x in calendar) != [expiry, datetime(2016, 1, 8)]): + raise AssertionError(f"Unexpected calendar spread legs: {[x.symbol.value for x in calendar]}") + + # No match selects nothing instead of raising + if chain.straddle(1000).count != 0: + raise AssertionError("Expected no legs for an expiry out of the chain") + + # Invalid arguments are rejected like the universe filters do + try: + chain.strangle(7, -5, 5) + raise AssertionError("Expected strangle() to reject a negative call strike distance") + except ArgumentException: + pass + + self._traded = False + + def on_data(self, slice): + if self._traded: + return + chain = slice.option_chains.get(self._option) + if not chain: + return + + # The same filter that selected the universe picks the legs from the slice chain + legs = chain.straddle(7) + if legs.count == 2: + leg = next(iter(legs)) + self.buy(OptionStrategies.straddle(self._option, leg.strike, leg.expiry), 1) + self._traded = True + + def on_end_of_algorithm(self): + if not self._traded: + raise AssertionError("Expected to trade the straddle selected from the slice option chain") + + def _assert_legs(self, legs, expiry, expected): + actual = sorted((x.right, x.strike) for x in legs) + if any(x.expiry != expiry for x in legs) or actual != sorted(expected): + raise AssertionError(f"Unexpected legs: {[x.symbol.value for x in legs]}") diff --git a/Common/Data/Market/BaseChain.cs b/Common/Data/Market/BaseChain.cs index 1341d16f51a8..c199926efce1 100644 --- a/Common/Data/Market/BaseChain.cs +++ b/Common/Data/Market/BaseChain.cs @@ -35,6 +35,7 @@ public class BaseChain : BaseData, IEnumerable private Dictionary>> _auxiliaryData; private readonly Lazy _dataframe; private readonly bool _flatten; + private DateTime? _exchangeTime; private Dictionary>> AuxiliaryData { @@ -59,6 +60,16 @@ public BaseData Underlying get; internal set; } + /// + /// The chain time in the exchange time zone, the reference date for the contract filters. Slice chains carry + /// their data's end time, chains built from universe data default to + /// + internal DateTime ExchangeTime + { + get => _exchangeTime ?? Time; + set => _exchangeTime = value; + } + /// /// Gets all ticks for every option contract in this chain, keyed by option symbol /// @@ -171,6 +182,7 @@ protected BaseChain(BaseChain other) { Symbol = other.Symbol; Time = other.Time; + _exchangeTime = other._exchangeTime; Value = other.Value; Underlying = other.Underlying; Ticks = other.Ticks; @@ -178,6 +190,24 @@ protected BaseChain(BaseChain other) TradeBars = other.TradeBars; Contracts = other.Contracts; FilteredContracts = other.FilteredContracts; + _auxiliaryData = other._auxiliaryData; + } + + /// + /// Initializes a new instance of the class as a copy of the specified chain + /// containing only the given subset of its contracts. The underlying, ticks, trade bars, quote bars and auxiliary data are shared with the source chain + /// + /// The chain to copy + /// The contracts to keep + protected BaseChain(BaseChain other, IEnumerable contracts) + : this(other) + { + Contracts = new(); + Contracts.Time = other.Contracts.Time; + foreach (var contract in contracts) + { + Contracts[contract.Symbol] = contract; + } } /// diff --git a/Common/Data/Market/OptionChain.Filters.cs b/Common/Data/Market/OptionChain.Filters.cs new file mode 100644 index 000000000000..3fdaac86b4ac --- /dev/null +++ b/Common/Data/Market/OptionChain.Filters.cs @@ -0,0 +1,527 @@ +/* + * 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 Python.Runtime; +using QuantConnect.Securities; + +namespace QuantConnect.Data.Market +{ + /// + /// The option chain filters, the same ones the option universe selection offers, see . + /// Each filter returns a new chain, leaving this one untouched + /// + public partial class OptionChain + { + #region Filters + + /// + /// Selects the contracts with strikes in the given range relative to the underlying price, in number of strikes. + /// Same as + /// + /// The minimum strike relative to the underlying price, for example, -1 would filter out contracts further than 1 strike below market price + /// The maximum strike relative to the underlying price, for example, +1 would filter out contracts further than 1 strike above market price + /// A new chain with the filter applied + public OptionChain Strikes(int minStrike, int maxStrike) + { + return Filter(universe => universe.Strikes(minStrike, maxStrike)); + } + + /// + /// Selects the contracts expiring in the given range relative to the chain date. + /// Same as + /// + /// The minimum time until expiry to include, for example, TimeSpan.FromDays(10) + /// would exclude contracts expiring in less than 10 days + /// The maximum time until expiry to include, for example, TimeSpan.FromDays(10) + /// would exclude contracts expiring in more than 10 days + /// A new chain with the filter applied + public OptionChain Expiration(TimeSpan minExpiry, TimeSpan maxExpiry) + { + return Filter(universe => universe.Expiration(minExpiry, maxExpiry)); + } + + /// + /// Selects the contracts expiring in the given range of days relative to the chain date. + /// Same as + /// + /// The minimum time, expressed in days, until expiry to include, for example, 10 + /// would exclude contracts expiring in less than 10 days + /// The maximum time, expressed in days, until expiry to include, for example, 10 + /// would exclude contracts expiring in more than 10 days + /// A new chain with the filter applied + public OptionChain Expiration(int minExpiryDays, int maxExpiryDays) + { + return Filter(universe => universe.Expiration(minExpiryDays, maxExpiryDays)); + } + + /// + /// Selects the call contracts. Same as + /// + /// A new chain with the filter applied + public OptionChain CallsOnly() + { + return Filter(universe => universe.CallsOnly()); + } + + /// + /// Selects the put contracts. Same as + /// + /// A new chain with the filter applied + public OptionChain PutsOnly() + { + return Filter(universe => universe.PutsOnly()); + } + + /// + /// Selects the standard contracts in the chain, excluding weeklys. Unlike , + /// it applies to the contracts already selected, so it can be combined with the expiry filters in any order + /// + /// A new chain with the filter applied + public OptionChain StandardsOnly() + { + return Filter(universe => universe.StandardsOnly()); + } + + /// + /// Selects the non standard weekly contracts in the chain. Unlike , + /// it applies to the contracts already selected, so it can be combined with the expiry filters in any order + /// + /// A new chain with the filter applied + public OptionChain WeeklysOnly() + { + return Filter(universe => universe.WeeklysOnly()); + } + + /// + /// Selects the contracts of the nearest expiration. Same as + /// + /// A new chain with the filter applied + public OptionChain FrontMonth() + { + return Filter(universe => universe.FrontMonth()); + } + + /// + /// Selects the contracts of all expirations but the nearest one. Same as + /// + /// A new chain with the filter applied + public OptionChain BackMonths() + { + return Filter(universe => universe.BackMonths()); + } + + /// + /// Selects the contracts of the second nearest expiration. Same as + /// + /// A new chain with the filter applied + public OptionChain BackMonth() + { + return Filter(universe => universe.BackMonth()); + } + + /// + /// Selects the contracts with delta in the given range. Same as + /// + /// The minimum delta value + /// The maximum delta value + /// A new chain with the filter applied + public OptionChain Delta(decimal min, decimal max) + { + return Filter(universe => universe.Delta(min, max)); + } + + /// + /// Selects the contracts with delta in the given range. Alias for + /// + /// The minimum delta value + /// The maximum delta value + /// A new chain with the filter applied + public OptionChain D(decimal min, decimal max) + { + return Delta(min, max); + } + + /// + /// Selects the contracts with gamma in the given range. Same as + /// + /// The minimum gamma value + /// The maximum gamma value + /// A new chain with the filter applied + public OptionChain Gamma(decimal min, decimal max) + { + return Filter(universe => universe.Gamma(min, max)); + } + + /// + /// Selects the contracts with gamma in the given range. Alias for + /// + /// The minimum gamma value + /// The maximum gamma value + /// A new chain with the filter applied + public OptionChain G(decimal min, decimal max) + { + return Gamma(min, max); + } + + /// + /// Selects the contracts with theta in the given range. Same as + /// + /// The minimum theta value + /// The maximum theta value + /// A new chain with the filter applied + public OptionChain Theta(decimal min, decimal max) + { + return Filter(universe => universe.Theta(min, max)); + } + + /// + /// Selects the contracts with theta in the given range. Alias for + /// + /// The minimum theta value + /// The maximum theta value + /// A new chain with the filter applied + public OptionChain T(decimal min, decimal max) + { + return Theta(min, max); + } + + /// + /// Selects the contracts with vega in the given range. Same as + /// + /// The minimum vega value + /// The maximum vega value + /// A new chain with the filter applied + public OptionChain Vega(decimal min, decimal max) + { + return Filter(universe => universe.Vega(min, max)); + } + + /// + /// Selects the contracts with vega in the given range. Alias for + /// + /// The minimum vega value + /// The maximum vega value + /// A new chain with the filter applied + public OptionChain V(decimal min, decimal max) + { + return Vega(min, max); + } + + /// + /// Selects the contracts with rho in the given range. Same as + /// + /// The minimum rho value + /// The maximum rho value + /// A new chain with the filter applied + public OptionChain Rho(decimal min, decimal max) + { + return Filter(universe => universe.Rho(min, max)); + } + + /// + /// Selects the contracts with rho in the given range. Alias for + /// + /// The minimum rho value + /// The maximum rho value + /// A new chain with the filter applied + public OptionChain R(decimal min, decimal max) + { + return Rho(min, max); + } + + /// + /// Selects the contracts with implied volatility in the given range. Same as + /// + /// The minimum implied volatility value + /// The maximum implied volatility value + /// A new chain with the filter applied + public OptionChain ImpliedVolatility(decimal min, decimal max) + { + return Filter(universe => universe.ImpliedVolatility(min, max)); + } + + /// + /// Selects the contracts with implied volatility in the given range. Alias for + /// + /// The minimum implied volatility value + /// The maximum implied volatility value + /// A new chain with the filter applied + public OptionChain IV(decimal min, decimal max) + { + return ImpliedVolatility(min, max); + } + + /// + /// Selects the contracts with open interest in the given range. Same as + /// + /// The minimum open interest value + /// The maximum open interest value + /// A new chain with the filter applied + public OptionChain OpenInterest(long min, long max) + { + return Filter(universe => universe.OpenInterest(min, max)); + } + + /// + /// Selects the contracts with open interest in the given range. Alias for + /// + /// The minimum open interest value + /// The maximum open interest value + /// A new chain with the filter applied + public OptionChain OI(long min, long max) + { + return OpenInterest(min, max); + } + + /// + /// Selects the contracts matching the given predicate, e.g. chain.where(lambda contract: contract.open_interest > 100). + /// From C# use Linq's Where, which keeps this chain's type untouched + /// + /// Function determining which contracts are kept + /// A new chain with the filter applied + public OptionChain Where(PyObject predicate) + { + return new OptionChain(this, Contracts.Values.Where(predicate.SafeAs>())); + } + + #endregion + + #region Strategy filters + + /// + /// Selects the single call contract with the closest match to the criteria given, for a naked, covered or protective call. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance from the current underlying price + /// A new chain with the selected contracts, empty if there is no match + public OptionChain NakedCall(int minDaysTillExpiry = 30, decimal strikeFromAtm = 0) + { + return Filter(universe => universe.NakedCall(minDaysTillExpiry, strikeFromAtm)); + } + + /// + /// Selects the single put contract with the closest match to the criteria given, for a naked, covered or protective put. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance from the current underlying price + /// A new chain with the selected contracts, empty if there is no match + public OptionChain NakedPut(int minDaysTillExpiry = 30, decimal strikeFromAtm = 0) + { + return Filter(universe => universe.NakedPut(minDaysTillExpiry, strikeFromAtm)); + } + + /// + /// Selects the 2 call contracts with the same expiry and different strikes closest to the criteria given, for a bull or bear call spread. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance from the current underlying price of the higher strike price + /// The desired strike price distance from the current underlying price of the lower strike price + /// A new chain with the selected contracts, empty if there is no match + public OptionChain CallSpread(int minDaysTillExpiry = 30, decimal higherStrikeFromAtm = 5, decimal? lowerStrikeFromAtm = null) + { + return Filter(universe => universe.CallSpread(minDaysTillExpiry, higherStrikeFromAtm, lowerStrikeFromAtm)); + } + + /// + /// Selects the 2 put contracts with the same expiry and different strikes closest to the criteria given, for a bull or bear put spread. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance from the current underlying price of the higher strike price + /// The desired strike price distance from the current underlying price of the lower strike price + /// A new chain with the selected contracts, empty if there is no match + public OptionChain PutSpread(int minDaysTillExpiry = 30, decimal higherStrikeFromAtm = 5, decimal? lowerStrikeFromAtm = null) + { + return Filter(universe => universe.PutSpread(minDaysTillExpiry, higherStrikeFromAtm, lowerStrikeFromAtm)); + } + + /// + /// Selects the 2 call contracts with the same strike and different expiries closest to the criteria given, for a call calendar spread. Same as + /// + /// The desired strike price distance from the current underlying price + /// The minimum days till expiry of the closer contract from the current time, closest expiry will be selected + /// The minimum days till expiry of the further contract from the current time, closest expiry will be selected + /// A new chain with the selected contracts, empty if there is no match + public OptionChain CallCalendarSpread(decimal strikeFromAtm = 0, int minNearDaysTillExpiry = 30, int minFarDaysTillExpiry = 60) + { + return Filter(universe => universe.CallCalendarSpread(strikeFromAtm, minNearDaysTillExpiry, minFarDaysTillExpiry)); + } + + /// + /// Selects the 2 put contracts with the same strike and different expiries closest to the criteria given, for a put calendar spread. Same as + /// + /// The desired strike price distance from the current underlying price + /// The minimum days till expiry of the closer contract from the current time, closest expiry will be selected + /// The minimum days till expiry of the further contract from the current time, closest expiry will be selected + /// A new chain with the selected contracts, empty if there is no match + public OptionChain PutCalendarSpread(decimal strikeFromAtm = 0, int minNearDaysTillExpiry = 30, int minFarDaysTillExpiry = 60) + { + return Filter(universe => universe.PutCalendarSpread(strikeFromAtm, minNearDaysTillExpiry, minFarDaysTillExpiry)); + } + + /// + /// Selects an OTM call and an OTM put with the same expiry closest to the criteria given, for a strangle. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance from the current underlying price of the OTM call, must be positive + /// The desired strike price distance from the current underlying price of the OTM put, must be negative + /// A new chain with the selected contracts, empty if there is no match + public OptionChain Strangle(int minDaysTillExpiry = 30, decimal callStrikeFromAtm = 5, decimal putStrikeFromAtm = -5) + { + return Filter(universe => universe.Strangle(minDaysTillExpiry, callStrikeFromAtm, putStrikeFromAtm)); + } + + /// + /// Selects the ATM call and the ATM put with the same expiry closest to the criteria given, for a straddle. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// A new chain with the selected contracts, empty if there is no match + public OptionChain Straddle(int minDaysTillExpiry = 30) + { + return Filter(universe => universe.Straddle(minDaysTillExpiry)); + } + + /// + /// Selects a call and a put with the same expiry and a lower put strike closest to the criteria given, for a protective collar. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance from the current underlying price of the call + /// The desired strike price distance from the current underlying price of the put + /// A new chain with the selected contracts, empty if there is no match + public OptionChain ProtectiveCollar(int minDaysTillExpiry = 30, decimal callStrikeFromAtm = 5, decimal putStrikeFromAtm = -5) + { + return Filter(universe => universe.ProtectiveCollar(minDaysTillExpiry, callStrikeFromAtm, putStrikeFromAtm)); + } + + /// + /// Selects a call and a put with the same expiry and strike closest to the criteria given, for a conversion or reverse conversion. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance from the current underlying price + /// A new chain with the selected contracts, empty if there is no match + public OptionChain Conversion(int minDaysTillExpiry = 30, decimal strikeFromAtm = 5) + { + return Filter(universe => universe.Conversion(minDaysTillExpiry, strikeFromAtm)); + } + + /// + /// Selects an ITM, an ATM and an OTM call with the same expiry and equal strike distance closest to the criteria given, for a call butterfly. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance of the ITM and OTM calls from the current underlying price + /// A new chain with the selected contracts, empty if there is no match + public OptionChain CallButterfly(int minDaysTillExpiry = 30, decimal strikeSpread = 5) + { + return Filter(universe => universe.CallButterfly(minDaysTillExpiry, strikeSpread)); + } + + /// + /// Selects an ITM, an ATM and an OTM put with the same expiry and equal strike distance closest to the criteria given, for a put butterfly. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance of the ITM and OTM puts from the current underlying price + /// A new chain with the selected contracts, empty if there is no match + public OptionChain PutButterfly(int minDaysTillExpiry = 30, decimal strikeSpread = 5) + { + return Filter(universe => universe.PutButterfly(minDaysTillExpiry, strikeSpread)); + } + + /// + /// Selects an OTM call, an ATM call, an ATM put and an OTM put with the same expiry and equal strike distance closest to the criteria given, for an iron butterfly. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance of the OTM call and the OTM put from the current underlying price + /// A new chain with the selected contracts, empty if there is no match + public OptionChain IronButterfly(int minDaysTillExpiry = 30, decimal strikeSpread = 5) + { + return Filter(universe => universe.IronButterfly(minDaysTillExpiry, strikeSpread)); + } + + /// + /// Selects a far OTM call, a near OTM call, a near OTM put and a far OTM put with the same expiry closest to the criteria given, for an iron condor. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance of the near call and near put from the current underlying price + /// The desired strike price distance of the far call and far put from the current underlying price + /// A new chain with the selected contracts, empty if there is no match + public OptionChain IronCondor(int minDaysTillExpiry = 30, decimal nearStrikeSpread = 5, decimal farStrikeSpread = 10) + { + return Filter(universe => universe.IronCondor(minDaysTillExpiry, nearStrikeSpread, farStrikeSpread)); + } + + /// + /// Selects an OTM call, an ITM call, an OTM put and an ITM put with the same expiry closest to the criteria given, for a box spread. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance of the OTM call and the OTM put from the current underlying price + /// A new chain with the selected contracts, empty if there is no match + public OptionChain BoxSpread(int minDaysTillExpiry = 30, decimal strikeSpread = 5) + { + return Filter(universe => universe.BoxSpread(minDaysTillExpiry, strikeSpread)); + } + + /// + /// Selects 2 calls and 2 puts with the same strike and 2 expiries closest to the criteria given, for a jelly roll. Same as + /// + /// The desired strike price distance from the current underlying price + /// The minimum days till expiry of the closer contract from the current time, closest expiry will be selected + /// The minimum days till expiry of the further contract from the current time, closest expiry will be selected + /// A new chain with the selected contracts, empty if there is no match + public OptionChain JellyRoll(decimal strikeFromAtm = 0, int minNearDaysTillExpiry = 30, int minFarDaysTillExpiry = 60) + { + return Filter(universe => universe.JellyRoll(strikeFromAtm, minNearDaysTillExpiry, minFarDaysTillExpiry)); + } + + /// + /// Selects 3 calls with the same expiry and different strikes closest to the criteria given, for a bull or bear call ladder. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance from the current underlying price of the higher strike price + /// The desired strike price distance from the current underlying price of the middle strike price + /// The desired strike price distance from the current underlying price of the lower strike price + /// A new chain with the selected contracts, empty if there is no match + public OptionChain CallLadder(int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal middleStrikeFromAtm, decimal lowerStrikeFromAtm) + { + return Filter(universe => universe.CallLadder(minDaysTillExpiry, higherStrikeFromAtm, middleStrikeFromAtm, lowerStrikeFromAtm)); + } + + /// + /// Selects 3 puts with the same expiry and different strikes closest to the criteria given, for a bull or bear put ladder. Same as + /// + /// The minimum days till expiry from the current time, closest expiry will be selected + /// The desired strike price distance from the current underlying price of the higher strike price + /// The desired strike price distance from the current underlying price of the middle strike price + /// The desired strike price distance from the current underlying price of the lower strike price + /// A new chain with the selected contracts, empty if there is no match + public OptionChain PutLadder(int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal middleStrikeFromAtm, decimal lowerStrikeFromAtm) + { + return Filter(universe => universe.PutLadder(minDaysTillExpiry, higherStrikeFromAtm, middleStrikeFromAtm, lowerStrikeFromAtm)); + } + + /// + /// Applies the given universe filter to the contracts of this chain and returns the result as a new chain + /// + /// The universe filter to apply + private OptionChain Filter(Func filter) + { + var universe = new OptionChainFilterUniverse(this); + // the type filters (standards/weeklys) are only applied on demand, like the universe selection does after the user filter + return new OptionChain(this, filter(universe).ApplyTypesFilter()); + } + + #endregion + } +} diff --git a/Common/Data/Market/OptionChain.cs b/Common/Data/Market/OptionChain.cs index 562cd18e0a0f..20c1ec0a7126 100644 --- a/Common/Data/Market/OptionChain.cs +++ b/Common/Data/Market/OptionChain.cs @@ -22,9 +22,12 @@ namespace QuantConnect.Data.Market { /// /// Represents an entire chain of option contracts for a single underlying security. - /// This type is + /// This type is . + /// The chain can be narrowed down with the same filters available for option universe selection + /// (see and ), e.g. chain.calls_only().expiration(0, 30).strikes(-2, 2). + /// Each filter returns a new chain, leaving this one untouched. /// - public class OptionChain : BaseChain + public partial class OptionChain : BaseChain, IOptionContractFilters { /// /// Initializes a new instance of the class @@ -49,9 +52,15 @@ public OptionChain(Symbol canonicalOptionSymbol, DateTime time, IEnumerable + /// Initializes a new instance of the class as a copy of the specified chain + /// containing only the given subset of its contracts + /// + private OptionChain(OptionChain other, IEnumerable contracts) + : base(other, contracts) + { + } + /// /// Return a new instance clone of this object, used in fill forward /// diff --git a/Common/Data/Market/OptionContract.cs b/Common/Data/Market/OptionContract.cs index 3d44ec15f2c9..4006c4c573aa 100644 --- a/Common/Data/Market/OptionContract.cs +++ b/Common/Data/Market/OptionContract.cs @@ -104,6 +104,11 @@ public class OptionContract : BaseContract /// public decimal UnderlyingLastPrice => _optionData.UnderlyingLastPrice; + /// + /// The option symbol properties + /// + internal SymbolProperties SymbolProperties => _symbolProperties; + /// /// Initializes a new instance of the class /// diff --git a/Common/Securities/ContractSecurityFilterUniverse.cs b/Common/Securities/ContractSecurityFilterUniverse.cs index 7a317eeb4a90..ec1be57ef2f4 100644 --- a/Common/Securities/ContractSecurityFilterUniverse.cs +++ b/Common/Securities/ContractSecurityFilterUniverse.cs @@ -19,6 +19,7 @@ using Python.Runtime; using System.Collections; using System.Collections.Generic; +using QuantConnect.Data; namespace QuantConnect.Securities { @@ -28,7 +29,7 @@ namespace QuantConnect.Securities /// public abstract class ContractSecurityFilterUniverse : IDerivativeSecurityFilterUniverse where T : ContractSecurityFilterUniverse - where TData : IChainUniverseData + where TData : ISymbolProvider { private bool _alreadyAppliedTypeFilters; @@ -153,12 +154,19 @@ internal T ApplyTypesFilter() return (T)this; } + // Every contract passes by default, so skip the pass and only pin the ordering rule for StandardsOnly() + if (Type == DefaultExpirationType) + { + _alreadyAppliedTypeFilters = true; + return (T)this; + } + // memoization map for ApplyTypesFilter() var memoizedMap = new Dictionary(); Func memoizedIsStandardType = data => { - var dt = data.ID.Date; + var dt = data.Symbol.ID.Date; bool result; if (memoizedMap.TryGetValue(dt, out result)) @@ -252,9 +260,9 @@ public T WeeklysOnly() public virtual T FrontMonth() { ApplyTypesFilter(); - var ordered = Data.OrderBy(x => x.ID.Date).ToList(); + var ordered = Data.OrderBy(x => x.Symbol.ID.Date).ToList(); if (ordered.Count == 0) return (T)this; - var frontMonth = ordered.TakeWhile(x => ordered[0].ID.Date == x.ID.Date); + var frontMonth = ordered.TakeWhile(x => ordered[0].Symbol.ID.Date == x.Symbol.ID.Date); Data = frontMonth.ToList(); return (T)this; @@ -267,9 +275,9 @@ public virtual T FrontMonth() public virtual T BackMonths() { ApplyTypesFilter(); - var ordered = Data.OrderBy(x => x.ID.Date).ToList(); + var ordered = Data.OrderBy(x => x.Symbol.ID.Date).ToList(); if (ordered.Count == 0) return (T)this; - var backMonths = ordered.SkipWhile(x => ordered[0].ID.Date == x.ID.Date); + var backMonths = ordered.SkipWhile(x => ordered[0].Symbol.ID.Date == x.Symbol.ID.Date); Data = backMonths.ToList(); return (T)this; @@ -317,7 +325,7 @@ public virtual T Expiration(TimeSpan minExpiry, TimeSpan maxExpiry) var maxExpiryToDate = referenceDate + maxExpiry; Data = Data - .Where(symbol => symbol.ID.Date.Date >= minExpiryToDate && symbol.ID.Date.Date <= maxExpiryToDate) + .Where(data => data.Symbol.ID.Date.Date >= minExpiryToDate && data.Symbol.ID.Date.Date <= maxExpiryToDate) .ToList(); return (T)this; diff --git a/Common/Securities/IDerivativeSecurityFilterUniverse.cs b/Common/Securities/IDerivativeSecurityFilterUniverse.cs index a3a61cddfce7..32f28d178d14 100644 --- a/Common/Securities/IDerivativeSecurityFilterUniverse.cs +++ b/Common/Securities/IDerivativeSecurityFilterUniverse.cs @@ -15,6 +15,7 @@ */ using System.Collections.Generic; +using QuantConnect.Data; namespace QuantConnect.Securities { @@ -22,7 +23,7 @@ namespace QuantConnect.Securities /// Represents derivative symbols universe used in filtering. /// public interface IDerivativeSecurityFilterUniverse : IEnumerable - where T : IChainUniverseData + where T : ISymbolProvider { /// /// The number of contracts in the universe diff --git a/Common/Securities/Option/IOptionContractFilters.cs b/Common/Securities/Option/IOptionContractFilters.cs new file mode 100644 index 000000000000..095601441bc1 --- /dev/null +++ b/Common/Securities/Option/IOptionContractFilters.cs @@ -0,0 +1,238 @@ +/* + * 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; + +namespace QuantConnect.Securities +{ + /// + /// The option contract filters shared by the option universe selection () + /// and the option chain (), so both offer the same filters with the same semantics. + /// OptionChainTests.ChainExposesEveryUniverseFilter checks that every universe filter is declared here + /// + /// The implementing type, returned by every filter for chaining + public interface IOptionContractFilters + { + /// + /// Selects the contracts with strikes in the given range relative to the underlying price, in number of strikes + /// + TSelf Strikes(int minStrike, int maxStrike); + + /// + /// Selects the contracts expiring in the given range relative to the current date + /// + TSelf Expiration(TimeSpan minExpiry, TimeSpan maxExpiry); + + /// + /// Selects the contracts expiring in the given range of days relative to the current date + /// + TSelf Expiration(int minExpiryDays, int maxExpiryDays); + + /// + /// Selects the call contracts + /// + TSelf CallsOnly(); + + /// + /// Selects the put contracts + /// + TSelf PutsOnly(); + + /// + /// Selects the standard contracts, excluding weeklys + /// + TSelf StandardsOnly(); + + /// + /// Selects the non standard weekly contracts + /// + TSelf WeeklysOnly(); + + /// + /// Selects the contracts of the nearest expiration + /// + TSelf FrontMonth(); + + /// + /// Selects the contracts of all expirations but the nearest one + /// + TSelf BackMonths(); + + /// + /// Selects the contracts of the second nearest expiration + /// + TSelf BackMonth(); + + /// + /// Selects the contracts with delta in the given range + /// + TSelf Delta(decimal min, decimal max); + + /// + /// Selects the contracts with delta in the given range. Alias for + /// + TSelf D(decimal min, decimal max); + + /// + /// Selects the contracts with gamma in the given range + /// + TSelf Gamma(decimal min, decimal max); + + /// + /// Selects the contracts with gamma in the given range. Alias for + /// + TSelf G(decimal min, decimal max); + + /// + /// Selects the contracts with theta in the given range + /// + TSelf Theta(decimal min, decimal max); + + /// + /// Selects the contracts with theta in the given range. Alias for + /// + TSelf T(decimal min, decimal max); + + /// + /// Selects the contracts with vega in the given range + /// + TSelf Vega(decimal min, decimal max); + + /// + /// Selects the contracts with vega in the given range. Alias for + /// + TSelf V(decimal min, decimal max); + + /// + /// Selects the contracts with rho in the given range + /// + TSelf Rho(decimal min, decimal max); + + /// + /// Selects the contracts with rho in the given range. Alias for + /// + TSelf R(decimal min, decimal max); + + /// + /// Selects the contracts with implied volatility in the given range + /// + TSelf ImpliedVolatility(decimal min, decimal max); + + /// + /// Selects the contracts with implied volatility in the given range. Alias for + /// + TSelf IV(decimal min, decimal max); + + /// + /// Selects the contracts with open interest in the given range + /// + TSelf OpenInterest(long min, long max); + + /// + /// Selects the contracts with open interest in the given range. Alias for + /// + TSelf OI(long min, long max); + + /// + /// Selects the single call contract with the closest match to the criteria given + /// + TSelf NakedCall(int minDaysTillExpiry = 30, decimal strikeFromAtm = 0); + + /// + /// Selects the single put contract with the closest match to the criteria given + /// + TSelf NakedPut(int minDaysTillExpiry = 30, decimal strikeFromAtm = 0); + + /// + /// Selects the 2 call contracts with the same expiry and different strikes closest to the criteria given + /// + TSelf CallSpread(int minDaysTillExpiry = 30, decimal higherStrikeFromAtm = 5, decimal? lowerStrikeFromAtm = null); + + /// + /// Selects the 2 put contracts with the same expiry and different strikes closest to the criteria given + /// + TSelf PutSpread(int minDaysTillExpiry = 30, decimal higherStrikeFromAtm = 5, decimal? lowerStrikeFromAtm = null); + + /// + /// Selects the 2 call contracts with the same strike and different expiries closest to the criteria given + /// + TSelf CallCalendarSpread(decimal strikeFromAtm = 0, int minNearDaysTillExpiry = 30, int minFarDaysTillExpiry = 60); + + /// + /// Selects the 2 put contracts with the same strike and different expiries closest to the criteria given + /// + TSelf PutCalendarSpread(decimal strikeFromAtm = 0, int minNearDaysTillExpiry = 30, int minFarDaysTillExpiry = 60); + + /// + /// Selects an OTM call and an OTM put with the same expiry closest to the criteria given + /// + TSelf Strangle(int minDaysTillExpiry = 30, decimal callStrikeFromAtm = 5, decimal putStrikeFromAtm = -5); + + /// + /// Selects the ATM call and the ATM put with the same expiry closest to the criteria given + /// + TSelf Straddle(int minDaysTillExpiry = 30); + + /// + /// Selects a call and a put with the same expiry and a lower put strike closest to the criteria given + /// + TSelf ProtectiveCollar(int minDaysTillExpiry = 30, decimal callStrikeFromAtm = 5, decimal putStrikeFromAtm = -5); + + /// + /// Selects a call and a put with the same expiry and strike closest to the criteria given + /// + TSelf Conversion(int minDaysTillExpiry = 30, decimal strikeFromAtm = 5); + + /// + /// Selects an ITM, an ATM and an OTM call with the same expiry and equal strike distance closest to the criteria given + /// + TSelf CallButterfly(int minDaysTillExpiry = 30, decimal strikeSpread = 5); + + /// + /// Selects an ITM, an ATM and an OTM put with the same expiry and equal strike distance closest to the criteria given + /// + TSelf PutButterfly(int minDaysTillExpiry = 30, decimal strikeSpread = 5); + + /// + /// Selects an OTM call, an ATM call, an ATM put and an OTM put with the same expiry and equal strike distance closest to the criteria given + /// + TSelf IronButterfly(int minDaysTillExpiry = 30, decimal strikeSpread = 5); + + /// + /// Selects a far OTM call, a near OTM call, a near OTM put and a far OTM put with the same expiry closest to the criteria given + /// + TSelf IronCondor(int minDaysTillExpiry = 30, decimal nearStrikeSpread = 5, decimal farStrikeSpread = 10); + + /// + /// Selects an OTM call, an ITM call, an OTM put and an ITM put with the same expiry closest to the criteria given + /// + TSelf BoxSpread(int minDaysTillExpiry = 30, decimal strikeSpread = 5); + + /// + /// Selects 2 calls and 2 puts with the same strike and 2 expiries closest to the criteria given + /// + TSelf JellyRoll(decimal strikeFromAtm = 0, int minNearDaysTillExpiry = 30, int minFarDaysTillExpiry = 60); + + /// + /// Selects 3 calls with the same expiry and different strikes closest to the criteria given + /// + TSelf CallLadder(int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal middleStrikeFromAtm, decimal lowerStrikeFromAtm); + + /// + /// Selects 3 puts with the same expiry and different strikes closest to the criteria given + /// + TSelf PutLadder(int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal middleStrikeFromAtm, decimal lowerStrikeFromAtm); + } +} diff --git a/Common/Securities/Option/OptionChainFilterUniverse.cs b/Common/Securities/Option/OptionChainFilterUniverse.cs new file mode 100644 index 000000000000..37d68d575813 --- /dev/null +++ b/Common/Securities/Option/OptionChainFilterUniverse.cs @@ -0,0 +1,95 @@ +/* + * 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.Collections.Generic; +using System.Linq; +using QuantConnect.Data; +using QuantConnect.Data.Market; + +namespace QuantConnect.Securities +{ + /// + /// Option contracts filter over the contracts of an , so chains offer + /// the same filters as the option universe selection () + /// + internal class OptionChainFilterUniverse : BaseOptionFilterUniverse + { + private readonly Symbol _symbol; + private SecurityExchangeHours _exchangeHours; + + /// + /// The option exchange hours + /// + protected override SecurityExchangeHours ExchangeHours => + _exchangeHours ??= MarketHoursDatabase.FromDataFolder().GetExchangeHours(_symbol.ID.Market, _symbol, _symbol.SecurityType); + + /// + /// The option security type + /// + protected override SecurityType SecurityType => _symbol.SecurityType; + + /// + /// Initializes a new instance of the class over the contracts of the given chain + /// + /// The option chain to filter + public OptionChainFilterUniverse(OptionChain chain) + : base(GetContracts(chain), GetUnderlying(chain), chain.ExchangeTime, GetStrikeMultiplier(chain)) + { + _symbol = chain.Symbol; + } + + /// + /// Not supported: the chain filters only ever select contracts that are already in the chain + /// + protected override OptionContract CreateDataInstance(Symbol symbol) + { + throw new InvalidOperationException($"OptionChainFilterUniverse.CreateDataInstance(): {symbol} is not part of the chain"); + } + + /// + /// Gets the greeks of the given contract + /// + protected override Greeks GetGreeks(OptionContract contract) => contract.Greeks; + + /// + /// Gets the implied volatility of the given contract + /// + protected override decimal GetImpliedVolatility(OptionContract contract) => contract.ImpliedVolatility; + + /// + /// Gets the open interest of the given contract + /// + protected override decimal GetOpenInterest(OptionContract contract) => contract.OpenInterest; + + private static IReadOnlyList GetContracts(OptionChain chain) + { + // The dictionary caches its values as a list that is replaced, never mutated, so it is safe to share + return chain.Contracts.Values as IReadOnlyList ?? chain.Contracts.Values.ToList(); + } + + private static BaseData GetUnderlying(OptionChain chain) + { + // A chain without underlying data carries an empty placeholder, which must not be used as a zero price + var underlying = chain.Underlying; + return underlying != null && underlying.Price != 0 ? underlying : null; + } + + private static decimal GetStrikeMultiplier(OptionChain chain) + { + return chain.Contracts.Values.FirstOrDefault()?.SymbolProperties?.StrikeMultiplier ?? 1; + } + } +} diff --git a/Common/Securities/Option/OptionFilterUniverse.cs b/Common/Securities/Option/OptionFilterUniverse.cs index 5cb946a97286..f7050a692203 100644 --- a/Common/Securities/Option/OptionFilterUniverse.cs +++ b/Common/Securities/Option/OptionFilterUniverse.cs @@ -20,6 +20,7 @@ using System.Runtime.CompilerServices; using Python.Runtime; using QuantConnect.Data; +using QuantConnect.Data.Market; using QuantConnect.Data.UniverseSelection; using QuantConnect.Securities.FutureOption; using QuantConnect.Securities.IndexOption; @@ -28,12 +29,15 @@ namespace QuantConnect.Securities { /// - /// Represents options symbols universe used in filtering. + /// Base option contracts filter, shared by the option universe selection filter () + /// and the option chain filters () so both offer the same filters with the same semantics /// - public class OptionFilterUniverse : ContractSecurityFilterUniverse + /// The concrete filter universe type + /// The option contract data type + public abstract class BaseOptionFilterUniverse : ContractSecurityFilterUniverse, IOptionContractFilters + where TUniverse : BaseOptionFilterUniverse + where TData : ISymbolProvider { - private Option.Option _option; - // Fields used in relative strikes filter private List _uniqueStrikes; private bool _refreshUniqueStrikes; @@ -45,6 +49,31 @@ public class OptionFilterUniverse : ContractSecurityFilterUniverse protected BaseData UnderlyingInternal { get; set; } + /// + /// The option exchange hours, used to resolve trading dates. Can be null, in which case no date adjustment is made + /// + protected abstract SecurityExchangeHours ExchangeHours { get; } + + /// + /// The option security type + /// + protected abstract SecurityType SecurityType { get; } + + /// + /// Gets the greeks of the given contract + /// + protected abstract Greeks GetGreeks(TData contract); + + /// + /// Gets the implied volatility of the given contract + /// + protected abstract decimal GetImpliedVolatility(TData contract); + + /// + /// Gets the open interest of the given contract + /// + protected abstract decimal GetOpenInterest(TData contract); + /// /// The underlying price data /// @@ -57,27 +86,29 @@ public BaseData Underlying } /// - /// Constructs OptionFilterUniverse + /// Constructs BaseOptionFilterUniverse /// By default, the filter includes both standard and weekly contracts. /// - /// The canonical option chain security - public OptionFilterUniverse(Option.Option option) + /// The option strike multiplier, see + protected BaseOptionFilterUniverse(decimal underlyingScaleFactor) { - _option = option; - _underlyingScaleFactor = option.SymbolProperties.StrikeMultiplier; + _underlyingScaleFactor = underlyingScaleFactor; } /// - /// Constructs OptionFilterUniverse + /// Constructs BaseOptionFilterUniverse /// - /// Used for testing only - public OptionFilterUniverse(Option.Option option, IReadOnlyList allData, BaseData underlying, decimal underlyingScaleFactor = 1) - : base(allData, underlying.EndTime) + /// All data for the option contracts + /// The current underlying last data point + /// The current local time + /// The option strike multiplier, see + protected BaseOptionFilterUniverse(IReadOnlyList allData, BaseData underlying, DateTime localTime, decimal underlyingScaleFactor = 1) + : base(allData, localTime) { - _option = option; UnderlyingInternal = underlying; _refreshUniqueStrikes = true; _underlyingScaleFactor = underlyingScaleFactor; + _lastExchangeDate = localTime.Date; } /// @@ -86,7 +117,7 @@ public OptionFilterUniverse(Option.Option option, IReadOnlyList /// All data for the option contracts /// The current underlying last data point /// The current local time - public void Refresh(IReadOnlyList allContractsData, BaseData underlying, DateTime localTime) + public void Refresh(IReadOnlyList allContractsData, BaseData underlying, DateTime localTime) { base.Refresh(allContractsData, localTime); @@ -112,19 +143,6 @@ protected override bool IsStandard(Symbol symbol) } } - /// - /// Creates a new instance of the data type for the given symbol - /// - /// A data instance for the given symbol - protected override OptionUniverse CreateDataInstance(Symbol symbol) - { - return new OptionUniverse() - { - Symbol = symbol, - Time = LocalTime - }; - } - /// /// Adjusts the date to the next trading day if the current date is not a trading day, so that expiration filter is properly applied. /// e.g. Selection for Mondays happen on Friday midnight (Saturday start), so if the minimum time to expiration is, say 0, @@ -135,9 +153,9 @@ protected override OptionUniverse CreateDataInstance(Symbol symbol) protected override DateTime AdjustExpirationReferenceDate(DateTime referenceDate) { // Check whether the reference time is a tradable date: - if (!_option.Exchange.Hours.IsDateOpen(referenceDate)) + if (ExchangeHours != null && !ExchangeHours.IsDateOpen(referenceDate)) { - referenceDate = _option.Exchange.Hours.GetNextTradingDay(referenceDate); + referenceDate = ExchangeHours.GetNextTradingDay(referenceDate); } return referenceDate; @@ -149,11 +167,11 @@ protected override DateTime AdjustExpirationReferenceDate(DateTime referenceDate /// The minimum strike relative to the underlying price, for example, -1 would filter out contracts further than 1 strike below market price /// The maximum strike relative to the underlying price, for example, +1 would filter out contracts further than 1 strike above market price /// Universe with filter applied - public OptionFilterUniverse Strikes(int minStrike, int maxStrike) + public TUniverse Strikes(int minStrike, int maxStrike) { if (UnderlyingInternal == null) { - return this; + return (TUniverse)this; } if (_refreshUniqueStrikes || _uniqueStrikes == null) @@ -235,19 +253,19 @@ public OptionFilterUniverse Strikes(int minStrike, int maxStrike) Data = Data .Where(data => { - var price = data.ID.StrikePrice; + var price = data.Symbol.ID.StrikePrice; return price >= minPrice && price <= maxPrice; } ).ToList(); - return this; + return (TUniverse)this; } /// /// Sets universe of call options (if any) as a selection /// /// Universe with filter applied - public OptionFilterUniverse CallsOnly() + public TUniverse CallsOnly() { return Contracts(contracts => contracts.Where(x => x.Symbol.ID.OptionRight == OptionRight.Call)); } @@ -256,7 +274,7 @@ public OptionFilterUniverse CallsOnly() /// Sets universe of put options (if any) as a selection /// /// Universe with filter applied - public OptionFilterUniverse PutsOnly() + public TUniverse PutsOnly() { return Contracts(contracts => contracts.Where(x => x.Symbol.ID.OptionRight == OptionRight.Put)); } @@ -268,7 +286,7 @@ public OptionFilterUniverse PutsOnly() /// The desire strike price distance from the current underlying price /// Applicable to Naked Call, Covered Call, and Protective Call Option Strategy /// Universe with filter applied - public OptionFilterUniverse NakedCall(int minDaysTillExpiry = 30, decimal strikeFromAtm = 0) + public TUniverse NakedCall(int minDaysTillExpiry = 30, decimal strikeFromAtm = 0) { return SingleContract(OptionRight.Call, minDaysTillExpiry, strikeFromAtm); } @@ -280,13 +298,18 @@ public OptionFilterUniverse NakedCall(int minDaysTillExpiry = 30, decimal strike /// The desire strike price distance from the current underlying price /// Applicable to Naked Put, Covered Put, and Protective Put Option Strategy /// Universe with filter applied - public OptionFilterUniverse NakedPut(int minDaysTillExpiry = 30, decimal strikeFromAtm = 0) + public TUniverse NakedPut(int minDaysTillExpiry = 30, decimal strikeFromAtm = 0) { return SingleContract(OptionRight.Put, minDaysTillExpiry, strikeFromAtm); } - private OptionFilterUniverse SingleContract(OptionRight right, int minDaysTillExpiry = 30, decimal strikeFromAtm = 0) + private TUniverse SingleContract(OptionRight right, int minDaysTillExpiry = 30, decimal strikeFromAtm = 0) { + if (UnderlyingInternal == null) + { + return Empty(); + } + // Select the expiry as the nearest to set days later var contractsForExpiry = GetContractsForExpiry(AllSymbols, minDaysTillExpiry); var contracts = contractsForExpiry.Where(x => x.ID.OptionRight == right).ToList(); @@ -310,7 +333,7 @@ private OptionFilterUniverse SingleContract(OptionRight right, int minDaysTillEx /// The desire strike price distance from the current underlying price of the lower strike price /// Applicable to Bear Call Spread and Bull Call Spread Option Strategy /// Universe with filter applied - public OptionFilterUniverse CallSpread(int minDaysTillExpiry = 30, decimal higherStrikeFromAtm = 5, decimal? lowerStrikeFromAtm = null) + public TUniverse CallSpread(int minDaysTillExpiry = 30, decimal higherStrikeFromAtm = 5, decimal? lowerStrikeFromAtm = null) { return Spread(OptionRight.Call, minDaysTillExpiry, higherStrikeFromAtm, lowerStrikeFromAtm); } @@ -323,12 +346,12 @@ public OptionFilterUniverse CallSpread(int minDaysTillExpiry = 30, decimal highe /// The desire strike price distance from the current underlying price of the lower strike price /// Applicable to Bear Put Spread and Bull Put Spread Option Strategy /// Universe with filter applied - public OptionFilterUniverse PutSpread(int minDaysTillExpiry = 30, decimal higherStrikeFromAtm = 5, decimal? lowerStrikeFromAtm = null) + public TUniverse PutSpread(int minDaysTillExpiry = 30, decimal higherStrikeFromAtm = 5, decimal? lowerStrikeFromAtm = null) { return Spread(OptionRight.Put, minDaysTillExpiry, higherStrikeFromAtm, lowerStrikeFromAtm); } - private OptionFilterUniverse Spread(OptionRight right, int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal? lowerStrikeFromAtm = null) + private TUniverse Spread(OptionRight right, int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal? lowerStrikeFromAtm = null) { if (!lowerStrikeFromAtm.HasValue) { @@ -341,6 +364,11 @@ private OptionFilterUniverse Spread(OptionRight right, int minDaysTillExpiry, de + $"{nameof(higherStrikeFromAtm)}, {nameof(lowerStrikeFromAtm)}"); } + if (UnderlyingInternal == null) + { + return Empty(); + } + // Select the expiry as the nearest to set days later var contractsForExpiry = GetContractsForExpiry(AllSymbols, minDaysTillExpiry); var contracts = contractsForExpiry.Where(x => x.ID.OptionRight == right).ToList(); @@ -372,7 +400,7 @@ private OptionFilterUniverse Spread(OptionRight right, int minDaysTillExpiry, de /// The mininum days till expiry of the further conrtact from the current time, closest expiry will be selected /// Applicable to Long and Short Call Calendar Spread Option Strategy /// Universe with filter applied - public OptionFilterUniverse CallCalendarSpread(decimal strikeFromAtm = 0, int minNearDaysTillExpiry = 30, int minFarDaysTillExpiry = 60) + public TUniverse CallCalendarSpread(decimal strikeFromAtm = 0, int minNearDaysTillExpiry = 30, int minFarDaysTillExpiry = 60) { return CalendarSpread(OptionRight.Call, strikeFromAtm, minNearDaysTillExpiry, minFarDaysTillExpiry); } @@ -385,12 +413,12 @@ public OptionFilterUniverse CallCalendarSpread(decimal strikeFromAtm = 0, int mi /// The mininum days till expiry of the further conrtact from the current time, closest expiry will be selected /// Applicable to Long and Short Put Calendar Spread Option Strategy /// Universe with filter applied - public OptionFilterUniverse PutCalendarSpread(decimal strikeFromAtm = 0, int minNearDaysTillExpiry = 30, int minFarDaysTillExpiry = 60) + public TUniverse PutCalendarSpread(decimal strikeFromAtm = 0, int minNearDaysTillExpiry = 30, int minFarDaysTillExpiry = 60) { return CalendarSpread(OptionRight.Put, strikeFromAtm, minNearDaysTillExpiry, minFarDaysTillExpiry); } - private OptionFilterUniverse CalendarSpread(OptionRight right, decimal strikeFromAtm, int minNearDaysTillExpiry, int minFarDaysTillExpiry) + private TUniverse CalendarSpread(OptionRight right, decimal strikeFromAtm, int minNearDaysTillExpiry, int minFarDaysTillExpiry) { if (minFarDaysTillExpiry <= minNearDaysTillExpiry) { @@ -403,6 +431,11 @@ private OptionFilterUniverse CalendarSpread(OptionRight right, decimal strikeFro throw new ArgumentException("CalendarSpread(): near expiry argument must be positive."); } + if (UnderlyingInternal == null) + { + return Empty(); + } + // Select the set strike var strike = GetStrike(AllSymbols, strikeFromAtm); var contracts = AllSymbols.Where(x => x.ID.StrikePrice == strike && x.ID.OptionRight == right).ToList(); @@ -432,7 +465,7 @@ private OptionFilterUniverse CalendarSpread(OptionRight right, decimal strikeFro /// The desire strike price distance from the current underlying price of the OTM put. It must be negative. /// Applicable to Long and Short Strangle Option Strategy /// Universe with filter applied - public OptionFilterUniverse Strangle(int minDaysTillExpiry = 30, decimal callStrikeFromAtm = 5, decimal putStrikeFromAtm = -5) + public TUniverse Strangle(int minDaysTillExpiry = 30, decimal callStrikeFromAtm = 5, decimal putStrikeFromAtm = -5) { if (callStrikeFromAtm <= 0) { @@ -453,7 +486,7 @@ public OptionFilterUniverse Strangle(int minDaysTillExpiry = 30, decimal callStr /// The minimum days till expiry from the current time, closest expiry will be selected /// Applicable to Long and Short Straddle Option Strategy /// Universe with filter applied - public OptionFilterUniverse Straddle(int minDaysTillExpiry = 30) + public TUniverse Straddle(int minDaysTillExpiry = 30) { return CallPutSpread(minDaysTillExpiry, 0, 0); } @@ -466,7 +499,7 @@ public OptionFilterUniverse Straddle(int minDaysTillExpiry = 30) /// The desire strike price distance from the current underlying price of the put. /// Applicable to Protective Collar Option Strategy /// Universe with filter applied - public OptionFilterUniverse ProtectiveCollar(int minDaysTillExpiry = 30, decimal callStrikeFromAtm = 5, decimal putStrikeFromAtm = -5) + public TUniverse ProtectiveCollar(int minDaysTillExpiry = 30, decimal callStrikeFromAtm = 5, decimal putStrikeFromAtm = -5) { if (callStrikeFromAtm <= putStrikeFromAtm) { @@ -476,9 +509,9 @@ public OptionFilterUniverse ProtectiveCollar(int minDaysTillExpiry = 30, decimal var filtered = CallPutSpread(minDaysTillExpiry, callStrikeFromAtm, putStrikeFromAtm); - var callStrike = filtered.Single(x => x.ID.OptionRight == OptionRight.Call).ID.StrikePrice; - var putStrike = filtered.Single(x => x.ID.OptionRight == OptionRight.Put).ID.StrikePrice; - if (callStrike <= putStrike) + var call = filtered.SingleOrDefault(x => x.Symbol.ID.OptionRight == OptionRight.Call); + var put = filtered.SingleOrDefault(x => x.Symbol.ID.OptionRight == OptionRight.Put); + if (call == null || put == null || call.Symbol.ID.StrikePrice <= put.Symbol.ID.StrikePrice) { return Empty(); } @@ -493,13 +526,18 @@ public OptionFilterUniverse ProtectiveCollar(int minDaysTillExpiry = 30, decimal /// The desire strike price distance from the current underlying price /// Applicable to Conversion and Reverse Conversion Option Strategy /// Universe with filter applied - public OptionFilterUniverse Conversion(int minDaysTillExpiry = 30, decimal strikeFromAtm = 5) + public TUniverse Conversion(int minDaysTillExpiry = 30, decimal strikeFromAtm = 5) { return CallPutSpread(minDaysTillExpiry, strikeFromAtm, strikeFromAtm); } - private OptionFilterUniverse CallPutSpread(int minDaysTillExpiry, decimal callStrikeFromAtm, decimal putStrikeFromAtm, bool otm = false) + private TUniverse CallPutSpread(int minDaysTillExpiry, decimal callStrikeFromAtm, decimal putStrikeFromAtm, bool otm = false) { + if (UnderlyingInternal == null) + { + return Empty(); + } + // Select the expiry as the nearest to set days later var contracts = GetContractsForExpiry(AllSymbols, minDaysTillExpiry).ToList(); @@ -534,7 +572,7 @@ private OptionFilterUniverse CallPutSpread(int minDaysTillExpiry, decimal callSt /// The desire strike price distance of the ITM call and the OTM call from the current underlying price /// Applicable to Long and Short Call Butterfly Option Strategy /// Universe with filter applied - public OptionFilterUniverse CallButterfly(int minDaysTillExpiry = 30, decimal strikeSpread = 5) + public TUniverse CallButterfly(int minDaysTillExpiry = 30, decimal strikeSpread = 5) { return Butterfly(OptionRight.Call, minDaysTillExpiry, strikeSpread); } @@ -546,18 +584,23 @@ public OptionFilterUniverse CallButterfly(int minDaysTillExpiry = 30, decimal st /// The desire strike price distance of the ITM put and the OTM put from the current underlying price /// Applicable to Long and Short Put Butterfly Option Strategy /// Universe with filter applied - public OptionFilterUniverse PutButterfly(int minDaysTillExpiry = 30, decimal strikeSpread = 5) + public TUniverse PutButterfly(int minDaysTillExpiry = 30, decimal strikeSpread = 5) { return Butterfly(OptionRight.Put, minDaysTillExpiry, strikeSpread); } - private OptionFilterUniverse Butterfly(OptionRight right, int minDaysTillExpiry, decimal strikeSpread) + private TUniverse Butterfly(OptionRight right, int minDaysTillExpiry, decimal strikeSpread) { if (strikeSpread <= 0) { throw new ArgumentException("ProtectiveCollar(): strikeSpread arguments must be positive"); } + if (UnderlyingInternal == null) + { + return Empty(); + } + // Select the expiry as the nearest to set days later var contractsForExpiry = GetContractsForExpiry(AllSymbols, minDaysTillExpiry); var contracts = contractsForExpiry.Where(x => x.ID.OptionRight == right).ToList(); @@ -576,9 +619,9 @@ private OptionFilterUniverse Butterfly(OptionRight right, int minDaysTillExpiry, } // Select the contracts - var filtered = this.Where(x => - x.ID.Date == contracts[0].ID.Date && x.ID.OptionRight == right && - (x.ID.StrikePrice == atmStrike || x.ID.StrikePrice == lowerStrike || x.ID.StrikePrice == upperStrike)); + var filtered = Contracts(data => data.Where(x => + x.Symbol.ID.Date == contracts[0].ID.Date && x.Symbol.ID.OptionRight == right && + (x.Symbol.ID.StrikePrice == atmStrike || x.Symbol.ID.StrikePrice == lowerStrike || x.Symbol.ID.StrikePrice == upperStrike))); if (filtered.Count() != 3) { return Empty(); @@ -593,13 +636,18 @@ private OptionFilterUniverse Butterfly(OptionRight right, int minDaysTillExpiry, /// The desire strike price distance of the OTM call and the OTM put from the current underlying price /// Applicable to Long and Short Iron Butterfly Option Strategy /// Universe with filter applied - public OptionFilterUniverse IronButterfly(int minDaysTillExpiry = 30, decimal strikeSpread = 5) + public TUniverse IronButterfly(int minDaysTillExpiry = 30, decimal strikeSpread = 5) { if (strikeSpread <= 0) { throw new ArgumentException("IronButterfly(): strikeSpread arguments must be positive"); } + if (UnderlyingInternal == null) + { + return Empty(); + } + // Select the expiry as the nearest to set days later var contracts = GetContractsForExpiry(AllSymbols, minDaysTillExpiry).ToList(); var calls = contracts.Where(x => x.ID.OptionRight == OptionRight.Call && x.ID.StrikePrice > Underlying.Price).ToList(); @@ -619,12 +667,12 @@ public OptionFilterUniverse IronButterfly(int minDaysTillExpiry = 30, decimal st otmPutStrike = atmStrike * 2 - otmCallStrike; } - var filtered = this.Where(x => - x.ID.Date == contracts[0].ID.Date && ( - x.ID.StrikePrice == atmStrike || - (x.ID.OptionRight == OptionRight.Call && x.ID.StrikePrice == otmCallStrike) || - (x.ID.OptionRight == OptionRight.Put && x.ID.StrikePrice == otmPutStrike) - )); + var filtered = Contracts(data => data.Where(x => + x.Symbol.ID.Date == contracts[0].ID.Date && ( + x.Symbol.ID.StrikePrice == atmStrike || + (x.Symbol.ID.OptionRight == OptionRight.Call && x.Symbol.ID.StrikePrice == otmCallStrike) || + (x.Symbol.ID.OptionRight == OptionRight.Put && x.Symbol.ID.StrikePrice == otmPutStrike) + ))); if (filtered.Count() != 4) { return Empty(); @@ -641,7 +689,7 @@ public OptionFilterUniverse IronButterfly(int minDaysTillExpiry = 30, decimal st /// The desire strike price distance of the further-to-expiry call and the further-to-expiry put from the current underlying price /// Applicable to Long and Short Iron Condor Option Strategy /// Universe with filter applied - public OptionFilterUniverse IronCondor(int minDaysTillExpiry = 30, decimal nearStrikeSpread = 5, decimal farStrikeSpread = 10) + public TUniverse IronCondor(int minDaysTillExpiry = 30, decimal nearStrikeSpread = 5, decimal farStrikeSpread = 10) { if (nearStrikeSpread <= 0 || farStrikeSpread <= 0) { @@ -655,6 +703,11 @@ public OptionFilterUniverse IronCondor(int minDaysTillExpiry = 30, decimal nearS + $"{nameof(nearStrikeSpread)}, {nameof(farStrikeSpread)}"); } + if (UnderlyingInternal == null) + { + return Empty(); + } + // Select the expiry as the nearest to set days later var contracts = GetContractsForExpiry(AllSymbols, minDaysTillExpiry).ToList(); var calls = contracts.Where(x => x.ID.OptionRight == OptionRight.Call && x.ID.StrikePrice > Underlying.Price).ToList(); @@ -676,13 +729,13 @@ public OptionFilterUniverse IronCondor(int minDaysTillExpiry = 30, decimal nearS } // Select the contracts - var filtered = this.Where(x => - x.ID.Date == contracts[0].ID.Date && ( - (x.ID.OptionRight == OptionRight.Call && x.ID.StrikePrice == nearCallStrike) || - (x.ID.OptionRight == OptionRight.Put && x.ID.StrikePrice == nearPutStrike) || - (x.ID.OptionRight == OptionRight.Call && x.ID.StrikePrice == farCallStrike) || - (x.ID.OptionRight == OptionRight.Put && x.ID.StrikePrice == farPutStrike) - )); + var filtered = Contracts(data => data.Where(x => + x.Symbol.ID.Date == contracts[0].ID.Date && ( + (x.Symbol.ID.OptionRight == OptionRight.Call && x.Symbol.ID.StrikePrice == nearCallStrike) || + (x.Symbol.ID.OptionRight == OptionRight.Put && x.Symbol.ID.StrikePrice == nearPutStrike) || + (x.Symbol.ID.OptionRight == OptionRight.Call && x.Symbol.ID.StrikePrice == farCallStrike) || + (x.Symbol.ID.OptionRight == OptionRight.Put && x.Symbol.ID.StrikePrice == farPutStrike) + ))); if (filtered.Count() != 4) { return Empty(); @@ -698,13 +751,18 @@ public OptionFilterUniverse IronCondor(int minDaysTillExpiry = 30, decimal nearS /// The desire strike price distance of the OTM call and the OTM put from the current underlying price /// Applicable to Long and Short Box Spread Option Strategy /// Universe with filter applied - public OptionFilterUniverse BoxSpread(int minDaysTillExpiry = 30, decimal strikeSpread = 5) + public TUniverse BoxSpread(int minDaysTillExpiry = 30, decimal strikeSpread = 5) { if (strikeSpread <= 0) { throw new ArgumentException($"BoxSpread(): strike arguments must be positive, {nameof(strikeSpread)}"); } + if (UnderlyingInternal == null) + { + return Empty(); + } + // Select the expiry as the nearest to set days later var contracts = GetContractsForExpiry(AllSymbols, minDaysTillExpiry).ToList(); if (contracts.Count == 0) @@ -717,9 +775,9 @@ public OptionFilterUniverse BoxSpread(int minDaysTillExpiry = 30, decimal strike var lowerStrike = GetStrike(contracts.Where(x => x.ID.StrikePrice < higherStrike && x.ID.StrikePrice < Underlying.Price), -strikeSpread); // Select the contracts - var filtered = this.Where(x => - (x.ID.StrikePrice == higherStrike || x.ID.StrikePrice == lowerStrike) && - x.ID.Date == contracts[0].ID.Date); + var filtered = Contracts(data => data.Where(x => + (x.Symbol.ID.StrikePrice == higherStrike || x.Symbol.ID.StrikePrice == lowerStrike) && + x.Symbol.ID.Date == contracts[0].ID.Date)); if (filtered.Count() != 4) { return Empty(); @@ -735,7 +793,7 @@ public OptionFilterUniverse BoxSpread(int minDaysTillExpiry = 30, decimal strike /// The mininum days till expiry of the further conrtact from the current time, closest expiry will be selected /// Applicable to Long and Short Jelly Roll Option Strategy /// Universe with filter applied - public OptionFilterUniverse JellyRoll(decimal strikeFromAtm = 0, int minNearDaysTillExpiry = 30, int minFarDaysTillExpiry = 60) + public TUniverse JellyRoll(decimal strikeFromAtm = 0, int minNearDaysTillExpiry = 30, int minFarDaysTillExpiry = 60) { if (minFarDaysTillExpiry <= minNearDaysTillExpiry) { @@ -748,6 +806,11 @@ public OptionFilterUniverse JellyRoll(decimal strikeFromAtm = 0, int minNearDays throw new ArgumentException("JellyRoll(): near expiry argument must be positive."); } + if (UnderlyingInternal == null) + { + return Empty(); + } + // Select the set strike var strike = AllSymbols.OrderBy(x => Math.Abs(Underlying.Price - x.ID.StrikePrice + strikeFromAtm)) .First().ID.StrikePrice; @@ -769,7 +832,7 @@ public OptionFilterUniverse JellyRoll(decimal strikeFromAtm = 0, int minNearDays } var farExpiry = farExpiryContract.ID.Date; - var filtered = this.Where(x => x.ID.StrikePrice == strike && (x.ID.Date == nearExpiry || x.ID.Date == farExpiry)); + var filtered = Contracts(data => data.Where(x => x.Symbol.ID.StrikePrice == strike && (x.Symbol.ID.Date == nearExpiry || x.Symbol.ID.Date == farExpiry))); if (filtered.Count() != 4) { return Empty(); @@ -786,7 +849,7 @@ public OptionFilterUniverse JellyRoll(decimal strikeFromAtm = 0, int minNearDays /// The desire strike price distance from the current underlying price of the lower strike price /// Applicable to Bear Call Ladder and Bull Call Ladder Option Strategy /// Universe with filter applied - public OptionFilterUniverse CallLadder(int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal middleStrikeFromAtm, decimal lowerStrikeFromAtm) + public TUniverse CallLadder(int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal middleStrikeFromAtm, decimal lowerStrikeFromAtm) { return Ladder(OptionRight.Call, minDaysTillExpiry, higherStrikeFromAtm, middleStrikeFromAtm, lowerStrikeFromAtm); } @@ -800,7 +863,7 @@ public OptionFilterUniverse CallLadder(int minDaysTillExpiry, decimal higherStri /// The desire strike price distance from the current underlying price of the lower strike price /// Applicable to Bear Put Ladder and Bull Put Ladder Option Strategy /// Universe with filter applied - public OptionFilterUniverse PutLadder(int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal middleStrikeFromAtm, decimal lowerStrikeFromAtm) + public TUniverse PutLadder(int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal middleStrikeFromAtm, decimal lowerStrikeFromAtm) { return Ladder(OptionRight.Put, minDaysTillExpiry, higherStrikeFromAtm, middleStrikeFromAtm, lowerStrikeFromAtm); } @@ -811,10 +874,10 @@ public OptionFilterUniverse PutLadder(int minDaysTillExpiry, decimal higherStrik /// The minimum Delta value /// The maximum Delta value /// Universe with filter applied - public OptionFilterUniverse Delta(decimal min, decimal max) + public TUniverse Delta(decimal min, decimal max) { ValidateSecurityTypeForSupportedFilters(nameof(Delta)); - return this.Where(contractData => contractData.Greeks.Delta >= min && contractData.Greeks.Delta <= max); + return InRange(contract => GetGreeks(contract).Delta, min, max); } /// @@ -824,7 +887,7 @@ public OptionFilterUniverse Delta(decimal min, decimal max) /// The minimum Delta value /// The maximum Delta value /// Universe with filter applied - public OptionFilterUniverse D(decimal min, decimal max) + public TUniverse D(decimal min, decimal max) { return Delta(min, max); } @@ -835,10 +898,10 @@ public OptionFilterUniverse D(decimal min, decimal max) /// The minimum Gamma value /// The maximum Gamma value /// Universe with filter applied - public OptionFilterUniverse Gamma(decimal min, decimal max) + public TUniverse Gamma(decimal min, decimal max) { ValidateSecurityTypeForSupportedFilters(nameof(Gamma)); - return this.Where(contractData => contractData.Greeks.Gamma >= min && contractData.Greeks.Gamma <= max); + return InRange(contract => GetGreeks(contract).Gamma, min, max); } /// @@ -848,7 +911,7 @@ public OptionFilterUniverse Gamma(decimal min, decimal max) /// The minimum Gamma value /// The maximum Gamma value /// Universe with filter applied - public OptionFilterUniverse G(decimal min, decimal max) + public TUniverse G(decimal min, decimal max) { return Gamma(min, max); } @@ -859,10 +922,10 @@ public OptionFilterUniverse G(decimal min, decimal max) /// The minimum Theta value /// The maximum Theta value /// Universe with filter applied - public OptionFilterUniverse Theta(decimal min, decimal max) + public TUniverse Theta(decimal min, decimal max) { ValidateSecurityTypeForSupportedFilters(nameof(Theta)); - return this.Where(contractData => contractData.Greeks.Theta >= min && contractData.Greeks.Theta <= max); + return InRange(contract => GetGreeks(contract).Theta, min, max); } /// @@ -872,7 +935,7 @@ public OptionFilterUniverse Theta(decimal min, decimal max) /// The minimum Theta value /// The maximum Theta value /// Universe with filter applied - public OptionFilterUniverse T(decimal min, decimal max) + public TUniverse T(decimal min, decimal max) { return Theta(min, max); } @@ -883,10 +946,10 @@ public OptionFilterUniverse T(decimal min, decimal max) /// The minimum Vega value /// The maximum Vega value /// Universe with filter applied - public OptionFilterUniverse Vega(decimal min, decimal max) + public TUniverse Vega(decimal min, decimal max) { ValidateSecurityTypeForSupportedFilters(nameof(Vega)); - return this.Where(contractData => contractData.Greeks.Vega >= min && contractData.Greeks.Vega <= max); + return InRange(contract => GetGreeks(contract).Vega, min, max); } /// @@ -896,7 +959,7 @@ public OptionFilterUniverse Vega(decimal min, decimal max) /// The minimum Vega value /// The maximum Vega value /// Universe with filter applied - public OptionFilterUniverse V(decimal min, decimal max) + public TUniverse V(decimal min, decimal max) { return Vega(min, max); } @@ -907,10 +970,10 @@ public OptionFilterUniverse V(decimal min, decimal max) /// The minimum Rho value /// The maximum Rho value /// Universe with filter applied - public OptionFilterUniverse Rho(decimal min, decimal max) + public TUniverse Rho(decimal min, decimal max) { ValidateSecurityTypeForSupportedFilters(nameof(Rho)); - return this.Where(contractData => contractData.Greeks.Rho >= min && contractData.Greeks.Rho <= max); + return InRange(contract => GetGreeks(contract).Rho, min, max); } /// @@ -920,7 +983,7 @@ public OptionFilterUniverse Rho(decimal min, decimal max) /// The minimum Rho value /// The maximum Rho value /// Universe with filter applied - public OptionFilterUniverse R(decimal min, decimal max) + public TUniverse R(decimal min, decimal max) { return Rho(min, max); } @@ -931,10 +994,10 @@ public OptionFilterUniverse R(decimal min, decimal max) /// The minimum implied volatility value /// The maximum implied volatility value /// Universe with filter applied - public OptionFilterUniverse ImpliedVolatility(decimal min, decimal max) + public TUniverse ImpliedVolatility(decimal min, decimal max) { ValidateSecurityTypeForSupportedFilters(nameof(ImpliedVolatility)); - return this.Where(contractData => contractData.ImpliedVolatility >= min && contractData.ImpliedVolatility <= max); + return InRange(GetImpliedVolatility, min, max); } /// @@ -944,7 +1007,7 @@ public OptionFilterUniverse ImpliedVolatility(decimal min, decimal max) /// The minimum implied volatility value /// The maximum implied volatility value /// Universe with filter applied - public OptionFilterUniverse IV(decimal min, decimal max) + public TUniverse IV(decimal min, decimal max) { return ImpliedVolatility(min, max); } @@ -955,10 +1018,10 @@ public OptionFilterUniverse IV(decimal min, decimal max) /// The minimum open interest value /// The maximum open interest value /// Universe with filter applied - public OptionFilterUniverse OpenInterest(long min, long max) + public TUniverse OpenInterest(long min, long max) { ValidateSecurityTypeForSupportedFilters(nameof(OpenInterest)); - return this.Where(contractData => contractData.OpenInterest >= min && contractData.OpenInterest <= max); + return InRange(GetOpenInterest, min, max); } /// @@ -968,25 +1031,12 @@ public OptionFilterUniverse OpenInterest(long min, long max) /// The minimum open interest value /// The maximum open interest value /// Universe with filter applied - public OptionFilterUniverse OI(long min, long max) + public TUniverse OI(long min, long max) { return OpenInterest(min, max); } - /// - /// Implicitly convert the universe to a list of symbols - /// - /// -#pragma warning disable CA1002 // Do not expose generic lists -#pragma warning disable CA2225 // Operator overloads have named alternates - public static implicit operator List(OptionFilterUniverse universe) - { - return universe.AllSymbols.ToList(); - } -#pragma warning restore CA2225 // Operator overloads have named alternates -#pragma warning restore CA1002 // Do not expose generic lists - - private OptionFilterUniverse Ladder(OptionRight right, int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal middleStrikeFromAtm, decimal lowerStrikeFromAtm) + private TUniverse Ladder(OptionRight right, int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal middleStrikeFromAtm, decimal lowerStrikeFromAtm) { if (higherStrikeFromAtm <= lowerStrikeFromAtm || higherStrikeFromAtm <= middleStrikeFromAtm || middleStrikeFromAtm <= lowerStrikeFromAtm) { @@ -994,6 +1044,11 @@ private OptionFilterUniverse Ladder(OptionRight right, int minDaysTillExpiry, de + $"{nameof(higherStrikeFromAtm)}, {nameof(middleStrikeFromAtm)}, {nameof(lowerStrikeFromAtm)}"); } + if (UnderlyingInternal == null) + { + return Empty(); + } + // Select the expiry as the nearest to set days later var contracts = GetContractsForExpiry(AllSymbols.Where(x => x.ID.OptionRight == right).ToList(), minDaysTillExpiry); @@ -1012,7 +1067,7 @@ private OptionFilterUniverse Ladder(OptionRight right, int minDaysTillExpiry, de return Empty(); } - return this.WhereContains(new List { lowerStrikeContract, middleStrikeContract, higherStrikeContract }); + return Contracts(data => data.Where(x => x.Symbol == lowerStrikeContract || x.Symbol == middleStrikeContract || x.Symbol == higherStrikeContract)); } /// @@ -1032,22 +1087,34 @@ private IEnumerable GetContractsForExpiry(IEnumerable symbols, i ?.OrderBy(x => x.ID) ?? Enumerable.Empty(); } + /// + /// Selects the contracts whose value, given by the selector, is within the given range. The selector runs once per contract + /// + private TUniverse InRange(Func selector, decimal min, decimal max) + { + return Contracts(data => data.Where(contract => + { + var value = selector(contract); + return value >= min && value <= max; + })); + } + /// /// Helper method that will select no contract /// - private OptionFilterUniverse Empty() + private TUniverse Empty() { - Data = Enumerable.Empty().ToList(); - return this; + Data = Enumerable.Empty().ToList(); + return (TUniverse)this; } /// /// Helper method that will select the given contract list /// - private OptionFilterUniverse SymbolList(List contracts) + private TUniverse SymbolList(List contracts) { AllSymbols = contracts; - return this; + return (TUniverse)this; } private decimal GetStrike(IEnumerable symbols, decimal strikeFromAtm) @@ -1061,13 +1128,93 @@ private decimal GetStrike(IEnumerable symbols, decimal strikeFromAtm) [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ValidateSecurityTypeForSupportedFilters(string filterName) { - if (_option.Symbol.SecurityType == SecurityType.FutureOption) + if (SecurityType == SecurityType.FutureOption) { throw new InvalidOperationException($"{filterName} filter is not supported for future options."); } } } + /// + /// Represents options symbols universe used in filtering. + /// + public class OptionFilterUniverse : BaseOptionFilterUniverse + { + private readonly Option.Option _option; + + /// + /// The option exchange hours + /// + protected override SecurityExchangeHours ExchangeHours => _option?.Exchange.Hours; + + /// + /// The option security type + /// + protected override SecurityType SecurityType => _option.Symbol.SecurityType; + + /// + /// Constructs OptionFilterUniverse + /// By default, the filter includes both standard and weekly contracts. + /// + /// The canonical option chain security + public OptionFilterUniverse(Option.Option option) + : base(option.SymbolProperties.StrikeMultiplier) + { + _option = option; + } + + /// + /// Constructs OptionFilterUniverse + /// + /// Used for testing only + public OptionFilterUniverse(Option.Option option, IReadOnlyList allData, BaseData underlying, decimal underlyingScaleFactor = 1) + : base(allData, underlying, underlying.EndTime, underlyingScaleFactor) + { + _option = option; + } + + /// + /// Creates a new instance of the data type for the given symbol + /// + /// A data instance for the given symbol + protected override OptionUniverse CreateDataInstance(Symbol symbol) + { + return new OptionUniverse() + { + Symbol = symbol, + Time = LocalTime + }; + } + + /// + /// Gets the greeks of the given contract + /// + protected override Greeks GetGreeks(OptionUniverse contract) => contract.Greeks; + + /// + /// Gets the implied volatility of the given contract + /// + protected override decimal GetImpliedVolatility(OptionUniverse contract) => contract.ImpliedVolatility; + + /// + /// Gets the open interest of the given contract + /// + protected override decimal GetOpenInterest(OptionUniverse contract) => contract.OpenInterest; + + /// + /// Implicitly convert the universe to a list of symbols + /// + /// +#pragma warning disable CA1002 // Do not expose generic lists +#pragma warning disable CA2225 // Operator overloads have named alternates + public static implicit operator List(OptionFilterUniverse universe) + { + return universe.AllSymbols.ToList(); + } +#pragma warning restore CA2225 // Operator overloads have named alternates +#pragma warning restore CA1002 // Do not expose generic lists + } + /// /// Extensions for Linq support /// @@ -1081,8 +1228,7 @@ public static class OptionFilterUniverseEx /// Universe with filter applied public static OptionFilterUniverse Where(this OptionFilterUniverse universe, Func predicate) { - universe.Data = universe.Data.Where(predicate).ToList(); - return universe; + return universe.Contracts(data => data.Where(predicate)); } /// @@ -1093,8 +1239,7 @@ public static OptionFilterUniverse Where(this OptionFilterUniverse universe, Fun /// Universe with filter applied public static OptionFilterUniverse Where(this OptionFilterUniverse universe, PyObject predicate) { - universe.Data = universe.Data.Where(predicate.SafeAs>()).ToList(); - return universe; + return universe.Where(predicate.SafeAs>()); } /// @@ -1105,8 +1250,7 @@ public static OptionFilterUniverse Where(this OptionFilterUniverse universe, PyO /// Universe with filter applied public static OptionFilterUniverse Select(this OptionFilterUniverse universe, Func mapFunc) { - universe.AllSymbols = universe.Data.Select(mapFunc).ToList(); - return universe; + return universe.Contracts(data => data.Select(mapFunc)); } /// @@ -1128,8 +1272,7 @@ public static OptionFilterUniverse Select(this OptionFilterUniverse universe, Py /// Universe with filter applied public static OptionFilterUniverse SelectMany(this OptionFilterUniverse universe, Func> mapFunc) { - universe.AllSymbols = universe.Data.SelectMany(mapFunc).ToList(); - return universe; + return universe.Contracts(data => data.SelectMany(mapFunc)); } /// @@ -1151,8 +1294,7 @@ public static OptionFilterUniverse SelectMany(this OptionFilterUniverse universe /// Universe with filter applied public static OptionFilterUniverse WhereContains(this OptionFilterUniverse universe, List filterList) { - universe.Data = universe.Data.Where(x => filterList.Contains(x)).ToList(); - return universe; + return universe.Where(x => filterList.Contains(x.Symbol)); } /// diff --git a/Engine/DataFeeds/TimeSliceFactory.cs b/Engine/DataFeeds/TimeSliceFactory.cs index 28208e25696a..153db02c58ba 100644 --- a/Engine/DataFeeds/TimeSliceFactory.cs +++ b/Engine/DataFeeds/TimeSliceFactory.cs @@ -427,7 +427,8 @@ private bool HandleOptionData(DateTime algorithmTime, BaseData baseData, OptionC var canonical = symbol.Canonical; if (!optionChains.TryGetValue(canonical, out chain)) { - chain = new OptionChain(canonical, algorithmTime); + // the data is already in the exchange time zone, unlike the algorithm time the chain is stamped with + chain = new OptionChain(canonical, algorithmTime) { ExchangeTime = baseData.EndTime }; optionChains[canonical] = chain; } @@ -504,7 +505,7 @@ private bool HandleFuturesData(DateTime algorithmTime, BaseData baseData, Future return false; } - chain = new FuturesChain(canonical, algorithmTime); + chain = new FuturesChain(canonical, algorithmTime) { ExchangeTime = baseData.EndTime }; futuresChains[canonical] = chain; } diff --git a/Tests/Common/Data/Market/OptionChainTests.cs b/Tests/Common/Data/Market/OptionChainTests.cs new file mode 100644 index 000000000000..f117c4dbd962 --- /dev/null +++ b/Tests/Common/Data/Market/OptionChainTests.cs @@ -0,0 +1,481 @@ +/* + * 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using NUnit.Framework; +using Python.Runtime; +using QuantConnect.Data; +using QuantConnect.Data.Market; +using QuantConnect.Data.UniverseSelection; +using QuantConnect.Securities; +using QuantConnect.Securities.Option; + +namespace QuantConnect.Tests.Common.Data.Market +{ + [TestFixture] + public class OptionChainTests + { + private static readonly DateTime Date = new(2016, 2, 26); + private static readonly Symbol Canonical = Symbol.CreateCanonicalOption(Symbols.SPY); + private const decimal UnderlyingPrice = 101m; + private static readonly DateTime[] Expiries = { new(2016, 3, 4), new(2016, 3, 18), new(2016, 4, 15), new(2016, 6, 17) }; + private static readonly decimal[] Strikes = { 90m, 95m, 97.5m, 100m, 102.5m, 105m, 110m }; + + private List _data; + private BaseData _underlying; + private SymbolProperties _symbolProperties; + private Option _option; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + (_data, _underlying) = CreateUniverseData(Date, UnderlyingPrice, Expiries, Strikes); + _symbolProperties = SymbolPropertiesDatabase.FromDataFolder().GetSymbolProperties(QuantConnect.Market.USA, Canonical, SecurityType.Option, Currencies.USD); + _option = CreateOption(); + } + + private static IEnumerable FilterCases() + { + yield return Case("Strikes(-2, 2)", u => u.Strikes(-2, 2), c => c.Strikes(-2, 2)); + yield return Case("Strikes(0, 0)", u => u.Strikes(0, 0), c => c.Strikes(0, 0)); + yield return Case("Strikes(-1, 0)", u => u.Strikes(-1, 0), c => c.Strikes(-1, 0)); + yield return Case("Strikes(-100, -10)", u => u.Strikes(-100, -10), c => c.Strikes(-100, -10), empty: true); + yield return Case("Expiration(0, 10)", u => u.Expiration(0, 10), c => c.Expiration(0, 10)); + yield return Case("Expiration(10, 60)", u => u.Expiration(10, 60), c => c.Expiration(10, 60)); + yield return Case("Expiration(TimeSpan)", u => u.Expiration(TimeSpan.FromDays(30), TimeSpan.FromDays(200)), + c => c.Expiration(TimeSpan.FromDays(30), TimeSpan.FromDays(200))); + yield return Case("Expiration(500, 600)", u => u.Expiration(500, 600), c => c.Expiration(500, 600), empty: true); + yield return Case("CallsOnly", u => u.CallsOnly(), c => c.CallsOnly()); + yield return Case("PutsOnly", u => u.PutsOnly(), c => c.PutsOnly()); + yield return Case("StandardsOnly", u => u.StandardsOnly(), c => c.StandardsOnly()); + yield return Case("WeeklysOnly", u => u.WeeklysOnly(), c => c.WeeklysOnly()); + yield return Case("FrontMonth", u => u.FrontMonth(), c => c.FrontMonth()); + yield return Case("BackMonth", u => u.BackMonth(), c => c.BackMonth()); + yield return Case("BackMonths", u => u.BackMonths(), c => c.BackMonths()); + yield return Case("Delta", u => u.Delta(0.4m, 0.6m), c => c.Delta(0.4m, 0.6m)); + yield return Case("D", u => u.D(-0.6m, -0.4m), c => c.D(-0.6m, -0.4m)); + yield return Case("Delta(5, 6)", u => u.Delta(5, 6), c => c.Delta(5, 6), empty: true); + yield return Case("Gamma", u => u.Gamma(0.012m, 0.02m), c => c.Gamma(0.012m, 0.02m)); + yield return Case("G", u => u.G(0.012m, 0.02m), c => c.G(0.012m, 0.02m)); + // theta is annualized from the per day value in the file + yield return Case("Theta", u => u.Theta(-365, -219), c => c.Theta(-365, -219)); + yield return Case("T", u => u.T(-365, -219), c => c.T(-365, -219)); + yield return Case("Vega", u => u.Vega(6, 8), c => c.Vega(6, 8)); + yield return Case("V", u => u.V(6, 8), c => c.V(6, 8)); + yield return Case("Rho", u => u.Rho(2, 4), c => c.Rho(2, 4)); + yield return Case("R", u => u.R(2, 4), c => c.R(2, 4)); + yield return Case("ImpliedVolatility", u => u.ImpliedVolatility(0.16m, 0.18m), c => c.ImpliedVolatility(0.16m, 0.18m)); + yield return Case("IV", u => u.IV(0.16m, 0.18m), c => c.IV(0.16m, 0.18m)); + yield return Case("OpenInterest", u => u.OpenInterest(200, 500), c => c.OpenInterest(200, 500)); + yield return Case("OI", u => u.OI(200, 500), c => c.OI(200, 500)); + yield return Case("CallsOnly.Expiration.Strikes", u => u.CallsOnly().Expiration(0, 30).Strikes(-1, 1), + c => c.CallsOnly().Expiration(0, 30).Strikes(-1, 1)); + yield return Case("PutsOnly.FrontMonth.Strikes", u => u.PutsOnly().FrontMonth().Strikes(-2, 0), + c => c.PutsOnly().FrontMonth().Strikes(-2, 0)); + yield return Case("StandardsOnly.FrontMonth", u => u.StandardsOnly().FrontMonth(), c => c.StandardsOnly().FrontMonth()); + yield return Case("WeeklysOnly.CallsOnly", u => u.WeeklysOnly().CallsOnly(), c => c.WeeklysOnly().CallsOnly()); + yield return Case("Expiration.Delta.Strikes", u => u.Expiration(0, 30).Delta(0.4m, 0.6m).Strikes(-3, 3), + c => c.Expiration(0, 30).Delta(0.4m, 0.6m).Strikes(-3, 3)); + yield return Case("NakedCall", u => u.NakedCall(10, 0), c => c.NakedCall(10, 0)); + yield return Case("NakedPut", u => u.NakedPut(10, -5), c => c.NakedPut(10, -5)); + yield return Case("NakedCall(1000)", u => u.NakedCall(1000, 0), c => c.NakedCall(1000, 0), empty: true); + yield return Case("CallSpread", u => u.CallSpread(10, 5), c => c.CallSpread(10, 5)); + yield return Case("PutSpread", u => u.PutSpread(10, 5, -5), c => c.PutSpread(10, 5, -5)); + yield return Case("CallCalendarSpread", u => u.CallCalendarSpread(0, 10, 40), c => c.CallCalendarSpread(0, 10, 40)); + yield return Case("PutCalendarSpread", u => u.PutCalendarSpread(0, 10, 40), c => c.PutCalendarSpread(0, 10, 40)); + yield return Case("Strangle", u => u.Strangle(10, 5, -5), c => c.Strangle(10, 5, -5)); + yield return Case("Straddle", u => u.Straddle(10), c => c.Straddle(10)); + yield return Case("ProtectiveCollar", u => u.ProtectiveCollar(10, 5, -5), c => c.ProtectiveCollar(10, 5, -5)); + yield return Case("Conversion", u => u.Conversion(10, 5), c => c.Conversion(10, 5)); + yield return Case("CallButterfly", u => u.CallButterfly(10, 5), c => c.CallButterfly(10, 5)); + yield return Case("PutButterfly", u => u.PutButterfly(10, 5), c => c.PutButterfly(10, 5)); + yield return Case("IronButterfly", u => u.IronButterfly(10, 5), c => c.IronButterfly(10, 5)); + yield return Case("IronCondor", u => u.IronCondor(10, 5, 10), c => c.IronCondor(10, 5, 10)); + yield return Case("BoxSpread", u => u.BoxSpread(10, 5), c => c.BoxSpread(10, 5)); + yield return Case("JellyRoll", u => u.JellyRoll(0, 10, 40), c => c.JellyRoll(0, 10, 40)); + yield return Case("CallLadder", u => u.CallLadder(10, 10, 5, -5), c => c.CallLadder(10, 10, 5, -5)); + yield return Case("PutLadder", u => u.PutLadder(10, 10, 5, -5), c => c.PutLadder(10, 10, 5, -5)); + yield return Case("StandardsOnly.IronCondor", u => u.StandardsOnly().IronCondor(10, 5, 10), c => c.StandardsOnly().IronCondor(10, 5, 10)); + } + + [TestCaseSource(nameof(FilterCases))] + public void ChainFiltersMatchUniverseFilters(Func universeFilter, + Func chainFilter, bool expectEmpty) + { + // the universe selection applies the contract type filters after the user filter + var expected = universeFilter(CreateUniverse()).ApplyTypesFilter().AsEnumerable().Select(x => x.Symbol.Value).ToList(); + var actual = chainFilter(CreateChain()).Select(x => x.Symbol.Value).ToList(); + + Assert.AreEqual(expectEmpty, expected.Count == 0); + CollectionAssert.AreEquivalent(expected, actual); + } + + [Test] + public void ChainExposesEveryUniverseFilter() + { + // Contracts() takes explicit symbols or a selector, which only makes sense for the universe selection + var universeFilters = typeof(OptionFilterUniverse) + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(x => x.ReturnType == typeof(OptionFilterUniverse) && x.Name != "Contracts" && !x.IsDefined(typeof(ObsoleteAttribute))) + .ToList(); + + Assert.IsNotEmpty(universeFilters); + foreach (var universeFilter in universeFilters) + { + var parameters = universeFilter.GetParameters().Select(x => x.ParameterType).ToArray(); + var chainFilter = typeof(IOptionContractFilters).GetMethod(universeFilter.Name, parameters); + Assert.IsNotNull(chainFilter, $"{universeFilter.Name}({string.Join(", ", parameters.Select(x => x.Name))}) is not an option chain filter"); + } + } + + [Test] + public void FilteredChainSharesTheAuxiliaryData() + { + var chain = CreateChain(); + var symbol = chain.First().Symbol; + chain.AddData(new TestAuxData { Symbol = symbol, Time = Date, Value = 1 }); + + var filtered = chain.PutsOnly(); + + Assert.IsNotNull(chain.GetAux(symbol)); + Assert.AreSame(chain.GetAux(symbol), filtered.GetAux(symbol)); + } + + [Test] + public void FilteredChainIsANewChainSharingTheSourceProperties() + { + var chain = CreateChain(); + var count = chain.Count; + + var filtered = chain.CallsOnly().FrontMonth(); + + Assert.AreNotSame(chain, filtered); + Assert.AreEqual(count, chain.Count); + Assert.AreEqual(Strikes.Length, filtered.Count); + Assert.IsTrue(filtered.All(x => x.Right == OptionRight.Call && x.Expiry == Expiries[0])); + Assert.IsTrue(filtered.ContainsKey(filtered.First().Symbol)); + Assert.AreEqual(chain.Symbol, filtered.Symbol); + Assert.AreEqual(chain.Time, filtered.Time); + Assert.AreSame(chain.Underlying, filtered.Underlying); + } + + [Test] + public void UnderlyingIsTakenFromTheContractsData() + { + var chain = CreateChain(); + + Assert.AreEqual(UnderlyingPrice, chain.Underlying.Price); + } + + [Test] + public void FiltersOnAnEmptyChainReturnAnEmptyChain() + { + var chain = new OptionChain(Canonical, Date); + + foreach (var testCase in FilterCases()) + { + var filter = (Func)testCase.Arguments[1]; + Assert.AreEqual(0, filter(chain).Count, testCase.TestName); + } + } + + [Test] + public void FiltersUseTheExchangeTimeAsTheReferenceDate() + { + // The engine stamps slice chains in the algorithm time zone, which can already be the day after the exchange date + var exchangeDate = new DateTime(2016, 3, 4); + var (data, underlying) = CreateUniverseData(exchangeDate, UnderlyingPrice, new[] { exchangeDate, Expiries[1] }, Strikes); + var chain = new OptionChain(Canonical, exchangeDate, data, _symbolProperties) { Time = exchangeDate.AddDays(1) }; + var universe = CreateUniverse(data, underlying, exchangeDate).Expiration(0, 0).ToList(); + + Assert.AreEqual(2 * Strikes.Length, universe.Count); + Assert.AreEqual(0, chain.Expiration(0, 0).Count); + + chain.ExchangeTime = exchangeDate; + var filtered = chain.Expiration(0, 0); + + CollectionAssert.AreEquivalent(universe.Select(x => x.Symbol.Value), filtered.Select(x => x.Symbol.Value)); + Assert.AreEqual(chain.Time, filtered.Time); + Assert.AreEqual(exchangeDate, filtered.ExchangeTime); + CollectionAssert.AreEquivalent(universe.Where(x => x.ID.OptionRight == OptionRight.Call).Select(x => x.Symbol.Value), + chain.CallsOnly().Expiration(0, 0).Select(x => x.Symbol.Value)); + } + + [Test] + public void StrikesFilterIsSkippedWithoutUnderlyingPrice() + { + var contracts = _data.Select(x => new OptionUniverse(x) { Underlying = null }).ToList(); + var chain = new OptionChain(Canonical, Date, contracts, _symbolProperties); + + Assert.AreEqual(0, chain.Underlying.Price); + Assert.AreEqual(chain.Count, chain.Strikes(0, 0).Count); + } + + [Test] + public void FiltersAreAvailableFromPython() + { + var chain = CreateChain(); + var expectedFiltered = chain.CallsOnly().Expiration(0, 30).Strikes(-1, 1).Select(x => x.Symbol).ToList(); + var expectedWhere = chain.Where(x => x.Right == OptionRight.Put && x.Strike > 100).Select(x => x.Symbol).ToList(); + Assert.IsNotEmpty(expectedFiltered); + Assert.IsNotEmpty(expectedWhere); + + using (Py.GIL()) + { + using var module = PyModule.FromString(nameof(OptionChainTests), @" +from AlgorithmImports import * + +def filter_chain(chain): + return chain.calls_only().expiration(0, 30).strikes(-1, 1) + +def where_chain(chain): + return chain.where(lambda contract: contract.right == OptionRight.PUT and contract.strike > 100) +"); + using var pyChain = chain.ToPython(); + + using var filtered = module.GetAttr("filter_chain").Invoke(pyChain); + CollectionAssert.AreEqual(expectedFiltered, filtered.As().Select(x => x.Symbol).ToList()); + + using var where = module.GetAttr("where_chain").Invoke(pyChain); + CollectionAssert.AreEqual(expectedWhere, where.As().Select(x => x.Symbol).ToList()); + } + } + + private static IEnumerable StrategyFilterCases() + { + yield return Case("NakedCall", u => u.NakedCall(10, 0), c => c.NakedCall(10, 0)); + yield return Case("CallSpread", u => u.CallSpread(10, 5, -5), c => c.CallSpread(10, 5, -5)); + yield return Case("CallCalendarSpread", u => u.CallCalendarSpread(0, 10, 40), c => c.CallCalendarSpread(0, 10, 40)); + yield return Case("Strangle", u => u.Strangle(10, 5, -5), c => c.Strangle(10, 5, -5)); + yield return Case("Straddle", u => u.Straddle(10), c => c.Straddle(10)); + yield return Case("ProtectiveCollar", u => u.ProtectiveCollar(10, 5, -5), c => c.ProtectiveCollar(10, 5, -5)); + yield return Case("Conversion", u => u.Conversion(10, 5), c => c.Conversion(10, 5)); + yield return Case("CallButterfly", u => u.CallButterfly(10, 5), c => c.CallButterfly(10, 5)); + yield return Case("IronButterfly", u => u.IronButterfly(10, 5), c => c.IronButterfly(10, 5)); + yield return Case("IronCondor", u => u.IronCondor(10, 5, 10), c => c.IronCondor(10, 5, 10)); + yield return Case("BoxSpread", u => u.BoxSpread(10, 5), c => c.BoxSpread(10, 5)); + yield return Case("JellyRoll", u => u.JellyRoll(0, 10, 40), c => c.JellyRoll(0, 10, 40)); + yield return Case("CallLadder", u => u.CallLadder(10, 5, 0, -5), c => c.CallLadder(10, 5, 0, -5)); + } + + [TestCaseSource(nameof(StrategyFilterCases))] + public void StrategyFiltersSelectNothingWithoutUnderlyingPrice(Func universeFilter, + Func chainFilter, bool _) + { + var contracts = _data.Select(x => new OptionUniverse(x) { Underlying = null }).ToList(); + var chain = new OptionChain(Canonical, Date, contracts, _symbolProperties); + var universe = new OptionFilterUniverse(_option); + universe.Refresh(contracts, null, Date); + + Assert.AreEqual(0, chain.Underlying.Price); + Assert.AreEqual(0, universeFilter(universe).Count); + Assert.AreEqual(0, chainFilter(chain).Count); + } + + [Test] + public void StrategyFiltersValidateArgumentsLikeTheUniverseFilters() + { + var contracts = _data.Select(x => new OptionUniverse(x) { Underlying = null }).ToList(); + var chains = new[] { CreateChain(), new OptionChain(Canonical, Date, contracts, _symbolProperties) }; + + foreach (var chain in chains) + { + Assert.Throws(() => chain.Strangle(10, -5, 5)); + Assert.Throws(() => chain.CallSpread(10, 5, 10)); + Assert.Throws(() => chain.IronCondor(10, 10, 5)); + Assert.Throws(() => chain.CallCalendarSpread(0, 40, 10)); + } + } + + [Test] + public void TypeFiltersApplyToTheChainContractsInAnyOrder() + { + var chain = CreateChain(); + var expected = CreateUniverse().StandardsOnly().FrontMonth().ToList().Select(x => x.Symbol.Value).ToList(); + + // The front month, 2016-03-04, is a weekly and 2016-03-18 the first standard expiry + Assert.AreEqual(2 * Strikes.Length, expected.Count); + CollectionAssert.AreEquivalent(expected, chain.StandardsOnly().FrontMonth().Select(x => x.Symbol.Value)); + Assert.AreEqual(0, chain.FrontMonth().StandardsOnly().Count); + Assert.IsTrue(chain.FrontMonth().WeeklysOnly().All(x => x.Expiry == Expiries[0])); + Assert.Throws(() => CreateUniverse().FrontMonth().StandardsOnly()); + } + + [Test] + public void StrategyFiltersAreAvailableFromPython() + { + var chain = CreateChain(); + var expectedIronCondor = chain.IronCondor(10, 5, 10).Select(x => x.Symbol.Value).ToList(); + var expectedNakedPut = chain.NakedPut(10, -5).Select(x => x.Symbol.Value).ToList(); + Assert.AreEqual(4, expectedIronCondor.Count); + Assert.AreEqual(1, expectedNakedPut.Count); + + using (Py.GIL()) + { + using var module = PyModule.FromString(nameof(OptionChainTests) + "Strategies", @" +from AlgorithmImports import * + +def iron_condor(chain): + return chain.iron_condor(10, 5, 10) + +def naked_put(chain): + return chain.naked_put(min_days_till_expiry=10, strike_from_atm=-5) +"); + using var pyChain = chain.ToPython(); + + using var ironCondor = module.GetAttr("iron_condor").Invoke(pyChain); + CollectionAssert.AreEquivalent(expectedIronCondor, ironCondor.As().Select(x => x.Symbol.Value).ToList()); + + using var nakedPut = module.GetAttr("naked_put").Invoke(pyChain); + CollectionAssert.AreEqual(expectedNakedPut, nakedPut.As().Select(x => x.Symbol.Value).ToList()); + } + } + + private class TestAuxData : BaseData + { + } + + private static TestCaseData Case(string name, Func universeFilter, + Func chainFilter, bool empty = false) + { + return new TestCaseData(universeFilter, chainFilter, empty).SetName("{m}(" + name + ")"); + } + + private OptionFilterUniverse CreateUniverse(List data = null, BaseData underlying = null, DateTime? date = null) + { + data ??= _data; + underlying ??= _underlying; + var universe = new OptionFilterUniverse(_option, data, underlying); + universe.Refresh(data, underlying, date ?? Date); + return universe; + } + + private OptionChain CreateChain() + { + return new OptionChain(Canonical, Date, _data, _symbolProperties); + } + + private static Option CreateOption() + { + var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(Canonical.ID.Market, Canonical, Canonical.SecurityType); + return new Option( + exchangeHours, + new SubscriptionDataConfig(typeof(TradeBar), Canonical, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, false, false), + new Cash(Currencies.USD, 0, 1m), + new OptionSymbolProperties(SymbolProperties.GetDefault(Currencies.USD)), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null); + } + + /// + /// Creates option universe data for every expiry/strike/right combination, with synthetic but monotonic + /// greeks, implied volatility and open interest so every range filter has a distinct answer + /// + private static (List, BaseData) CreateUniverseData(DateTime date, decimal spot, DateTime[] expiries, decimal[] strikes) + { + var contracts = new List<(Symbol, decimal, decimal, Greeks)>(); + var i = 0; + foreach (var expiry in expiries) + { + foreach (var strike in strikes) + { + foreach (var right in new[] { OptionRight.Call, OptionRight.Put }) + { + var symbol = Symbol.CreateOption(Canonical.Underlying, Canonical.ID.Market, OptionStyle.American, right, strike, expiry); + var callDelta = Math.Clamp(0.5m + (spot - strike) / 20m, 0.05m, 0.95m); + var delta = right == OptionRight.Call ? callDelta : callDelta - 1; + var greeks = new Greeks(delta, 0.01m + 0.001m * i, 5 + i, -(0.5m + 0.1m * i) * 365m, 1 + i, 0); + contracts.Add((symbol, 100 * (i + 1), 0.15m + 0.01m * i, greeks)); + i++; + } + } + } + + return CreateUniverseData(Canonical, date, spot, contracts); + } + + /// + /// Creates option universe data by writing a universe file with the same code the data generator uses, + /// , and reading it back with , + /// so the tests follow the file format instead of hard coding it + /// + /// The canonical option symbol + /// The universe file date + /// The underlying price, no underlying row is written when null + /// The contract rows to write + internal static (List contracts, BaseData underlying) CreateUniverseData(Symbol canonical, DateTime date, decimal? spot, + IEnumerable<(Symbol symbol, decimal openInterest, decimal impliedVolatility, Greeks greeks)> contracts) + { + var rows = contracts.ToList(); + var csv = new StringBuilder(); + csv.AppendLine("#" + OptionUniverse.CsvHeader(canonical.SecurityType)); + if (spot.HasValue) + { + csv.AppendLine(OptionUniverse.ToCsv(canonical.Underlying, spot.Value, spot.Value, spot.Value, spot.Value, 1000, null, null, null)); + } + var i = 0; + foreach (var (symbol, openInterest, impliedVolatility, greeks) in rows) + { + var price = 1 + i++; + csv.AppendLine(OptionUniverse.ToCsv(symbol, price, price, price, price, i, openInterest, impliedVolatility, greeks)); + } + + var config = new SubscriptionDataConfig(typeof(OptionUniverse), canonical, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, true, true, false); + var data = new List(); + BaseData underlying = null; + var factory = new OptionUniverse(); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(csv.ToString())); + using var reader = new StreamReader(stream); + while (!reader.EndOfStream) + { + var line = (OptionUniverse)factory.Reader(config, reader, date, false); + if (line == null) + { + continue; + } + if (line.Symbol.HasUnderlying) + { + // the underlying row comes first in the file and is attached to each contract, like the universe collection does + line.Underlying = underlying; + data.Add(line); + } + else + { + underlying = line; + } + } + + // Fail here if the serializer and the reader ever drift apart, rather than silently filtering the wrong values + Assert.AreEqual(rows.Count, data.Count); + for (var j = 0; j < rows.Count; j++) + { + Assert.AreEqual(rows[j].symbol, data[j].Symbol); + Assert.AreEqual(rows[j].openInterest, data[j].OpenInterest); + Assert.AreEqual(rows[j].impliedVolatility, data[j].ImpliedVolatility); + Assert.AreEqual(rows[j].greeks.Delta, data[j].Greeks.Delta); + Assert.AreEqual(rows[j].greeks.Theta, data[j].Greeks.Theta); + Assert.AreEqual(rows[j].greeks.Rho, data[j].Greeks.Rho); + } + Assert.AreEqual(spot ?? 0, underlying?.Price ?? 0); + + return (data, underlying); + } + } +} diff --git a/Tests/Common/Securities/Options/OptionFilterUniverseTests.cs b/Tests/Common/Securities/Options/OptionFilterUniverseTests.cs index 4a24e92ea195..bec1e639c4fd 100644 --- a/Tests/Common/Securities/Options/OptionFilterUniverseTests.cs +++ b/Tests/Common/Securities/Options/OptionFilterUniverseTests.cs @@ -230,6 +230,18 @@ public void OptionUnivereDataFiltersAreNotSupportedForFutureOptions() }); } + [Test] + public void TypeFiltersMustBeAppliedBeforeExpiryFilters() + { + var universe = new OptionFilterUniverse(GetOption(), _testOptionsData, _underlying); + var count = universe.Count; + + universe.FrontMonth(); + + Assert.Less(universe.Count, count); + Assert.Throws(() => universe.StandardsOnly()); + } + [Test] public void CountReturnsTheNumberOfContractsInTheUniverse() { diff --git a/Tests/Engine/DataFeeds/TimeSliceTests.cs b/Tests/Engine/DataFeeds/TimeSliceTests.cs index 95ef83abccbf..66ac87a81a82 100644 --- a/Tests/Engine/DataFeeds/TimeSliceTests.cs +++ b/Tests/Engine/DataFeeds/TimeSliceTests.cs @@ -178,6 +178,37 @@ public void OptionsDataHasVolume() } } + [TestCase(SecurityType.Option)] + [TestCase(SecurityType.Future)] + public void ChainExchangeTimeIsInTheExchangeTimeZone(SecurityType securityType) + { + var symbol = securityType == SecurityType.Option ? Symbols.SPY_C_192_Feb19_2016 : Symbols.Fut_SPY_Mar19_2016; + var config = new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, false); + var security = GetSecurity(config); + // 15:00 in New York on 2016-02-18 is already 05:00 on 2016-02-19 in Tokyo; the bars end at the slice time + var utcTime = new DateTime(2016, 2, 18, 20, 0, 0); + var time = utcTime.ConvertFromUtc(TimeZones.NewYork).AddMinutes(-1); + + var packets = new List(); + if (security is Option option) + { + var underlying = option.Underlying; + packets.Add(new DataFeedPacket(underlying, underlying.SubscriptionDataConfig, + new List { new TradeBar(time, underlying.Symbol, 100, 100, 110, 106, 100) })); + } + packets.Add(new DataFeedPacket(security, config, new List { new TradeBar(time, symbol, 100, 100, 110, 106, 100) })); + + var slice = new TimeSliceFactory(TimeZones.Tokyo).Create(utcTime, packets, + SecurityChangesTests.CreateNonInternal(Enumerable.Empty(), Enumerable.Empty()), + new Dictionary()).Slice; + var (chainTime, exchangeTime) = securityType == SecurityType.Option + ? (slice.OptionChains.Values.Single().Time, slice.OptionChains.Values.Single().ExchangeTime) + : (slice.FutureChains.Values.Single().Time, slice.FutureChains.Values.Single().ExchangeTime); + + Assert.AreEqual(new DateTime(2016, 2, 19, 5, 0, 0), chainTime); + Assert.AreEqual(new DateTime(2016, 2, 18, 15, 0, 0), exchangeTime); + } + [Test] public void SuspiciousTicksAreNotAddedToConsolidatorUpdateData() { @@ -266,16 +297,16 @@ private Security GetSecurity(SubscriptionDataConfig config) if (config.Symbol.SecurityType == SecurityType.Option) { var option = new Option( - SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), + SecurityExchangeHours.AlwaysOpen(config.ExchangeTimeZone), config, new Cash(Currencies.USD, 0, 1m), new OptionSymbolProperties(SymbolProperties.GetDefault(Currencies.USD)), ErrorCurrencyConverter.Instance, RegisteredSecurityDataTypesProvider.Null); var underlyingConfig = new SubscriptionDataConfig(typeof(TradeBar), config.Symbol.Underlying, Resolution.Second, - TimeZones.Utc, TimeZones.Utc, true, true, false); + config.ExchangeTimeZone, config.ExchangeTimeZone, true, true, false); var equity = new Equity( - SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), + SecurityExchangeHours.AlwaysOpen(config.ExchangeTimeZone), underlyingConfig, new Cash(Currencies.USD, 0, 1m), SymbolProperties.GetDefault(Currencies.USD), @@ -289,7 +320,7 @@ private Security GetSecurity(SubscriptionDataConfig config) if (config.Symbol.SecurityType == SecurityType.Future) { return new Future( - SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), + SecurityExchangeHours.AlwaysOpen(config.ExchangeTimeZone), config, new Cash(Currencies.USD, 0, 1m), SymbolProperties.GetDefault(Currencies.USD), @@ -298,7 +329,7 @@ private Security GetSecurity(SubscriptionDataConfig config) } return new Security( - SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), + SecurityExchangeHours.AlwaysOpen(config.ExchangeTimeZone), config, new Cash(Currencies.USD, 0, 1m), SymbolProperties.GetDefault(Currencies.USD),