chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:02:50 +08:00
commit 0fc60fdcb1
5008 changed files with 910633 additions and 0 deletions
@@ -0,0 +1,137 @@
/*
* 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.Linq;
using System.Collections.Generic;
using NUnit.Framework;
using QuantConnect.Util;
using System;
using QuantConnect.Data.Market;
using QuantConnect.Algorithm;
using QuantConnect.Lean.Engine.Setup;
namespace QuantConnect.Tests.Common.Statistics
{
[TestFixture]
public class AnnualPerformanceTests
{
private List<TradeBar> _spy = new List<TradeBar>();
/// <summary>
/// Instance of QC Algorithm.
/// Use to get <see cref="Interfaces.IAlgorithmSettings.TradingDaysPerYear"/> for clear calculation in <seealso cref="QuantConnect.Statistics.Statistics.AnnualPerformance"/>
/// </summary>
private QCAlgorithm _algorithm;
[SetUp]
public void GetSPY()
{
_algorithm = new QCAlgorithm();
BaseSetupHandler.SetBrokerageTradingDayPerYear(_algorithm);
var symbol = Symbol.Create("SPY", SecurityType.Equity, Market.USA);
var path = LeanData.GenerateZipFilePath(Globals.DataFolder, symbol, new DateTime(2020, 3, 1), Resolution.Daily, TickType.Trade);
var config = new QuantConnect.Data.SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, false, false, false);
foreach (var line in QuantConnect.Compression.ReadLines(path))
{
var bar = TradeBar.ParseEquity(config, line, DateTime.Now.Date);
_spy.Add(bar);
}
}
[TearDown]
public void Delete()
{
_spy.Clear();
}
[Test]
public void TotalMarketPerformance()
{
var performance = new List<double>();
for (var i = 1; i < _spy.Count; i++)
{
performance.Add((double)((_spy[i].Close / _spy[i - 1].Close) - 1));
}
var result = QuantConnect.Statistics.Statistics.AnnualPerformance(performance, _algorithm.Settings.TradingDaysPerYear.Value);
Assert.AreEqual(0.082859685889996371, result);
}
[Test]
public void BearMarketPerformance()
{
var performance = new List<double>();
var start = new DateTime(2008, 5, 1);
var end = new DateTime(2009, 1, 1);
for (var i = 1; i < _spy.Count; i++)
{
if ((_spy[i].EndTime < start) || (_spy[i].EndTime > end))
{
continue;
}
performance.Add((double)((_spy[i].Close / _spy[i - 1].Close) - 1));
}
var result = QuantConnect.Statistics.Statistics.AnnualPerformance(performance, _algorithm.Settings.TradingDaysPerYear.Value);
Assert.AreEqual(-0.41546561808009674, result);
}
[Test]
public void BullMarketPerformance()
{
var performance = new List<double>();
var start = new DateTime(2017, 1, 1);
var end = new DateTime(2018, 1, 1);
for (var i = 1; i < _spy.Count; i++)
{
if ((_spy[i].EndTime < start) || (_spy[i].EndTime > end))
{
continue;
}
performance.Add((double)((_spy[i].Close / _spy[i - 1].Close) - 1));
}
var result = QuantConnect.Statistics.Statistics.AnnualPerformance(performance, _algorithm.Settings.TradingDaysPerYear.Value);
Assert.AreEqual(0.19741738320179447, result);
}
[Test]
public void FullYearPerformance()
{
// Ensure mean is 1
var performance = Enumerable.Repeat(0.5, 176).ToList();
performance.AddRange(Enumerable.Repeat(1.5, 176).ToList());
var result = QuantConnect.Statistics.Statistics.AnnualPerformance(performance, 4);
Assert.AreEqual(15.0, result);
}
[Test]
public void AllZeros()
{
var performance = Enumerable.Repeat(0.0, 252).ToList();
var result = QuantConnect.Statistics.Statistics.AnnualPerformance(performance, _algorithm.Settings.TradingDaysPerYear.Value);
Assert.AreEqual(0.0, result);
}
}
}
@@ -0,0 +1,62 @@
/*
* 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 NUnit.Framework;
namespace QuantConnect.Tests.Common.Statistics
{
[TestFixture]
internal class DrawdownRecoveryTests
{
[Test, TestCaseSource(nameof(TestCases))]
public void DrawdownMetricsMaximumRecoveryTimeTests(List<decimal> data, decimal expectedRecoveryTime)
{
var startDate = new DateTime(2025, 1, 1);
var equity = new SortedDictionary<DateTime, decimal>();
for (int i = 0; i < data.Count; i++)
{
var value = data[i];
equity[startDate.AddDays(i)] = value;
}
var result = QuantConnect.Statistics.Statistics.CalculateDrawdownMetrics(equity).DrawdownRecovery;
Assert.AreEqual(expectedRecoveryTime, result);
}
private static IEnumerable<TestCaseData> TestCases()
{
yield return new TestCaseData(new List<decimal> { 100, 90, 100 }, 2m).SetName("RecoveryAfterOneDip2Days");
yield return new TestCaseData(new List<decimal> { 100, 90, 95, 100 }, 3m).SetName("RecoveryAfterPartialThenFull3Days");
yield return new TestCaseData(new List<decimal> { 100, 90, 100, 90, 100 }, 2m).SetName("RecoveryFromTwoEqualDips2DaysEach");
yield return new TestCaseData(new List<decimal> { 100, 90, 100, 90, 80, 100 }, 3m).SetName("TakesLongestRecoveryAmongMultipleDrawdowns");
yield return new TestCaseData(new List<decimal> { 100, 90, 95, 90, 100 }, 4m).SetName("RecoveryFromNestedDrawdowns4Days");
yield return new TestCaseData(new List<decimal> { 100, 90, 80, 70 }, 0m).SetName("NoRecoveryContinuousDecline");
yield return new TestCaseData(new List<decimal> { 100, 90, 95, 90 }, 0m).SetName("NoRecoveryPartialButNoNewHigh");
yield return new TestCaseData(new List<decimal> { 50, 100, 98, 99, 100 }, 3m).SetName("RecoveryFromSecondaryPeak3Days");
yield return new TestCaseData(new List<decimal> { 100, 100, 100 }, 0m).SetName("NoDrawdownFlatLine");
yield return new TestCaseData(new List<decimal> { 100 }, 0m).SetName("NoDrawdownSingleValue");
yield return new TestCaseData(new List<decimal>(), 0m).SetName("NoDrawdownEmptyList");
yield return new TestCaseData(new List<decimal> { 100, 98, 100, 101, 100, 99 }, 2m).SetName("RecoveryBeforeNewHigh2Days");
yield return new TestCaseData(new List<decimal> { 100, 97, 99, 97, 100 }, 4m).SetName("RecoveryWithMultipleDips4Days");
}
}
}
@@ -0,0 +1,247 @@
/*
* 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.Statistics;
using QuantConnect.Tests.Indicators;
using static Microsoft.FSharp.Core.ByRefKinds;
namespace QuantConnect.Tests.Common.Statistics
{
[TestFixture]
class PortfolioStatisticsTests
{
private const decimal TradeFee = 2;
private readonly DateTime _startTime = new DateTime(2015, 08, 06, 15, 30, 0);
/// <summary>
/// TradingDaysPerYear: Use like backward compatibility
/// </summary>
/// <remarks><see cref="Interfaces.IAlgorithmSettings.TradingDaysPerYear"></remarks>
protected const int _tradingDaysPerYear = 252;
[Test]
public void ITMOptionAssignment([Values] bool win)
{
var statistics = GetPortfolioStatistics(win, _tradingDaysPerYear, new List<double> { 0, 0 }, new List<double> { 0, 0 });
if (win)
{
Assert.AreEqual(1m, statistics.WinRate);
Assert.AreEqual(0m, statistics.LossRate);
}
else
{
Assert.AreEqual(0.5m, statistics.WinRate);
Assert.AreEqual(0.5m, statistics.LossRate);
}
Assert.AreEqual(0.1173913043478260869565217391m, statistics.AverageWinRate);
Assert.AreEqual(-0.08m, statistics.AverageLossRate);
Assert.AreEqual(1.4673913043478260869565217388m, statistics.ProfitLossRatio);
}
public static IEnumerable<TestCaseData> StatisticsCases
{
get
{
yield return new TestCaseData(202, 0.00589787137120101M, 0.0767976000354244M, -3.0952570635188M, 0.167632655086644M, 0.252197874915608M);
yield return new TestCaseData(252, 0.00735774052248839M, 0.0857772727620108M, -3.3486737318423M, 0.187233350684845M, 0.257146306116665M);
yield return new TestCaseData(365, 0.0106570448043979M, 0.103232963748978M, -3.75507953923657M, 0.225335372429895M, 0.264390639112978M);
}
}
[TestCaseSource(nameof(StatisticsCases))]
public void ITMOptionAssignmentWithDifferentTradingDaysPerYearValue(
int tradingDaysPerYear, decimal expectedAnnualVariance, decimal expectedAnnualStandardDeviation,
decimal expectedSharpeRatio, decimal expectedTrackingError, decimal expectedProbabilisticSharpeRatio)
{
var listPerformance = new List<double> { -0.009025132, 0.003653969, 0, 0 };
var listBenchmark = new List<double> { -0.011587791300935783, 0.00054375782787618543, 0.022165997700413956, 0.006263266301918822 };
var statistics = GetPortfolioStatistics(true, tradingDaysPerYear, listPerformance, listBenchmark);
Assert.AreEqual(expectedAnnualVariance, statistics.AnnualVariance);
Assert.AreEqual(expectedAnnualStandardDeviation, statistics.AnnualStandardDeviation);
Assert.AreEqual(expectedSharpeRatio, statistics.SharpeRatio);
Assert.AreEqual(expectedTrackingError, statistics.TrackingError);
Assert.AreEqual(expectedProbabilisticSharpeRatio, statistics.ProbabilisticSharpeRatio);
}
[Test]
public void SharpeRatioAndProbabilisticSharpeRatioStayConsistent()
{
// A low-volatility asset with small positive daily returns
var start = new DateTime(2023, 1, 1);
var performance = new List<double>();
var equity = new SortedDictionary<DateTime, decimal>();
var value = 1_000_000m;
var random = new Random(42);
for (var i = 0; i < 500; i++)
{
// Random daily return drawn from a normal distribution
var z = Math.Sqrt(-2.0 * Math.Log(1 - random.NextDouble())) * Math.Cos(2.0 * Math.PI * random.NextDouble());
var dailyReturn = 0.00018 + 0.00016 * z;
performance.Add(dailyReturn);
value *= (decimal)(1 + dailyReturn);
equity[start.AddDays(i)] = value;
}
PortfolioStatistics BuildStatistics(decimal riskFreeRate) => new PortfolioStatistics(
new SortedDictionary<DateTime, decimal>(), equity, new SortedDictionary<DateTime, decimal>(),
performance, performance, 1_000_000m,
new ConstantRiskFreeRateInterestRateModel(riskFreeRate), _tradingDaysPerYear);
// Without a risk-free rate both the Sharpe ratio and the PSR are high
var grossStatistics = BuildStatistics(0m);
Assert.Greater(grossStatistics.SharpeRatio, 0m);
Assert.Greater(grossStatistics.ProbabilisticSharpeRatio, 0.5m);
// A risk-free rate above the return turns the Sharpe ratio negative, and the PSR drops with it
var excessStatistics = BuildStatistics(0.068m);
Assert.Less(excessStatistics.SharpeRatio, 0m);
Assert.Less(excessStatistics.ProbabilisticSharpeRatio, 0.1m);
}
[Test]
public void VaRMatchesExternalData()
{
var externalFileName = "spy_valueatrisk.csv";
var data = TestHelper.GetCsvFileStream(externalFileName);
var listPerformance = new List<double>();
var iteration = 0;
foreach (var row in data)
{
if (iteration == 0)
{
iteration++;
continue;
}
Parse.TryParse(row["returns"], NumberStyles.Float, out double returns);
listPerformance.Add(returns);
Parse.TryParse(row["VaR_99"], NumberStyles.Float, out decimal expected99);
Parse.TryParse(row["VaR_95"], NumberStyles.Float, out decimal expected95);
var statistics = GetPortfolioStatistics(
true,
_tradingDaysPerYear,
listPerformance,
new List<double> { 0, 0 });
Assert.AreEqual(Math.Round(expected99, 3), statistics.ValueAtRisk99);
Assert.AreEqual(Math.Round(expected95, 3), statistics.ValueAtRisk95);
}
}
[Test]
public void VaRIsZeroIfLessThan2Samples()
{
var listPerformance = new List<double> { 0.006196177273682046 };
var statistics = GetPortfolioStatistics(
true,
_tradingDaysPerYear,
listPerformance,
new List<double> { 0, 0 });
Assert.Zero(statistics.ValueAtRisk99);
Assert.Zero(statistics.ValueAtRisk95);
}
[Test]
public void PortfolioStatisticsDoesNotFailWhenAnnualPerformanceIsLarge()
{
var profitLoss = new SortedDictionary<DateTime, decimal>();
var equity = new SortedDictionary<DateTime, decimal>();
var portfolioTurnover = new SortedDictionary<DateTime, decimal>();
var listPerformance = new List<double>() { 0.6281421, 2.3815, -0.620932, 0.2795571 };
var listBenchmark = new List<double>() { -0.0015610669230773247, -0.024440492469623223, 0.008600225248460628, -0.020019532547249266 };
var startingCapital = 100000;
var riskFreeInterestRateModel = new InterestRateProvider();
var tradingDaysPerYear = 252;
Assert.DoesNotThrow(() => new PortfolioStatistics(profitLoss, equity, portfolioTurnover, listPerformance, listBenchmark, startingCapital, riskFreeInterestRateModel, tradingDaysPerYear));
}
/// <summary>
/// Initialize and return Portfolio Statistics depends on input data
/// </summary>
/// <param name="win">create profitable trade or not</param>
/// <param name="tradingDaysPerYear">amount days per year for brokerage (e.g. crypto exchange use 365 days)</param>
/// <param name="listPerformance">The list of algorithm performance values</param>
/// <param name="listBenchmark">The list of benchmark values</param>
/// <returns>The <see cref="PortfolioStatistics"/> class represents a set of statistics calculated from equity and benchmark samples</returns>
private PortfolioStatistics GetPortfolioStatistics(bool win, int tradingDaysPerYear, List<double> listPerformance, List<double> listBenchmark)
{
var trades = CreateITMOptionAssignment(win);
var profitLoss = new SortedDictionary<DateTime, decimal>(trades.ToDictionary(x => x.ExitTime, x => x.ProfitLoss));
var winCount = trades.Count(x => x.IsWin);
var lossCount = trades.Count - winCount;
return new PortfolioStatistics(profitLoss, new SortedDictionary<DateTime, decimal>(),
new SortedDictionary<DateTime, decimal>(), listPerformance, listBenchmark, 100000,
new InterestRateProvider(), tradingDaysPerYear, winCount, lossCount);
}
private List<Trade> CreateITMOptionAssignment(bool win)
{
var time = _startTime;
return new List<Trade>
{
new Trade
{
Symbols = [Symbols.SPY_C_192_Feb19_2016],
EntryTime = time,
EntryPrice = 80m,
Direction = TradeDirection.Long,
Quantity = 10,
ExitTime = time.AddMinutes(20),
ExitPrice = 0m,
ProfitLoss = -8000m,
TotalFees = TradeFee,
MAE = -8000m,
MFE = 0,
IsWin = win
},
new Trade
{
Symbols =[Symbols.SPY],
EntryTime = time.AddMinutes(20),
EntryPrice = 192m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = time.AddMinutes(30),
ExitPrice = 300m,
ProfitLoss = 10800m,
TotalFees = TradeFee,
MAE = 0,
MFE = 10800m,
IsWin = true
},
};
}
}
}
@@ -0,0 +1,110 @@
/*
* 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.Collections.Generic;
using NUnit.Framework;
namespace QuantConnect.Tests.Common.Statistics
{
[TestFixture]
public class ProbabilisticSharpeRatioTests
{
[Test]
public void SameAsBenchmark()
{
var performance = new List<double> { 0.01, 0.02, 0.01, 0, 0, 3 };
var benchmark = new List<double> { 0.01, 0.02, 0.01, 0, 0, 3 };
var benchmarkSharpeRatio = QuantConnect.Statistics.Statistics.ObservedSharpeRatio(benchmark);
var result = QuantConnect.Statistics.Statistics.ProbabilisticSharpeRatio(performance,
benchmarkSharpeRatio);
// they zero each other out
Assert.AreEqual(0.5d, result, 0.001);
}
[Test]
public void BeatBenchmark()
{
var performance = new List<double> { 0.01, 0.02, 0.01, 0, 0,3 };
var benchmark = new List<double> { 0, 0, 0, -0.1, 0, 0.01, 0 };
var benchmarkSharpeRatio = QuantConnect.Statistics.Statistics.ObservedSharpeRatio(benchmark);
var result = QuantConnect.Statistics.Statistics.ProbabilisticSharpeRatio(performance,
benchmarkSharpeRatio);
Assert.AreEqual(1d, result, 0.001);
}
[Test]
public void LoseAgainstBenchmark()
{
var benchmark = new List<double> { 0.01, 0.02, 0.01, 0, 0, 3 };
var performance = new List<double> { 0, 0, 0, -0.1, 0, 0.01, 0 };
var benchmarkSharpeRatio = QuantConnect.Statistics.Statistics.ObservedSharpeRatio(benchmark);
var result = QuantConnect.Statistics.Statistics.ProbabilisticSharpeRatio(performance,
benchmarkSharpeRatio);
Assert.AreEqual(0d, result, 0.001);
}
[Test]
public void ZeroValues()
{
var benchmark = new List<double> { 0, 0, 0 };
var performance = new List<double> { 0, 0, 0 };
var benchmarkSharpeRatio = QuantConnect.Statistics.Statistics.ObservedSharpeRatio(benchmark);
var result = QuantConnect.Statistics.Statistics.ProbabilisticSharpeRatio(performance,
benchmarkSharpeRatio);
Assert.AreEqual(0d, result, 0.001);
}
[Test]
public void UsesRiskFreeRateForObservedSharpeRatio()
{
// Gross returns clear the benchmark, so on a gross basis the PSR is high
var performance = new List<double> { 0.01, 0.02, 0.01, 0, 0, 3 };
var benchmarkSharpeRatio = 1.0d / System.Math.Sqrt(252);
var grossResult = QuantConnect.Statistics.Statistics.ProbabilisticSharpeRatio(performance, benchmarkSharpeRatio);
// A per-sample risk free rate above the average return makes the excess return negative,
// so the PSR must collapse below the gross one
var excessReturnResult = QuantConnect.Statistics.Statistics.ProbabilisticSharpeRatio(performance, benchmarkSharpeRatio, 0.6);
Assert.Greater(grossResult, 0.5d);
Assert.Less(excessReturnResult, 0.5d);
Assert.Greater(grossResult, excessReturnResult);
}
[Test]
public void ObservedSharpeRatioSubtractsRiskFreeRate()
{
var performance = new List<double> { 0.02, 0.04 };
// A risk free rate equal to the average return zeroes the excess observed sharpe ratio
Assert.AreEqual(0d, QuantConnect.Statistics.Statistics.ObservedSharpeRatio(performance, 0.03d), 1e-12);
// and it is strictly lower than the gross observed sharpe ratio
Assert.Greater(QuantConnect.Statistics.Statistics.ObservedSharpeRatio(performance),
QuantConnect.Statistics.Statistics.ObservedSharpeRatio(performance, 0.03d));
}
}
}
@@ -0,0 +1,92 @@
/*
* 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.Linq;
using NUnit.Framework;
using QuantConnect.Data;
using QuantConnect.Statistics;
namespace QuantConnect.Tests.Common.Statistics
{
[TestFixture]
public class StatisticsBuilderTests
{
/// <summary>
/// TradingDaysPerYear: Use like backward compatibility
/// </summary>
/// <remarks><see cref="Interfaces.IAlgorithmSettings.TradingDaysPerYear"></remarks>
protected const int _tradingDaysPerYear = 252;
[Test]
public void MisalignedValues_ShouldThrow_DuringGeneration()
{
var testBenchmarkPoints = new List<ChartPoint>
{
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 1, 16, 0, 0), DateTimeKind.Utc), 100),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 2, 16, 0, 0), DateTimeKind.Utc), 102),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 3, 16, 0, 0), DateTimeKind.Utc), 110),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 4, 16, 0, 0), DateTimeKind.Utc), 110),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 5, 16, 0, 0), DateTimeKind.Utc), 120),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 6, 16, 0, 0), DateTimeKind.Utc), 130),
};
var testEquityPoints = new List<ChartPoint>
{
new ChartPoint(DateTime.SpecifyKind(new DateTime(2018, 12, 31, 16, 0, 0), DateTimeKind.Utc), 100000),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 1, 16, 0, 0), DateTimeKind.Utc), 100000),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 2, 16, 0, 0), DateTimeKind.Utc), 102000),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 3, 16, 0, 0), DateTimeKind.Utc), 110000),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 4, 16, 0, 0), DateTimeKind.Utc), 110000),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 5, 16, 0, 0), DateTimeKind.Utc), 120000),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 6, 16, 0, 0), DateTimeKind.Utc), 130000),
};
var misalignedTestPerformancePoints = new List<ChartPoint>
{
new ChartPoint(DateTime.SpecifyKind(new DateTime(2018, 12, 31), DateTimeKind.Utc), 1000m * 100m),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 1, 16, 0, 0), DateTimeKind.Utc), 0.25m * 100m),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 2, 16, 0, 0), DateTimeKind.Utc), 0.02m * 100m),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 3, 16, 0, 0), DateTimeKind.Utc), 0.0784313725490196m * 100m),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 4, 16, 0, 0), DateTimeKind.Utc), 0 * 100m),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 5, 16, 0, 0), DateTimeKind.Utc), 0.090909090909090m * 100m),
new ChartPoint(DateTime.SpecifyKind(new DateTime(2019, 1, 6, 16, 0, 0), DateTimeKind.Utc), 0.083333333333333m * 100m)
};
Assert.Throws<ArgumentException>(() =>
{
StatisticsBuilder.Generate(
new List<Trade>(),
new SortedDictionary<DateTime, decimal>(),
testEquityPoints.Cast<ISeriesPoint>().ToList(),
misalignedTestPerformancePoints.Cast<ISeriesPoint>().ToList(),
testBenchmarkPoints.Cast<ISeriesPoint>().ToList(),
new List<ISeriesPoint>(),
100000m,
0m,
1,
null,
"$",
new QuantConnect.Securities.SecurityTransactionManager(
null,
new QuantConnect.Securities.SecurityManager(new TimeKeeper(DateTime.UtcNow))),
new InterestRateProvider(),
_tradingDaysPerYear);
}, "Misaligned values provided, but we still generate statistics");
}
}
}
@@ -0,0 +1,154 @@
/*
* 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.Linq;
using System.Collections.Generic;
using NUnit.Framework;
using QuantConnect.Util;
using QuantConnect.Data.Market;
using QuantConnect.Algorithm;
using QuantConnect.Lean.Engine.Setup;
namespace QuantConnect.Tests.Common.Statistics
{
[TestFixture]
public class TrackingErrorTests
{
private List<TradeBar> _spy = new List<TradeBar>();
private List<TradeBar> _aapl = new List<TradeBar>();
private List<double> _spyPerformance = new List<double>();
private List<double> _aaplPerformance = new List<double>();
/// <summary>
/// Instance of QC Algorithm.
/// Use to get <see cref="Interfaces.IAlgorithmSettings.TradingDaysPerYear"/> for clear calculation in <seealso cref="QuantConnect.Statistics.Statistics.AnnualPerformance"/>
/// </summary>
private QCAlgorithm _algorithm;
[OneTimeSetUp]
public void GetData()
{
_algorithm = new QCAlgorithm();
BaseSetupHandler.SetBrokerageTradingDayPerYear(_algorithm);
var spy = Symbol.Create("SPY", SecurityType.Equity, Market.USA);
var spyPath = LeanData.GenerateZipFilePath(Globals.DataFolder, spy, new DateTime(2020, 3, 1), Resolution.Daily, TickType.Trade);
var spyConfig = new QuantConnect.Data.SubscriptionDataConfig(typeof(TradeBar), spy, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, false, false, false);
var endDate = new DateTime(2020, 3, 8);
foreach (var line in QuantConnect.Compression.ReadLines(spyPath))
{
var bar = TradeBar.ParseEquity(spyConfig, line, DateTime.Now.Date);
if (bar.EndTime < endDate)
{
_spy.Add(bar);
}
}
for (var i = 1; i < _spy.Count; i++)
{
_spyPerformance.Add((double)((_spy[i].Close / _spy[i - 1].Close) - 1));
}
var aapl = Symbol.Create("AAPL", SecurityType.Equity, Market.USA);
var aaplPath = LeanData.GenerateZipFilePath(Globals.DataFolder, aapl, new DateTime(2020, 3, 1), Resolution.Daily, TickType.Trade);
var aaplConfig = new QuantConnect.Data.SubscriptionDataConfig(typeof(TradeBar), aapl, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, false, false, false);
foreach (var line in QuantConnect.Compression.ReadLines(aaplPath))
{
var bar = TradeBar.ParseEquity(aaplConfig, line, DateTime.Now.Date);
if (bar.EndTime < endDate)
{
_aapl.Add(bar);
}
}
for (var i = 1; i < _aapl.Count; i++)
{
_aaplPerformance.Add((double)((_aapl[i].Close / _aapl[i - 1].Close) - 1));
}
}
[OneTimeTearDown]
public void Delete()
{
_spy.Clear();
_aapl.Clear();
_spyPerformance.Clear();
_aaplPerformance.Clear();
}
[Test]
public void OneYearPerformance()
{
var result = QuantConnect.Statistics.Statistics.TrackingError(_aaplPerformance.Take(252).ToList(), _spyPerformance.Take(252).ToList(), _algorithm.Settings.TradingDaysPerYear.Value);
Assert.AreEqual(0.52780899407691173, result);
}
[Test]
public void TotalPerformance()
{
// This might seem arbitrary, but there's 1 missing date vs. AAPL for SPY data, and it happens to be at line 5555 for date 2020-01-31
var result = QuantConnect.Statistics.Statistics.TrackingError(_aaplPerformance.Take(5555).ToList(), _spyPerformance.Take(5555).ToList(), _algorithm.Settings.TradingDaysPerYear.Value);
Assert.AreEqual(0.43074391577621751d, result, 0.00001);
}
[Test]
public void IdenticalPerformance()
{
var random = new Random();
var benchmarkPerformance = Enumerable.Repeat(random.NextDouble(), 252).ToList();
var algoPerformance = benchmarkPerformance.Select(element => element).ToList();
var result = QuantConnect.Statistics.Statistics.TrackingError(algoPerformance, benchmarkPerformance, _algorithm.Settings.TradingDaysPerYear.Value);
Assert.AreEqual(0.0, result);
}
[Test]
public void DifferentPerformance()
{
var benchmarkPerformance = new List<double>();
var algoPerformance = new List<double>();
// Gives us two sequences whose difference is always -175
// This sequence will have variance 0
var baseReturn = -176;
for (var i = 1; i <= 252; i++)
{
benchmarkPerformance.Add(baseReturn + 1);
algoPerformance.Add((baseReturn * 2) + 2);
}
var result = QuantConnect.Statistics.Statistics.TrackingError(algoPerformance, benchmarkPerformance, _algorithm.Settings.TradingDaysPerYear.Value);
Assert.AreEqual(0.0, result);
}
[Test]
public void AllZeros()
{
var benchmarkPerformance = Enumerable.Repeat(0.0, 252).ToList();
var algoPerformance = Enumerable.Repeat(0.0, 252).ToList();
var result = QuantConnect.Statistics.Statistics.TrackingError(algoPerformance, benchmarkPerformance, _algorithm.Settings.TradingDaysPerYear.Value);
Assert.AreEqual(0.0, result);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,669 @@
/*
* 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 NUnit.Framework;
using QuantConnect.Statistics;
namespace QuantConnect.Tests.Common.Statistics
{
[TestFixture]
class TradeStatisticsTests
{
private const decimal TradeFee = 2;
private readonly DateTime _startTime = new DateTime(2015, 08, 06, 15, 30, 0);
[Test]
public void NoTrades()
{
var statistics = new TradeStatistics(new List<Trade>());
Assert.AreEqual(null, statistics.StartDateTime);
Assert.AreEqual(null, statistics.EndDateTime);
Assert.AreEqual(0, statistics.TotalNumberOfTrades);
Assert.AreEqual(0, statistics.NumberOfWinningTrades);
Assert.AreEqual(0, statistics.NumberOfLosingTrades);
Assert.AreEqual(0, statistics.TotalProfitLoss);
Assert.AreEqual(0, statistics.TotalProfit);
Assert.AreEqual(0, statistics.TotalLoss);
Assert.AreEqual(0, statistics.LargestProfit);
Assert.AreEqual(0, statistics.LargestLoss);
Assert.AreEqual(0, statistics.AverageProfitLoss);
Assert.AreEqual(0, statistics.AverageProfit);
Assert.AreEqual(0, statistics.AverageLoss);
Assert.AreEqual(TimeSpan.Zero, statistics.AverageTradeDuration);
Assert.AreEqual(TimeSpan.Zero, statistics.AverageWinningTradeDuration);
Assert.AreEqual(TimeSpan.Zero, statistics.AverageLosingTradeDuration);
Assert.AreEqual(0, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(0, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(0, statistics.ProfitLossRatio);
Assert.AreEqual(0, statistics.WinLossRatio);
Assert.AreEqual(0, statistics.WinRate);
Assert.AreEqual(0, statistics.LossRate);
Assert.AreEqual(0, statistics.AverageMAE);
Assert.AreEqual(0, statistics.AverageMFE);
Assert.AreEqual(0, statistics.LargestMAE);
Assert.AreEqual(0, statistics.LargestMFE);
Assert.AreEqual(0, statistics.MaximumClosedTradeDrawdown);
Assert.AreEqual(0, statistics.MaximumIntraTradeDrawdown);
Assert.AreEqual(0, statistics.ProfitLossStandardDeviation);
Assert.AreEqual(0, statistics.ProfitLossDownsideDeviation);
Assert.AreEqual(0, statistics.ProfitFactor);
Assert.AreEqual(0, statistics.SharpeRatio);
Assert.AreEqual(0, statistics.SortinoRatio);
Assert.AreEqual(0, statistics.ProfitToMaxDrawdownRatio);
Assert.AreEqual(0, statistics.MaximumEndTradeDrawdown);
Assert.AreEqual(0, statistics.AverageEndTradeDrawdown);
Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration);
Assert.AreEqual(0, statistics.TotalFees);
}
[Test]
public void ThreeWinners()
{
var statistics = new TradeStatistics(CreateThreeWinners());
Assert.AreEqual(_startTime, statistics.StartDateTime);
Assert.AreEqual(_startTime.AddMinutes(40), statistics.EndDateTime);
Assert.AreEqual(3, statistics.TotalNumberOfTrades);
Assert.AreEqual(3, statistics.NumberOfWinningTrades);
Assert.AreEqual(0, statistics.NumberOfLosingTrades);
Assert.AreEqual(50, statistics.TotalProfitLoss);
Assert.AreEqual(50, statistics.TotalProfit);
Assert.AreEqual(0, statistics.TotalLoss);
Assert.AreEqual(20, statistics.LargestProfit);
Assert.AreEqual(0, statistics.LargestLoss);
Assert.AreEqual(16.666666666666666666666666667m, statistics.AverageProfitLoss);
Assert.AreEqual(16.666666666666666666666666667m, statistics.AverageProfit);
Assert.AreEqual(0, statistics.AverageLoss);
Assert.AreEqual(TimeSpan.FromMinutes(20), statistics.AverageTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(20), statistics.AverageWinningTradeDuration);
Assert.AreEqual(TimeSpan.Zero, statistics.AverageLosingTradeDuration);
Assert.AreEqual(3, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(0, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(0, statistics.ProfitLossRatio);
Assert.AreEqual(10, statistics.WinLossRatio);
Assert.AreEqual(1, statistics.WinRate);
Assert.AreEqual(0, statistics.LossRate);
Assert.AreEqual(-16.666666666666666666666666667m, statistics.AverageMAE);
Assert.AreEqual(33.333333333333333333333333333m, statistics.AverageMFE);
Assert.AreEqual(-30, statistics.LargestMAE);
Assert.AreEqual(40, statistics.LargestMFE);
Assert.AreEqual(0, statistics.MaximumClosedTradeDrawdown);
Assert.AreEqual(-70, statistics.MaximumIntraTradeDrawdown);
Assert.AreEqual(5.77350269189626m, statistics.ProfitLossStandardDeviation);
Assert.AreEqual(0, statistics.ProfitLossDownsideDeviation);
Assert.AreEqual(10, statistics.ProfitFactor);
Assert.AreEqual(2.8867513459481276450914878051m, statistics.SharpeRatio);
Assert.AreEqual(0, statistics.SortinoRatio);
Assert.AreEqual(10, statistics.ProfitToMaxDrawdownRatio);
Assert.AreEqual(20, statistics.MaximumEndTradeDrawdown);
Assert.AreEqual(-16.666666666666666666666666666m, statistics.AverageEndTradeDrawdown);
Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration);
Assert.AreEqual(6, statistics.TotalFees);
}
private IEnumerable<Trade> CreateThreeWinners()
{
var time = _startTime;
return new List<Trade>
{
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time,
EntryPrice = 1.07m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = time.AddMinutes(20),
ExitPrice = 1.09m,
ProfitLoss = 20,
TotalFees = TradeFee,
MAE = -5,
MFE = 30,
EndTradeDrawdown = 10
},
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time.AddMinutes(10),
EntryPrice = 1.08m,
Direction = TradeDirection.Long,
Quantity = 2000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = 20,
TotalFees = TradeFee,
MAE = -30,
MFE = 40,
EndTradeDrawdown = 20
},
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time.AddMinutes(30),
EntryPrice = 1.08m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = 10,
TotalFees = TradeFee,
MAE = -15,
MFE = 30,
EndTradeDrawdown = 20
}
};
}
[Test]
public void ThreeLosers()
{
var statistics = new TradeStatistics(CreateThreeLosers());
Assert.AreEqual(_startTime, statistics.StartDateTime);
Assert.AreEqual(_startTime.AddMinutes(40), statistics.EndDateTime);
Assert.AreEqual(3, statistics.TotalNumberOfTrades);
Assert.AreEqual(0, statistics.NumberOfWinningTrades);
Assert.AreEqual(3, statistics.NumberOfLosingTrades);
Assert.AreEqual(-50, statistics.TotalProfitLoss);
Assert.AreEqual(0, statistics.TotalProfit);
Assert.AreEqual(-50, statistics.TotalLoss);
Assert.AreEqual(0, statistics.LargestProfit);
Assert.AreEqual(-20, statistics.LargestLoss);
Assert.AreEqual(-16.666666666666666666666666667m, statistics.AverageProfitLoss);
Assert.AreEqual(0, statistics.AverageProfit);
Assert.AreEqual(-16.666666666666666666666666667m, statistics.AverageLoss);
Assert.AreEqual(TimeSpan.FromMinutes(20), statistics.AverageTradeDuration);
Assert.AreEqual(TimeSpan.Zero, statistics.AverageWinningTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(20), statistics.AverageLosingTradeDuration);
Assert.AreEqual(0, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(3, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(0, statistics.ProfitLossRatio);
Assert.AreEqual(0, statistics.WinLossRatio);
Assert.AreEqual(0, statistics.WinRate);
Assert.AreEqual(1, statistics.LossRate);
Assert.AreEqual(-33.333333333333333333333333333m, statistics.AverageMAE);
Assert.AreEqual(16.666666666666666666666666667m, statistics.AverageMFE);
Assert.AreEqual(-40, statistics.LargestMAE);
Assert.AreEqual(30, statistics.LargestMFE);
Assert.AreEqual(-50, statistics.MaximumClosedTradeDrawdown);
Assert.AreEqual(-80, statistics.MaximumIntraTradeDrawdown);
Assert.AreEqual(5.77350269189626m, statistics.ProfitLossStandardDeviation);
Assert.AreEqual(5.77350269189626m, statistics.ProfitLossDownsideDeviation);
Assert.AreEqual(0, statistics.ProfitFactor);
Assert.AreEqual(-2.8867513459481276450914878051m, statistics.SharpeRatio);
Assert.AreEqual(-2.8867513459481276450914878051m, statistics.SortinoRatio);
Assert.AreEqual(-1, statistics.ProfitToMaxDrawdownRatio);
Assert.AreEqual(50, statistics.MaximumEndTradeDrawdown);
Assert.AreEqual(-33.333333333333333333333333334m, statistics.AverageEndTradeDrawdown);
Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration);
Assert.AreEqual(6, statistics.TotalFees);
}
private IEnumerable<Trade> CreateThreeLosers()
{
var time = _startTime;
return new List<Trade>
{
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time,
EntryPrice = 1.07m,
Direction = TradeDirection.Short,
Quantity = 1000,
ExitTime = time.AddMinutes(20),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -30,
MFE = 5,
EndTradeDrawdown = 25
},
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time.AddMinutes(10),
EntryPrice = 1.08m,
Direction = TradeDirection.Short,
Quantity = 2000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -40,
MFE = 30,
EndTradeDrawdown = 50
},
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time.AddMinutes(30),
EntryPrice = 1.08m,
Direction = TradeDirection.Short,
Quantity = 1000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = -10,
TotalFees = TradeFee,
MAE = -30,
MFE = 15,
EndTradeDrawdown = 25
}
};
}
[Test]
public void TwoLosersOneWinner()
{
var statistics = new TradeStatistics(CreateTwoLosersOneWinner());
Assert.AreEqual(_startTime, statistics.StartDateTime);
Assert.AreEqual(_startTime.AddMinutes(40), statistics.EndDateTime);
Assert.AreEqual(3, statistics.TotalNumberOfTrades);
Assert.AreEqual(1, statistics.NumberOfWinningTrades);
Assert.AreEqual(2, statistics.NumberOfLosingTrades);
Assert.AreEqual(-30, statistics.TotalProfitLoss);
Assert.AreEqual(10, statistics.TotalProfit);
Assert.AreEqual(-40, statistics.TotalLoss);
Assert.AreEqual(10, statistics.LargestProfit);
Assert.AreEqual(-20, statistics.LargestLoss);
Assert.AreEqual(-10, statistics.AverageProfitLoss);
Assert.AreEqual(10, statistics.AverageProfit);
Assert.AreEqual(-20, statistics.AverageLoss);
Assert.AreEqual(TimeSpan.FromSeconds(800), statistics.AverageTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(10), statistics.AverageWinningTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(15), statistics.AverageLosingTradeDuration);
Assert.AreEqual(1, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(2, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(0.5m, statistics.ProfitLossRatio);
Assert.AreEqual(0.5m, statistics.WinLossRatio);
Assert.AreEqual(0.3333333333333333333333333333m, statistics.WinRate);
Assert.AreEqual(0.6666666666666666666666666667m, statistics.LossRate);
Assert.AreEqual(-28.333333333333333333333333333333m, statistics.AverageMAE);
Assert.AreEqual(21.666666666666666666666666666667m, statistics.AverageMFE);
Assert.AreEqual(-40, statistics.LargestMAE);
Assert.AreEqual(30, statistics.LargestMFE);
Assert.AreEqual(-40, statistics.MaximumClosedTradeDrawdown);
Assert.AreEqual(-70, statistics.MaximumIntraTradeDrawdown);
Assert.AreEqual(17.3205080756888m, statistics.ProfitLossStandardDeviation);
Assert.AreEqual(0, statistics.ProfitLossDownsideDeviation);
Assert.AreEqual(0.25m, statistics.ProfitFactor);
Assert.AreEqual(-0.5773502691896248623516308943m, statistics.SharpeRatio);
Assert.AreEqual(0, statistics.SortinoRatio);
Assert.AreEqual(-0.75m, statistics.ProfitToMaxDrawdownRatio);
Assert.AreEqual(50, statistics.MaximumEndTradeDrawdown);
Assert.AreEqual(-31.666666666666666666666666666667m, statistics.AverageEndTradeDrawdown);
Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration);
Assert.AreEqual(6, statistics.TotalFees);
}
private IEnumerable<Trade> CreateTwoLosersOneWinner()
{
var time = _startTime;
return new List<Trade>
{
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time,
EntryPrice = 1.07m,
Direction = TradeDirection.Short,
Quantity = 1000,
ExitTime = time.AddMinutes(20),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -30,
MFE = 5,
EndTradeDrawdown = 30
},
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time.AddMinutes(10),
EntryPrice = 1.08m,
Direction = TradeDirection.Short,
Quantity = 2000,
ExitTime = time.AddMinutes(20),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -40,
MFE = 30,
EndTradeDrawdown = 50
},
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time.AddMinutes(30),
EntryPrice = 1.08m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = 10,
TotalFees = TradeFee,
MAE = -15,
MFE = 30,
EndTradeDrawdown = 20
}
};
}
[Test]
public void OneWinnerTwoLosers()
{
var statistics = new TradeStatistics(CreateOneWinnerTwoLosers());
Assert.AreEqual(_startTime, statistics.StartDateTime);
Assert.AreEqual(_startTime.AddMinutes(40), statistics.EndDateTime);
Assert.AreEqual(3, statistics.TotalNumberOfTrades);
Assert.AreEqual(1, statistics.NumberOfWinningTrades);
Assert.AreEqual(2, statistics.NumberOfLosingTrades);
Assert.AreEqual(-30, statistics.TotalProfitLoss);
Assert.AreEqual(10, statistics.TotalProfit);
Assert.AreEqual(-40, statistics.TotalLoss);
Assert.AreEqual(10, statistics.LargestProfit);
Assert.AreEqual(-20, statistics.LargestLoss);
Assert.AreEqual(-10, statistics.AverageProfitLoss);
Assert.AreEqual(10, statistics.AverageProfit);
Assert.AreEqual(-20, statistics.AverageLoss);
Assert.AreEqual(TimeSpan.FromSeconds(800), statistics.AverageTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(10), statistics.AverageWinningTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(15), statistics.AverageLosingTradeDuration);
Assert.AreEqual(1, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(2, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(0.5m, statistics.ProfitLossRatio);
Assert.AreEqual(0.5m, statistics.WinLossRatio);
Assert.AreEqual(0.3333333333333333333333333333m, statistics.WinRate);
Assert.AreEqual(0.6666666666666666666666666667m, statistics.LossRate);
Assert.AreEqual(-28.333333333333333333333333333333m, statistics.AverageMAE);
Assert.AreEqual(21.666666666666666666666666666667m, statistics.AverageMFE);
Assert.AreEqual(-40, statistics.LargestMAE);
Assert.AreEqual(30, statistics.LargestMFE);
Assert.AreEqual(-40, statistics.MaximumClosedTradeDrawdown);
Assert.AreEqual(-80, statistics.MaximumIntraTradeDrawdown);
Assert.AreEqual(17.3205080756888m, statistics.ProfitLossStandardDeviation);
Assert.AreEqual(0, statistics.ProfitLossDownsideDeviation);
Assert.AreEqual(0.25m, statistics.ProfitFactor);
Assert.AreEqual(-0.5773502691896248623516308943m, statistics.SharpeRatio);
Assert.AreEqual(0, statistics.SortinoRatio);
Assert.AreEqual(-0.75m, statistics.ProfitToMaxDrawdownRatio);
Assert.AreEqual(50, statistics.MaximumEndTradeDrawdown);
Assert.AreEqual(-31.666666666666666666666666666667m, statistics.AverageEndTradeDrawdown);
Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration);
Assert.AreEqual(6, statistics.TotalFees);
}
private IEnumerable<Trade> CreateOneWinnerTwoLosers()
{
var time = _startTime;
return new List<Trade>
{
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time,
EntryPrice = 1.08m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = time.AddMinutes(10),
ExitPrice = 1.09m,
ProfitLoss = 10,
TotalFees = TradeFee,
MAE = -15,
MFE = 30,
EndTradeDrawdown = 20
},
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time.AddMinutes(20),
EntryPrice = 1.07m,
Direction = TradeDirection.Short,
Quantity = 1000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -30,
MFE = 5,
EndTradeDrawdown = 25
},
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time.AddMinutes(30),
EntryPrice = 1.08m,
Direction = TradeDirection.Short,
Quantity = 2000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -40,
MFE = 30,
EndTradeDrawdown = 50
}
};
}
[Test]
public void OneLoserTwoWinners()
{
var statistics = new TradeStatistics(CreateOneLoserTwoWinners());
Assert.AreEqual(_startTime, statistics.StartDateTime);
Assert.AreEqual(_startTime.AddMinutes(40), statistics.EndDateTime);
Assert.AreEqual(3, statistics.TotalNumberOfTrades);
Assert.AreEqual(2, statistics.NumberOfWinningTrades);
Assert.AreEqual(1, statistics.NumberOfLosingTrades);
Assert.AreEqual(10, statistics.TotalProfitLoss);
Assert.AreEqual(30, statistics.TotalProfit);
Assert.AreEqual(-20, statistics.TotalLoss);
Assert.AreEqual(20, statistics.LargestProfit);
Assert.AreEqual(-20, statistics.LargestLoss);
Assert.AreEqual(3.3333333333333333333333333333m, statistics.AverageProfitLoss);
Assert.AreEqual(15, statistics.AverageProfit);
Assert.AreEqual(-20, statistics.AverageLoss);
Assert.AreEqual(TimeSpan.FromSeconds(800), statistics.AverageTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(10), statistics.AverageWinningTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(20), statistics.AverageLosingTradeDuration);
Assert.AreEqual(2, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(1, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(0.75m, statistics.ProfitLossRatio);
Assert.AreEqual(2, statistics.WinLossRatio);
Assert.AreEqual(0.6666666666666666666666666667m, statistics.WinRate);
Assert.AreEqual(0.3333333333333333333333333333m, statistics.LossRate);
Assert.AreEqual(-28.333333333333333333333333333333m, statistics.AverageMAE);
Assert.AreEqual(21.666666666666666666666666666667m, statistics.AverageMFE);
Assert.AreEqual(-40, statistics.LargestMAE);
Assert.AreEqual(30, statistics.LargestMFE);
Assert.AreEqual(-20, statistics.MaximumClosedTradeDrawdown);
Assert.AreEqual(-70, statistics.MaximumIntraTradeDrawdown);
Assert.AreEqual(20.8166599946613m, statistics.ProfitLossStandardDeviation);
Assert.AreEqual(0, statistics.ProfitLossDownsideDeviation);
Assert.AreEqual(1.5m, statistics.ProfitFactor);
Assert.AreEqual(0.1601281538050873438895842626m, statistics.SharpeRatio);
Assert.AreEqual(0, statistics.SortinoRatio);
Assert.AreEqual(0.5m, statistics.ProfitToMaxDrawdownRatio);
Assert.AreEqual(25, statistics.MaximumEndTradeDrawdown);
Assert.AreEqual(-18.333333333333333333333333334m, statistics.AverageEndTradeDrawdown);
Assert.AreEqual(TimeSpan.FromMinutes(40), statistics.MaximumDrawdownDuration);
Assert.AreEqual(6, statistics.TotalFees);
}
private IEnumerable<Trade> CreateOneLoserTwoWinners()
{
var time = _startTime;
return new List<Trade>
{
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time,
EntryPrice = 1.07m,
Direction = TradeDirection.Short,
Quantity = 1000,
ExitTime = time.AddMinutes(20),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -30,
MFE = 5,
EndTradeDrawdown = 25
},
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time.AddMinutes(10),
EntryPrice = 1.08m,
Direction = TradeDirection.Long,
Quantity = 2000,
ExitTime = time.AddMinutes(20),
ExitPrice = 1.09m,
ProfitLoss = 20,
TotalFees = TradeFee,
MAE = -40,
MFE = 30,
EndTradeDrawdown = 10
},
new Trade
{
Symbols =[Symbols.EURUSD],
EntryTime = time.AddMinutes(30),
EntryPrice = 1.08m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = 10,
TotalFees = TradeFee,
MAE = -15,
MFE = 30,
EndTradeDrawdown = 20
}
};
}
[Test]
public void ITMOptionAssignment([Values] bool win)
{
var statistics = new TradeStatistics(CreateITMOptionAssignment(win));
if (win)
{
Assert.AreEqual(2, statistics.NumberOfWinningTrades);
Assert.AreEqual(0, statistics.NumberOfLosingTrades);
Assert.AreEqual(2, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(0, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(10, statistics.WinLossRatio);
Assert.AreEqual(1m, statistics.WinRate);
Assert.AreEqual(0m, statistics.LossRate);
}
else
{
Assert.AreEqual(1, statistics.NumberOfWinningTrades);
Assert.AreEqual(1, statistics.NumberOfLosingTrades);
Assert.AreEqual(1, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(1, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(1m, statistics.WinLossRatio);
Assert.AreEqual(0.5m, statistics.WinRate);
Assert.AreEqual(0.5m, statistics.LossRate);
}
Assert.AreEqual(_startTime, statistics.StartDateTime);
Assert.AreEqual(_startTime.AddMinutes(30), statistics.EndDateTime);
Assert.AreEqual(2, statistics.TotalNumberOfTrades);
Assert.AreEqual(28000m, statistics.TotalProfitLoss);
Assert.AreEqual(108000m, statistics.TotalProfit);
Assert.AreEqual(-80000m, statistics.TotalLoss);
Assert.AreEqual(108000m, statistics.LargestProfit);
Assert.AreEqual(-80000m, statistics.LargestLoss);
Assert.AreEqual(14000m, statistics.AverageProfitLoss);
Assert.AreEqual(108000m, statistics.AverageProfit);
Assert.AreEqual(-80000m, statistics.AverageLoss);
Assert.AreEqual(TimeSpan.FromMinutes(15), statistics.AverageTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(10), statistics.AverageWinningTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(20), statistics.AverageLosingTradeDuration);
Assert.AreEqual(1.35m, statistics.ProfitLossRatio);
Assert.AreEqual(-40000m, statistics.AverageMAE);
Assert.AreEqual(54000m, statistics.AverageMFE);
Assert.AreEqual(-80000, statistics.LargestMAE);
Assert.AreEqual(108000, statistics.LargestMFE);
Assert.AreEqual(-80000, statistics.MaximumClosedTradeDrawdown);
Assert.AreEqual(-108000, statistics.MaximumIntraTradeDrawdown);
Assert.AreEqual(132936.074863071m, statistics.ProfitLossStandardDeviation);
Assert.AreEqual(0m, statistics.ProfitLossDownsideDeviation);
Assert.AreEqual(1.35m, statistics.ProfitFactor);
Assert.AreEqual(0.1053137759214006433027413265m, statistics.SharpeRatio);
Assert.AreEqual(0m, statistics.SortinoRatio);
Assert.AreEqual(0.35m, statistics.ProfitToMaxDrawdownRatio);
Assert.AreEqual(80000, statistics.MaximumEndTradeDrawdown);
Assert.AreEqual(-40000m, statistics.AverageEndTradeDrawdown);
Assert.AreEqual(TimeSpan.FromMinutes(30), statistics.MaximumDrawdownDuration);
Assert.AreEqual(4, statistics.TotalFees);
}
private IEnumerable<Trade> CreateITMOptionAssignment(bool win)
{
var time = _startTime;
return new List<Trade>
{
new Trade
{
Symbols = [Symbols.SPY_C_192_Feb19_2016],
EntryTime = time,
EntryPrice = 80m,
Direction = TradeDirection.Long,
Quantity = 10,
ExitTime = time.AddMinutes(20),
ExitPrice = 0m,
ProfitLoss = -80000m,
TotalFees = TradeFee,
MAE = -80000m,
MFE = 0,
EndTradeDrawdown = 80000m,
IsWin = win,
},
new Trade
{
Symbols =[Symbols.SPY],
EntryTime = time.AddMinutes(20),
EntryPrice = 192m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = time.AddMinutes(30),
ExitPrice = 300m,
ProfitLoss = 108000m,
TotalFees = TradeFee,
MAE = 0,
MFE = 108000m,
EndTradeDrawdown = 0m,
IsWin = true,
},
};
}
}
}
+125
View File
@@ -0,0 +1,125 @@
/*
* 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 Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using QuantConnect.Statistics;
using System;
namespace QuantConnect.Tests.Common.Statistics
{
[TestFixture]
public class TradeTests
{
[Test]
public void JsonSerializationRoundTrip()
{
var trade = MakeTrade();
var json = JsonConvert.SerializeObject(trade);
var deserializedTrade = JsonConvert.DeserializeObject<Trade>(json);
CollectionAssert.AreEqual(trade.Symbols, deserializedTrade.Symbols);
Assert.AreEqual(trade.EntryTime, deserializedTrade.EntryTime);
Assert.AreEqual(trade.EntryPrice, deserializedTrade.EntryPrice);
Assert.AreEqual(trade.Direction, deserializedTrade.Direction);
Assert.AreEqual(trade.Quantity, deserializedTrade.Quantity);
Assert.AreEqual(trade.ExitTime, deserializedTrade.ExitTime);
Assert.AreEqual(trade.ExitPrice, deserializedTrade.ExitPrice);
Assert.AreEqual(trade.ProfitLoss, deserializedTrade.ProfitLoss);
Assert.AreEqual(trade.TotalFees, deserializedTrade.TotalFees);
Assert.AreEqual(trade.MAE, deserializedTrade.MAE);
Assert.AreEqual(trade.MFE, deserializedTrade.MFE);
// For backwards compatibility, also verify Symbol property is set correctly
Assert.IsNotNull(trade.Symbol);
Assert.AreEqual(trade.Symbols[0], trade.Symbol);
Assert.AreEqual(trade.Symbol, deserializedTrade.Symbol);
}
[Test]
public void DeprecatedSymbolIsNotSerialized()
{
var trade = MakeTrade();
var jsonStr = JsonConvert.SerializeObject(trade);
var json = JObject.Parse(jsonStr);
Assert.IsFalse(json.ContainsKey("Symbol"));
}
[Test]
public void CanDeserializeOldFormatWithSymbol()
{
var jsonTrade = @"
{
""Symbol"": {
""value"": ""EURUSD"",
""id"": ""EURUSD 8G"",
""permtick"": ""EURUSD""
},
""EntryTime"": ""2023-01-02T12:31:45"",
""EntryPrice"": 1.07,
""Direction"": 0,
""Quantity"": 1000.0,
""ExitTime"": ""2023-01-02T12:51:45"",
""ExitPrice"": 1.09,
""ProfitLoss"": 20.0,
""TotalFees"": 2.5,
""MAE"": -5.0,
""MFE"": 30.0,
""Duration"": ""00:20:00"",
""EndTradeDrawdown"": -10.0,
""IsWin"": false,
""OrderIds"": []
}";
var deserializedTrade = JsonConvert.DeserializeObject<Trade>(jsonTrade);
Assert.IsNotNull(deserializedTrade);
CollectionAssert.AreEqual(new[] { Symbols.EURUSD }, deserializedTrade.Symbols);
Assert.AreEqual(new DateTime(2023, 1, 2, 12, 31, 45), deserializedTrade.EntryTime);
Assert.AreEqual(1.07m, deserializedTrade.EntryPrice);
Assert.AreEqual(TradeDirection.Long, deserializedTrade.Direction);
Assert.AreEqual(1000m, deserializedTrade.Quantity);
Assert.AreEqual(new DateTime(2023, 1, 2, 12, 51, 45), deserializedTrade.ExitTime);
Assert.AreEqual(1.09m, deserializedTrade.ExitPrice);
Assert.AreEqual(20m, deserializedTrade.ProfitLoss);
Assert.AreEqual(2.5m, deserializedTrade.TotalFees);
Assert.AreEqual(-5m, deserializedTrade.MAE);
Assert.AreEqual(30m, deserializedTrade.MFE);
// For backwards compatibility, also verify Symbol property is set correctly
Assert.IsNotNull(deserializedTrade.Symbol);
Assert.AreEqual(deserializedTrade.Symbols[0], deserializedTrade.Symbol);
}
private static Trade MakeTrade()
{
var entryTime = new DateTime(2023, 1, 2, 12, 31, 45);
var exitTime = entryTime.AddMinutes(20);
var trade = new Trade
{
Symbols = [Symbols.EURUSD],
EntryTime = entryTime,
EntryPrice = 1.07m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = exitTime,
ExitPrice = 1.09m,
ProfitLoss = 20,
TotalFees = 2.5m,
MAE = -5,
MFE = 30
};
return trade;
}
}
}