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