Lean  $LEAN_TAG$
StatisticsBuilder.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.Collections.Generic;
18 using System.Linq;
19 using System.Runtime.CompilerServices;
20 using QuantConnect.Data;
22 using QuantConnect.Util;
23 
25 {
26  /// <summary>
27  /// The <see cref="StatisticsBuilder"/> class creates summary and rolling statistics from trades, equity and benchmark points
28  /// </summary>
29  public static class StatisticsBuilder
30  {
31  /// <summary>
32  /// Generates the statistics and returns the results
33  /// </summary>
34  /// <param name="trades">The list of closed trades</param>
35  /// <param name="profitLoss">Trade record of profits and losses</param>
36  /// <param name="pointsEquity">The list of daily equity values</param>
37  /// <param name="pointsPerformance">The list of algorithm performance values</param>
38  /// <param name="pointsBenchmark">The list of benchmark values</param>
39  /// <param name="pointsPortfolioTurnover">The list of portfolio turnover daily samples</param>
40  /// <param name="startingCapital">The algorithm starting capital</param>
41  /// <param name="totalFees">The total fees</param>
42  /// <param name="totalOrders">The total number of transactions</param>
43  /// <param name="estimatedStrategyCapacity">The estimated capacity of this strategy</param>
44  /// <param name="accountCurrencySymbol">The account currency symbol</param>
45  /// <param name="transactions">
46  /// The transaction manager to get number of winning and losing transactions
47  /// </param>
48  /// <param name="riskFreeInterestRateModel">The risk free interest rate model to use</param>
49  /// <param name="tradingDaysPerYear">The number of trading days per year</param>
50  /// <returns>Returns a <see cref="StatisticsResults"/> object</returns>
51  public static StatisticsResults Generate(
52  List<Trade> trades,
53  SortedDictionary<DateTime, decimal> profitLoss,
54  List<ISeriesPoint> pointsEquity,
55  List<ISeriesPoint> pointsPerformance,
56  List<ISeriesPoint> pointsBenchmark,
57  List<ISeriesPoint> pointsPortfolioTurnover,
58  decimal startingCapital,
59  decimal totalFees,
60  int totalOrders,
61  CapacityEstimate estimatedStrategyCapacity,
62  string accountCurrencySymbol,
63  SecurityTransactionManager transactions,
64  IRiskFreeInterestRateModel riskFreeInterestRateModel,
65  int tradingDaysPerYear)
66  {
67  var equity = ChartPointToDictionary(pointsEquity);
68 
69  var firstDate = equity.Keys.FirstOrDefault().Date;
70  var lastDate = equity.Keys.LastOrDefault().Date;
71 
72  var totalPerformance = GetAlgorithmPerformance(firstDate, lastDate, trades, profitLoss, equity, pointsPerformance, pointsBenchmark,
73  pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel, tradingDaysPerYear);
74  var rollingPerformances = GetRollingPerformances(firstDate, lastDate, trades, profitLoss, equity, pointsPerformance, pointsBenchmark,
75  pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel, tradingDaysPerYear);
76  var summary = GetSummary(totalPerformance, estimatedStrategyCapacity, totalFees, totalOrders, accountCurrencySymbol);
77 
78  return new StatisticsResults(totalPerformance, rollingPerformances, summary);
79  }
80 
81  /// <summary>
82  /// Returns the performance of the algorithm in the specified date range
83  /// </summary>
84  /// <param name="fromDate">The initial date of the range</param>
85  /// <param name="toDate">The final date of the range</param>
86  /// <param name="trades">The list of closed trades</param>
87  /// <param name="profitLoss">Trade record of profits and losses</param>
88  /// <param name="equity">The list of daily equity values</param>
89  /// <param name="pointsPerformance">The list of algorithm performance values</param>
90  /// <param name="pointsBenchmark">The list of benchmark values</param>
91  /// <param name="pointsPortfolioTurnover">The list of portfolio turnover daily samples</param>
92  /// <param name="startingCapital">The algorithm starting capital</param>
93  /// <param name="transactions">
94  /// The transaction manager to get number of winning and losing transactions
95  /// </param>
96  /// <param name="riskFreeInterestRateModel">The risk free interest rate model to use</param>
97  /// <param name="tradingDaysPerYear">The number of trading days per year</param>
98  /// <returns>The algorithm performance</returns>
99  private static AlgorithmPerformance GetAlgorithmPerformance(
100  DateTime fromDate,
101  DateTime toDate,
102  List<Trade> trades,
103  SortedDictionary<DateTime, decimal> profitLoss,
104  SortedDictionary<DateTime, decimal> equity,
105  List<ISeriesPoint> pointsPerformance,
106  List<ISeriesPoint> pointsBenchmark,
107  List<ISeriesPoint> pointsPortfolioTurnover,
108  decimal startingCapital,
109  SecurityTransactionManager transactions,
110  IRiskFreeInterestRateModel riskFreeInterestRateModel,
111  int tradingDaysPerYear)
112  {
113  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));
114 
115  // No portfolio equity for the period means that there is no performance to be computed
116  if (periodEquity.IsNullOrEmpty())
117  {
118  return new AlgorithmPerformance();
119  }
120 
121  var periodTrades = trades.Where(x => x.ExitTime.Date >= fromDate && x.ExitTime < toDate.AddDays(1)).ToList();
122  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));
123  var periodWinCount = transactions.WinningTransactions.Count(x => x.Key >= fromDate && x.Key.Date < toDate.AddDays(1));
124  var periodLossCount = transactions.LosingTransactions.Count(x => x.Key >= fromDate && x.Key.Date < toDate.AddDays(1));
125 
126  // Convert our charts to dictionaries
127  // NOTE: Day 0 refers to sample taken at 12AM on StartDate, performance[0] always = 0, benchmark[0] is benchmark value preceding start date.
128  var benchmark = ChartPointToDictionary(pointsBenchmark, fromDate, toDate);
129  var performance = ChartPointToDictionary(pointsPerformance, fromDate, toDate);
130  var portfolioTurnover = ChartPointToDictionary(pointsPortfolioTurnover, fromDate, toDate);
131 
132  // Ensure our series are aligned
133  if (benchmark.Count != performance.Count)
134  {
135  throw new ArgumentException($"Benchmark and performance series has {Math.Abs(benchmark.Count - performance.Count)} misaligned values.");
136  }
137 
138  // Convert our benchmark values into a percentage daily performance of the benchmark, this will shorten the series by one since
139  // its the percentage change between each entry (No day 0 sample)
140  var benchmarkEnumerable = CreateBenchmarkDifferences(benchmark, fromDate, toDate);
141 
142  var listBenchmark = benchmarkEnumerable.Select(x => x.Value).ToList();
143  var listPerformance = PreprocessPerformanceValues(performance).Select(x => x.Value).ToList();
144 
145  var runningCapital = equity.Count == periodEquity.Count ? startingCapital : periodEquity.Values.FirstOrDefault();
146 
147  return new AlgorithmPerformance(periodTrades, periodProfitLoss, periodEquity, portfolioTurnover, listPerformance, listBenchmark,
148  runningCapital, periodWinCount, periodLossCount, riskFreeInterestRateModel, tradingDaysPerYear);
149  }
150 
151  /// <summary>
152  /// Returns the rolling performances of the algorithm
153  /// </summary>
154  /// <param name="firstDate">The first date of the total period</param>
155  /// <param name="lastDate">The last date of the total period</param>
156  /// <param name="trades">The list of closed trades</param>
157  /// <param name="profitLoss">Trade record of profits and losses</param>
158  /// <param name="equity">The list of daily equity values</param>
159  /// <param name="pointsPerformance">The list of algorithm performance values</param>
160  /// <param name="pointsBenchmark">The list of benchmark values</param>
161  /// <param name="pointsPortfolioTurnover">The list of portfolio turnover daily samples</param>
162  /// <param name="startingCapital">The algorithm starting capital</param>
163  /// <param name="transactions">
164  /// The transaction manager to get number of winning and losing transactions
165  /// </param>
166  /// <param name="riskFreeInterestRateModel">The risk free interest rate model to use</param>
167  /// <param name="tradingDaysPerYear">The number of trading days per year</param>
168  /// <returns>A dictionary with the rolling performances</returns>
169  private static Dictionary<string, AlgorithmPerformance> GetRollingPerformances(
170  DateTime firstDate,
171  DateTime lastDate,
172  List<Trade> trades,
173  SortedDictionary<DateTime, decimal> profitLoss,
174  SortedDictionary<DateTime, decimal> equity,
175  List<ISeriesPoint> pointsPerformance,
176  List<ISeriesPoint> pointsBenchmark,
177  List<ISeriesPoint> pointsPortfolioTurnover,
178  decimal startingCapital,
179  SecurityTransactionManager transactions,
180  IRiskFreeInterestRateModel riskFreeInterestRateModel,
181  int tradingDaysPerYear)
182  {
183  var rollingPerformances = new Dictionary<string, AlgorithmPerformance>();
184 
185  var monthPeriods = new[] { 1, 3, 6, 12 };
186  foreach (var monthPeriod in monthPeriods)
187  {
188  var ranges = GetPeriodRanges(monthPeriod, firstDate, lastDate);
189 
190  foreach (var period in ranges)
191  {
192  var key = $"M{monthPeriod}_{period.EndDate.ToStringInvariant("yyyyMMdd")}";
193  var periodPerformance = GetAlgorithmPerformance(period.StartDate, period.EndDate, trades, profitLoss, equity, pointsPerformance,
194  pointsBenchmark, pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel, tradingDaysPerYear);
195  rollingPerformances[key] = periodPerformance;
196  }
197  }
198 
199  return rollingPerformances;
200  }
201 
202  /// <summary>
203  /// Returns a summary of the algorithm performance as a dictionary
204  /// </summary>
205  private static Dictionary<string, string> GetSummary(AlgorithmPerformance totalPerformance, CapacityEstimate estimatedStrategyCapacity,
206  decimal totalFees, int totalOrders, string accountCurrencySymbol)
207  {
208  var capacity = 0m;
209  var lowestCapacitySymbol = Symbol.Empty;
210  if (estimatedStrategyCapacity != null)
211  {
212  capacity = estimatedStrategyCapacity.Capacity;
213  lowestCapacitySymbol = estimatedStrategyCapacity.LowestCapacityAsset ?? Symbol.Empty;
214  }
215 
216  return new Dictionary<string, string>
217  {
218  { PerformanceMetrics.TotalOrders, totalOrders.ToStringInvariant() },
219  { PerformanceMetrics.AverageWin, Math.Round(totalPerformance.PortfolioStatistics.AverageWinRate.SafeMultiply100(), 2).ToStringInvariant() + "%" },
220  { PerformanceMetrics.AverageLoss, Math.Round(totalPerformance.PortfolioStatistics.AverageLossRate.SafeMultiply100(), 2).ToStringInvariant() + "%" },
221  { PerformanceMetrics.CompoundingAnnualReturn, Math.Round(totalPerformance.PortfolioStatistics.CompoundingAnnualReturn.SafeMultiply100(), 3).ToStringInvariant() + "%" },
222  { PerformanceMetrics.Drawdown, Math.Round(totalPerformance.PortfolioStatistics.Drawdown.SafeMultiply100(), 3).ToStringInvariant() + "%" },
223  { PerformanceMetrics.Expectancy, Math.Round(totalPerformance.PortfolioStatistics.Expectancy, 3).ToStringInvariant() },
224  { PerformanceMetrics.StartEquity, Math.Round(totalPerformance.PortfolioStatistics.StartEquity, 2).ToStringInvariant() },
225  { PerformanceMetrics.EndEquity, Math.Round(totalPerformance.PortfolioStatistics.EndEquity, 2).ToStringInvariant() },
226  { PerformanceMetrics.NetProfit, Math.Round(totalPerformance.PortfolioStatistics.TotalNetProfit.SafeMultiply100(), 3).ToStringInvariant() + "%"},
227  { PerformanceMetrics.SharpeRatio, Math.Round((double)totalPerformance.PortfolioStatistics.SharpeRatio, 3).ToStringInvariant() },
228  { PerformanceMetrics.SortinoRatio, Math.Round((double)totalPerformance.PortfolioStatistics.SortinoRatio, 3).ToStringInvariant() },
229  { PerformanceMetrics.ProbabilisticSharpeRatio, Math.Round(totalPerformance.PortfolioStatistics.ProbabilisticSharpeRatio.SafeMultiply100(), 3).ToStringInvariant() + "%"},
230  { PerformanceMetrics.LossRate, Math.Round(totalPerformance.PortfolioStatistics.LossRate.SafeMultiply100()).ToStringInvariant() + "%" },
231  { PerformanceMetrics.WinRate, Math.Round(totalPerformance.PortfolioStatistics.WinRate.SafeMultiply100()).ToStringInvariant() + "%" },
232  { PerformanceMetrics.ProfitLossRatio, Math.Round(totalPerformance.PortfolioStatistics.ProfitLossRatio, 2).ToStringInvariant() },
233  { PerformanceMetrics.Alpha, Math.Round((double)totalPerformance.PortfolioStatistics.Alpha, 3).ToStringInvariant() },
234  { PerformanceMetrics.Beta, Math.Round((double)totalPerformance.PortfolioStatistics.Beta, 3).ToStringInvariant() },
235  { PerformanceMetrics.AnnualStandardDeviation, Math.Round((double)totalPerformance.PortfolioStatistics.AnnualStandardDeviation, 3).ToStringInvariant() },
236  { PerformanceMetrics.AnnualVariance, Math.Round((double)totalPerformance.PortfolioStatistics.AnnualVariance, 3).ToStringInvariant() },
237  { PerformanceMetrics.InformationRatio, Math.Round((double)totalPerformance.PortfolioStatistics.InformationRatio, 3).ToStringInvariant() },
238  { PerformanceMetrics.TrackingError, Math.Round((double)totalPerformance.PortfolioStatistics.TrackingError, 3).ToStringInvariant() },
239  { PerformanceMetrics.TreynorRatio, Math.Round((double)totalPerformance.PortfolioStatistics.TreynorRatio, 3).ToStringInvariant() },
240  { PerformanceMetrics.TotalFees, accountCurrencySymbol + totalFees.ToStringInvariant("0.00") },
241  { PerformanceMetrics.EstimatedStrategyCapacity, accountCurrencySymbol + capacity.RoundToSignificantDigits(2).ToStringInvariant() },
242  { PerformanceMetrics.LowestCapacityAsset, lowestCapacitySymbol != Symbol.Empty ? lowestCapacitySymbol.ID.ToString() : "" },
243  { PerformanceMetrics.PortfolioTurnover, Math.Round(totalPerformance.PortfolioStatistics.PortfolioTurnover.SafeMultiply100(), 2).ToStringInvariant() + "%" }
244  };
245  }
246 
247  /// <summary>
248  /// Helper class for rolling statistics
249  /// </summary>
250  private class PeriodRange
251  {
252  internal DateTime StartDate { get; set; }
253  internal DateTime EndDate { get; set; }
254  }
255 
256  /// <summary>
257  /// Gets a list of date ranges for the requested monthly period
258  /// </summary>
259  /// <remarks>The first and last ranges created are partial periods</remarks>
260  /// <param name="periodMonths">The number of months in the period (valid inputs are [1, 3, 6, 12])</param>
261  /// <param name="firstDate">The first date of the total period</param>
262  /// <param name="lastDate">The last date of the total period</param>
263  /// <returns>The list of date ranges</returns>
264  private static IEnumerable<PeriodRange> GetPeriodRanges(int periodMonths, DateTime firstDate, DateTime lastDate)
265  {
266  // get end dates
267  var date = lastDate.Date;
268  var endDates = new List<DateTime>();
269  do
270  {
271  endDates.Add(date);
272  date = new DateTime(date.Year, date.Month, 1).AddDays(-1);
273  } while (date >= firstDate);
274 
275  // build period ranges
276  var ranges = new List<PeriodRange> { new PeriodRange { StartDate = firstDate, EndDate = endDates[endDates.Count - 1] } };
277  for (var i = endDates.Count - 2; i >= 0; i--)
278  {
279  var startDate = ranges[ranges.Count - 1].EndDate.AddDays(1).AddMonths(1 - periodMonths);
280  if (startDate < firstDate) startDate = firstDate;
281 
282  ranges.Add(new PeriodRange
283  {
284  StartDate = startDate,
285  EndDate = endDates[i]
286  });
287  }
288 
289  return ranges;
290  }
291 
292  /// <summary>
293  /// Convert the charting data into an equity array.
294  /// </summary>
295  /// <remarks>This is required to convert the equity plot into a usable form for the statistics calculation</remarks>
296  /// <param name="points">ChartPoints Array</param>
297  /// <param name="fromDate">An optional starting date</param>
298  /// <param name="toDate">An optional ending date</param>
299  /// <returns>SortedDictionary of the equity decimal values ordered in time</returns>
300  private static SortedDictionary<DateTime, decimal> ChartPointToDictionary(IEnumerable<ISeriesPoint> points, DateTime? fromDate = null, DateTime? toDate = null)
301  {
302  var dictionary = new SortedDictionary<DateTime, decimal>();
303 
304  foreach (var point in points)
305  {
306  if (fromDate != null && point.Time.Date < fromDate) continue;
307  if (toDate != null && point.Time.Date >= ((DateTime)toDate).AddDays(1)) break;
308 
309  dictionary[point.Time] = GetPointValue(point);
310  }
311 
312  return dictionary;
313  }
314 
315  /// <summary>
316  /// Gets the value of a point, either ChartPoint.y or Candlestick.Close
317  /// </summary>
318  [MethodImpl(MethodImplOptions.AggressiveInlining)]
319  private static decimal GetPointValue(ISeriesPoint point)
320  {
321  if (point is ChartPoint)
322  {
323  return ((ChartPoint)point).y.Value;
324  }
325 
326  return ((Candlestick)point).Close.Value;
327  }
328 
329  /// <summary>
330  /// Yields pairs of date and percentage change for the period
331  /// </summary>
332  /// <param name="points">The values to calculate percentage change for</param>
333  /// <param name="fromDate">Starting date (inclusive)</param>
334  /// <param name="toDate">Ending date (inclusive)</param>
335  /// <returns>Pairs of date and percentage change</returns>
336  public static IEnumerable<KeyValuePair<DateTime, double>> CreateBenchmarkDifferences(IEnumerable<KeyValuePair<DateTime, decimal>> points, DateTime fromDate, DateTime toDate)
337  {
338  DateTime dtPrevious = default;
339  var previous = 0m;
340  var firstValueSkipped = false;
341  double deltaPercentage;
342 
343  // Get points performance array for the given period:
344  foreach (var kvp in points.Where(kvp => kvp.Key >= fromDate.Date && kvp.Key.Date <= toDate))
345  {
346  var dt = kvp.Key;
347  var value = kvp.Value;
348 
349  if (dtPrevious != default)
350  {
351  deltaPercentage = 0;
352  if (previous != 0)
353  {
354  deltaPercentage = (double)((value - previous) / previous);
355  }
356 
357  // We will skip past day 1 of performance values to deal with the OnOpen orders causing misalignment between benchmark and
358  // algorithm performance. So we drop the first value of listBenchmark (Day 1), and drop two values from performance (Day 0, Day 1)
359  if (firstValueSkipped)
360  {
361  yield return new KeyValuePair<DateTime, double>(dt, deltaPercentage);
362  }
363  else
364  {
365  firstValueSkipped = true;
366  }
367  }
368 
369  dtPrevious = dt;
370  previous = value;
371  }
372  }
373 
374  /// <summary>
375  /// Skips the first two entries from the given points and divides each entry by 100
376  /// </summary>
377  /// <param name="points">The values to divide by 100</param>
378  /// <returns>Pairs of date and performance value divided by 100</returns>
379  public static IEnumerable<KeyValuePair<DateTime, double>> PreprocessPerformanceValues(IEnumerable<KeyValuePair<DateTime, decimal>> points)
380  {
381  // We will skip past day 1 of performance values to deal with the OnOpen orders causing misalignment between benchmark and
382  // algorithm performance. So we drop two values from performance (Day 0, Day 1)
383  foreach (var kvp in points.Skip(2))
384  {
385  yield return new KeyValuePair<DateTime, double>(kvp.Key, (double)(kvp.Value / 100));
386  }
387  }
388  }
389 }