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