Files
quantconnect--lean/Tests/Api/OptimizationBacktestJsonConverterTests.cs
2026-07-13 13:02:50 +08:00

235 lines
14 KiB
C#

/*
* 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 Newtonsoft.Json;
using NUnit.Framework;
using QuantConnect.Api;
using QuantConnect.Optimizer.Parameters;
using QuantConnect.Statistics;
namespace QuantConnect.Tests.API
{
[TestFixture]
public class OptimizationBacktestJsonConverterTests
{
private const string _validSerialization = "{\"name\":\"ImABacktestName\",\"id\":\"backtestId\",\"progress\":0.0,\"exitCode\":0," +
"\"startDate\":\"2023-01-01T00:00:00Z\",\"endDate\":\"2024-01-01T00:00:00Z\",\"outOfSampleMaxEndDate\":\"2024-01-01T00:00:00Z\",\"outOfSampleDays\":10,\"statistics\":{\"0\":0.374,\"1\":0.217,\"2\":0.047,\"3\":-4.51,\"4\":2.86,\"5\":-0.664,\"6\":52.602,\"7\":17.800,\"8\":6300000.00,\"9\":0.196,\"10\":1.571,\"11\":27.0,\"12\":123.888,\"13\":77.188,\"14\":0.63,\"15\":1.707,\"16\":1390.49,\"17\":180.0,\"18\":0.233,\"19\":-0.558,\"20\":73.0,\"21\":0.1,\"22\":100000.0,\"23\":200000.0,\"24\":3.0}," +
"\"parameterSet\":{\"pinocho\":\"19\",\"pepe\":\"-1\"},\"equity\":[[1,1.0,1.0,1.0,1.0],[2,2.0,2.0,2.0,2.0],[3,3.0,3.0,3.0,3.0]]}";
private const string _oldValidSerialization = "{\"name\":\"ImABacktestName\",\"id\":\"backtestId\",\"progress\":0.0,\"exitCode\":0," +
"\"statistics\":{\"0\":0.374,\"1\":0.217,\"2\":0.047,\"3\":-4.51,\"4\":2.86,\"5\":-0.664,\"6\":52.602,\"7\":17.800,\"8\":6300000.00,\"9\":0.196,\"10\":1.571,\"11\":27.0,\"12\":123.888,\"13\":77.188,\"14\":0.63,\"15\":1.707,\"16\":1390.49,\"17\":180.0,\"18\":0.233,\"19\":-0.558,\"20\":73.0,\"21\":0.1,\"22\":100000.0,\"23\":200000.0,\"24\":3.0}," +
"\"parameterSet\":{\"pinocho\":\"19\",\"pepe\":\"-1\"},\"equity\":[[1,1.0,1.0,1.0,1.0],[2,2.0,2.0,2.0,2.0],[3,3.0,3.0,3.0,3.0]]}";
private const string _oldValid2Serialization = "{\"name\":\"ImABacktestName\",\"id\":\"backtestId\",\"progress\":0.0,\"exitCode\":0," +
"\"statistics\":{\"0\":0.374,\"1\":0.217,\"2\":0.047,\"3\":-4.51,\"4\":2.86,\"5\":-0.664,\"6\":52.602,\"7\":17.800,\"8\":6300000.00,\"9\":0.196,\"10\":1.571,\"11\":27.0,\"12\":123.888,\"13\":77.188,\"14\":0.63,\"15\":1.707,\"16\":1390.49,\"17\":180.0,\"18\":0.233,\"19\":-0.558,\"20\":73.0,\"21\":0.1,\"22\":100000.0,\"23\":200000.0,\"24\":3.0}," +
"\"parameterSet\":{\"pinocho\":\"19\",\"pepe\":\"-1\"},\"equity\":[[1,1.0],[2,2.0],[3,3.0]]}";
private const string _oldValid3Serialization = "{\"name\":\"ImABacktestName\",\"id\":\"backtestId\",\"progress\":0.0,\"exitCode\":0," +
"\"statistics\":{\"0\":0.374,\"1\":0.217,\"2\":0.047,\"3\":-4.51,\"4\":2.86,\"5\":-0.664,\"6\":52.602,\"7\":17.800,\"8\":6300000.00,\"9\":0.196,\"10\":1.571,\"11\":27.0,\"12\":123.888,\"13\":77.188,\"14\":0.63,\"15\":1.707,\"16\":1390.49,\"17\":180.0,\"18\":0.233,\"19\":-0.558,\"20\":73.0}," +
"\"parameterSet\":{\"pinocho\":\"19\",\"pepe\":\"-1\"},\"equity\":[[1,1.0],[2,2.0],[3,3.0]]}";
private const string _validSerializationWithCustomStats = "{\"name\":\"ImABacktestName\",\"id\":\"backtestId\",\"progress\":0.0,\"exitCode\":0," +
"\"startDate\":\"2023-01-01T00:00:00Z\",\"endDate\":\"2024-01-01T00:00:00Z\",\"outOfSampleMaxEndDate\":\"2024-01-01T00:00:00Z\",\"outOfSampleDays\":10,\"statistics\":{\"0\":0.374,\"1\":0.217,\"2\":0.047,\"3\":-4.51,\"4\":2.86,\"5\":-0.664,\"6\":52.602,\"7\":17.800,\"8\":6300000.00,\"9\":0.196,\"10\":1.571,\"11\":27.0,\"12\":123.888,\"13\":77.188,\"14\":0.63,\"15\":1.707,\"16\":1390.49,\"17\":180.0,\"18\":0.233,\"19\":-0.558,\"20\":73.0,\"21\":0.1,\"22\":100000.0,\"23\":200000.0,\"24\":3.0,\"customstat2\":5.4321,\"customstat1\":1.2345}," +
"\"parameterSet\":{\"pinocho\":\"19\",\"pepe\":\"-1\"},\"equity\":[[1,1.0,1.0,1.0,1.0],[2,2.0,2.0,2.0,2.0],[3,3.0,3.0,3.0,3.0]]}";
private const string _validOldStatsDeserialization = "{\"name\":\"ImABacktestName\",\"id\":\"backtestId\",\"progress\":0.0,\"exitCode\":0," +
"\"startDate\":\"2023-01-01T00:00:00Z\",\"endDate\":\"2024-01-01T00:00:00Z\",\"outOfSampleMaxEndDate\":\"2024-01-01T00:00:00Z\",\"outOfSampleDays\":10,\"statistics\":[0.374,0.217,0.047,-4.51,2.86,-0.664,52.602,17.800,6300000.00,0.196,1.571,27.0,123.888,77.188,0.63,1.707,1390.49,180.0,0.233,-0.558,73.0]," +
"\"parameterSet\":{\"pinocho\":\"19\",\"pepe\":\"-1\"},\"equity\":[[1,1.0,1.0,1.0,1.0],[2,2.0,2.0,2.0,2.0],[3,3.0,3.0,3.0,3.0]]}";
private const string _validOldStatsDeserialization2 = "{\"name\":\"ImABacktestName\",\"id\":\"backtestId\",\"progress\":0.0,\"exitCode\":0," +
"\"statistics\":[0.374,0.217,0.047,-4.51,2.86,-0.664,52.602,17.800,6300000.00,0.196,1.571,27.0,123.888,77.188,0.63,1.707,1390.49,180.0,0.233,-0.558,73.0]," +
"\"parameterSet\":{\"pinocho\":\"19\",\"pepe\":\"-1\"},\"equity\":[[1,1.0,1.0,1.0,1.0],[2,2.0,2.0,2.0,2.0],[3,3.0,3.0,3.0,3.0]]}";
private const string _validOldStatsDeserialization3 = "{\"name\":\"ImABacktestName\",\"id\":\"backtestId\",\"progress\":0.0,\"exitCode\":0," +
"\"statistics\":[0.374,0.217,0.047,-4.51,2.86,-0.664,52.602,17.800,6300000.00,0.196,1.571,27.0,123.888,77.188,0.63,1.707,1390.49,180.0,0.233,-0.558,73.0]," +
"\"parameterSet\":{\"pinocho\":\"19\",\"pepe\":\"-1\"},\"equity\":[[1,1.0],[2,2.0],[3,3.0]]}";
private const string _validOldStatsDeserializationWithLessStats = "{\"name\":\"ImABacktestName\",\"id\":\"backtestId\",\"progress\":0.0,\"exitCode\":0," +
"\"statistics\":[0.374,0.217,0.047,-4.51,2.86,-0.664,52.602,17.800,6300000.00,0.196,1.571,27.0,123.888,77.188,0.63]," +
"\"parameterSet\":{\"pinocho\":\"19\",\"pepe\":\"-1\"},\"equity\":[[1,1.0],[2,2.0],[3,3.0]]}";
[Test]
public void SerializationNulls()
{
var optimizationBacktest = new OptimizationBacktest(null, null, null);
var serialized = JsonConvert.SerializeObject(optimizationBacktest);
Assert.AreEqual("{}", serialized);
}
[TestCase(1)]
[TestCase(0)]
public void Serialization(int version)
{
var optimizationBacktest = new OptimizationBacktest(new ParameterSet(18,
new Dictionary<string, string>
{
{ "pinocho", "19" },
{ "pepe", "-1" }
}), "backtestId", "ImABacktestName");
optimizationBacktest.Statistics = new Dictionary<string, string>
{
{ "Total Orders", "180" },
{ "Average Win", "2.86%" },
{ "Average Loss", "-4.51%" },
{ "Compounding Annual Return", "52.602%" },
{ "Drawdown", "17.800%" },
{ "Expectancy", "0.196" },
{ "Start Equity", "100000" },
{ "End Equity", "200000" },
{ "Net Profit", "123.888%" },
{ "Sharpe Ratio", "1.707" },
{ "Sortino Ratio", "0.1" },
{ "Probabilistic Sharpe Ratio", "77.188%" },
{ "Loss Rate", "27%" },
{ "Win Rate", "73%" },
{ "Profit-Loss Ratio", "0.63" },
{ "Alpha", "0.374" },
{ "Beta", "-0.664" },
{ "Annual Standard Deviation", "0.217" },
{ "Annual Variance", "0.047" },
{ "Information Ratio", "1.571" },
{ "Tracking Error", "0.233" },
{ "Treynor Ratio", "-0.558" },
{ "Total Fees", "$1390.49" },
{ "Estimated Strategy Capacity", "ZRX6300000.00" },
{ "Drawdown Recovery", "3" }
};
optimizationBacktest.Equity = new CandlestickSeries
{
Values = new List<ISeriesPoint> { new Candlestick(1, 1, 1, 1, 1), new Candlestick(2, 2, 2, 2, 2), new Candlestick(3, 3, 3, 3, 3) }
};
if (version > 0)
{
optimizationBacktest.StartDate = new DateTime(2023, 01, 01);
optimizationBacktest.EndDate = new DateTime(2024, 01, 01);
optimizationBacktest.OutOfSampleMaxEndDate = new DateTime(2024, 01, 01);
optimizationBacktest.OutOfSampleDays = 10;
}
var serialized = JsonConvert.SerializeObject(optimizationBacktest);
var expected = _validSerialization;
if (version == 0)
{
expected = _oldValidSerialization;
}
Assert.AreEqual(expected, serialized);
}
[Test]
public void SerializationWithCustomStatistics()
{
var optimizationBacktest = new OptimizationBacktest(new ParameterSet(18,
new Dictionary<string, string>
{
{ "pinocho", "19" },
{ "pepe", "-1" }
}), "backtestId", "ImABacktestName");
optimizationBacktest.Statistics = new Dictionary<string, string>
{
{ "customstat2", "5.4321" },
{ "customstat1", "1.2345" },
{ "Total Orders", "180" },
{ "Average Win", "2.86%" },
{ "Average Loss", "-4.51%" },
{ "Compounding Annual Return", "52.602%" },
{ "Drawdown", "17.800%" },
{ "Expectancy", "0.196" },
{ "Start Equity", "100000" },
{ "End Equity", "200000" },
{ "Net Profit", "123.888%" },
{ "Sharpe Ratio", "1.707" },
{ "Sortino Ratio", "0.1" },
{ "Probabilistic Sharpe Ratio", "77.188%" },
{ "Loss Rate", "27%" },
{ "Win Rate", "73%" },
{ "Profit-Loss Ratio", "0.63" },
{ "Alpha", "0.374" },
{ "Beta", "-0.664" },
{ "Annual Standard Deviation", "0.217" },
{ "Annual Variance", "0.047" },
{ "Information Ratio", "1.571" },
{ "Tracking Error", "0.233" },
{ "Treynor Ratio", "-0.558" },
{ "Total Fees", "$1390.49" },
{ "Estimated Strategy Capacity", "ZRX6300000.00" },
{ "Drawdown Recovery", "3" }
};
optimizationBacktest.Equity = new CandlestickSeries
{
Values = new List<ISeriesPoint> { new Candlestick(1, 1, 1, 1, 1), new Candlestick(2, 2, 2, 2, 2), new Candlestick(3, 3, 3, 3, 3) }
};
optimizationBacktest.StartDate = new DateTime(2023, 01, 01);
optimizationBacktest.EndDate = new DateTime(2024, 01, 01);
optimizationBacktest.OutOfSampleMaxEndDate = new DateTime(2024, 01, 01);
optimizationBacktest.OutOfSampleDays = 10;
var serialized = JsonConvert.SerializeObject(optimizationBacktest);
Assert.AreEqual(_validSerializationWithCustomStats, serialized);
}
[TestCase(_validSerialization, false, 25)]
[TestCase(_oldValidSerialization, false, 25)]
[TestCase(_oldValid2Serialization, false, 25)]
// This case has only 21 stats because Sortino Ratio, Start Equity, End Equity and Drawdown Recovery were not supported
[TestCase(_oldValid3Serialization, false, 21)]
[TestCase(_validOldStatsDeserialization, false, 21)]
[TestCase(_validOldStatsDeserialization2, false, 21)]
[TestCase(_validOldStatsDeserialization3, false, 21)]
[TestCase(_validOldStatsDeserializationWithLessStats, false, 15)]
[TestCase(_validSerializationWithCustomStats, true, 25)]
public void Deserialization(string serialization, bool hasCustomStats, int expectedLeanStats)
{
var deserialized = JsonConvert.DeserializeObject<OptimizationBacktest>(serialization);
Assert.IsNotNull(deserialized);
Assert.AreEqual("ImABacktestName", deserialized.Name);
Assert.AreEqual("backtestId", deserialized.BacktestId);
Assert.AreEqual(0.0m, deserialized.Progress);
Assert.AreEqual(0, deserialized.ExitCode);
Assert.AreEqual(-1, deserialized.ParameterSet.Id);
Assert.IsTrue(deserialized.ParameterSet.Value.Count == 2);
Assert.IsTrue(deserialized.ParameterSet.Value["pinocho"] == "19");
Assert.IsTrue(deserialized.ParameterSet.Value["pepe"] == "-1");
Assert.IsTrue(deserialized.Equity.Values.Count == 3);
for (var i = 0; i < 3; i++)
{
var expected = i + 1;
Assert.IsTrue(((Candlestick)deserialized.Equity.Values[i]).LongTime == expected);
Assert.IsTrue(((Candlestick)deserialized.Equity.Values[i]).Open == expected);
Assert.IsTrue(((Candlestick)deserialized.Equity.Values[i]).High == expected);
Assert.IsTrue(((Candlestick)deserialized.Equity.Values[i]).Low == expected);
Assert.IsTrue(((Candlestick)deserialized.Equity.Values[i]).Close == expected);
}
Assert.AreEqual("77.188", deserialized.Statistics[PerformanceMetrics.ProbabilisticSharpeRatio]);
if (!hasCustomStats)
{
Assert.AreEqual(expectedLeanStats, deserialized.Statistics.Count);
}
else
{
// There are 2 custom stats
Assert.AreEqual(expectedLeanStats + 2, deserialized.Statistics.Count);
Assert.AreEqual("1.2345", deserialized.Statistics["customstat1"]);
Assert.AreEqual("5.4321", deserialized.Statistics["customstat2"]);
}
}
}
}