/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Linq; using MathNet.Numerics.Distributions; using MathNet.Numerics.Statistics; using Newtonsoft.Json; using QuantConnect.Data; using QuantConnect.Util; namespace QuantConnect.Statistics { /// /// The class represents a set of statistics calculated from equity and benchmark samples /// public class PortfolioStatistics { /// /// The average rate of return for winning trades /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal AverageWinRate { get; set; } /// /// The average rate of return for losing trades /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal AverageLossRate { get; set; } /// /// The ratio of the average win rate to the average loss rate /// /// If the average loss rate is zero, ProfitLossRatio is set to 0 [JsonConverter(typeof(JsonRoundingConverter))] public decimal ProfitLossRatio { get; set; } /// /// The ratio of the number of winning trades to the total number of trades /// /// If the total number of trades is zero, WinRate is set to zero [JsonConverter(typeof(JsonRoundingConverter))] public decimal WinRate { get; set; } /// /// The ratio of the number of losing trades to the total number of trades /// /// If the total number of trades is zero, LossRate is set to zero [JsonConverter(typeof(JsonRoundingConverter))] public decimal LossRate { get; set; } /// /// The expected value of the rate of return /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal Expectancy { get; set; } /// /// Initial Equity Total Value /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal StartEquity { get; set; } /// /// Final Equity Total Value /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal EndEquity { get; set; } /// /// Annual compounded returns statistic based on the final-starting capital and years. /// /// Also known as Compound Annual Growth Rate (CAGR) [JsonConverter(typeof(JsonRoundingConverter))] public decimal CompoundingAnnualReturn { get; set; } /// /// Drawdown maximum percentage. /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal Drawdown { get; set; } /// /// The total net profit percentage. /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal TotalNetProfit { get; set; } /// /// Sharpe ratio with respect to risk free rate: measures excess of return per unit of risk. /// /// With risk defined as the algorithm's volatility [JsonConverter(typeof(JsonRoundingConverter))] public decimal SharpeRatio { get; set; } /// /// Probabilistic Sharpe Ratio is a probability measure associated with the Sharpe ratio. /// It informs us of the probability that the estimated Sharpe ratio is greater than a chosen benchmark /// /// See https://www.quantconnect.com/forum/discussion/6483/probabilistic-sharpe-ratio/p1 [JsonConverter(typeof(JsonRoundingConverter))] public decimal ProbabilisticSharpeRatio { get; set; } /// /// Sortino ratio with respect to risk free rate: measures excess of return per unit of downside risk. /// /// With risk defined as the algorithm's volatility [JsonConverter(typeof(JsonRoundingConverter))] public decimal SortinoRatio { get; set; } /// /// Algorithm "Alpha" statistic - abnormal returns over the risk free rate and the relationshio (beta) with the benchmark returns. /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal Alpha { get; set; } /// /// Algorithm "beta" statistic - the covariance between the algorithm and benchmark performance, divided by benchmark's variance /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal Beta { get; set; } /// /// Annualized standard deviation /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal AnnualStandardDeviation { get; set; } /// /// Annualized variance statistic calculation using the daily performance variance and trading days per year. /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal AnnualVariance { get; set; } /// /// Information ratio - risk adjusted return /// /// (risk = tracking error volatility, a volatility measures that considers the volatility of both algo and benchmark) [JsonConverter(typeof(JsonRoundingConverter))] public decimal InformationRatio { get; set; } /// /// Tracking error volatility (TEV) statistic - a measure of how closely a portfolio follows the index to which it is benchmarked /// /// If algo = benchmark, TEV = 0 [JsonConverter(typeof(JsonRoundingConverter))] public decimal TrackingError { get; set; } /// /// 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 /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal TreynorRatio { get; set; } /// /// The average Portfolio Turnover /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal PortfolioTurnover { get; set; } /// /// The 1-day VaR for the portfolio, using the Variance-covariance approach. /// Assumes a 99% confidence level, 1 year lookback period, and that the returns are normally distributed. /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal ValueAtRisk99 { get; set; } /// /// The 1-day VaR for the portfolio, using the Variance-covariance approach. /// Assumes a 95% confidence level, 1 year lookback period, and that the returns are normally distributed. /// [JsonConverter(typeof(JsonRoundingConverter))] public decimal ValueAtRisk95 { get; set; } /// /// The recovery time of the maximum drawdown. /// [JsonConverter(typeof(JsonRoundingConverter))] public int DrawdownRecovery { get; set; } /// /// Initializes a new instance of the class /// /// Trade record of profits and losses /// The list of daily equity values /// The algorithm portfolio turnover /// The list of algorithm performance values /// The list of benchmark values /// The algorithm starting capital /// The risk free interest rate model to use /// The number of trading days per year /// /// The number of wins, including ITM options with profitLoss less than 0. /// If this and are null, they will be calculated from /// /// The number of losses public PortfolioStatistics( SortedDictionary profitLoss, SortedDictionary equity, SortedDictionary portfolioTurnover, List listPerformance, List listBenchmark, decimal startingCapital, IRiskFreeInterestRateModel riskFreeInterestRateModel, int tradingDaysPerYear, int? winCount = null, int? lossCount = null) { StartEquity = startingCapital; EndEquity = equity.LastOrDefault().Value; if (portfolioTurnover.Count > 0) { PortfolioTurnover = portfolioTurnover.Select(kvp => kvp.Value).Average(); } if (startingCapital == 0 // minimum amount of samples to calculate variance || listBenchmark.Count < 2 || listPerformance.Count < 2) { return; } var runningCapital = startingCapital; var totalProfit = 0m; var totalLoss = 0m; var totalWins = 0; var totalLosses = 0; foreach (var pair in profitLoss) { var tradeProfitLoss = pair.Value; if (tradeProfitLoss > 0) { totalProfit += tradeProfitLoss / runningCapital; totalWins++; } else { totalLoss += tradeProfitLoss / runningCapital; totalLosses++; } runningCapital += tradeProfitLoss; } AverageWinRate = totalWins == 0 ? 0 : totalProfit / totalWins; AverageLossRate = totalLosses == 0 ? 0 : totalLoss / totalLosses; ProfitLossRatio = AverageLossRate == 0 ? 0 : AverageWinRate / Math.Abs(AverageLossRate); // Set the actual total wins and losses count. // Some options assignments (ITM) count as wins even though they are losses. if (winCount.HasValue && lossCount.HasValue) { totalWins = winCount.Value; totalLosses = lossCount.Value; } var totalTrades = totalWins + totalLosses; WinRate = totalTrades == 0 ? 0 : (decimal)totalWins / totalTrades; LossRate = totalTrades == 0 ? 0 : (decimal)totalLosses / totalTrades; Expectancy = WinRate * ProfitLossRatio - LossRate; if (startingCapital != 0) { TotalNetProfit = equity.Values.LastOrDefault() / startingCapital - 1; } var fractionOfYears = (decimal)(equity.Keys.LastOrDefault() - equity.Keys.FirstOrDefault()).TotalDays / 365; CompoundingAnnualReturn = Statistics.CompoundingAnnualPerformance(startingCapital, equity.Values.LastOrDefault(), fractionOfYears); AnnualVariance = Statistics.AnnualVariance(listPerformance, tradingDaysPerYear).SafeDecimalCast(); AnnualStandardDeviation = (decimal)Math.Sqrt((double)AnnualVariance); var benchmarkAnnualPerformance = GetAnnualPerformance(listBenchmark, tradingDaysPerYear); var annualPerformance = GetAnnualPerformance(listPerformance, tradingDaysPerYear); var riskFreeRate = riskFreeInterestRateModel.GetAverageRiskFreeRate(equity.Select(x => x.Key)); SharpeRatio = AnnualStandardDeviation == 0 ? 0 : Statistics.SharpeRatio(annualPerformance, AnnualStandardDeviation, riskFreeRate); var annualDownsideDeviation = Statistics.AnnualDownsideStandardDeviation(listPerformance, tradingDaysPerYear).SafeDecimalCast(); SortinoRatio = annualDownsideDeviation == 0 ? 0 : Statistics.SharpeRatio(annualPerformance, annualDownsideDeviation, riskFreeRate); var benchmarkVariance = listBenchmark.Variance(); Beta = benchmarkVariance.IsNaNOrZero() ? 0 : (decimal)(listPerformance.Covariance(listBenchmark) / benchmarkVariance); Alpha = Beta == 0 ? 0 : annualPerformance - (riskFreeRate + Beta * (benchmarkAnnualPerformance - riskFreeRate)); TrackingError = (decimal)Statistics.TrackingError(listPerformance, listBenchmark, (double)tradingDaysPerYear); InformationRatio = TrackingError == 0 ? 0 : Extensions.SafeDecimalCast((double)annualPerformance - (double)benchmarkAnnualPerformance).SafeDivision(TrackingError); TreynorRatio = Beta == 0 ? 0 : Extensions.SafeDecimalCast((double)annualPerformance - (double)riskFreeRate).SafeDivision(Beta); // deannualize a 1 sharpe ratio var benchmarkSharpeRatio = 1.0d / Math.Sqrt(tradingDaysPerYear); ProbabilisticSharpeRatio = Statistics.ProbabilisticSharpeRatio(listPerformance, benchmarkSharpeRatio).SafeDecimalCast(); ValueAtRisk99 = GetValueAtRisk(listPerformance, tradingDaysPerYear, 0.99d); ValueAtRisk95 = GetValueAtRisk(listPerformance, tradingDaysPerYear, 0.95d); var drawdownMetrics = Statistics.CalculateDrawdownMetrics(equity, 3); Drawdown = drawdownMetrics.Drawdown; DrawdownRecovery = drawdownMetrics.DrawdownRecovery; } /// /// Initializes a new instance of the class /// public PortfolioStatistics() { } /// /// Annualized return statistic calculated as an average of daily trading performance multiplied by the number of trading days per year. /// /// Dictionary collection of double performance values /// Trading days per year for the assets in portfolio /// May be inaccurate for forex algorithms with more trading days in a year /// Double annual performance percentage private static decimal GetAnnualPerformance(List performance, int tradingDaysPerYear) { try { return Statistics.AnnualPerformance(performance, tradingDaysPerYear).SafeDecimalCast(); } catch (ArgumentException ex) { var partialSums = 0.0; var points = 0; double troublePoint = default; foreach (var point in performance) { points++; partialSums += point; if (Math.Pow(partialSums / points, tradingDaysPerYear).IsNaNOrInfinity()) { troublePoint = point; break; } } throw new ArgumentException($"PortfolioStatistics.GetAnnualPerformance(): An exception was thrown when trying to cast the annual performance value due to the following performance point: {troublePoint}. " + $"The exception thrown was the following: {ex.Message}."); } } private static decimal GetValueAtRisk( List performance, int lookbackPeriodDays, double confidenceLevel, int rounding = 3) { var periodPerformance = performance.TakeLast(lookbackPeriodDays); var mean = periodPerformance.Mean(); var standardDeviation = periodPerformance.StandardDeviation(); var valueAtRisk = (decimal)Normal.InvCDF(mean, standardDeviation, 1 - confidenceLevel); return Math.Round(valueAtRisk, rounding); } } }