From 87092a5dbf0c4f9f19d9264845935c6a4908e51a Mon Sep 17 00:00:00 2001 From: abhi-byte62 Date: Tue, 6 Oct 2026 23:07:33 +0530 Subject: [PATCH 1/3] feature-9813: Add Adjusted Sharpe Ratio (Pezier and White) to portfolio statistics --- .../Api/OptimizationBacktestJsonConverter.cs | 1 + Common/Statistics/PerformanceMetrics.cs | 6 + Common/Statistics/PortfolioStatistics.cs | 8 ++ Common/Statistics/Statistics.cs | 44 +++++++ Common/Statistics/StatisticsBuilder.cs | 1 + .../Statistics/AdjustedSharpeRatioTests.cs | 107 ++++++++++++++++++ .../Statistics/PortfolioStatisticsTests.cs | 2 + 7 files changed, 169 insertions(+) create mode 100644 Tests/Common/Statistics/AdjustedSharpeRatioTests.cs 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..d0818e994311 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, SharpeRatio); 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..7563fe8cc6c8 100644 --- a/Common/Statistics/Statistics.cs +++ b/Common/Statistics/Statistics.cs @@ -236,6 +236,50 @@ public static double ObservedSharpeRatio(List listPerformance, double ri return standardDeviation.IsNaNOrZero() ? 0 : performanceAverage / standardDeviation; } + /// + /// Calculates the Adjusted Sharpe Ratio (Pezier and White, 2006) which adjusts the Sharpe Ratio + /// for skewness and kurtosis of the return distribution. + /// + /// The performance samples to use + /// The annualized Sharpe ratio + /// The adjusted Sharpe ratio + public static double AdjustedSharpeRatio(List listPerformance, double sharpeRatio) + { + if (listPerformance.Count < 3 || sharpeRatio == 0) + { + return 0; + } + + var skewness = listPerformance.Skewness(); + var kurtosis = listPerformance.Kurtosis(); + + if (skewness.IsNaNOrInfinity() || kurtosis.IsNaNOrInfinity()) + { + return 0; + } + + var asr = sharpeRatio * (1.0d + (skewness / 6.0d) * sharpeRatio - (kurtosis / 24.0d) * Math.Pow(sharpeRatio, 2)); + + if (double.IsNaN(asr) || double.IsInfinity(asr)) + { + return 0; + } + + return asr; + } + + /// + /// Calculates the Adjusted Sharpe Ratio (Pezier and White, 2006) which adjusts the Sharpe Ratio + /// for skewness and kurtosis of the return distribution. + /// + /// The performance samples to use + /// The annualized Sharpe ratio + /// The adjusted Sharpe ratio + public static decimal AdjustedSharpeRatio(List listPerformance, decimal sharpeRatio) + { + return AdjustedSharpeRatio(listPerformance, (double)sharpeRatio).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..dfc3f73791aa --- /dev/null +++ b/Tests/Common/Statistics/AdjustedSharpeRatioTests.cs @@ -0,0 +1,107 @@ +/* + * 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 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); + Assert.AreEqual(0.0, result); + + var decimalResult = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, 0m); + 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, 1.5)); + Assert.AreEqual(0.0, QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(twoSamples, 1.5)); + Assert.AreEqual(0m, QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(twoSamples, 1.5m)); + } + + [Test] + public void MatchesPezierWhiteFormula() + { + var performance = new List { 0.01, 0.02, -0.005, 0.015, -0.01, 0.03, -0.02 }; + var sr = 1.8; + + var skewness = performance.Skewness(); + var kurtosis = performance.Kurtosis(); + var expected = sr * (1.0 + (skewness / 6.0) * sr - (kurtosis / 24.0) * Math.Pow(sr, 2)); + + var actual = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, sr); + Assert.AreEqual(expected, actual, 1e-10); + + var actualDecimal = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, (decimal)sr); + Assert.AreEqual((decimal)expected, actualDecimal); + } + + [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 sr = 1.5; + + var skewness = performance.Skewness(); + Assert.Less(skewness, 0); + + var asr = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, sr); + // With negative skew and positive kurtosis penalty, ASR must be strictly less than SR + Assert.Less(asr, sr); + } + + [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 sr = 0.8; + + var skewness = performance.Skewness(); + Assert.Greater(skewness, 0); + + var asr = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, sr); + Assert.Greater(asr, sr); + } + + [Test] + public void HandlesNaNAndInfinityGracefully() + { + var constantPerformance = new List { 0.01, 0.01, 0.01, 0.01 }; + // Skewness and kurtosis on constant values may be NaN due to zero variance + Assert.DoesNotThrow(() => + { + var result = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(constantPerformance, 1.0); + Assert.AreEqual(0.0, result); + }); + } + } +} diff --git a/Tests/Common/Statistics/PortfolioStatisticsTests.cs b/Tests/Common/Statistics/PortfolioStatisticsTests.cs index cf136c78a9ad..38084d61999b 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, grossStatistics.SharpeRatio), 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, excessStatistics.SharpeRatio), excessStatistics.AdjustedSharpeRatio); } [Test] From e0d8bde321b8dbc3f8f32009c53dfe4422da6a8a Mon Sep 17 00:00:00 2001 From: abhi-byte62 Date: Thu, 8 Oct 2026 14:22:34 +0530 Subject: [PATCH 2/3] feature-9813: Fix frequency scaling and excess kurtosis in Adjusted Sharpe Ratio --- Common/Statistics/PortfolioStatistics.cs | 2 +- Common/Statistics/Statistics.cs | 97 ++++++++++++++--- .../Statistics/AdjustedSharpeRatioTests.cs | 103 ++++++++++++++---- .../Statistics/PortfolioStatisticsTests.cs | 4 +- 4 files changed, 163 insertions(+), 43 deletions(-) diff --git a/Common/Statistics/PortfolioStatistics.cs b/Common/Statistics/PortfolioStatistics.cs index d0818e994311..41ef2d76513a 100644 --- a/Common/Statistics/PortfolioStatistics.cs +++ b/Common/Statistics/PortfolioStatistics.cs @@ -299,7 +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, SharpeRatio); + 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 7563fe8cc6c8..ca3d5cacb23b 100644 --- a/Common/Statistics/Statistics.cs +++ b/Common/Statistics/Statistics.cs @@ -237,28 +237,92 @@ public static double ObservedSharpeRatio(List listPerformance, double ri } /// - /// Calculates the Adjusted Sharpe Ratio (Pezier and White, 2006) which adjusts the Sharpe Ratio - /// for skewness and kurtosis of the return distribution. + /// 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 annualized Sharpe ratio - /// The adjusted Sharpe ratio - public static double AdjustedSharpeRatio(List listPerformance, double sharpeRatio) + /// 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 || sharpeRatio == 0) + 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 kurtosis = listPerformance.Kurtosis(); + 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 AdjustedSharpeRatio(List listPerformance, double annualizedSharpeRatio, double tradingDaysPerYear) + { + if (listPerformance.Count < 3 || annualizedSharpeRatio == 0 || tradingDaysPerYear <= 0) + { + return 0; + } - if (skewness.IsNaNOrInfinity() || kurtosis.IsNaNOrInfinity()) + var skewness = listPerformance.Skewness(); + var excessKurtosis = listPerformance.Kurtosis(); // MathNet returns excess kurtosis (K - 3) + + if (skewness.IsNaNOrInfinity() || excessKurtosis.IsNaNOrInfinity()) { return 0; } - var asr = sharpeRatio * (1.0d + (skewness / 6.0d) * sharpeRatio - (kurtosis / 24.0d) * Math.Pow(sharpeRatio, 2)); + // 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)) { @@ -269,15 +333,16 @@ public static double AdjustedSharpeRatio(List listPerformance, double sh } /// - /// Calculates the Adjusted Sharpe Ratio (Pezier and White, 2006) which adjusts the Sharpe Ratio - /// for skewness and kurtosis of the return distribution. + /// 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 performance samples to use - /// The annualized Sharpe ratio - /// The adjusted Sharpe ratio - public static decimal AdjustedSharpeRatio(List listPerformance, decimal sharpeRatio) + /// 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 AdjustedSharpeRatio(List listPerformance, decimal annualizedSharpeRatio, int tradingDaysPerYear) { - return AdjustedSharpeRatio(listPerformance, (double)sharpeRatio).SafeDecimalCast(); + return AdjustedSharpeRatio(listPerformance, (double)annualizedSharpeRatio, (double)tradingDaysPerYear).SafeDecimalCast(); } /// diff --git a/Tests/Common/Statistics/AdjustedSharpeRatioTests.cs b/Tests/Common/Statistics/AdjustedSharpeRatioTests.cs index dfc3f73791aa..e04ebead7092 100644 --- a/Tests/Common/Statistics/AdjustedSharpeRatioTests.cs +++ b/Tests/Common/Statistics/AdjustedSharpeRatioTests.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; +using System.Linq; using MathNet.Numerics.Statistics; using NUnit.Framework; using QuantConnect.Statistics; @@ -28,10 +29,10 @@ public class AdjustedSharpeRatioTests public void ZeroSharpeRatioReturnsZero() { var performance = new List { 0.01, -0.02, 0.015, -0.005 }; - var result = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, 0.0); + var result = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, 0.0, 252.0); Assert.AreEqual(0.0, result); - var decimalResult = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, 0m); + var decimalResult = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, 0m, 252); Assert.AreEqual(0m, decimalResult); } @@ -41,26 +42,79 @@ 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, 1.5)); - Assert.AreEqual(0.0, QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(twoSamples, 1.5)); - Assert.AreEqual(0m, QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(twoSamples, 1.5m)); + 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 MatchesPezierWhiteFormula() + public void MatchesPezierWhiteFormulaAtConsistentFrequency() { var performance = new List { 0.01, 0.02, -0.005, 0.015, -0.01, 0.03, -0.02 }; - var sr = 1.8; + var tradingDaysPerYear = 252.0; + var riskFreeRate = 0.0; + var observedSharpeRatio = QuantConnect.Statistics.Statistics.ObservedSharpeRatio(performance, riskFreeRate); var skewness = performance.Skewness(); - var kurtosis = performance.Kurtosis(); - var expected = sr * (1.0 + (skewness / 6.0) * sr - (kurtosis / 24.0) * Math.Pow(sr, 2)); + var excessKurtosis = performance.Kurtosis(); // Excess kurtosis from MathNet (K - 3) - var actual = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, sr); - Assert.AreEqual(expected, actual, 1e-10); + var expectedPeriodAsr = observedSharpeRatio * (1.0 + (skewness / 6.0) * observedSharpeRatio - (excessKurtosis / 24.0) * Math.Pow(observedSharpeRatio, 2)); + var expectedAnnualizedAsr = expectedPeriodAsr * Math.Sqrt(tradingDaysPerYear); - var actualDecimal = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, (decimal)sr); - Assert.AreEqual((decimal)expected, actualDecimal); + 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.AdjustedSharpeRatio(performance, annualizedSharpe, tradingDaysPerYear); + + Assert.AreEqual(directAsr, scaledOverloadAsr, 1e-10); + } + + [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); + } + + 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 + Assert.AreEqual(dailyAsr, monthlyAsr, 0.15); } [Test] @@ -68,14 +122,14 @@ 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 sr = 1.5; - var skewness = performance.Skewness(); Assert.Less(skewness, 0); - var asr = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, sr); - // With negative skew and positive kurtosis penalty, ASR must be strictly less than SR - Assert.Less(asr, sr); + 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] @@ -83,23 +137,24 @@ 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 sr = 0.8; - var skewness = performance.Skewness(); Assert.Greater(skewness, 0); - var asr = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, sr); - Assert.Greater(asr, sr); + 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 }; - // Skewness and kurtosis on constant values may be NaN due to zero variance + // Zero variance results in NaN for skewness / kurtosis Assert.DoesNotThrow(() => { - var result = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(constantPerformance, 1.0); + 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 38084d61999b..3342a6ad4c2c 100644 --- a/Tests/Common/Statistics/PortfolioStatisticsTests.cs +++ b/Tests/Common/Statistics/PortfolioStatisticsTests.cs @@ -116,13 +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, grossStatistics.SharpeRatio), grossStatistics.AdjustedSharpeRatio); + 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, excessStatistics.SharpeRatio), excessStatistics.AdjustedSharpeRatio); + Assert.AreEqual(QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, (double)0.068m / _tradingDaysPerYear, _tradingDaysPerYear).SafeDecimalCast(), excessStatistics.AdjustedSharpeRatio); } [Test] From ac16f59fe19bfa1816bfe98faa978baaf6c544ab Mon Sep 17 00:00:00 2001 From: abhi-byte62 Date: Thu, 8 Oct 2026 19:06:05 +0530 Subject: [PATCH 3/3] fix(statistics): disambiguate annualized AdjustedSharpeRatio overload method name --- Common/Statistics/Statistics.cs | 6 +++--- Tests/Common/Statistics/AdjustedSharpeRatioTests.cs | 12 ++++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Common/Statistics/Statistics.cs b/Common/Statistics/Statistics.cs index ca3d5cacb23b..d53283fa0fe0 100644 --- a/Common/Statistics/Statistics.cs +++ b/Common/Statistics/Statistics.cs @@ -302,7 +302,7 @@ public static decimal AdjustedSharpeRatio(List listPerformance, decimal /// The annualized Sharpe ratio /// The number of trading days per year /// The annualized adjusted Sharpe ratio - public static double AdjustedSharpeRatio(List listPerformance, double annualizedSharpeRatio, double tradingDaysPerYear) + public static double AdjustedSharpeRatioFromAnnualized(List listPerformance, double annualizedSharpeRatio, double tradingDaysPerYear = 252) { if (listPerformance.Count < 3 || annualizedSharpeRatio == 0 || tradingDaysPerYear <= 0) { @@ -340,9 +340,9 @@ public static double AdjustedSharpeRatio(List listPerformance, double an /// The annualized Sharpe ratio /// The number of trading days per year /// The annualized adjusted Sharpe ratio - public static decimal AdjustedSharpeRatio(List listPerformance, decimal annualizedSharpeRatio, int tradingDaysPerYear) + public static decimal AdjustedSharpeRatioFromAnnualized(List listPerformance, decimal annualizedSharpeRatio, int tradingDaysPerYear) { - return AdjustedSharpeRatio(listPerformance, (double)annualizedSharpeRatio, (double)tradingDaysPerYear).SafeDecimalCast(); + return AdjustedSharpeRatioFromAnnualized(listPerformance, (double)annualizedSharpeRatio, (double)tradingDaysPerYear).SafeDecimalCast(); } /// diff --git a/Tests/Common/Statistics/AdjustedSharpeRatioTests.cs b/Tests/Common/Statistics/AdjustedSharpeRatioTests.cs index e04ebead7092..fc18ee8e7aad 100644 --- a/Tests/Common/Statistics/AdjustedSharpeRatioTests.cs +++ b/Tests/Common/Statistics/AdjustedSharpeRatioTests.cs @@ -79,9 +79,11 @@ public void AnnualizedSharpeOverloadMatchesDirectCalculation() var annualizedSharpe = observedSharpe * Math.Sqrt(tradingDaysPerYear); var directAsr = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, riskFreeRate, tradingDaysPerYear); - var scaledOverloadAsr = QuantConnect.Statistics.Statistics.AdjustedSharpeRatio(performance, annualizedSharpe, 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] @@ -110,10 +112,16 @@ public void FrequencyConsistencyAcrossSamplingHorizons() 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 + // 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); }