Lean  $LEAN_TAG$
PortfolioStatistics.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 MathNet.Numerics.Distributions;
20 using MathNet.Numerics.Statistics;
21 using Newtonsoft.Json;
22 using QuantConnect.Data;
23 using QuantConnect.Util;
24 
26 {
27  /// <summary>
28  /// The <see cref="PortfolioStatistics"/> class represents a set of statistics calculated from equity and benchmark samples
29  /// </summary>
30  public class PortfolioStatistics
31  {
32  /// <summary>
33  /// The average rate of return for winning trades
34  /// </summary>
35  [JsonConverter(typeof(JsonRoundingConverter))]
36  public decimal AverageWinRate { get; set; }
37 
38  /// <summary>
39  /// The average rate of return for losing trades
40  /// </summary>
41  [JsonConverter(typeof(JsonRoundingConverter))]
42  public decimal AverageLossRate { get; set; }
43 
44  /// <summary>
45  /// The ratio of the average win rate to the average loss rate
46  /// </summary>
47  /// <remarks>If the average loss rate is zero, ProfitLossRatio is set to 0</remarks>
48  [JsonConverter(typeof(JsonRoundingConverter))]
49  public decimal ProfitLossRatio { get; set; }
50 
51  /// <summary>
52  /// The ratio of the number of winning trades to the total number of trades
53  /// </summary>
54  /// <remarks>If the total number of trades is zero, WinRate is set to zero</remarks>
55  [JsonConverter(typeof(JsonRoundingConverter))]
56  public decimal WinRate { get; set; }
57 
58  /// <summary>
59  /// The ratio of the number of losing trades to the total number of trades
60  /// </summary>
61  /// <remarks>If the total number of trades is zero, LossRate is set to zero</remarks>
62  [JsonConverter(typeof(JsonRoundingConverter))]
63  public decimal LossRate { get; set; }
64 
65  /// <summary>
66  /// The expected value of the rate of return
67  /// </summary>
68  [JsonConverter(typeof(JsonRoundingConverter))]
69  public decimal Expectancy { get; set; }
70 
71  /// <summary>
72  /// Initial Equity Total Value
73  /// </summary>
74  [JsonConverter(typeof(JsonRoundingConverter))]
75  public decimal StartEquity { get; set; }
76 
77  /// <summary>
78  /// Final Equity Total Value
79  /// </summary>
80  [JsonConverter(typeof(JsonRoundingConverter))]
81  public decimal EndEquity { get; set; }
82 
83  /// <summary>
84  /// Annual compounded returns statistic based on the final-starting capital and years.
85  /// </summary>
86  /// <remarks>Also known as Compound Annual Growth Rate (CAGR)</remarks>
87  [JsonConverter(typeof(JsonRoundingConverter))]
88  public decimal CompoundingAnnualReturn { get; set; }
89 
90  /// <summary>
91  /// Drawdown maximum percentage.
92  /// </summary>
93  [JsonConverter(typeof(JsonRoundingConverter))]
94  public decimal Drawdown { get; set; }
95 
96  /// <summary>
97  /// The total net profit percentage.
98  /// </summary>
99  [JsonConverter(typeof(JsonRoundingConverter))]
100  public decimal TotalNetProfit { get; set; }
101 
102  /// <summary>
103  /// Sharpe ratio with respect to risk free rate: measures excess of return per unit of risk.
104  /// </summary>
105  /// <remarks>With risk defined as the algorithm's volatility</remarks>
106  [JsonConverter(typeof(JsonRoundingConverter))]
107  public decimal SharpeRatio { get; set; }
108 
109  /// <summary>
110  /// Probabilistic Sharpe Ratio is a probability measure associated with the Sharpe ratio.
111  /// It informs us of the probability that the estimated Sharpe ratio is greater than a chosen benchmark
112  /// </summary>
113  /// <remarks>See https://www.quantconnect.com/forum/discussion/6483/probabilistic-sharpe-ratio/p1</remarks>
114  [JsonConverter(typeof(JsonRoundingConverter))]
115  public decimal ProbabilisticSharpeRatio { get; set; }
116 
117  /// <summary>
118  /// Sortino ratio with respect to risk free rate: measures excess of return per unit of downside risk.
119  /// </summary>
120  /// <remarks>With risk defined as the algorithm's volatility</remarks>
121  [JsonConverter(typeof(JsonRoundingConverter))]
122  public decimal SortinoRatio { get; set; }
123 
124  /// <summary>
125  /// Algorithm "Alpha" statistic - abnormal returns over the risk free rate and the relationshio (beta) with the benchmark returns.
126  /// </summary>
127  [JsonConverter(typeof(JsonRoundingConverter))]
128  public decimal Alpha { get; set; }
129 
130  /// <summary>
131  /// Algorithm "beta" statistic - the covariance between the algorithm and benchmark performance, divided by benchmark's variance
132  /// </summary>
133  [JsonConverter(typeof(JsonRoundingConverter))]
134  public decimal Beta { get; set; }
135 
136  /// <summary>
137  /// Annualized standard deviation
138  /// </summary>
139  [JsonConverter(typeof(JsonRoundingConverter))]
140  public decimal AnnualStandardDeviation { get; set; }
141 
142  /// <summary>
143  /// Annualized variance statistic calculation using the daily performance variance and trading days per year.
144  /// </summary>
145  [JsonConverter(typeof(JsonRoundingConverter))]
146  public decimal AnnualVariance { get; set; }
147 
148  /// <summary>
149  /// Information ratio - risk adjusted return
150  /// </summary>
151  /// <remarks>(risk = tracking error volatility, a volatility measures that considers the volatility of both algo and benchmark)</remarks>
152  [JsonConverter(typeof(JsonRoundingConverter))]
153  public decimal InformationRatio { get; set; }
154 
155  /// <summary>
156  /// Tracking error volatility (TEV) statistic - a measure of how closely a portfolio follows the index to which it is benchmarked
157  /// </summary>
158  /// <remarks>If algo = benchmark, TEV = 0</remarks>
159  [JsonConverter(typeof(JsonRoundingConverter))]
160  public decimal TrackingError { get; set; }
161 
162  /// <summary>
163  /// Treynor ratio statistic is a measurement of the returns earned in excess of that which could have been earned on an investment that has no diversifiable risk
164  /// </summary>
165  [JsonConverter(typeof(JsonRoundingConverter))]
166  public decimal TreynorRatio { get; set; }
167 
168  /// <summary>
169  /// The average Portfolio Turnover
170  /// </summary>
171  [JsonConverter(typeof(JsonRoundingConverter))]
172  public decimal PortfolioTurnover { get; set; }
173 
174  /// <summary>
175  /// The 1-day VaR for the portfolio, using the Variance-covariance approach.
176  /// Assumes a 99% confidence level, 1 year lookback period, and that the returns are normally distributed.
177  /// </summary>
178  [JsonConverter(typeof(JsonRoundingConverter))]
179  public decimal ValueAtRisk99 { get; set; }
180 
181  /// <summary>
182  /// The 1-day VaR for the portfolio, using the Variance-covariance approach.
183  /// Assumes a 95% confidence level, 1 year lookback period, and that the returns are normally distributed.
184  /// </summary>
185  [JsonConverter(typeof(JsonRoundingConverter))]
186  public decimal ValueAtRisk95 { get; set; }
187 
188  /// <summary>
189  /// Initializes a new instance of the <see cref="PortfolioStatistics"/> class
190  /// </summary>
191  /// <param name="profitLoss">Trade record of profits and losses</param>
192  /// <param name="equity">The list of daily equity values</param>
193  /// <param name="portfolioTurnover">The algorithm portfolio turnover</param>
194  /// <param name="listPerformance">The list of algorithm performance values</param>
195  /// <param name="listBenchmark">The list of benchmark values</param>
196  /// <param name="startingCapital">The algorithm starting capital</param>
197  /// <param name="riskFreeInterestRateModel">The risk free interest rate model to use</param>
198  /// <param name="tradingDaysPerYear">The number of trading days per year</param>
199  /// <param name="winCount">
200  /// The number of wins, including ITM options with profitLoss less than 0.
201  /// If this and <paramref name="lossCount"/> are null, they will be calculated from <paramref name="profitLoss"/>
202  /// </param>
203  /// <param name="lossCount">The number of losses</param>
205  SortedDictionary<DateTime, decimal> profitLoss,
206  SortedDictionary<DateTime, decimal> equity,
207  SortedDictionary<DateTime, decimal> portfolioTurnover,
208  List<double> listPerformance,
209  List<double> listBenchmark,
210  decimal startingCapital,
211  IRiskFreeInterestRateModel riskFreeInterestRateModel,
212  int tradingDaysPerYear,
213  int? winCount = null,
214  int? lossCount = null)
215  {
216  StartEquity = startingCapital;
217  EndEquity = equity.LastOrDefault().Value;
218 
219  if (portfolioTurnover.Count > 0)
220  {
221  PortfolioTurnover = portfolioTurnover.Select(kvp => kvp.Value).Average();
222  }
223 
224  if (startingCapital == 0
225  // minimum amount of samples to calculate variance
226  || listBenchmark.Count < 2
227  || listPerformance.Count < 2)
228  {
229  return;
230  }
231 
232  var runningCapital = startingCapital;
233  var totalProfit = 0m;
234  var totalLoss = 0m;
235  var totalWins = 0;
236  var totalLosses = 0;
237  foreach (var pair in profitLoss)
238  {
239  var tradeProfitLoss = pair.Value;
240 
241  if (tradeProfitLoss > 0)
242  {
243  totalProfit += tradeProfitLoss / runningCapital;
244  totalWins++;
245  }
246  else
247  {
248  totalLoss += tradeProfitLoss / runningCapital;
249  totalLosses++;
250  }
251 
252  runningCapital += tradeProfitLoss;
253  }
254 
255  AverageWinRate = totalWins == 0 ? 0 : totalProfit / totalWins;
256  AverageLossRate = totalLosses == 0 ? 0 : totalLoss / totalLosses;
258 
259  // Set the actual total wins and losses count.
260  // Some options assignments (ITM) count as wins even though they are losses.
261  if (winCount.HasValue && lossCount.HasValue)
262  {
263  totalWins = winCount.Value;
264  totalLosses = lossCount.Value;
265  }
266 
267  var totalTrades = totalWins + totalLosses;
268  WinRate = totalTrades == 0 ? 0 : (decimal) totalWins / totalTrades;
269  LossRate = totalTrades == 0 ? 0 : (decimal) totalLosses / totalTrades;
271 
272  if (startingCapital != 0)
273  {
274  TotalNetProfit = equity.Values.LastOrDefault() / startingCapital - 1;
275  }
276 
277  var fractionOfYears = (decimal) (equity.Keys.LastOrDefault() - equity.Keys.FirstOrDefault()).TotalDays / 365;
278  CompoundingAnnualReturn = Statistics.CompoundingAnnualPerformance(startingCapital, equity.Values.LastOrDefault(), fractionOfYears);
279 
280  Drawdown = DrawdownPercent(equity, 3);
281 
282  AnnualVariance = Statistics.AnnualVariance(listPerformance, tradingDaysPerYear).SafeDecimalCast();
283  AnnualStandardDeviation = (decimal) Math.Sqrt((double) AnnualVariance);
284 
285  var benchmarkAnnualPerformance = GetAnnualPerformance(listBenchmark, tradingDaysPerYear);
286  var annualPerformance = GetAnnualPerformance(listPerformance, tradingDaysPerYear);
287 
288  var riskFreeRate = riskFreeInterestRateModel.GetAverageRiskFreeRate(equity.Select(x => x.Key));
289  SharpeRatio = AnnualStandardDeviation == 0 ? 0 : Statistics.SharpeRatio(annualPerformance, AnnualStandardDeviation, riskFreeRate);
290 
291  var annualDownsideDeviation = Statistics.AnnualDownsideStandardDeviation(listPerformance, tradingDaysPerYear).SafeDecimalCast();
292  SortinoRatio = annualDownsideDeviation == 0 ? 0 : Statistics.SharpeRatio(annualPerformance, annualDownsideDeviation, riskFreeRate);
293 
294  var benchmarkVariance = listBenchmark.Variance();
295  Beta = benchmarkVariance.IsNaNOrZero() ? 0 : (decimal) (listPerformance.Covariance(listBenchmark) / benchmarkVariance);
296 
297  Alpha = Beta == 0 ? 0 : annualPerformance - (riskFreeRate + Beta * (benchmarkAnnualPerformance - riskFreeRate));
298 
299  TrackingError = (decimal)Statistics.TrackingError(listPerformance, listBenchmark, (double)tradingDaysPerYear);
300 
301  InformationRatio = TrackingError == 0 ? 0 : (annualPerformance - benchmarkAnnualPerformance) / TrackingError;
302 
303  TreynorRatio = Beta == 0 ? 0 : (annualPerformance - riskFreeRate) / Beta;
304 
305  // deannualize a 1 sharpe ratio
306  var benchmarkSharpeRatio = 1.0d / Math.Sqrt(tradingDaysPerYear);
307  ProbabilisticSharpeRatio = Statistics.ProbabilisticSharpeRatio(listPerformance, benchmarkSharpeRatio).SafeDecimalCast();
308 
309  ValueAtRisk99 = GetValueAtRisk(listPerformance, tradingDaysPerYear, 0.99d);
310  ValueAtRisk95 = GetValueAtRisk(listPerformance, tradingDaysPerYear, 0.95d);
311  }
312 
313  /// <summary>
314  /// Initializes a new instance of the <see cref="PortfolioStatistics"/> class
315  /// </summary>
317  {
318  }
319 
320  /// <summary>
321  /// Drawdown maximum percentage.
322  /// </summary>
323  /// <param name="equityOverTime">The list of daily equity values</param>
324  /// <param name="rounding">The number of decimal places to round the result</param>
325  /// <returns>The drawdown percentage</returns>
326  private static decimal DrawdownPercent(SortedDictionary<DateTime, decimal> equityOverTime, int rounding = 2)
327  {
328  var prices = equityOverTime.Values.ToList();
329  if (prices.Count == 0) return 0;
330 
331  var drawdowns = new List<decimal>();
332  var high = prices[0];
333  foreach (var price in prices)
334  {
335  if (price > high) high = price;
336  if (high > 0) drawdowns.Add(price / high - 1);
337  }
338 
339  return Math.Round(Math.Abs(drawdowns.Min()), rounding);
340  }
341 
342  /// <summary>
343  /// Annualized return statistic calculated as an average of daily trading performance multiplied by the number of trading days per year.
344  /// </summary>
345  /// <param name="performance">Dictionary collection of double performance values</param>
346  /// <param name="tradingDaysPerYear">Trading days per year for the assets in portfolio</param>
347  /// <remarks>May be inaccurate for forex algorithms with more trading days in a year</remarks>
348  /// <returns>Double annual performance percentage</returns>
349  private static decimal GetAnnualPerformance(List<double> performance, int tradingDaysPerYear)
350  {
351  try
352  {
353  return Statistics.AnnualPerformance(performance, tradingDaysPerYear).SafeDecimalCast();
354  }
355  catch (ArgumentException ex)
356  {
357  var partialSums = 0.0;
358  var points = 0;
359  double troublePoint = default;
360  foreach(var point in performance)
361  {
362  points++;
363  partialSums += point;
364  if (Math.Pow(partialSums / points, tradingDaysPerYear).IsNaNOrInfinity())
365  {
366  troublePoint = point;
367  break;
368  }
369  }
370 
371  throw new ArgumentException($"PortfolioStatistics.GetAnnualPerformance(): An exception was thrown when trying to cast the annual performance value due to the following performance point: {troublePoint}. " +
372  $"The exception thrown was the following: {ex.Message}.");
373  }
374  }
375 
376  private static decimal GetValueAtRisk(
377  List<double> performance,
378  int lookbackPeriodDays,
379  double confidenceLevel,
380  int rounding = 3)
381  {
382  var periodPerformance = performance.TakeLast(lookbackPeriodDays);
383  var mean = periodPerformance.Mean();
384  var standardDeviation = periodPerformance.StandardDeviation();
385  var valueAtRisk = (decimal)Normal.InvCDF(mean, standardDeviation, 1 - confidenceLevel);
386  return Math.Round(valueAtRisk, rounding);
387  }
388  }
389 }