/*
* 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.Consolidators;
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)
{
foreach (var parts in GetCsvFileStream(externalDataFilename))
{
if (!(parts.ContainsKey("Close") && parts.ContainsKey(targetColumn)))
{
Assert.Fail($"Didn't find one of 'Close' or '{targetColumn}' in the header.");
break;
}
var close = parts.GetCsvValue("close").ToDecimal();
var date = Time.ParseDate(parts.GetCsvValue("date", "time"));
indicator.Update(date, close);
if (!indicator.IsReady || string.IsNullOrEmpty(parts.GetCsvValue(targetColumn).Trim()))
{
continue;
}
var expected = Parse.Double(parts.GetCsvValue(targetColumn));
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.ToStringInvariant("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.ToStringInvariant("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.ToStringInvariant("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
foreach (var parts in GetCsvFileStream(externalDataFilename))
{
var tradebar = parts.GetTradeBar();
indicator.Update(tradebar);
if (!indicator.IsReady || string.IsNullOrEmpty(parts.GetCsvValue(targetColumn).Trim()))
{
continue;
}
var expected = Parse.Double(parts.GetCsvValue(targetColumn));
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)
{
foreach (var parts in GetCsvFileStream(externalDataFilename))
{
var tradebar = parts.GetTradeBar();
indicator.Update(tradebar);
if (!indicator.IsReady || string.IsNullOrEmpty(parts.GetCsvValue(targetColumn).Trim()))
{
continue;
}
double expected = Parse.Double(parts.GetCsvValue(targetColumn));
customAssertion.Invoke(indicator, expected);
}
}
///
/// Updates the given consolidator with the entries from the given external CSV file
///
/// RenkoConsolidator instance to update
/// The external CSV file name
public static void UpdateRenkoConsolidator(IDataConsolidator renkoConsolidator, string externalDataFilename)
{
foreach (var parts in GetCsvFileStream(externalDataFilename))
{
var tradebar = parts.GetTradeBar();
if (tradebar.Volume == 0)
{
tradebar.Volume = 1;
}
renkoConsolidator.Update(tradebar);
}
}
///
/// 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 parts in GetCsvFileStream(externalDataFilename))
{
if (!(parts.ContainsKey("Close")))
{
Assert.Fail("Didn't find column 'Close'");
break;
}
indicator.Update(date, parts.GetCsvValue("close").ToDecimal());
}
Assert.IsTrue(indicator.IsReady);
indicator.Reset();
AssertIndicatorIsInDefaultState(indicator);
}
///
/// Gets a stream of lines from the specified file
///
/// The external CSV file name
public static IEnumerable> GetCsvFileStream(string externalDataFilename)
{
var enumerator = File.ReadLines(Path.Combine("TestData", FileExtension.ToNormalizedPath(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 => GetTradeBar(values, fileHasVolume));
}
///
/// 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);
}
}
}
///
/// 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));
}
///
/// Grabs the TradeBar values from the set of keys
///
public static TradeBar GetTradeBar(this IReadOnlyDictionary dictionary, bool forceVolumeColumn = false)
{
var sid = (dictionary.ContainsKey("symbol") || dictionary.ContainsKey("ticker"))
? SecurityIdentifier.GenerateEquity(dictionary.GetCsvValue("symbol", "ticker"), Market.USA)
: SecurityIdentifier.Empty;
return new TradeBar
{
Symbol = sid != SecurityIdentifier.Empty
? new Symbol(sid, dictionary.GetCsvValue("symbol", "ticker"))
: Symbol.Empty,
Time = Time.ParseDate(dictionary.GetCsvValue("date", "time")),
Open = dictionary.GetCsvValue("open").ToDecimal(),
High = dictionary.GetCsvValue("high").ToDecimal(),
Low = dictionary.GetCsvValue("low").ToDecimal(),
Close = dictionary.GetCsvValue("close").ToDecimal(),
Volume = forceVolumeColumn || dictionary.ContainsKey("volume") ? Parse.Long(dictionary.GetCsvValue("volume"), NumberStyles.AllowExponent | NumberStyles.AllowDecimalPoint) : 0
};
}
}
}