chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user