/*
* 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