Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature issue#4581 Maximum Drawdown Recovery Time. #8280

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e036eac
Implement a prototype of the maximum recovery time function.
swisstackle Aug 14, 2024
7c4cbc3
Add unit test skeletons.
swisstackle Aug 14, 2024
93590a0
Add failing test
swisstackle Aug 14, 2024
6bd5cee
Issue #4581: Implement MaxDrawdownRecoveryTime.
swisstackle Aug 21, 2024
83270c0
Issue 4581: Add DTO for Drawdown Percentage, Drawdown Enddate, and Hi…
swisstackle Aug 22, 2024
3ac7b75
Issue 4581: Fix bgu for when lDrawdowns list is empty.
swisstackle Aug 22, 2024
2aa999c
Issue 4581: Change names of tests. Change name of file.
swisstackle Aug 22, 2024
5135892
Issue 4581: Make adjustements to flow of adding drawdowns to lDrawdowns.
swisstackle Aug 23, 2024
7bfcef9
Issue 4581: Add multiple unit tests.
swisstackle Aug 23, 2024
c0e3d83
Issue #4581: Change name of unit test
swisstackle Aug 24, 2024
ffd3160
Issue #4581: Add to PerformanceMetrics
swisstackle Aug 24, 2024
cc507f0
Issue #4581: Add Maximum Drawdown Recovery to PortolioStatistics class.
swisstackle Aug 24, 2024
32b3afa
Issue #4581: Add to portolfio statistics class.
swisstackle Aug 24, 2024
6a81574
Issue #4581: Add to statistics builder.
swisstackle Aug 24, 2024
c0a53ab
Issue #4581: Add report key.
swisstackle Aug 24, 2024
fca85cc
Case #4581: Convert to decimal.
swisstackle Aug 24, 2024
8fddd3b
Issue #4581: Correct comment.
swisstackle Aug 24, 2024
95a6c42
Issue #4581: Correct performance metrics view model string.
swisstackle Aug 24, 2024
e2c9d20
Case #4581: Correct statistics builder view model string..again.
swisstackle Aug 24, 2024
a3f0c58
Issue #4581: Placed DradownDradownDateHighValueDTO at the end of the …
swisstackle Aug 24, 2024
cb7f28f
Issue #4581: Add 2 new tests.
swisstackle Aug 24, 2024
2b0bb2a
Issue #4581: Change algorithm so that when multiple maximum drawdowns…
swisstackle Aug 24, 2024
49ac507
Issue #4581: Add unit test.
swisstackle Aug 24, 2024
845bc8c
Issue #4581: Remove reportkey. Change dto name.
swisstackle Aug 24, 2024
83fbce0
Issue #4581: Change summary.
swisstackle Aug 24, 2024
854c5b1
Issue #4581: Change comment.
swisstackle Aug 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Common/Statistics/DrawdownPercentageDrawdownDatesHighValueDTO.cs
Original file line number Diff line number Diff line change
@@ -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 DrawdownPercentageDrawdownDatesHighValueDTO
{
public decimal DrawdownPercent { get; }
public List<DateTime> MaxDrawdownEndDates { get; }
public decimal HighPrice { get; }

public DrawdownPercentageDrawdownDatesHighValueDTO(decimal drawdownPercent, List<DateTime> maxDrawdownEndDates, decimal recoveryThresholdPrice)
{
DrawdownPercent = drawdownPercent;
MaxDrawdownEndDates = maxDrawdownEndDates;
HighPrice = recoveryThresholdPrice;
}
}
}
6 changes: 6 additions & 0 deletions Common/Statistics/PerformanceMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,11 @@ public static class PerformanceMetrics
/// The average Portfolio Turnover
/// </summary>
public const string PortfolioTurnover = "Portfolio Turnover";

/// <summary>
/// The recovery time of the maximum drawdown.
/// </summary>
public const string MaxDrawdownRecovery = "Maximum Drawdown Recovery";

}
}
9 changes: 9 additions & 0 deletions Common/Statistics/PortfolioStatistics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ public class PortfolioStatistics
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ValueAtRisk95 { get; set; }

/// <summary>
/// The recovery time of the maximum drawdown.
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal MaximumDrawdownRecovery { get; set; }


/// <summary>
/// Initializes a new instance of the <see cref="PortfolioStatistics"/> class
/// </summary>
Expand Down Expand Up @@ -308,6 +315,8 @@ public PortfolioStatistics(

ValueAtRisk99 = GetValueAtRisk(listPerformance, tradingDaysPerYear, 0.99d);
ValueAtRisk95 = GetValueAtRisk(listPerformance, tradingDaysPerYear, 0.95d);

MaximumDrawdownRecovery = (decimal)Statistics.MaxDrawdownRecoveryTime(equity, 3);
}

/// <summary>
Expand Down
102 changes: 84 additions & 18 deletions Common/Statistics/Statistics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,24 +39,7 @@ public class Statistics
/// <returns></returns>
public static decimal DrawdownPercent(SortedDictionary<DateTime, decimal> equityOverTime, int rounding = 2)
{
var dd = 0m;
try
{
var lPrices = equityOverTime.Values.ToList();
var lDrawdowns = new List<decimal>();
var high = lPrices[0];
foreach (var price in lPrices)
{
if (price >= high) high = price;
lDrawdowns.Add((price/high) - 1);
}
dd = Math.Round(Math.Abs(lDrawdowns.Min()), rounding);
}
catch (Exception err)
{
Log.Error(err);
}
return dd;
return DrawdownPercentDrawdownDatesHighValue(equityOverTime, rounding).DrawdownPercent;
}

/// <summary>
Expand Down Expand Up @@ -281,6 +265,88 @@ public static decimal DrawdownPercent(decimal current, decimal high, int roundin
return Math.Round(drawdownPercentage, roundingDecimals);
}

/// <summary>
/// Returns Drawdown percentage, the dates the drawdown ended and the price value at which the drawdowns started.
/// </summary>
/// <param name="equityOverTime"></param>
/// <param name="rounding"></param>
/// <returns></returns>
private static DrawdownPercentageDrawdownDatesHighValueDTO DrawdownPercentDrawdownDatesHighValue(SortedDictionary<DateTime, decimal> equityOverTime, int rounding = 2)
{
var dd = 0m;
var maxDrawdownDates = new List<DateTime>();
var highValue = 0m;
try
{
var lPrices = equityOverTime.ToList();
var lDrawdowns = new List<decimal>();
var high = lPrices[0].Value;
decimal maxDrawdown = 0;

foreach (var timePricePair in lPrices)
{
if (timePricePair.Value >= high) high = timePricePair.Value;
var drawdown = (timePricePair.Value / high) - 1;

if (drawdown < maxDrawdown)
{
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(maxDrawdown), rounding);
}
catch (Exception err)
{
Log.Error(err);
}
return new DrawdownPercentageDrawdownDatesHighValueDTO(dd, maxDrawdownDates, highValue);
}

/// <summary>
/// Calculates the recovery time of the maximum drawdown in days. If there are multiple maximum drawdown, it picks the longer drawdown to report.
/// </summary>
/// <param name="equityOverTime">Price Data</param>
/// <param name="rounding">Amount of decimals to round the result to.</param>
/// <returns>Recovery time of maximum drawdown in days.</returns>
public static decimal MaxDrawdownRecoveryTime(SortedDictionary<DateTime, decimal> equityOverTime, int rounding = 2)
{
var drawdownInfo = DrawdownPercentDrawdownDatesHighValue(equityOverTime);

if (drawdownInfo.MaxDrawdownEndDates.Count == 0)
{
return 0; // No drawdown occurred
}

var recoveryThresholdPrice = drawdownInfo.HighPrice;
decimal longestRecoveryTime = 0;

foreach (var maxDrawdownDate in drawdownInfo.MaxDrawdownEndDates)
{
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 Math.Round(longestRecoveryTime, rounding);
}

} // End of Statistics

} // End of Namespace
3 changes: 2 additions & 1 deletion Common/Statistics/StatisticsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,8 @@ private static Dictionary<string, string> 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() + " day(s)."},
};
}

Expand Down
Loading