/*
* 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.IO;
using System.Linq;
using System.Threading.Tasks;
using NodaTime;
using NUnit.Framework;
using QuantConnect.Algorithm;
using QuantConnect.Configuration;
using QuantConnect.Data;
using QuantConnect.Interfaces;
using QuantConnect.Lean.Engine;
using QuantConnect.Lean.Engine.DataFeeds;
using QuantConnect.Lean.Engine.HistoricalData;
using QuantConnect.Lean.Engine.Results;
using QuantConnect.Lean.Engine.Setup;
using QuantConnect.Lean.Engine.Storage;
using QuantConnect.Logging;
using QuantConnect.Orders;
using QuantConnect.Packets;
using QuantConnect.Securities;
using QuantConnect.Util;
using HistoryRequest = QuantConnect.Data.HistoryRequest;
namespace QuantConnect.Tests
{
///
/// Provides methods for running an algorithm and testing it's performance metrics
///
public static class AlgorithmRunner
{
public static AlgorithmRunnerResults RunLocalBacktest(
string algorithm,
Dictionary expectedStatistics,
Language language,
AlgorithmStatus expectedFinalStatus,
DateTime? startDate = null,
DateTime? endDate = null,
string setupHandler = "RegressionSetupHandlerWrapper",
decimal? initialCash = null,
string algorithmLocation = null,
bool returnLogs = false,
Dictionary customConfigurations = null)
{
AlgorithmManager algorithmManager = null;
var statistics = new Dictionary();
BacktestingResultHandler results = null;
Composer.Instance.Reset();
SymbolCache.Clear();
TextSubscriptionDataSourceReader.ClearCache();
MarketOnCloseOrder.SubmissionTimeBuffer = MarketOnCloseOrder.DefaultSubmissionTimeBuffer;
// clean up object storage
var objectStorePath = LocalObjectStore.DefaultObjectStore;
if (Directory.Exists(objectStorePath))
{
Directory.Delete(objectStorePath, true);
}
var ordersLogFile = string.Empty;
var logFile = $"./regression/{algorithm}.{language.ToLower()}.log";
Directory.CreateDirectory(Path.GetDirectoryName(logFile));
File.Delete(logFile);
var logs = new List();
var reducedDiskSize = TestContext.Parameters.Exists("reduced-disk-size") &&
bool.Parse(TestContext.Parameters["reduced-disk-size"]);
try
{
Config.Set("algorithm-type-name", algorithm);
Config.Set("live-mode", "false");
Config.Set("environment", "");
Config.Set("messaging-handler", "QuantConnect.Tests.RegressionTestMessageHandler");
Config.Set("job-queue-handler", "QuantConnect.Queues.JobQueue");
Config.Set("setup-handler", setupHandler);
Config.Set("history-provider", "RegressionHistoryProviderWrapper");
Config.Set("api-handler", "QuantConnect.Api.Api");
Config.Set("result-handler", "QuantConnect.Lean.Engine.Results.RegressionResultHandler");
Config.Set("fundamental-data-provider", "QuantConnect.Tests.Common.Data.Fundamental.TestFundamentalDataProvider");
Config.Set("algorithm-language", language.ToString());
Config.Set("data-monitor", typeof(NullDataMonitor).Name);
if (string.IsNullOrEmpty(algorithmLocation))
{
Config.Set("algorithm-location",
language == Language.Python
? "../../../Algorithm.Python/" + algorithm + ".py"
: "QuantConnect.Algorithm." + language + ".dll");
}
else
{
Config.Set("algorithm-location", algorithmLocation);
}
// set the custom configuration
if (customConfigurations != null)
{
foreach (var (key, value) in customConfigurations)
{
Config.Set(key, value);
}
}
// Store initial log variables
var initialLogHandler = Log.LogHandler;
var initialDebugEnabled = Log.DebuggingEnabled;
var newLogHandlers = new List();
if (MaintainLogHandlerAttribute.LogHandler != null)
{
newLogHandlers.Add(MaintainLogHandlerAttribute.LogHandler);
}
// Use our current test LogHandler and a FileLogHandler
if (!reducedDiskSize)
{
newLogHandlers.Add(new FileLogHandler(logFile, false));
}
if (returnLogs)
{
var storeLog = (string logMessage) => logs.Add(logMessage);
newLogHandlers.Add(new FunctionalLogHandler(storeLog, storeLog, storeLog));
}
using (Log.LogHandler = new CompositeLogHandler(newLogHandlers.ToArray()))
using (var algorithmHandlers = Initializer.GetAlgorithmHandlers())
using (var systemHandlers = Initializer.GetSystemHandlers())
using (var workerThread = new TestWorkerThread())
{
Log.DebuggingEnabled = !reducedDiskSize;
Log.Trace("");
Log.Trace("{0}: Running " + algorithm + "...", DateTime.UtcNow);
Log.Trace("");
// run the algorithm in its own thread
var engine = new Lean.Engine.Engine(systemHandlers, algorithmHandlers, false);
Task.Factory.StartNew(() =>
{
try
{
string algorithmPath;
var job = (BacktestNodePacket)systemHandlers.JobQueue.NextJob(out algorithmPath);
job.BacktestId = algorithm;
job.PeriodStart = startDate;
job.PeriodFinish = endDate;
if (initialCash.HasValue)
{
job.CashAmount = new CashAmount(initialCash.Value, Currencies.USD);
}
algorithmManager = new AlgorithmManager(false, job);
var regressionTestMessageHandler = systemHandlers.Notify as RegressionTestMessageHandler;
if (regressionTestMessageHandler != null)
{
regressionTestMessageHandler.SetAlgorithmManager(algorithmManager);
}
systemHandlers.LeanManager.Initialize(systemHandlers, algorithmHandlers, job, algorithmManager);
engine.Run(job, algorithmManager, algorithmPath, workerThread);
ordersLogFile = ((RegressionResultHandler)algorithmHandlers.Results).LogFilePath;
}
catch (Exception e)
{
Log.Trace($"Error in AlgorithmRunner task: {e}");
}
}).Wait();
var regressionResultHandler = (RegressionResultHandler)algorithmHandlers.Results;
results = regressionResultHandler;
statistics = regressionResultHandler.FinalStatistics;
if (expectedFinalStatus == AlgorithmStatus.Completed && regressionResultHandler.HasRuntimeError)
{
Assert.Fail($"There was a runtime error running the algorithm");
}
}
// Reset settings to initial values
Log.LogHandler = initialLogHandler;
Log.DebuggingEnabled = initialDebugEnabled;
}
catch (Exception ex)
{
if (expectedFinalStatus != AlgorithmStatus.RuntimeError)
{
Log.Error("{0} {1}", ex.Message, ex.StackTrace);
}
}
AssertAlgorithmState(expectedFinalStatus, algorithmManager?.State, expectedStatistics, statistics);
if (!reducedDiskSize)
{
// we successfully passed the regression test, copy the log file so we don't have to continually
// re-run master in order to compare against a passing run
var passedFile = logFile.Replace("./regression/", "./passed/");
Directory.CreateDirectory(Path.GetDirectoryName(passedFile));
File.Delete(passedFile);
File.Copy(logFile, passedFile);
var passedOrderLogFile = ordersLogFile.Replace("./regression/", "./passed/");
Directory.CreateDirectory(Path.GetDirectoryName(passedFile));
File.Delete(passedOrderLogFile);
if (File.Exists(ordersLogFile)) File.Copy(ordersLogFile, passedOrderLogFile);
}
return new AlgorithmRunnerResults(algorithm, language, algorithmManager, results, logs);
}
///
/// Used to intercept the algorithm instance to aid the
///
public class RegressionSetupHandlerWrapper : BacktestingSetupHandler
{
public static IAlgorithm Algorithm { get; protected set; }
public override IAlgorithm CreateAlgorithmInstance(AlgorithmNodePacket algorithmNodePacket, string assemblyPath)
{
Algorithm = base.CreateAlgorithmInstance(algorithmNodePacket, assemblyPath);
var framework = Algorithm as QCAlgorithm;
if (framework != null)
{
framework.DebugMode = true;
}
return Algorithm;
}
}
///
/// Used to perform checks against history requests for all regression algorithms
///
public class RegressionHistoryProviderWrapper : SubscriptionDataReaderHistoryProvider
{
public override IEnumerable GetHistory(IEnumerable requests, DateTimeZone sliceTimeZone)
{
requests = requests.ToList();
if (requests.Any(r => r.Symbol.SecurityType != SecurityType.Option && r.Symbol.SecurityType != SecurityType.IndexOption
&& r.Symbol.SecurityType != SecurityType.FutureOption
&& r.Symbol.SecurityType != SecurityType.Future && r.Symbol.IsCanonical()))
{
throw new RegressionTestException($"Invalid history request symbols: {string.Join(",", requests.Select(x => x.Symbol))}");
}
return base.GetHistory(requests, sliceTimeZone);
}
}
public class TestWorkerThread : WorkerThread
{
}
public class NullDataMonitor : IDataMonitor
{
public void Dispose()
{ }
public void Exit()
{ }
public void OnNewDataRequest(object sender, DataProviderNewDataRequestEventArgs e)
{ }
}
public static void AssertAlgorithmState(AlgorithmStatus expectedFinalStatus, AlgorithmStatus? actualState,
IDictionary expectedStatistics, IDictionary statistics)
{
if (actualState != expectedFinalStatus)
{
Assert.Fail($"Algorithm state should be {expectedFinalStatus} and is: {actualState}");
}
foreach (var expectedStat in expectedStatistics)
{
if (statistics == null)
{
Assert.Fail("Algorithm statistics are null");
break;
}
string result;
Assert.IsTrue(statistics.TryGetValue(expectedStat.Key, out result), "Missing key: " + expectedStat.Key);
// normalize -0 & 0, they are the same thing
var expected = expectedStat.Value;
if (expected == "-0")
{
expected = "0";
}
if (result == "-0")
{
result = "0";
}
Assert.AreEqual(expected, result, "Failed on " + expectedStat.Key);
}
}
}
}