diff --git a/Algorithm.CSharp/MultipleUniverseSelectionOrderRegressionAlgorithm.cs b/Algorithm.CSharp/MultipleUniverseSelectionOrderRegressionAlgorithm.cs new file mode 100644 index 000000000000..85cce43ab576 --- /dev/null +++ b/Algorithm.CSharp/MultipleUniverseSelectionOrderRegressionAlgorithm.cs @@ -0,0 +1,121 @@ +/* + * 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.Linq; +using System.Collections.Generic; +using QuantConnect.Data; +using QuantConnect.Data.Fundamental; +using QuantConnect.Interfaces; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm asserting that multiple universe selection functions are called + /// in the order the universes were added to the algorithm + /// + public class MultipleUniverseSelectionOrderRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private int _selectionCallCount; + + public override void Initialize() + { + SetStartDate(2014, 3, 24); + SetEndDate(2014, 3, 28); + UniverseSettings.Resolution = Resolution.Daily; + + AddUniverse(SelectAssets1); + AddUniverse(SelectAssets2); + AddUniverse(SelectAssets3); + } + + private IEnumerable SelectAssets1(IEnumerable fundamentals) + { + ValidateSelectionOrder(1); + return Enumerable.Empty(); + } + + private IEnumerable SelectAssets2(IEnumerable fundamentals) + { + ValidateSelectionOrder(2); + return Enumerable.Empty(); + } + + private IEnumerable SelectAssets3(IEnumerable fundamentals) + { + ValidateSelectionOrder(3); + return Enumerable.Empty(); + } + + private void ValidateSelectionOrder(int universeIndex) + { + var expectedPositionInCycle = universeIndex - 1; + if (_selectionCallCount % 3 != expectedPositionInCycle) + { + throw new RegressionTestException($"Universes are not being selected in the order they were added. Expected universe {expectedPositionInCycle + 1} but got universe {universeIndex}."); + } + _selectionCallCount++; + } + + public override void OnEndOfAlgorithm() + { + if (_selectionCallCount < 3) + { + throw new RegressionTestException($"Expected all 3 universes to be selected at least once, but got {_selectionCallCount} calls."); + } + } + + public bool CanRunLocally { get; } = true; + + public List Languages { get; } = new() { Language.CSharp }; + + public long DataPoints => -1; + + public int AlgorithmHistoryDataPoints => 0; + + public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + public Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "0"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000"}, + {"End Equity", "100000"}, + {"Net Profit", "0%"}, + {"Sharpe Ratio", "0"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0"}, + {"Beta", "0"}, + {"Annual Standard Deviation", "0"}, + {"Annual Variance", "0"}, + {"Information Ratio", "-0.404"}, + {"Tracking Error", "0.094"}, + {"Treynor Ratio", "0"}, + {"Total Fees", "$0.00"}, + {"Estimated Strategy Capacity", "$0"}, + {"Lowest Capacity Asset", ""}, + {"Portfolio Turnover", "0%"}, + {"Drawdown Recovery", "0"}, + {"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"} + }; + } +} diff --git a/Algorithm/QCAlgorithm.Universe.cs b/Algorithm/QCAlgorithm.Universe.cs index 3f249e067498..7454e9156fdd 100644 --- a/Algorithm/QCAlgorithm.Universe.cs +++ b/Algorithm/QCAlgorithm.Universe.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Linq; using System.Collections.Specialized; +using System.Threading; using NodaTime; using QuantConnect.Algorithm.Selection; using QuantConnect.Data; @@ -33,6 +34,7 @@ public partial class QCAlgorithm // save universe additions and apply at end of time step // this removes temporal dependencies from w/in initialize method // original motivation: adding equity/options to enforce equity raw data mode + private static int _universeCount; private readonly object _pendingUniverseAdditionsLock = new object(); private readonly List _pendingUserDefinedUniverseSecurityChanges = new(); private bool _pendingUniverseAdditions; @@ -686,7 +688,7 @@ private SubscriptionDataConfig GetCustomUniverseConfiguration(Type dataType, str market ??= Market.USA; if (string.IsNullOrEmpty(name)) { - name = $"{dataType.Name}-{market}-{Guid.NewGuid()}"; + name = $"{dataType.Name}-{market}-{Interlocked.Increment(ref _universeCount):D10}-{Guid.NewGuid()}"; } // same as 'AddData<>' 'T' type will be treated as custom/base data type with always open market hours universeSymbol = QuantConnect.Symbol.Create(name, SecurityType.Base, market, baseDataType: dataType); diff --git a/Common/Data/Fundamental/FundamentalUniverse.cs b/Common/Data/Fundamental/FundamentalUniverse.cs index cea55689c360..cadc8c52a715 100644 --- a/Common/Data/Fundamental/FundamentalUniverse.cs +++ b/Common/Data/Fundamental/FundamentalUniverse.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; +using System.Threading; using Python.Runtime; using QuantConnect.Data.UniverseSelection; @@ -31,6 +32,7 @@ public class Fundamentals : FundamentalUniverse { } /// public class FundamentalUniverse : BaseDataCollection { + private static int _universeCount; private static readonly Fundamental _factory = new(); /// @@ -106,7 +108,7 @@ public override Resolution DefaultResolution() public override Symbol UniverseSymbol(string market = null) { market ??= QuantConnect.Market.USA; - var ticker = $"{GetType().Name}-{market}-{Guid.NewGuid()}"; + var ticker = $"{GetType().Name}-{market}-{Interlocked.Increment(ref _universeCount):D10}-{Guid.NewGuid()}"; return Symbol.Create(ticker, SecurityType.Equity, market, baseDataType: GetType()); } diff --git a/Common/Data/UniverseSelection/BaseDataCollection.cs b/Common/Data/UniverseSelection/BaseDataCollection.cs index 98a39cc8f73f..973f7d5d1733 100644 --- a/Common/Data/UniverseSelection/BaseDataCollection.cs +++ b/Common/Data/UniverseSelection/BaseDataCollection.cs @@ -18,6 +18,7 @@ using System.Linq; using System.Collections; using System.Collections.Generic; +using System.Threading; using QuantConnect.Python; namespace QuantConnect.Data.UniverseSelection @@ -27,6 +28,7 @@ namespace QuantConnect.Data.UniverseSelection /// public class BaseDataCollection : BaseData, IEnumerable { + private static int _universeCount; private DateTime _endTime; /// @@ -152,7 +154,7 @@ public BaseDataCollection(BaseDataCollection other) public virtual Symbol UniverseSymbol(string market = null) { market ??= QuantConnect.Market.USA; - var ticker = $"{GetType().Name}-{market}-{Guid.NewGuid()}"; + var ticker = $"{GetType().Name}-{market}-{Interlocked.Increment(ref _universeCount):D10}-{Guid.NewGuid()}"; return Symbol.Create(ticker, SecurityType.Base, market, baseDataType: GetType()); } diff --git a/Common/Data/UniverseSelection/ETFConstituentsUniverseFactory.cs b/Common/Data/UniverseSelection/ETFConstituentsUniverseFactory.cs index d398c9ad27d6..210aea4323cf 100644 --- a/Common/Data/UniverseSelection/ETFConstituentsUniverseFactory.cs +++ b/Common/Data/UniverseSelection/ETFConstituentsUniverseFactory.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Python.Runtime; namespace QuantConnect.Data.UniverseSelection @@ -25,6 +26,7 @@ namespace QuantConnect.Data.UniverseSelection /// public class ETFConstituentsUniverseFactory : ConstituentsUniverse { + private static int _universeCount; private const string _etfConstituentsUniverseIdentifier = "qc-universe-etf-constituents"; /// @@ -56,8 +58,7 @@ public ETFConstituentsUniverseFactory(Symbol symbol, UniverseSettings universeSe /// Universe Symbol with ETF set as underlying private static Symbol CreateConstituentUniverseETFSymbol(Symbol compositeSymbol) { - var guid = Guid.NewGuid().ToString(); - var universeTicker = _etfConstituentsUniverseIdentifier + '-' + guid; + var universeTicker = $"{_etfConstituentsUniverseIdentifier}-{Interlocked.Increment(ref _universeCount):D10}-{Guid.NewGuid()}"; return new Symbol( SecurityIdentifier.GenerateConstituentIdentifier( diff --git a/Tests/Algorithm/AlgorithmAddUniverseTests.cs b/Tests/Algorithm/AlgorithmAddUniverseTests.cs index fcd21fcaf2ea..9d7c93e0fe23 100644 --- a/Tests/Algorithm/AlgorithmAddUniverseTests.cs +++ b/Tests/Algorithm/AlgorithmAddUniverseTests.cs @@ -19,6 +19,7 @@ using NUnit.Framework; using Python.Runtime; using QuantConnect.Algorithm; +using QuantConnect.Data.Fundamental; using QuantConnect.Data.UniverseSelection; using QuantConnect.Tests.Engine.DataFeeds; @@ -37,7 +38,7 @@ public void AddUniverseWithETFConstituentUniverseDefinitionTickerPython(string t { AssertConstituentUniverseDefinitionsSymbol(ticker, market, false, true, true); } - + [TestCaseSource(nameof(ETFConstituentUniverseTestCases))] public void AddUniverseWithETFConstituentUniverseDefinitionSymbol(string ticker, string market) { @@ -86,35 +87,35 @@ private void AssertConstituentUniverseDefinitionsSymbol(string ticker, string ma if (isSymbol && isEtf) { constituentUniverse = isPython - ? algo.Universe.ETF(symbol, algo.UniverseSettings, (PyObject) null) + ? algo.Universe.ETF(symbol, algo.UniverseSettings, (PyObject)null) : algo.Universe.ETF(symbol, algo.UniverseSettings, CreateReturnAllFunc()); } else if (isEtf) { constituentUniverse = isPython - ? algo.Universe.ETF(ticker, market, algo.UniverseSettings, (PyObject) null) + ? algo.Universe.ETF(ticker, market, algo.UniverseSettings, (PyObject)null) : algo.Universe.ETF(ticker, market, algo.UniverseSettings, CreateReturnAllFunc()); } else if (isSymbol) { constituentUniverse = isPython - ? algo.Universe.Index(symbol, algo.UniverseSettings, (PyObject) null) + ? algo.Universe.Index(symbol, algo.UniverseSettings, (PyObject)null) : algo.Universe.Index(symbol, algo.UniverseSettings, CreateReturnAllFunc()); } else { constituentUniverse = isPython - ? algo.Universe.Index(ticker, market, algo.UniverseSettings, (PyObject) null) + ? algo.Universe.Index(ticker, market, algo.UniverseSettings, (PyObject)null) : algo.Universe.Index(ticker, market, algo.UniverseSettings, CreateReturnAllFunc()); } Assert.IsTrue(constituentUniverse.Configuration.Symbol.HasUnderlying); Assert.AreEqual(symbol, constituentUniverse.Configuration.Symbol.Underlying); - + Assert.AreEqual(symbol.SecurityType, constituentUniverse.Configuration.Symbol.SecurityType); Assert.IsTrue(constituentUniverse.Configuration.Symbol.ID.Symbol.StartsWithInvariant("qc-universe-")); } - + private static TestCaseData[] ETFConstituentUniverseTestCases() { return new[] @@ -148,5 +149,34 @@ private Func, IEnumerable> CreateRet { return x => x.Select(y => y.Symbol); } + + [TestCase(typeof(BaseDataCollection))] + [TestCase(typeof(FundamentalUniverse))] + [TestCase(typeof(ETFConstituentsUniverseFactory))] + public void UniverseSymbolsSortInCreationOrder(Type dataType) + { + Symbol symbol1, symbol2, symbol3; + if (dataType == typeof(ETFConstituentsUniverseFactory)) + { + var composite = Symbol.Create("SPY", SecurityType.Equity, Market.USA); + using var universe1 = new ETFConstituentsUniverseFactory(composite, null); + using var universe2 = new ETFConstituentsUniverseFactory(composite, null); + using var universe3 = new ETFConstituentsUniverseFactory(composite, null); + symbol1 = universe1.Configuration.Symbol; + symbol2 = universe2.Configuration.Symbol; + symbol3 = universe3.Configuration.Symbol; + } + else + { + var instance = (BaseDataCollection)Activator.CreateInstance(dataType); + symbol1 = instance.UniverseSymbol(); + symbol2 = instance.UniverseSymbol(); + symbol3 = instance.UniverseSymbol(); + } + + var comparer = StringComparer.OrdinalIgnoreCase; + Assert.That(comparer.Compare(symbol1.Value, symbol2.Value) < 0); + Assert.That(comparer.Compare(symbol2.Value, symbol3.Value) < 0); + } } }