/*
* 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.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using NUnit.Framework;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Indicators;
namespace QuantConnect.Tests.Indicators
{
///
/// Provides helper methods for testing indicatora
///
public static class TestHelper
{
///
/// Gets a stream of IndicatorDataPoints that can be fed to an indicator. The data stream starts at {DateTime.Today, 1m} and
/// increasing at {1 second, 1m}
///
/// The number of data points to stream
/// Function to produce the value of the data, null to use the index
/// A stream of IndicatorDataPoints
public static IEnumerable GetDataStream(int count, Func valueProducer = null)
{
var reference = DateTime.Today;
valueProducer = valueProducer ?? (x => x);
for (int i = 0; i < count; i++)
{
yield return new IndicatorDataPoint(reference.AddSeconds(i), valueProducer.Invoke(i));
}
}
///
/// Compare the specified indicator against external data using the spy_with_indicators.txt file.
/// The 'Close' column will be fed to the indicator as input
///
/// The indicator under test
/// The column with the correct answers
/// The maximum delta between expected and actual
public static void TestIndicator(IndicatorBase indicator, string targetColumn, double epsilon = 1e-3)
{
TestIndicator(indicator, "spy_with_indicators.txt", targetColumn, (i, expected) => Assert.AreEqual(expected, (double) i.Current.Value, epsilon));
}
///
/// Compare the specified indicator against external data using the specificied comma delimited text file.
/// The 'Close' column will be fed to the indicator as input
///
/// The indicator under test
///
/// The column with the correct answers
/// Sets custom assertion logic, parameter is the indicator, expected value from the file
public static void TestIndicator(IndicatorBase indicator, string externalDataFilename, string targetColumn, Action, double> customAssertion)
{
// assumes the Date is in the first index
bool first = true;
int closeIndex = -1;
int targetIndex = -1;
foreach (var line in File.ReadLines(Path.Combine("TestData", externalDataFilename)))
{
string[] parts = line.Split(new[] {','}, StringSplitOptions.None);
if (first)
{
first = false;
for (int i = 0; i < parts.Length; i++)
{
if (parts[i].Trim() == "Close")
{
closeIndex = i;
}
if (parts[i].Trim() == targetColumn)
{
targetIndex = i;
}
}
if (closeIndex*targetIndex < 0)
{
Assert.Fail("Didn't find one of 'Close' or '{0}' in the header: " + line, targetColumn);
}
continue;
}
decimal close = decimal.Parse(parts[closeIndex], CultureInfo.InvariantCulture);
DateTime date = Time.ParseDate(parts[0]);
var data = new IndicatorDataPoint(date, close);
indicator.Update(data);
if (!indicator.IsReady || parts[targetIndex].Trim() == string.Empty)
{
continue;
}
double expected = double.Parse(parts[targetIndex], CultureInfo.InvariantCulture);
customAssertion.Invoke(indicator, expected);
}
}
///
/// Compare the specified indicator against external data using the specificied comma delimited text file.
/// The 'Close' column will be fed to the indicator as input
///
/// The indicator under test
///
/// The column with the correct answers
/// The maximum delta between expected and actual
public static void TestIndicator(IndicatorBase indicator, string externalDataFilename, string targetColumn, double epsilon = 1e-3)
{
TestIndicator(indicator, externalDataFilename, targetColumn, (i, expected) => Assert.AreEqual(expected, (double)i.Current.Value, epsilon, "Failed at " + i.Current.Time.ToString("o")));
}
///
/// Compare the specified indicator against external data using the specificied comma delimited text file.
/// The 'Close' column will be fed to the indicator as input
///
/// The indicator under test
///
/// The column with the correct answers
/// The maximum delta between expected and actual
public static void TestIndicator(IndicatorBase indicator, string externalDataFilename, string targetColumn, double epsilon = 1e-3)
{
TestIndicator(indicator, externalDataFilename, targetColumn, (i, expected) => Assert.AreEqual(expected, (double)i.Current.Value, epsilon, "Failed at " + i.Current.Time.ToString("o")));
}
///
/// Compare the specified indicator against external data using the specificied comma delimited text file.
/// The 'Close' column will be fed to the indicator as input
///
/// The indicator under test
///
/// The column with the correct answers
/// A function that receives the indicator as input and outputs a value to match the target column
/// The maximum delta between expected and actual
public static void TestIndicator(T indicator, string externalDataFilename, string targetColumn, Func selector, double epsilon = 1e-3)
where T : Indicator
{
TestIndicator(indicator, externalDataFilename, targetColumn, (i, expected) => Assert.AreEqual(expected, selector(indicator), epsilon, "Failed at " + i.Current.Time.ToString("o")));
}
///
/// Compare the specified indicator against external data using the specified comma delimited text file.
/// The 'Close' column will be fed to the indicator as input
///
/// The indicator under test
/// The external CSV file name
/// The column with the correct answers
/// Sets custom assertion logic, parameter is the indicator, expected value from the file
public static void TestIndicator(IndicatorBase indicator, string externalDataFilename, string targetColumn, Action, double> customAssertion)
{
// TODO : Collapse duplicate implementations -- type constraint shenanigans and after 4am
bool first = true;
int targetIndex = -1;
bool fileHasVolume = false;
foreach (var line in File.ReadLines(Path.Combine("TestData", externalDataFilename)))
{
var parts = line.Split(',');
if (first)
{
fileHasVolume = parts[5].Trim() == "Volume";
first = false;
for (int i = 0; i < parts.Length; i++)
{
if (parts[i].Trim() == targetColumn)
{
targetIndex = i;
break;
}
}
continue;
}
var tradebar = new TradeBar
{
Time = Time.ParseDate(parts[0]),
Open = parts[1].ToDecimal(),
High = parts[2].ToDecimal(),
Low = parts[3].ToDecimal(),
Close = parts[4].ToDecimal(),
Volume = fileHasVolume ? long.Parse(parts[5], NumberStyles.AllowExponent | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture) : 0
};
indicator.Update(tradebar);
if (!indicator.IsReady || parts[targetIndex].Trim() == string.Empty)
{
continue;
}
double expected = double.Parse(parts[targetIndex], CultureInfo.InvariantCulture);
customAssertion.Invoke(indicator, expected);
}
}
///
/// Compare the specified indicator against external data using the specified comma delimited text file.
/// The 'Close' column will be fed to the indicator as input
///
/// The indicator under test
/// The external CSV file name
/// The column with the correct answers
/// Sets custom assertion logic, parameter is the indicator, expected value from the file
public static void TestIndicator(IndicatorBase indicator, string externalDataFilename, string targetColumn, Action, double> customAssertion)
{
bool first = true;
int targetIndex = -1;
bool fileHasVolume = false;
foreach (var line in File.ReadLines(Path.Combine("TestData", externalDataFilename)))
{
var parts = line.Split(',');
if (first)
{
fileHasVolume = parts[5].Trim() == "Volume";
first = false;
for (int i = 0; i < parts.Length; i++)
{
if (parts[i].Trim() == targetColumn)
{
targetIndex = i;
break;
}
}
continue;
}
var tradebar = new TradeBar
{
Time = Time.ParseDate(parts[0]),
Open = parts[1].ToDecimal(),
High = parts[2].ToDecimal(),
Low = parts[3].ToDecimal(),
Close = parts[4].ToDecimal(),
Volume = fileHasVolume ? long.Parse(parts[5], NumberStyles.AllowExponent | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture) : 0
};
indicator.Update(tradebar);
if (!indicator.IsReady || parts[targetIndex].Trim() == string.Empty)
{
continue;
}
double expected = double.Parse(parts[targetIndex], CultureInfo.InvariantCulture);
customAssertion.Invoke(indicator, expected);
}
}
///
/// Tests a reset of the specified indicator after processing external data using the specified comma delimited text file.
/// The 'Close' column will be fed to the indicator as input
///
/// The indicator under test
/// The external CSV file name
public static void TestIndicatorReset(IndicatorBase indicator, string externalDataFilename)
{
foreach (var data in GetTradeBarStream(externalDataFilename, false))
{
indicator.Update(data);
}
Assert.IsTrue(indicator.IsReady);
indicator.Reset();
AssertIndicatorIsInDefaultState(indicator);
}
///
/// Tests a reset of the specified indicator after processing external data using the specified comma delimited text file.
/// The 'Close' column will be fed to the indicator as input
///
/// The indicator under test
/// The external CSV file name
public static void TestIndicatorReset(IndicatorBase indicator, string externalDataFilename)
{
foreach (var data in GetTradeBarStream(externalDataFilename, false))
{
indicator.Update(data);
}
Assert.IsTrue(indicator.IsReady);
indicator.Reset();
AssertIndicatorIsInDefaultState(indicator);
}
///
/// Tests a reset of the specified indicator after processing external data using the specified comma delimited text file.
/// The 'Close' column will be fed to the indicator as input
///
/// The indicator under test
/// The external CSV file name
public static void TestIndicatorReset(IndicatorBase indicator, string externalDataFilename)
{
var date = DateTime.Today;
foreach (var data in GetTradeBarStream(externalDataFilename, false))
{
indicator.Update(date, data.Close);
}
Assert.IsTrue(indicator.IsReady);
indicator.Reset();
AssertIndicatorIsInDefaultState(indicator);
}
public static IEnumerable> GetCsvFileStream(string externalDataFilename)
{
var enumerator = File.ReadLines(Path.Combine("TestData", externalDataFilename)).GetEnumerator();
if (!enumerator.MoveNext())
{
yield break;
}
string[] header = enumerator.Current.Split(',');
while (enumerator.MoveNext())
{
var values = enumerator.Current.Split(',');
var headerAndValues = header.Zip(values, (h, v) => new {h, v});
var dictionary = headerAndValues.ToDictionary(x => x.h.Trim(), x => x.v.Trim(), StringComparer.OrdinalIgnoreCase);
yield return new ReadOnlyDictionary(dictionary);
}
}
///
/// Gets a stream of trade bars from the specified file
///
public static IEnumerable GetTradeBarStream(string externalDataFilename, bool fileHasVolume = true)
{
return GetCsvFileStream(externalDataFilename).Select(values => new TradeBar
{
Time = Time.ParseDate(values.GetCsvValue("date", "time")),
Open = values.GetCsvValue("open").ToDecimal(),
High = values.GetCsvValue("high").ToDecimal(),
Low = values.GetCsvValue("low").ToDecimal(),
Close = values.GetCsvValue("close").ToDecimal(),
Volume = fileHasVolume ? long.Parse(values.GetCsvValue("volume"), NumberStyles.AllowExponent | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture) : 0
});
}
///
/// Asserts that the indicator has zero samples, is not ready, and has the default value
///
/// The indicator to assert
public static void AssertIndicatorIsInDefaultState(IndicatorBase indicator)
where T : IBaseData
{
Assert.AreEqual(0m, indicator.Current.Value);
Assert.AreEqual(DateTime.MinValue, indicator.Current.Time);
Assert.AreEqual(0, indicator.Samples);
Assert.IsFalse(indicator.IsReady);
var fields = indicator.GetType().GetProperties()
.Where(x => x.PropertyType.IsSubclassOfGeneric(typeof(IndicatorBase)) ||
x.PropertyType.IsSubclassOfGeneric(typeof(IndicatorBase)) ||
x.PropertyType.IsSubclassOfGeneric(typeof(IndicatorBase)));
foreach (var field in fields)
{
var subIndicator = field.GetValue(indicator);
if (subIndicator == null ||
subIndicator is ConstantIndicator ||
subIndicator is ConstantIndicator ||
subIndicator is ConstantIndicator)
continue;
if (field.PropertyType.IsSubclassOfGeneric(typeof (IndicatorBase)))
{
AssertIndicatorIsInDefaultState(subIndicator as IndicatorBase);
}
else if (field.PropertyType.IsSubclassOfGeneric(typeof(IndicatorBase)))
{
AssertIndicatorIsInDefaultState(subIndicator as IndicatorBase);
}
else if (field.PropertyType.IsSubclassOfGeneric(typeof(IndicatorBase)))
{
AssertIndicatorIsInDefaultState(subIndicator as IndicatorBase);
}
}
}
///
/// Gets a customAssertion action which will gaurantee that the delta between the expected and the
/// actual continues to decrease with a lower bound as specified by the epsilon parameter. This is useful
/// for testing indicators which retain theoretically infinite information via methods such as exponential smoothing
///
/// The largest increase in the delta permitted
///
public static Action, double> AssertDeltaDecreases(double epsilon)
{
double delta = double.MaxValue;
return (indicator, expected) =>
{
// the delta should be forever decreasing
var currentDelta = Math.Abs((double) indicator.Current.Value - expected);
if (currentDelta - delta > epsilon)
{
Assert.Fail("The delta increased!");
//Console.WriteLine(indicator.Value.Time.Date.ToShortDateString() + " - " + indicator.Value.Data.ToString("000.000") + " \t " + expected.ToString("000.000") + " \t " + currentDelta.ToString("0.000"));
}
delta = currentDelta;
};
}
///
/// Grabs the first value from the set of keys
///
private static string GetCsvValue(this IReadOnlyDictionary dictionary, params string[] keys)
{
string value = null;
if (keys.Any(key => dictionary.TryGetValue(key, out value)))
{
return value;
}
throw new ArgumentException("Unable to find column: " + string.Join(", ", keys));
}
}
}