diff --git a/Common/Api/OptimizationBacktestJsonConverter.cs b/Common/Api/OptimizationBacktestJsonConverter.cs
index 5308cbce835a..119207baad37 100644
--- a/Common/Api/OptimizationBacktestJsonConverter.cs
+++ b/Common/Api/OptimizationBacktestJsonConverter.cs
@@ -57,6 +57,7 @@ public class OptimizationBacktestJsonConverter : JsonConverter
{ PerformanceMetrics.StartEquity, 22 },
{ PerformanceMetrics.EndEquity, 23 },
{ PerformanceMetrics.DrawdownRecovery, 24 },
+ { PerformanceMetrics.AdjustedSharpeRatio, 25 },
};
private static string[] StatisticNames { get; } = StatisticsIndices
diff --git a/Common/Statistics/PerformanceMetrics.cs b/Common/Statistics/PerformanceMetrics.cs
index 5dd2ccce92b1..ae7abb7901b4 100644
--- a/Common/Statistics/PerformanceMetrics.cs
+++ b/Common/Statistics/PerformanceMetrics.cs
@@ -20,6 +20,12 @@ namespace QuantConnect.Statistics
///
public static class PerformanceMetrics
{
+ ///
+ /// Adjusted Sharpe ratio: penalizes the Sharpe ratio for negative skewness and fat tails (kurtosis).
+ ///
+ /// Pezier and White (2006)
+ public const string AdjustedSharpeRatio = "Adjusted Sharpe Ratio";
+
///
/// Algorithm "Alpha" statistic - abnormal returns over the risk free rate and the relationshio (beta) with the benchmark returns.
///
diff --git a/Common/Statistics/PortfolioStatistics.cs b/Common/Statistics/PortfolioStatistics.cs
index a0f326ec9a5d..41ef2d76513a 100644
--- a/Common/Statistics/PortfolioStatistics.cs
+++ b/Common/Statistics/PortfolioStatistics.cs
@@ -106,6 +106,13 @@ public class PortfolioStatistics
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal SharpeRatio { get; set; }
+ ///
+ /// Adjusted Sharpe ratio: penalizes the Sharpe ratio for negative skewness and fat tails (kurtosis).
+ ///
+ /// Pezier and White (2006)
+ [JsonConverter(typeof(JsonRoundingConverter))]
+ public decimal AdjustedSharpeRatio { 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
@@ -292,6 +299,7 @@ public PortfolioStatistics(
var riskFreeRate = riskFreeInterestRateModel.GetAverageRiskFreeRate(equity.Select(x => x.Key));
SharpeRatio = AnnualStandardDeviation == 0 ? 0 : Statistics.SharpeRatio(annualPerformance, AnnualStandardDeviation, riskFreeRate);
+ AdjustedSharpeRatio = Statistics.AdjustedSharpeRatio(listPerformance, (double)riskFreeRate / tradingDaysPerYear, tradingDaysPerYear).SafeDecimalCast();
var annualDownsideDeviation = Statistics.AnnualDownsideStandardDeviation(listPerformance, tradingDaysPerYear).SafeDecimalCast();
SortinoRatio = annualDownsideDeviation == 0 ? 0 : Statistics.SharpeRatio(annualPerformance, annualDownsideDeviation, riskFreeRate);
diff --git a/Common/Statistics/Statistics.cs b/Common/Statistics/Statistics.cs
index 788fca27988f..d53283fa0fe0 100644
--- a/Common/Statistics/Statistics.cs
+++ b/Common/Statistics/Statistics.cs
@@ -236,6 +236,115 @@ public static double ObservedSharpeRatio(List listPerformance, double ri
return standardDeviation.IsNaNOrZero() ? 0 : performanceAverage / standardDeviation;
}
+ ///
+ /// Calculates the annualized Adjusted Sharpe Ratio (Pezier and White, 2006) which adjusts the Sharpe Ratio
+ /// for skewness and excess kurtosis of the return distribution at consistent time horizons.
+ ///
+ ///
+ /// Under i.i.d. returns, the per-period adjustment is computed from the per-period Sharpe ratio,
+ /// sample skewness, and sample excess kurtosis (MathNet's returns excess kurtosis, K - 3),
+ /// and then annualized by multiplying by sqrt(tradingDaysPerYear).
+ ///
+ /// The performance samples to use
+ /// The per-sample risk-free rate (e.g. annual risk-free rate / tradingDaysPerYear)
+ /// The number of trading days per year for annualization
+ /// The annualized adjusted Sharpe ratio
+ public static double AdjustedSharpeRatio(List listPerformance, double riskFreeRate = 0, double tradingDaysPerYear = 252)
+ {
+ if (listPerformance.Count < 3 || tradingDaysPerYear <= 0)
+ {
+ return 0;
+ }
+
+ var observedSharpeRatio = ObservedSharpeRatio(listPerformance, riskFreeRate);
+ if (observedSharpeRatio == 0 || double.IsNaN(observedSharpeRatio) || double.IsInfinity(observedSharpeRatio))
+ {
+ return 0;
+ }
+
+ var skewness = listPerformance.Skewness();
+ var excessKurtosis = listPerformance.Kurtosis(); // MathNet returns excess kurtosis (K - 3)
+
+ if (skewness.IsNaNOrInfinity() || excessKurtosis.IsNaNOrInfinity())
+ {
+ return 0;
+ }
+
+ // Pezier and White (2006) expansion at the sampling frequency
+ var asrPeriod = observedSharpeRatio * (1.0d + (skewness / 6.0d) * observedSharpeRatio - (excessKurtosis / 24.0d) * Math.Pow(observedSharpeRatio, 2));
+
+ if (double.IsNaN(asrPeriod) || double.IsInfinity(asrPeriod))
+ {
+ return 0;
+ }
+
+ return asrPeriod * Math.Sqrt(tradingDaysPerYear);
+ }
+
+ ///
+ /// Calculates the annualized Adjusted Sharpe Ratio (Pezier and White, 2006) which adjusts the Sharpe Ratio
+ /// for skewness and excess kurtosis of the return distribution at consistent time horizons.
+ ///
+ /// The performance samples to use
+ /// The per-sample risk-free rate (e.g. annual risk-free rate / tradingDaysPerYear)
+ /// The number of trading days per year for annualization
+ /// The annualized adjusted Sharpe ratio
+ public static decimal AdjustedSharpeRatio(List listPerformance, decimal riskFreeRate, int tradingDaysPerYear)
+ {
+ return AdjustedSharpeRatio(listPerformance, (double)riskFreeRate, tradingDaysPerYear).SafeDecimalCast();
+ }
+
+ ///
+ /// Calculates the Adjusted Sharpe Ratio (Pezier and White, 2006) given an annualized Sharpe ratio,
+ /// scaling sample skewness and excess kurtosis to the annual horizon.
+ ///
+ /// The per-period performance samples to use for skewness and excess kurtosis
+ /// The annualized Sharpe ratio
+ /// The number of trading days per year
+ /// The annualized adjusted Sharpe ratio
+ public static double AdjustedSharpeRatioFromAnnualized(List listPerformance, double annualizedSharpeRatio, double tradingDaysPerYear = 252)
+ {
+ if (listPerformance.Count < 3 || annualizedSharpeRatio == 0 || tradingDaysPerYear <= 0)
+ {
+ return 0;
+ }
+
+ var skewness = listPerformance.Skewness();
+ var excessKurtosis = listPerformance.Kurtosis(); // MathNet returns excess kurtosis (K - 3)
+
+ if (skewness.IsNaNOrInfinity() || excessKurtosis.IsNaNOrInfinity())
+ {
+ return 0;
+ }
+
+ // Scale per-period skewness and excess kurtosis to annual horizon:
+ // S_annual = S_period / sqrt(T), K_excess_annual = K_excess_period / T
+ var annualSkewness = skewness / Math.Sqrt(tradingDaysPerYear);
+ var annualExcessKurtosis = excessKurtosis / tradingDaysPerYear;
+
+ var asr = annualizedSharpeRatio * (1.0d + (annualSkewness / 6.0d) * annualizedSharpeRatio - (annualExcessKurtosis / 24.0d) * Math.Pow(annualizedSharpeRatio, 2));
+
+ if (double.IsNaN(asr) || double.IsInfinity(asr))
+ {
+ return 0;
+ }
+
+ return asr;
+ }
+
+ ///
+ /// Calculates the Adjusted Sharpe Ratio (Pezier and White, 2006) given an annualized Sharpe ratio,
+ /// scaling sample skewness and excess kurtosis to the annual horizon.
+ ///
+ /// The per-period performance samples to use for skewness and excess kurtosis
+ /// The annualized Sharpe ratio
+ /// The number of trading days per year
+ /// The annualized adjusted Sharpe ratio
+ public static decimal AdjustedSharpeRatioFromAnnualized(List listPerformance, decimal annualizedSharpeRatio, int tradingDaysPerYear)
+ {
+ return AdjustedSharpeRatioFromAnnualized(listPerformance, (double)annualizedSharpeRatio, (double)tradingDaysPerYear).SafeDecimalCast();
+ }
+
///
/// Calculate the drawdown between a high and current value
///
diff --git a/Common/Statistics/StatisticsBuilder.cs b/Common/Statistics/StatisticsBuilder.cs
index 7fd403b6e0ef..11bf35b69594 100644
--- a/Common/Statistics/StatisticsBuilder.cs
+++ b/Common/Statistics/StatisticsBuilder.cs
@@ -227,6 +227,7 @@ private static Dictionary GetSummary(AlgorithmPerformance totalP
{ PerformanceMetrics.SharpeRatio, Math.Round((double)totalPerformance.PortfolioStatistics.SharpeRatio, 3).ToStringInvariant() },
{ PerformanceMetrics.SortinoRatio, Math.Round((double)totalPerformance.PortfolioStatistics.SortinoRatio, 3).ToStringInvariant() },
{ PerformanceMetrics.ProbabilisticSharpeRatio, Math.Round(totalPerformance.PortfolioStatistics.ProbabilisticSharpeRatio.SafeMultiply100(), 3).ToStringInvariant() + "%"},
+ { PerformanceMetrics.AdjustedSharpeRatio, Math.Round((double)totalPerformance.PortfolioStatistics.AdjustedSharpeRatio, 3).ToStringInvariant() },
{ PerformanceMetrics.LossRate, Math.Round(totalPerformance.PortfolioStatistics.LossRate.SafeMultiply100()).ToStringInvariant() + "%" },
{ PerformanceMetrics.WinRate, Math.Round(totalPerformance.PortfolioStatistics.WinRate.SafeMultiply100()).ToStringInvariant() + "%" },
{ PerformanceMetrics.ProfitLossRatio, Math.Round(totalPerformance.PortfolioStatistics.ProfitLossRatio, 2).ToStringInvariant() },
diff --git a/Tests/Common/Statistics/AdjustedSharpeRatioTests.cs b/Tests/Common/Statistics/AdjustedSharpeRatioTests.cs
new file mode 100644
index 000000000000..fc18ee8e7aad
--- /dev/null
+++ b/Tests/Common/Statistics/AdjustedSharpeRatioTests.cs
@@ -0,0 +1,170 @@
+/*
+ * 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.Statistics;
+using NUnit.Framework;
+using QuantConnect.Statistics;
+
+namespace QuantConnect.Tests.Common.Statistics
+{
+ [TestFixture]
+ public class AdjustedSharpeRatioTests
+ {
+ [Test]
+ public void ZeroSharpeRatioReturnsZero()
+ {
+ var performance = new List { 0.01, -0.02, 0.015, -0.005 };
+ var result = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, 0.0, 252.0);
+ Assert.AreEqual(0.0, result);
+
+ var decimalResult = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, 0m, 252);
+ Assert.AreEqual(0m, decimalResult);
+ }
+
+ [Test]
+ public void LessThanThreeSamplesReturnsZero()
+ {
+ var singleSample = new List { 0.05 };
+ var twoSamples = new List { 0.01, 0.02 };
+
+ Assert.AreEqual(0.0, QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(singleSample, 0.0, 252.0));
+ Assert.AreEqual(0.0, QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(twoSamples, 0.0, 252.0));
+ Assert.AreEqual(0m, QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(twoSamples, 0m, 252));
+ }
+
+ [Test]
+ public void MatchesPezierWhiteFormulaAtConsistentFrequency()
+ {
+ var performance = new List { 0.01, 0.02, -0.005, 0.015, -0.01, 0.03, -0.02 };
+ var tradingDaysPerYear = 252.0;
+ var riskFreeRate = 0.0;
+
+ var observedSharpeRatio = QuantConnect.Statistics.Statistics.ObservedSharpeRatio(performance, riskFreeRate);
+ var skewness = performance.Skewness();
+ var excessKurtosis = performance.Kurtosis(); // Excess kurtosis from MathNet (K - 3)
+
+ var expectedPeriodAsr = observedSharpeRatio * (1.0 + (skewness / 6.0) * observedSharpeRatio - (excessKurtosis / 24.0) * Math.Pow(observedSharpeRatio, 2));
+ var expectedAnnualizedAsr = expectedPeriodAsr * Math.Sqrt(tradingDaysPerYear);
+
+ var actual = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, riskFreeRate, tradingDaysPerYear);
+ Assert.AreEqual(expectedAnnualizedAsr, actual, 1e-10);
+
+ var actualDecimal = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, (decimal)riskFreeRate, (int)tradingDaysPerYear);
+ Assert.AreEqual((decimal)expectedAnnualizedAsr, actualDecimal);
+ }
+
+ [Test]
+ public void AnnualizedSharpeOverloadMatchesDirectCalculation()
+ {
+ var performance = new List { 0.005, 0.012, -0.003, 0.008, -0.006, 0.015, -0.004 };
+ var tradingDaysPerYear = 252;
+ var riskFreeRate = 0.0;
+
+ var observedSharpe = QuantConnect.Statistics.Statistics.ObservedSharpeRatio(performance, riskFreeRate);
+ var annualizedSharpe = observedSharpe * Math.Sqrt(tradingDaysPerYear);
+
+ var directAsr = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, riskFreeRate, tradingDaysPerYear);
+ var scaledOverloadAsr = QuantConnect.Statistics.Statistics.AdjustedSharpeRatioFromAnnualized(performance, annualizedSharpe, tradingDaysPerYear);
+ var scaledOverloadDecimal = QuantConnect.Statistics.Statistics.AdjustedSharpeRatioFromAnnualized(performance, (decimal)annualizedSharpe, (int)tradingDaysPerYear);
+
+ Assert.AreEqual(directAsr, scaledOverloadAsr, 1e-10);
+ Assert.AreEqual((decimal)directAsr, scaledOverloadDecimal);
+ }
+
+ [Test]
+ public void FrequencyConsistencyAcrossSamplingHorizons()
+ {
+ // Simulate daily return series
+ var random = new Random(42);
+ var dailyReturns = new List();
+ for (var i = 0; i < 2520; i++)
+ {
+ // Mixture of normal to introduce mild skewness and kurtosis
+ var u1 = random.NextDouble();
+ var u2 = random.NextDouble();
+ var z = Math.Sqrt(-2.0 * Math.Log(1.0 - u1)) * Math.Cos(2.0 * Math.PI * u2);
+ var r = (random.NextDouble() < 0.05) ? (0.0005 + 0.03 * z - 0.01) : (0.0005 + 0.01 * z);
+ dailyReturns.Add(r);
+ }
+
+ // Aggregate into 21-day (monthly) returns
+ var monthlyReturns = new List();
+ var chunkSize = 21;
+ for (var i = 0; i < dailyReturns.Count; i += chunkSize)
+ {
+ var chunk = dailyReturns.Skip(i).Take(chunkSize);
+ var compounded = chunk.Aggregate(1.0, (acc, val) => acc * (1.0 + val)) - 1.0;
+ monthlyReturns.Add(compounded);
+ }
+
+ // Baseline unadjusted annualized Sharpe ratio should be consistent across horizons (tight tolerance)
+ var dailySharpe = QuantConnect.Statistics.Statistics.SharpeRatio(dailyReturns, 0.0, 252.0);
+ var monthlySharpe = QuantConnect.Statistics.Statistics.SharpeRatio(monthlyReturns, 0.0, 252.0 / chunkSize);
+ Assert.AreEqual(dailySharpe, monthlySharpe, 0.05);
+
+ var dailyAsr = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(dailyReturns, 0.0, 252.0);
+ var monthlyAsr = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(monthlyReturns, 0.0, 252.0 / chunkSize);
+
+ // Both frequencies estimate annualized ASR consistently without being distorted by unscaled daily kurtosis.
+ // Note: 0.15 tolerance accounts for sample estimation noise in higher moments (skewness and heavy-tailed kurtosis) across 120 monthly points.
+ Assert.AreEqual(dailyAsr, monthlyAsr, 0.15);
+ }
+
+ [Test]
+ public void NegativeSkewPenalizesSharpeRatio()
+ {
+ // Negatively skewed returns: frequent small gains and occasional large drawdowns
+ var performance = new List { 0.01, 0.012, 0.009, 0.011, 0.01, 0.013, -0.08 };
+ var skewness = performance.Skewness();
+ Assert.Less(skewness, 0);
+
+ var observedSharpe = QuantConnect.Statistics.Statistics.ObservedSharpeRatio(performance, 0.0);
+ var annualizedSharpe = observedSharpe * Math.Sqrt(252);
+ var asr = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, 0.0, 252.0);
+
+ Assert.Less(asr, annualizedSharpe);
+ }
+
+ [Test]
+ public void PositiveSkewIncreasesSharpeRatio()
+ {
+ // Positively skewed returns: frequent small losses/flat and occasional big right-tail wins
+ var performance = new List { -0.002, -0.001, 0.001, -0.003, -0.001, 0.002, 0.08 };
+ var skewness = performance.Skewness();
+ Assert.Greater(skewness, 0);
+
+ var observedSharpe = QuantConnect.Statistics.Statistics.ObservedSharpeRatio(performance, 0.0);
+ var annualizedSharpe = observedSharpe * Math.Sqrt(252);
+ var asr = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, 0.0, 252.0);
+
+ Assert.Greater(asr, annualizedSharpe);
+ }
+
+ [Test]
+ public void HandlesNaNAndInfinityGracefully()
+ {
+ var constantPerformance = new List { 0.01, 0.01, 0.01, 0.01 };
+ // Zero variance results in NaN for skewness / kurtosis
+ Assert.DoesNotThrow(() =>
+ {
+ var result = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(constantPerformance, 0.0, 252.0);
+ Assert.AreEqual(0.0, result);
+ });
+ }
+ }
+}
diff --git a/Tests/Common/Statistics/PortfolioStatisticsTests.cs b/Tests/Common/Statistics/PortfolioStatisticsTests.cs
index cf136c78a9ad..3342a6ad4c2c 100644
--- a/Tests/Common/Statistics/PortfolioStatisticsTests.cs
+++ b/Tests/Common/Statistics/PortfolioStatisticsTests.cs
@@ -116,11 +116,13 @@ public void SharpeRatioAndProbabilisticSharpeRatioStayConsistent()
var grossStatistics = BuildStatistics(0m);
Assert.Greater(grossStatistics.SharpeRatio, 0m);
Assert.Greater(grossStatistics.ProbabilisticSharpeRatio, 0.5m);
+ Assert.AreEqual(QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, 0.0, _tradingDaysPerYear).SafeDecimalCast(), grossStatistics.AdjustedSharpeRatio);
// A risk-free rate above the return turns the Sharpe ratio negative, and the PSR drops with it
var excessStatistics = BuildStatistics(0.068m);
Assert.Less(excessStatistics.SharpeRatio, 0m);
Assert.Less(excessStatistics.ProbabilisticSharpeRatio, 0.1m);
+ Assert.AreEqual(QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, (double)0.068m / _tradingDaysPerYear, _tradingDaysPerYear).SafeDecimalCast(), excessStatistics.AdjustedSharpeRatio);
}
[Test]