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