/* * 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.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; using NUnit.Framework; using QuantConnect.Algorithm; using QuantConnect.Data.Market; using QuantConnect.Indicators; using QuantConnect.Logging; using QuantConnect.Tests.Engine.DataFeeds; namespace QuantConnect.Tests.Indicators { /// /// Test class for QuantConnect.Indicators.Indicator /// [TestFixture] public class IndicatorTests { [Test] public void NameSaves() { // just testing that we get the right name out const string name = "name"; var target = new TestIndicator(name); Assert.AreEqual(name, target.Name); } [Test] public void UpdatesProperly() { // we want to make sure the initialized value is the default value // for a datapoint, and also verify the our indicator updates as we // expect it to, in this case, it should return identity var target = new TestIndicator(); Assert.AreEqual(DateTime.MinValue, target.Current.Time); Assert.AreEqual(0m, target.Current.Value); var time = DateTime.UtcNow; var data = new IndicatorDataPoint(time, 1m); target.Update(data); Assert.AreEqual(1m, target.Current.Value); target.Update(new IndicatorDataPoint(time.AddMilliseconds(1), 2m)); Assert.AreEqual(2m, target.Current.Value); } [Test] public void ShouldNotThrowOnDifferentDataType() { var target = new TestIndicator(); Assert.DoesNotThrow(() => { target.Update(new Tick()); }); } [Test] public void PassesOnDuplicateTimes() { var target = new TestIndicator(); var time = DateTime.UtcNow; const decimal value1 = 1m; var data = new IndicatorDataPoint(time, value1); target.Update(data); Assert.AreEqual(value1, target.Current.Value); // this won't update because we told it to ignore duplicate // data based on time target.Update(data); Assert.AreEqual(value1, target.Current.Value); } [Test] public void SortsTheSameAsDecimalDescending() { int count = 100; var targets = Enumerable.Range(0, count) .Select(x => new TestIndicator(x.ToString(CultureInfo.InvariantCulture))) .ToList(); for (int i = 0; i < targets.Count; i++) { targets[i].Update(DateTime.Today, i); } var expected = Enumerable.Range(0, count) .Select(x => (decimal)x) .OrderByDescending(x => x) .ToList(); var actual = targets.OrderByDescending(x => x).ToList(); foreach (var pair in expected.Zip>(actual, Tuple.Create)) { Assert.AreEqual(pair.Item1, pair.Item2.Current.Value); } } [Test] public void SortsTheSameAsDecimalAsecending() { int count = 100; var targets = Enumerable.Range(0, count).Select(x => new TestIndicator(x.ToString(CultureInfo.InvariantCulture))).ToList(); for (int i = 0; i < targets.Count; i++) { targets[i].Update(DateTime.Today, i); } var expected = Enumerable.Range(0, count).Select(x => (decimal)x).OrderBy(x => x).ToList(); var actual = targets.OrderBy(x => x).ToList(); foreach (var pair in expected.Zip>(actual, Tuple.Create)) { Assert.AreEqual(pair.Item1, pair.Item2.Current.Value); } } [Test] public void ComparisonFunctions() { TestComparisonOperators(); TestComparisonOperators(); TestComparisonOperators(); TestComparisonOperators(); } [Test] public void EqualsMethodShouldNotThrowExceptions() { var indicator = new TestIndicator(); var res = true; try { res = indicator.Equals(new Exception("")); } catch (InvalidCastException) { Assert.Fail(); } Assert.IsFalse(res); } [Test] public void IndicatorMustBeEqualToItself() { var indicators = typeof(Indicator).Assembly.GetTypes() .Where(t => t.BaseType.Name != "CandlestickPattern" && !t.Name.StartsWith("<")) .OrderBy(t => t.Name) .ToList(); var counter = 0; object instantiatedIndicator; foreach (var indicator in indicators) { try { instantiatedIndicator = Activator.CreateInstance(indicator, new object[] { 10 }); counter++; } catch (Exception) { // Some indicators will fail because they don't have a single-parameter constructor. continue; } Assert.IsTrue(instantiatedIndicator.Equals(instantiatedIndicator)); var anotherInstantiatedIndicator = Activator.CreateInstance(indicator, new object[] { 10 }); Assert.IsFalse(instantiatedIndicator.Equals(anotherInstantiatedIndicator)); } Log.Trace($"{counter} indicators out of {indicators.Count} were tested."); } [Test] public void IndicatorsOfDifferentTypeDiplaySameCurrentTime() { var algorithm = new QCAlgorithm(); algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm)); var spy = algorithm.AddEquity("SPY"); var indicatorTimeList = new List(); // RSI is a DataPointIndicator algorithm.RSI(spy.Symbol, 14).Updated += (_, e) => indicatorTimeList.Add(e.EndTime); // STO is a BarIndicator algorithm.STO(spy.Symbol, 14, 2, 2).Updated += (_, e) => indicatorTimeList.Add(e.EndTime); // MFI is a TradeBarIndicator algorithm.MFI(spy.Symbol, 14).Updated += (_, e) => indicatorTimeList.Add(e.EndTime); var consolidators = spy.Subscriptions.SelectMany(x => x.Consolidators).ToList(); Assert.AreEqual(3, consolidators.Count); // One consolidator for each indicator var bars = new[] { 30, 31 }.Select(d => new TradeBar(new DateTime(2020, 03, 04, 9, d, 0), spy.Symbol, 100, 100, 100, 100, 1000)); foreach (var bar in bars) { foreach (var consolidator in consolidators) { consolidator.Update(bar); } } // All indicators should have the same EndTime, with xx:31:00 & xx:32:00 Assert.AreEqual(6, indicatorTimeList.Count); Assert.AreEqual(2, indicatorTimeList.Distinct().Count()); Assert.AreEqual(3, indicatorTimeList.Count(x => x.Minute == 31)); Assert.AreEqual(3, indicatorTimeList.Count(x => x.Minute == 32)); } [TestCase(2)] [TestCase(5)] [TestCase(10)] public void IndicatorKeepsHistory(int historyWindow) { var indicator = new TestIndicator("Test indicator"); indicator.Window.Size = historyWindow; var points = new List(100); var referenceDate = new DateTime(2023, 06, 12, 9, 0, 0); for (int i = 0; i < 100; i++) { // The first iteration will not update the indicator. By default, first value is IndicatorDataPoint(DateTime.MinValue, 0) if (i == 0) { var defaultValue = new IndicatorDataPoint(DateTime.MinValue, 0); Assert.AreEqual(defaultValue, indicator.Current); Assert.AreEqual(defaultValue, indicator[0]); } else { var dateTime = referenceDate.AddMinutes(i); indicator.Update(dateTime, i); var expected = new IndicatorDataPoint(dateTime, i); Assert.AreEqual(expected, indicator.Current); Assert.AreEqual(expected, indicator[0]); } points.Insert(0, indicator[0]); var startIndex = Math.Max(0, i - historyWindow + 1); for (int j = startIndex; j <= i; j++) { Assert.AreEqual(points[i - j], indicator[i - j]); } // Check the enumerator var windowPoints = indicator.ToList(); var count = i - startIndex < historyWindow ? i - startIndex + 1 : historyWindow; CollectionAssert.AreEqual(points.GetRange(0, count), windowPoints); } } [Test] public void HistoryWindowIsCorrectlyReset() { var indicator = new TestIndicator("Test indicator"); indicator.Window.Size = 20; // Update the indicator a few times var referenceDate = new DateTime(2023, 06, 12, 9, 0, 0); for (var i = 1; i < indicator.Window.Size; i++) { indicator.Update(referenceDate.AddMinutes(i - 1), i); } Assert.AreEqual(indicator.Window.Size, indicator.Window.Count); indicator.Reset(); // Window size is kept Assert.AreEqual(20, indicator.Window.Size); // Window values are removed Assert.AreEqual(1, indicator.Window.Count); Assert.AreEqual(new IndicatorDataPoint(DateTime.MinValue, 0), indicator[0]); Assert.IsNull(indicator[1]); } [Test] public void CanAccessCurrentAndPreviousState() { var indicator = new TestIndicator("Test indicator"); indicator.Window.Size = 10; // Update the indicator a few times var referenceDate = new DateTime(2023, 06, 12, 9, 0, 0); var dataPoints = new List(indicator.Window.Size); for (var i = 0; i < indicator.Window.Size; i++) { var dateTime = referenceDate.AddMinutes(i); indicator.Update(dateTime, i); dataPoints.Add(new IndicatorDataPoint(dateTime, i)); } Assert.AreEqual(dataPoints[^1], indicator.Current); Assert.AreEqual(dataPoints[^1], indicator[0]); Assert.AreEqual(dataPoints[^2], indicator.Previous); Assert.AreEqual(dataPoints[^2], indicator[1]); } [Test] public void PreviousValueIsNotNullAtStart() { var indicator = new TestIndicator("Test indicator"); // Access current and previous without warming the indicator up var defaultValue = new IndicatorDataPoint(DateTime.MinValue, 0); Assert.IsNotNull(indicator.Current); Assert.AreEqual(defaultValue, indicator.Current); Assert.IsNotNull(indicator.Previous); Assert.AreEqual(defaultValue, indicator.Previous); } [Test] public void IndicatorShouldRetainSymbolWhenUpdatedWithDifferentDataType() { var target = new TestIndicator(); var date = new DateTime(2020, 1, 1); target.Update(new Tick(date, Symbols.SPY, 1, 1)); Assert.AreEqual(Symbols.SPY, target.Current.Symbol); } private static void TestComparisonOperators() { var indicator = new TestIndicator(); TestOperator(indicator, default(TValue), "GreaterThan", true, false); TestOperator(indicator, default(TValue), "GreaterThan", false, false); TestOperator(indicator, default(TValue), "GreaterThanOrEqual", true, true); TestOperator(indicator, default(TValue), "GreaterThanOrEqual", false, true); TestOperator(indicator, default(TValue), "LessThan", true, false); TestOperator(indicator, default(TValue), "LessThan", false, false); TestOperator(indicator, default(TValue), "LessThanOrEqual", true, true); TestOperator(indicator, default(TValue), "LessThanOrEqual", false, true); TestOperator(indicator, default(TValue), "Equality", true, true); TestOperator(indicator, default(TValue), "Equality", false, true); TestOperator(indicator, default(TValue), "Inequality", true, false); TestOperator(indicator, default(TValue), "Inequality", false, false); } private static void TestOperator(TIndicator indicator, TValue value, string opName, bool tvalueIsFirstParm, bool expected) { var method = GetOperatorMethodInfo(opName, tvalueIsFirstParm ? 0 : 1); var ctIndicator = Expression.Constant(indicator); var ctValue = Expression.Constant(value); var call = tvalueIsFirstParm ? Expression.Call(method, ctValue, ctIndicator) : Expression.Call(method, ctIndicator, ctValue); var lamda = Expression.Lambda>(call); var func = lamda.Compile(); Assert.AreEqual(expected, func()); } private static MethodInfo GetOperatorMethodInfo(string @operator, int argIndex) { var methodName = "op_" + @operator; var method = typeof(IndicatorBase).GetMethods(BindingFlags.Static | BindingFlags.Public) .SingleOrDefault(x => x.Name == methodName && x.GetParameters()[argIndex].ParameterType == typeof(T)); if (method == null) { Assert.Fail("Failed to find method for " + @operator + " of type " + typeof(T).Name + " at index: " + argIndex); } return method; } private class TestIndicator : Indicator { /// /// Initializes a new instance of the Indicator class using the specified name. /// /// The name of this indicator public TestIndicator(string name) : base(name) { } /// /// Initializes a new instance of the Indicator class using the name "test" /// public TestIndicator() : base("test") { } /// /// Gets a flag indicating when this indicator is ready and fully initialized /// public override bool IsReady { get { return true; } } /// /// Computes the next value of this indicator from the given state /// /// The input given to the indicator /// A new value for this indicator protected override decimal ComputeNextValue(IndicatorDataPoint input) { return input.Value; } } } }