Skip to content

Commit 495cd45

Browse files
committed
Fix drawdown calculation and revert unnecesary changes
1 parent e122c59 commit 495cd45

7 files changed

Lines changed: 45 additions & 31 deletions

File tree

Common/Statistics/AlgorithmPerformance.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public class AlgorithmPerformance
4444
/// </summary>
4545
/// <param name="trades">The list of closed trades</param>
4646
/// <param name="profitLoss">Trade record of profits and losses</param>
47-
/// <param name="equityPoints">The equity curve series points</param>
47+
/// <param name="equity">The list of daily equity values</param>
48+
/// <param name="equityPoints">The equity curve OHLC series points, used for drawdown calculation</param>
4849
/// <param name="portfolioTurnover">The algorithm portfolio turnover</param>
4950
/// <param name="listPerformance">The list of algorithm performance values</param>
5051
/// <param name="listBenchmark">The list of benchmark values</param>
@@ -56,6 +57,7 @@ public class AlgorithmPerformance
5657
public AlgorithmPerformance(
5758
List<Trade> trades,
5859
SortedDictionary<DateTime, decimal> profitLoss,
60+
SortedDictionary<DateTime, decimal> equity,
5961
List<ISeriesPoint> equityPoints,
6062
SortedDictionary<DateTime, decimal> portfolioTurnover,
6163
List<double> listPerformance,
@@ -67,7 +69,7 @@ public AlgorithmPerformance(
6769
int tradingDaysPerYear)
6870
{
6971
TradeStatistics = new TradeStatistics(trades);
70-
PortfolioStatistics = new PortfolioStatistics(profitLoss, equityPoints, portfolioTurnover, listPerformance, listBenchmark, startingCapital,
72+
PortfolioStatistics = new PortfolioStatistics(profitLoss, equity, equityPoints, portfolioTurnover, listPerformance, listBenchmark, startingCapital,
7173
riskFreeInterestRateModel, tradingDaysPerYear, winningTransactions, losingTransactions);
7274
ClosedTrades = trades;
7375
}

Common/Statistics/PortfolioStatistics.cs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,8 @@ public class PortfolioStatistics
196196
/// Initializes a new instance of the <see cref="PortfolioStatistics"/> class
197197
/// </summary>
198198
/// <param name="profitLoss">Trade record of profits and losses</param>
199-
/// <param name="equityPoints">The equity curve series points</param>
199+
/// <param name="equity">The list of daily equity values</param>
200+
/// <param name="equityPoints">The equity curve OHLC series points, used for drawdown calculation</param>
200201
/// <param name="portfolioTurnover">The algorithm portfolio turnover</param>
201202
/// <param name="listPerformance">The list of algorithm performance values</param>
202203
/// <param name="listBenchmark">The list of benchmark values</param>
@@ -210,6 +211,7 @@ public class PortfolioStatistics
210211
/// <param name="lossCount">The number of losses</param>
211212
public PortfolioStatistics(
212213
SortedDictionary<DateTime, decimal> profitLoss,
214+
SortedDictionary<DateTime, decimal> equity,
213215
List<ISeriesPoint> equityPoints,
214216
SortedDictionary<DateTime, decimal> portfolioTurnover,
215217
List<double> listPerformance,
@@ -221,7 +223,7 @@ public PortfolioStatistics(
221223
int? lossCount = null)
222224
{
223225
StartEquity = startingCapital;
224-
EndEquity = Statistics.GetClose(equityPoints.LastOrDefault());
226+
EndEquity = equity.LastOrDefault().Value;
225227

226228
if (portfolioTurnover.Count > 0)
227229
{
@@ -278,24 +280,19 @@ public PortfolioStatistics(
278280

279281
if (startingCapital != 0)
280282
{
281-
TotalNetProfit = EndEquity / startingCapital - 1;
283+
TotalNetProfit = equity.Values.LastOrDefault() / startingCapital - 1;
282284
}
283285

284-
if (equityPoints.Count >= 2)
285-
{
286-
var lastTime = equityPoints.Last().Time;
287-
var firstTime = equityPoints.First().Time;
288-
var fractionOfYears = (decimal)(lastTime - firstTime).TotalDays / 365;
289-
CompoundingAnnualReturn = Statistics.CompoundingAnnualPerformance(startingCapital, EndEquity, fractionOfYears);
290-
}
286+
var fractionOfYears = (decimal)(equity.Keys.LastOrDefault() - equity.Keys.FirstOrDefault()).TotalDays / 365;
287+
CompoundingAnnualReturn = Statistics.CompoundingAnnualPerformance(startingCapital, equity.Values.LastOrDefault(), fractionOfYears);
291288

292289
AnnualVariance = Statistics.AnnualVariance(listPerformance, tradingDaysPerYear).SafeDecimalCast();
293290
AnnualStandardDeviation = (decimal)Math.Sqrt((double)AnnualVariance);
294291

295292
var benchmarkAnnualPerformance = GetAnnualPerformance(listBenchmark, tradingDaysPerYear);
296293
var annualPerformance = GetAnnualPerformance(listPerformance, tradingDaysPerYear);
297294

298-
var riskFreeRate = riskFreeInterestRateModel.GetAverageRiskFreeRate(equityPoints.Select(x => x.Time));
295+
var riskFreeRate = riskFreeInterestRateModel.GetAverageRiskFreeRate(equity.Select(x => x.Key));
299296
SharpeRatio = AnnualStandardDeviation == 0 ? 0 : Statistics.SharpeRatio(annualPerformance, AnnualStandardDeviation, riskFreeRate);
300297

301298
var annualDownsideDeviation = Statistics.AnnualDownsideStandardDeviation(listPerformance, tradingDaysPerYear).SafeDecimalCast();

Common/Statistics/Statistics.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,9 @@ public static DrawdownMetrics CalculateDrawdownMetrics(List<ISeriesPoint> equity
277277
var point = equityPoints[i];
278278
GetHighLow(point, out var high, out var low);
279279

280+
// Use the previous peak to calculate the drawdown
281+
var previousPeak = peakEquity;
282+
280283
// Update peak equity using the high price
281284
if (high >= peakEquity)
282285
{
@@ -291,8 +294,8 @@ public static DrawdownMetrics CalculateDrawdownMetrics(List<ISeriesPoint> equity
291294
peakDate = point.Time;
292295
}
293296

294-
// Calculate current drawdown from peak using the low price
295-
var currentDrawdown = (low / peakEquity) - 1;
297+
// Calculate current drawdown from previous peak using the low price
298+
var currentDrawdown = (low / previousPeak) - 1;
296299
if (currentDrawdown < 0)
297300
{
298301
maxDrawdown = Math.Min(maxDrawdown, currentDrawdown);

Common/Statistics/StatisticsBuilder.cs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,14 @@ public static StatisticsResults Generate(
6464
IRiskFreeInterestRateModel riskFreeInterestRateModel,
6565
int tradingDaysPerYear)
6666
{
67-
var firstDate = equityPoints.Count > 0 ? equityPoints.First().Time.Date : default;
68-
var lastDate = equityPoints.Count > 0 ? equityPoints.Last().Time.Date : default;
67+
var equity = ChartPointToDictionary(equityPoints);
6968

70-
var totalPerformance = GetAlgorithmPerformance(firstDate, lastDate, trades, profitLoss, equityPoints, pointsPerformance, pointsBenchmark,
69+
var firstDate = equity.Keys.FirstOrDefault().Date;
70+
var lastDate = equity.Keys.LastOrDefault().Date;
71+
72+
var totalPerformance = GetAlgorithmPerformance(firstDate, lastDate, trades, profitLoss, equity, equityPoints, pointsPerformance, pointsBenchmark,
7173
pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel, tradingDaysPerYear);
72-
var rollingPerformances = GetRollingPerformances(firstDate, lastDate, trades, profitLoss, equityPoints, pointsPerformance, pointsBenchmark,
74+
var rollingPerformances = GetRollingPerformances(firstDate, lastDate, trades, profitLoss, equity, equityPoints, pointsPerformance, pointsBenchmark,
7375
pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel, tradingDaysPerYear);
7476
var summary = GetSummary(totalPerformance, estimatedStrategyCapacity, totalFees, totalOrders, accountCurrencySymbol);
7577

@@ -83,7 +85,8 @@ public static StatisticsResults Generate(
8385
/// <param name="toDate">The final date of the range</param>
8486
/// <param name="trades">The list of closed trades</param>
8587
/// <param name="profitLoss">Trade record of profits and losses</param>
86-
/// <param name="equityPoints">The equity curve series points</param>
88+
/// <param name="equity">The list of daily equity values</param>
89+
/// <param name="equityPoints">The equity curve OHLC series points, used for drawdown calculation</param>
8790
/// <param name="pointsPerformance">The list of algorithm performance values</param>
8891
/// <param name="pointsBenchmark">The list of benchmark values</param>
8992
/// <param name="pointsPortfolioTurnover">The list of portfolio turnover daily samples</param>
@@ -99,6 +102,7 @@ private static AlgorithmPerformance GetAlgorithmPerformance(
99102
DateTime toDate,
100103
List<Trade> trades,
101104
SortedDictionary<DateTime, decimal> profitLoss,
105+
SortedDictionary<DateTime, decimal> equity,
102106
List<ISeriesPoint> equityPoints,
103107
List<ISeriesPoint> pointsPerformance,
104108
List<ISeriesPoint> pointsBenchmark,
@@ -108,13 +112,15 @@ private static AlgorithmPerformance GetAlgorithmPerformance(
108112
IRiskFreeInterestRateModel riskFreeInterestRateModel,
109113
int tradingDaysPerYear)
110114
{
115+
var periodEquity = new SortedDictionary<DateTime, decimal>(equity.Where(x => x.Key.Date >= fromDate && x.Key.Date < toDate.AddDays(1)).ToDictionary(x => x.Key, y => y.Value));
111116
var periodEquityPoints = equityPoints?.Where(x => x.Time.Date >= fromDate && x.Time.Date < toDate.AddDays(1)).ToList();
112117

113118
// No portfolio equity for the period means that there is no performance to be computed
114-
if (periodEquityPoints.IsNullOrEmpty())
119+
if (periodEquity.IsNullOrEmpty())
115120
{
116121
return new AlgorithmPerformance();
117122
}
123+
118124
var periodTrades = trades.Where(x => x.ExitTime.Date >= fromDate && x.ExitTime < toDate.AddDays(1)).ToList();
119125
var periodProfitLoss = new SortedDictionary<DateTime, decimal>(profitLoss.Where(x => x.Key >= fromDate && x.Key.Date < toDate.AddDays(1)).ToDictionary(x => x.Key, y => y.Value));
120126
var periodWinCount = transactions.WinningTransactions.Count(x => x.Key >= fromDate && x.Key.Date < toDate.AddDays(1));
@@ -139,9 +145,9 @@ private static AlgorithmPerformance GetAlgorithmPerformance(
139145
var listBenchmark = benchmarkEnumerable.Select(x => x.Value).ToList();
140146
var listPerformance = PreprocessPerformanceValues(performance).Select(x => x.Value).ToList();
141147

142-
var runningCapital = equityPoints.Count == periodEquityPoints.Count ? startingCapital : Statistics.GetClose(periodEquityPoints.First());
148+
var runningCapital = equity.Count == periodEquity.Count ? startingCapital : periodEquity.Values.FirstOrDefault();
143149

144-
return new AlgorithmPerformance(periodTrades, periodProfitLoss, periodEquityPoints, portfolioTurnover, listPerformance, listBenchmark,
150+
return new AlgorithmPerformance(periodTrades, periodProfitLoss, periodEquity, periodEquityPoints, portfolioTurnover, listPerformance, listBenchmark,
145151
runningCapital, periodWinCount, periodLossCount, riskFreeInterestRateModel, tradingDaysPerYear);
146152
}
147153

@@ -152,7 +158,8 @@ private static AlgorithmPerformance GetAlgorithmPerformance(
152158
/// <param name="lastDate">The last date of the total period</param>
153159
/// <param name="trades">The list of closed trades</param>
154160
/// <param name="profitLoss">Trade record of profits and losses</param>
155-
/// <param name="equityPoints">The equity curve series points</param>
161+
/// <param name="equity">The list of daily equity values</param>
162+
/// <param name="equityPoints">The equity curve OHLC series points, used for drawdown calculation</param>
156163
/// <param name="pointsPerformance">The list of algorithm performance values</param>
157164
/// <param name="pointsBenchmark">The list of benchmark values</param>
158165
/// <param name="pointsPortfolioTurnover">The list of portfolio turnover daily samples</param>
@@ -168,6 +175,7 @@ private static Dictionary<string, AlgorithmPerformance> GetRollingPerformances(
168175
DateTime lastDate,
169176
List<Trade> trades,
170177
SortedDictionary<DateTime, decimal> profitLoss,
178+
SortedDictionary<DateTime, decimal> equity,
171179
List<ISeriesPoint> equityPoints,
172180
List<ISeriesPoint> pointsPerformance,
173181
List<ISeriesPoint> pointsBenchmark,
@@ -187,7 +195,7 @@ private static Dictionary<string, AlgorithmPerformance> GetRollingPerformances(
187195
foreach (var period in ranges)
188196
{
189197
var key = $"M{monthPeriod}_{period.EndDate.ToStringInvariant("yyyyMMdd")}";
190-
var periodPerformance = GetAlgorithmPerformance(period.StartDate, period.EndDate, trades, profitLoss, equityPoints, pointsPerformance,
198+
var periodPerformance = GetAlgorithmPerformance(period.StartDate, period.EndDate, trades, profitLoss, equity, equityPoints, pointsPerformance,
191199
pointsBenchmark, pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel, tradingDaysPerYear);
192200
rollingPerformances[key] = periodPerformance;
193201
}

Research/QuantBook.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,8 @@ public PyDict GetPortfolioStatistics(PyObject dataFrame)
821821
}
822822

823823
// Convert the double into decimal
824-
var equityPoints = dictEquity.Select(kvp => (ISeriesPoint)new ChartPoint(kvp.Key, (decimal)kvp.Value)).ToList();
824+
var equity = new SortedDictionary<DateTime, decimal>(dictEquity.ToDictionary(kvp => kvp.Key, kvp => (decimal)kvp.Value));
825+
var equityPoints = equity.Select(kvp => (ISeriesPoint)new ChartPoint(kvp.Key, kvp.Value)).ToList();
825826
var profitLoss = new SortedDictionary<DateTime, decimal>(dictPL.ToDictionary(kvp => kvp.Key, kvp => double.IsNaN(kvp.Value) ? 0 : (decimal)kvp.Value));
826827

827828
// Gets the last value of the day of the benchmark and equity
@@ -835,7 +836,7 @@ public PyDict GetPortfolioStatistics(PyObject dataFrame)
835836
BaseSetupHandler.SetBrokerageTradingDayPerYear(algorithm: this);
836837

837838
// Compute portfolio statistics
838-
var stats = new PortfolioStatistics(profitLoss, equityPoints, new(), listPerformance, listBenchmark, startingCapital, RiskFreeInterestRateModel,
839+
var stats = new PortfolioStatistics(profitLoss, equity, equityPoints, new(), listPerformance, listBenchmark, startingCapital, RiskFreeInterestRateModel,
839840
Settings.TradingDaysPerYear.Value);
840841

841842
result.SetItem("Average Win (%)", Convert.ToDouble(stats.AverageWinRate * 100).ToPython());

Tests/Common/Statistics/DrawdownRecoveryTests.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ public void DrawdownMetricsMaximumRecoveryTimeTests(List<decimal> data, decimal
4040
[Test]
4141
public void CandlestickUsesHighForPeakAndLowForDrawdown()
4242
{
43-
// Day 2 High=105 sets the peak: Low=80 -> drawdown = (80/105) - 1 --> 0.24
43+
// Day 1: peak = 100
44+
// Day 2: High=105 updates peak, but drawdown is measured against the previous peak
45+
// not the same bar High, because we don't know which came first.
46+
// drawdown = (80 / 100) - 1 = -0.2
4447
var startDate = new DateTime(2025, 1, 1);
4548

4649
var points = new List<ISeriesPoint>
@@ -52,7 +55,7 @@ public void CandlestickUsesHighForPeakAndLowForDrawdown()
5255

5356
var metrics = QuantConnect.Statistics.Statistics.CalculateDrawdownMetrics(points);
5457

55-
Assert.AreEqual(0.24m, metrics.Drawdown);
58+
Assert.AreEqual(0.2m, metrics.Drawdown);
5659
}
5760

5861
[Test]

Tests/Common/Statistics/PortfolioStatisticsTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public void PortfolioStatisticsDoesNotFailWhenAnnualPerformanceIsLarge()
148148
var riskFreeInterestRateModel = new InterestRateProvider();
149149
var tradingDaysPerYear = 252;
150150

151-
Assert.DoesNotThrow(() => new PortfolioStatistics(profitLoss, new List<ISeriesPoint>(), portfolioTurnover, listPerformance, listBenchmark, startingCapital, riskFreeInterestRateModel, tradingDaysPerYear));
151+
Assert.DoesNotThrow(() => new PortfolioStatistics(profitLoss, new SortedDictionary<DateTime, decimal>(), new List<ISeriesPoint>(), portfolioTurnover, listPerformance, listBenchmark, startingCapital, riskFreeInterestRateModel, tradingDaysPerYear));
152152
}
153153

154154
/// <summary>
@@ -165,7 +165,7 @@ private PortfolioStatistics GetPortfolioStatistics(bool win, int tradingDaysPerY
165165
var profitLoss = new SortedDictionary<DateTime, decimal>(trades.ToDictionary(x => x.ExitTime, x => x.ProfitLoss));
166166
var winCount = trades.Count(x => x.IsWin);
167167
var lossCount = trades.Count - winCount;
168-
return new PortfolioStatistics(profitLoss, new List<ISeriesPoint>(),
168+
return new PortfolioStatistics(profitLoss, new SortedDictionary<DateTime, decimal>(), new List<ISeriesPoint>(),
169169
new SortedDictionary<DateTime, decimal>(), listPerformance, listBenchmark, 100000,
170170
new InterestRateProvider(), tradingDaysPerYear, winCount, lossCount);
171171
}

0 commit comments

Comments
 (0)