diff --git a/Algorithm.CSharp/FutureOptionChainFiltersRegressionAlgorithm.cs b/Algorithm.CSharp/FutureOptionChainFiltersRegressionAlgorithm.cs
new file mode 100644
index 000000000000..fa197fdf129e
--- /dev/null
+++ b/Algorithm.CSharp/FutureOptionChainFiltersRegressionAlgorithm.cs
@@ -0,0 +1,196 @@
+/*
+ * 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 strike, expiration and moneyness filters on future options: in the universe selection
+ /// of the future and of its options, on the chains of the and on
+ ///
+ public class FutureOptionChainFiltersRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
+ {
+ private static readonly DateTime MarchExpiry = new(2020, 3, 20);
+ private static readonly decimal[] SelectedStrikes = [3200m, 3210m, 3220m, 3230m, 3240m, 3250m];
+
+ private Symbol _es;
+ private bool _chainSeen;
+ private bool _traded;
+
+ public override void Initialize()
+ {
+ SetStartDate(2020, 1, 5);
+ SetEndDate(2020, 1, 6);
+ SetCash(1000000);
+
+ // The March 2020 future, by its expiration date
+ var es = AddFuture(Futures.Indices.SP500EMini, Resolution.Minute, Market.CME);
+ es.SetFilter(universe => universe.Expiration([MarchExpiry]));
+ _es = es.Symbol;
+
+ // Its options: the out of the money contracts within three strikes of the future price
+ AddFutureOption(_es, universe => universe.Strikes(-3, 3).OutOfTheMoney());
+
+ // The option chain of the March future from the universe data: one expiration, the future at 3223.75
+ var chain = OptionChain(QuantConnect.Symbol.CreateFuture(Futures.Indices.SP500EMini, Market.CME, MarchExpiry));
+ if (chain.Count == 0 || chain.Underlying.Price != 3223.75m || chain.Symbol.SecurityType != SecurityType.FutureOption)
+ {
+ throw new RegressionTestException($"Expected the March ES option chain at 3223.75 but got {chain.Count} contracts at {chain.Underlying.Price}");
+ }
+ // Strikes are 10 points apart around the money: three each side of 3223.75 are 3200 to 3250
+ AssertStrikes(chain.Strikes(-3, 3).OutOfTheMoney().CallsOnly(), "Strikes(-3, 3).OutOfTheMoney().CallsOnly()", 3230m, 3240m, 3250m);
+ AssertStrikes(chain.Strikes(-3, 3).OutOfTheMoney().PutsOnly(), "Strikes(-3, 3).OutOfTheMoney().PutsOnly()", 3200m, 3210m, 3220m);
+ // Only the put is listed at 3310
+ AssertStrikes(chain.StrikesAbove(3300m).StrikesBelow(3320m), "StrikesAbove(3300).StrikesBelow(3320)", 3310m);
+ AssertStrikes(chain.StrikesAbove(3300m).StrikesBelow(3320m).CallsOnly(), "StrikesAbove(3300).StrikesBelow(3320).CallsOnly()");
+ // The strikes on either side of 3223.75 are 3220 and 3230; within 5 points only 3220
+ AssertStrikes(chain.AtTheMoney(), "AtTheMoney()", 3220m, 3220m, 3230m, 3230m);
+ AssertStrikes(chain.AtTheMoney(5m), "AtTheMoney(5)", 3220m, 3220m);
+ if (chain.AtTheMoney(0).Count != 0 || chain.Expiration([MarchExpiry]).Count != chain.Count || chain.FarthestExpiration().Count != chain.Count
+ || chain.ExpiringAfter(MarchExpiry).Count != 0 || chain.ZeroDte().Count != 0
+ || chain.StandardsOnly().Count != chain.Count || chain.WeeklysOnly().Count != 0)
+ {
+ throw new RegressionTestException("Expiration or contract type filters mismatch on the March ES option chain");
+ }
+ }
+
+ public override void OnData(Slice slice)
+ {
+ // One chain per future contract, keyed by its canonical option symbol
+ foreach (var chain in slice.OptionChains.Values)
+ {
+ if (chain.Symbol.Underlying.ID.Date != MarchExpiry)
+ {
+ throw new RegressionTestException($"Unexpected option chain for {chain.Symbol.Underlying}");
+ }
+ _chainSeen = true;
+
+ // The universe selected the out of the money contracts within three strikes of the previous close: 3200 to 3250
+ if (chain.Count == 0 || chain.Strikes(SelectedStrikes).Count != chain.Count || chain.Expiration([MarchExpiry]).Count != chain.Count)
+ {
+ throw new RegressionTestException($"The option chain disagrees with the universe filter: {string.Join(", ", chain.Select(x => x.Symbol.Value))}");
+ }
+
+ // The moneyness filters partition the chain around the current future price, and match the strike bounds for a single right
+ var price = chain.Underlying.Price;
+ var otm = chain.OutOfTheMoney();
+ var itm = chain.InTheMoney();
+ if (otm.Count + itm.Count + chain.Strikes([price]).Count != chain.Count
+ || otm.Any(x => x.Right == OptionRight.Call ? x.Strike <= price : x.Strike >= price)
+ || itm.Any(x => x.Right == OptionRight.Call ? x.Strike >= price : x.Strike <= price)
+ || chain.CallsOnly().OutOfTheMoney().Count != chain.CallsOnly().StrikesAbove(price).Count
+ || chain.PutsOnly().OutOfTheMoney().Count != chain.PutsOnly().StrikesBelow(price).Count)
+ {
+ throw new RegressionTestException($"Moneyness filters mismatch at {price}");
+ }
+
+ // Buy the out of the money call closest to the future price
+ if (!_traded)
+ {
+ var contract = otm.CallsOnly().OrderBy(x => x.Strike).FirstOrDefault();
+ if (contract != null)
+ {
+ MarketOrder(contract.Symbol, 1);
+ _traded = true;
+ }
+ }
+ }
+ }
+
+ public override void OnEndOfAlgorithm()
+ {
+ if (!_chainSeen || !_traded)
+ {
+ throw new RegressionTestException($"Expected the March ES option chain ({_chainSeen}) and a trade ({_traded})");
+ }
+ }
+
+ private static void AssertStrikes(OptionChain chain, string filter, params decimal[] expected)
+ {
+ var actual = chain.Select(x => x.Strike).OrderBy(x => x).ToList();
+ if (!actual.SequenceEqual(expected.OrderBy(x => x)))
+ {
+ throw new RegressionTestException($"{filter}: expected strikes {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 => 7888;
+
+ ///
+ /// Data Points count of the algorithm history
+ ///
+ public int AlgorithmHistoryDataPoints => 1;
+
+ ///
+ /// Final status of the algorithm
+ ///
+ public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
+
+ ///
+ /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
+ ///
+ public Dictionary ExpectedStatistics => new Dictionary
+ {
+ {"Total Orders", "1"},
+ {"Average Win", "0%"},
+ {"Average Loss", "0%"},
+ {"Compounding Annual Return", "0%"},
+ {"Drawdown", "0%"},
+ {"Expectancy", "0"},
+ {"Start Equity", "1000000"},
+ {"End Equity", "1000586.08"},
+ {"Net Profit", "0%"},
+ {"Sharpe Ratio", "0"},
+ {"Sortino Ratio", "0"},
+ {"Probabilistic Sharpe Ratio", "0%"},
+ {"Loss Rate", "0%"},
+ {"Win Rate", "0%"},
+ {"Profit-Loss Ratio", "0"},
+ {"Alpha", "0"},
+ {"Beta", "0"},
+ {"Annual Standard Deviation", "0"},
+ {"Annual Variance", "0"},
+ {"Information Ratio", "0"},
+ {"Tracking Error", "0"},
+ {"Treynor Ratio", "0"},
+ {"Total Fees", "$1.42"},
+ {"Estimated Strategy Capacity", "$6900000.00"},
+ {"Lowest Capacity Asset", "ES XCZJLDR35F50|ES XCZJLC9NOB29"},
+ {"Portfolio Turnover", "0.18%"},
+ {"Drawdown Recovery", "0"},
+ {"OrderListHash", "8786bed30a9a11b79580196098932f23"}
+ };
+ }
+}
diff --git a/Algorithm.CSharp/FutureUniverseFiltersRegressionAlgorithm.cs b/Algorithm.CSharp/FutureUniverseFiltersRegressionAlgorithm.cs
new file mode 100644
index 000000000000..575ace63d42a
--- /dev/null
+++ b/Algorithm.CSharp/FutureUniverseFiltersRegressionAlgorithm.cs
@@ -0,0 +1,161 @@
+/*
+ * 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.Interfaces;
+using QuantConnect.Securities;
+
+namespace QuantConnect.Algorithm.CSharp
+{
+ ///
+ /// Regression algorithm using the expiration set and bound filters in the futures universe selection,
+ /// the same ones the option universes and chains offer, and checking the selected chains in the
+ ///
+ public class FutureUniverseFiltersRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
+ {
+ private static readonly DateTime EndOf2013 = new(2013, 12, 31);
+ private static readonly DateTime EndOfNovember2014 = new(2014, 11, 30);
+
+ 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 2014 contracts up to September
+ var es = AddFuture(Futures.Indices.SP500EMini, Resolution.Minute, Market.CME);
+ es.SetFilter(universe => universe.ExpiringAfter(EndOf2013).ExpiringBefore(EndOfNovember2014));
+ _es = es.Symbol;
+
+ // The contracts expiring this year
+ var gc = AddFuture(Futures.Metals.Gold, Resolution.Minute, Market.COMEX);
+ gc.SetFilter(universe => universe.ExpiringBefore(new DateTime(2014, 1, 1)));
+ _gc = gc.Symbol;
+
+ // The full chain from the universe data lists the December 2013 contract and the March to December 2014 ones
+ var chain = FuturesChain(_es);
+ var expiries = chain.Select(x => x.Expiry).OrderBy(x => x).ToList();
+ if (expiries.Count != 5 || expiries[0] > EndOf2013 || expiries.Skip(1).Any(x => x.Year != 2014))
+ {
+ throw new RegressionTestException($"Unexpected ES chain expiries: {string.Join(", ", expiries)}");
+ }
+ }
+
+ public override void OnData(Slice slice)
+ {
+ if (slice.FuturesChains.TryGetValue(_es, out var esChain))
+ {
+ _esChainSeen = true;
+ // March, June and September 2014
+ if (esChain.Count == 0 || esChain.Count > 3 || esChain.Any(x => x.Expiry <= EndOf2013 || x.Expiry >= EndOfNovember2014))
+ {
+ throw new RegressionTestException($"The ES chain disagrees with the universe filter: {string.Join(", ", esChain.Select(x => x.Expiry))}");
+ }
+ if (!_traded)
+ {
+ MarketOrder(esChain.OrderBy(x => x.Expiry).First().Symbol, 1);
+ _traded = true;
+ }
+ }
+
+ if (slice.FuturesChains.TryGetValue(_gc, out var gcChain))
+ {
+ _gcChainSeen = true;
+ // October, November and December 2013
+ if (gcChain.Count == 0 || gcChain.Count > 3 || gcChain.Any(x => x.Expiry.Year != 2013))
+ {
+ throw new RegressionTestException($"The GC 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})");
+ }
+ }
+
+ ///
+ /// 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 => 38894;
+
+ ///
+ /// 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.CSharp/IndexOptionChainFiltersRegressionAlgorithm.cs b/Algorithm.CSharp/IndexOptionChainFiltersRegressionAlgorithm.cs
new file mode 100644
index 000000000000..cace1d17a085
--- /dev/null
+++ b/Algorithm.CSharp/IndexOptionChainFiltersRegressionAlgorithm.cs
@@ -0,0 +1,235 @@
+/*
+ * 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;
+
+namespace QuantConnect.Algorithm.CSharp
+{
+ ///
+ /// Regression algorithm using the strike, expiration and moneyness filters on index options: in the universe selection
+ /// of standard and weekly contracts, on the chains of the and on
+ ///
+ public class IndexOptionChainFiltersRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
+ {
+ private static readonly DateTime FirstDay = new(2021, 1, 4);
+ private static readonly DateTime StandardExpiry = new(2021, 1, 15);
+
+ private Symbol _spx;
+ private Symbol _spxw;
+ private bool _spxChainSeen;
+ private bool _zeroDteSeen;
+ private bool _traded;
+
+ public override void Initialize()
+ {
+ SetStartDate(2021, 1, 4);
+ SetEndDate(2021, 1, 8);
+ SetCash(1000000);
+
+ // Standard SPX contracts: the out of the money ones with strikes below 4000
+ var spx = AddIndexOption("SPX");
+ spx.SetFilter(universe => universe.OutOfTheMoney().StrikesBelow(4000m));
+ _spx = spx.Symbol;
+
+ // Weekly SPXW contracts: the 3700 strike of the expirations after the first day
+ var spxw = AddIndexOption("SPX", "SPXW");
+ spxw.SetFilter(universe => universe.Strikes([3700m]).ExpiringAfter(FirstDay));
+ _spxw = spxw.Symbol;
+
+ // The latest universe data, from 2020-12-31, lists the 3200, 3700, 3800 and 4250 calls and the 3200 and 4200 puts
+ // expiring on 2021-01-15, with the index at 3766.63: the same filters narrow the chain down
+ var chain = OptionChain(_spx);
+ if (chain.Count != 6 || chain.Underlying.Price != 3766.63m)
+ {
+ throw new RegressionTestException($"Expected the 6 SPX contracts at 3766.63 but got {chain.Count} at {chain.Underlying.Price}");
+ }
+ AssertContracts(chain.OutOfTheMoney(), "OutOfTheMoney()", (3800m, OptionRight.Call), (4250m, OptionRight.Call), (3200m, OptionRight.Put));
+ AssertContracts(chain.InTheMoney(), "InTheMoney()", (3200m, OptionRight.Call), (3700m, OptionRight.Call), (4200m, OptionRight.Put));
+ // The strikes on either side of 3766.63 are 3700 and 3800, both listed as calls only; 50 points reach 3800, 25 none
+ AssertContracts(chain.AtTheMoney(), "AtTheMoney()", (3700m, OptionRight.Call), (3800m, OptionRight.Call));
+ AssertContracts(chain.AtTheMoney(50m), "AtTheMoney(50)", (3800m, OptionRight.Call));
+ AssertContracts(chain.AtTheMoney(25m), "AtTheMoney(25)");
+ AssertContracts(chain.AtTheMoney(0), "AtTheMoney(0)");
+ AssertContracts(chain.StrikesAbove(3700m).StrikesBelow(4250m), "StrikesAbove(3700).StrikesBelow(4250)", (3800m, OptionRight.Call), (4200m, OptionRight.Put));
+ AssertContracts(chain.Strikes([3200m, 4250m]), "Strikes([3200, 4250])", (3200m, OptionRight.Call), (4250m, OptionRight.Call), (3200m, OptionRight.Put));
+ AssertContracts(chain.OutOfTheMoney().StrikesBelow(4000m), "the SPX universe filter", (3800m, OptionRight.Call), (3200m, OptionRight.Put));
+ if (chain.Expiration([StandardExpiry]).Count != chain.Count || chain.FarthestExpiration().Count != chain.Count
+ || chain.ExpiringAfter(StandardExpiry).Count != 0 || chain.ExpiringBefore(StandardExpiry).Count != 0 || chain.ZeroDte().Count != 0)
+ {
+ throw new RegressionTestException("Expected every SPX contract to expire on 2021-01-15");
+ }
+ }
+
+ public override void OnData(Slice slice)
+ {
+ if (slice.OptionChains.TryGetValue(_spx, out var spxChain))
+ {
+ _spxChainSeen = true;
+ // The universe selected the out of the money contracts below 4000: the 3800 call and the 3200 put.
+ // The index stays between those strikes, so the chain filter agrees with the universe filter
+ AssertContracts(spxChain, "the SPX slice chain", (3800m, OptionRight.Call), (3200m, OptionRight.Put));
+ if (spxChain.OutOfTheMoney().Count != spxChain.Count)
+ {
+ throw new RegressionTestException("Expected the SPX slice chain to be out of the money");
+ }
+ AssertMoneyness(spxChain);
+ }
+
+ if (!slice.OptionChains.TryGetValue(_spxw, out var chain))
+ {
+ return;
+ }
+
+ // The universe selected the 3700 strike of the expirations after the first day: 2021-01-06 and 2021-01-08
+ if (chain.Count == 0 || chain.Strikes([3700m]).Count != chain.Count || chain.ExpiringAfter(FirstDay).Count != chain.Count
+ || chain.ExpiringBefore(new DateTime(2021, 1, 9)).Count != chain.Count)
+ {
+ throw new RegressionTestException("The SPXW slice chain disagrees with the universe filter");
+ }
+ AssertMoneyness(chain);
+
+ var zeroDte = chain.ZeroDte();
+ if (zeroDte.Any(x => x.Expiry.Date != Time.Date))
+ {
+ throw new RegressionTestException("ZeroDte() selected contracts not expiring today");
+ }
+ _zeroDteSeen |= zeroDte.Count > 0;
+
+ var farthest = chain.FarthestExpiration();
+ var maxExpiry = chain.Max(x => x.Expiry);
+ if (farthest.Count == 0 || farthest.Any(x => x.Expiry != maxExpiry))
+ {
+ throw new RegressionTestException("FarthestExpiration() mismatch");
+ }
+
+ // Buy the 3700 call of the nearest expiration after today
+ if (!_traded)
+ {
+ var contract = chain.CallsOnly().ExpiringAfter(Time).FrontMonth().FirstOrDefault();
+ if (contract != null)
+ {
+ MarketOrder(contract.Symbol, 1);
+ _traded = true;
+ }
+ }
+ }
+
+ public override void OnEndOfAlgorithm()
+ {
+ if (!_spxChainSeen || !_zeroDteSeen || !_traded)
+ {
+ throw new RegressionTestException($"Expected the SPX chain ({_spxChainSeen}), a 0DTE SPXW contract ({_zeroDteSeen}) and a trade ({_traded})");
+ }
+ }
+
+ ///
+ /// The moneyness filters partition the chain around the current index price, and match the strike bounds for a single right
+ ///
+ private static void AssertMoneyness(OptionChain chain)
+ {
+ var price = chain.Underlying.Price;
+ var otm = chain.OutOfTheMoney();
+ var itm = chain.InTheMoney();
+ if (otm.Count + itm.Count + chain.Strikes([price]).Count != chain.Count
+ || otm.Any(x => x.Right == OptionRight.Call ? x.Strike <= price : x.Strike >= price)
+ || itm.Any(x => x.Right == OptionRight.Call ? x.Strike >= price : x.Strike <= price)
+ || chain.CallsOnly().OutOfTheMoney().Count != chain.CallsOnly().StrikesAbove(price).Count
+ || chain.PutsOnly().OutOfTheMoney().Count != chain.PutsOnly().StrikesBelow(price).Count)
+ {
+ throw new RegressionTestException($"Moneyness filters mismatch at {price}");
+ }
+ }
+
+ private static void AssertContracts(OptionChain chain, string filter, params (decimal strike, OptionRight right)[] expected)
+ {
+ var actual = chain.Select(x => (x.Strike, x.Right)).OrderBy(x => x.Strike).ThenBy(x => x.Right).ToList();
+ var expectedContracts = expected.OrderBy(x => x.strike).ThenBy(x => x.right).ToList();
+ if (!actual.SequenceEqual(expectedContracts))
+ {
+ throw new RegressionTestException($"{filter}: expected {Format(expectedContracts)} but got {Format(actual)}");
+ }
+ }
+
+ private static string Format(IEnumerable<(decimal strike, OptionRight right)> contracts)
+ {
+ return string.Join(", ", contracts.Select(x => $"{x.strike} {x.right}"));
+ }
+
+ ///
+ /// 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 => 25607;
+
+ ///
+ /// Data Points count of the algorithm history
+ ///
+ public int AlgorithmHistoryDataPoints => 1;
+
+ ///
+ /// Final status of the algorithm
+ ///
+ public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
+
+ ///
+ /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
+ ///
+ public Dictionary ExpectedStatistics => new Dictionary
+ {
+ {"Total Orders", "2"},
+ {"Average Win", "0%"},
+ {"Average Loss", "-0.75%"},
+ {"Compounding Annual Return", "-42.123%"},
+ {"Drawdown", "0.800%"},
+ {"Expectancy", "-1"},
+ {"Start Equity", "1000000"},
+ {"End Equity", "992475"},
+ {"Net Profit", "-0.752%"},
+ {"Sharpe Ratio", "-3.457"},
+ {"Sortino Ratio", "0"},
+ {"Probabilistic Sharpe Ratio", "22.012%"},
+ {"Loss Rate", "100%"},
+ {"Win Rate", "0%"},
+ {"Profit-Loss Ratio", "0"},
+ {"Alpha", "1.468"},
+ {"Beta", "-0.369"},
+ {"Annual Standard Deviation", "0.04"},
+ {"Annual Variance", "0.002"},
+ {"Information Ratio", "-38.008"},
+ {"Tracking Error", "0.118"},
+ {"Treynor Ratio", "0.377"},
+ {"Total Fees", "$0.00"},
+ {"Estimated Strategy Capacity", "$940000.00"},
+ {"Lowest Capacity Asset", "SPXW XKZ5O96SL626|SPX 31"},
+ {"Portfolio Turnover", "0.13%"},
+ {"Drawdown Recovery", "2"},
+ {"OrderListHash", "8e3ebdde25785c0e5d3527d7260d2fdc"}
+ };
+ }
+}
diff --git a/Algorithm.CSharp/OptionChainFiltersRegressionAlgorithm.cs b/Algorithm.CSharp/OptionChainFiltersRegressionAlgorithm.cs
index d73971b1fa41..6220b922dc27 100644
--- a/Algorithm.CSharp/OptionChainFiltersRegressionAlgorithm.cs
+++ b/Algorithm.CSharp/OptionChainFiltersRegressionAlgorithm.cs
@@ -20,6 +20,7 @@
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Interfaces;
+using QuantConnect.Securities;
using QuantConnect.Securities.Option;
namespace QuantConnect.Algorithm.CSharp
@@ -43,7 +44,7 @@ public override void Initialize()
var option = AddOption("GOOG");
_option = option.Symbol;
// The same words select the universe and, below, narrow down the chains
- option.SetFilter(universe => universe.CallsOnly().Expiration(1, 10).Strikes(-2, 2));
+ option.SetFilter(universe => universe.CallsOnly().Expiration(1, 10).Strikes(-2, 2).OutOfTheMoney());
var chain = OptionChain(_option);
if (chain.Count == 0)
@@ -88,6 +89,47 @@ public override void Initialize()
{
throw new RegressionTestException("Delta filter mismatch");
}
+
+ // Moneyness filters split the strikes around the underlying price, ATM is the closest strike
+ var price = chain.Underlying.Price;
+ var otm = chain.OutOfTheMoney();
+ var itm = chain.InTheMoney();
+ if (otm.Count == 0 || itm.Count == 0 || otm.Count + itm.Count + chain.Strikes([price]).Count != chain.Count
+ || otm.Any(x => x.Right == OptionRight.Call ? x.Strike <= price : x.Strike >= price)
+ || itm.Any(x => x.Right == OptionRight.Call ? x.Strike >= price : x.Strike <= price))
+ {
+ throw new RegressionTestException("Out/in the money filters mismatch");
+ }
+ // By default the strikes on either side of the 748.54 close, 747.5 and 750, also reached within 2.5 points but not within 1;
+ // a chain whose strikes start more than 2% above the close has no strike at the money
+ var atm = chain.AtTheMoney();
+ if (atm.Count == 0 || atm.Count != chain.Strikes([747.5m, 750m]).Count || atm.Any(x => x.Strike != 747.5m && x.Strike != 750m)
+ || chain.AtTheMoney(2.5m).Count != atm.Count || chain.AtTheMoney(1m).Count != 0 || chain.AtTheMoney(0).Count != 0
+ || chain.StrikesAbove(price + 20).AtTheMoney().Count != 0)
+ {
+ throw new RegressionTestException("Expected AtTheMoney() to select the 747.5 and 750 strikes, AtTheMoney(1) none");
+ }
+
+ // Strike sets and bounds are absolute, unlike the relative Strikes(min, max)
+ var strikes = chain.Strikes([745m, 750m]);
+ if (strikes.Count == 0 || strikes.Any(x => x.Strike != 745m && x.Strike != 750m)
+ || chain.StrikesAbove(750m).StrikesBelow(755m).Any(x => x.Strike != 752.5m)
+ || chain.StrikesAbove(price).Count + chain.StrikesBelow(price).Count + chain.Strikes([price]).Count != chain.Count)
+ {
+ throw new RegressionTestException("Strike set or bound filters mismatch");
+ }
+
+ // Expiration sets and bounds, today's expiration and the farthest one
+ var frontMonth = new DateTime(2015, 12, 24);
+ var farthest = chain.FarthestExpiration();
+ if (chain.Expiration([frontMonth]).Count != chain.FrontMonth().Count
+ || chain.ZeroDte().Count != chain.Expiration(0, 0).Count
+ || chain.ExpiringAfter(frontMonth).Count + chain.FrontMonth().Count != chain.Count
+ || chain.ExpiringBefore(frontMonth).Count != 0
+ || farthest.Count == 0 || farthest.Any(x => x.Expiry != chain.Max(c => c.Expiry)))
+ {
+ throw new RegressionTestException("Expiration set, bound, ZeroDte() or FarthestExpiration() filters mismatch");
+ }
}
public override void OnData(Slice slice)
@@ -97,12 +139,27 @@ public override void OnData(Slice slice)
return;
}
- // The universe only selected calls expiring 1 to 10 days out, so the chain filters agree with it
- if (chain.CallsOnly().Expiration(1, 10).Count != chain.Count || chain.PutsOnly().Count != 0)
+ // The universe only selected the out of the money calls expiring 1 to 10 days out, two strikes around the
+ // previous close: 750 and 752.5 on 2015-12-31. The chain filters agree with it
+ if (chain.CallsOnly().Expiration(1, 10).Count != chain.Count || chain.PutsOnly().Count != 0
+ || chain.Strikes([750m, 752.5m]).Count != chain.Count || chain.Expiration([new DateTime(2015, 12, 31)]).Count != chain.Count)
{
throw new RegressionTestException("Slice chain filters disagree with the universe filter");
}
+ // On a calls only chain the moneyness filters are the strike bounds around the current price
+ var price = chain.Underlying.Price;
+ if (chain.OutOfTheMoney().Count != chain.StrikesAbove(price).Count || chain.InTheMoney().Count != chain.StrikesBelow(price).Count
+ || chain.OutOfTheMoney().Count + chain.InTheMoney().Count + chain.Strikes([price]).Count != chain.Count)
+ {
+ throw new RegressionTestException("Slice chain moneyness filters mismatch");
+ }
+ if (chain.ExpiringAfter(Time).Count != chain.Count || chain.ExpiringBefore(Time).Count != 0 || chain.ZeroDte().Count != 0
+ || chain.FarthestExpiration().Count != chain.Count)
+ {
+ throw new RegressionTestException("Slice chain expiration filters mismatch");
+ }
+
// Buy the call at the first strike at or above the underlying price
var contract = chain.Strikes(0, 0).FirstOrDefault();
if (contract != null)
@@ -145,7 +202,7 @@ private static void AssertContracts(OptionChain chain, OptionRight right, DateTi
///
/// Data Points count of all timeslices of algorithm
///
- public long DataPoints => 7080;
+ public long DataPoints => 5861;
///
/// Data Points count of the algorithm history
diff --git a/Algorithm.Python/FutureOptionChainFiltersRegressionAlgorithm.py b/Algorithm.Python/FutureOptionChainFiltersRegressionAlgorithm.py
new file mode 100644
index 000000000000..fadf1e496084
--- /dev/null
+++ b/Algorithm.Python/FutureOptionChainFiltersRegressionAlgorithm.py
@@ -0,0 +1,94 @@
+# 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 strike, expiration and moneyness filters on future options: in the universe selection
+### of the future and of its options, on the chains of the slice and on option_chain()
+###
+class FutureOptionChainFiltersRegressionAlgorithm(QCAlgorithm):
+ MARCH_EXPIRY = datetime(2020, 3, 20)
+ SELECTED_STRIKES = [3200, 3210, 3220, 3230, 3240, 3250]
+
+ def initialize(self):
+ self.set_start_date(2020, 1, 5)
+ self.set_end_date(2020, 1, 6)
+ self.set_cash(1000000)
+
+ # The March 2020 future, by its expiration date
+ es = self.add_future(Futures.Indices.SP_500_E_MINI, Resolution.MINUTE, Market.CME)
+ es.set_filter(lambda universe: universe.expiration([self.MARCH_EXPIRY]))
+ self._es = es.symbol
+
+ # Its options: the out of the money contracts within three strikes of the future price
+ self.add_future_option(self._es, lambda universe: universe.strikes(-3, 3).out_of_the_money())
+
+ self._chain_seen = False
+ self._traded = False
+
+ # The option chain of the March future from the universe data: one expiration, the future at 3223.75
+ chain = self.option_chain(Symbol.create_future(Futures.Indices.SP_500_E_MINI, Market.CME, self.MARCH_EXPIRY))
+ if chain.count == 0 or chain.underlying.price != 3223.75 or chain.symbol.security_type != SecurityType.FUTURE_OPTION:
+ raise AssertionError(f"Expected the March ES option chain at 3223.75 but got {chain.count} contracts at {chain.underlying.price}")
+ # Strikes are 10 points apart around the money: three each side of 3223.75 are 3200 to 3250
+ self._assert_strikes(chain.strikes(-3, 3).out_of_the_money().calls_only(), "strikes(-3, 3).out_of_the_money().calls_only()", [3230, 3240, 3250])
+ self._assert_strikes(chain.strikes(-3, 3).out_of_the_money().puts_only(), "strikes(-3, 3).out_of_the_money().puts_only()", [3200, 3210, 3220])
+ # Only the put is listed at 3310
+ self._assert_strikes(chain.strikes_above(3300).strikes_below(3320), "strikes_above(3300).strikes_below(3320)", [3310])
+ self._assert_strikes(chain.strikes_above(3300).strikes_below(3320).calls_only(), "strikes_above(3300).strikes_below(3320).calls_only()", [])
+ # The strikes on either side of 3223.75 are 3220 and 3230; within 5 points only 3220
+ self._assert_strikes(chain.at_the_money(), "at_the_money()", [3220, 3220, 3230, 3230])
+ self._assert_strikes(chain.at_the_money(5), "at_the_money(5)", [3220, 3220])
+ if (chain.at_the_money(0).count != 0 or chain.expiration([self.MARCH_EXPIRY]).count != chain.count or chain.farthest_expiration().count != chain.count
+ or chain.expiring_after(self.MARCH_EXPIRY).count != 0 or chain.zero_dte().count != 0
+ or chain.standards_only().count != chain.count or chain.weeklys_only().count != 0):
+ raise AssertionError("Expiration or contract type filters mismatch on the March ES option chain")
+
+ def on_data(self, slice):
+ # One chain per future contract, keyed by its canonical option symbol
+ for chain in slice.option_chains.values():
+ if chain.symbol.underlying.id.date != self.MARCH_EXPIRY:
+ raise AssertionError(f"Unexpected option chain for {chain.symbol.underlying}")
+ self._chain_seen = True
+
+ # The universe selected the out of the money contracts within three strikes of the previous close: 3200 to 3250
+ if chain.count == 0 or chain.strikes(self.SELECTED_STRIKES).count != chain.count or chain.expiration([self.MARCH_EXPIRY]).count != chain.count:
+ raise AssertionError(f"The option chain disagrees with the universe filter: {[x.symbol.value for x in chain]}")
+
+ # The moneyness filters partition the chain around the current future price, and match the strike bounds for a single right
+ price = chain.underlying.price
+ otm = chain.out_of_the_money()
+ itm = chain.in_the_money()
+ if (otm.count + itm.count + chain.strikes([price]).count != chain.count
+ or any((x.strike <= price if x.right == OptionRight.CALL else x.strike >= price) for x in otm)
+ or any((x.strike >= price if x.right == OptionRight.CALL else x.strike <= price) for x in itm)
+ or chain.calls_only().out_of_the_money().count != chain.calls_only().strikes_above(price).count
+ or chain.puts_only().out_of_the_money().count != chain.puts_only().strikes_below(price).count):
+ raise AssertionError(f"Moneyness filters mismatch at {price}")
+
+ # Buy the out of the money call closest to the future price
+ if not self._traded:
+ calls = sorted(otm.calls_only(), key=lambda x: x.strike)
+ if calls:
+ self.market_order(calls[0].symbol, 1)
+ self._traded = True
+
+ def on_end_of_algorithm(self):
+ if not self._chain_seen or not self._traded:
+ raise AssertionError(f"Expected the March ES option chain ({self._chain_seen}) and a trade ({self._traded})")
+
+ def _assert_strikes(self, chain, filter_name, expected):
+ actual = sorted(float(x.strike) for x in chain)
+ if actual != sorted(float(x) for x in expected):
+ raise AssertionError(f"{filter_name}: expected strikes {expected} but got {actual}")
diff --git a/Algorithm.Python/FutureUniverseFiltersRegressionAlgorithm.py b/Algorithm.Python/FutureUniverseFiltersRegressionAlgorithm.py
new file mode 100644
index 000000000000..59b2f587af0a
--- /dev/null
+++ b/Algorithm.Python/FutureUniverseFiltersRegressionAlgorithm.py
@@ -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.
+
+from AlgorithmImports import *
+
+###
+### Regression algorithm using the expiration set and bound filters in the futures universe selection,
+### the same ones the option universes and chains offer, and checking the selected chains in the slice
+###
+class FutureUniverseFiltersRegressionAlgorithm(QCAlgorithm):
+ END_OF_2013 = datetime(2013, 12, 31)
+ END_OF_NOVEMBER_2014 = datetime(2014, 11, 30)
+
+ def initialize(self):
+ self.set_start_date(2013, 10, 7)
+ self.set_end_date(2013, 10, 9)
+ self.set_cash(1000000)
+
+ # The 2014 contracts up to September
+ es = self.add_future(Futures.Indices.SP_500_E_MINI, Resolution.MINUTE, Market.CME)
+ es.set_filter(lambda universe: universe.expiring_after(self.END_OF_2013).expiring_before(self.END_OF_NOVEMBER_2014))
+ self._es = es.symbol
+
+ # The contracts expiring this year
+ gc = self.add_future(Futures.Metals.GOLD, Resolution.MINUTE, Market.COMEX)
+ gc.set_filter(lambda universe: universe.expiring_before(datetime(2014, 1, 1)))
+ self._gc = gc.symbol
+
+ self._es_chain_seen = False
+ self._gc_chain_seen = False
+ self._traded = False
+
+ # The full chain from the universe data lists the December 2013 contract and the March to December 2014 ones
+ chain = self.futures_chain(self._es)
+ expiries = sorted(x.expiry for x in chain)
+ if len(expiries) != 5 or expiries[0] > self.END_OF_2013 or any(x.year != 2014 for x in expiries[1:]):
+ raise AssertionError(f"Unexpected ES chain expiries: {expiries}")
+
+ def on_data(self, slice):
+ es_chain = slice.futures_chains.get(self._es)
+ if es_chain:
+ self._es_chain_seen = True
+ # March, June and September 2014
+ if es_chain.count == 0 or es_chain.count > 3 or any(x.expiry <= self.END_OF_2013 or x.expiry >= self.END_OF_NOVEMBER_2014 for x in es_chain):
+ raise AssertionError(f"The ES chain disagrees with the universe filter: {[x.expiry for x in es_chain]}")
+ if not self._traded:
+ self.market_order(min(es_chain, key=lambda x: x.expiry).symbol, 1)
+ self._traded = True
+
+ gc_chain = slice.futures_chains.get(self._gc)
+ if gc_chain:
+ self._gc_chain_seen = True
+ # October, November and December 2013
+ if gc_chain.count == 0 or gc_chain.count > 3 or any(x.expiry.year != 2013 for x in gc_chain):
+ raise AssertionError(f"The GC 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})")
diff --git a/Algorithm.Python/IndexOptionChainFiltersRegressionAlgorithm.py b/Algorithm.Python/IndexOptionChainFiltersRegressionAlgorithm.py
new file mode 100644
index 000000000000..ca67ee2b55cf
--- /dev/null
+++ b/Algorithm.Python/IndexOptionChainFiltersRegressionAlgorithm.py
@@ -0,0 +1,122 @@
+# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from AlgorithmImports import *
+
+###
+### Regression algorithm using the strike, expiration and moneyness filters on index options: in the universe selection
+### of standard and weekly contracts, on the chains of the slice and on option_chain()
+###
+class IndexOptionChainFiltersRegressionAlgorithm(QCAlgorithm):
+ FIRST_DAY = datetime(2021, 1, 4)
+ STANDARD_EXPIRY = datetime(2021, 1, 15)
+
+ def initialize(self):
+ self.set_start_date(2021, 1, 4)
+ self.set_end_date(2021, 1, 8)
+ self.set_cash(1000000)
+
+ # Standard SPX contracts: the out of the money ones with strikes below 4000
+ spx = self.add_index_option("SPX")
+ spx.set_filter(lambda universe: universe.out_of_the_money().strikes_below(4000))
+ self._spx = spx.symbol
+
+ # Weekly SPXW contracts: the 3700 strike of the expirations after the first day
+ spxw = self.add_index_option("SPX", "SPXW")
+ spxw.set_filter(lambda universe: universe.strikes([3700]).expiring_after(self.FIRST_DAY))
+ self._spxw = spxw.symbol
+
+ self._spx_chain_seen = False
+ self._zero_dte_seen = False
+ self._traded = False
+
+ # The latest universe data, from 2020-12-31, lists the 3200, 3700, 3800 and 4250 calls and the 3200 and 4200 puts
+ # expiring on 2021-01-15, with the index at 3766.63: the same filters narrow the chain down
+ chain = self.option_chain(self._spx)
+ if chain.count != 6 or chain.underlying.price != 3766.63:
+ raise AssertionError(f"Expected the 6 SPX contracts at 3766.63 but got {chain.count} at {chain.underlying.price}")
+ self._assert_contracts(chain.out_of_the_money(), "out_of_the_money()", [(3800, OptionRight.CALL), (4250, OptionRight.CALL), (3200, OptionRight.PUT)])
+ self._assert_contracts(chain.in_the_money(), "in_the_money()", [(3200, OptionRight.CALL), (3700, OptionRight.CALL), (4200, OptionRight.PUT)])
+ # The strikes on either side of 3766.63 are 3700 and 3800, both listed as calls only; 50 points reach 3800, 25 none
+ self._assert_contracts(chain.at_the_money(), "at_the_money()", [(3700, OptionRight.CALL), (3800, OptionRight.CALL)])
+ self._assert_contracts(chain.at_the_money(50), "at_the_money(50)", [(3800, OptionRight.CALL)])
+ self._assert_contracts(chain.at_the_money(25), "at_the_money(25)", [])
+ self._assert_contracts(chain.at_the_money(0), "at_the_money(0)", [])
+ self._assert_contracts(chain.strikes_above(3700).strikes_below(4250), "strikes_above(3700).strikes_below(4250)", [(3800, OptionRight.CALL), (4200, OptionRight.PUT)])
+ self._assert_contracts(chain.strikes([3200, 4250]), "strikes([3200, 4250])", [(3200, OptionRight.CALL), (4250, OptionRight.CALL), (3200, OptionRight.PUT)])
+ self._assert_contracts(chain.out_of_the_money().strikes_below(4000), "the SPX universe filter", [(3800, OptionRight.CALL), (3200, OptionRight.PUT)])
+ if (chain.expiration([self.STANDARD_EXPIRY]).count != chain.count or chain.farthest_expiration().count != chain.count
+ or chain.expiring_after(self.STANDARD_EXPIRY).count != 0 or chain.expiring_before(self.STANDARD_EXPIRY).count != 0
+ or chain.zero_dte().count != 0):
+ raise AssertionError("Expected every SPX contract to expire on 2021-01-15")
+
+ def on_data(self, slice):
+ spx_chain = slice.option_chains.get(self._spx)
+ if spx_chain:
+ self._spx_chain_seen = True
+ # The universe selected the out of the money contracts below 4000: the 3800 call and the 3200 put.
+ # The index stays between those strikes, so the chain filter agrees with the universe filter
+ self._assert_contracts(spx_chain, "the SPX slice chain", [(3800, OptionRight.CALL), (3200, OptionRight.PUT)])
+ if spx_chain.out_of_the_money().count != spx_chain.count:
+ raise AssertionError("Expected the SPX slice chain to be out of the money")
+ self._assert_moneyness(spx_chain)
+
+ chain = slice.option_chains.get(self._spxw)
+ if not chain:
+ return
+
+ # The universe selected the 3700 strike of the expirations after the first day: 2021-01-06 and 2021-01-08
+ if (chain.count == 0 or chain.strikes([3700]).count != chain.count or chain.expiring_after(self.FIRST_DAY).count != chain.count
+ or chain.expiring_before(datetime(2021, 1, 9)).count != chain.count):
+ raise AssertionError("The SPXW slice chain disagrees with the universe filter")
+ self._assert_moneyness(chain)
+
+ zero_dte = chain.zero_dte()
+ if any(x.expiry.date() != self.time.date() for x in zero_dte):
+ raise AssertionError("zero_dte() selected contracts not expiring today")
+ self._zero_dte_seen |= zero_dte.count > 0
+
+ farthest = chain.farthest_expiration()
+ max_expiry = max(x.expiry for x in chain)
+ if farthest.count == 0 or any(x.expiry != max_expiry for x in farthest):
+ raise AssertionError("farthest_expiration() mismatch")
+
+ # Buy the 3700 call of the nearest expiration after today
+ if not self._traded:
+ contract = next(iter(chain.calls_only().expiring_after(self.time).front_month()), None)
+ if contract is not None:
+ self.market_order(contract.symbol, 1)
+ self._traded = True
+
+ def on_end_of_algorithm(self):
+ if not self._spx_chain_seen or not self._zero_dte_seen or not self._traded:
+ raise AssertionError(f"Expected the SPX chain ({self._spx_chain_seen}), a 0DTE SPXW contract ({self._zero_dte_seen}) and a trade ({self._traded})")
+
+ def _assert_moneyness(self, chain):
+ '''The moneyness filters partition the chain around the current index price, and match the strike bounds for a single right'''
+ price = chain.underlying.price
+ otm = chain.out_of_the_money()
+ itm = chain.in_the_money()
+ if (otm.count + itm.count + chain.strikes([price]).count != chain.count
+ or any((x.strike <= price if x.right == OptionRight.CALL else x.strike >= price) for x in otm)
+ or any((x.strike >= price if x.right == OptionRight.CALL else x.strike <= price) for x in itm)
+ or chain.calls_only().out_of_the_money().count != chain.calls_only().strikes_above(price).count
+ or chain.puts_only().out_of_the_money().count != chain.puts_only().strikes_below(price).count):
+ raise AssertionError(f"Moneyness filters mismatch at {price}")
+
+ def _assert_contracts(self, chain, filter_name, expected):
+ key = lambda contract: (float(contract[0]), contract[1] == OptionRight.PUT)
+ actual = sorted(((x.strike, x.right) for x in chain), key=key)
+ expected = sorted(expected, key=key)
+ if [key(x) for x in actual] != [key(x) for x in expected]:
+ raise AssertionError(f"{filter_name}: expected {expected} but got {actual}")
diff --git a/Algorithm.Python/OptionChainFiltersRegressionAlgorithm.py b/Algorithm.Python/OptionChainFiltersRegressionAlgorithm.py
index 3452d3235d80..11ef3f0c45da 100644
--- a/Algorithm.Python/OptionChainFiltersRegressionAlgorithm.py
+++ b/Algorithm.Python/OptionChainFiltersRegressionAlgorithm.py
@@ -27,7 +27,7 @@ def initialize(self):
option = self.add_option("GOOG")
self._option = option.symbol
# The same words select the universe and, below, narrow down the chains
- option.set_filter(lambda universe: universe.calls_only().expiration(1, 10).strikes(-2, 2))
+ option.set_filter(lambda universe: universe.calls_only().expiration(1, 10).strikes(-2, 2).out_of_the_money())
chain = self.option_chain(self._option)
if chain.count == 0:
@@ -61,6 +61,40 @@ def initialize(self):
if deltas.count == 0 or deltas.count != expected_deltas or any(not 0.5 <= x.greeks.delta <= 0.6 for x in deltas):
raise AssertionError("Delta filter mismatch")
+ # Moneyness filters split the strikes around the underlying price, ATM is the closest strike
+ price = chain.underlying.price
+ otm = chain.otm()
+ itm = chain.itm()
+ if (otm.count == 0 or itm.count == 0 or otm.count + itm.count + chain.strikes([price]).count != chain.count
+ or any((x.strike <= price if x.right == OptionRight.CALL else x.strike >= price) for x in otm)
+ or any((x.strike >= price if x.right == OptionRight.CALL else x.strike <= price) for x in itm)):
+ raise AssertionError("Out/in the money filters mismatch")
+ # By default the strikes on either side of the 748.54 close, 747.5 and 750, also reached within 2.5 points but not within 1;
+ # a chain whose strikes start more than 2% above the close has no strike at the money
+ atm = chain.atm()
+ if (atm.count == 0 or atm.count != chain.strikes([747.5, 750]).count or any(x.strike != 747.5 and x.strike != 750 for x in atm)
+ or chain.atm(2.5).count != atm.count or chain.atm(1).count != 0 or chain.atm(0).count != 0
+ or chain.strikes_above(price + 20).atm().count != 0):
+ raise AssertionError("Expected atm() to select the 747.5 and 750 strikes, atm(1) none")
+
+ # Strike sets and bounds are absolute, unlike the relative strikes(min, max)
+ strikes = chain.strikes([745, 750])
+ if (strikes.count == 0 or any(x.strike != 745 and x.strike != 750 for x in strikes)
+ or any(x.strike != 752.5 for x in chain.strikes_above(750).strikes_below(755))
+ or chain.strikes_above(price).count + chain.strikes_below(price).count + chain.strikes([price]).count != chain.count):
+ raise AssertionError("Strike set or bound filters mismatch")
+
+ # Expiration sets and bounds, today's expiration and the farthest one
+ front_month = datetime(2015, 12, 24)
+ farthest = chain.farthest_expiration()
+ max_expiry = max(x.expiry for x in chain)
+ if (chain.expiration([front_month]).count != chain.front_month().count
+ or chain.zero_dte().count != chain.expiration(0, 0).count
+ or chain.expiring_after(front_month).count + chain.front_month().count != chain.count
+ or chain.expiring_before(front_month).count != 0
+ or farthest.count == 0 or any(x.expiry != max_expiry for x in farthest)):
+ raise AssertionError("Expiration set, bound, zero_dte() or farthest_expiration() filters mismatch")
+
# where() takes a predicate, like the universe filter does
high_open_interest = chain.where(lambda x: x.open_interest > 1000)
if high_open_interest.count == 0 or high_open_interest.count != sum(1 for x in chain if x.open_interest > 1000):
@@ -75,10 +109,21 @@ def on_data(self, slice):
if not chain:
return
- # The universe only selected calls expiring 1 to 10 days out, so the chain filters agree with it
- if chain.calls_only().expiration(1, 10).count != chain.count or chain.puts_only().count != 0:
+ # The universe only selected the out of the money calls expiring 1 to 10 days out, two strikes around the
+ # previous close: 750 and 752.5 on 2015-12-31. The chain filters agree with it
+ if (chain.calls_only().expiration(1, 10).count != chain.count or chain.puts_only().count != 0
+ or chain.strikes([750, 752.5]).count != chain.count or chain.expiration([datetime(2015, 12, 31)]).count != chain.count):
raise AssertionError("Slice chain filters disagree with the universe filter")
+ # On a calls only chain the moneyness filters are the strike bounds around the current price
+ price = chain.underlying.price
+ if (chain.out_of_the_money().count != chain.strikes_above(price).count or chain.in_the_money().count != chain.strikes_below(price).count
+ or chain.out_of_the_money().count + chain.in_the_money().count + chain.strikes([price]).count != chain.count):
+ raise AssertionError("Slice chain moneyness filters mismatch")
+ if (chain.expiring_after(self.time).count != chain.count or chain.expiring_before(self.time).count != 0 or chain.zero_dte().count != 0
+ or chain.farthest_expiration().count != chain.count):
+ raise AssertionError("Slice chain expiration filters mismatch")
+
# Buy the call at the first strike at or above the underlying price
contract = next(iter(chain.strikes(0, 0)), None)
if contract is not None:
diff --git a/Common/Data/Market/BaseContract.cs b/Common/Data/Market/BaseContract.cs
index 19110435d8f0..0d9210d2bb61 100644
--- a/Common/Data/Market/BaseContract.cs
+++ b/Common/Data/Market/BaseContract.cs
@@ -48,6 +48,12 @@ public Symbol Symbol
///
public DateTime Expiry => Symbol.ID.Date;
+ ///
+ /// Calendar days from this contract's time until it stops trading
+ ///
+ [PandasIgnore]
+ public virtual int DaysToExpiry => (Expiry.Date - Time.Date).Days;
+
///
/// Gets the local date time this contract's data was last updated
///
diff --git a/Common/Data/Market/DataDictionary.cs b/Common/Data/Market/DataDictionary.cs
index 4e4e36b36811..8103ab39f4c8 100644
--- a/Common/Data/Market/DataDictionary.cs
+++ b/Common/Data/Market/DataDictionary.cs
@@ -85,7 +85,7 @@ public override T this[Symbol symbol]
}
set
{
- _items = null;
+ ClearCache();
base[symbol] = value;
}
}
diff --git a/Common/Data/Market/OptionChain.Filters.cs b/Common/Data/Market/OptionChain.Filters.cs
index 3fdaac86b4ac..19b4c0179931 100644
--- a/Common/Data/Market/OptionChain.Filters.cs
+++ b/Common/Data/Market/OptionChain.Filters.cs
@@ -14,6 +14,7 @@
*/
using System;
+using System.Collections.Generic;
using System.Linq;
using Python.Runtime;
using QuantConnect.Securities;
@@ -68,6 +69,81 @@ 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
+ ///
+ /// The strike prices
+ /// A new chain with the filter applied
+ public OptionChain Strikes(IEnumerable strikes)
+ {
+ return Filter(universe => universe.Strikes(strikes));
+ }
+
+ ///
+ /// Selects the contracts with strikes above the given price, excluding it.
+ /// Same as
+ ///
+ /// The price the strikes must be above
+ /// A new chain with the filter applied
+ public OptionChain StrikesAbove(decimal price)
+ {
+ return Filter(universe => universe.StrikesAbove(price));
+ }
+
+ ///
+ /// Selects the contracts with strikes below the given price, excluding it.
+ /// Same as
+ ///
+ /// The price the strikes must be below
+ /// A new chain with the filter applied
+ 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
///
@@ -86,6 +162,71 @@ public OptionChain PutsOnly()
return Filter(universe => universe.PutsOnly());
}
+ ///
+ /// Selects the out of the money contracts: calls with strikes above the underlying price and puts with strikes below it.
+ /// Same as
+ ///
+ /// A new chain with the filter applied, empty when the underlying price is unknown
+ public OptionChain OutOfTheMoney()
+ {
+ return Filter(universe => universe.OutOfTheMoney());
+ }
+
+ ///
+ /// Selects the out of the money contracts. Alias for
+ ///
+ /// A new chain with the filter applied
+ public OptionChain OTM()
+ {
+ return OutOfTheMoney();
+ }
+
+ ///
+ /// Selects the in the money contracts: calls with strikes below the underlying price and puts with strikes above it.
+ /// Same as
+ ///
+ /// A new chain with the filter applied, empty when the underlying price is unknown
+ public OptionChain InTheMoney()
+ {
+ return Filter(universe => universe.InTheMoney());
+ }
+
+ ///
+ /// Selects the in the money contracts. Alias for
+ ///
+ /// A new chain with the filter applied
+ public OptionChain ITM()
+ {
+ return InTheMoney();
+ }
+
+ ///
+ /// Selects the contracts at the money: the ones with strikes within the given distance of the underlying price, or by default
+ /// the ones at the strikes on either side of it. Same as
+ ///
+ /// The largest distance between a strike and the underlying price for its contracts to be at
+ /// the money, in units of the underlying price. Zero selects only a strike equal to the price. Null, the default, selects the
+ /// strikes on either side of the price, the highest at or below it and the lowest at or above it, each only when it is within
+ /// the percentage of the price given by
+ /// A new chain with the filter applied, empty when the underlying price is unknown
+ public OptionChain AtTheMoney(decimal? maxStrikeDistance = null)
+ {
+ return Filter(universe => universe.AtTheMoney(maxStrikeDistance));
+ }
+
+ ///
+ /// Selects the contracts at the money. Alias for
+ ///
+ /// The largest distance between a strike and the underlying price for its contracts to be at
+ /// the money, in units of the underlying price. Zero selects only a strike equal to the price. Null, the default, selects the
+ /// strikes on either side of the price, the highest at or below it and the lowest at or above it, each only when it is within
+ /// the percentage of the price given by
+ /// A new chain with the filter applied
+ 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
@@ -115,6 +256,15 @@ 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
///
diff --git a/Common/Securities/ContractSecurityFilterUniverse.cs b/Common/Securities/ContractSecurityFilterUniverse.cs
index ec1be57ef2f4..ba4b80ce5b1f 100644
--- a/Common/Securities/ContractSecurityFilterUniverse.cs
+++ b/Common/Securities/ContractSecurityFilterUniverse.cs
@@ -268,6 +268,34 @@ public virtual T FrontMonth()
return (T)this;
}
+ ///
+ /// Returns the contracts of the farthest expiration
+ ///
+ /// Universe with filter applied
+ public virtual T FarthestExpiration()
+ {
+ ApplyTypesFilter();
+ // one pass: a later expiration restarts the selection, the same one extends it
+ var farthestDate = DateTime.MinValue;
+ var farthest = new List();
+ foreach (var data in Data)
+ {
+ var date = data.Symbol.ID.Date;
+ if (date > farthestDate)
+ {
+ farthestDate = date;
+ farthest.Clear();
+ }
+ if (date == farthestDate)
+ {
+ farthest.Add(data);
+ }
+ }
+
+ Data = farthest;
+ return (T)this;
+ }
+
///
/// Returns a list of back month contracts
///
@@ -344,6 +372,42 @@ public T Expiration(int minExpiryDays, int maxExpiryDays)
return Expiration(TimeSpan.FromDays(minExpiryDays), TimeSpan.FromDays(maxExpiryDays));
}
+ ///
+ /// Applies filter selecting the contracts expiring on any of the given dates. Time of day is ignored
+ ///
+ /// The expiration dates
+ /// Universe with filter applied
+ public T Expiration(IEnumerable expiries)
+ {
+ var expiryDates = expiries.Select(expiry => expiry.Date).ToHashSet();
+ Data = Data.Where(data => expiryDates.Contains(data.Symbol.ID.Date.Date)).ToList();
+ return (T)this;
+ }
+
+ ///
+ /// Applies filter selecting the contracts expiring after the given date, excluding it. Time of day is ignored
+ ///
+ /// The date the expirations must be after
+ /// Universe with filter applied
+ public T ExpiringAfter(DateTime date)
+ {
+ var expiryDate = date.Date;
+ Data = Data.Where(data => data.Symbol.ID.Date.Date > expiryDate).ToList();
+ return (T)this;
+ }
+
+ ///
+ /// Applies filter selecting the contracts expiring before the given date, excluding it. Time of day is ignored
+ ///
+ /// The date the expirations must be before
+ /// Universe with filter applied
+ public T ExpiringBefore(DateTime date)
+ {
+ var expiryDate = date.Date;
+ Data = Data.Where(data => data.Symbol.ID.Date.Date < expiryDate).ToList();
+ return (T)this;
+ }
+
///
/// 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/Option/IOptionContractFilters.cs b/Common/Securities/Option/IOptionContractFilters.cs
index 095601441bc1..ae24dc316f28 100644
--- a/Common/Securities/Option/IOptionContractFilters.cs
+++ b/Common/Securities/Option/IOptionContractFilters.cs
@@ -14,6 +14,7 @@
*/
using System;
+using System.Collections.Generic;
namespace QuantConnect.Securities
{
@@ -40,6 +41,73 @@ public interface IOptionContractFilters
///
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
+ ///
+ TSelf Strikes(IEnumerable strikes);
+
+ ///
+ /// Selects the contracts with strikes above the given price, excluding it
+ ///
+ TSelf StrikesAbove(decimal price);
+
+ ///
+ /// Selects the contracts with strikes below the given price, excluding it
+ ///
+ 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
+ ///
+ TSelf OutOfTheMoney();
+
+ ///
+ /// Selects the out of the money contracts. Alias for
+ ///
+ TSelf OTM();
+
+ ///
+ /// Selects the in the money contracts: calls below and puts above the underlying price
+ ///
+ TSelf InTheMoney();
+
+ ///
+ /// Selects the in the money contracts. Alias for
+ ///
+ TSelf ITM();
+
+ ///
+ /// Selects the contracts with strikes within the given distance of the underlying price, in units of it, zero only a strike
+ /// equal to the price; null, the default, the strikes on either side of the price, each within the percentage of it
+ /// given by
+ ///
+ TSelf AtTheMoney(decimal? maxStrikeDistance = null);
+
+ ///
+ /// Selects the contracts at the money. Alias for
+ ///
+ TSelf ATM(decimal? maxStrikeDistance = null);
+
///
/// Selects the call contracts
///
@@ -65,6 +133,11 @@ public interface IOptionContractFilters
///
TSelf FrontMonth();
+ ///
+ /// Selects the contracts of the farthest expiration
+ ///
+ TSelf FarthestExpiration();
+
///
/// Selects the contracts of all expirations but the nearest one
///
diff --git a/Common/Securities/Option/OptionFilterUniverse.cs b/Common/Securities/Option/OptionFilterUniverse.cs
index f7050a692203..0d11f6b79a5a 100644
--- a/Common/Securities/Option/OptionFilterUniverse.cs
+++ b/Common/Securities/Option/OptionFilterUniverse.cs
@@ -25,6 +25,7 @@
using QuantConnect.Securities.FutureOption;
using QuantConnect.Securities.IndexOption;
using QuantConnect.Securities.Option;
+using QuantConnect.Util;
namespace QuantConnect.Securities
{
@@ -279,6 +280,137 @@ public TUniverse PutsOnly()
return Contracts(contracts => contracts.Where(x => x.Symbol.ID.OptionRight == OptionRight.Put));
}
+ ///
+ /// Applies filter selecting the contracts with any of the given strike prices
+ ///
+ /// The strike prices
+ /// Universe with filter applied
+ public TUniverse Strikes(IEnumerable strikes)
+ {
+ var strikeSet = strikes.ToHashSet();
+ return Contracts(contracts => contracts.Where(x => strikeSet.Contains(x.Symbol.ID.StrikePrice)));
+ }
+
+ ///
+ /// Applies filter selecting the contracts with strikes above the given price, excluding it
+ ///
+ /// The price the strikes must be above
+ /// Universe with filter applied
+ public TUniverse StrikesAbove(decimal price)
+ {
+ return Contracts(contracts => contracts.Where(x => x.Symbol.ID.StrikePrice > price));
+ }
+
+ ///
+ /// Applies filter selecting the contracts with strikes below the given price, excluding it
+ ///
+ /// The price the strikes must be below
+ /// Universe with filter applied
+ 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
+ ///
+ /// Universe with filter applied
+ public TUniverse OutOfTheMoney()
+ {
+ if (!TryGetUnderlyingPrice(out var price))
+ {
+ return Empty();
+ }
+ return Contracts(contracts => contracts.Where(x => OptionPayoff.IsOutOfTheMoney(price, x.Symbol.ID.StrikePrice, x.Symbol.ID.OptionRight)));
+ }
+
+ ///
+ /// Applies filter selecting the out of the money contracts. Alias for
+ ///
+ /// Universe with filter applied
+ public TUniverse OTM()
+ {
+ return OutOfTheMoney();
+ }
+
+ ///
+ /// Applies filter selecting the in the money contracts: calls with strikes below the underlying price
+ /// and puts with strikes above it. Selects nothing when the underlying price is unknown
+ ///
+ /// Universe with filter applied
+ public TUniverse InTheMoney()
+ {
+ if (!TryGetUnderlyingPrice(out var price))
+ {
+ return Empty();
+ }
+ return Contracts(contracts => contracts.Where(x => OptionPayoff.IsInTheMoney(price, x.Symbol.ID.StrikePrice, x.Symbol.ID.OptionRight)));
+ }
+
+ ///
+ /// Applies filter selecting the in the money contracts. Alias for
+ ///
+ /// Universe with filter applied
+ public TUniverse ITM()
+ {
+ return InTheMoney();
+ }
+
+ ///
+ /// Applies filter selecting the contracts at the money: the ones with strikes within the given distance of the underlying price,
+ /// or by default the ones at the strikes on either side of it. Selects nothing when the underlying price is unknown
+ ///
+ /// The largest distance between a strike and the underlying price for its contracts to be at
+ /// the money, in units of the underlying price. Zero selects only a strike equal to the price. Null, the default, selects the
+ /// strikes on either side of the price, the highest at or below it and the lowest at or above it, each only when it is within
+ /// the percentage of the price given by
+ /// Universe with filter applied
+ public TUniverse AtTheMoney(decimal? maxStrikeDistance = null)
+ {
+ if (maxStrikeDistance < 0)
+ {
+ throw new ArgumentException($"AtTheMoney(): {nameof(maxStrikeDistance)} must not be negative");
+ }
+ if (!TryGetUnderlyingPrice(out var price))
+ {
+ return Empty();
+ }
+ if (!maxStrikeDistance.HasValue)
+ {
+ return Strikes(GetBracketingStrikes(price));
+ }
+ // the price is in strike units, see SymbolProperties.StrikeMultiplier, so the distance is scaled the same way
+ var maxDistance = maxStrikeDistance.Value / _underlyingScaleFactor;
+ if (maxDistance == 0)
+ {
+ return Strikes([price]);
+ }
+ return Contracts(contracts => contracts.Where(x => Math.Abs(x.Symbol.ID.StrikePrice - price) <= maxDistance));
+ }
+
+ ///
+ /// Applies filter selecting the contracts at the money. Alias for
+ ///
+ /// The largest distance between a strike and the underlying price for its contracts to be at
+ /// the money, in units of the underlying price. Zero selects only a strike equal to the price. Null, the default, selects the
+ /// strikes on either side of the price, the highest at or below it and the lowest at or above it, each only when it is within
+ /// the percentage of the price given by
+ /// Universe with filter applied
+ public TUniverse ATM(decimal? maxStrikeDistance = null)
+ {
+ return AtTheMoney(maxStrikeDistance);
+ }
+
///
/// Sets universe of a single call contract with the closest match to criteria given
///
@@ -1099,6 +1231,16 @@ private TUniverse InRange(Func selector, decimal min, decimal ma
}));
}
+ ///
+ /// Gets the underlying price in strike units, false when the underlying is unknown
+ ///
+ private bool TryGetUnderlyingPrice(out decimal price)
+ {
+ // some option strikes are a fraction of the underlying, see SymbolProperties.StrikeMultiplier
+ price = UnderlyingInternal == null ? 0 : UnderlyingInternal.Price / _underlyingScaleFactor;
+ return UnderlyingInternal != null;
+ }
+
///
/// Helper method that will select no contract
///
@@ -1119,8 +1261,50 @@ private TUniverse SymbolList(List contracts)
private decimal GetStrike(IEnumerable symbols, decimal strikeFromAtm)
{
- return symbols.OrderBy(x => Math.Abs(Underlying.Price + strikeFromAtm - x.ID.StrikePrice))
- .Select(x => x.ID.StrikePrice)
+ return GetClosestStrike(symbols, Underlying.Price + strikeFromAtm);
+ }
+
+ ///
+ /// Gets the highest strike at or below the price and the lowest at or above it, each only when it is within the percentage
+ /// of the price given by , one when they coincide
+ ///
+ private List GetBracketingStrikes(decimal price)
+ {
+ decimal? below = null;
+ decimal? above = null;
+ foreach (var strike in AllSymbols.Select(x => x.ID.StrikePrice))
+ {
+ if (strike <= price && (below == null || strike > below))
+ {
+ below = strike;
+ }
+ if (strike >= price && (above == null || strike < above))
+ {
+ above = strike;
+ }
+ }
+
+ var maxDistance = price * OptionFilterUniverse.DefaultAtTheMoneyStrikeDistance;
+ var strikes = new List(2);
+ if (below != null && price - below <= maxDistance)
+ {
+ strikes.Add(below.Value);
+ }
+ if (above != null && above != below && above - price <= maxDistance)
+ {
+ strikes.Add(above.Value);
+ }
+ return strikes;
+ }
+
+ ///
+ /// Gets the strike closest to the target price, the lower one on ties, or decimal.MaxValue when there are no symbols
+ ///
+ private static decimal GetClosestStrike(IEnumerable symbols, decimal targetPrice)
+ {
+ return symbols.Select(x => x.ID.StrikePrice)
+ .OrderBy(strike => Math.Abs(targetPrice - strike))
+ .ThenBy(strike => strike)
.DefaultIfEmpty(decimal.MaxValue)
.First();
}
@@ -1140,8 +1324,27 @@ private void ValidateSecurityTypeForSupportedFilters(string filterName)
///
public class OptionFilterUniverse : BaseOptionFilterUniverse
{
+ private static decimal _defaultAtTheMoneyStrikeDistance = 0.02m;
+
private readonly Option.Option _option;
+ ///
+ /// How far from the underlying price, as a percentage of it, a strike on either side can be and still count as at the money
+ /// by default in . 0.02, 2%, unless changed
+ ///
+ public static decimal DefaultAtTheMoneyStrikeDistance
+ {
+ get => _defaultAtTheMoneyStrikeDistance;
+ set
+ {
+ if (value < 0)
+ {
+ throw new ArgumentException($"{nameof(DefaultAtTheMoneyStrikeDistance)} must not be negative");
+ }
+ _defaultAtTheMoneyStrikeDistance = value;
+ }
+ }
+
///
/// The option exchange hours
///
diff --git a/Common/Util/OptionPayoff.cs b/Common/Util/OptionPayoff.cs
index bd4545fc2f9f..06e670ab88c6 100644
--- a/Common/Util/OptionPayoff.cs
+++ b/Common/Util/OptionPayoff.cs
@@ -74,5 +74,83 @@ public static double GetPayOff(double underlyingPrice, double strike, OptionRigh
{
return right == OptionRight.Call ? underlyingPrice - strike : strike - underlyingPrice;
}
+
+ ///
+ /// Whether the option is in the money: a call with the strike below the underlying price, a put with the strike above it
+ ///
+ /// The price of the underlying
+ /// The strike price of the option
+ /// The option right of the option, call or put
+ /// True if the option has intrinsic value
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsInTheMoney(decimal underlyingPrice, decimal strike, OptionRight right)
+ {
+ return GetPayOff(underlyingPrice, strike, right) > 0;
+ }
+
+ ///
+ /// Whether the option is in the money: a call with the strike below the underlying price, a put with the strike above it
+ ///
+ /// The price of the underlying
+ /// The strike price of the option
+ /// The option right of the option, call or put
+ /// True if the option has intrinsic value
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsInTheMoney(double underlyingPrice, double strike, OptionRight right)
+ {
+ return GetPayOff(underlyingPrice, strike, right) > 0;
+ }
+
+ ///
+ /// Whether the option is at the money: the strike equals the underlying price
+ ///
+ /// The price of the underlying
+ /// The strike price of the option
+ /// The option right of the option, call or put
+ /// True if the strike equals the underlying price
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsAtTheMoney(decimal underlyingPrice, decimal strike, OptionRight right)
+ {
+ return GetPayOff(underlyingPrice, strike, right) == 0;
+ }
+
+ ///
+ /// Whether the option is at the money: the strike equals the underlying price
+ ///
+ /// The price of the underlying
+ /// The strike price of the option
+ /// The option right of the option, call or put
+ /// True if the strike equals the underlying price
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsAtTheMoney(double underlyingPrice, double strike, OptionRight right)
+ {
+ return GetPayOff(underlyingPrice, strike, right) == 0;
+ }
+
+ ///
+ /// Whether the option is out of the money: a call with the strike above the underlying price, a put with the strike below it
+ ///
+ /// The price of the underlying
+ /// The strike price of the option
+ /// The option right of the option, call or put
+ /// True if the option has no intrinsic value and is not at the money
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsOutOfTheMoney(decimal underlyingPrice, decimal strike, OptionRight right)
+ {
+ return GetPayOff(underlyingPrice, strike, right) < 0;
+ }
+
+ ///
+ /// Whether the option is out of the money: a call with the strike above the underlying price, a put with the strike below it
+ ///
+ /// The price of the underlying
+ /// The strike price of the option
+ /// The option right of the option, call or put
+ /// True if the option has no intrinsic value and is not at the money
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsOutOfTheMoney(double underlyingPrice, double strike, OptionRight right)
+ {
+ return GetPayOff(underlyingPrice, strike, right) < 0;
+ }
}
}
diff --git a/Tests/Common/Data/Market/DataDictionaryTests.cs b/Tests/Common/Data/Market/DataDictionaryTests.cs
new file mode 100644
index 000000000000..f47466f55135
--- /dev/null
+++ b/Tests/Common/Data/Market/DataDictionaryTests.cs
@@ -0,0 +1,49 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using QuantConnect.Data.Market;
+
+namespace QuantConnect.Tests.Common.Data.Market
+{
+ [TestFixture]
+ public class DataDictionaryTests
+ {
+ [Test]
+ public void IndexerSetterRefreshesTheCachedKeysAndValues()
+ {
+ var dictionary = new TradeBars(new DateTime(2016, 2, 26));
+ dictionary.Add(Symbols.SPY, new TradeBar { Symbol = Symbols.SPY, Close = 1 });
+
+ // read every cached view, then add through the indexer like the option chains do
+ Assert.AreEqual(1, dictionary.Keys.Count);
+ Assert.AreEqual(1, dictionary.Values.Count);
+ Assert.AreEqual(1, dictionary.Count());
+
+ dictionary[Symbols.AAPL] = new TradeBar { Symbol = Symbols.AAPL, Close = 2 };
+
+ CollectionAssert.AreEquivalent(new[] { Symbols.SPY, Symbols.AAPL }, dictionary.Keys);
+ CollectionAssert.AreEquivalent(new[] { 1m, 2m }, dictionary.Values.Select(x => x.Close));
+ Assert.AreEqual(2, dictionary.Count());
+
+ // replacing an entry refreshes the values too
+ dictionary[Symbols.AAPL] = new TradeBar { Symbol = Symbols.AAPL, Close = 3 };
+
+ CollectionAssert.AreEquivalent(new[] { 1m, 3m }, dictionary.Values.Select(x => x.Close));
+ }
+ }
+}
diff --git a/Tests/Common/Data/Market/OptionChainTests.cs b/Tests/Common/Data/Market/OptionChainTests.cs
index f117c4dbd962..a2456cc7bab2 100644
--- a/Tests/Common/Data/Market/OptionChainTests.cs
+++ b/Tests/Common/Data/Market/OptionChainTests.cs
@@ -62,8 +62,33 @@ private static IEnumerable FilterCases()
yield return Case("Expiration(TimeSpan)", u => u.Expiration(TimeSpan.FromDays(30), TimeSpan.FromDays(200)),
c => c.Expiration(TimeSpan.FromDays(30), TimeSpan.FromDays(200)));
yield return Case("Expiration(500, 600)", u => u.Expiration(500, 600), c => c.Expiration(500, 600), empty: true);
+ yield return Case("Expiration(dates)", u => u.Expiration([Expiries[1], Expiries[3]]), c => c.Expiration([Expiries[1], Expiries[3]]));
+ yield return Case("Expiration(date, time of day)", u => u.Expiration([Expiries[1].AddHours(10)]), c => c.Expiration([Expiries[1].AddHours(10)]));
+ yield return Case("Expiration(unlisted dates)", u => u.Expiration([Date, Date.AddDays(1)]), c => c.Expiration([Date, Date.AddDays(1)]), empty: true);
+ yield return Case("Expiration(no dates)", u => u.Expiration([]), c => c.Expiration([]), empty: true);
+ yield return Case("ExpiringAfter", u => u.ExpiringAfter(Expiries[1]), c => c.ExpiringAfter(Expiries[1]));
+ yield return Case("ExpiringBefore", u => u.ExpiringBefore(Expiries[1].AddHours(10)), c => c.ExpiringBefore(Expiries[1].AddHours(10)));
+ yield return Case("ExpiringAfter.ExpiringBefore", u => u.ExpiringAfter(Expiries[0]).ExpiringBefore(Expiries[3]), c => c.ExpiringAfter(Expiries[0]).ExpiringBefore(Expiries[3]));
+ yield return Case("ExpiringAfter(last)", u => u.ExpiringAfter(Expiries[3]), c => c.ExpiringAfter(Expiries[3]), empty: true);
+ yield return Case("FarthestExpiration", u => u.FarthestExpiration(), c => c.FarthestExpiration());
+ yield return Case("StandardsOnly.FarthestExpiration", u => u.StandardsOnly().FarthestExpiration(), c => c.StandardsOnly().FarthestExpiration());
+ yield return Case("Strikes(100, 105)", u => u.Strikes([100m, 105m]), c => c.Strikes([100m, 105m]));
+ yield return Case("Strikes(101)", u => u.Strikes([101m]), c => c.Strikes([101m]), empty: true);
+ yield return Case("StrikesAbove", u => u.StrikesAbove(100m), c => c.StrikesAbove(100m));
+ yield return Case("StrikesBelow", u => u.StrikesBelow(100m), c => c.StrikesBelow(100m));
+ 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("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());
+ yield return Case("OTM.CallsOnly", u => u.OTM().CallsOnly(), c => c.OTM().CallsOnly());
+ yield return Case("InTheMoney", u => u.InTheMoney(), c => c.InTheMoney());
+ yield return Case("ITM.PutsOnly.Expiration(0, 10)", u => u.ITM().PutsOnly().Expiration(0, 10), c => c.ITM().PutsOnly().Expiration(0, 10));
+ yield return Case("AtTheMoney", u => u.AtTheMoney(), c => c.AtTheMoney());
+ yield return Case("AtTheMoney(0)", u => u.AtTheMoney(0), c => c.AtTheMoney(0), empty: true);
+ yield return Case("AtTheMoney(1)", u => u.AtTheMoney(1m), c => c.AtTheMoney(1m));
+ yield return Case("Expiration(0, 10).ATM(2.5)", u => u.Expiration(0, 10).ATM(2.5m), c => c.Expiration(0, 10).ATM(2.5m));
yield return Case("StandardsOnly", u => u.StandardsOnly(), c => c.StandardsOnly());
yield return Case("WeeklysOnly", u => u.WeeklysOnly(), c => c.WeeklysOnly());
yield return Case("FrontMonth", u => u.FrontMonth(), c => c.FrontMonth());
@@ -247,6 +272,12 @@ def filter_chain(chain):
def where_chain(chain):
return chain.where(lambda contract: contract.right == OptionRight.PUT and contract.strike > 100)
+
+def sets(chain):
+ return chain.strikes([100, 105]).expiration([datetime(2016, 3, 18), datetime(2016, 6, 17)])
+
+def bounds(chain):
+ return chain.strikes_above(95).strikes_below(105).expiring_after(datetime(2016, 3, 4)).expiring_before(datetime(2016, 6, 17)).farthest_expiration()
");
using var pyChain = chain.ToPython();
@@ -255,6 +286,19 @@ def where_chain(chain):
using var where = module.GetAttr("where_chain").Invoke(pyChain);
CollectionAssert.AreEqual(expectedWhere, where.As().Select(x => x.Symbol).ToList());
+
+ // strike and date lists convert to the C# collections
+ var expectedSets = chain.Strikes([100m, 105m]).Expiration([Expiries[1], Expiries[3]]).Select(x => x.Symbol).ToList();
+ Assert.AreEqual(8, expectedSets.Count);
+ using var sets = module.GetAttr("sets").Invoke(pyChain);
+ CollectionAssert.AreEqual(expectedSets, sets.As().Select(x => x.Symbol).ToList());
+
+ var expectedBounds = chain.StrikesAbove(95m).StrikesBelow(105m).ExpiringAfter(Expiries[0]).ExpiringBefore(Expiries[3]).FarthestExpiration()
+ .Select(x => x.Symbol).ToList();
+ Assert.AreEqual(6, expectedBounds.Count);
+ Assert.IsTrue(expectedBounds.All(x => x.ID.Date == Expiries[2]));
+ using var bounds = module.GetAttr("bounds").Invoke(pyChain);
+ CollectionAssert.AreEqual(expectedBounds, bounds.As().Select(x => x.Symbol).ToList());
}
}
@@ -304,6 +348,150 @@ public void StrategyFiltersValidateArgumentsLikeTheUniverseFilters()
}
}
+ // By default the strikes on either side of the price that are within 2% of it: one when the price is a strike, one when the
+ // other side is too far (97.5 from 95.5), none when both are (105 and 110 from 107.5) or the price is outside the strikes
+ [TestCase(100, null, new[] { 100.0 })]
+ [TestCase(101, null, new[] { 100.0, 102.5 })]
+ [TestCase(103.75, null, new[] { 102.5, 105.0 })]
+ [TestCase(95.5, null, new[] { 95.0 })]
+ [TestCase(107.5, null, new double[0])]
+ [TestCase(110, null, new[] { 110.0 })]
+ [TestCase(120, null, new double[0])]
+ [TestCase(80, null, new double[0])]
+ // A zero distance requires a strike equal to the price
+ [TestCase(100, 0, new[] { 100.0 })]
+ [TestCase(101, 0, new double[0])]
+ // Otherwise every strike within the distance
+ [TestCase(101, 1, new[] { 100.0 })]
+ [TestCase(101, 0.5, new double[0])]
+ [TestCase(103.75, 1.25, new[] { 102.5, 105.0 })]
+ [TestCase(101.25, 1.25, new[] { 100.0, 102.5 })]
+ [TestCase(101.25, 1, new double[0])]
+ [TestCase(101, 5, new[] { 97.5, 100.0, 102.5, 105.0 })]
+ public void MoneynessFiltersSplitTheStrikesAroundTheUnderlyingPrice(double underlyingPrice, double? maxStrikeDistance, double[] atmStrikes)
+ {
+ var price = (decimal)underlyingPrice;
+ var (data, _) = CreateUniverseData(Date, price, Expiries, Strikes);
+ var chain = new OptionChain(Canonical, Date, data, _symbolProperties);
+ Assert.AreEqual(price, chain.Underlying.Price);
+
+ var otm = chain.OutOfTheMoney();
+ var itm = chain.InTheMoney();
+ Assert.IsNotEmpty(otm);
+ Assert.IsNotEmpty(itm);
+ Assert.IsTrue(otm.All(x => x.Right == OptionRight.Call ? x.Strike > price : x.Strike < price));
+ Assert.IsTrue(itm.All(x => x.Right == OptionRight.Call ? x.Strike < price : x.Strike > price));
+ // a strike equal to the price is neither out nor in the money
+ Assert.AreEqual(chain.Count, otm.Count + itm.Count + chain.Strikes([price]).Count);
+
+ var atm = chain.AtTheMoney((decimal?)maxStrikeDistance);
+ Assert.AreEqual(atmStrikes.Length * 2 * Expiries.Length, atm.Count);
+ CollectionAssert.AreEquivalent(atmStrikes.Select(x => (decimal)x), atm.Select(x => x.Strike).Distinct());
+ Assert.Throws(() => chain.AtTheMoney(-1m));
+ Assert.Throws(() => CreateUniverse().AtTheMoney(-1m));
+ }
+
+ [Test]
+ public void FiltersWorkOnFutureOptionChains()
+ {
+ // March 2020 ES options on the March 2020 future, the universe rows carry the future price
+ var future = Symbol.CreateFuture("ES", QuantConnect.Market.CME, new DateTime(2020, 3, 20));
+ var canonical = Symbol.CreateCanonicalOption(future);
+ var date = new DateTime(2020, 1, 3);
+ var contracts = new List<(Symbol, decimal, decimal, Greeks)>();
+ foreach (var strike in new[] { 3200m, 3210m, 3220m, 3230m, 3240m })
+ {
+ foreach (var right in new[] { OptionRight.Call, OptionRight.Put })
+ {
+ var symbol = Symbol.CreateOption(future, QuantConnect.Market.CME, OptionStyle.American, right, strike, future.ID.Date);
+ contracts.Add((symbol, 100, 0.15m, new Greeks(0.5m, 0.01m, 5, -0.5m, 1, 0)));
+ }
+ }
+ var (data, underlying) = CreateUniverseData(canonical, date, 3223.75m, contracts);
+ var symbolProperties = SymbolPropertiesDatabase.FromDataFolder().GetSymbolProperties(QuantConnect.Market.CME, canonical, SecurityType.FutureOption, Currencies.USD);
+ var chain = new OptionChain(canonical, date, data, symbolProperties);
+ Assert.AreEqual(SecurityType.FutureOption, chain.Symbol.SecurityType);
+ Assert.AreEqual(10, chain.Count);
+ Assert.AreEqual(3223.75m, chain.Underlying.Price);
+
+ // moneyness against the future price
+ CollectionAssert.AreEquivalent(new[] { 3230m, 3240m }, chain.OutOfTheMoney().CallsOnly().Select(x => x.Strike));
+ CollectionAssert.AreEquivalent(new[] { 3200m, 3210m, 3220m }, chain.OutOfTheMoney().PutsOnly().Select(x => x.Strike));
+ Assert.AreEqual(0, chain.AtTheMoney(0).Count);
+ // the strikes on either side of 3223.75, and within 5 points only 3220
+ CollectionAssert.AreEquivalent(new[] { 3220m, 3220m, 3230m, 3230m }, chain.AtTheMoney().Select(x => x.Strike));
+ CollectionAssert.AreEquivalent(new[] { 3220m, 3220m }, chain.AtTheMoney(5m).Select(x => x.Strike));
+
+ // 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);
+ 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));
+
+ // and match the universe filters of a future option over the same rows
+ var universe = new OptionFilterUniverse(CreateOption(canonical), data, underlying);
+ universe.Refresh(data, underlying, date);
+ var expected = universe.Strikes(-1, 1).OutOfTheMoney().ExpiringBefore(new DateTime(2020, 4, 1)).ToList().Select(x => x.Symbol.Value).ToList();
+ Assert.IsNotEmpty(expected);
+ CollectionAssert.AreEquivalent(expected, chain.Strikes(-1, 1).OutOfTheMoney().ExpiringBefore(new DateTime(2020, 4, 1)).Select(x => x.Symbol.Value));
+ }
+
+ [Test]
+ public void ContractsCountTheDaysToTheirExpiration()
+ {
+ var chain = CreateChain();
+ // universe rows are stamped at the end of their day, so the contracts count from the next date
+ var reference = chain.First().Time.Date;
+ Assert.AreEqual(Date.AddDays(1), reference);
+ var expected = Expiries.Select(expiry => (expiry - reference).Days).ToList();
+ CollectionAssert.AreEquivalent(expected, chain.Select(x => x.DaysToExpiry).Distinct());
+ Assert.AreEqual(expected[0], chain.FrontMonth().First().DaysToExpiry);
+ Assert.AreEqual(expected[3], chain.FarthestExpiration().First().DaysToExpiry);
+ }
+
+ [Test]
+ public void DefaultAtTheMoneyStrikeDistanceIsConfigurable()
+ {
+ var chain = CreateChain();
+ var original = OptionFilterUniverse.DefaultAtTheMoneyStrikeDistance;
+ try
+ {
+ // at 101 the strikes on either side are 1 and 1.5 away: within 2%, not within 0.5%, only the lower within 1.2%
+ CollectionAssert.AreEquivalent(new[] { 100m, 102.5m }, chain.AtTheMoney().Select(x => x.Strike).Distinct());
+ OptionFilterUniverse.DefaultAtTheMoneyStrikeDistance = 0.005m;
+ Assert.AreEqual(0, chain.AtTheMoney().Count);
+ Assert.AreEqual(0, CreateUniverse().AtTheMoney().Count);
+ OptionFilterUniverse.DefaultAtTheMoneyStrikeDistance = 0.012m;
+ CollectionAssert.AreEquivalent(new[] { 100m }, chain.AtTheMoney().Select(x => x.Strike).Distinct());
+ CollectionAssert.AreEquivalent(new[] { 100m }, CreateUniverse().AtTheMoney().Select(x => x.Symbol.ID.StrikePrice).Distinct());
+ Assert.Throws(() => OptionFilterUniverse.DefaultAtTheMoneyStrikeDistance = -0.01m);
+ }
+ finally
+ {
+ OptionFilterUniverse.DefaultAtTheMoneyStrikeDistance = original;
+ }
+ }
+
+ [Test]
+ public void MoneynessFiltersSelectNothingWithoutUnderlyingPrice()
+ {
+ var contracts = _data.Select(x => new OptionUniverse(x) { Underlying = null }).ToList();
+ var chain = new OptionChain(Canonical, Date, contracts, _symbolProperties);
+ Assert.AreEqual(0, chain.Underlying.Price);
+
+ Assert.AreEqual(0, chain.OutOfTheMoney().Count);
+ Assert.AreEqual(0, chain.InTheMoney().Count);
+ Assert.AreEqual(0, chain.AtTheMoney(100m).Count);
+ Func[] filters = [u => u.OutOfTheMoney(), u => u.InTheMoney(), u => u.AtTheMoney(100m)];
+ foreach (var filter in filters)
+ {
+ var universe = new OptionFilterUniverse(_option);
+ universe.Refresh(contracts, null, Date);
+ Assert.AreEqual(0, filter(universe).Count);
+ }
+ }
+
[Test]
public void TypeFiltersApplyToTheChainContractsInAnyOrder()
{
@@ -372,12 +560,13 @@ private OptionChain CreateChain()
return new OptionChain(Canonical, Date, _data, _symbolProperties);
}
- private static Option CreateOption()
+ private static Option CreateOption(Symbol canonical = null)
{
- var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(Canonical.ID.Market, Canonical, Canonical.SecurityType);
+ canonical ??= Canonical;
+ var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(canonical.ID.Market, canonical, canonical.SecurityType);
return new Option(
exchangeHours,
- new SubscriptionDataConfig(typeof(TradeBar), Canonical, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, false, false),
+ new SubscriptionDataConfig(typeof(TradeBar), canonical, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, false, false),
new Cash(Currencies.USD, 0, 1m),
new OptionSymbolProperties(SymbolProperties.GetDefault(Currencies.USD)),
ErrorCurrencyConverter.Instance,
@@ -468,10 +657,14 @@ internal static (List contracts, BaseData underlying) CreateUniv
{
Assert.AreEqual(rows[j].symbol, data[j].Symbol);
Assert.AreEqual(rows[j].openInterest, data[j].OpenInterest);
- Assert.AreEqual(rows[j].impliedVolatility, data[j].ImpliedVolatility);
- Assert.AreEqual(rows[j].greeks.Delta, data[j].Greeks.Delta);
- Assert.AreEqual(rows[j].greeks.Theta, data[j].Greeks.Theta);
- Assert.AreEqual(rows[j].greeks.Rho, data[j].Greeks.Rho);
+ // future option universe files carry no implied volatility or greeks
+ if (canonical.SecurityType != SecurityType.FutureOption)
+ {
+ Assert.AreEqual(rows[j].impliedVolatility, data[j].ImpliedVolatility);
+ Assert.AreEqual(rows[j].greeks.Delta, data[j].Greeks.Delta);
+ Assert.AreEqual(rows[j].greeks.Theta, data[j].Greeks.Theta);
+ Assert.AreEqual(rows[j].greeks.Rho, data[j].Greeks.Rho);
+ }
}
Assert.AreEqual(spot ?? 0, underlying?.Price ?? 0);
diff --git a/Tests/Common/Securities/FutureFilterTests.cs b/Tests/Common/Securities/FutureFilterTests.cs
index 48a7f4c288fe..c99747347aef 100644
--- a/Tests/Common/Securities/FutureFilterTests.cs
+++ b/Tests/Common/Securities/FutureFilterTests.cs
@@ -15,6 +15,7 @@
*/
using System;
+using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Python.Runtime;
@@ -376,6 +377,29 @@ public void FiltersExpirationCycles()
Assert.AreEqual(5, filtered.Count);
}
+ [Test]
+ public void FiltersExpirationSetsBoundsAndFarthestExpiration()
+ {
+ var time = new DateTime(2013, 10, 7);
+ var expiries = new[]
+ {
+ new DateTime(2013, 12, 20), new DateTime(2014, 3, 21), new DateTime(2014, 6, 20), new DateTime(2014, 9, 19), new DateTime(2014, 12, 19)
+ };
+ var data = expiries.Select(expiry => new FutureUniverse { Symbol = Symbol.CreateFuture("ES", Market.CME, expiry) }).ToList();
+ FutureFilterUniverse Universe() => new(data, time);
+ static IEnumerable Expiries(FutureFilterUniverse universe) => universe.Select(x => x.Symbol.ID.Date);
+
+ // sets ignore the time of day, bounds exclude the date itself
+ CollectionAssert.AreEqual(new[] { expiries[1], expiries[3] }, Expiries(Universe().Expiration([expiries[1], expiries[3].AddHours(10)])));
+ Assert.AreEqual(0, Universe().Expiration([]).Count);
+ CollectionAssert.AreEqual(expiries.Skip(1), Expiries(Universe().ExpiringAfter(expiries[0])));
+ CollectionAssert.AreEqual(expiries.Take(2), Expiries(Universe().ExpiringBefore(expiries[2])));
+ CollectionAssert.AreEqual(new[] { expiries[2] }, Expiries(Universe().ExpiringAfter(expiries[1]).ExpiringBefore(expiries[3])));
+ CollectionAssert.AreEqual(new[] { expiries[4] }, Expiries(Universe().FarthestExpiration()));
+ CollectionAssert.AreEqual(new[] { expiries[0] }, Expiries(Universe().FrontMonth()));
+ Assert.AreEqual(0, new FutureFilterUniverse(new List(), time).FarthestExpiration().Count);
+ }
+
[Test]
public void FilterTypeDoesNotBreakOnMissingExpiryFunction()
{
diff --git a/Tests/Common/Util/OptionPayoffTests.cs b/Tests/Common/Util/OptionPayoffTests.cs
new file mode 100644
index 000000000000..dfe05d1245aa
--- /dev/null
+++ b/Tests/Common/Util/OptionPayoffTests.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 NUnit.Framework;
+using QuantConnect.Util;
+
+namespace QuantConnect.Tests.Common.Util
+{
+ [TestFixture]
+ public class OptionPayoffTests
+ {
+ // A call is in the money below the underlying price, a put above it, both are at the money at the price
+ [TestCase(OptionRight.Call, 100, 90, true, false, false)]
+ [TestCase(OptionRight.Call, 100, 100, false, true, false)]
+ [TestCase(OptionRight.Call, 100, 110, false, false, true)]
+ [TestCase(OptionRight.Put, 100, 90, false, false, true)]
+ [TestCase(OptionRight.Put, 100, 100, false, true, false)]
+ [TestCase(OptionRight.Put, 100, 110, true, false, false)]
+ public void ClassifiesMoneyness(OptionRight right, double underlyingPrice, double strike, bool inTheMoney, bool atTheMoney, bool outOfTheMoney)
+ {
+ Assert.AreEqual(inTheMoney, OptionPayoff.IsInTheMoney((decimal)underlyingPrice, (decimal)strike, right));
+ Assert.AreEqual(atTheMoney, OptionPayoff.IsAtTheMoney((decimal)underlyingPrice, (decimal)strike, right));
+ Assert.AreEqual(outOfTheMoney, OptionPayoff.IsOutOfTheMoney((decimal)underlyingPrice, (decimal)strike, right));
+
+ Assert.AreEqual(inTheMoney, OptionPayoff.IsInTheMoney(underlyingPrice, strike, right));
+ Assert.AreEqual(atTheMoney, OptionPayoff.IsAtTheMoney(underlyingPrice, strike, right));
+ Assert.AreEqual(outOfTheMoney, OptionPayoff.IsOutOfTheMoney(underlyingPrice, strike, right));
+
+ // in the money contracts are the ones with intrinsic value
+ Assert.AreEqual(inTheMoney, OptionPayoff.GetIntrinsicValue((decimal)underlyingPrice, (decimal)strike, right) > 0);
+ }
+ }
+}