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); + } + } +}