/*
* 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;
}
}
}
}