/* * 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 QuantConnect.Logging; namespace QuantConnect.Statistics { /// /// Calculate all the statistics required from the backtest, based on the equity curve and the profit loss statement. /// /// This is a particularly ugly class and one of the first ones written. It should be thrown out and re-written. public class Statistics { /// /// Annual compounded returns statistic based on the final-starting capital and years. /// /// Algorithm starting capital /// Algorithm final capital /// Years trading /// Decimal fraction for annual compounding performance public static decimal CompoundingAnnualPerformance(decimal startingCapital, decimal finalCapital, decimal years) { if (years == 0 || startingCapital == 0) { return 0; } var power = 1 / (double)years; var baseNumber = (double)finalCapital / (double)startingCapital; var result = Math.Pow(baseNumber, power) - 1; return result.IsNaNOrInfinity() ? 0 : result.SafeDecimalCast(); } /// /// 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 unaccurate for forex algorithms with more trading days in a year /// Double annual performance percentage public static double AnnualPerformance(List performance, double tradingDaysPerYear) { return Math.Pow((performance.Average() + 1), tradingDaysPerYear) - 1; } /// /// Annualized variance statistic calculation using the daily performance variance and trading days per year. /// /// /// /// Invokes the variance extension in the MathNet Statistics class /// Annual variance value public static double AnnualVariance(List performance, double tradingDaysPerYear) { var variance = performance.Variance(); return variance.IsNaNOrZero() ? 0 : variance * tradingDaysPerYear; } /// /// Annualized standard deviation /// /// Collection of double values for daily performance /// Number of trading days for the assets in portfolio to get annualize standard deviation. /// /// Invokes the variance extension in the MathNet Statistics class. /// Feasibly the trading days per year can be fetched from the dictionary of performance which includes the date-times to get the range; if is more than 1 year data. /// /// Value for annual standard deviation public static double AnnualStandardDeviation(List performance, double tradingDaysPerYear) { return Math.Sqrt(AnnualVariance(performance, tradingDaysPerYear)); } /// /// Annualized variance statistic calculation using the daily performance variance and trading days per year. /// /// /// /// Minimum acceptable return /// Invokes the variance extension in the MathNet Statistics class /// Annual variance value public static double AnnualDownsideVariance(List performance, double tradingDaysPerYear, double minimumAcceptableReturn = 0) { return AnnualVariance(performance.Where(ret => ret < minimumAcceptableReturn).ToList(), tradingDaysPerYear); } /// /// Annualized downside standard deviation /// /// Collection of double values for daily performance /// Number of trading days for the assets in portfolio to get annualize standard deviation. /// Minimum acceptable return /// Value for annual downside standard deviation public static double AnnualDownsideStandardDeviation(List performance, double tradingDaysPerYear, double minimumAcceptableReturn = 0) { return Math.Sqrt(AnnualDownsideVariance(performance, tradingDaysPerYear, minimumAcceptableReturn)); } /// /// 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 /// Double collection of algorithm daily performance values /// Double collection of benchmark daily performance values /// Number of trading days per year /// Value for tracking error public static double TrackingError(List algoPerformance, List benchmarkPerformance, double tradingDaysPerYear) { // Un-equal lengths will blow up other statistics, but this will handle the case here if (algoPerformance.Count != benchmarkPerformance.Count) { return 0.0; } var performanceDifference = new List(); for (var i = 0; i < algoPerformance.Count; i++) { performanceDifference.Add(algoPerformance[i] - benchmarkPerformance[i]); } return Math.Sqrt(AnnualVariance(performanceDifference, tradingDaysPerYear)); } /// /// Sharpe ratio with respect to risk free rate: measures excess of return per unit of risk. /// /// With risk defined as the algorithm's volatility /// Average daily performance /// Standard deviation of the daily performance /// The risk free rate /// Value for sharpe ratio public static double SharpeRatio(double averagePerformance, double standardDeviation, double riskFreeRate) { return standardDeviation == 0 ? 0 : (averagePerformance - riskFreeRate) / standardDeviation; } /// /// Sharpe ratio with respect to risk free rate: measures excess of return per unit of risk. /// /// With risk defined as the algorithm's volatility /// Average daily performance /// Standard deviation of the daily performance /// The risk free rate /// Value for sharpe ratio public static decimal SharpeRatio(decimal averagePerformance, decimal standardDeviation, decimal riskFreeRate) { return SharpeRatio((double)averagePerformance, (double)standardDeviation, (double)riskFreeRate).SafeDecimalCast(); } /// /// Sharpe ratio with respect to risk free rate: measures excess of return per unit of risk. /// /// With risk defined as the algorithm's volatility /// Collection of double values for the algorithm daily performance /// The risk free rate /// Trading days per year for the assets in portfolio /// Value for sharpe ratio public static double SharpeRatio(List algoPerformance, double riskFreeRate, double tradingDaysPerYear) { return SharpeRatio(AnnualPerformance(algoPerformance, tradingDaysPerYear), AnnualStandardDeviation(algoPerformance, tradingDaysPerYear), riskFreeRate); } /// /// 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 /// Collection of double values for the algorithm daily performance /// The risk free rate /// Trading days per year for the assets in portfolio /// Minimum acceptable return for Sortino ratio calculation /// Value for Sortino ratio public static double SortinoRatio(List algoPerformance, double riskFreeRate, double tradingDaysPerYear, double minimumAcceptableReturn = 0) { return SharpeRatio(AnnualPerformance(algoPerformance, tradingDaysPerYear), AnnualDownsideStandardDeviation(algoPerformance, tradingDaysPerYear, minimumAcceptableReturn), riskFreeRate); } /// /// Helper method to calculate the probabilistic sharpe ratio /// /// The list of algorithm performance values /// The benchmark sharpe ratio to use /// Probabilistic Sharpe Ratio public static double ProbabilisticSharpeRatio(List listPerformance, double benchmarkSharpeRatio) { var observedSharpeRatio = ObservedSharpeRatio(listPerformance); var skewness = listPerformance.Skewness(); var kurtosis = listPerformance.Kurtosis(); var operandA = skewness * observedSharpeRatio; var operandB = ((kurtosis - 1) / 4) * (Math.Pow(observedSharpeRatio, 2)); // Calculated standard deviation of point estimate var estimateStandardDeviation = Math.Pow((1 - operandA + operandB) / (listPerformance.Count - 1), 0.5); if (double.IsNaN(estimateStandardDeviation)) { return 0; } // Calculate PSR(benchmark) var value = estimateStandardDeviation.IsNaNOrZero() ? 0 : (observedSharpeRatio - benchmarkSharpeRatio) / estimateStandardDeviation; return (new Normal()).CumulativeDistribution(value); } /// /// Calculates the observed sharpe ratio /// /// The performance samples to use /// The observed sharpe ratio public static double ObservedSharpeRatio(List listPerformance) { var performanceAverage = listPerformance.Average(); var standardDeviation = listPerformance.StandardDeviation(); // we don't annualize it return standardDeviation.IsNaNOrZero() ? 0 : performanceAverage / standardDeviation; } /// /// Calculate the drawdown between a high and current value /// /// Current value /// Latest maximum /// Digits to round the result too /// Drawdown percentage public static decimal DrawdownPercent(decimal current, decimal high, int roundingDecimals = 2) { if (high == 0) { throw new ArgumentException("High value must not be 0"); } var drawdownPercentage = ((current / high) - 1) * 100; return Math.Round(drawdownPercentage, roundingDecimals); } /// /// Calculates the maximum drawdown percentage and the maximum recovery time (in days) /// from a historical equity time series. /// /// Time series of equity values indexed by date /// Number of decimals to round the results to /// A object containing MaxDrawdown (percentage) and MaxRecoveryTime (in days) public static DrawdownMetrics CalculateDrawdownMetrics(SortedDictionary equityOverTime, int rounding = 2) { decimal maxDrawdown = 0m; decimal maxRecoveryTime = 0m; try { if (equityOverTime.Count < 2) return new DrawdownMetrics(0m, 0); var equityList = equityOverTime.ToList(); var peakEquity = equityList[0].Value; var peakDate = equityList[0].Key; DateTime? drawdownStartDate = null; foreach (var point in equityList) { // Update peak equity if a new high is reached (or matched) if (point.Value >= peakEquity) { // If we were in a drawdown, calculate recovery time if (drawdownStartDate.HasValue) { var recoveryDays = (decimal)(point.Key - drawdownStartDate.Value).TotalDays; maxRecoveryTime = Math.Max(maxRecoveryTime, recoveryDays); drawdownStartDate = null; } peakEquity = point.Value; peakDate = point.Key; } // Calculate current drawdown from peak var currentDrawdown = (point.Value / peakEquity) - 1; if (currentDrawdown < 0) { maxDrawdown = Math.Min(maxDrawdown, currentDrawdown); // Mark the start of the drawdown period if (!drawdownStartDate.HasValue) { drawdownStartDate = peakDate; } } } // Return absolute drawdown percentage and max recovery time in days return new DrawdownMetrics(Math.Round(Math.Abs(maxDrawdown), rounding), (int)maxRecoveryTime); } catch (Exception err) { Log.Error(err); return new DrawdownMetrics(0m, 0); } } } // End of Statistics } // End of Namespace