From e036eaca5bffe5037e02091cf478434868984b8f Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Wed, 14 Aug 2024 18:25:11 -0400 Subject: [PATCH 01/26] Implement a prototype of the maximum recovery time function. --- Common/Statistics/Statistics.cs | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Common/Statistics/Statistics.cs b/Common/Statistics/Statistics.cs index 289a4453e484..cde9543efbcd 100644 --- a/Common/Statistics/Statistics.cs +++ b/Common/Statistics/Statistics.cs @@ -281,6 +281,53 @@ public static decimal DrawdownPercent(decimal current, decimal high, int roundin return Math.Round(drawdownPercentage, roundingDecimals); } + /// + /// Calculates the maximum recovery time of price data. + /// + /// A sorted dictionary of TimeSeries objects (Time and Price) used to calculate the maximum recovery time. + /// Maximum Recovery Time + + public static TimeSpan MaximumRecoveryTime(SortedDictionary equityOverTime) + { + + if (equityOverTime == null || equityOverTime.Count < 2) + return TimeSpan.Zero; + + TimeSpan maxRecoveryTime = TimeSpan.Zero; + DateTime peakTime = equityOverTime.First().Key; + decimal peakPrice = equityOverTime.First().Value; + DateTime troughTime = equityOverTime.First().Key; + decimal troughPrice = equityOverTime.First().Value; + + foreach (var point in equityOverTime) + { + var currentPrice = point.Value; + var currentTime = point.Key; + if (currentPrice > peakPrice) + { + peakPrice = currentPrice; + peakTime = currentTime; + troughPrice = currentPrice; + troughTime = currentTime; + } + else if (currentPrice < troughPrice) + { + troughPrice = currentPrice; + troughTime = currentTime; + } + else if (currentPrice >= peakPrice && troughTime > peakTime) + { + TimeSpan recoveryTime = currentTime - troughTime; + if (recoveryTime > maxRecoveryTime) + { + maxRecoveryTime = recoveryTime; + } + } + } + + return maxRecoveryTime; + } + } // End of Statistics } // End of Namespace From 7c4cbc3bff7027b85ad9ff38aae848a5f2c24547 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Wed, 14 Aug 2024 18:46:54 -0400 Subject: [PATCH 02/26] Add unit test skeletons. --- .../Common/Statistics/MaximumRecoveryTests.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 Tests/Common/Statistics/MaximumRecoveryTests.cs diff --git a/Tests/Common/Statistics/MaximumRecoveryTests.cs b/Tests/Common/Statistics/MaximumRecoveryTests.cs new file mode 100644 index 000000000000..035675842902 --- /dev/null +++ b/Tests/Common/Statistics/MaximumRecoveryTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using QuantConnect.Statistics; + +namespace QuantConnect.Tests.Common.Statistics +{ + internal class MaximumRecoveryTests + { + [Test] + public void MaximumRecoveryTests_RepeatedDrawdownsSameLevelButOneLonger_ReturnLongerDrawdown() + { + + } + + [Test] + public void MaximumRecoveryTests_RepeatedDrawdownsSameLevelSameLength_ReturnOneOfThem() + { + + } + + [Test] + public void MaximumRecoveryTests_NoRecovery_ReturnZero() + { + + } + + [Test] + public void MaximumRecoveryTests_BasicRecovery_ReturnRecoveryTime() + { + + } + + [Test] + public void MaximumRecoveryTests_FlatPrice_ReturnZero() + { + + } + + [Test] + public void MaximumRecoveryTests_SingleDataPoint_ReturnZero() + { + + } + + [Test] + public void MaximumRecoveryTests_EmptyDataSet_ReturnZero() + { + + } + + [Test] + public void MaximumRecoveryTests_LongRecoveryIntermediatePeaks_ReturnCorrectRecoveryTime() + { + + } + + [Test] + public void MaximumRecoveryTests_PriceCrashesThroughPreviousHigh_ReturnCorrectRecoveryTime() + { + + } + + } +} From 93590a0e68fda36e992ad40be15d4ef0d1cf550e Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Wed, 14 Aug 2024 19:08:20 -0400 Subject: [PATCH 03/26] Add failing test --- .../Common/Statistics/MaximumRecoveryTests.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Tests/Common/Statistics/MaximumRecoveryTests.cs b/Tests/Common/Statistics/MaximumRecoveryTests.cs index 035675842902..643d2adaad7d 100644 --- a/Tests/Common/Statistics/MaximumRecoveryTests.cs +++ b/Tests/Common/Statistics/MaximumRecoveryTests.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using NUnit.Framework; +using QLNet; using QuantConnect.Statistics; namespace QuantConnect.Tests.Common.Statistics @@ -25,6 +26,31 @@ public void MaximumRecoveryTests_RepeatedDrawdownsSameLevelSameLength_ReturnOneO [Test] public void MaximumRecoveryTests_NoRecovery_ReturnZero() { + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 100 }, + { startDate.AddDays(1), 90 }, + { startDate.AddDays(2), 98 }, + { startDate.AddDays(3), 99 } + }; + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaximumRecoveryTime(equityOverTime); + Assert.AreEqual(TimeSpan.Zero, maximumRecoveryTime); + } + + [Test] + public void MaximumRecoveryTests_BasicRecoveryWithin_ReturnRecoveryTime() + { + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 100 }, + { startDate.AddDays(1), 99 }, + { startDate.AddDays(2), 98 }, + { startDate.AddDays(3), 99 } + }; + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaximumRecoveryTime(equityOverTime); + Assert.AreEqual(TimeSpan.FromDays(1), maximumRecoveryTime); } From 6bd5cee99f07d7f1c0dd25d218e91f071f667dfa Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Wed, 21 Aug 2024 14:57:51 -0400 Subject: [PATCH 04/26] Issue #4581: Implement MaxDrawdownRecoveryTime. --- Common/Statistics/Statistics.cs | 90 ++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 41 deletions(-) diff --git a/Common/Statistics/Statistics.cs b/Common/Statistics/Statistics.cs index cde9543efbcd..9930e3f1ba07 100644 --- a/Common/Statistics/Statistics.cs +++ b/Common/Statistics/Statistics.cs @@ -18,6 +18,7 @@ using System.IO; using System.Linq; using System.Net; +using MathNet.Numerics; using MathNet.Numerics.Distributions; using MathNet.Numerics.Statistics; using QuantConnect.Logging; @@ -37,17 +38,37 @@ public class Statistics /// /// public static decimal DrawdownPercent(SortedDictionary equityOverTime, int rounding = 2) + { + return DrawdownPercentDrawdownDateHighValue(equityOverTime, rounding).DrawdownPercent; + } + + /// + /// Returns Drawdown percentage, the date the drawdown ended and the price value at which the drawdown started. + /// + /// + /// + /// + + private static DrawdownDrawdownDateHighValueDTO DrawdownPercentDrawdownDateHighValue(SortedDictionary equityOverTime, int rounding = 2) { var dd = 0m; + var maxDrawdownDate = DateTime.MinValue; + var highValueOutsideOfTryBlock = 0m; try { - var lPrices = equityOverTime.Values.ToList(); + var lPrices = equityOverTime.ToList(); var lDrawdowns = new List(); - var high = lPrices[0]; - foreach (var price in lPrices) + var high = lPrices[0].Value; + foreach (var kvp in lPrices) { - if (price >= high) high = price; - lDrawdowns.Add((price/high) - 1); + if (kvp.Value >= high) high = kvp.Value; + var drawdown = (kvp.Value / high) - 1; + lDrawdowns.Add(drawdown); + if (drawdown < lDrawdowns.Min()) + { + maxDrawdownDate = kvp.Key; + highValueOutsideOfTryBlock = high; + } } dd = Math.Round(Math.Abs(lDrawdowns.Min()), rounding); } @@ -55,7 +76,7 @@ public static decimal DrawdownPercent(SortedDictionary equity { Log.Error(err); } - return dd; + return new DrawdownDrawdownDateHighValueDTO(dd, maxDrawdownDate, highValueOutsideOfTryBlock); } /// @@ -282,50 +303,37 @@ public static decimal DrawdownPercent(decimal current, decimal high, int roundin } /// - /// Calculates the maximum recovery time of price data. + /// Calculates the recovery time of the maximum drawdown in days. /// - /// A sorted dictionary of TimeSeries objects (Time and Price) used to calculate the maximum recovery time. - /// Maximum Recovery Time + /// Price Data + /// Latest maximum + /// Digits to round the result to. + /// Recovery time of maximum drawdown in days. - public static TimeSpan MaximumRecoveryTime(SortedDictionary equityOverTime) + public static double MaxDrawdownRecoveryTime(SortedDictionary equityOverTime, int rounding = 2) { + var drawdownDrawdownDateHighValueDTO = DrawdownPercentDrawdownDateHighValue(equityOverTime); - if (equityOverTime == null || equityOverTime.Count < 2) - return TimeSpan.Zero; + var recoveryThresholdPrice = drawdownDrawdownDateHighValueDTO.HighPrice; + var maxDradownEndDate = drawdownDrawdownDateHighValueDTO.MaxDrawdownEndDate; - TimeSpan maxRecoveryTime = TimeSpan.Zero; - DateTime peakTime = equityOverTime.First().Key; - decimal peakPrice = equityOverTime.First().Value; - DateTime troughTime = equityOverTime.First().Key; - decimal troughPrice = equityOverTime.First().Value; + if (maxDradownEndDate == DateTime.MinValue) + { + return 0; // No drawdown occurred. Do we want a special return value for this? + } + + var recoveryDate = equityOverTime + .Where(kvp => kvp.Key > maxDradownEndDate && kvp.Value >= recoveryThresholdPrice) + .Select(kvp => kvp.Key) + .DefaultIfEmpty(DateTime.MaxValue) + .First(); - foreach (var point in equityOverTime) + if (recoveryDate == DateTime.MaxValue) { - var currentPrice = point.Value; - var currentTime = point.Key; - if (currentPrice > peakPrice) - { - peakPrice = currentPrice; - peakTime = currentTime; - troughPrice = currentPrice; - troughTime = currentTime; - } - else if (currentPrice < troughPrice) - { - troughPrice = currentPrice; - troughTime = currentTime; - } - else if (currentPrice >= peakPrice && troughTime > peakTime) - { - TimeSpan recoveryTime = currentTime - troughTime; - if (recoveryTime > maxRecoveryTime) - { - maxRecoveryTime = recoveryTime; - } - } + return 0; // Not yet recovered } - return maxRecoveryTime; + return (recoveryDate - maxDradownEndDate).TotalDays.Round(rounding); } } // End of Statistics From 83270c0bc06dafc3ca37cbf9294739b9eb1434fd Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Thu, 22 Aug 2024 15:51:32 -0400 Subject: [PATCH 05/26] Issue 4581: Add DTO for Drawdown Percentage, Drawdown Enddate, and High Value --- .../DrawdownDrawdownDateHighValueDTO.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Common/Statistics/DrawdownDrawdownDateHighValueDTO.cs diff --git a/Common/Statistics/DrawdownDrawdownDateHighValueDTO.cs b/Common/Statistics/DrawdownDrawdownDateHighValueDTO.cs new file mode 100644 index 000000000000..b5fe94574756 --- /dev/null +++ b/Common/Statistics/DrawdownDrawdownDateHighValueDTO.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QuantConnect.Statistics +{ + internal class DrawdownDrawdownDateHighValueDTO + { + public decimal DrawdownPercent { get; } + public DateTime MaxDrawdownEndDate { get; } + public decimal HighPrice { get; } + + public DrawdownDrawdownDateHighValueDTO(decimal drawdownPercent, DateTime maxDrawdownEndDate, decimal recoveryThresholdPrice) + { + DrawdownPercent = drawdownPercent; + MaxDrawdownEndDate = maxDrawdownEndDate; + HighPrice = recoveryThresholdPrice; + } + } +} From 3ac7b75c28398dd61eef87ccf54bafe8e9a81b10 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Thu, 22 Aug 2024 18:18:51 -0400 Subject: [PATCH 06/26] Issue 4581: Fix bgu for when lDrawdowns list is empty. --- Common/Statistics/Statistics.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Common/Statistics/Statistics.cs b/Common/Statistics/Statistics.cs index 9930e3f1ba07..3a2a297b5188 100644 --- a/Common/Statistics/Statistics.cs +++ b/Common/Statistics/Statistics.cs @@ -59,12 +59,13 @@ private static DrawdownDrawdownDateHighValueDTO DrawdownPercentDrawdownDateHighV var lPrices = equityOverTime.ToList(); var lDrawdowns = new List(); var high = lPrices[0].Value; + highValueOutsideOfTryBlock = high; foreach (var kvp in lPrices) { if (kvp.Value >= high) high = kvp.Value; var drawdown = (kvp.Value / high) - 1; lDrawdowns.Add(drawdown); - if (drawdown < lDrawdowns.Min()) + if (drawdown <= lDrawdowns.Min()) { maxDrawdownDate = kvp.Key; highValueOutsideOfTryBlock = high; From 2aa999c67ca331b7b3d70abc56ae5048e1de6176 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Thu, 22 Aug 2024 18:20:51 -0400 Subject: [PATCH 07/26] Issue 4581: Change names of tests. Change name of file. --- ...eryTests.cs => MaxDradownRecoveryTests.cs} | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) rename Tests/Common/Statistics/{MaximumRecoveryTests.cs => MaxDradownRecoveryTests.cs} (52%) diff --git a/Tests/Common/Statistics/MaximumRecoveryTests.cs b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs similarity index 52% rename from Tests/Common/Statistics/MaximumRecoveryTests.cs rename to Tests/Common/Statistics/MaxDradownRecoveryTests.cs index 643d2adaad7d..8ab9b7d5cfa3 100644 --- a/Tests/Common/Statistics/MaximumRecoveryTests.cs +++ b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs @@ -9,22 +9,22 @@ namespace QuantConnect.Tests.Common.Statistics { - internal class MaximumRecoveryTests + internal class MaxDradownRecoveryTests { [Test] - public void MaximumRecoveryTests_RepeatedDrawdownsSameLevelButOneLonger_ReturnLongerDrawdown() + public void MaxDradownRecoveryTests_RepeatedDrawdownsSameLevelButOneLonger_ReturnLongerDrawdown() { } [Test] - public void MaximumRecoveryTests_RepeatedDrawdownsSameLevelSameLength_ReturnOneOfThem() + public void MaxDradownRecoveryTests_RepeatedDrawdownsSameLevelSameLength_ReturnOneOfThem() { } [Test] - public void MaximumRecoveryTests_NoRecovery_ReturnZero() + public void MaxDrawdownRecoveryTests_NoRecovery_ReturnZero() { var startDate = DateTime.MinValue; var equityOverTime = new SortedDictionary @@ -34,12 +34,15 @@ public void MaximumRecoveryTests_NoRecovery_ReturnZero() { startDate.AddDays(2), 98 }, { startDate.AddDays(3), 99 } }; - var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaximumRecoveryTime(equityOverTime); - Assert.AreEqual(TimeSpan.Zero, maximumRecoveryTime); + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(0, maximumRecoveryTime); } + /// + /// Tests fake recovery from 99 to 98 to 99. Max drawdown is 100 to 98, so this should have a recovery. + /// [Test] - public void MaximumRecoveryTests_BasicRecoveryWithin_ReturnRecoveryTime() + public void MaxDrawdownRecoveryTests_BasicRecoveryWithin_ReturnRecoveryTime() { var startDate = DateTime.MinValue; var equityOverTime = new SortedDictionary @@ -49,43 +52,43 @@ public void MaximumRecoveryTests_BasicRecoveryWithin_ReturnRecoveryTime() { startDate.AddDays(2), 98 }, { startDate.AddDays(3), 99 } }; - var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaximumRecoveryTime(equityOverTime); - Assert.AreEqual(TimeSpan.FromDays(1), maximumRecoveryTime); + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(0, maximumRecoveryTime); } [Test] - public void MaximumRecoveryTests_BasicRecovery_ReturnRecoveryTime() + public void MaxDradownRecoveryTests_BasicRecovery_ReturnRecoveryTime() { } [Test] - public void MaximumRecoveryTests_FlatPrice_ReturnZero() + public void MaxDradownRecoveryTests_FlatPrice_ReturnZero() { } [Test] - public void MaximumRecoveryTests_SingleDataPoint_ReturnZero() + public void MaxDradownRecoveryTests_SingleDataPoint_ReturnZero() { } [Test] - public void MaximumRecoveryTests_EmptyDataSet_ReturnZero() + public void MaxDradownRecoveryTests_EmptyDataSet_ReturnZero() { } [Test] - public void MaximumRecoveryTests_LongRecoveryIntermediatePeaks_ReturnCorrectRecoveryTime() + public void MaxDradownRecoveryTests_LongRecoveryIntermediatePeaks_ReturnCorrectRecoveryTime() { } [Test] - public void MaximumRecoveryTests_PriceCrashesThroughPreviousHigh_ReturnCorrectRecoveryTime() + public void MaxDradownRecoveryTests_PriceCrashesThroughPreviousHigh_ReturnCorrectRecoveryTime() { } From 5135892361b827b20968e4f10555617590aef9c6 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Thu, 22 Aug 2024 20:32:19 -0400 Subject: [PATCH 08/26] Issue 4581: Make adjustements to flow of adding drawdowns to lDrawdowns. --- Common/Statistics/Statistics.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Common/Statistics/Statistics.cs b/Common/Statistics/Statistics.cs index 3a2a297b5188..7b46ae8dd4e6 100644 --- a/Common/Statistics/Statistics.cs +++ b/Common/Statistics/Statistics.cs @@ -64,12 +64,15 @@ private static DrawdownDrawdownDateHighValueDTO DrawdownPercentDrawdownDateHighV { if (kvp.Value >= high) high = kvp.Value; var drawdown = (kvp.Value / high) - 1; - lDrawdowns.Add(drawdown); - if (drawdown <= lDrawdowns.Min()) + if (lDrawdowns.Any()) { - maxDrawdownDate = kvp.Key; - highValueOutsideOfTryBlock = high; + if (drawdown < lDrawdowns.Min()) + { + maxDrawdownDate = kvp.Key; + highValueOutsideOfTryBlock = high; + } } + lDrawdowns.Add(drawdown); } dd = Math.Round(Math.Abs(lDrawdowns.Min()), rounding); } From 7bfcef95e32ff2642f138547defc44b51cd55807 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Thu, 22 Aug 2024 20:32:42 -0400 Subject: [PATCH 09/26] Issue 4581: Add multiple unit tests. --- .../Statistics/MaxDradownRecoveryTests.cs | 128 +++++++++++++++++- 1 file changed, 127 insertions(+), 1 deletion(-) diff --git a/Tests/Common/Statistics/MaxDradownRecoveryTests.cs b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs index 8ab9b7d5cfa3..2c4c641fc138 100644 --- a/Tests/Common/Statistics/MaxDradownRecoveryTests.cs +++ b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs @@ -42,7 +42,7 @@ public void MaxDrawdownRecoveryTests_NoRecovery_ReturnZero() /// Tests fake recovery from 99 to 98 to 99. Max drawdown is 100 to 98, so this should have a recovery. /// [Test] - public void MaxDrawdownRecoveryTests_BasicRecoveryWithin_ReturnRecoveryTime() + public void MaxDrawdownRecoveryTests_FakeRecovery_ReturnNoRecovery() { var startDate = DateTime.MinValue; var equityOverTime = new SortedDictionary @@ -57,41 +57,167 @@ public void MaxDrawdownRecoveryTests_BasicRecoveryWithin_ReturnRecoveryTime() Assert.AreEqual(0, maximumRecoveryTime); } + [Test] + public void MaxDradownRecoveryTests_LowThenHighThenLowWithoutRecovery_ReturnNoRecovery() + { + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 50 }, + { startDate.AddDays(1), 100 }, + { startDate.AddDays(2), 98 }, + { startDate.AddDays(3), 99 } + }; + + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(0, maximumRecoveryTime); + } + + [Test] + public void MaxDradownRecoveryTests_LowThenHighThenLowWithRecovery_ReturnRecoveryOfTwo() + { + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 50 }, + { startDate.AddDays(1), 100 }, + { startDate.AddDays(2), 98 }, + { startDate.AddDays(3), 99 }, + { startDate.AddDays(4), 100 } + }; + + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(2, maximumRecoveryTime); + } + [Test] public void MaxDradownRecoveryTests_BasicRecovery_ReturnRecoveryTime() { + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 100 }, + { startDate.AddDays(1), 98 }, + { startDate.AddDays(2), 99 }, + { startDate.AddDays(3), 100 } + }; + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(2, maximumRecoveryTime); } [Test] public void MaxDradownRecoveryTests_FlatPrice_ReturnZero() { + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 100 }, + { startDate.AddDays(1), 100 }, + { startDate.AddDays(2), 100 }, + { startDate.AddDays(3), 100 } + }; + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(0, maximumRecoveryTime); } [Test] public void MaxDradownRecoveryTests_SingleDataPoint_ReturnZero() { + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 100 }, + }; + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(0, maximumRecoveryTime); } [Test] public void MaxDradownRecoveryTests_EmptyDataSet_ReturnZero() { + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + }; + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(0, maximumRecoveryTime); } [Test] public void MaxDradownRecoveryTests_LongRecoveryIntermediatePeaks_ReturnCorrectRecoveryTime() { + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 100 }, + { startDate.AddDays(1), 97 }, + { startDate.AddDays(2), 99 }, + { startDate.AddDays(3), 97 }, + { startDate.AddDays(4), 100 }, + }; + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(3, maximumRecoveryTime); } [Test] public void MaxDradownRecoveryTests_PriceCrashesThroughPreviousHigh_ReturnCorrectRecoveryTime() { + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 100 }, + { startDate.AddDays(1), 98 }, + { startDate.AddDays(2), 100 }, + { startDate.AddDays(3), 101 }, + { startDate.AddDays(4), 100 }, // Crashes through previous high here, but crash doesnt exceed max drawdown. + { startDate.AddDays(5), 99 } + }; + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(1, maximumRecoveryTime); } + [Test] + public void MaxDradownRecoveryTests_TwoMaxDrawdownsOneDoesntRecover_ReturnNoRecovery() + { + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 100 }, + { startDate.AddDays(1), 98 }, + { startDate.AddDays(2), 100 }, + { startDate.AddDays(3), 101 }, + { startDate.AddDays(4), 100 }, + { startDate.AddDays(5), 98 } + }; + + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(0, maximumRecoveryTime); + } + + [Test] + public void MaxDradownRecoveryTests__ReturnNoRecovery() + { + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 100 }, + { startDate.AddDays(1), 98 }, + { startDate.AddDays(2), 100 }, + { startDate.AddDays(3), 101 }, + { startDate.AddDays(4), 100 }, + { startDate.AddDays(5), 98 } + }; + + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(0, maximumRecoveryTime); + } + + } } From c0e3d8363b7fcc02b6e48c1ee8a418b1a7f73729 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Fri, 23 Aug 2024 20:52:59 -0400 Subject: [PATCH 10/26] Issue #4581: Change name of unit test --- Tests/Common/Statistics/MaxDradownRecoveryTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Common/Statistics/MaxDradownRecoveryTests.cs b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs index 2c4c641fc138..1577f5d4a669 100644 --- a/Tests/Common/Statistics/MaxDradownRecoveryTests.cs +++ b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs @@ -201,7 +201,7 @@ public void MaxDradownRecoveryTests_TwoMaxDrawdownsOneDoesntRecover_ReturnNoReco } [Test] - public void MaxDradownRecoveryTests__ReturnNoRecovery() + public void MaxDradownRecoveryTests_NewDrawdownHigher_ReturnNoRecovery() { var startDate = DateTime.MinValue; var equityOverTime = new SortedDictionary From ffd3160f8efc263daa7decabec1da57a904da48e Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Fri, 23 Aug 2024 20:57:20 -0400 Subject: [PATCH 11/26] Issue #4581: Add to PerformanceMetrics --- Common/Statistics/PerformanceMetrics.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Common/Statistics/PerformanceMetrics.cs b/Common/Statistics/PerformanceMetrics.cs index 7151700d7665..177d93ae91df 100644 --- a/Common/Statistics/PerformanceMetrics.cs +++ b/Common/Statistics/PerformanceMetrics.cs @@ -156,5 +156,11 @@ public static class PerformanceMetrics /// The average Portfolio Turnover /// public const string PortfolioTurnover = "Portfolio Turnover"; + + /// + /// The average Portfolio Turnover + /// + public const string MaxDrawdownRecovery = "Maximum Drawdown Recovery"; + } } From cc507f0df91f11a3bd36bb0fa0933e97f3ec9297 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Fri, 23 Aug 2024 21:05:58 -0400 Subject: [PATCH 12/26] Issue #4581: Add Maximum Drawdown Recovery to PortolioStatistics class. --- Common/Statistics/PortfolioStatistics.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Common/Statistics/PortfolioStatistics.cs b/Common/Statistics/PortfolioStatistics.cs index ed89def721e5..446c5ee65e1d 100644 --- a/Common/Statistics/PortfolioStatistics.cs +++ b/Common/Statistics/PortfolioStatistics.cs @@ -185,6 +185,13 @@ public class PortfolioStatistics [JsonConverter(typeof(JsonRoundingConverter))] public decimal ValueAtRisk95 { get; set; } + /// + /// The recovery time of the maximum drawdown. + /// + [JsonConverter(typeof(JsonRoundingConverter))] + public decimal MaximumDrawdownRecovery { get; set; } + + /// /// Initializes a new instance of the class /// From 32b3afa66cbaa995f6e83869c9e8cf451287f4e9 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Fri, 23 Aug 2024 21:06:23 -0400 Subject: [PATCH 13/26] Issue #4581: Add to portolfio statistics class. --- Common/Statistics/PortfolioStatistics.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Common/Statistics/PortfolioStatistics.cs b/Common/Statistics/PortfolioStatistics.cs index 446c5ee65e1d..6b046f3a52cb 100644 --- a/Common/Statistics/PortfolioStatistics.cs +++ b/Common/Statistics/PortfolioStatistics.cs @@ -315,6 +315,8 @@ public PortfolioStatistics( ValueAtRisk99 = GetValueAtRisk(listPerformance, tradingDaysPerYear, 0.99d); ValueAtRisk95 = GetValueAtRisk(listPerformance, tradingDaysPerYear, 0.95d); + + MaximumDrawdownRecovery = (decimal)Statistics.MaxDrawdownRecoveryTime(equity, 3); } /// From 6a81574e1fa37f2ffd8a96909c73a2dab40e2314 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Fri, 23 Aug 2024 21:06:50 -0400 Subject: [PATCH 14/26] Issue #4581: Add to statistics builder. --- Common/Statistics/StatisticsBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Common/Statistics/StatisticsBuilder.cs b/Common/Statistics/StatisticsBuilder.cs index 8e2a4ae6f1c2..04ce430644f6 100644 --- a/Common/Statistics/StatisticsBuilder.cs +++ b/Common/Statistics/StatisticsBuilder.cs @@ -240,7 +240,8 @@ private static Dictionary GetSummary(AlgorithmPerformance totalP { PerformanceMetrics.TotalFees, accountCurrencySymbol + totalFees.ToStringInvariant("0.00") }, { PerformanceMetrics.EstimatedStrategyCapacity, accountCurrencySymbol + capacity.RoundToSignificantDigits(2).ToStringInvariant() }, { PerformanceMetrics.LowestCapacityAsset, lowestCapacitySymbol != Symbol.Empty ? lowestCapacitySymbol.ID.ToString() : "" }, - { PerformanceMetrics.PortfolioTurnover, Math.Round(totalPerformance.PortfolioStatistics.PortfolioTurnover.SafeMultiply100(), 2).ToStringInvariant() + "%" } + { PerformanceMetrics.PortfolioTurnover, Math.Round(totalPerformance.PortfolioStatistics.PortfolioTurnover.SafeMultiply100(), 2).ToStringInvariant() + "%" }, + { PerformanceMetrics.MaxDrawdownRecovery, totalPerformance.PortfolioStatistics.MaximumDrawdownRecovery.ToStringInvariant() + " time units."}, // We might have to figure out the time unit the algo runs under. }; } From c0a53aba65d7d865fc60442f511390fbfae540a1 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Fri, 23 Aug 2024 21:12:09 -0400 Subject: [PATCH 15/26] Issue #4581: Add report key. --- Report/ReportKey.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Report/ReportKey.cs b/Report/ReportKey.cs index c46f8483c8dd..d93328b48b75 100644 --- a/Report/ReportKey.cs +++ b/Report/ReportKey.cs @@ -57,5 +57,6 @@ internal static class ReportKey public const string CrisisPlots = @"{{$HTML-CRISIS-PLOTS}}"; public const string CrisisTitle = @"{{$TEXT-CRISIS-TITLE}}"; public const string CrisisContents = @"{{$PLOT-CRISIS-CONTENT}}"; + public const string MaximumDrawdownRecovery = @"{{$PLOT-MAXIMUMDRAWDOWN-RECOVERY}}"; } } From fca85ccf8e923b91e378bdd9f6f41b1450ffbfb7 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Fri, 23 Aug 2024 21:14:16 -0400 Subject: [PATCH 16/26] Case #4581: Convert to decimal. --- Common/Statistics/Statistics.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Common/Statistics/Statistics.cs b/Common/Statistics/Statistics.cs index 7b46ae8dd4e6..53aec6ea1b9f 100644 --- a/Common/Statistics/Statistics.cs +++ b/Common/Statistics/Statistics.cs @@ -314,7 +314,7 @@ public static decimal DrawdownPercent(decimal current, decimal high, int roundin /// Digits to round the result to. /// Recovery time of maximum drawdown in days. - public static double MaxDrawdownRecoveryTime(SortedDictionary equityOverTime, int rounding = 2) + public static decimal MaxDrawdownRecoveryTime(SortedDictionary equityOverTime, int rounding = 2) { var drawdownDrawdownDateHighValueDTO = DrawdownPercentDrawdownDateHighValue(equityOverTime); @@ -337,7 +337,7 @@ public static double MaxDrawdownRecoveryTime(SortedDictionary return 0; // Not yet recovered } - return (recoveryDate - maxDradownEndDate).TotalDays.Round(rounding); + return (decimal)(recoveryDate - maxDradownEndDate).TotalDays.Round(rounding); } } // End of Statistics From 8fddd3bf26af9f94bb91bf43d9366169ddfe9904 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Fri, 23 Aug 2024 21:17:19 -0400 Subject: [PATCH 17/26] Issue #4581: Correct comment. --- Common/Statistics/PerformanceMetrics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Common/Statistics/PerformanceMetrics.cs b/Common/Statistics/PerformanceMetrics.cs index 177d93ae91df..7fe7446d97c6 100644 --- a/Common/Statistics/PerformanceMetrics.cs +++ b/Common/Statistics/PerformanceMetrics.cs @@ -158,7 +158,7 @@ public static class PerformanceMetrics public const string PortfolioTurnover = "Portfolio Turnover"; /// - /// The average Portfolio Turnover + /// The recovery time of the maximum drawdown. /// public const string MaxDrawdownRecovery = "Maximum Drawdown Recovery"; From 95a6c4267e2f821936f1e706a31184a238ce7238 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Fri, 23 Aug 2024 21:20:31 -0400 Subject: [PATCH 18/26] Issue #4581: Correct performance metrics view model string. --- Common/Statistics/StatisticsBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Common/Statistics/StatisticsBuilder.cs b/Common/Statistics/StatisticsBuilder.cs index 04ce430644f6..dfd3ae54fa54 100644 --- a/Common/Statistics/StatisticsBuilder.cs +++ b/Common/Statistics/StatisticsBuilder.cs @@ -241,7 +241,7 @@ private static Dictionary GetSummary(AlgorithmPerformance totalP { PerformanceMetrics.EstimatedStrategyCapacity, accountCurrencySymbol + capacity.RoundToSignificantDigits(2).ToStringInvariant() }, { PerformanceMetrics.LowestCapacityAsset, lowestCapacitySymbol != Symbol.Empty ? lowestCapacitySymbol.ID.ToString() : "" }, { PerformanceMetrics.PortfolioTurnover, Math.Round(totalPerformance.PortfolioStatistics.PortfolioTurnover.SafeMultiply100(), 2).ToStringInvariant() + "%" }, - { PerformanceMetrics.MaxDrawdownRecovery, totalPerformance.PortfolioStatistics.MaximumDrawdownRecovery.ToStringInvariant() + " time units."}, // We might have to figure out the time unit the algo runs under. + { PerformanceMetrics.MaxDrawdownRecovery, totalPerformance.PortfolioStatistics.MaximumDrawdownRecovery.ToStringInvariant() + " day."}, }; } From e2c9d2035fcdfbc525438019fc23cc682a3f3792 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Fri, 23 Aug 2024 21:21:21 -0400 Subject: [PATCH 19/26] Case #4581: Correct statistics builder view model string..again. --- Common/Statistics/StatisticsBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Common/Statistics/StatisticsBuilder.cs b/Common/Statistics/StatisticsBuilder.cs index dfd3ae54fa54..3c49f3f0c1e7 100644 --- a/Common/Statistics/StatisticsBuilder.cs +++ b/Common/Statistics/StatisticsBuilder.cs @@ -241,7 +241,7 @@ private static Dictionary GetSummary(AlgorithmPerformance totalP { PerformanceMetrics.EstimatedStrategyCapacity, accountCurrencySymbol + capacity.RoundToSignificantDigits(2).ToStringInvariant() }, { PerformanceMetrics.LowestCapacityAsset, lowestCapacitySymbol != Symbol.Empty ? lowestCapacitySymbol.ID.ToString() : "" }, { PerformanceMetrics.PortfolioTurnover, Math.Round(totalPerformance.PortfolioStatistics.PortfolioTurnover.SafeMultiply100(), 2).ToStringInvariant() + "%" }, - { PerformanceMetrics.MaxDrawdownRecovery, totalPerformance.PortfolioStatistics.MaximumDrawdownRecovery.ToStringInvariant() + " day."}, + { PerformanceMetrics.MaxDrawdownRecovery, totalPerformance.PortfolioStatistics.MaximumDrawdownRecovery.ToStringInvariant() + " day(s)."}, }; } From a3f0c58841cb8f44fb28e34b161c9c7b2ff02433 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Sat, 24 Aug 2024 11:57:56 -0400 Subject: [PATCH 20/26] Issue #4581: Placed DradownDradownDateHighValueDTO at the end of the file for simpler diff. --- Common/Statistics/Statistics.cs | 81 ++++++++++++++++----------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/Common/Statistics/Statistics.cs b/Common/Statistics/Statistics.cs index 53aec6ea1b9f..d43dae96d01b 100644 --- a/Common/Statistics/Statistics.cs +++ b/Common/Statistics/Statistics.cs @@ -42,47 +42,6 @@ public static decimal DrawdownPercent(SortedDictionary equity return DrawdownPercentDrawdownDateHighValue(equityOverTime, rounding).DrawdownPercent; } - /// - /// Returns Drawdown percentage, the date the drawdown ended and the price value at which the drawdown started. - /// - /// - /// - /// - - private static DrawdownDrawdownDateHighValueDTO DrawdownPercentDrawdownDateHighValue(SortedDictionary equityOverTime, int rounding = 2) - { - var dd = 0m; - var maxDrawdownDate = DateTime.MinValue; - var highValueOutsideOfTryBlock = 0m; - try - { - var lPrices = equityOverTime.ToList(); - var lDrawdowns = new List(); - var high = lPrices[0].Value; - highValueOutsideOfTryBlock = high; - foreach (var kvp in lPrices) - { - if (kvp.Value >= high) high = kvp.Value; - var drawdown = (kvp.Value / high) - 1; - if (lDrawdowns.Any()) - { - if (drawdown < lDrawdowns.Min()) - { - maxDrawdownDate = kvp.Key; - highValueOutsideOfTryBlock = high; - } - } - lDrawdowns.Add(drawdown); - } - dd = Math.Round(Math.Abs(lDrawdowns.Min()), rounding); - } - catch (Exception err) - { - Log.Error(err); - } - return new DrawdownDrawdownDateHighValueDTO(dd, maxDrawdownDate, highValueOutsideOfTryBlock); - } - /// /// Annual compounded returns statistic based on the final-starting capital and years. /// @@ -306,6 +265,46 @@ public static decimal DrawdownPercent(decimal current, decimal high, int roundin return Math.Round(drawdownPercentage, roundingDecimals); } + /// + /// Returns Drawdown percentage, the date the drawdown ended and the price value at which the drawdown started. + /// + /// + /// + /// + private static DrawdownDrawdownDateHighValueDTO DrawdownPercentDrawdownDateHighValue(SortedDictionary equityOverTime, int rounding = 2) + { + var dd = 0m; + var maxDrawdownDate = DateTime.MinValue; + var highValueOutsideOfTryBlock = 0m; + try + { + var lPrices = equityOverTime.ToList(); + var lDrawdowns = new List(); + var high = lPrices[0].Value; + highValueOutsideOfTryBlock = high; + foreach (var timePricePair in lPrices) + { + if (timePricePair.Value >= high) high = timePricePair.Value; + var drawdown = (timePricePair.Value / high) - 1; + if (lDrawdowns.Any()) + { + if (drawdown < lDrawdowns.Min()) + { + maxDrawdownDate = timePricePair.Key; + highValueOutsideOfTryBlock = high; + } + } + lDrawdowns.Add(drawdown); + } + dd = Math.Round(Math.Abs(lDrawdowns.Min()), rounding); + } + catch (Exception err) + { + Log.Error(err); + } + return new DrawdownDrawdownDateHighValueDTO(dd, maxDrawdownDate, highValueOutsideOfTryBlock); + } + /// /// Calculates the recovery time of the maximum drawdown in days. /// From cb7f28fce337186a1625f4a144e252571ad2b90c Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Sat, 24 Aug 2024 12:02:50 -0400 Subject: [PATCH 21/26] Issue #4581: Add 2 new tests. --- .../Statistics/MaxDradownRecoveryTests.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/Tests/Common/Statistics/MaxDradownRecoveryTests.cs b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs index 1577f5d4a669..fab743e327c3 100644 --- a/Tests/Common/Statistics/MaxDradownRecoveryTests.cs +++ b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs @@ -14,13 +14,34 @@ internal class MaxDradownRecoveryTests [Test] public void MaxDradownRecoveryTests_RepeatedDrawdownsSameLevelButOneLonger_ReturnLongerDrawdown() { - + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 100 }, + { startDate.AddDays(1), 90 }, + { startDate.AddDays(2), 100 }, + { startDate.AddDays(3), 90 }, + { startDate.AddDays(3), 99 }, + { startDate.AddDays(2), 100 }, + }; + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(2, maximumRecoveryTime); } [Test] public void MaxDradownRecoveryTests_RepeatedDrawdownsSameLevelSameLength_ReturnOneOfThem() { - + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 100 }, + { startDate.AddDays(1), 90 }, + { startDate.AddDays(2), 100 }, + { startDate.AddDays(3), 90 }, + { startDate.AddDays(2), 100 }, + }; + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(1, maximumRecoveryTime); } [Test] From 2b0bb2adcc4800c411b947777c9878cdc2f58701 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Sat, 24 Aug 2024 12:34:29 -0400 Subject: [PATCH 22/26] Issue #4581: Change algorithm so that when multiple maximum drawdowns occur, the longest of all recoveries is reported. --- .../DrawdownDrawdownDateHighValueDTO.cs | 6 +- Common/Statistics/Statistics.cs | 69 +++++++++++-------- .../Statistics/MaxDradownRecoveryTests.cs | 6 +- 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/Common/Statistics/DrawdownDrawdownDateHighValueDTO.cs b/Common/Statistics/DrawdownDrawdownDateHighValueDTO.cs index b5fe94574756..12b1b9e1b3b0 100644 --- a/Common/Statistics/DrawdownDrawdownDateHighValueDTO.cs +++ b/Common/Statistics/DrawdownDrawdownDateHighValueDTO.cs @@ -9,13 +9,13 @@ namespace QuantConnect.Statistics internal class DrawdownDrawdownDateHighValueDTO { public decimal DrawdownPercent { get; } - public DateTime MaxDrawdownEndDate { get; } + public List MaxDrawdownEndDates { get; } public decimal HighPrice { get; } - public DrawdownDrawdownDateHighValueDTO(decimal drawdownPercent, DateTime maxDrawdownEndDate, decimal recoveryThresholdPrice) + public DrawdownDrawdownDateHighValueDTO(decimal drawdownPercent, List maxDrawdownEndDates, decimal recoveryThresholdPrice) { DrawdownPercent = drawdownPercent; - MaxDrawdownEndDate = maxDrawdownEndDate; + MaxDrawdownEndDates = maxDrawdownEndDates; HighPrice = recoveryThresholdPrice; } } diff --git a/Common/Statistics/Statistics.cs b/Common/Statistics/Statistics.cs index d43dae96d01b..27a9b65c3e03 100644 --- a/Common/Statistics/Statistics.cs +++ b/Common/Statistics/Statistics.cs @@ -39,7 +39,7 @@ public class Statistics /// public static decimal DrawdownPercent(SortedDictionary equityOverTime, int rounding = 2) { - return DrawdownPercentDrawdownDateHighValue(equityOverTime, rounding).DrawdownPercent; + return DrawdownPercentDrawdownDatesHighValue(equityOverTime, rounding).DrawdownPercent; } /// @@ -266,77 +266,86 @@ public static decimal DrawdownPercent(decimal current, decimal high, int roundin } /// - /// Returns Drawdown percentage, the date the drawdown ended and the price value at which the drawdown started. + /// Returns Drawdown percentage, the dates the drawdown ended and the price value at which the drawdowns started. /// /// /// /// - private static DrawdownDrawdownDateHighValueDTO DrawdownPercentDrawdownDateHighValue(SortedDictionary equityOverTime, int rounding = 2) + private static DrawdownDrawdownDateHighValueDTO DrawdownPercentDrawdownDatesHighValue(SortedDictionary equityOverTime, int rounding = 2) { var dd = 0m; - var maxDrawdownDate = DateTime.MinValue; - var highValueOutsideOfTryBlock = 0m; + var maxDrawdownDates = new List(); + var highValue = 0m; try { var lPrices = equityOverTime.ToList(); var lDrawdowns = new List(); var high = lPrices[0].Value; - highValueOutsideOfTryBlock = high; + decimal maxDrawdown = 0; + foreach (var timePricePair in lPrices) { if (timePricePair.Value >= high) high = timePricePair.Value; var drawdown = (timePricePair.Value / high) - 1; - if (lDrawdowns.Any()) + + if (drawdown < maxDrawdown) { - if (drawdown < lDrawdowns.Min()) - { - maxDrawdownDate = timePricePair.Key; - highValueOutsideOfTryBlock = high; - } + maxDrawdown = drawdown; + maxDrawdownDates.Clear(); + maxDrawdownDates.Add(timePricePair.Key); + highValue = high; } + else if (drawdown == maxDrawdown && maxDrawdownDates.Count > 0) + { + maxDrawdownDates.Add(timePricePair.Key); + } + lDrawdowns.Add(drawdown); } - dd = Math.Round(Math.Abs(lDrawdowns.Min()), rounding); + dd = Math.Round(Math.Abs(maxDrawdown), rounding); } catch (Exception err) { Log.Error(err); } - return new DrawdownDrawdownDateHighValueDTO(dd, maxDrawdownDate, highValueOutsideOfTryBlock); + return new DrawdownDrawdownDateHighValueDTO(dd, maxDrawdownDates, highValue); } /// - /// Calculates the recovery time of the maximum drawdown in days. + /// Calculates the recovery time of the maximum drawdown in days. If there are multiple maximum drawdown, it picks the longer drawdown to report. /// /// Price Data /// Latest maximum /// Digits to round the result to. /// Recovery time of maximum drawdown in days. - public static decimal MaxDrawdownRecoveryTime(SortedDictionary equityOverTime, int rounding = 2) { - var drawdownDrawdownDateHighValueDTO = DrawdownPercentDrawdownDateHighValue(equityOverTime); - - var recoveryThresholdPrice = drawdownDrawdownDateHighValueDTO.HighPrice; - var maxDradownEndDate = drawdownDrawdownDateHighValueDTO.MaxDrawdownEndDate; + var drawdownInfo = DrawdownPercentDrawdownDatesHighValue(equityOverTime); - if (maxDradownEndDate == DateTime.MinValue) + if (drawdownInfo.MaxDrawdownEndDates.Count == 0) { - return 0; // No drawdown occurred. Do we want a special return value for this? + return 0; // No drawdown occurred } - var recoveryDate = equityOverTime - .Where(kvp => kvp.Key > maxDradownEndDate && kvp.Value >= recoveryThresholdPrice) - .Select(kvp => kvp.Key) - .DefaultIfEmpty(DateTime.MaxValue) - .First(); + var recoveryThresholdPrice = drawdownInfo.HighPrice; + decimal longestRecoveryTime = 0; - if (recoveryDate == DateTime.MaxValue) + foreach (var maxDrawdownDate in drawdownInfo.MaxDrawdownEndDates) { - return 0; // Not yet recovered + var recoveryDate = equityOverTime + .Where(kvp => kvp.Key > maxDrawdownDate && kvp.Value >= recoveryThresholdPrice) + .Select(kvp => kvp.Key) + .DefaultIfEmpty(DateTime.MaxValue) + .Min(); + + if (recoveryDate != DateTime.MaxValue) + { + var recoveryTime = (decimal)(recoveryDate - maxDrawdownDate).TotalDays; + longestRecoveryTime = Math.Max(longestRecoveryTime, recoveryTime); + } } - return (decimal)(recoveryDate - maxDradownEndDate).TotalDays.Round(rounding); + return Math.Round(longestRecoveryTime, rounding); } } // End of Statistics diff --git a/Tests/Common/Statistics/MaxDradownRecoveryTests.cs b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs index fab743e327c3..bc01428d1fb5 100644 --- a/Tests/Common/Statistics/MaxDradownRecoveryTests.cs +++ b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs @@ -21,8 +21,8 @@ public void MaxDradownRecoveryTests_RepeatedDrawdownsSameLevelButOneLonger_Retur { startDate.AddDays(1), 90 }, { startDate.AddDays(2), 100 }, { startDate.AddDays(3), 90 }, - { startDate.AddDays(3), 99 }, - { startDate.AddDays(2), 100 }, + { startDate.AddDays(4), 99 }, + { startDate.AddDays(5), 100 }, }; var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); Assert.AreEqual(2, maximumRecoveryTime); @@ -38,7 +38,7 @@ public void MaxDradownRecoveryTests_RepeatedDrawdownsSameLevelSameLength_ReturnO { startDate.AddDays(1), 90 }, { startDate.AddDays(2), 100 }, { startDate.AddDays(3), 90 }, - { startDate.AddDays(2), 100 }, + { startDate.AddDays(4), 100 }, }; var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); Assert.AreEqual(1, maximumRecoveryTime); From 49ac507bd52063c330b7ff4e7ae9f5132d8057eb Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Sat, 24 Aug 2024 12:39:28 -0400 Subject: [PATCH 23/26] Issue #4581: Add unit test. --- .../Statistics/MaxDradownRecoveryTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Tests/Common/Statistics/MaxDradownRecoveryTests.cs b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs index bc01428d1fb5..4fda4dfaea77 100644 --- a/Tests/Common/Statistics/MaxDradownRecoveryTests.cs +++ b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs @@ -28,6 +28,23 @@ public void MaxDradownRecoveryTests_RepeatedDrawdownsSameLevelButOneLonger_Retur Assert.AreEqual(2, maximumRecoveryTime); } + [Test] + public void MaxDradownRecoveryTests_RepeatedDrawdownsSameLevelButOneLongerLongerFirst_ReturnLongerDrawdown() + { + var startDate = DateTime.MinValue; + var equityOverTime = new SortedDictionary + { + { startDate, 100 }, + { startDate.AddDays(1), 90 }, + { startDate.AddDays(4), 99 }, + { startDate.AddDays(2), 100 }, + { startDate.AddDays(3), 90 }, + { startDate.AddDays(5), 100 }, + }; + var maximumRecoveryTime = QuantConnect.Statistics.Statistics.MaxDrawdownRecoveryTime(equityOverTime); + Assert.AreEqual(2, maximumRecoveryTime); + } + [Test] public void MaxDradownRecoveryTests_RepeatedDrawdownsSameLevelSameLength_ReturnOneOfThem() { From 845bc8cdcc638a577a3332b405545c105145ae74 Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Sat, 24 Aug 2024 12:52:19 -0400 Subject: [PATCH 24/26] Issue #4581: Remove reportkey. Change dto name. --- ...eDTO.cs => DrawdownPercentageDrawdownDatesHighValueDTO.cs} | 4 ++-- Common/Statistics/Statistics.cs | 4 ++-- Report/ReportKey.cs | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) rename Common/Statistics/{DrawdownDrawdownDateHighValueDTO.cs => DrawdownPercentageDrawdownDatesHighValueDTO.cs} (69%) diff --git a/Common/Statistics/DrawdownDrawdownDateHighValueDTO.cs b/Common/Statistics/DrawdownPercentageDrawdownDatesHighValueDTO.cs similarity index 69% rename from Common/Statistics/DrawdownDrawdownDateHighValueDTO.cs rename to Common/Statistics/DrawdownPercentageDrawdownDatesHighValueDTO.cs index 12b1b9e1b3b0..b5695b19bf3f 100644 --- a/Common/Statistics/DrawdownDrawdownDateHighValueDTO.cs +++ b/Common/Statistics/DrawdownPercentageDrawdownDatesHighValueDTO.cs @@ -6,13 +6,13 @@ namespace QuantConnect.Statistics { - internal class DrawdownDrawdownDateHighValueDTO + internal class DrawdownPercentageDrawdownDatesHighValueDTO { public decimal DrawdownPercent { get; } public List MaxDrawdownEndDates { get; } public decimal HighPrice { get; } - public DrawdownDrawdownDateHighValueDTO(decimal drawdownPercent, List maxDrawdownEndDates, decimal recoveryThresholdPrice) + public DrawdownPercentageDrawdownDatesHighValueDTO(decimal drawdownPercent, List maxDrawdownEndDates, decimal recoveryThresholdPrice) { DrawdownPercent = drawdownPercent; MaxDrawdownEndDates = maxDrawdownEndDates; diff --git a/Common/Statistics/Statistics.cs b/Common/Statistics/Statistics.cs index 27a9b65c3e03..964905fb19e2 100644 --- a/Common/Statistics/Statistics.cs +++ b/Common/Statistics/Statistics.cs @@ -271,7 +271,7 @@ public static decimal DrawdownPercent(decimal current, decimal high, int roundin /// /// /// - private static DrawdownDrawdownDateHighValueDTO DrawdownPercentDrawdownDatesHighValue(SortedDictionary equityOverTime, int rounding = 2) + private static DrawdownPercentageDrawdownDatesHighValueDTO DrawdownPercentDrawdownDatesHighValue(SortedDictionary equityOverTime, int rounding = 2) { var dd = 0m; var maxDrawdownDates = new List(); @@ -308,7 +308,7 @@ private static DrawdownDrawdownDateHighValueDTO DrawdownPercentDrawdownDatesHigh { Log.Error(err); } - return new DrawdownDrawdownDateHighValueDTO(dd, maxDrawdownDates, highValue); + return new DrawdownPercentageDrawdownDatesHighValueDTO(dd, maxDrawdownDates, highValue); } /// diff --git a/Report/ReportKey.cs b/Report/ReportKey.cs index d93328b48b75..c46f8483c8dd 100644 --- a/Report/ReportKey.cs +++ b/Report/ReportKey.cs @@ -57,6 +57,5 @@ internal static class ReportKey public const string CrisisPlots = @"{{$HTML-CRISIS-PLOTS}}"; public const string CrisisTitle = @"{{$TEXT-CRISIS-TITLE}}"; public const string CrisisContents = @"{{$PLOT-CRISIS-CONTENT}}"; - public const string MaximumDrawdownRecovery = @"{{$PLOT-MAXIMUMDRAWDOWN-RECOVERY}}"; } } From 83fbce0c468b84e8f8d16233215127d2dde9b94a Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Sat, 24 Aug 2024 12:59:27 -0400 Subject: [PATCH 25/26] Issue #4581: Change summary. --- Common/Statistics/Statistics.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Common/Statistics/Statistics.cs b/Common/Statistics/Statistics.cs index 964905fb19e2..4fa1a944d4ff 100644 --- a/Common/Statistics/Statistics.cs +++ b/Common/Statistics/Statistics.cs @@ -315,8 +315,7 @@ private static DrawdownPercentageDrawdownDatesHighValueDTO DrawdownPercentDrawdo /// Calculates the recovery time of the maximum drawdown in days. If there are multiple maximum drawdown, it picks the longer drawdown to report. /// /// Price Data - /// Latest maximum - /// Digits to round the result to. + /// Amount of decimals to round the result to. /// Recovery time of maximum drawdown in days. public static decimal MaxDrawdownRecoveryTime(SortedDictionary equityOverTime, int rounding = 2) { From 854c5b17f5447a74a2e1f1139aa07e16d21aafdd Mon Sep 17 00:00:00 2001 From: Alain Schaerer Date: Sat, 24 Aug 2024 13:11:33 -0400 Subject: [PATCH 26/26] Issue #4581: Change comment. --- Tests/Common/Statistics/MaxDradownRecoveryTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Common/Statistics/MaxDradownRecoveryTests.cs b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs index 4fda4dfaea77..c3af18cef8f5 100644 --- a/Tests/Common/Statistics/MaxDradownRecoveryTests.cs +++ b/Tests/Common/Statistics/MaxDradownRecoveryTests.cs @@ -77,7 +77,7 @@ public void MaxDrawdownRecoveryTests_NoRecovery_ReturnZero() } /// - /// Tests fake recovery from 99 to 98 to 99. Max drawdown is 100 to 98, so this should have a recovery. + /// Tests fake recovery from 99 to 98 to 99. Max drawdown is 100 to 98, so this should have no recovery. /// [Test] public void MaxDrawdownRecoveryTests_FakeRecovery_ReturnNoRecovery()