diff --git a/Algorithm.CSharp/BasicTemplateFuturesAlgorithm.cs b/Algorithm.CSharp/BasicTemplateFuturesAlgorithm.cs index 8bb42742ea27..2ca8baa160fa 100644 --- a/Algorithm.CSharp/BasicTemplateFuturesAlgorithm.cs +++ b/Algorithm.CSharp/BasicTemplateFuturesAlgorithm.cs @@ -88,11 +88,7 @@ public override void OnData(Slice slice) foreach(var chain in slice.FutureChains) { // find the front contract expiring no earlier than in 90 days - var contract = ( - from futuresContract in chain.Value.OrderBy(x => x.Expiry) - where futuresContract.Expiry > Time.Date.AddDays(90) - select futuresContract - ).FirstOrDefault(); + var contract = chain.Value.ExpiringAfter(Time.Date.AddDays(90)).FrontMonth().FirstOrDefault(); // if found, trade it if (contract != null) diff --git a/Algorithm.CSharp/FuturesChainFiltersRegressionAlgorithm.cs b/Algorithm.CSharp/FuturesChainFiltersRegressionAlgorithm.cs new file mode 100644 index 000000000000..4d73ba7a56a9 --- /dev/null +++ b/Algorithm.CSharp/FuturesChainFiltersRegressionAlgorithm.cs @@ -0,0 +1,209 @@ +/* + * 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; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm using the futures chain filters, the same ones the futures universe selection offers, + /// on and on the chains of the + /// + public class FuturesChainFiltersRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private static readonly DateTime EndOf2013 = new(2013, 12, 31); + + private Symbol _es; + private Symbol _gc; + private bool _esChainSeen; + private bool _gcChainSeen; + private bool _traded; + + public override void Initialize() + { + SetStartDate(2013, 10, 7); + SetEndDate(2013, 10, 9); + SetCash(1000000); + + // The contracts expiring within a year + var es = AddFuture(Futures.Indices.SP500EMini, Resolution.Minute, Market.CME); + es.SetFilter(universe => universe.Expiration(0, 365)); + _es = es.Symbol; + + // The liquid contracts, by open interest + var gc = AddFuture(Futures.Metals.Gold, Resolution.Minute, Market.COMEX); + gc.SetFilter(universe => universe.OpenInterest(100000, long.MaxValue)); + _gc = gc.Symbol; + + // The full chain from the universe data: December 2013 and March, June, September and December 2014 + var chain = FuturesChain(_es); + if (chain.Count != 5) + { + throw new RegressionTestException($"Expected 5 ES contracts but got {chain.Count}"); + } + AssertExpiries(chain.FrontMonth(), "FrontMonth()", (2013, 12)); + AssertExpiries(chain.BackMonth(), "BackMonth()", (2014, 3)); + AssertExpiries(chain.BackMonths(), "BackMonths()", (2014, 3), (2014, 6), (2014, 9), (2014, 12)); + AssertExpiries(chain.FarthestExpiration(), "FarthestExpiration()", (2014, 12)); + AssertExpiries(chain.ExpirationCycle([3, 9]), "ExpirationCycle([3, 9])", (2014, 3), (2014, 9)); + // ES contracts are named after their expiration month, so the contract month filters agree with the expiration ones + AssertExpiries(chain.ContractMonths([3, 9]), "ContractMonths([3, 9])", (2014, 3), (2014, 9)); + AssertExpiries(chain.ExpiringBefore(EndOf2013), "ExpiringBefore(2013-12-31)", (2013, 12)); + AssertExpiries(chain.ExpiringAfter(EndOf2013).ExpiringBefore(new DateTime(2014, 7, 1)), "ExpiringAfter(2013-12-31).ExpiringBefore(2014-07-01)", (2014, 3), (2014, 6)); + AssertExpiries(chain.Expiration([chain.FrontMonth().First().Expiry]), "Expiration([front month expiry])", (2013, 12)); + if (chain.ZeroDte().Count != 0 || chain.StandardsOnly().Count != chain.Count || chain.WeeklysOnly().Count != 0) + { + throw new RegressionTestException("Expected no contract expiring today and only standard contracts"); + } + + // The liquidity filters read the universe data: only the front month has more than a million contracts open + AssertExpiries(chain.OpenInterest(1000000, long.MaxValue), "OpenInterest(1000000, max)", (2013, 12)); + if (chain.OI(0, 1000000).Count != chain.Count - 1 || chain.Volume(1, long.MaxValue).Count != chain.Count(x => x.Volume >= 1)) + { + throw new RegressionTestException("Open interest or volume filter mismatch"); + } + } + + public override void OnData(Slice slice) + { + if (slice.FuturesChains.TryGetValue(_es, out var esChain)) + { + _esChainSeen = true; + // The universe selected the contracts expiring within a year, so the chain filters agree with it + if (esChain.Count == 0 || esChain.Count > 4 || esChain.Expiration(0, 365).Count != esChain.Count || esChain.ExpiringAfter(Time).Count != esChain.Count + || esChain.ZeroDte().Count != 0 || esChain.ExpirationCycle([3, 6, 9, 12]).Count != esChain.Count || esChain.ExpirationCycle([1, 2]).Count != 0 + || esChain.StandardsOnly().Count != esChain.Count || esChain.WeeklysOnly().Count != 0) + { + throw new RegressionTestException("The ES slice chain disagrees with the universe filter"); + } + var frontMonth = esChain.FrontMonth(); + var farthest = esChain.FarthestExpiration(); + if (frontMonth.Count == 0 || frontMonth.Any(x => x.Expiry != esChain.Min(c => c.Expiry)) || farthest.Any(x => x.Expiry != esChain.Max(c => c.Expiry)) + || esChain.BackMonths().Count != esChain.Count - frontMonth.Count) + { + throw new RegressionTestException("Front month, back months or farthest expiration mismatch on the ES slice chain"); + } + if (esChain.OpenInterest(1, long.MaxValue).Count != esChain.Count(x => x.OpenInterest >= 1) || esChain.Volume(1, long.MaxValue).Count != esChain.Count(x => x.Volume >= 1)) + { + throw new RegressionTestException("Open interest or volume filter mismatch on the ES slice chain"); + } + + // Buy the front contract expiring at least 90 days out + if (!_traded) + { + var contract = esChain.ExpiringAfter(Time.Date.AddDays(90)).FrontMonth().FirstOrDefault(); + if (contract != null) + { + MarketOrder(contract.Symbol, 1); + _traded = true; + } + } + } + + if (slice.FuturesChains.TryGetValue(_gc, out var gcChain)) + { + _gcChainSeen = true; + // Only the December 2013 contract had more than a hundred thousand contracts open + if (gcChain.Count == 0 || gcChain.Any(x => x.Expiry.Year != 2013 || x.Expiry.Month != 12) || gcChain.FrontMonth().Count != gcChain.Count) + { + throw new RegressionTestException($"The GC slice chain disagrees with the universe filter: {string.Join(", ", gcChain.Select(x => x.Expiry))}"); + } + } + } + + public override void OnEndOfAlgorithm() + { + if (!_esChainSeen || !_gcChainSeen || !_traded) + { + throw new RegressionTestException($"Expected the ES chain ({_esChainSeen}), the GC chain ({_gcChainSeen}) and a trade ({_traded})"); + } + } + + private static void AssertExpiries(FuturesChain chain, string filter, params (int year, int month)[] expected) + { + var actual = chain.Select(x => (x.Expiry.Year, x.Expiry.Month)).OrderBy(x => x).ToList(); + if (!actual.SequenceEqual(expected.OrderBy(x => x))) + { + throw new RegressionTestException($"{filter}: expected {string.Join(", ", expected)} but got {string.Join(", ", actual)}"); + } + } + + /// + /// 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 => 34838; + + /// + /// 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", "-11.911%"}, + {"Drawdown", "0.200%"}, + {"Expectancy", "0"}, + {"Start Equity", "1000000"}, + {"End Equity", "998958.2"}, + {"Net Profit", "-0.104%"}, + {"Sharpe Ratio", "-9.32"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "-0.048"}, + {"Beta", "0.095"}, + {"Annual Standard Deviation", "0.013"}, + {"Annual Variance", "0"}, + {"Information Ratio", "5.187"}, + {"Tracking Error", "0.123"}, + {"Treynor Ratio", "-1.269"}, + {"Total Fees", "$2.15"}, + {"Estimated Strategy Capacity", "$940000000.00"}, + {"Lowest Capacity Asset", "ES VP274HSU1AF5"}, + {"Portfolio Turnover", "2.77%"}, + {"Drawdown Recovery", "0"}, + {"OrderListHash", "3b6b723d50c0d435d763aa456af197a6"} + }; + } +} diff --git a/Algorithm.Python/BasicTemplateFuturesAlgorithm.py b/Algorithm.Python/BasicTemplateFuturesAlgorithm.py index 517598d77fea..26694ca239bf 100644 --- a/Algorithm.Python/BasicTemplateFuturesAlgorithm.py +++ b/Algorithm.Python/BasicTemplateFuturesAlgorithm.py @@ -49,12 +49,9 @@ def initialize(self): def on_data(self,slice): if not self.portfolio.invested: for chain in slice.future_chains: - # Get contracts expiring no earlier than in 90 days - contracts = list(filter(lambda x: x.expiry > self.time + timedelta(90), chain.value)) - - # if there is any contract, trade the front contract - if len(contracts) == 0: continue - front = sorted(contracts, key = lambda x: x.expiry, reverse=True)[0] + # Get the front contract expiring no earlier than in 90 days, if any, and trade it + front = next(iter(chain.value.expiring_after(self.time + timedelta(90)).front_month()), None) + if front is None: continue self.contract_symbol = front.symbol self.market_order(front.symbol , 1) diff --git a/Algorithm.Python/FuturesChainFiltersRegressionAlgorithm.py b/Algorithm.Python/FuturesChainFiltersRegressionAlgorithm.py new file mode 100644 index 000000000000..98efb59501f5 --- /dev/null +++ b/Algorithm.Python/FuturesChainFiltersRegressionAlgorithm.py @@ -0,0 +1,106 @@ +# 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 using the futures chain filters, the same ones the futures universe selection offers, +### on futures_chain() and on the chains of the slice +### +class FuturesChainFiltersRegressionAlgorithm(QCAlgorithm): + END_OF_2013 = datetime(2013, 12, 31) + MAX_LONG = 2**62 + + def initialize(self): + self.set_start_date(2013, 10, 7) + self.set_end_date(2013, 10, 9) + self.set_cash(1000000) + + # The contracts expiring within a year + es = self.add_future(Futures.Indices.SP_500_E_MINI, Resolution.MINUTE, Market.CME) + es.set_filter(lambda universe: universe.expiration(0, 365)) + self._es = es.symbol + + # The liquid contracts, by open interest + gc = self.add_future(Futures.Metals.GOLD, Resolution.MINUTE, Market.COMEX) + gc.set_filter(lambda universe: universe.open_interest(100000, self.MAX_LONG)) + self._gc = gc.symbol + + self._es_chain_seen = False + self._gc_chain_seen = False + self._traded = False + + # The full chain from the universe data: December 2013 and March, June, September and December 2014 + chain = self.futures_chain(self._es) + if chain.count != 5: + raise AssertionError(f"Expected 5 ES contracts but got {chain.count}") + self._assert_expiries(chain.front_month(), "front_month()", [(2013, 12)]) + self._assert_expiries(chain.back_month(), "back_month()", [(2014, 3)]) + self._assert_expiries(chain.back_months(), "back_months()", [(2014, 3), (2014, 6), (2014, 9), (2014, 12)]) + self._assert_expiries(chain.farthest_expiration(), "farthest_expiration()", [(2014, 12)]) + self._assert_expiries(chain.expiration_cycle([3, 9]), "expiration_cycle([3, 9])", [(2014, 3), (2014, 9)]) + # ES contracts are named after their expiration month, so the contract month filters agree with the expiration ones + self._assert_expiries(chain.contract_months([3, 9]), "contract_months([3, 9])", [(2014, 3), (2014, 9)]) + self._assert_expiries(chain.expiring_before(self.END_OF_2013), "expiring_before(2013-12-31)", [(2013, 12)]) + self._assert_expiries(chain.expiring_after(self.END_OF_2013).expiring_before(datetime(2014, 7, 1)), "expiring_after(2013-12-31).expiring_before(2014-07-01)", [(2014, 3), (2014, 6)]) + self._assert_expiries(chain.expiration([next(iter(chain.front_month())).expiry]), "expiration([front month expiry])", [(2013, 12)]) + if chain.zero_dte().count != 0 or chain.standards_only().count != chain.count or chain.weeklys_only().count != 0: + raise AssertionError("Expected no contract expiring today and only standard contracts") + + # The liquidity filters read the universe data: only the front month has more than a million contracts open + self._assert_expiries(chain.open_interest(1000000, self.MAX_LONG), "open_interest(1000000, max)", [(2013, 12)]) + if chain.oi(0, 1000000).count != chain.count - 1 or chain.volume(1, self.MAX_LONG).count != sum(1 for x in chain if x.volume >= 1): + raise AssertionError("Open interest or volume filter mismatch") + + def on_data(self, slice): + es_chain = slice.futures_chains.get(self._es) + if es_chain: + self._es_chain_seen = True + # The universe selected the contracts expiring within a year, so the chain filters agree with it + if (es_chain.count == 0 or es_chain.count > 4 or es_chain.expiration(0, 365).count != es_chain.count or es_chain.expiring_after(self.time).count != es_chain.count + or es_chain.zero_dte().count != 0 or es_chain.expiration_cycle([3, 6, 9, 12]).count != es_chain.count or es_chain.expiration_cycle([1, 2]).count != 0 + or es_chain.standards_only().count != es_chain.count or es_chain.weeklys_only().count != 0): + raise AssertionError("The ES slice chain disagrees with the universe filter") + front_month = es_chain.front_month() + farthest = es_chain.farthest_expiration() + min_expiry = min(x.expiry for x in es_chain) + max_expiry = max(x.expiry for x in es_chain) + if (front_month.count == 0 or any(x.expiry != min_expiry for x in front_month) or any(x.expiry != max_expiry for x in farthest) + or es_chain.back_months().count != es_chain.count - front_month.count): + raise AssertionError("Front month, back months or farthest expiration mismatch on the ES slice chain") + if (es_chain.open_interest(1, self.MAX_LONG).count != sum(1 for x in es_chain if x.open_interest >= 1) + or es_chain.volume(1, self.MAX_LONG).count != sum(1 for x in es_chain if x.volume >= 1)): + raise AssertionError("Open interest or volume filter mismatch on the ES slice chain") + + # Buy the front contract expiring at least 90 days out + if not self._traded: + contract = next(iter(es_chain.expiring_after(self.time + timedelta(days=90)).front_month()), None) + if contract is not None: + self.market_order(contract.symbol, 1) + self._traded = True + + gc_chain = slice.futures_chains.get(self._gc) + if gc_chain: + self._gc_chain_seen = True + # Only the December 2013 contract had more than a hundred thousand contracts open + if gc_chain.count == 0 or any(x.expiry.year != 2013 or x.expiry.month != 12 for x in gc_chain) or gc_chain.front_month().count != gc_chain.count: + raise AssertionError(f"The GC slice chain disagrees with the universe filter: {[x.expiry for x in gc_chain]}") + + def on_end_of_algorithm(self): + if not self._es_chain_seen or not self._gc_chain_seen or not self._traded: + raise AssertionError(f"Expected the ES chain ({self._es_chain_seen}), the GC chain ({self._gc_chain_seen}) and a trade ({self._traded})") + + def _assert_expiries(self, chain, filter_name, expected): + actual = sorted((x.expiry.year, x.expiry.month) for x in chain) + if actual != sorted(expected): + raise AssertionError(f"{filter_name}: expected {expected} but got {actual}") diff --git a/Common/Data/Market/BaseChain.Filters.cs b/Common/Data/Market/BaseChain.Filters.cs new file mode 100644 index 000000000000..c8cbbb659c81 --- /dev/null +++ b/Common/Data/Market/BaseChain.Filters.cs @@ -0,0 +1,267 @@ +/* + * 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 Python.Runtime; +using QuantConnect.Securities; + +namespace QuantConnect.Data.Market +{ + /// + /// A chain of contracts with the filters of its universe selection, see . + /// Each filter returns a new chain, leaving this one untouched + /// + /// The type of contract in the chain + /// The type of the contracts collection + /// The chain type, returned by every filter + /// The filter universe applying the filters to the contracts of the chain + public abstract class BaseChain : BaseChain, IContractFilters + where T : BaseContract + where TContractsCollection : DataDictionary, new() + where TSelf : BaseChain + where TUniverse : ContractSecurityFilterUniverse + { + /// + /// Initializes a new instance of the class + /// + /// The symbol for this chain + /// The time of this chain + /// The type of data this chain represents + /// Whether to flatten the data frame + protected BaseChain(Symbol canonicalSymbol, DateTime time, MarketDataType dataType, bool flatten = true) + : base(canonicalSymbol, time, dataType, flatten) + { + } + + /// + /// Initializes a new instance of the class as a copy of the specified chain + /// + /// The chain to copy + protected BaseChain(BaseChain other) + : base(other) + { + } + + /// + /// Initializes a new instance of the class as a copy of the specified chain + /// containing only the given subset of its contracts + /// + /// The chain to copy + /// The contracts to keep + protected BaseChain(BaseChain other, IEnumerable contracts) + : base(other, contracts) + { + } + + #region Filters + + /// + /// 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 TSelf 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 TSelf Expiration(int minExpiryDays, int maxExpiryDays) + { + return Filter(universe => universe.Expiration(minExpiryDays, maxExpiryDays)); + } + + /// + /// Selects the contracts expiring on any of the given dates. Time of day is ignored. + /// Same as + /// + /// The expiration dates + /// A new chain with the filter applied + public TSelf Expiration(IEnumerable expiries) + { + return Filter(universe => universe.Expiration(expiries)); + } + + /// + /// Selects the contracts expiring after the given date, excluding it. Time of day is ignored. + /// Same as + /// + /// The date the expirations must be after + /// A new chain with the filter applied + public TSelf ExpiringAfter(DateTime date) + { + return Filter(universe => universe.ExpiringAfter(date)); + } + + /// + /// Selects the contracts expiring before the given date, excluding it. Time of day is ignored. + /// Same as + /// + /// The date the expirations must be before + /// A new chain with the filter applied + public TSelf ExpiringBefore(DateTime date) + { + return Filter(universe => universe.ExpiringBefore(date)); + } + + /// + /// Selects the contracts expiring today. Same as + /// + /// A new chain with the filter applied + public TSelf ZeroDte() + { + return Filter(universe => universe.ZeroDte()); + } + + /// + /// Selects the standard 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 TSelf StandardsOnly() + { + return Filter(universe => universe.StandardsOnly()); + } + + /// + /// Selects the non standard 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 TSelf WeeklysOnly() + { + return Filter(universe => universe.WeeklysOnly()); + } + + /// + /// Selects the contracts of the nearest expiration. Same as + /// + /// A new chain with the filter applied + public TSelf FrontMonth() + { + return Filter(universe => universe.FrontMonth()); + } + + /// + /// Selects the contracts of the farthest expiration. Same as + /// + /// A new chain with the filter applied + public TSelf FarthestExpiration() + { + return Filter(universe => universe.FarthestExpiration()); + } + + /// + /// Selects the contracts of all expirations but the nearest one. Same as + /// + /// A new chain with the filter applied + public TSelf BackMonths() + { + return Filter(universe => universe.BackMonths()); + } + + /// + /// Selects the contracts of the second nearest expiration. Same as + /// + /// A new chain with the filter applied + public TSelf BackMonth() + { + return Filter(universe => universe.BackMonth()); + } + + /// + /// 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 TSelf 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 TSelf OI(long min, long max) + { + return OpenInterest(min, max); + } + + /// + /// Selects the contracts with volume in the given range. Same as + /// + /// The minimum volume + /// The maximum volume + /// A new chain with the filter applied + public TSelf Volume(long min, long max) + { + return Filter(universe => universe.Volume(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 TSelf Where(PyObject predicate) + { + return CreateChain(Contracts.Values.Where(predicate.SafeAs>())); + } + + #endregion + + /// + /// Applies the given universe filter to the contracts of this chain and returns the result as a new chain + /// + /// The universe filter to apply + /// A new chain with the filter applied + protected TSelf Filter(Func filter) + { + var universe = CreateFilterUniverse(); + // the type filters (standards/weeklys) are only applied on demand, like the universe selection does after the user filter + return CreateChain(filter(universe).ApplyTypesFilter()); + } + + /// + /// Creates the filter universe over the contracts of this chain + /// + protected abstract TUniverse CreateFilterUniverse(); + + /// + /// Creates a copy of this chain with only the given contracts + /// + /// The contracts to keep + protected abstract TSelf CreateChain(IEnumerable contracts); + } +} diff --git a/Common/Data/Market/FuturesChain.Filters.cs b/Common/Data/Market/FuturesChain.Filters.cs new file mode 100644 index 000000000000..fe495e50904b --- /dev/null +++ b/Common/Data/Market/FuturesChain.Filters.cs @@ -0,0 +1,69 @@ +/* + * 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.Collections.Generic; +using QuantConnect.Securities; + +namespace QuantConnect.Data.Market +{ + /// + /// The futures chain filters, the same ones the futures universe selection offers, see . + /// The filters shared with the option chains live in . + /// Each filter returns a new chain, leaving this one untouched + /// + public partial class FuturesChain + { + /// + /// Selects the contracts expiring in any of the given months of the year. + /// Same as + /// + /// Months to select contracts from, see + /// A new chain with the filter applied + public FuturesChain ExpirationCycle(IEnumerable months) + { + return Filter(universe => universe.ExpirationCycle(months)); + } + + /// + /// Selects the contracts whose contract month is any of the given months of the year, like + /// but by the contract month, the month the contract is named after, which for some products, e.g. crude oil, is the month + /// after the expiration month. + /// Same as + /// + /// Months of the year to select contracts from, see + /// A new chain with the filter applied + public FuturesChain ContractMonths(IEnumerable months) + { + return Filter(universe => universe.ContractMonths(months)); + } + + /// + /// Creates the filter universe over the contracts of this chain + /// + protected override FuturesChainFilterUniverse CreateFilterUniverse() + { + return new FuturesChainFilterUniverse(this); + } + + /// + /// Creates a copy of this chain with only the given contracts + /// + /// The contracts to keep + protected override FuturesChain CreateChain(IEnumerable contracts) + { + return new FuturesChain(this, contracts); + } + } +} diff --git a/Common/Data/Market/FuturesChain.cs b/Common/Data/Market/FuturesChain.cs index de0acebb37f5..199b25facf4e 100644 --- a/Common/Data/Market/FuturesChain.cs +++ b/Common/Data/Market/FuturesChain.cs @@ -16,14 +16,18 @@ using System; using System.Collections.Generic; using QuantConnect.Data.UniverseSelection; +using QuantConnect.Securities; namespace QuantConnect.Data.Market { /// /// Represents an entire chain of futures contracts for a single underlying - /// This type is + /// This type is . + /// The chain can be narrowed down with the same filters available for futures universe selection + /// (see and ), e.g. chain.expiration(0, 90).front_month(). + /// Each filter returns a new chain, leaving this one untouched. /// - public class FuturesChain : BaseChain + public partial class FuturesChain : BaseChain, IFutureContractFilters { /// /// Initializes a new instance of the class @@ -61,6 +65,15 @@ private FuturesChain(FuturesChain other) { } + /// + /// Initializes a new instance of the class as a copy of the specified chain + /// containing only the given subset of its contracts + /// + private FuturesChain(FuturesChain other, IEnumerable contracts) + : base(other, contracts) + { + } + /// /// Return a new instance clone of this object, used in fill forward /// diff --git a/Common/Data/Market/OptionChain.Filters.cs b/Common/Data/Market/OptionChain.Filters.cs index 19b4c0179931..70c006a0a0b0 100644 --- a/Common/Data/Market/OptionChain.Filters.cs +++ b/Common/Data/Market/OptionChain.Filters.cs @@ -13,16 +13,14 @@ * limitations under the License. */ -using System; using System.Collections.Generic; -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 . + /// The filters shared with the futures chains live in . /// Each filter returns a new chain, leaving this one untouched /// public partial class OptionChain @@ -41,67 +39,6 @@ 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 contracts expiring on any of the given dates. Time of day is ignored. - /// Same as - /// - /// The expiration dates - /// A new chain with the filter applied - public OptionChain Expiration(IEnumerable expiries) - { - return Filter(universe => universe.Expiration(expiries)); - } - - /// - /// Selects the contracts expiring after the given date, excluding it. Time of day is ignored. - /// Same as - /// - /// The date the expirations must be after - /// A new chain with the filter applied - public OptionChain ExpiringAfter(DateTime date) - { - return Filter(universe => universe.ExpiringAfter(date)); - } - - /// - /// Selects the contracts expiring before the given date, excluding it. Time of day is ignored. - /// Same as - /// - /// The date the expirations must be before - /// A new chain with the filter applied - public OptionChain ExpiringBefore(DateTime date) - { - return Filter(universe => universe.ExpiringBefore(date)); - } - /// /// Selects the contracts with any of the given strike prices. /// Same as @@ -135,15 +72,6 @@ public OptionChain StrikesBelow(decimal price) return Filter(universe => universe.StrikesBelow(price)); } - /// - /// Selects the contracts expiring today. Same as - /// - /// A new chain with the filter applied - public OptionChain ZeroDte() - { - return Filter(universe => universe.ZeroDte()); - } - /// /// Selects the call contracts. Same as /// @@ -227,62 +155,6 @@ public OptionChain ATM(decimal? maxStrikeDistance = null) return AtTheMoney(maxStrikeDistance); } - /// - /// 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 the farthest expiration. Same as - /// - /// A new chain with the filter applied - public OptionChain FarthestExpiration() - { - return Filter(universe => universe.FarthestExpiration()); - } - - /// - /// 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 /// @@ -415,39 +287,6 @@ 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 @@ -661,17 +500,23 @@ public OptionChain PutLadder(int minDaysTillExpiry, decimal higherStrikeFromAtm, return Filter(universe => universe.PutLadder(minDaysTillExpiry, higherStrikeFromAtm, middleStrikeFromAtm, lowerStrikeFromAtm)); } + #endregion + /// - /// Applies the given universe filter to the contracts of this chain and returns the result as a new chain + /// Creates the filter universe over the contracts of this chain /// - /// The universe filter to apply - private OptionChain Filter(Func filter) + protected override OptionChainFilterUniverse CreateFilterUniverse() { - 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()); + return new OptionChainFilterUniverse(this); } - #endregion + /// + /// Creates a copy of this chain with only the given contracts + /// + /// The contracts to keep + protected override OptionChain CreateChain(IEnumerable contracts) + { + return new OptionChain(this, contracts); + } } } diff --git a/Common/Data/Market/OptionChain.cs b/Common/Data/Market/OptionChain.cs index 20c1ec0a7126..15f5cf8887dc 100644 --- a/Common/Data/Market/OptionChain.cs +++ b/Common/Data/Market/OptionChain.cs @@ -27,7 +27,7 @@ namespace QuantConnect.Data.Market /// (see and ), e.g. chain.calls_only().expiration(0, 30).strikes(-2, 2). /// Each filter returns a new chain, leaving this one untouched. /// - public partial class OptionChain : BaseChain, IOptionContractFilters + public partial class OptionChain : BaseChain, IOptionContractFilters { /// /// Initializes a new instance of the class diff --git a/Common/Securities/ContractSecurityFilterUniverse.cs b/Common/Securities/ContractSecurityFilterUniverse.cs index ba4b80ce5b1f..7e0356c54832 100644 --- a/Common/Securities/ContractSecurityFilterUniverse.cs +++ b/Common/Securities/ContractSecurityFilterUniverse.cs @@ -27,7 +27,7 @@ namespace QuantConnect.Securities /// Base class for contract symbols filtering universes. /// Used by OptionFilterUniverse and FutureFilterUniverse /// - public abstract class ContractSecurityFilterUniverse : IDerivativeSecurityFilterUniverse + public abstract class ContractSecurityFilterUniverse : IDerivativeSecurityFilterUniverse, IContractFilters where T : ContractSecurityFilterUniverse where TData : ISymbolProvider { @@ -143,6 +143,16 @@ protected ContractSecurityFilterUniverse(IReadOnlyList allData, DateTime /// A data instance for the given symbol protected abstract TData CreateDataInstance(Symbol symbol); + /// + /// Gets the open interest of the given contract + /// + protected abstract decimal GetOpenInterest(TData contract); + + /// + /// Gets the volume of the given contract + /// + protected abstract decimal GetVolume(TData contract); + /// /// Returns universe, filtered by contract type /// @@ -372,6 +382,15 @@ public T Expiration(int minExpiryDays, int maxExpiryDays) return Expiration(TimeSpan.FromDays(minExpiryDays), TimeSpan.FromDays(maxExpiryDays)); } + /// + /// Applies filter selecting the contracts expiring today + /// + /// Universe with filter applied + public T ZeroDte() + { + return Expiration(0, 0); + } + /// /// Applies filter selecting the contracts expiring on any of the given dates. Time of day is ignored /// @@ -408,6 +427,55 @@ public T ExpiringBefore(DateTime date) return (T)this; } + /// + /// Applies filter selecting the contracts with open interest between the given range + /// + /// The minimum open interest value + /// The maximum open interest value + /// Universe with filter applied + public virtual T OpenInterest(long min, long max) + { + return InRange(GetOpenInterest, min, max); + } + + /// + /// Applies filter selecting the contracts with open interest between the given range. Alias for + /// + /// The minimum open interest value + /// The maximum open interest value + /// Universe with filter applied + public T OI(long min, long max) + { + return OpenInterest(min, max); + } + + /// + /// Applies filter selecting the contracts with volume between the given range + /// + /// The minimum volume + /// The maximum volume + /// Universe with filter applied + public T Volume(long min, long max) + { + return InRange(GetVolume, min, max); + } + + /// + /// Selects the contracts whose value, given by the selector, is within the given range. The selector runs once per contract + /// + /// Gets the value of a contract + /// The minimum value + /// The maximum value + /// Universe with filter applied + protected T InRange(Func selector, decimal min, decimal max) + { + return Contracts(data => data.Where(contract => + { + var value = selector(contract); + return value >= min && value <= max; + })); + } + /// /// Explicitly sets the selected contract symbols for this universe. /// This overrides and and all other methods of selecting symbols assuming it is called last. diff --git a/Common/Securities/Future/FutureFilterUniverse.cs b/Common/Securities/Future/FutureFilterUniverse.cs index 8a30010d1a6d..67c5fd5bdb74 100644 --- a/Common/Securities/Future/FutureFilterUniverse.cs +++ b/Common/Securities/Future/FutureFilterUniverse.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.Linq; +using QuantConnect.Data; using QuantConnect.Data.UniverseSelection; using QuantConnect.Securities.Future; using QuantConnect.Util; @@ -24,14 +25,21 @@ namespace QuantConnect.Securities { /// - /// Represents futures symbols universe used in filtering. + /// Base future contracts filter, shared by the futures universe selection filter () + /// and the futures chain filters () so both offer the same filters with the same semantics /// - public class FutureFilterUniverse : ContractSecurityFilterUniverse + /// The concrete filter universe type + /// The future contract data type + public abstract class BaseFutureFilterUniverse : ContractSecurityFilterUniverse, IFutureContractFilters + where TUniverse : BaseFutureFilterUniverse + where TData : ISymbolProvider { /// - /// Constructs FutureFilterUniverse + /// Constructs BaseFutureFilterUniverse /// - public FutureFilterUniverse(IReadOnlyList allData, DateTime localTime) + /// All data for the future contracts + /// The current local time + protected BaseFutureFilterUniverse(IReadOnlyList allData, DateTime localTime) : base(allData, localTime) { } @@ -45,6 +53,44 @@ protected override bool IsStandard(Symbol symbol) return FutureSymbol.IsStandard(symbol); } + /// + /// Applies filter selecting futures contracts based on expiration cycles. See for details + /// + /// Months to select contracts from + /// Universe with filter applied + public TUniverse ExpirationCycle(IEnumerable months) + { + var monthHashSet = months.ToHashSet(); + return Contracts(contracts => contracts.Where(x => monthHashSet.Contains(x.Symbol.ID.Date.Month))); + } + + /// + /// Selects the contracts whose contract month is any of the given months of the year, see . + /// Like but by the contract month, the month the contract is named after, which for some products, + /// e.g. crude oil, is the month after the expiration month, see + /// + /// Months of the year to select contracts from + /// Universe with filter applied + public TUniverse ContractMonths(IEnumerable months) + { + var monthHashSet = months.ToHashSet(); + return Contracts(contracts => contracts.Where(x => monthHashSet.Contains(FuturesExpiryUtilityFunctions.GetFutureContractMonth(x.Symbol).Month))); + } + } + + /// + /// Represents futures symbols universe used in filtering. + /// + public class FutureFilterUniverse : BaseFutureFilterUniverse + { + /// + /// Constructs FutureFilterUniverse + /// + public FutureFilterUniverse(IReadOnlyList allData, DateTime localTime) + : base(allData, localTime) + { + } + /// /// Creates a new instance of the data type for the given symbol /// @@ -59,15 +105,14 @@ protected override FutureUniverse CreateDataInstance(Symbol symbol) } /// - /// Applies filter selecting futures contracts based on expiration cycles. See for details + /// Gets the open interest of the given contract /// - /// Months to select contracts from - /// Universe with filter applied - public FutureFilterUniverse ExpirationCycle(int[] months) - { - var monthHashSet = months.ToHashSet(); - return this.Where(x => monthHashSet.Contains(x.ID.Date.Month)); - } + protected override decimal GetOpenInterest(FutureUniverse contract) => contract.OpenInterest; + + /// + /// Gets the volume of the given contract + /// + protected override decimal GetVolume(FutureUniverse contract) => contract.Volume; } /// diff --git a/Common/Securities/Future/FuturesChainFilterUniverse.cs b/Common/Securities/Future/FuturesChainFilterUniverse.cs new file mode 100644 index 000000000000..e4b18b3146fe --- /dev/null +++ b/Common/Securities/Future/FuturesChainFilterUniverse.cs @@ -0,0 +1,62 @@ +/* + * 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.Market; + +namespace QuantConnect.Securities +{ + /// + /// Future contracts filter over the contracts of a , so chains offer + /// the same filters as the futures universe selection () + /// + public class FuturesChainFilterUniverse : BaseFutureFilterUniverse + { + /// + /// Initializes a new instance of the class over the contracts of the given chain + /// + /// The futures chain to filter + internal FuturesChainFilterUniverse(FuturesChain chain) + : base(GetContracts(chain), chain.ExchangeTime) + { + } + + /// + /// Not supported: the chain filters only ever select contracts that are already in the chain + /// + protected override FuturesContract CreateDataInstance(Symbol symbol) + { + throw new InvalidOperationException($"FuturesChainFilterUniverse.CreateDataInstance(): {symbol} is not part of the chain"); + } + + /// + /// Gets the open interest of the given contract + /// + protected override decimal GetOpenInterest(FuturesContract contract) => contract.OpenInterest; + + /// + /// Gets the volume of the given contract + /// + protected override decimal GetVolume(FuturesContract contract) => contract.Volume; + + private static IReadOnlyList GetContracts(FuturesChain 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(); + } + } +} diff --git a/Common/Securities/Future/IFutureContractFilters.cs b/Common/Securities/Future/IFutureContractFilters.cs new file mode 100644 index 000000000000..ec33bcc59f09 --- /dev/null +++ b/Common/Securities/Future/IFutureContractFilters.cs @@ -0,0 +1,39 @@ +/* + * 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.Collections.Generic; + +namespace QuantConnect.Securities +{ + /// + /// The future contract filters shared by the futures universe selection () + /// and the futures chain (), so both offer the same filters with the same semantics. + /// FuturesChainTests.ChainExposesEveryUniverseFilter checks that every universe filter is declared here + /// + /// The implementing type, returned by every filter for chaining + public interface IFutureContractFilters : IContractFilters + { + /// + /// Selects the contracts expiring in any of the given months of the year, see + /// + TSelf ExpirationCycle(IEnumerable months); + + /// + /// Selects the contracts whose contract month is any of the given months of the year, see . + /// Like but by the contract month instead of the expiration month + /// + TSelf ContractMonths(IEnumerable months); + } +} diff --git a/Common/Securities/IContractFilters.cs b/Common/Securities/IContractFilters.cs new file mode 100644 index 000000000000..93426b5cefe4 --- /dev/null +++ b/Common/Securities/IContractFilters.cs @@ -0,0 +1,103 @@ +/* + * 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; + +namespace QuantConnect.Securities +{ + /// + /// The contract filters shared by every derivative universe selection and chain: expirations, contract types and liquidity. + /// and add the option and future specific ones + /// + /// The implementing type, returned by every filter for chaining + public interface IContractFilters + { + /// + /// 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 contracts expiring on any of the given dates, ignoring the time of day + /// + TSelf Expiration(IEnumerable expiries); + + /// + /// Selects the contracts expiring after the given date, excluding it + /// + TSelf ExpiringAfter(DateTime date); + + /// + /// Selects the contracts expiring before the given date, excluding it + /// + TSelf ExpiringBefore(DateTime date); + + /// + /// Selects the contracts expiring today + /// + TSelf ZeroDte(); + + /// + /// 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 the farthest expiration + /// + TSelf FarthestExpiration(); + + /// + /// 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 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 contracts with volume in the given range + /// + TSelf Volume(long min, long max); + } +} diff --git a/Common/Securities/Option/IOptionContractFilters.cs b/Common/Securities/Option/IOptionContractFilters.cs index ae24dc316f28..3492479dcac8 100644 --- a/Common/Securities/Option/IOptionContractFilters.cs +++ b/Common/Securities/Option/IOptionContractFilters.cs @@ -13,49 +13,24 @@ * limitations under the License. */ -using System; using System.Collections.Generic; 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. + /// and the option chain (), so both offer the same filters with the same semantics, + /// on top of the ones every contract has, see . /// OptionChainTests.ChainExposesEveryUniverseFilter checks that every universe filter is declared here /// /// The implementing type, returned by every filter for chaining - public interface IOptionContractFilters + public interface IOptionContractFilters : IContractFilters { /// /// 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 contracts expiring on any of the given dates, ignoring the time of day - /// - TSelf Expiration(IEnumerable expiries); - - /// - /// Selects the contracts expiring after the given date, excluding it - /// - TSelf ExpiringAfter(DateTime date); - - /// - /// Selects the contracts expiring before the given date, excluding it - /// - TSelf ExpiringBefore(DateTime date); - /// /// Selects the contracts with any of the given strike prices /// @@ -71,11 +46,6 @@ public interface IOptionContractFilters /// TSelf StrikesBelow(decimal price); - /// - /// Selects the contracts expiring today - /// - TSelf ZeroDte(); - /// /// Selects the out of the money contracts: calls above and puts below the underlying price /// @@ -118,36 +88,6 @@ public interface IOptionContractFilters /// 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 the farthest expiration - /// - TSelf FarthestExpiration(); - - /// - /// 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 /// @@ -208,16 +148,6 @@ public interface IOptionContractFilters /// 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 /// diff --git a/Common/Securities/Option/OptionChainFilterUniverse.cs b/Common/Securities/Option/OptionChainFilterUniverse.cs index 37d68d575813..79ee28623d74 100644 --- a/Common/Securities/Option/OptionChainFilterUniverse.cs +++ b/Common/Securities/Option/OptionChainFilterUniverse.cs @@ -25,7 +25,7 @@ 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 + public class OptionChainFilterUniverse : BaseOptionFilterUniverse { private readonly Symbol _symbol; private SecurityExchangeHours _exchangeHours; @@ -45,7 +45,7 @@ internal class OptionChainFilterUniverse : BaseOptionFilterUniverse class over the contracts of the given chain /// /// The option chain to filter - public OptionChainFilterUniverse(OptionChain chain) + internal OptionChainFilterUniverse(OptionChain chain) : base(GetContracts(chain), GetUnderlying(chain), chain.ExchangeTime, GetStrikeMultiplier(chain)) { _symbol = chain.Symbol; @@ -74,6 +74,11 @@ protected override OptionContract CreateDataInstance(Symbol symbol) /// protected override decimal GetOpenInterest(OptionContract contract) => contract.OpenInterest; + /// + /// Gets the volume of the given contract + /// + protected override decimal GetVolume(OptionContract contract) => contract.Volume; + 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 diff --git a/Common/Securities/Option/OptionFilterUniverse.cs b/Common/Securities/Option/OptionFilterUniverse.cs index 0d11f6b79a5a..53dd5dc95d91 100644 --- a/Common/Securities/Option/OptionFilterUniverse.cs +++ b/Common/Securities/Option/OptionFilterUniverse.cs @@ -70,11 +70,6 @@ public abstract class BaseOptionFilterUniverse : ContractSecur /// protected abstract decimal GetImpliedVolatility(TData contract); - /// - /// Gets the open interest of the given contract - /// - protected abstract decimal GetOpenInterest(TData contract); - /// /// The underlying price data /// @@ -311,15 +306,6 @@ public TUniverse StrikesBelow(decimal price) return Contracts(contracts => contracts.Where(x => x.Symbol.ID.StrikePrice < price)); } - /// - /// Applies filter selecting the contracts expiring today - /// - /// Universe with filter applied - public TUniverse ZeroDte() - { - return Expiration(0, 0); - } - /// /// Applies filter selecting the out of the money contracts: calls with strikes above the underlying price /// and puts with strikes below it. Selects nothing when the underlying price is unknown @@ -1144,28 +1130,17 @@ public TUniverse IV(decimal min, decimal max) return ImpliedVolatility(min, max); } - /// - /// Applies the filter to the universe selecting the contracts with open interest between the given range - /// - /// The minimum open interest value - /// The maximum open interest value - /// Universe with filter applied - public TUniverse OpenInterest(long min, long max) - { - ValidateSecurityTypeForSupportedFilters(nameof(OpenInterest)); - return InRange(GetOpenInterest, min, max); - } - /// /// Applies the filter to the universe selecting the contracts with open interest between the given range. - /// Alias for + /// Not supported for future options /// /// The minimum open interest value /// The maximum open interest value /// Universe with filter applied - public TUniverse OI(long min, long max) + public override TUniverse OpenInterest(long min, long max) { - return OpenInterest(min, max); + ValidateSecurityTypeForSupportedFilters(nameof(OpenInterest)); + return base.OpenInterest(min, max); } private TUniverse Ladder(OptionRight right, int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal middleStrikeFromAtm, decimal lowerStrikeFromAtm) @@ -1219,18 +1194,6 @@ 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; - })); - } - /// /// Gets the underlying price in strike units, false when the underlying is unknown /// @@ -1404,6 +1367,11 @@ protected override OptionUniverse CreateDataInstance(Symbol symbol) /// protected override decimal GetOpenInterest(OptionUniverse contract) => contract.OpenInterest; + /// + /// Gets the volume of the given contract + /// + protected override decimal GetVolume(OptionUniverse contract) => contract.Volume; + /// /// Implicitly convert the universe to a list of symbols /// diff --git a/Tests/Common/Data/Market/FuturesChainTests.cs b/Tests/Common/Data/Market/FuturesChainTests.cs new file mode 100644 index 000000000000..f79d5c580809 --- /dev/null +++ b/Tests/Common/Data/Market/FuturesChainTests.cs @@ -0,0 +1,285 @@ +/* + * 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; + +namespace QuantConnect.Tests.Common.Data.Market +{ + [TestFixture] + public class FuturesChainTests + { + private static readonly DateTime Date = new(2013, 10, 7); + private static readonly Symbol Canonical = Symbol.Create("ES", SecurityType.Future, QuantConnect.Market.CME); + // The quarterly ES contracts listed in the sample data, with their standard expiration dates + private static readonly DateTime[] Expiries = + { + new(2013, 12, 20), new(2014, 3, 21), new(2014, 6, 20), new(2014, 9, 19), new(2014, 12, 19) + }; + private static readonly decimal[] Volumes = { 5000m, 4000m, 3000m, 2000m, 1000m }; + private static readonly decimal[] OpenInterests = { 900000m, 60000m, 9000m, 2000m, 500m }; + + private List _data; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + _data = CreateUniverseData(Date, Expiries, Volumes, OpenInterests); + } + + private static IEnumerable FilterCases() + { + yield return Case("Expiration(0, 90)", u => u.Expiration(0, 90), c => c.Expiration(0, 90)); + yield return Case("Expiration(TimeSpan)", u => u.Expiration(TimeSpan.FromDays(60), TimeSpan.FromDays(300)), + c => c.Expiration(TimeSpan.FromDays(60), TimeSpan.FromDays(300))); + yield return Case("Expiration(500, 600)", u => u.Expiration(500, 600), c => c.Expiration(500, 600), empty: true); + yield return Case("Expiration(dates)", u => u.Expiration([Expiries[1], Expiries[3].AddHours(10)]), c => c.Expiration([Expiries[1], Expiries[3].AddHours(10)])); + yield return Case("Expiration(no dates)", u => u.Expiration([]), c => c.Expiration([]), empty: true); + yield return Case("ExpiringAfter", u => u.ExpiringAfter(Expiries[0]), c => c.ExpiringAfter(Expiries[0])); + yield return Case("ExpiringBefore", u => u.ExpiringBefore(Expiries[2]), c => c.ExpiringBefore(Expiries[2])); + yield return Case("ExpiringAfter.ExpiringBefore", u => u.ExpiringAfter(Expiries[0]).ExpiringBefore(Expiries[3]), c => c.ExpiringAfter(Expiries[0]).ExpiringBefore(Expiries[3])); + yield return Case("ZeroDte", u => u.ZeroDte(), c => c.ZeroDte(), empty: true); + yield return Case("StandardsOnly", u => u.StandardsOnly(), c => c.StandardsOnly()); + yield return Case("WeeklysOnly", u => u.WeeklysOnly(), c => c.WeeklysOnly(), empty: true); + yield return Case("FrontMonth", u => u.FrontMonth(), c => c.FrontMonth()); + yield return Case("BackMonths", u => u.BackMonths(), c => c.BackMonths()); + yield return Case("BackMonth", u => u.BackMonth(), c => c.BackMonth()); + yield return Case("FarthestExpiration", u => u.FarthestExpiration(), c => c.FarthestExpiration()); + yield return Case("StandardsOnly.FrontMonth", u => u.StandardsOnly().FrontMonth(), c => c.StandardsOnly().FrontMonth()); + yield return Case("ExpirationCycle(3, 9)", u => u.ExpirationCycle([3, 9]), c => c.ExpirationCycle([3, 9])); + yield return Case("ExpirationCycle(1)", u => u.ExpirationCycle([1]), c => c.ExpirationCycle([1]), empty: true); + yield return Case("ContractMonths(3, 12)", u => u.ContractMonths([3, 12]), c => c.ContractMonths([3, 12])); + yield return Case("ContractMonths(1)", u => u.ContractMonths([1]), c => c.ContractMonths([1]), empty: true); + yield return Case("OpenInterest(1000, 100000)", u => u.OpenInterest(1000, 100000), c => c.OpenInterest(1000, 100000)); + yield return Case("OI(0, 600)", u => u.OI(0, 600), c => c.OI(0, 600)); + yield return Case("OpenInterest(0, 0)", u => u.OpenInterest(0, 0), c => c.OpenInterest(0, 0), empty: true); + yield return Case("Volume(2000, 4000)", u => u.Volume(2000, 4000), c => c.Volume(2000, 4000)); + yield return Case("Volume.FrontMonth", u => u.Volume(1, long.MaxValue).FrontMonth(), c => c.Volume(1, long.MaxValue).FrontMonth()); + yield return Case("Expiration.BackMonths.ExpirationCycle", u => u.Expiration(0, 300).BackMonths().ExpirationCycle([3, 6]), + c => c.Expiration(0, 300).BackMonths().ExpirationCycle([3, 6])); + } + + [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(FutureFilterUniverse) + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(x => x.ReturnType == typeof(FutureFilterUniverse) && x.Name != "Contracts" && !x.IsDefined(typeof(ObsoleteAttribute))) + .ToList(); + + Assert.IsNotEmpty(universeFilters); + var interfaces = typeof(IFutureContractFilters).GetInterfaces().Prepend(typeof(IFutureContractFilters)).ToList(); + foreach (var universeFilter in universeFilters) + { + var parameters = universeFilter.GetParameters().Select(x => x.ParameterType).ToArray(); + var chainFilter = interfaces.Select(x => x.GetMethod(universeFilter.Name, parameters)).FirstOrDefault(x => x != null); + Assert.IsNotNull(chainFilter, $"{universeFilter.Name}({string.Join(", ", parameters.Select(x => x.Name))}) is not a futures chain filter"); + } + } + + [Test] + public void FiltersLeaveTheSourceChainUntouched() + { + var chain = CreateChain(); + var filtered = chain.FrontMonth(); + + Assert.AreEqual(Expiries.Length, chain.Count); + Assert.AreEqual(1, filtered.Count); + Assert.AreEqual(chain.Symbol, filtered.Symbol); + Assert.AreEqual(chain.Time, filtered.Time); + Assert.AreSame(chain.Contracts[filtered.Single().Symbol], filtered.Single()); + } + + [Test] + public void FiltersOnAnEmptyChainReturnAnEmptyChain() + { + var chain = new FuturesChain(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 = Expiries[0]; + var data = CreateUniverseData(exchangeDate, [exchangeDate, Expiries[1]], Volumes.Take(2).ToArray(), OpenInterests.Take(2).ToArray()); + var chain = new FuturesChain(Canonical, exchangeDate, data) { Time = exchangeDate.AddDays(1) }; + var universe = new FutureFilterUniverse(data, exchangeDate).Expiration(0, 0).ToList(); + + Assert.AreEqual(1, 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); + Assert.AreEqual(1, chain.ZeroDte().Count); + } + + [Test] + public void TypeFiltersApplyToTheChainContractsInAnyOrder() + { + // A contract expiring off its standard date, 2014-01-17 for January, is a non standard one. It cannot go through the + // universe file format, which only carries the contract month, so the rows are built directly + var weekly = new FutureUniverse { Symbol = Symbol.CreateFuture("ES", QuantConnect.Market.CME, new DateTime(2014, 1, 10)), Time = Date }; + var data = _data.Concat([weekly]).ToList(); + var chain = new FuturesChain(Canonical, Date, data); + var universe = new FutureFilterUniverse(data, Date); + + Assert.AreEqual(Expiries.Length + 1, chain.Count); + CollectionAssert.AreEquivalent(_data.Select(x => x.Symbol.Value), chain.StandardsOnly().Select(x => x.Symbol.Value)); + CollectionAssert.AreEqual(new[] { weekly.Symbol.Value }, chain.WeeklysOnly().Select(x => x.Symbol.Value)); + CollectionAssert.AreEquivalent(universe.StandardsOnly().FrontMonth().ToList().Select(x => x.Symbol.Value), chain.StandardsOnly().FrontMonth().Select(x => x.Symbol.Value)); + + // The front month, December 2013, is standard; the chain composes the type filters in either order, the universe does not + Assert.AreEqual(0, chain.FrontMonth().WeeklysOnly().Count); + Assert.AreEqual(weekly.Symbol, chain.WeeklysOnly().FrontMonth().Single().Symbol); + Assert.Throws(() => new FutureFilterUniverse(data, Date).FrontMonth().StandardsOnly()); + } + + [Test] + public void FiltersAreAvailableFromPython() + { + var chain = CreateChain(); + var expectedFiltered = chain.Expiration(0, 300).BackMonths().ExpirationCycle([3, 6]).Select(x => x.Symbol).ToList(); + var expectedSets = chain.Expiration([Expiries[1], Expiries[3]]).OpenInterest(1000, 100000).Select(x => x.Symbol).ToList(); + var expectedWhere = chain.Where(x => x.Volume >= 3000).Select(x => x.Symbol).ToList(); + var expectedContractMonths = chain.ContractMonths([3, 12]).ExpiringAfter(Expiries[0]).Select(x => x.Symbol).ToList(); + Assert.AreEqual(2, expectedFiltered.Count); + Assert.AreEqual(2, expectedSets.Count); + Assert.AreEqual(3, expectedWhere.Count); + Assert.AreEqual(2, expectedContractMonths.Count); + + using (Py.GIL()) + { + using var module = PyModule.FromString(nameof(FuturesChainTests), @" +from AlgorithmImports import * + +def filter_chain(chain): + return chain.expiration(0, 300).back_months().expiration_cycle([3, 6]) + +def sets(chain): + return chain.expiration([datetime(2014, 3, 21), datetime(2014, 9, 19)]).open_interest(1000, 100000) + +def where_chain(chain): + return chain.where(lambda contract: contract.volume >= 3000) + +def contract_months(chain): + return chain.contract_months([3, 12]).expiring_after(datetime(2013, 12, 20)) +"); + 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 sets = module.GetAttr("sets").Invoke(pyChain); + CollectionAssert.AreEqual(expectedSets, sets.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()); + + using var contractMonths = module.GetAttr("contract_months").Invoke(pyChain); + CollectionAssert.AreEqual(expectedContractMonths, contractMonths.As().Select(x => x.Symbol).ToList()); + } + } + + private static TestCaseData Case(string name, Func universeFilter, + Func chainFilter, bool empty = false) + { + return new TestCaseData(universeFilter, chainFilter, empty).SetName("{m}(" + name + ")"); + } + + private FutureFilterUniverse CreateUniverse() + { + return new FutureFilterUniverse(_data, Date); + } + + private FuturesChain CreateChain() + { + return new FuturesChain(Canonical, Date, _data); + } + + /// + /// Creates futures 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 + /// + private static List CreateUniverseData(DateTime date, DateTime[] expiries, decimal[] volumes, decimal[] openInterests) + { + var symbols = expiries.Select(expiry => Symbol.CreateFuture("ES", QuantConnect.Market.CME, expiry)).ToList(); + var csv = new StringBuilder(); + csv.AppendLine("#" + FutureUniverse.CsvHeader); + for (var i = 0; i < symbols.Count; i++) + { + var price = 1600 + i; + csv.AppendLine(FutureUniverse.ToCsv(symbols[i], price, price, price, price, volumes[i], openInterests[i])); + } + + var config = new SubscriptionDataConfig(typeof(FutureUniverse), Canonical, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, true, true, false); + var data = new List(); + var factory = new FutureUniverse(); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(csv.ToString())); + using var reader = new StreamReader(stream); + while (!reader.EndOfStream) + { + var line = (FutureUniverse)factory.Reader(config, reader, date, false); + if (line != null) + { + data.Add(line); + } + } + + // the file format is the data generator's, so the rows must come back as written + Assert.AreEqual(symbols.Count, data.Count); + for (var i = 0; i < symbols.Count; i++) + { + Assert.AreEqual(symbols[i], data[i].Symbol); + Assert.AreEqual(volumes[i], data[i].Volume); + Assert.AreEqual(openInterests[i], data[i].OpenInterest); + } + return data; + } + } +} diff --git a/Tests/Common/Data/Market/OptionChainTests.cs b/Tests/Common/Data/Market/OptionChainTests.cs index a2456cc7bab2..a9242e17b0f0 100644 --- a/Tests/Common/Data/Market/OptionChainTests.cs +++ b/Tests/Common/Data/Market/OptionChainTests.cs @@ -79,6 +79,8 @@ private static IEnumerable FilterCases() yield return Case("StrikesAbove.StrikesBelow", u => u.StrikesAbove(95m).StrikesBelow(105m), c => c.StrikesAbove(95m).StrikesBelow(105m)); yield return Case("StrikesAbove(max)", u => u.StrikesAbove(110m), c => c.StrikesAbove(110m), empty: true); yield return Case("ZeroDte", u => u.ZeroDte(), c => c.ZeroDte(), empty: true); + yield return Case("Volume(5, 20)", u => u.Volume(5, 20), c => c.Volume(5, 20)); + yield return Case("Volume(5, 20).OpenInterest(100, 1500)", u => u.Volume(5, 20).OpenInterest(100, 1500), c => c.Volume(5, 20).OpenInterest(100, 1500)); yield return Case("CallsOnly", u => u.CallsOnly(), c => c.CallsOnly()); yield return Case("PutsOnly", u => u.PutsOnly(), c => c.PutsOnly()); yield return Case("OutOfTheMoney", u => u.OutOfTheMoney(), c => c.OutOfTheMoney()); @@ -165,7 +167,10 @@ public void ChainExposesEveryUniverseFilter() foreach (var universeFilter in universeFilters) { var parameters = universeFilter.GetParameters().Select(x => x.ParameterType).ToArray(); - var chainFilter = typeof(IOptionContractFilters).GetMethod(universeFilter.Name, parameters); + // the shared contract filters are declared on the base interface + var chainFilter = typeof(IOptionContractFilters).GetInterfaces().Prepend(typeof(IOptionContractFilters)) + .Select(x => x.GetMethod(universeFilter.Name, parameters)) + .FirstOrDefault(x => x != null); Assert.IsNotNull(chainFilter, $"{universeFilter.Name}({string.Join(", ", parameters.Select(x => x.Name))}) is not an option chain filter"); } } @@ -425,6 +430,10 @@ public void FiltersWorkOnFutureOptionChains() // the expiration filters count from the CME date, every ES option is a standard contract Assert.AreEqual(10, chain.Expiration(70, 80).Count); Assert.AreEqual(0, chain.ZeroDte().Count); + // a chain dated on a Saturday counts from the next CME session, 75 days to the expiry instead of 76 + var saturdayChain = new OptionChain(canonical, new DateTime(2020, 1, 4), data, symbolProperties); + Assert.AreEqual(10, saturdayChain.Expiration(0, 75).Count); + Assert.AreEqual(0, saturdayChain.Expiration(76, 80).Count); Assert.AreEqual(10, chain.StandardsOnly().FarthestExpiration().Count); Assert.AreEqual(0, chain.WeeklysOnly().Count); Assert.IsTrue(chain.All(x => x.DaysToExpiry == (future.ID.Date - x.Time.Date).Days)); diff --git a/Tests/Common/Securities/FutureFilterTests.cs b/Tests/Common/Securities/FutureFilterTests.cs index c99747347aef..409b1ca438f6 100644 --- a/Tests/Common/Securities/FutureFilterTests.cs +++ b/Tests/Common/Securities/FutureFilterTests.cs @@ -377,6 +377,30 @@ public void FiltersExpirationCycles() Assert.AreEqual(5, filtered.Count); } + [Test] + public void FiltersContractMonths() + { + var time = new DateTime(2013, 10, 7); + // ES contracts are named after their expiration month, crude oil contracts after the month following it + var data = new List + { + new() { Symbol = Symbol.CreateFuture("ES", Market.CME, new DateTime(2013, 12, 20)) }, + new() { Symbol = Symbol.CreateFuture("ES", Market.CME, new DateTime(2014, 3, 21)) }, + new() { Symbol = Symbol.CreateFuture("CL", Market.NYMEX, new DateTime(2013, 11, 20)) }, + new() { Symbol = Symbol.CreateFuture("CL", Market.NYMEX, new DateTime(2014, 2, 20)) }, + new() { Symbol = Symbol.CreateFuture("CL", Market.NYMEX, new DateTime(2014, 5, 20)) } + }; + FutureFilterUniverse Universe() => new(data, time); + static IEnumerable Symbols(FutureFilterUniverse universe) => universe.AsEnumerable().Select(x => x.Symbol); + + CollectionAssert.AreEqual(new[] { data[0].Symbol, data[2].Symbol }, Symbols(Universe().ContractMonths([12]))); + CollectionAssert.AreEqual(new[] { data[1].Symbol, data[3].Symbol, data[4].Symbol }, Symbols(Universe().ContractMonths([3, 6]))); + CollectionAssert.AreEqual(new[] { data[0].Symbol, data[1].Symbol }, Symbols(Universe().ExpirationCycle(FutureExpirationCycles.March))); + CollectionAssert.AreEqual(data.Select(x => x.Symbol), Symbols(Universe().ContractMonths(FutureExpirationCycles.March))); + Assert.AreEqual(0, Universe().ContractMonths([2]).Count); + Assert.AreEqual(0, Universe().ContractMonths([]).Count); + } + [Test] public void FiltersExpirationSetsBoundsAndFarthestExpiration() { @@ -400,6 +424,24 @@ public void FiltersExpirationSetsBoundsAndFarthestExpiration() Assert.AreEqual(0, new FutureFilterUniverse(new List(), time).FarthestExpiration().Count); } + [Test] + public void FiltersOpenInterestVolumeAndZeroDte() + { + var time = new DateTime(2013, 10, 7); + var expiries = new[] { time, new DateTime(2013, 12, 20), new DateTime(2014, 3, 21), new DateTime(2014, 6, 20) }; + // the universe rows carry open, high, low, close, volume and open interest + var data = expiries.Select((expiry, i) => new FutureUniverse(time, Symbol.CreateFuture("ES", Market.CME, expiry), $"1,1,1,1,{1000 * (i + 1)},{10 * (i + 1)}")).ToList(); + FutureFilterUniverse Universe() => new(data, time); + static IEnumerable Expiries(FutureFilterUniverse universe) => universe.Select(x => x.Symbol.ID.Date); + + CollectionAssert.AreEqual(new[] { expiries[0] }, Expiries(Universe().ZeroDte())); + CollectionAssert.AreEqual(new[] { expiries[1], expiries[2] }, Expiries(Universe().OpenInterest(20, 30))); + CollectionAssert.AreEqual(new[] { expiries[3] }, Expiries(Universe().OI(31, long.MaxValue))); + CollectionAssert.AreEqual(new[] { expiries[0], expiries[1] }, Expiries(Universe().Volume(0, 2000))); + CollectionAssert.AreEqual(new[] { expiries[2] }, Expiries(Universe().Volume(2500, 3500).OpenInterest(0, 100))); + Assert.AreEqual(0, Universe().Volume(5000, 6000).Count); + } + [Test] public void FilterTypeDoesNotBreakOnMissingExpiryFunction() {