From 1b867d263e4068f60daa364940cd6e6be3e4ae12 Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Mon, 21 Sep 2020 17:01:26 -0300 Subject: [PATCH 1/5] OptionChain and OptionContract improvements - QCAlgorithm.AddUniverse will return the added Universe instance. - Adding new OptionChainedUniverseSelectionModel will monitor a Universe changes and will spwan new OptionChainUniverse from it's selections. Adding regression test Py/C#. - Adding new OptionContractUniverse that will own option contracts and their underlying symbol. Adding regression test - Fix double notification for security changes, bug seen in updated UniverseSelectionRegressionAlgorithm - Remove UniverseSelection special handling for Option and Future chains - Fix DataManager not removing SubscriptionDataConfigs for Subscriptions which finished before being removed from the universe - Refactor detection of user added Universe so that they do not get removed after calling the UniverseSelectionModel --- ...ptionContractExpiresRegressionAlgorithm.cs | 165 +++++++++++++ ...ContractFromUniverseRegressionAlgorithm.cs | 216 ++++++++++++++++++ ...eOptionUniverseChainRegressionAlgorithm.cs | 185 +++++++++++++++ .../QuantConnect.Algorithm.CSharp.csproj | 3 + .../UniverseSelectionRegressionAlgorithm.cs | 38 +-- .../Selection/OptionUniverseSelectionModel.cs | 97 +------- ...eOptionUniverseChainRegressionAlgorithm.py | 99 ++++++++ .../QuantConnect.Algorithm.Python.csproj | 1 + Algorithm/QCAlgorithm.Framework.cs | 7 +- Algorithm/QCAlgorithm.Python.cs | 78 ++++--- Algorithm/QCAlgorithm.Universe.cs | 93 +++++--- Algorithm/QCAlgorithm.cs | 34 ++- Algorithm/QuantConnect.Algorithm.csproj | 2 + .../OptionChainedUniverseSelectionModel.cs | 83 +++++++ Algorithm/Selection/OptionContractUniverse.cs | 97 ++++++++ .../FineFundamentalFilteredUniverse.cs | 2 + .../UniverseSelection/FuturesChainUniverse.cs | 28 ++- .../UniverseSelection/OptionChainUniverse.cs | 28 +-- Common/Data/UniverseSelection/Universe.cs | 38 ++- .../UniverseSelection/UniverseSettings.cs | 13 ++ .../UniverseSelection/UserDefinedUniverse.cs | 5 +- Common/Extensions.cs | 68 +++++- .../Securities/Option/OptionFilterUniverse.cs | 12 + Engine/AlgorithmManager.cs | 11 +- Engine/DataFeeds/DataManager.cs | 13 +- Engine/DataFeeds/SubscriptionSynchronizer.cs | 3 +- Engine/DataFeeds/UniverseSelection.cs | 43 +--- Tests/Engine/DataFeeds/MockDataFeed.cs | 7 +- 28 files changed, 1217 insertions(+), 252 deletions(-) create mode 100644 Algorithm.CSharp/AddOptionContractExpiresRegressionAlgorithm.cs create mode 100644 Algorithm.CSharp/AddOptionContractFromUniverseRegressionAlgorithm.cs create mode 100644 Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs create mode 100644 Algorithm.Python/CoarseFineOptionUniverseChainRegressionAlgorithm.py create mode 100644 Algorithm/Selection/OptionChainedUniverseSelectionModel.cs create mode 100644 Algorithm/Selection/OptionContractUniverse.cs diff --git a/Algorithm.CSharp/AddOptionContractExpiresRegressionAlgorithm.cs b/Algorithm.CSharp/AddOptionContractExpiresRegressionAlgorithm.cs new file mode 100644 index 000000000000..b2f83549d5e4 --- /dev/null +++ b/Algorithm.CSharp/AddOptionContractExpiresRegressionAlgorithm.cs @@ -0,0 +1,165 @@ +/* + * 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 QuantConnect.Data; +using QuantConnect.Interfaces; +using System.Collections.Generic; +using QuantConnect.Securities.Option; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// We add an option contract using and place a trade and wait till it expires + /// later will liquidate the resulting equity position and assert both option and underlying get removed + /// + public class AddOptionContractExpiresRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private DateTime _expiration = new DateTime(2014, 06, 21); + private Option _option; + private Symbol _twx; + private bool _traded; + + public override void Initialize() + { + SetStartDate(2014, 06, 05); + SetEndDate(2014, 06, 30); + + _twx = QuantConnect.Symbol.Create("TWX", SecurityType.Equity, Market.USA); + + AddUniverse("my-daily-universe-name", time => new List { "AAPL" }); + } + + public override void OnData(Slice data) + { + if (_option == null) + { + var option = OptionChainProvider.GetOptionContractList(_twx, Time) + .OrderBy(symbol => symbol.ID.Symbol) + .FirstOrDefault(optionContract => optionContract.ID.Date == _expiration + && optionContract.ID.OptionRight == OptionRight.Call + && optionContract.ID.OptionStyle == OptionStyle.American); + if (option != null) + { + _option = AddOptionContract(option); + } + } + + if (!Portfolio.Invested && _option != null && Securities[_option.Symbol].Price != 0 && !_traded) + { + _traded = true; + Buy(_option.Symbol, 1); + + foreach (var symbol in new [] { _option.Symbol, _option.Symbol.Underlying }) + { + var config = SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(symbol).ToList(); + + if (!config.Any()) + { + throw new Exception($"Was expecting configurations for {symbol}"); + } + if (config.Any(dataConfig => dataConfig.DataNormalizationMode != DataNormalizationMode.Raw)) + { + throw new Exception($"Was expecting DataNormalizationMode.Raw configurations for {symbol}"); + } + } + } + + if (Time.Date > _expiration) + { + if (SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(_option.Symbol).Any()) + { + throw new Exception($"Unexpected configurations for {_option.Symbol} after it has been delisted"); + } + + if (Securities[_twx].Invested) + { + if (!SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(_twx).Any()) + { + throw new Exception($"Was expecting configurations for {_twx}"); + } + + // first we liquidate the option exercised position + Liquidate(_twx); + } + } + else if (Time.Date > _expiration && !Securities[_twx].Invested) + { + if (SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(_twx).Any()) + { + throw new Exception($"Unexpected configurations for {_twx} after it has been liquidated"); + } + } + } + + /// + /// 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 Language[] Languages { get; } = { Language.CSharp }; + + /// + /// 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 Trades", "3"}, + {"Average Win", "2.94%"}, + {"Average Loss", "-2.98%"}, + {"Compounding Annual Return", "-1.749%"}, + {"Drawdown", "0.200%"}, + {"Expectancy", "-0.006"}, + {"Net Profit", "-0.124%"}, + {"Sharpe Ratio", "3.589"}, + {"Probabilistic Sharpe Ratio", "90.473%"}, + {"Loss Rate", "50%"}, + {"Win Rate", "50%"}, + {"Profit-Loss Ratio", "0.99"}, + {"Alpha", "0.007"}, + {"Beta", "-0.001"}, + {"Annual Standard Deviation", "0.002"}, + {"Annual Variance", "0"}, + {"Information Ratio", "-2.913"}, + {"Tracking Error", "0.057"}, + {"Treynor Ratio", "-4.947"}, + {"Total Fees", "$2.00"}, + {"Fitness Score", "0.001"}, + {"Kelly Criterion Estimate", "0"}, + {"Kelly Criterion Probability Value", "0"}, + {"Sortino Ratio", "-0.946"}, + {"Return Over Maximum Drawdown", "-10.498"}, + {"Portfolio Turnover", "0.007"}, + {"Total Insights Generated", "0"}, + {"Total Insights Closed", "0"}, + {"Total Insights Analysis Completed", "0"}, + {"Long Insight Count", "0"}, + {"Short Insight Count", "0"}, + {"Long/Short Ratio", "100%"}, + {"Estimated Monthly Alpha Value", "$0"}, + {"Total Accumulated Estimated Alpha Value", "$0"}, + {"Mean Population Estimated Insight Value", "$0"}, + {"Mean Population Direction", "0%"}, + {"Mean Population Magnitude", "0%"}, + {"Rolling Averaged Population Direction", "0%"}, + {"Rolling Averaged Population Magnitude", "0%"}, + {"OrderListHash", "1472204120"} + }; + } +} diff --git a/Algorithm.CSharp/AddOptionContractFromUniverseRegressionAlgorithm.cs b/Algorithm.CSharp/AddOptionContractFromUniverseRegressionAlgorithm.cs new file mode 100644 index 000000000000..2839f06a0112 --- /dev/null +++ b/Algorithm.CSharp/AddOptionContractFromUniverseRegressionAlgorithm.cs @@ -0,0 +1,216 @@ +/* + * 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.UniverseSelection; +using QuantConnect.Interfaces; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// We add an option contract using and place a trade, the underlying + /// gets deselected from the universe selection but should still be present since we manually added the option contract. + /// Later we call and expect both option and underlying to be removed. + /// + public class AddOptionContractFromUniverseRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private DateTime _expiration = new DateTime(2014, 06, 21); + private SecurityChanges _securityChanges = SecurityChanges.None; + private Symbol _option; + private Symbol _aapl; + private Symbol _twx; + private bool _traded; + + public override void Initialize() + { + _twx = QuantConnect.Symbol.Create("TWX", SecurityType.Equity, Market.USA); + _aapl = QuantConnect.Symbol.Create("AAPL", SecurityType.Equity, Market.USA); + UniverseSettings.Resolution = Resolution.Minute; + UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw; + + SetStartDate(2014, 06, 05); + SetEndDate(2014, 06, 09); + + AddUniverse(enumerable => new[] { Time.Date <= new DateTime(2014, 6, 5) ? _twx : _aapl }, + enumerable => new[] { Time.Date <= new DateTime(2014, 6, 5) ? _twx : _aapl }); + } + + public override void OnData(Slice data) + { + if (!Portfolio.Invested && _option != null && Securities[_option].Price != 0 && !_traded) + { + _traded = true; + Buy(_option, 1); + } + + if (Time.Date > new DateTime(2014, 6, 5)) + { + if (Time < new DateTime(2014, 6, 6, 14, 0, 0)) + { + var configs = SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(_twx); + // assert underlying still there after the universe selection removed it, still used by the manually added option contract + if (!configs.Any()) + { + throw new Exception($"Was expecting configurations for {_twx}" + + $" even after it has been deselected from coarse universe because we still have the option contract."); + } + } + else if (Time == new DateTime(2014, 6, 6, 14, 0, 0)) + { + // liquidate & remove the option + RemoveOptionContract(_option); + } + // assert underlying was finally removed + else if(Time > new DateTime(2014, 6, 6, 14, 0, 0)) + { + foreach (var symbol in new[] { _option, _option.Underlying }) + { + var configs = SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(symbol); + if (configs.Any()) + { + throw new Exception($"Unexpected configuration for {symbol} after it has been deselected from coarse universe and option contract is removed."); + } + } + } + } + } + + public override void OnSecuritiesChanged(SecurityChanges changes) + { + if (_securityChanges.RemovedSecurities.Intersect(changes.RemovedSecurities).Any()) + { + throw new Exception($"SecurityChanges.RemovedSecurities intersect {changes.RemovedSecurities}. We expect no duplicate!"); + } + if (_securityChanges.AddedSecurities.Intersect(changes.AddedSecurities).Any()) + { + throw new Exception($"SecurityChanges.AddedSecurities intersect {changes.RemovedSecurities}. We expect no duplicate!"); + } + // keep track of all removed and added securities + _securityChanges += changes; + + if (changes.AddedSecurities.Any(security => security.Symbol.SecurityType == SecurityType.Option)) + { + return; + } + + foreach (var addedSecurity in changes.AddedSecurities) + { + var option = OptionChainProvider.GetOptionContractList(addedSecurity.Symbol, Time) + .OrderBy(symbol => symbol.ID.Symbol) + .First(optionContract => optionContract.ID.Date == _expiration + && optionContract.ID.OptionRight == OptionRight.Call + && optionContract.ID.OptionStyle == OptionStyle.American); + AddOptionContract(option); + + foreach (var symbol in new[] { option, option.Underlying }) + { + var config = SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(symbol).ToList(); + + if (!config.Any()) + { + throw new Exception($"Was expecting configurations for {symbol}"); + } + if (config.Any(dataConfig => dataConfig.DataNormalizationMode != DataNormalizationMode.Raw)) + { + throw new Exception($"Was expecting DataNormalizationMode.Raw configurations for {symbol}"); + } + } + + // just keep the first we got + if (_option == null) + { + _option = option; + } + } + } + + public override void OnEndOfAlgorithm() + { + if (SubscriptionManager.Subscriptions.Any(dataConfig => dataConfig.Symbol == _twx || dataConfig.Symbol.Underlying == _twx)) + { + throw new Exception($"Was NOT expecting any configurations for {_twx} or it's options, since we removed the contract"); + } + + if (SubscriptionManager.Subscriptions.All(dataConfig => dataConfig.Symbol != _aapl)) + { + throw new Exception($"Was expecting configurations for {_aapl}"); + } + if (SubscriptionManager.Subscriptions.All(dataConfig => dataConfig.Symbol.Underlying != _aapl)) + { + throw new Exception($"Was expecting options configurations for {_aapl}"); + } + } + + /// + /// 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 Language[] Languages { get; } = { Language.CSharp }; + + /// + /// 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 Trades", "2"}, + {"Average Win", "0%"}, + {"Average Loss", "-0.23%"}, + {"Compounding Annual Return", "-15.596%"}, + {"Drawdown", "0.200%"}, + {"Expectancy", "-1"}, + {"Net Profit", "-0.232%"}, + {"Sharpe Ratio", "-7.739"}, + {"Probabilistic Sharpe Ratio", "1.216%"}, + {"Loss Rate", "100%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0.027"}, + {"Beta", "-0.174"}, + {"Annual Standard Deviation", "0.006"}, + {"Annual Variance", "0"}, + {"Information Ratio", "-11.586"}, + {"Tracking Error", "0.042"}, + {"Treynor Ratio", "0.286"}, + {"Total Fees", "$2.00"}, + {"Fitness Score", "0"}, + {"Kelly Criterion Estimate", "0"}, + {"Kelly Criterion Probability Value", "0"}, + {"Sortino Ratio", "-19.883"}, + {"Return Over Maximum Drawdown", "-67.224"}, + {"Portfolio Turnover", "0.014"}, + {"Total Insights Generated", "0"}, + {"Total Insights Closed", "0"}, + {"Total Insights Analysis Completed", "0"}, + {"Long Insight Count", "0"}, + {"Short Insight Count", "0"}, + {"Long/Short Ratio", "100%"}, + {"Estimated Monthly Alpha Value", "$0"}, + {"Total Accumulated Estimated Alpha Value", "$0"}, + {"Mean Population Estimated Insight Value", "$0"}, + {"Mean Population Direction", "0%"}, + {"Mean Population Magnitude", "0%"}, + {"Rolling Averaged Population Direction", "0%"}, + {"Rolling Averaged Population Magnitude", "0%"}, + {"OrderListHash", "721476625"} + }; + } +} diff --git a/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs b/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs new file mode 100644 index 000000000000..809190dcbdbf --- /dev/null +++ b/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs @@ -0,0 +1,185 @@ +/* + * 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.UniverseSelection; +using QuantConnect.Interfaces; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Demonstration of how to chain a coarse and fine universe selection with an option chain universe selection model + /// that will add and remove an for each symbol selected on fine + /// + public class CoarseFineOptionUniverseChainRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + // initialize our changes to nothing + private SecurityChanges _changes = SecurityChanges.None; + private int _optionCount; + private Symbol _lastEquityAdded; + private Symbol _aapl; + private Symbol _twx; + + public override void Initialize() + { + _twx = QuantConnect.Symbol.Create("TWX", SecurityType.Equity, Market.USA); + _aapl = QuantConnect.Symbol.Create("AAPL", SecurityType.Equity, Market.USA); + UniverseSettings.Resolution = Resolution.Minute; + + SetStartDate(2014, 06, 05); + SetEndDate(2014, 06, 06); + + var selectionUniverse = AddUniverse(enumerable => new[] { Time.Date <= new DateTime(2014, 6, 5) ? _twx : _aapl }, + enumerable => new[] { Time.Date <= new DateTime(2014, 6, 5) ? _twx : _aapl }); + + AddChainedEquityOptionUniverseSelectionModel(selectionUniverse, universe => + { + return universe.IncludeWeeklys() + .FrontMonth() + .Contracts(universe.Take(5)); + }); + } + + public override void OnData(Slice data) + { + // if we have no changes, do nothing + if (_changes == SecurityChanges.None || + _changes.AddedSecurities.Any(security => security.Price == 0)) + { + return; + } + + // liquidate removed securities + foreach (var security in _changes.RemovedSecurities) + { + if (security.Invested) + { + Liquidate(security.Symbol); + } + } + + foreach (var security in _changes.AddedSecurities) + { + if (!security.Symbol.HasUnderlying) + { + _lastEquityAdded = security.Symbol; + } + else + { + // options added should all match prev added security + if (security.Symbol.Underlying != _lastEquityAdded) + { + throw new Exception($"Unexpected symbol added {security.Symbol}"); + } + + _optionCount++; + } + + SetHoldings(security.Symbol, 0.05m); + + var config = SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(security.Symbol).ToList(); + + if (!config.Any()) + { + throw new Exception($"Was expecting configurations for {security.Symbol}"); + } + if (config.Any(dataConfig => dataConfig.DataNormalizationMode != DataNormalizationMode.Raw)) + { + throw new Exception($"Was expecting DataNormalizationMode.Raw configurations for {security.Symbol}"); + } + } + _changes = SecurityChanges.None; + } + + public override void OnSecuritiesChanged(SecurityChanges changes) + { + _changes += changes; + } + + public override void OnEndOfAlgorithm() + { + var config = SubscriptionManager.Subscriptions.ToList(); + if (config.Any(dataConfig => dataConfig.Symbol == _twx || dataConfig.Symbol.Underlying == _twx)) + { + throw new Exception($"Was NOT expecting any configurations for {_twx} or it's options, since coarse/fine should deselected it"); + } + + if (_optionCount == 0) + { + throw new Exception("Option universe chain did not add any option!"); + } + } + + /// + /// 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 Language[] Languages { get; } = { Language.CSharp, Language.Python }; + + /// + /// 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 Trades", "13"}, + {"Average Win", "0.65%"}, + {"Average Loss", "-0.05%"}, + {"Compounding Annual Return", "3216040423556140000000000%"}, + {"Drawdown", "0.500%"}, + {"Expectancy", "1.393"}, + {"Net Profit", "32.840%"}, + {"Sharpe Ratio", "7.14272222483913E+15"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "83%"}, + {"Win Rate", "17%"}, + {"Profit-Loss Ratio", "13.36"}, + {"Alpha", "2.59468989671647E+16"}, + {"Beta", "67.661"}, + {"Annual Standard Deviation", "3.633"}, + {"Annual Variance", "13.196"}, + {"Information Ratio", "7.24987266907741E+15"}, + {"Tracking Error", "3.579"}, + {"Treynor Ratio", "383485597312030"}, + {"Total Fees", "$13.00"}, + {"Fitness Score", "0.232"}, + {"Kelly Criterion Estimate", "0"}, + {"Kelly Criterion Probability Value", "0"}, + {"Sortino Ratio", "79228162514264337593543950335"}, + {"Return Over Maximum Drawdown", "79228162514264337593543950335"}, + {"Portfolio Turnover", "0.232"}, + {"Total Insights Generated", "0"}, + {"Total Insights Closed", "0"}, + {"Total Insights Analysis Completed", "0"}, + {"Long Insight Count", "0"}, + {"Short Insight Count", "0"}, + {"Long/Short Ratio", "100%"}, + {"Estimated Monthly Alpha Value", "$0"}, + {"Total Accumulated Estimated Alpha Value", "$0"}, + {"Mean Population Estimated Insight Value", "$0"}, + {"Mean Population Direction", "0%"}, + {"Mean Population Magnitude", "0%"}, + {"Rolling Averaged Population Direction", "0%"}, + {"Rolling Averaged Population Magnitude", "0%"}, + {"OrderListHash", "1630141557"} + }; + } +} diff --git a/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj b/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj index 27f277c8d004..413a27fe7fe1 100644 --- a/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj +++ b/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj @@ -142,10 +142,13 @@ Properties\SharedAssemblyInfo.cs + + + diff --git a/Algorithm.CSharp/UniverseSelectionRegressionAlgorithm.cs b/Algorithm.CSharp/UniverseSelectionRegressionAlgorithm.cs index fd32809cb090..21f12fa5930e 100644 --- a/Algorithm.CSharp/UniverseSelectionRegressionAlgorithm.cs +++ b/Algorithm.CSharp/UniverseSelectionRegressionAlgorithm.cs @@ -172,32 +172,32 @@ private void AssertQuantity(Security security, int expected) /// public Dictionary ExpectedStatistics => new Dictionary { - {"Total Trades", "5"}, + {"Total Trades", "4"}, {"Average Win", "0.64%"}, {"Average Loss", "0%"}, - {"Compounding Annual Return", "-74.197%"}, - {"Drawdown", "6.600%"}, + {"Compounding Annual Return", "-56.577%"}, + {"Drawdown", "3.800%"}, {"Expectancy", "0"}, - {"Net Profit", "-6.115%"}, - {"Sharpe Ratio", "-2.281"}, - {"Probabilistic Sharpe Ratio", "11.870%"}, + {"Net Profit", "-3.811%"}, + {"Sharpe Ratio", "-2.773"}, + {"Probabilistic Sharpe Ratio", "13.961%"}, {"Loss Rate", "0%"}, {"Win Rate", "100%"}, {"Profit-Loss Ratio", "0"}, - {"Alpha", "-0.684"}, - {"Beta", "-0.113"}, - {"Annual Standard Deviation", "0.292"}, - {"Annual Variance", "0.085"}, - {"Information Ratio", "-1.606"}, - {"Tracking Error", "0.312"}, - {"Treynor Ratio", "5.866"}, - {"Total Fees", "$5.00"}, - {"Fitness Score", "0.017"}, + {"Alpha", "-0.504"}, + {"Beta", "-0.052"}, + {"Annual Standard Deviation", "0.179"}, + {"Annual Variance", "0.032"}, + {"Information Ratio", "-1.599"}, + {"Tracking Error", "0.207"}, + {"Treynor Ratio", "9.508"}, + {"Total Fees", "$4.00"}, + {"Fitness Score", "0.008"}, {"Kelly Criterion Estimate", "0"}, {"Kelly Criterion Probability Value", "0"}, - {"Sortino Ratio", "-2.584"}, - {"Return Over Maximum Drawdown", "-11.287"}, - {"Portfolio Turnover", "0.177"}, + {"Sortino Ratio", "-3.791"}, + {"Return Over Maximum Drawdown", "-14.846"}, + {"Portfolio Turnover", "0.136"}, {"Total Insights Generated", "0"}, {"Total Insights Closed", "0"}, {"Total Insights Analysis Completed", "0"}, @@ -211,7 +211,7 @@ private void AssertQuantity(Security security, int expected) {"Mean Population Magnitude", "0%"}, {"Rolling Averaged Population Direction", "0%"}, {"Rolling Averaged Population Magnitude", "0%"}, - {"OrderListHash", "-1386253041"} + {"OrderListHash", "1484950465"} }; } } diff --git a/Algorithm.Framework/Selection/OptionUniverseSelectionModel.cs b/Algorithm.Framework/Selection/OptionUniverseSelectionModel.cs index a141ba719744..39e206e5472e 100644 --- a/Algorithm.Framework/Selection/OptionUniverseSelectionModel.cs +++ b/Algorithm.Framework/Selection/OptionUniverseSelectionModel.cs @@ -15,12 +15,9 @@ using System; using System.Collections.Generic; -using QuantConnect.Data; -using QuantConnect.Data.Auxiliary; using QuantConnect.Data.UniverseSelection; using QuantConnect.Interfaces; using QuantConnect.Securities; -using QuantConnect.Securities.Option; namespace QuantConnect.Algorithm.Framework.Selection { @@ -107,53 +104,11 @@ public override IEnumerable CreateUniverses(QCAlgorithm algorithm) // prevent creating duplicate option chains -- one per underlying if (uniqueUnderlyingSymbols.Add(optionSymbol.Underlying)) { - yield return CreateOptionChain(algorithm, optionSymbol); + yield return algorithm.CreateOptionChain(optionSymbol, Filter, _universeSettings); } } } - /// - /// Creates the canonical chain security for a given symbol - /// - /// The algorithm instance to create universes for - /// Symbol of the option - /// Universe settings define attributes of created subscriptions, such as their resolution and the minimum time in universe before they can be removed - /// Performs extra initialization (such as setting models) after we create a new security object - /// for the given symbol - [Obsolete("This method is obsolete because SecurityInitializer is obsolete and will not be used.")] - protected virtual Option CreateOptionChainSecurity(QCAlgorithm algorithm, Symbol symbol, UniverseSettings settings, ISecurityInitializer initializer) - { - return CreateOptionChainSecurity( - algorithm.SubscriptionManager.SubscriptionDataConfigService, - symbol, - settings, - algorithm.Securities); - } - - /// - /// Creates the canonical chain security for a given symbol - /// - /// The service used to create new - /// Symbol of the option - /// Universe settings define attributes of created subscriptions, such as their resolution and the minimum time in universe before they can be removed - /// Used to create new - /// for the given symbol - protected virtual Option CreateOptionChainSecurity( - ISubscriptionDataConfigService subscriptionDataConfigService, - Symbol symbol, - UniverseSettings settings, - SecurityManager securityManager) - { - var config = subscriptionDataConfigService.Add( - typeof(ZipEntryName), - symbol, - settings.Resolution, - settings.FillForward, - settings.ExtendedMarketHours, - false); - return (Option)securityManager.CreateSecurity(symbol, config, settings.Leverage, false); - } - /// /// Defines the option chain universe filter /// @@ -162,55 +117,5 @@ protected virtual OptionFilterUniverse Filter(OptionFilterUniverse filter) // NOP return filter; } - - /// - /// Creates a for a given symbol - /// - /// The algorithm instance to create universes for - /// Symbol of the option - /// for the given symbol - private OptionChainUniverse CreateOptionChain(QCAlgorithm algorithm, Symbol symbol) - { - if (symbol.SecurityType != SecurityType.Option) - { - throw new ArgumentException("CreateOptionChain requires an option symbol."); - } - - // rewrite non-canonical symbols to be canonical - var market = symbol.ID.Market; - var underlying = symbol.Underlying; - if (!symbol.IsCanonical()) - { - var alias = $"?{underlying.Value}"; - symbol = Symbol.Create(underlying.Value, SecurityType.Option, market, alias); - } - - // resolve defaults if not specified - var settings = _universeSettings ?? algorithm.UniverseSettings; - - // create canonical security object, but don't duplicate if it already exists - Security security; - Option optionChain; - if (!algorithm.Securities.TryGetValue(symbol, out security)) - { - optionChain = CreateOptionChainSecurity( - algorithm.SubscriptionManager.SubscriptionDataConfigService, - symbol, - settings, - algorithm.Securities); - } - else - { - optionChain = (Option)security; - } - - // set the option chain contract filter function - optionChain.SetFilter(Filter); - - // force option chain security to not be directly tradable AFTER it's configured to ensure it's not overwritten - optionChain.IsTradable = false; - - return new OptionChainUniverse(optionChain, settings, algorithm.LiveMode); - } } } \ No newline at end of file diff --git a/Algorithm.Python/CoarseFineOptionUniverseChainRegressionAlgorithm.py b/Algorithm.Python/CoarseFineOptionUniverseChainRegressionAlgorithm.py new file mode 100644 index 000000000000..0cc37a9bad5d --- /dev/null +++ b/Algorithm.Python/CoarseFineOptionUniverseChainRegressionAlgorithm.py @@ -0,0 +1,99 @@ +# 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 clr import AddReference +AddReference("System.Core") +AddReference("System.Collections") +AddReference("QuantConnect.Common") +AddReference("QuantConnect.Algorithm") + +from System import * +from QuantConnect import * +from QuantConnect.Algorithm import * +from QuantConnect.Data.UniverseSelection import * +from datetime import * + +### +### Demonstration of how to chain a coarse and fine universe selection with an option chain universe selection model +### that will add and remove an'OptionChainUniverse' for each symbol selected on fine +### +class CoarseFineOptionUniverseChainRegressionAlgorithm(QCAlgorithm): + + def Initialize(self): + '''Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.''' + + self.SetStartDate(2014,6,5) #Set Start Date + self.SetEndDate(2014,6,6) #Set End Date + + self.UniverseSettings.Resolution = Resolution.Minute + self._twx = Symbol.Create("TWX", SecurityType.Equity, Market.USA) + self._aapl = Symbol.Create("AAPL", SecurityType.Equity, Market.USA) + self._lastEquityAdded = None + self._changes = None + self._optionCount = 0 + + universe = self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) + + self.AddChainedEquityOptionUniverseSelectionModel(universe, self.OptionFilterFunction) + + def OptionFilterFunction(self, universe): + universe.IncludeWeeklys().FrontMonth() + + contracts = list() + for symbol in universe: + if len(contracts) == 5: + break + contracts.append(symbol) + return universe.Contracts(contracts) + + def CoarseSelectionFunction(self, coarse): + if self.Time <= datetime(2014,6,5): + return [ self._twx ] + return [ self._aapl ] + + def FineSelectionFunction(self, fine): + if self.Time <= datetime(2014,6,5): + return [ self._twx ] + return [ self._aapl ] + + def OnData(self, data): + if self._changes == None or any(security.Price == 0 for security in self._changes.AddedSecurities): + return + + # liquidate removed securities + for security in self._changes.RemovedSecurities: + if security.Invested: + self.Liquidate(security.Symbol); + + for security in self._changes.AddedSecurities: + if not security.Symbol.HasUnderlying: + self._lastEquityAdded = security.Symbol; + else: + # options added should all match prev added security + if security.Symbol.Underlying != self._lastEquityAdded: + raise ValueError(f"Unexpected symbol added {security.Symbol}") + self._optionCount += 1 + + self.SetHoldings(security.Symbol, 0.05) + self._changes = None + + # this event fires whenever we have changes to our universe + def OnSecuritiesChanged(self, changes): + if self._changes == None: + self._changes = changes + return + self._changes = self._changes.op_Addition(self._changes, changes) + + def OnEndOfAlgorithm(self): + if self._optionCount == 0: + raise ValueError("Option universe chain did not add any option!") diff --git a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj index 83191dacc352..3da19fa44384 100644 --- a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj +++ b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj @@ -83,6 +83,7 @@ + diff --git a/Algorithm/QCAlgorithm.Framework.cs b/Algorithm/QCAlgorithm.Framework.cs index 05b947192f5e..b311ec5f5145 100644 --- a/Algorithm/QCAlgorithm.Framework.cs +++ b/Algorithm/QCAlgorithm.Framework.cs @@ -71,7 +71,9 @@ public void FrameworkPostInitialize() { foreach (var universe in UniverseSelection.CreateUniverses(this)) { - AddUniverse(universe); + // on purpose we don't call 'AddUniverse' here so that these universes don't get registered as user added + // this is so that later during 'UniverseSelection.CreateUniverses' we wont remove them from UniverseManager + _pendingUniverseAdditions.Add(universe); } if (DebugMode) @@ -94,8 +96,7 @@ public void OnFrameworkData(Slice slice) foreach (var ukvp in UniverseManager) { var universeSymbol = ukvp.Key; - var qcUserDefined = UserDefinedUniverse.CreateSymbol(ukvp.Value.SecurityType, ukvp.Value.Market); - if (universeSymbol.Equals(qcUserDefined)) + if (_userAddedUniverses.Contains(universeSymbol)) { // prevent removal of qc algorithm created user defined universes continue; diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs index 1b9dd08744f9..f69d670b95e4 100644 --- a/Algorithm/QCAlgorithm.Python.cs +++ b/Algorithm/QCAlgorithm.Python.cs @@ -230,22 +230,22 @@ private Security AddDataImpl(Type dataType, Symbol symbol, Resolution? resolutio /// will be executed on day changes in the NewYork time zone ( /// /// Defines an initial coarse selection - public void AddUniverse(PyObject pyObject) + public Universe AddUniverse(PyObject pyObject) { Func, object> coarseFunc; Universe universe; if (pyObject.TryConvert(out universe)) { - AddUniverse(universe); + return AddUniverse(universe); } else if (pyObject.TryConvert(out universe, allowPythonDerivative: true)) { - AddUniverse(new UniversePythonWrapper(pyObject)); + return AddUniverse(new UniversePythonWrapper(pyObject)); } else if (pyObject.TryConvertToDelegate(out coarseFunc)) { - AddUniverse(coarseFunc.ConvertToUniverseSelectionSymbolDelegate()); + return AddUniverse(coarseFunc.ConvertToUniverseSelectionSymbolDelegate()); } else { @@ -262,7 +262,7 @@ public void AddUniverse(PyObject pyObject) /// /// Defines an initial coarse selection or a universe /// Defines a more detailed selection with access to more data - public void AddUniverse(PyObject pyObject, PyObject pyfine) + public Universe AddUniverse(PyObject pyObject, PyObject pyfine) { Func, object> coarseFunc; Func, object> fineFunc; @@ -270,11 +270,11 @@ public void AddUniverse(PyObject pyObject, PyObject pyfine) if (pyObject.TryConvert(out universe) && pyfine.TryConvertToDelegate(out fineFunc)) { - AddUniverse(universe, fineFunc.ConvertToUniverseSelectionSymbolDelegate()); + return AddUniverse(universe, fineFunc.ConvertToUniverseSelectionSymbolDelegate()); } else if (pyObject.TryConvertToDelegate(out coarseFunc) && pyfine.TryConvertToDelegate(out fineFunc)) { - AddUniverse(coarseFunc.ConvertToUniverseSelectionSymbolDelegate(), + return AddUniverse(coarseFunc.ConvertToUniverseSelectionSymbolDelegate(), fineFunc.ConvertToUniverseSelectionSymbolDelegate()); } else @@ -293,10 +293,10 @@ public void AddUniverse(PyObject pyObject, PyObject pyfine) /// A unique name for this universe /// The resolution this universe should be triggered on /// Function delegate that accepts a DateTime and returns a collection of string symbols - public void AddUniverse(string name, Resolution resolution, PyObject pySelector) + public Universe AddUniverse(string name, Resolution resolution, PyObject pySelector) { var selector = pySelector.ConvertToDelegate>(); - AddUniverse(name, resolution, selector.ConvertToUniverseSelectionStringDelegate()); + return AddUniverse(name, resolution, selector.ConvertToUniverseSelectionStringDelegate()); } /// @@ -305,10 +305,10 @@ public void AddUniverse(string name, Resolution resolution, PyObject pySelector) /// /// A unique name for this universe /// Function delegate that accepts a DateTime and returns a collection of string symbols - public void AddUniverse(string name, PyObject pySelector) + public Universe AddUniverse(string name, PyObject pySelector) { var selector = pySelector.ConvertToDelegate>(); - AddUniverse(name, selector.ConvertToUniverseSelectionStringDelegate()); + return AddUniverse(name, selector.ConvertToUniverseSelectionStringDelegate()); } /// @@ -320,10 +320,10 @@ public void AddUniverse(string name, PyObject pySelector) /// The market of the universe /// The subscription settings used for securities added from this universe /// Function delegate that accepts a DateTime and returns a collection of string symbols - public void AddUniverse(SecurityType securityType, string name, Resolution resolution, string market, UniverseSettings universeSettings, PyObject pySelector) + public Universe AddUniverse(SecurityType securityType, string name, Resolution resolution, string market, UniverseSettings universeSettings, PyObject pySelector) { var selector = pySelector.ConvertToDelegate>(); - AddUniverse(securityType, name, resolution, market, universeSettings, selector.ConvertToUniverseSelectionStringDelegate()); + return AddUniverse(securityType, name, resolution, market, universeSettings, selector.ConvertToUniverseSelectionStringDelegate()); } /// @@ -334,9 +334,9 @@ public void AddUniverse(SecurityType securityType, string name, Resolution resol /// The data type /// A unique name for this universe /// Function delegate that performs selection on the universe data - public void AddUniverse(PyObject T, string name, PyObject selector) + public Universe AddUniverse(PyObject T, string name, PyObject selector) { - AddUniverse(T.CreateType(), SecurityType.Equity, name, Resolution.Daily, Market.USA, UniverseSettings, selector); + return AddUniverse(T.CreateType(), SecurityType.Equity, name, Resolution.Daily, Market.USA, UniverseSettings, selector); } /// @@ -348,9 +348,9 @@ public void AddUniverse(PyObject T, string name, PyObject selector) /// A unique name for this universe /// The epected resolution of the universe data /// Function delegate that performs selection on the universe data - public void AddUniverse(PyObject T, string name, Resolution resolution, PyObject selector) + public Universe AddUniverse(PyObject T, string name, Resolution resolution, PyObject selector) { - AddUniverse(T.CreateType(), SecurityType.Equity, name, resolution, Market.USA, UniverseSettings, selector); + return AddUniverse(T.CreateType(), SecurityType.Equity, name, resolution, Market.USA, UniverseSettings, selector); } /// @@ -363,9 +363,9 @@ public void AddUniverse(PyObject T, string name, Resolution resolution, PyObject /// The epected resolution of the universe data /// The settings used for securities added by this universe /// Function delegate that performs selection on the universe data - public void AddUniverse(PyObject T, string name, Resolution resolution, UniverseSettings universeSettings, PyObject selector) + public Universe AddUniverse(PyObject T, string name, Resolution resolution, UniverseSettings universeSettings, PyObject selector) { - AddUniverse(T.CreateType(), SecurityType.Equity, name, resolution, Market.USA, universeSettings, selector); + return AddUniverse(T.CreateType(), SecurityType.Equity, name, resolution, Market.USA, universeSettings, selector); } /// @@ -377,9 +377,9 @@ public void AddUniverse(PyObject T, string name, Resolution resolution, Universe /// A unique name for this universe /// The settings used for securities added by this universe /// Function delegate that performs selection on the universe data - public void AddUniverse(PyObject T, string name, UniverseSettings universeSettings, PyObject selector) + public Universe AddUniverse(PyObject T, string name, UniverseSettings universeSettings, PyObject selector) { - AddUniverse(T.CreateType(), SecurityType.Equity, name, Resolution.Daily, Market.USA, universeSettings, selector); + return AddUniverse(T.CreateType(), SecurityType.Equity, name, Resolution.Daily, Market.USA, universeSettings, selector); } /// @@ -392,9 +392,9 @@ public void AddUniverse(PyObject T, string name, UniverseSettings universeSettin /// The epected resolution of the universe data /// The market for selected symbols /// Function delegate that performs selection on the universe data - public void AddUniverse(PyObject T, SecurityType securityType, string name, Resolution resolution, string market, PyObject selector) + public Universe AddUniverse(PyObject T, SecurityType securityType, string name, Resolution resolution, string market, PyObject selector) { - AddUniverse(T.CreateType(), securityType, name, resolution, market, UniverseSettings, selector); + return AddUniverse(T.CreateType(), securityType, name, resolution, market, UniverseSettings, selector); } /// @@ -407,9 +407,9 @@ public void AddUniverse(PyObject T, SecurityType securityType, string name, Reso /// The market for selected symbols /// The subscription settings to use for newly created subscriptions /// Function delegate that performs selection on the universe data - public void AddUniverse(PyObject T, SecurityType securityType, string name, Resolution resolution, string market, UniverseSettings universeSettings, PyObject selector) + public Universe AddUniverse(PyObject T, SecurityType securityType, string name, Resolution resolution, string market, UniverseSettings universeSettings, PyObject selector) { - AddUniverse(T.CreateType(), securityType, name, resolution, market, universeSettings, selector); + return AddUniverse(T.CreateType(), securityType, name, resolution, market, universeSettings, selector); } /// @@ -422,7 +422,7 @@ public void AddUniverse(PyObject T, SecurityType securityType, string name, Reso /// The market for selected symbols /// The subscription settings to use for newly created subscriptions /// Function delegate that performs selection on the universe data - public void AddUniverse(Type dataType, SecurityType securityType, string name, Resolution resolution, string market, UniverseSettings universeSettings, PyObject pySelector) + public Universe AddUniverse(Type dataType, SecurityType securityType, string name, Resolution resolution, string market, UniverseSettings universeSettings, PyObject pySelector) { var marketHoursDbEntry = MarketHoursDatabase.GetEntry(market, name, securityType); var dataTimeZone = marketHoursDbEntry.DataTimeZone; @@ -432,7 +432,7 @@ public void AddUniverse(Type dataType, SecurityType securityType, string name, R var selector = pySelector.ConvertToDelegate, object>>(); - AddUniverse(new FuncUniverse(config, universeSettings, SecurityInitializer, baseDatas => + return AddUniverse(new FuncUniverse(config, universeSettings, SecurityInitializer, baseDatas => { var result = selector(baseDatas); return ReferenceEquals(result, Universe.Unchanged) @@ -442,6 +442,30 @@ public void AddUniverse(Type dataType, SecurityType securityType, string name, R )); } + /// + /// Creates a new universe selection model and adds it to the algorithm. This universe selection model will chain to the security + /// changes of a given selection output and create a new for each of them + /// + /// The universe we want to chain an option universe selection model too + /// The option filter universe to use + public void AddChainedEquityOptionUniverseSelectionModel(PyObject universe, PyObject optionFilter) + { + Func convertedOptionChain; + Universe universeToChain; + + if (universe.TryConvert(out universeToChain) && optionFilter.TryConvertToDelegate(out convertedOptionChain)) + { + AddChainedEquityOptionUniverseSelectionModel(universeToChain, convertedOptionChain); + } + else + { + using (Py.GIL()) + { + throw new ArgumentException($"QCAlgorithm.AddChainedEquityOptionUniverseSelectionModel: {universe.Repr()} or {optionFilter.Repr()} is not a valid argument."); + } + } + } + /// /// Registers the consolidator to receive automatic updates as well as configures the indicator to receive updates /// from the consolidator. diff --git a/Algorithm/QCAlgorithm.Universe.cs b/Algorithm/QCAlgorithm.Universe.cs index debdff3a6351..d9194d996686 100644 --- a/Algorithm/QCAlgorithm.Universe.cs +++ b/Algorithm/QCAlgorithm.Universe.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Linq; +using QuantConnect.Algorithm.Selection; using QuantConnect.Data; using QuantConnect.Data.Fundamental; using QuantConnect.Data.UniverseSelection; @@ -32,6 +33,8 @@ public partial class QCAlgorithm private readonly object _pendingUniverseAdditionsLock = new object(); private readonly List _pendingUserDefinedUniverseSecurityAdditions = new List(); private readonly List _pendingUniverseAdditions = new List(); + // this is so that later during 'UniverseSelection.CreateUniverses' we wont remove these user universes from the UniverseManager + private readonly HashSet _userAddedUniverses = new HashSet(); /// /// Gets universe manager which holds universes keyed by their symbol @@ -179,11 +182,13 @@ public UniverseDefinitions Universe /// Adds the universe to the algorithm /// /// The universe to be added - public void AddUniverse(Universe universe) + public Universe AddUniverse(Universe universe) { // The universe will be added at the end of time step, same as the AddData user defined universes. // This is required to be independent of the start and end date set during initialize _pendingUniverseAdditions.Add(universe); + _userAddedUniverses.Add(universe.Configuration.Symbol); + return universe; } /// @@ -194,9 +199,9 @@ public void AddUniverse(Universe universe) /// The data type /// A unique name for this universe /// Function delegate that performs selection on the universe data - public void AddUniverse(string name, Func, IEnumerable> selector) + public Universe AddUniverse(string name, Func, IEnumerable> selector) { - AddUniverse(SecurityType.Equity, name, Resolution.Daily, Market.USA, UniverseSettings, selector); + return AddUniverse(SecurityType.Equity, name, Resolution.Daily, Market.USA, UniverseSettings, selector); } /// @@ -207,9 +212,9 @@ public void AddUniverse(string name, Func, IEnumerable /// The data type /// A unique name for this universe /// Function delegate that performs selection on the universe data - public void AddUniverse(string name, Func, IEnumerable> selector) + public Universe AddUniverse(string name, Func, IEnumerable> selector) { - AddUniverse(SecurityType.Equity, name, Resolution.Daily, Market.USA, UniverseSettings, selector); + return AddUniverse(SecurityType.Equity, name, Resolution.Daily, Market.USA, UniverseSettings, selector); } /// @@ -221,9 +226,9 @@ public void AddUniverse(string name, Func, IEnumerable /// A unique name for this universe /// The settings used for securities added by this universe /// Function delegate that performs selection on the universe data - public void AddUniverse(string name, UniverseSettings universeSettings, Func, IEnumerable> selector) + public Universe AddUniverse(string name, UniverseSettings universeSettings, Func, IEnumerable> selector) { - AddUniverse(SecurityType.Equity, name, Resolution.Daily, Market.USA, universeSettings, selector); + return AddUniverse(SecurityType.Equity, name, Resolution.Daily, Market.USA, universeSettings, selector); } /// @@ -235,9 +240,9 @@ public void AddUniverse(string name, UniverseSettings universeSettings, Func< /// A unique name for this universe /// The settings used for securities added by this universe /// Function delegate that performs selection on the universe data - public void AddUniverse(string name, UniverseSettings universeSettings, Func, IEnumerable> selector) + public Universe AddUniverse(string name, UniverseSettings universeSettings, Func, IEnumerable> selector) { - AddUniverse(SecurityType.Equity, name, Resolution.Daily, Market.USA, universeSettings, selector); + return AddUniverse(SecurityType.Equity, name, Resolution.Daily, Market.USA, universeSettings, selector); } /// @@ -249,9 +254,9 @@ public void AddUniverse(string name, UniverseSettings universeSettings, Func< /// A unique name for this universe /// The epected resolution of the universe data /// Function delegate that performs selection on the universe data - public void AddUniverse(string name, Resolution resolution, Func, IEnumerable> selector) + public Universe AddUniverse(string name, Resolution resolution, Func, IEnumerable> selector) { - AddUniverse(SecurityType.Equity, name, resolution, Market.USA, UniverseSettings, selector); + return AddUniverse(SecurityType.Equity, name, resolution, Market.USA, UniverseSettings, selector); } /// @@ -263,9 +268,9 @@ public void AddUniverse(string name, Resolution resolution, FuncA unique name for this universe /// The epected resolution of the universe data /// Function delegate that performs selection on the universe data - public void AddUniverse(string name, Resolution resolution, Func, IEnumerable> selector) + public Universe AddUniverse(string name, Resolution resolution, Func, IEnumerable> selector) { - AddUniverse(SecurityType.Equity, name, resolution, Market.USA, UniverseSettings, selector); + return AddUniverse(SecurityType.Equity, name, resolution, Market.USA, UniverseSettings, selector); } /// @@ -278,9 +283,9 @@ public void AddUniverse(string name, Resolution resolution, FuncThe epected resolution of the universe data /// The settings used for securities added by this universe /// Function delegate that performs selection on the universe data - public void AddUniverse(string name, Resolution resolution, UniverseSettings universeSettings, Func, IEnumerable> selector) + public Universe AddUniverse(string name, Resolution resolution, UniverseSettings universeSettings, Func, IEnumerable> selector) { - AddUniverse(SecurityType.Equity, name, resolution, Market.USA, universeSettings, selector); + return AddUniverse(SecurityType.Equity, name, resolution, Market.USA, universeSettings, selector); } /// @@ -293,9 +298,9 @@ public void AddUniverse(string name, Resolution resolution, UniverseSettings /// The epected resolution of the universe data /// The settings used for securities added by this universe /// Function delegate that performs selection on the universe data - public void AddUniverse(string name, Resolution resolution, UniverseSettings universeSettings, Func, IEnumerable> selector) + public Universe AddUniverse(string name, Resolution resolution, UniverseSettings universeSettings, Func, IEnumerable> selector) { - AddUniverse(SecurityType.Equity, name, resolution, Market.USA, universeSettings, selector); + return AddUniverse(SecurityType.Equity, name, resolution, Market.USA, universeSettings, selector); } /// @@ -308,9 +313,9 @@ public void AddUniverse(string name, Resolution resolution, UniverseSettings /// The epected resolution of the universe data /// The market for selected symbols /// Function delegate that performs selection on the universe data - public void AddUniverse(SecurityType securityType, string name, Resolution resolution, string market, Func, IEnumerable> selector) + public Universe AddUniverse(SecurityType securityType, string name, Resolution resolution, string market, Func, IEnumerable> selector) { - AddUniverse(securityType, name, resolution, market, UniverseSettings, selector); + return AddUniverse(securityType, name, resolution, market, UniverseSettings, selector); } /// @@ -323,9 +328,9 @@ public void AddUniverse(SecurityType securityType, string name, Resolution re /// The epected resolution of the universe data /// The market for selected symbols /// Function delegate that performs selection on the universe data - public void AddUniverse(SecurityType securityType, string name, Resolution resolution, string market, Func, IEnumerable> selector) + public Universe AddUniverse(SecurityType securityType, string name, Resolution resolution, string market, Func, IEnumerable> selector) { - AddUniverse(securityType, name, resolution, market, UniverseSettings, selector); + return AddUniverse(securityType, name, resolution, market, UniverseSettings, selector); } /// @@ -338,14 +343,14 @@ public void AddUniverse(SecurityType securityType, string name, Resolution re /// The market for selected symbols /// The subscription settings to use for newly created subscriptions /// Function delegate that performs selection on the universe data - public void AddUniverse(SecurityType securityType, string name, Resolution resolution, string market, UniverseSettings universeSettings, Func, IEnumerable> selector) + public Universe AddUniverse(SecurityType securityType, string name, Resolution resolution, string market, UniverseSettings universeSettings, Func, IEnumerable> selector) { var marketHoursDbEntry = MarketHoursDatabase.GetEntry(market, name, securityType); var dataTimeZone = marketHoursDbEntry.DataTimeZone; var exchangeTimeZone = marketHoursDbEntry.ExchangeHours.TimeZone; var symbol = QuantConnect.Symbol.Create(name, securityType, market, baseDataType: typeof(T)); var config = new SubscriptionDataConfig(typeof(T), symbol, resolution, dataTimeZone, exchangeTimeZone, false, false, true, true, isFilteredSubscription: false); - AddUniverse(new FuncUniverse(config, universeSettings, SecurityInitializer, d => selector(d.OfType()))); + return AddUniverse(new FuncUniverse(config, universeSettings, SecurityInitializer, d => selector(d.OfType()))); } /// @@ -358,14 +363,14 @@ public void AddUniverse(SecurityType securityType, string name, Resolution re /// The market for selected symbols /// The subscription settings to use for newly created subscriptions /// Function delegate that performs selection on the universe data - public void AddUniverse(SecurityType securityType, string name, Resolution resolution, string market, UniverseSettings universeSettings, Func, IEnumerable> selector) + public Universe AddUniverse(SecurityType securityType, string name, Resolution resolution, string market, UniverseSettings universeSettings, Func, IEnumerable> selector) { var marketHoursDbEntry = MarketHoursDatabase.GetEntry(market, name, securityType); var dataTimeZone = marketHoursDbEntry.DataTimeZone; var exchangeTimeZone = marketHoursDbEntry.ExchangeHours.TimeZone; var symbol = QuantConnect.Symbol.Create(name, securityType, market, baseDataType: typeof(T)); var config = new SubscriptionDataConfig(typeof(T), symbol, resolution, dataTimeZone, exchangeTimeZone, false, false, true, true, isFilteredSubscription: false); - AddUniverse(new FuncUniverse(config, universeSettings, SecurityInitializer, + return AddUniverse(new FuncUniverse(config, universeSettings, SecurityInitializer, d => selector(d.OfType()).Select(x => QuantConnect.Symbol.Create(x, securityType, market, baseDataType: typeof(T)))) ); } @@ -375,9 +380,9 @@ public void AddUniverse(SecurityType securityType, string name, Resolution re /// will be executed on day changes in the NewYork time zone ( /// /// Defines an initial coarse selection - public void AddUniverse(Func, IEnumerable> selector) + public Universe AddUniverse(Func, IEnumerable> selector) { - AddUniverse(new CoarseFundamentalUniverse(UniverseSettings, SecurityInitializer, selector)); + return AddUniverse(new CoarseFundamentalUniverse(UniverseSettings, SecurityInitializer, selector)); } /// @@ -386,11 +391,11 @@ public void AddUniverse(Func, IEnumerable /// /// Defines an initial coarse selection /// Defines a more detailed selection with access to more data - public void AddUniverse(Func, IEnumerable> coarseSelector, Func, IEnumerable> fineSelector) + public Universe AddUniverse(Func, IEnumerable> coarseSelector, Func, IEnumerable> fineSelector) { var coarse = new CoarseFundamentalUniverse(UniverseSettings, SecurityInitializer, coarseSelector); - AddUniverse(new FineFundamentalFilteredUniverse(coarse, fineSelector)); + return AddUniverse(new FineFundamentalFilteredUniverse(coarse, fineSelector)); } /// @@ -399,9 +404,9 @@ public void AddUniverse(Func, IEnumerable /// /// The universe to be filtered with fine fundamental selection /// Defines a more detailed selection with access to more data - public void AddUniverse(Universe universe, Func, IEnumerable> fineSelector) + public Universe AddUniverse(Universe universe, Func, IEnumerable> fineSelector) { - AddUniverse(new FineFundamentalFilteredUniverse(universe, fineSelector)); + return AddUniverse(new FineFundamentalFilteredUniverse(universe, fineSelector)); } /// @@ -410,9 +415,9 @@ public void AddUniverse(Universe universe, Func, IE /// /// A unique name for this universe /// Function delegate that accepts a DateTime and returns a collection of string symbols - public void AddUniverse(string name, Func> selector) + public Universe AddUniverse(string name, Func> selector) { - AddUniverse(SecurityType.Equity, name, Resolution.Daily, Market.USA, UniverseSettings, selector); + return AddUniverse(SecurityType.Equity, name, Resolution.Daily, Market.USA, UniverseSettings, selector); } /// @@ -422,9 +427,9 @@ public void AddUniverse(string name, Func> selecto /// A unique name for this universe /// The resolution this universe should be triggered on /// Function delegate that accepts a DateTime and returns a collection of string symbols - public void AddUniverse(string name, Resolution resolution, Func> selector) + public Universe AddUniverse(string name, Resolution resolution, Func> selector) { - AddUniverse(SecurityType.Equity, name, resolution, Market.USA, UniverseSettings, selector); + return AddUniverse(SecurityType.Equity, name, resolution, Market.USA, UniverseSettings, selector); } /// @@ -436,14 +441,25 @@ public void AddUniverse(string name, Resolution resolution, FuncThe market of the universe /// The subscription settings used for securities added from this universe /// Function delegate that accepts a DateTime and returns a collection of string symbols - public void AddUniverse(SecurityType securityType, string name, Resolution resolution, string market, UniverseSettings universeSettings, Func> selector) + public Universe AddUniverse(SecurityType securityType, string name, Resolution resolution, string market, UniverseSettings universeSettings, Func> selector) { var marketHoursDbEntry = MarketHoursDatabase.GetEntry(market, name, securityType); var dataTimeZone = marketHoursDbEntry.DataTimeZone; var exchangeTimeZone = marketHoursDbEntry.ExchangeHours.TimeZone; var symbol = QuantConnect.Symbol.Create(name, securityType, market); var config = new SubscriptionDataConfig(typeof(CoarseFundamental), symbol, resolution, dataTimeZone, exchangeTimeZone, false, false, true, isFilteredSubscription: false); - AddUniverse(new UserDefinedUniverse(config, universeSettings, resolution.ToTimeSpan(), selector)); + return AddUniverse(new UserDefinedUniverse(config, universeSettings, resolution.ToTimeSpan(), selector)); + } + + /// + /// Creates a new universe selection model and adds it to the algorithm. This universe selection model will chain to the security + /// changes of a given selection output and create a new for each of them + /// + /// The universe we want to chain an option universe selection model too + /// The option filter universe to use + public void AddChainedEquityOptionUniverseSelectionModel(Universe universe, Func optionFilter) + { + AddUniverseSelection(new OptionChainedUniverseSelectionModel(universe, optionFilter)); } /// @@ -501,7 +517,8 @@ private void AddToUserDefinedUniverse( TimeSpan.Zero), QuantConnect.Time.MaxTimeSpan, new List()); - _pendingUniverseAdditions.Add(universe); + + AddUniverse(universe); } } } diff --git a/Algorithm/QCAlgorithm.cs b/Algorithm/QCAlgorithm.cs index 107a4f12b8f5..49c595ac6600 100644 --- a/Algorithm/QCAlgorithm.cs +++ b/Algorithm/QCAlgorithm.cs @@ -45,6 +45,7 @@ using QuantConnect.Algorithm.Framework.Portfolio; using QuantConnect.Algorithm.Framework.Risk; using QuantConnect.Algorithm.Framework.Selection; +using QuantConnect.Algorithm.Selection; using QuantConnect.Storage; namespace QuantConnect.Algorithm @@ -1609,7 +1610,7 @@ public Future AddFutureContract(Symbol symbol, Resolution? resolution = null, bo /// The new security public Option AddOptionContract(Symbol symbol, Resolution? resolution = null, bool fillDataForward = true, decimal leverage = Security.NullLeverage) { - var configs = SubscriptionManager.SubscriptionDataConfigService.Add(symbol, resolution, fillDataForward); + var configs = SubscriptionManager.SubscriptionDataConfigService.Add(symbol, resolution, fillDataForward, dataNormalizationMode:DataNormalizationMode.Raw); var option = (Option)Securities.CreateSecurity(symbol, configs, leverage); // add underlying if not present @@ -1643,8 +1644,26 @@ public Option AddOptionContract(Symbol symbol, Resolution? resolution = null, bo equity.RefreshDataNormalizationModeProperty(); option.Underlying = equity; + Securities.Add(option); - AddToUserDefinedUniverse(option, configs); + // get or create the universe + var universeSymbol = OptionContractUniverse.CreateSymbol(symbol.ID.Market, symbol.Underlying.SecurityType); + Universe universe; + if (!UniverseManager.TryGetValue(universeSymbol, out universe)) + { + universe = _pendingUniverseAdditions.FirstOrDefault(u => u.Configuration.Symbol == universeSymbol) + ?? AddUniverse(new OptionContractUniverse(new SubscriptionDataConfig(configs.First(), symbol: universeSymbol), UniverseSettings)); + } + + // update the universe + var optionUniverse = universe as OptionContractUniverse; + if (optionUniverse != null) + { + foreach (var subscriptionDataConfig in configs.Concat(underlyingConfigs)) + { + optionUniverse.Add(subscriptionDataConfig); + } + } return option; } @@ -1691,6 +1710,17 @@ public Crypto AddCrypto(string ticker, Resolution? resolution = null, string mar return AddSecurity(SecurityType.Crypto, ticker, resolution, market, fillDataForward, leverage, false); } + /// + /// Removes the security with the specified symbol. This will cancel all + /// open orders and then liquidate any existing holdings + /// + /// The symbol of the security to be removed + /// Sugar syntax for + public bool RemoveOptionContract(Symbol symbol) + { + return RemoveSecurity(symbol); + } + /// /// Removes the security with the specified symbol. This will cancel all /// open orders and then liquidate any existing holdings diff --git a/Algorithm/QuantConnect.Algorithm.csproj b/Algorithm/QuantConnect.Algorithm.csproj index 3a115938ce9d..1ef461346336 100644 --- a/Algorithm/QuantConnect.Algorithm.csproj +++ b/Algorithm/QuantConnect.Algorithm.csproj @@ -179,6 +179,8 @@ + + diff --git a/Algorithm/Selection/OptionChainedUniverseSelectionModel.cs b/Algorithm/Selection/OptionChainedUniverseSelectionModel.cs new file mode 100644 index 000000000000..eeea258dcc42 --- /dev/null +++ b/Algorithm/Selection/OptionChainedUniverseSelectionModel.cs @@ -0,0 +1,83 @@ +/* + * 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 QuantConnect.Interfaces; +using QuantConnect.Securities; +using System.Collections.Generic; +using QuantConnect.Data.UniverseSelection; +using QuantConnect.Algorithm.Framework.Selection; + +namespace QuantConnect.Algorithm.Selection +{ + /// + /// This universe selection model will chain to the security changes of a given selection + /// output and create a new for each of them + /// + public class OptionChainedUniverseSelectionModel : UniverseSelectionModel + { + private DateTime _nextRefreshTimeUtc; + private IEnumerable _currentSymbols; + private readonly UniverseSettings _universeSettings; + private readonly Func _optionFilter; + + /// + /// Gets the next time the framework should invoke the `CreateUniverses` method to refresh the set of universes. + /// + public override DateTime GetNextRefreshTimeUtc() => _nextRefreshTimeUtc; + + /// + /// Creates a new instance of + /// + /// The universe we want to chain to + /// The option filter universe to use + /// Universe settings define attributes of created subscriptions, such as their resolution and the minimum time in universe before they can be removed + public OptionChainedUniverseSelectionModel(Universe universe, + Func optionFilter, + UniverseSettings universeSettings = null) + { + _optionFilter = optionFilter; + _universeSettings = universeSettings; + _nextRefreshTimeUtc = DateTime.MaxValue; + + _currentSymbols = Enumerable.Empty(); + universe.SelectionChanged += (sender, args) => + { + // the universe we were watching changed, this will trigger a call to CreateUniverses + _nextRefreshTimeUtc = DateTime.MinValue; + + _currentSymbols = ((Universe.SelectionEventArgs)args).CurrentSelection + .Select(symbol => Symbol.Create(symbol.Value, SecurityType.Option, symbol.ID.Market, $"?{symbol.Value}")) + .ToList(); + }; + } + + /// + /// Creates the universes for this algorithm. Called once after + /// + /// The algorithm instance to create universes for + /// The universes to be used by the algorithm + public override IEnumerable CreateUniverses(QCAlgorithm algorithm) + { + _nextRefreshTimeUtc = DateTime.MaxValue; + + foreach (var optionSymbol in _currentSymbols) + { + yield return algorithm.CreateOptionChain(optionSymbol, _optionFilter, _universeSettings); + } + } + } +} diff --git a/Algorithm/Selection/OptionContractUniverse.cs b/Algorithm/Selection/OptionContractUniverse.cs new file mode 100644 index 000000000000..8f3c9d039369 --- /dev/null +++ b/Algorithm/Selection/OptionContractUniverse.cs @@ -0,0 +1,97 @@ +/* + * 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 QuantConnect.Data; +using System.Collections.Generic; +using System.Collections.Specialized; +using QuantConnect.Data.UniverseSelection; + +namespace QuantConnect.Algorithm.Selection +{ + /// + /// This universe will hold single option contracts and their underlying, managing removals and additions + /// + public class OptionContractUniverse : UserDefinedUniverse + { + private readonly HashSet _symbols; + + /// + /// Creates a new empty instance + /// + /// The universe configuration to use + /// The universe settings to use + public OptionContractUniverse(SubscriptionDataConfig configuration, UniverseSettings universeSettings) + : base(configuration, universeSettings, Time.EndOfTimeTimeSpan, + // Argument isn't used since we override 'SelectSymbols' + Enumerable.Empty()) + { + _symbols = new HashSet(); + } + + /// + /// Returns the symbols defined by the user for this universe + /// + /// The current utc time + /// The symbols to remain in the universe + /// The data that passes the filter + public override IEnumerable SelectSymbols(DateTime utcTime, BaseDataCollection data) + { + return _symbols; + } + + /// + /// Event invocator for the event + /// + /// The notify collection changed event arguments + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + { + if (args.Action == NotifyCollectionChangedAction.Remove) + { + var removedSymbol = (Symbol)args.OldItems[0]; + _symbols.Remove(removedSymbol); + + // the option has been removed! This can happen when the user manually removed the option contract we remove the underlying + if (removedSymbol.SecurityType == SecurityType.Option) + { + Remove(removedSymbol.Underlying); + } + } + else if (args.Action == NotifyCollectionChangedAction.Add) + { + // QCAlgorithm.AddOptionContract will add both underlying and option contract + _symbols.Add((Symbol)args.NewItems[0]); + } + + base.OnCollectionChanged(args); + } + + /// + /// Creates a user defined universe symbol + /// + /// The market + /// The underlying option security type + /// A symbol for user defined universe of the specified security type and market + public static Symbol CreateSymbol(string market, SecurityType securityType) + { + var ticker = $"qc-universe-optioncontract-{securityType.SecurityTypeToLower()}-{market.ToLowerInvariant()}"; + var underlying = Symbol.Create(ticker, securityType, market); + var sid = SecurityIdentifier.GenerateOption(SecurityIdentifier.DefaultDate, underlying.ID, market, 0, 0, 0); + + return new Symbol(sid, ticker); + } + } +} diff --git a/Common/Data/UniverseSelection/FineFundamentalFilteredUniverse.cs b/Common/Data/UniverseSelection/FineFundamentalFilteredUniverse.cs index 6345e3e596c3..c05170b5e557 100644 --- a/Common/Data/UniverseSelection/FineFundamentalFilteredUniverse.cs +++ b/Common/Data/UniverseSelection/FineFundamentalFilteredUniverse.cs @@ -40,6 +40,7 @@ public FineFundamentalFilteredUniverse(Universe universe, Func OnSelectionChanged(((SelectionEventArgs) args).CurrentSelection); } /// @@ -52,6 +53,7 @@ public FineFundamentalFilteredUniverse(Universe universe, PyObject fineSelector) { var func = fineSelector.ConvertToDelegate, Symbol[]>>(); FineFundamentalUniverse = new FineFundamentalUniverse(universe.UniverseSettings, universe.SecurityInitializer, func); + FineFundamentalUniverse.SelectionChanged += (sender, args) => OnSelectionChanged(((SelectionEventArgs)args).CurrentSelection); } /// diff --git a/Common/Data/UniverseSelection/FuturesChainUniverse.cs b/Common/Data/UniverseSelection/FuturesChainUniverse.cs index 6776f3ce1919..75620779ef74 100644 --- a/Common/Data/UniverseSelection/FuturesChainUniverse.cs +++ b/Common/Data/UniverseSelection/FuturesChainUniverse.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.Linq; using QuantConnect.Data.Market; +using QuantConnect.Interfaces; using QuantConnect.Securities; using QuantConnect.Securities.Future; using QuantConnect.Util; @@ -29,8 +30,6 @@ namespace QuantConnect.Data.UniverseSelection /// public class FuturesChainUniverse : Universe { - private static readonly IReadOnlyList dataTypes = new[] { TickType.Quote, TickType.Trade, TickType.OpenInterest }; - private readonly UniverseSettings _universeSettings; private DateTime _cacheDate; @@ -150,5 +149,30 @@ public override bool CanRemoveMember(DateTime utcTime, Security security) } return false; } + + /// + /// Gets the subscription requests to be added for the specified security + /// + /// The security to get subscriptions for + /// The current time in utc. This is the frontier time of the algorithm + /// The max end time + /// Instance which implements interface + /// All subscriptions required by this security + public override IEnumerable GetSubscriptionRequests(Security security, DateTime currentTimeUtc, DateTime maximumEndTimeUtc, + ISubscriptionDataConfigService subscriptionService) + { + if (Future.Symbol.Underlying == security.Symbol) + { + Future.Underlying = security; + } + else + { + // set the underlying security and pricing model from the canonical security + var future = (Future)security; + future.Underlying = Future.Underlying; + } + + return base.GetSubscriptionRequests(security, currentTimeUtc, maximumEndTimeUtc, subscriptionService); + } } } diff --git a/Common/Data/UniverseSelection/OptionChainUniverse.cs b/Common/Data/UniverseSelection/OptionChainUniverse.cs index 92682fd313f8..c45a4abc361e 100644 --- a/Common/Data/UniverseSelection/OptionChainUniverse.cs +++ b/Common/Data/UniverseSelection/OptionChainUniverse.cs @@ -54,7 +54,7 @@ public OptionChainUniverse(Option option, { Option = option; _underlyingSymbol = new[] { Option.Symbol.Underlying }; - _universeSettings = universeSettings; + _universeSettings = new UniverseSettings(universeSettings) { DataNormalizationMode = DataNormalizationMode.Raw }; _liveMode = liveMode; _optionFilterUniverse = new OptionFilterUniverse(); } @@ -75,7 +75,7 @@ public OptionChainUniverse(Option option, { Option = option; _underlyingSymbol = new[] { Option.Symbol.Underlying }; - _universeSettings = universeSettings; + _universeSettings = new UniverseSettings(universeSettings) { DataNormalizationMode = DataNormalizationMode.Raw }; _liveMode = liveMode; _optionFilterUniverse = new OptionFilterUniverse(); } @@ -244,18 +244,20 @@ public override bool CanRemoveMember(DateTime utcTime, Security security) public override IEnumerable GetSubscriptionRequests(Security security, DateTime currentTimeUtc, DateTime maximumEndTimeUtc, ISubscriptionDataConfigService subscriptionService) { - var result = subscriptionService.Add(security.Symbol, UniverseSettings.Resolution, - UniverseSettings.FillForward, - UniverseSettings.ExtendedMarketHours, - // force raw data normalization mode for underlying - dataNormalizationMode: DataNormalizationMode.Raw); + if (Option.Symbol.Underlying == security.Symbol) + { + Option.Underlying = security; + security.SetDataNormalizationMode(DataNormalizationMode.Raw); + } + else + { + // set the underlying security and pricing model from the canonical security + var option = (Option)security; + option.Underlying = Option.Underlying; + option.PriceModel = Option.PriceModel; + } - return result.Select(config => new SubscriptionRequest(isUniverseSubscription: false, - universe: this, - security: security, - configuration: config, - startTimeUtc: currentTimeUtc, - endTimeUtc: maximumEndTimeUtc)); + return base.GetSubscriptionRequests(security, currentTimeUtc, maximumEndTimeUtc, subscriptionService); } } } \ No newline at end of file diff --git a/Common/Data/UniverseSelection/Universe.cs b/Common/Data/UniverseSelection/Universe.cs index 0d945da28eb4..2e4b011688e8 100644 --- a/Common/Data/UniverseSelection/Universe.cs +++ b/Common/Data/UniverseSelection/Universe.cs @@ -51,6 +51,11 @@ public virtual ConcurrentDictionary Securities private set; } + /// + /// Event fired when the universe selection has changed + /// + public event EventHandler SelectionChanged; + /// /// Gets the security type of this universe /// @@ -190,6 +195,7 @@ public IEnumerable PerformSelection(DateTime utcTime, BaseDataCollection // select empty set of symbols after dispose requested if (DisposeRequested) { + OnSelectionChanged(); return Enumerable.Empty(); } @@ -206,6 +212,8 @@ public IEnumerable PerformSelection(DateTime utcTime, BaseDataCollection { return Unchanged; } + + OnSelectionChanged(selections); return selections; } @@ -339,11 +347,20 @@ public virtual void SetSecurityInitializer(ISecurityInitializer securityInitiali /// /// Marks this universe as disposed and ready to remove all child subscriptions /// - public void Dispose() + public virtual void Dispose() { DisposeRequested = true; } + /// + /// Event invocator for the event + /// + /// The current universe selection + protected void OnSelectionChanged(HashSet selection = null) + { + SelectionChanged?.Invoke(this, new SelectionEventArgs(selection ?? new HashSet())); + } + /// /// Provides a value to indicate that no changes should be made to the universe. /// This value is intended to be returned by reference via @@ -394,5 +411,24 @@ public Member(DateTime added, Security security) Security = security; } } + + /// + /// Event fired when the universe selection changes + /// + public class SelectionEventArgs : EventArgs + { + /// + /// The current universe selection + /// + public HashSet CurrentSelection { get; } + + /// + /// Creates a new instance + /// + public SelectionEventArgs(HashSet currentSelection) + { + CurrentSelection = currentSelection; + } + } } } \ No newline at end of file diff --git a/Common/Data/UniverseSelection/UniverseSettings.cs b/Common/Data/UniverseSelection/UniverseSettings.cs index 65ae5506c2b2..6aab09ef8d6f 100644 --- a/Common/Data/UniverseSelection/UniverseSettings.cs +++ b/Common/Data/UniverseSelection/UniverseSettings.cs @@ -74,5 +74,18 @@ public UniverseSettings(Resolution resolution, decimal leverage, bool fillForwar MinimumTimeInUniverse = minimumTimeInUniverse; DataNormalizationMode = dataNormalizationMode; } + + /// + /// Initializes a new instance of the class + /// + public UniverseSettings(UniverseSettings universeSettings) + { + Resolution = universeSettings.Resolution; + Leverage = universeSettings.Leverage; + FillForward = universeSettings.FillForward; + ExtendedMarketHours = universeSettings.ExtendedMarketHours; + MinimumTimeInUniverse = universeSettings.MinimumTimeInUniverse; + DataNormalizationMode = universeSettings.DataNormalizationMode; + } } } \ No newline at end of file diff --git a/Common/Data/UniverseSelection/UserDefinedUniverse.cs b/Common/Data/UniverseSelection/UserDefinedUniverse.cs index dae655270b10..57fe47d51600 100644 --- a/Common/Data/UniverseSelection/UserDefinedUniverse.cs +++ b/Common/Data/UniverseSelection/UserDefinedUniverse.cs @@ -239,7 +239,7 @@ public bool Remove(Symbol symbol) /// /// Returns the symbols defined by the user for this universe /// - /// The curren utc time + /// The current utc time /// The symbols to remain in the universe /// The data that passes the filter public override IEnumerable SelectSymbols(DateTime utcTime, BaseDataCollection data) @@ -278,8 +278,7 @@ public virtual IEnumerable GetTriggerTimes(DateTime startTimeUtc, Date /// The notify collection changed event arguments protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { - var handler = CollectionChanged; - if (handler != null) handler(this, e); + CollectionChanged?.Invoke(this, e); } /// diff --git a/Common/Extensions.cs b/Common/Extensions.cs index 0ef1a554b990..920939dba681 100644 --- a/Common/Extensions.cs +++ b/Common/Extensions.cs @@ -48,6 +48,8 @@ using Timer = System.Timers.Timer; using static QuantConnect.StringExtensions; using System.Runtime.CompilerServices; +using QuantConnect.Data.Auxiliary; +using QuantConnect.Securities.Option; namespace QuantConnect { @@ -2272,13 +2274,69 @@ public static OrderDirection GetOrderDirection(decimal quantity) var sign = Math.Sign(quantity); switch (sign) { - case 1: return OrderDirection.Buy; - case 0: return OrderDirection.Hold; + case 1: return OrderDirection.Buy; + case 0: return OrderDirection.Hold; case -1: return OrderDirection.Sell; - default: throw new ApplicationException( - $"The skies are falling and the oceans are rising! Math.Sign({quantity}) returned {sign} :/" - ); + default: + throw new ApplicationException( + $"The skies are falling and the oceans are rising! Math.Sign({quantity}) returned {sign} :/" + ); + } + } + + /// + /// Creates a for a given symbol + /// + /// The algorithm instance to create universes for + /// Symbol of the option + /// The option filter to use + /// The universe settings, will use algorithm settings if null + /// for the given symbol + public static OptionChainUniverse CreateOptionChain(this IAlgorithm algorithm, Symbol symbol, Func filter, UniverseSettings universeSettings = null) + { + if (symbol.SecurityType != SecurityType.Option) + { + throw new ArgumentException("CreateOptionChain requires an option symbol."); } + + // rewrite non-canonical symbols to be canonical + var market = symbol.ID.Market; + var underlying = symbol.Underlying; + if (!symbol.IsCanonical()) + { + var alias = $"?{underlying.Value}"; + symbol = Symbol.Create(underlying.Value, SecurityType.Option, market, alias); + } + + // resolve defaults if not specified + var settings = universeSettings ?? algorithm.UniverseSettings; + + // create canonical security object, but don't duplicate if it already exists + Security security; + Option optionChain; + if (!algorithm.Securities.TryGetValue(symbol, out security)) + { + var config = algorithm.SubscriptionManager.SubscriptionDataConfigService.Add( + typeof(ZipEntryName), + symbol, + settings.Resolution, + settings.FillForward, + settings.ExtendedMarketHours, + false); + optionChain = (Option)algorithm.Securities.CreateSecurity(symbol, config, settings.Leverage, false); + } + else + { + optionChain = (Option)security; + } + + // set the option chain contract filter function + optionChain.SetFilter(filter); + + // force option chain security to not be directly tradable AFTER it's configured to ensure it's not overwritten + optionChain.IsTradable = false; + + return new OptionChainUniverse(optionChain, settings, algorithm.LiveMode); } } } diff --git a/Common/Securities/Option/OptionFilterUniverse.cs b/Common/Securities/Option/OptionFilterUniverse.cs index 9ad1d50015b2..e290ff45107c 100644 --- a/Common/Securities/Option/OptionFilterUniverse.cs +++ b/Common/Securities/Option/OptionFilterUniverse.cs @@ -19,6 +19,7 @@ using QuantConnect.Data; using System.Linq; using System.Collections; +using Python.Runtime; using QuantConnect.Securities.Option; namespace QuantConnect.Securities @@ -342,6 +343,17 @@ public OptionFilterUniverse Expiration(int minExpiryDays, int maxExpiryDays) return Expiration(TimeSpan.FromDays(minExpiryDays), TimeSpan.FromDays(maxExpiryDays)); } + /// + /// Explicitly sets the selected contract symbols for this universe. + /// This overrides and and all other methods of selecting symbols assuming it is called last. + /// + /// The option contract symbol objects to select + public OptionFilterUniverse Contracts(PyObject contracts) + { + _allSymbols = contracts.ConvertToSymbolEnumerable(); + return 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/Engine/AlgorithmManager.cs b/Engine/AlgorithmManager.cs index 6b643260d58f..6ffe70b29033 100644 --- a/Engine/AlgorithmManager.cs +++ b/Engine/AlgorithmManager.cs @@ -1003,13 +1003,22 @@ private static void HandleDelistedSymbols(IAlgorithm algorithm, Delistings newDe security.IsTradable = false; security.IsDelisted = true; + // the subscription are getting removed from the data feed because they end // remove security from all universes foreach (var ukvp in algorithm.UniverseManager) { var universe = ukvp.Value; if (universe.ContainsMember(security.Symbol)) { - universe.RemoveMember(algorithm.UtcTime, security); + var userUniverse = universe as UserDefinedUniverse; + if (userUniverse != null) + { + userUniverse.Remove(security.Symbol); + } + else + { + universe.RemoveMember(algorithm.UtcTime, security); + } } } diff --git a/Engine/DataFeeds/DataManager.cs b/Engine/DataFeeds/DataManager.cs index 0da10f139d71..321ceb9e9013 100644 --- a/Engine/DataFeeds/DataManager.cs +++ b/Engine/DataFeeds/DataManager.cs @@ -179,7 +179,9 @@ public bool AddSubscription(SubscriptionRequest request) if (DataFeedSubscriptions.TryGetValue(request.Configuration, out subscription)) { // duplicate subscription request - return subscription.AddSubscriptionRequest(request); + subscription.AddSubscriptionRequest(request); + // only result true if the existing subscription is internal, we actually added something from the users perspective + return subscription.Configuration.IsInternalFeed; } // before adding the configuration to the data feed let's assert it's valid @@ -259,6 +261,14 @@ public bool RemoveSubscription(SubscriptionDataConfig configuration, Universe un return true; } } + else if (universe != null) + { + // a universe requested removal of a subscription which wasn't present anymore, this can happen when a subscription ends + // it will get removed from the data feed subscription list, but the configuration will remain until the universe removes it + // why? the effect I found is that the fill models are using these subscriptions to determine which data they could use + SubscriptionDataConfig config; + _subscriptionManagerSubscriptions.TryRemove(configuration, out config); + } return false; } @@ -342,6 +352,7 @@ public SubscriptionDataConfig SubscriptionManagerGetOrAdd(SubscriptionDataConfig /// The owning the configuration to remove private void RemoveSubscriptionDataConfig(Subscription subscription) { + // the subscription could of ended but might still be part of the universe if (subscription.RemovedFromUniverse.Value) { SubscriptionDataConfig config; diff --git a/Engine/DataFeeds/SubscriptionSynchronizer.cs b/Engine/DataFeeds/SubscriptionSynchronizer.cs index 20501546582b..9c39d872d0e6 100644 --- a/Engine/DataFeeds/SubscriptionSynchronizer.cs +++ b/Engine/DataFeeds/SubscriptionSynchronizer.cs @@ -237,8 +237,7 @@ public IEnumerable Sync(IEnumerable subscriptions, /// protected virtual void OnSubscriptionFinished(Subscription subscription) { - var handler = SubscriptionFinished; - if (handler != null) handler(this, subscription); + SubscriptionFinished?.Invoke(this, subscription); } /// diff --git a/Engine/DataFeeds/UniverseSelection.cs b/Engine/DataFeeds/UniverseSelection.cs index 6f2c55d95c45..91b7f580fe57 100644 --- a/Engine/DataFeeds/UniverseSelection.cs +++ b/Engine/DataFeeds/UniverseSelection.cs @@ -26,8 +26,6 @@ using QuantConnect.Securities; using QuantConnect.Util; using QuantConnect.Data.Fundamental; -using QuantConnect.Securities.Future; -using QuantConnect.Securities.Option; namespace QuantConnect.Lean.Engine.DataFeeds { @@ -318,12 +316,10 @@ public SecurityChanges ApplyUniverseSelection(Universe universe, DateTime dateTi security = _securityService.CreateSecurity(symbol, configs, universe.UniverseSettings.Leverage, symbol.ID.SecurityType == SecurityType.Option); pendingAdditions.Add(symbol, security); - - SetUnderlyingSecurity(universe, security); } var addedSubscription = false; - + var dataFeedAdded = false; foreach (var request in universe.GetSubscriptionRequests(security, dateTimeUtc, algorithmEndDateUtc, _algorithm.SubscriptionManager.SubscriptionDataConfigService)) { @@ -341,7 +337,9 @@ public SecurityChanges ApplyUniverseSelection(Universe universe, DateTime dateTi _dataManager.RemoveSubscription(toRemove); } - _dataManager.AddSubscription(request); + // 'dataFeedAdded' will help us notify the user for security changes only once per non internal subscription + // for example two universes adding the sample configuration, we don't want two notifications + dataFeedAdded = _dataManager.AddSubscription(request); // only update our security changes if we actually added data if (!request.IsUniverseSubscription) @@ -356,7 +354,7 @@ public SecurityChanges ApplyUniverseSelection(Universe universe, DateTime dateTi { var addedMember = universe.AddMember(dateTimeUtc, security); - if (addedMember) + if (addedMember && dataFeedAdded) { additions.Add(security); } @@ -495,36 +493,5 @@ private void RemoveSecurityFromUniverse( SymbolCache.TryRemove(member.Symbol); } } - - /// - /// This method sets the underlying security for and - /// - private void SetUnderlyingSecurity(Universe universe, Security security) - { - var optionChainUniverse = universe as OptionChainUniverse; - var futureChainUniverse = universe as FuturesChainUniverse; - if (optionChainUniverse != null) - { - if (!security.Symbol.HasUnderlying) - { - // create the underlying w/ raw mode - security.SetDataNormalizationMode(DataNormalizationMode.Raw); - optionChainUniverse.Option.Underlying = security; - } - else - { - // set the underlying security and pricing model from the canonical security - var option = (Option)security; - option.Underlying = optionChainUniverse.Option.Underlying; - option.PriceModel = optionChainUniverse.Option.PriceModel; - } - } - else if (futureChainUniverse != null) - { - // set the underlying security and pricing model from the canonical security - var future = (Future)security; - future.Underlying = futureChainUniverse.Future.Underlying; - } - } } } \ No newline at end of file diff --git a/Tests/Engine/DataFeeds/MockDataFeed.cs b/Tests/Engine/DataFeeds/MockDataFeed.cs index 584c4aae4095..280678c59bd8 100644 --- a/Tests/Engine/DataFeeds/MockDataFeed.cs +++ b/Tests/Engine/DataFeeds/MockDataFeed.cs @@ -14,6 +14,7 @@ * */ +using System.Collections.Generic; using QuantConnect.Data.UniverseSelection; using QuantConnect.Interfaces; using QuantConnect.Lean.Engine.Results; @@ -23,6 +24,7 @@ namespace QuantConnect.Lean.Engine.DataFeeds { public class MockDataFeed : IDataFeed { + private List _dummyData = new List(); public bool IsActive { get; } public void Initialize( @@ -41,7 +43,10 @@ IDataChannelProvider channelProvider public Subscription CreateSubscription(SubscriptionRequest request) { - return null; + var offsetProvider = new TimeZoneOffsetProvider(request.Configuration.ExchangeTimeZone, + request.StartTimeUtc, + request.EndTimeUtc); + return new Subscription(request, _dummyData.GetEnumerator(), offsetProvider); } public void RemoveSubscription(Subscription subscription) From ad36e1ca26af52911004af22252deb9bfa9c2177 Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Fri, 2 Oct 2020 15:34:03 -0300 Subject: [PATCH 2/5] Add check for option underlying price is set --- .../CoarseFineOptionUniverseChainRegressionAlgorithm.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs b/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs index 809190dcbdbf..fa7af743a980 100644 --- a/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs +++ b/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs @@ -49,6 +49,10 @@ public override void Initialize() AddChainedEquityOptionUniverseSelectionModel(selectionUniverse, universe => { + if (universe.Underlying == null) + { + throw new Exception("Underlying data point is null! This shouldn't happen, each OptionChainUniverse handles and should provide this"); + } return universe.IncludeWeeklys() .FrontMonth() .Contracts(universe.Take(5)); From 3fbe6f8fcd7b3502ac9c9a6dd494afb7439a7ca0 Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Mon, 5 Oct 2020 20:13:50 -0300 Subject: [PATCH 3/5] Address reviews - Adding python regression algorithm for `AddOptionContractFromUniverseRegressionAlgorithm` and `AddOptionContractExpiresRegressionAlgorithm` - Rename QCAlgorithm new api method to `AddChainedOptionUniverse` --- ...ptionContractExpiresRegressionAlgorithm.cs | 17 ++-- ...ContractFromUniverseRegressionAlgorithm.cs | 4 +- ...eOptionUniverseChainRegressionAlgorithm.cs | 2 +- ...ptionContractExpiresRegressionAlgorithm.py | 67 ++++++++++++++ ...ContractFromUniverseRegressionAlgorithm.py | 87 +++++++++++++++++++ ...eOptionUniverseChainRegressionAlgorithm.py | 2 +- .../QuantConnect.Algorithm.Python.csproj | 2 + Algorithm/QCAlgorithm.Python.cs | 4 +- Algorithm/QCAlgorithm.Universe.cs | 2 +- 9 files changed, 171 insertions(+), 16 deletions(-) create mode 100644 Algorithm.Python/AddOptionContractExpiresRegressionAlgorithm.py create mode 100644 Algorithm.Python/AddOptionContractFromUniverseRegressionAlgorithm.py diff --git a/Algorithm.CSharp/AddOptionContractExpiresRegressionAlgorithm.cs b/Algorithm.CSharp/AddOptionContractExpiresRegressionAlgorithm.cs index b2f83549d5e4..a061e938437a 100644 --- a/Algorithm.CSharp/AddOptionContractExpiresRegressionAlgorithm.cs +++ b/Algorithm.CSharp/AddOptionContractExpiresRegressionAlgorithm.cs @@ -18,7 +18,6 @@ using QuantConnect.Data; using QuantConnect.Interfaces; using System.Collections.Generic; -using QuantConnect.Securities.Option; namespace QuantConnect.Algorithm.CSharp { @@ -29,7 +28,7 @@ namespace QuantConnect.Algorithm.CSharp public class AddOptionContractExpiresRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition { private DateTime _expiration = new DateTime(2014, 06, 21); - private Option _option; + private Symbol _option; private Symbol _twx; private bool _traded; @@ -54,16 +53,16 @@ public override void OnData(Slice data) && optionContract.ID.OptionStyle == OptionStyle.American); if (option != null) { - _option = AddOptionContract(option); + _option = AddOptionContract(option).Symbol; } } - if (!Portfolio.Invested && _option != null && Securities[_option.Symbol].Price != 0 && !_traded) + if (_option != null && Securities[_option].Price != 0 && !_traded) { _traded = true; - Buy(_option.Symbol, 1); + Buy(_option, 1); - foreach (var symbol in new [] { _option.Symbol, _option.Symbol.Underlying }) + foreach (var symbol in new [] { _option, _option.Underlying }) { var config = SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(symbol).ToList(); @@ -80,9 +79,9 @@ public override void OnData(Slice data) if (Time.Date > _expiration) { - if (SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(_option.Symbol).Any()) + if (SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(_option).Any()) { - throw new Exception($"Unexpected configurations for {_option.Symbol} after it has been delisted"); + throw new Exception($"Unexpected configurations for {_option} after it has been delisted"); } if (Securities[_twx].Invested) @@ -113,7 +112,7 @@ public override void OnData(Slice data) /// /// This is used by the regression test system to indicate which languages this algorithm is written in. /// - public Language[] Languages { get; } = { Language.CSharp }; + public Language[] Languages { get; } = { Language.CSharp, Language.Python }; /// /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm diff --git a/Algorithm.CSharp/AddOptionContractFromUniverseRegressionAlgorithm.cs b/Algorithm.CSharp/AddOptionContractFromUniverseRegressionAlgorithm.cs index 2839f06a0112..43f74c3f6145 100644 --- a/Algorithm.CSharp/AddOptionContractFromUniverseRegressionAlgorithm.cs +++ b/Algorithm.CSharp/AddOptionContractFromUniverseRegressionAlgorithm.cs @@ -52,7 +52,7 @@ public override void Initialize() public override void OnData(Slice data) { - if (!Portfolio.Invested && _option != null && Securities[_option].Price != 0 && !_traded) + if (_option != null && Securities[_option].Price != 0 && !_traded) { _traded = true; Buy(_option, 1); @@ -164,7 +164,7 @@ public override void OnEndOfAlgorithm() /// /// This is used by the regression test system to indicate which languages this algorithm is written in. /// - public Language[] Languages { get; } = { Language.CSharp }; + public Language[] Languages { get; } = { Language.CSharp, Language.Python }; /// /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm diff --git a/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs b/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs index fa7af743a980..a71fb8e9b0b5 100644 --- a/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs +++ b/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs @@ -47,7 +47,7 @@ public override void Initialize() var selectionUniverse = AddUniverse(enumerable => new[] { Time.Date <= new DateTime(2014, 6, 5) ? _twx : _aapl }, enumerable => new[] { Time.Date <= new DateTime(2014, 6, 5) ? _twx : _aapl }); - AddChainedEquityOptionUniverseSelectionModel(selectionUniverse, universe => + AddChainedOptionUniverse(selectionUniverse, universe => { if (universe.Underlying == null) { diff --git a/Algorithm.Python/AddOptionContractExpiresRegressionAlgorithm.py b/Algorithm.Python/AddOptionContractExpiresRegressionAlgorithm.py new file mode 100644 index 000000000000..4f6c9e47507b --- /dev/null +++ b/Algorithm.Python/AddOptionContractExpiresRegressionAlgorithm.py @@ -0,0 +1,67 @@ +# 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 clr import AddReference +AddReference("System") +AddReference("QuantConnect.Algorithm") +AddReference("QuantConnect.Common") + +from System import * +from QuantConnect import * +from QuantConnect.Algorithm import * +from datetime import * + +### +### We add an option contract using 'QCAlgorithm.AddOptionContract' and place a trade, the underlying +### gets deselected from the universe selection but should still be present since we manually added the option contract. +### Later we call 'QCAlgorithm.RemoveOptionContract' and expect both option and underlying to be removed. +### +class AddOptionContractExpiresRegressionAlgorithm(QCAlgorithm): + def Initialize(self): + '''Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.''' + + self.SetStartDate(2014, 6, 5) + self.SetEndDate(2014, 6, 30) + + self._expiration = datetime(2014, 6, 21) + self._option = None + self._traded = False + + self._twx = Symbol.Create("TWX", SecurityType.Equity, Market.USA) + + self.AddUniverse("my-daily-universe-name", self.Selector) + + def Selector(self, time): + return [ "AAPL" ] + + def OnData(self, data): + '''OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. + + Arguments: + data: Slice object keyed by symbol containing the stock data + ''' + if self._option == None: + options = self.OptionChainProvider.GetOptionContractList(self._twx, self.Time) + options = sorted(options, key=lambda x: x.ID.Symbol) + + option = next((option for option in options if option.ID.Date == self._expiration and option.ID.OptionRight == OptionRight.Call and option.ID.OptionStyle == OptionStyle.American), None) + if option != None: + self._option = self.AddOptionContract(option).Symbol; + + if self._option != None and self.Securities[self._option].Price != 0 and not self._traded: + self._traded = True; + self.Buy(self._option, 1); + + if self.Time > self._expiration and self.Securities[self._twx].Invested: + # we liquidate the option exercised position + self.Liquidate(self._twx); \ No newline at end of file diff --git a/Algorithm.Python/AddOptionContractFromUniverseRegressionAlgorithm.py b/Algorithm.Python/AddOptionContractFromUniverseRegressionAlgorithm.py new file mode 100644 index 000000000000..878cc5208daa --- /dev/null +++ b/Algorithm.Python/AddOptionContractFromUniverseRegressionAlgorithm.py @@ -0,0 +1,87 @@ +# 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 clr import AddReference +AddReference("System") +AddReference("QuantConnect.Algorithm") +AddReference("QuantConnect.Common") + +from System import * +from QuantConnect import * +from QuantConnect.Algorithm import * +from datetime import * + +### +### We add an option contract using 'QCAlgorithm.AddOptionContract' and place a trade, the underlying +### gets deselected from the universe selection but should still be present since we manually added the option contract. +### Later we call 'QCAlgorithm.RemoveOptionContract' and expect both option and underlying to be removed. +### +class AddOptionContractFromUniverseRegressionAlgorithm(QCAlgorithm): + def Initialize(self): + '''Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.''' + + self.SetStartDate(2014, 6, 5) + self.SetEndDate(2014, 6, 9) + + self._expiration = datetime(2014, 6, 21) + self._securityChanges = None + self._option = None + self._traded = False + + self._twx = Symbol.Create("TWX", SecurityType.Equity, Market.USA) + self._aapl = Symbol.Create("AAPL", SecurityType.Equity, Market.USA) + self.UniverseSettings.Resolution = Resolution.Minute + self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw + + self.AddUniverse(self.Selector, self.Selector) + + def Selector(self, fundamental): + if self.Time <= datetime(2014, 6, 5): + return [ self._twx ] + return [ self._aapl ] + + def OnData(self, data): + '''OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. + + Arguments: + data: Slice object keyed by symbol containing the stock data + ''' + if self._option != None and self.Securities[self._option].Price != 0 and not self._traded: + self._traded = True; + self.Buy(self._option, 1); + + if self.Time == datetime(2014, 6, 6, 14, 0, 0): + # liquidate & remove the option + self.RemoveOptionContract(self._option) + + def OnSecuritiesChanged(self, changes): + # keep track of all removed and added securities + if self._securityChanges == None: + self._securityChanges = changes + else: + self._securityChanges.op_Addition(self._securityChanges, changes) + + if any(security.Symbol.SecurityType == SecurityType.Option for security in changes.AddedSecurities): + return + + for addedSecurity in changes.AddedSecurities: + options = self.OptionChainProvider.GetOptionContractList(addedSecurity.Symbol, self.Time) + options = sorted(options, key=lambda x: x.ID.Symbol) + + option = next((option for option in options if option.ID.Date == self._expiration and option.ID.OptionRight == OptionRight.Call and option.ID.OptionStyle == OptionStyle.American), None) + + self.AddOptionContract(option) + + # just keep the first we got + if self._option == None: + self._option = option \ No newline at end of file diff --git a/Algorithm.Python/CoarseFineOptionUniverseChainRegressionAlgorithm.py b/Algorithm.Python/CoarseFineOptionUniverseChainRegressionAlgorithm.py index 0cc37a9bad5d..f35f10d5c078 100644 --- a/Algorithm.Python/CoarseFineOptionUniverseChainRegressionAlgorithm.py +++ b/Algorithm.Python/CoarseFineOptionUniverseChainRegressionAlgorithm.py @@ -44,7 +44,7 @@ def Initialize(self): universe = self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) - self.AddChainedEquityOptionUniverseSelectionModel(universe, self.OptionFilterFunction) + self.AddChainedOptionUniverse(universe, self.OptionFilterFunction) def OptionFilterFunction(self, universe): universe.IncludeWeeklys().FrontMonth() diff --git a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj index 3da19fa44384..3b076a935c99 100644 --- a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj +++ b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj @@ -46,6 +46,8 @@ + + diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs index f69d670b95e4..9f58fe5ef04d 100644 --- a/Algorithm/QCAlgorithm.Python.cs +++ b/Algorithm/QCAlgorithm.Python.cs @@ -448,14 +448,14 @@ public Universe AddUniverse(Type dataType, SecurityType securityType, string nam /// /// The universe we want to chain an option universe selection model too /// The option filter universe to use - public void AddChainedEquityOptionUniverseSelectionModel(PyObject universe, PyObject optionFilter) + public void AddChainedOptionUniverse(PyObject universe, PyObject optionFilter) { Func convertedOptionChain; Universe universeToChain; if (universe.TryConvert(out universeToChain) && optionFilter.TryConvertToDelegate(out convertedOptionChain)) { - AddChainedEquityOptionUniverseSelectionModel(universeToChain, convertedOptionChain); + AddChainedOptionUniverse(universeToChain, convertedOptionChain); } else { diff --git a/Algorithm/QCAlgorithm.Universe.cs b/Algorithm/QCAlgorithm.Universe.cs index d9194d996686..51f200885a33 100644 --- a/Algorithm/QCAlgorithm.Universe.cs +++ b/Algorithm/QCAlgorithm.Universe.cs @@ -457,7 +457,7 @@ public Universe AddUniverse(SecurityType securityType, string name, Resolution r /// /// The universe we want to chain an option universe selection model too /// The option filter universe to use - public void AddChainedEquityOptionUniverseSelectionModel(Universe universe, Func optionFilter) + public void AddChainedOptionUniverse(Universe universe, Func optionFilter) { AddUniverseSelection(new OptionChainedUniverseSelectionModel(universe, optionFilter)); } From dcef6140a1e4938787546f25e12dfee364fe4e3c Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Wed, 7 Oct 2020 18:57:56 -0300 Subject: [PATCH 4/5] Fix universe refresh bug - Fix bug where a universe selection refresh would cause option or future chain universes from being removed. Adding regression algorithm reproducing the issue. --- ...AndUniverseSelectionRegressionAlgorithm.cs | 142 ++++++++++++++++++ .../QuantConnect.Algorithm.CSharp.csproj | 3 +- Algorithm/QCAlgorithm.cs | 4 +- 3 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 Algorithm.CSharp/OptionChainedAndUniverseSelectionRegressionAlgorithm.cs diff --git a/Algorithm.CSharp/OptionChainedAndUniverseSelectionRegressionAlgorithm.cs b/Algorithm.CSharp/OptionChainedAndUniverseSelectionRegressionAlgorithm.cs new file mode 100644 index 000000000000..57327f61ae72 --- /dev/null +++ b/Algorithm.CSharp/OptionChainedAndUniverseSelectionRegressionAlgorithm.cs @@ -0,0 +1,142 @@ +/* + * 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 QuantConnect.Data; +using QuantConnect.Interfaces; +using System.Collections.Generic; +using QuantConnect.Algorithm.Framework.Selection; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm making sure that the added universe selection does not remove the option chain during it's daily refresh + /// + public class OptionChainedAndUniverseSelectionRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private Symbol _aaplOption; + + public override void Initialize() + { + UniverseSettings.Resolution = Resolution.Minute; + + SetStartDate(2014, 06, 05); + SetEndDate(2014, 06, 09); + + _aaplOption = AddOption("AAPL").Symbol; + AddUniverseSelection(new DailyUniverseSelectionModel("MyCustomSelectionModel", time => new[] { "AAPL" }, this)); + } + + public override void OnData(Slice data) + { + if (!Portfolio.Invested) + { + Buy("AAPL", 1); + } + } + + public override void OnEndOfAlgorithm() + { + var config = SubscriptionManager.Subscriptions.ToList(); + if (config.All(dataConfig => dataConfig.Symbol != "AAPL")) + { + throw new Exception("Was expecting configurations for AAPL"); + } + if (config.All(dataConfig => dataConfig.Symbol.SecurityType != SecurityType.Option)) + { + throw new Exception($"Was expecting configurations for {_aaplOption}"); + } + } + + private class DailyUniverseSelectionModel : CustomUniverseSelectionModel + { + private DateTime _lastRefresh; + private IAlgorithm _algorithm; + + public DailyUniverseSelectionModel(string name, Func> selector, IAlgorithm algorithm) : base(name, selector) + { + _algorithm = algorithm; + } + + public override DateTime GetNextRefreshTimeUtc() + { + if (_lastRefresh != _algorithm.Time.Date) + { + _lastRefresh = _algorithm.Time.Date; + return DateTime.MinValue; + } + return DateTime.MaxValue; + } + } + + /// + /// 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 Language[] Languages { get; } = { Language.CSharp }; + + /// + /// 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 Trades", "1"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0.678%"}, + {"Drawdown", "3.700%"}, + {"Expectancy", "0"}, + {"Net Profit", "0.009%"}, + {"Sharpe Ratio", "7.969"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0.046"}, + {"Beta", "-0.032"}, + {"Annual Standard Deviation", "0.001"}, + {"Annual Variance", "0"}, + {"Information Ratio", "-24.461"}, + {"Tracking Error", "0.044"}, + {"Treynor Ratio", "-0.336"}, + {"Total Fees", "$1.00"}, + {"Fitness Score", "0.003"}, + {"Kelly Criterion Estimate", "0"}, + {"Kelly Criterion Probability Value", "0"}, + {"Sortino Ratio", "79228162514264337593543950335"}, + {"Return Over Maximum Drawdown", "79228162514264337593543950335"}, + {"Portfolio Turnover", "0.003"}, + {"Total Insights Generated", "0"}, + {"Total Insights Closed", "0"}, + {"Total Insights Analysis Completed", "0"}, + {"Long Insight Count", "0"}, + {"Short Insight Count", "0"}, + {"Long/Short Ratio", "100%"}, + {"Estimated Monthly Alpha Value", "$0"}, + {"Total Accumulated Estimated Alpha Value", "$0"}, + {"Mean Population Estimated Insight Value", "$0"}, + {"Mean Population Direction", "0%"}, + {"Mean Population Magnitude", "0%"}, + {"Rolling Averaged Population Direction", "0%"}, + {"Rolling Averaged Population Magnitude", "0%"}, + {"OrderListHash", "-1779427412"} + }; + } +} diff --git a/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj b/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj index 413a27fe7fe1..e72f29310f7c 100644 --- a/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj +++ b/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj @@ -149,6 +149,7 @@ + @@ -462,4 +463,4 @@ --> - + \ No newline at end of file diff --git a/Algorithm/QCAlgorithm.cs b/Algorithm/QCAlgorithm.cs index 49c595ac6600..4cdaaa39965d 100644 --- a/Algorithm/QCAlgorithm.cs +++ b/Algorithm/QCAlgorithm.cs @@ -1500,7 +1500,8 @@ public Security AddSecurity(Symbol symbol, Resolution? resolution = null, bool f { universe = new FuturesChainUniverse((Future)security, settings); } - _pendingUniverseAdditions.Add(universe); + + AddUniverse(universe); } return security; } @@ -1776,6 +1777,7 @@ public bool RemoveSecurity(Symbol symbol) // finally, dispose and remove the canonical security from the universe manager UniverseManager.Remove(symbol); + _userAddedUniverses.Remove(symbol); } } else From 352a67cd1d6f8c733ad759a28553d115cf5376da Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Fri, 9 Oct 2020 10:13:26 -0300 Subject: [PATCH 5/5] Rename new option universe Algorithm API method - Rename new option universe Algorith API method from AddChainedOptionUniverse to AddUniverseOptions - Rebase and update regression test order hash because of option expiration message changed --- .../AddOptionContractExpiresRegressionAlgorithm.cs | 2 +- .../CoarseFineOptionUniverseChainRegressionAlgorithm.cs | 4 ++-- .../CoarseFineOptionUniverseChainRegressionAlgorithm.py | 2 +- Algorithm/QCAlgorithm.Python.cs | 4 ++-- Algorithm/QCAlgorithm.Universe.cs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Algorithm.CSharp/AddOptionContractExpiresRegressionAlgorithm.cs b/Algorithm.CSharp/AddOptionContractExpiresRegressionAlgorithm.cs index a061e938437a..9f1a3a946c5b 100644 --- a/Algorithm.CSharp/AddOptionContractExpiresRegressionAlgorithm.cs +++ b/Algorithm.CSharp/AddOptionContractExpiresRegressionAlgorithm.cs @@ -158,7 +158,7 @@ public override void OnData(Slice data) {"Mean Population Magnitude", "0%"}, {"Rolling Averaged Population Direction", "0%"}, {"Rolling Averaged Population Magnitude", "0%"}, - {"OrderListHash", "1472204120"} + {"OrderListHash", "1123164511"} }; } } diff --git a/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs b/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs index a71fb8e9b0b5..b21a1d691a80 100644 --- a/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs +++ b/Algorithm.CSharp/CoarseFineOptionUniverseChainRegressionAlgorithm.cs @@ -47,7 +47,7 @@ public override void Initialize() var selectionUniverse = AddUniverse(enumerable => new[] { Time.Date <= new DateTime(2014, 6, 5) ? _twx : _aapl }, enumerable => new[] { Time.Date <= new DateTime(2014, 6, 5) ? _twx : _aapl }); - AddChainedOptionUniverse(selectionUniverse, universe => + AddUniverseOptions(selectionUniverse, universe => { if (universe.Underlying == null) { @@ -120,7 +120,7 @@ public override void OnEndOfAlgorithm() var config = SubscriptionManager.Subscriptions.ToList(); if (config.Any(dataConfig => dataConfig.Symbol == _twx || dataConfig.Symbol.Underlying == _twx)) { - throw new Exception($"Was NOT expecting any configurations for {_twx} or it's options, since coarse/fine should deselected it"); + throw new Exception($"Was NOT expecting any configurations for {_twx} or it's options, since coarse/fine should have deselected it"); } if (_optionCount == 0) diff --git a/Algorithm.Python/CoarseFineOptionUniverseChainRegressionAlgorithm.py b/Algorithm.Python/CoarseFineOptionUniverseChainRegressionAlgorithm.py index f35f10d5c078..b80491b603c0 100644 --- a/Algorithm.Python/CoarseFineOptionUniverseChainRegressionAlgorithm.py +++ b/Algorithm.Python/CoarseFineOptionUniverseChainRegressionAlgorithm.py @@ -44,7 +44,7 @@ def Initialize(self): universe = self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) - self.AddChainedOptionUniverse(universe, self.OptionFilterFunction) + self.AddUniverseOptions(universe, self.OptionFilterFunction) def OptionFilterFunction(self, universe): universe.IncludeWeeklys().FrontMonth() diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs index 9f58fe5ef04d..2c372cc5ae61 100644 --- a/Algorithm/QCAlgorithm.Python.cs +++ b/Algorithm/QCAlgorithm.Python.cs @@ -448,14 +448,14 @@ public Universe AddUniverse(Type dataType, SecurityType securityType, string nam /// /// The universe we want to chain an option universe selection model too /// The option filter universe to use - public void AddChainedOptionUniverse(PyObject universe, PyObject optionFilter) + public void AddUniverseOptions(PyObject universe, PyObject optionFilter) { Func convertedOptionChain; Universe universeToChain; if (universe.TryConvert(out universeToChain) && optionFilter.TryConvertToDelegate(out convertedOptionChain)) { - AddChainedOptionUniverse(universeToChain, convertedOptionChain); + AddUniverseOptions(universeToChain, convertedOptionChain); } else { diff --git a/Algorithm/QCAlgorithm.Universe.cs b/Algorithm/QCAlgorithm.Universe.cs index 51f200885a33..f1c460ba297f 100644 --- a/Algorithm/QCAlgorithm.Universe.cs +++ b/Algorithm/QCAlgorithm.Universe.cs @@ -457,7 +457,7 @@ public Universe AddUniverse(SecurityType securityType, string name, Resolution r /// /// The universe we want to chain an option universe selection model too /// The option filter universe to use - public void AddChainedOptionUniverse(Universe universe, Func optionFilter) + public void AddUniverseOptions(Universe universe, Func optionFilter) { AddUniverseSelection(new OptionChainedUniverseSelectionModel(universe, optionFilter)); }