From 86ded1e533eca02ac0511c7228aab5de2fd60ae7 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 14 Sep 2026 10:38:58 -0400 Subject: [PATCH 1/2] Add contract filters to futures chains and universes Split the contract filters shared by every derivative into IContractFilters, with IFutureContractFilters and IOptionContractFilters on top, and move the open interest and volume filters into ContractSecurityFilterUniverse so the futures universe gets them. Add BaseFutureFilterUniverse over the universe rows and the chain contracts, FuturesChainFilterUniverse, and FuturesChain filters that forward to it. The chain wrappers shared by OptionChain and FuturesChain live once in a BaseChain layer with the chain type and its filter universe as parameters. New futures filters: ContractMonth(year, month) and ContractMonths(months), by the contract month rather than the expiration month. FutureSymbol.IsStandard and FutureOptionSymbol.IsStandard back the standard and weekly filters. Tests: FuturesChainTests (parity with the universe, every universe filter declared on the interface, Python), FutureFilterTests, OptionChainTests, and the FuturesChainFiltersRegressionAlgorithm in C# and Python. The futures templates pick the front contract with the new filters. --- .../BasicTemplateFuturesAlgorithm.cs | 6 +- .../FuturesChainFiltersRegressionAlgorithm.cs | 210 +++++++++++++ .../BasicTemplateFuturesAlgorithm.py | 9 +- .../FuturesChainFiltersRegressionAlgorithm.py | 107 +++++++ Common/Data/Market/BaseChain.Filters.cs | 267 ++++++++++++++++ Common/Data/Market/FuturesChain.Filters.cs | 81 +++++ Common/Data/Market/FuturesChain.cs | 17 +- Common/Data/Market/OptionChain.Filters.cs | 183 +---------- Common/Data/Market/OptionChain.cs | 2 +- .../ContractSecurityFilterUniverse.cs | 70 ++++- .../Securities/Future/FutureFilterUniverse.cs | 81 ++++- .../Future/FuturesChainFilterUniverse.cs | 62 ++++ .../Future/IFutureContractFilters.cs | 45 +++ Common/Securities/IContractFilters.cs | 103 +++++++ .../Option/IOptionContractFilters.cs | 76 +---- .../Option/OptionChainFilterUniverse.cs | 9 +- .../Securities/Option/OptionFilterUniverse.cs | 50 +-- Tests/Common/Data/Market/FuturesChainTests.cs | 287 ++++++++++++++++++ Tests/Common/Data/Market/OptionChainTests.cs | 11 +- Tests/Common/Securities/FutureFilterTests.cs | 44 +++ 20 files changed, 1407 insertions(+), 313 deletions(-) create mode 100644 Algorithm.CSharp/FuturesChainFiltersRegressionAlgorithm.cs create mode 100644 Algorithm.Python/FuturesChainFiltersRegressionAlgorithm.py create mode 100644 Common/Data/Market/BaseChain.Filters.cs create mode 100644 Common/Data/Market/FuturesChain.Filters.cs create mode 100644 Common/Securities/Future/FuturesChainFilterUniverse.cs create mode 100644 Common/Securities/Future/IFutureContractFilters.cs create mode 100644 Common/Securities/IContractFilters.cs create mode 100644 Tests/Common/Data/Market/FuturesChainTests.cs 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..7aadf0332117 --- /dev/null +++ b/Algorithm.CSharp/FuturesChainFiltersRegressionAlgorithm.cs @@ -0,0 +1,210 @@ +/* + * 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.ContractMonth(2014, 6), "ContractMonth(2014, 6)", (2014, 6)); + 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..ed4b700be9e1 --- /dev/null +++ b/Algorithm.Python/FuturesChainFiltersRegressionAlgorithm.py @@ -0,0 +1,107 @@ +# 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_month(2014, 6), "contract_month(2014, 6)", [(2014, 6)]) + 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..7061db3998f3 --- /dev/null +++ b/Common/Data/Market/FuturesChain.Filters.cs @@ -0,0 +1,81 @@ +/* + * 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(int[] months) + { + return Filter(universe => universe.ExpirationCycle(months)); + } + + /// + /// Selects the contracts for the given 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 + /// + /// The contract year + /// The contract month, 1 to 12 + /// A new chain with the filter applied + public FuturesChain ContractMonth(int year, int month) + { + return Filter(universe => universe.ContractMonth(year, month)); + } + + /// + /// Selects the contracts whose contract month is any of the given months of the year, like + /// but by the contract month instead of 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..768b14f0904b 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,56 @@ 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(int[] months) + { + var monthHashSet = months.ToHashSet(); + return Contracts(contracts => contracts.Where(x => monthHashSet.Contains(x.Symbol.ID.Date.Month))); + } + + /// + /// Selects the contracts for the given contract month, the month the contract is named after. For some products, e.g. crude oil, + /// that is the month after the expiration month, see + /// + /// The contract year + /// The contract month, 1 to 12 + /// Universe with filter applied + public TUniverse ContractMonth(int year, int month) + { + var contractMonth = new DateTime(year, month, 1); + return Contracts(contracts => contracts.Where(x => FuturesExpiryUtilityFunctions.GetFutureContractMonth(x.Symbol) == contractMonth)); + } + + /// + /// Selects the contracts whose contract month is any of the given months of the year, see . + /// Like but by the contract month, which for some products, e.g. crude oil, is the month after the expiration month + /// + /// 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 +117,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..1d84c2a4e6d8 --- /dev/null +++ b/Common/Securities/Future/IFutureContractFilters.cs @@ -0,0 +1,45 @@ +/* + * 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(int[] months); + + /// + /// Selects the contracts for the given contract month, the month the contract is named after, which for some products, + /// e.g. crude oil, is the month after the expiration month + /// + TSelf ContractMonth(int year, int month); + + /// + /// 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..42e8c34c38e3 --- /dev/null +++ b/Tests/Common/Data/Market/FuturesChainTests.cs @@ -0,0 +1,287 @@ +/* + * 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("ContractMonth(2014, 6)", u => u.ContractMonth(2014, 6), c => c.ContractMonth(2014, 6)); + yield return Case("ContractMonth(2015, 3)", u => u.ContractMonth(2015, 3), c => c.ContractMonth(2015, 3), 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]).ContractMonth(2014, 12).Select(x => x.Symbol).ToList(); + Assert.AreEqual(2, expectedFiltered.Count); + Assert.AreEqual(2, expectedSets.Count); + Assert.AreEqual(3, expectedWhere.Count); + Assert.AreEqual(1, 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]).contract_month(2014, 12) +"); + 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..ab315f318872 100644 --- a/Tests/Common/Securities/FutureFilterTests.cs +++ b/Tests/Common/Securities/FutureFilterTests.cs @@ -377,6 +377,32 @@ 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().ContractMonth(2013, 12))); + CollectionAssert.AreEqual(new[] { data[1].Symbol, data[3].Symbol }, Symbols(Universe().ContractMonth(2014, 3))); + Assert.AreEqual(0, Universe().ContractMonth(2014, 2).Count); + 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([]).Count); + Assert.Throws(() => Universe().ContractMonth(2014, 13)); + } + [Test] public void FiltersExpirationSetsBoundsAndFarthestExpiration() { @@ -400,6 +426,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() { From 8987b298dae2d02896daae17510fe2d5fab16a11 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 14 Sep 2026 18:02:23 -0400 Subject: [PATCH 2/2] Drop the single contract month filter and take any months sequence in ExpirationCycle --- .../FuturesChainFiltersRegressionAlgorithm.cs | 1 - .../FuturesChainFiltersRegressionAlgorithm.py | 1 - Common/Data/Market/FuturesChain.Filters.cs | 18 +++--------------- .../Securities/Future/FutureFilterUniverse.cs | 18 +++--------------- .../Future/IFutureContractFilters.cs | 8 +------- Tests/Common/Data/Market/FuturesChainTests.cs | 8 +++----- Tests/Common/Securities/FutureFilterTests.cs | 6 ++---- 7 files changed, 12 insertions(+), 48 deletions(-) diff --git a/Algorithm.CSharp/FuturesChainFiltersRegressionAlgorithm.cs b/Algorithm.CSharp/FuturesChainFiltersRegressionAlgorithm.cs index 7aadf0332117..4d73ba7a56a9 100644 --- a/Algorithm.CSharp/FuturesChainFiltersRegressionAlgorithm.cs +++ b/Algorithm.CSharp/FuturesChainFiltersRegressionAlgorithm.cs @@ -65,7 +65,6 @@ public override void Initialize() 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.ContractMonth(2014, 6), "ContractMonth(2014, 6)", (2014, 6)); 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)); diff --git a/Algorithm.Python/FuturesChainFiltersRegressionAlgorithm.py b/Algorithm.Python/FuturesChainFiltersRegressionAlgorithm.py index ed4b700be9e1..98efb59501f5 100644 --- a/Algorithm.Python/FuturesChainFiltersRegressionAlgorithm.py +++ b/Algorithm.Python/FuturesChainFiltersRegressionAlgorithm.py @@ -50,7 +50,6 @@ def initialize(self): 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_month(2014, 6), "contract_month(2014, 6)", [(2014, 6)]) 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)]) diff --git a/Common/Data/Market/FuturesChain.Filters.cs b/Common/Data/Market/FuturesChain.Filters.cs index 7061db3998f3..fe495e50904b 100644 --- a/Common/Data/Market/FuturesChain.Filters.cs +++ b/Common/Data/Market/FuturesChain.Filters.cs @@ -31,27 +31,15 @@ public partial class FuturesChain /// /// Months to select contracts from, see /// A new chain with the filter applied - public FuturesChain ExpirationCycle(int[] months) + public FuturesChain ExpirationCycle(IEnumerable months) { return Filter(universe => universe.ExpirationCycle(months)); } - /// - /// Selects the contracts for the given 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 - /// - /// The contract year - /// The contract month, 1 to 12 - /// A new chain with the filter applied - public FuturesChain ContractMonth(int year, int month) - { - return Filter(universe => universe.ContractMonth(year, month)); - } - /// /// Selects the contracts whose contract month is any of the given months of the year, like - /// but by the contract month instead of the expiration month. + /// 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 diff --git a/Common/Securities/Future/FutureFilterUniverse.cs b/Common/Securities/Future/FutureFilterUniverse.cs index 768b14f0904b..67c5fd5bdb74 100644 --- a/Common/Securities/Future/FutureFilterUniverse.cs +++ b/Common/Securities/Future/FutureFilterUniverse.cs @@ -58,28 +58,16 @@ protected override bool IsStandard(Symbol symbol) /// /// Months to select contracts from /// Universe with filter applied - public TUniverse ExpirationCycle(int[] months) + 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 for the given contract month, the month the contract is named after. For some products, e.g. crude oil, - /// that is the month after the expiration month, see - /// - /// The contract year - /// The contract month, 1 to 12 - /// Universe with filter applied - public TUniverse ContractMonth(int year, int month) - { - var contractMonth = new DateTime(year, month, 1); - return Contracts(contracts => contracts.Where(x => FuturesExpiryUtilityFunctions.GetFutureContractMonth(x.Symbol) == contractMonth)); - } - /// /// Selects the contracts whose contract month is any of the given months of the year, see . - /// Like but by the contract month, which for some products, e.g. crude oil, is the month after the expiration month + /// 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 diff --git a/Common/Securities/Future/IFutureContractFilters.cs b/Common/Securities/Future/IFutureContractFilters.cs index 1d84c2a4e6d8..ec33bcc59f09 100644 --- a/Common/Securities/Future/IFutureContractFilters.cs +++ b/Common/Securities/Future/IFutureContractFilters.cs @@ -28,13 +28,7 @@ public interface IFutureContractFilters : IContractFilters /// /// Selects the contracts expiring in any of the given months of the year, see /// - TSelf ExpirationCycle(int[] months); - - /// - /// Selects the contracts for the given contract month, the month the contract is named after, which for some products, - /// e.g. crude oil, is the month after the expiration month - /// - TSelf ContractMonth(int year, int month); + TSelf ExpirationCycle(IEnumerable months); /// /// Selects the contracts whose contract month is any of the given months of the year, see . diff --git a/Tests/Common/Data/Market/FuturesChainTests.cs b/Tests/Common/Data/Market/FuturesChainTests.cs index 42e8c34c38e3..f79d5c580809 100644 --- a/Tests/Common/Data/Market/FuturesChainTests.cs +++ b/Tests/Common/Data/Market/FuturesChainTests.cs @@ -70,8 +70,6 @@ private static IEnumerable FilterCases() 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("ContractMonth(2014, 6)", u => u.ContractMonth(2014, 6), c => c.ContractMonth(2014, 6)); - yield return Case("ContractMonth(2015, 3)", u => u.ContractMonth(2015, 3), c => c.ContractMonth(2015, 3), 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)); @@ -188,11 +186,11 @@ public void FiltersAreAvailableFromPython() 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]).ContractMonth(2014, 12).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(1, expectedContractMonths.Count); + Assert.AreEqual(2, expectedContractMonths.Count); using (Py.GIL()) { @@ -209,7 +207,7 @@ def where_chain(chain): return chain.where(lambda contract: contract.volume >= 3000) def contract_months(chain): - return chain.contract_months([3, 12]).contract_month(2014, 12) + return chain.contract_months([3, 12]).expiring_after(datetime(2013, 12, 20)) "); using var pyChain = chain.ToPython(); diff --git a/Tests/Common/Securities/FutureFilterTests.cs b/Tests/Common/Securities/FutureFilterTests.cs index ab315f318872..409b1ca438f6 100644 --- a/Tests/Common/Securities/FutureFilterTests.cs +++ b/Tests/Common/Securities/FutureFilterTests.cs @@ -393,14 +393,12 @@ public void FiltersContractMonths() 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().ContractMonth(2013, 12))); - CollectionAssert.AreEqual(new[] { data[1].Symbol, data[3].Symbol }, Symbols(Universe().ContractMonth(2014, 3))); - Assert.AreEqual(0, Universe().ContractMonth(2014, 2).Count); + 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); - Assert.Throws(() => Universe().ContractMonth(2014, 13)); } [Test]