Sharpe Ratio

Jul 11, 2026

Pseudo-Statistics and Quantitative Charlatanism: The Effects of Reality on the Deflated Sharpe Ratio.

It is commonplace to observe that developing profitable investment strategies is difficult. Quant strategists are often driven to search over a large number of potential strategies, backtesting hundreds or thousands before selecting a potential candidate for trading. Given the relatively short history of available relevant data, quants often have to reason about their selected strategy using the same data used to select the strategy, a sure recipe for all kinds of biases and suboptimal decisions.

I have studied this problem quite a bit, and written about it elsewhere. In my research I have come across the "Deflated Sharpe Ratio" more than once, but had never taken it seriously. From reading the abstract of the original paper I thought it was just some kind of heuristic, instead of a statistical procedure. However, it has blossomed out into a small constellation of papers, with the intersected authorship consisting solely of Marcos López de Prado, Risk.net's 2021 Buy-Side Quant of the Year. As of writing this blog post the original paper has over 270 citations on google scholar, not all of them self-citations, with over 90 in the first half of 2026 alone, and a more extensive Wikipedia entry than the Sharpe ratio. The deflated Sharpe ratio has lodged itself in the firmament of quantitative finance, but how does it work and what does it do?

At a high level, the deflated Sharpe ratio procedure imagines that one observes historical performance of a large number of strategies. We can decompose historical performance as a sum of skill and luck. One then selects the strategy with the best in-sample performance, then performs some kind of inference on the skill of that strategy. In this case the skill can be quantified by the population Sharpe ratio (what I call the "signal-noise ratio"). One wants a strategy with high skill, since you cannot control luck: high skill leads to the best expected performance out-of-sample.

In our case the luck is the noise of the in-sample Sharpe ratio around the signal-noise ratio. The magnitude of this noise is controlled mostly by the length of the backtest period, and is approximately $1/T$ where $T$ days of backtested returns are observed.

The setup to the original DSR paper imagines that the distribution of sample Sharpe ratios is Gaussian with mean zero and variance $\sigma_s^2 + \sigma_l^2$, where $\sigma_s^2$ is the variance of the skill part, and $\sigma_l^2$ is the variance of the luck part, equal to around $1/T$. This is a simplified representation of the DSR procedure, ignoring some corrections for skew and kurtosis of returns, which will have no bearing on our analysis.

The DSR procedure is built on the "False Strategy Theorem" which gives a formula for the asymptotic expected value of the maximum of $n$ i.i.d. standard Gaussian random variables. We can think of this as just a function of $n$, call it $f(n)$. Now let $\hat{\zeta}_i$ be the observed Sharpe ratios of the $i$th backtest, and suppose we reorder them so that $\hat{\zeta}_n$ is the largest in-sample value. We are trying to perform inference on the skill of this strategy. The DSR is basically computed as $$ DSR = \Phi\left(\frac{\hat{\zeta}_n - \sqrt{\sigma_s^2 + \sigma_l^2}f(n)}{\sigma_l}\right). $$ That is, we subtract the (approximate) expected value of the maximum of skill plus luck, divide by the spread of the luck, then feed the result through the CDF of the normal distribution.

In the original 2014 paper, the authors allude to the DSR being used as a statistical procedure. They give a number of example computations, and talk about rejecting a null hypothesis based on whether the computed DSR exceeds 0.95 or not. (A larger value of the DSR apparent casts doubt on the null hypothesis.) The 2018 paper by López de Prado and Lewis defines the DSR as "the probability that the true SR exceeds a user-defined benchmark level $SR^*$, where that level is adjusted to reflect the multiplicity of trials." Ignoring that frequentists do not assign probabilities to underlying parameters, the intention is fairly clear that DSR is to be interpreted as a p-value.

Wait, What?

I had a number of concerns after reading this paper. Among them:

  1. The assumption that sample Sharpe ratios are normally distributed seems indefensible in practice, and is likely load-bearing. There are two issues here. Even if one assumes that returns are well behaved (independent Gaussian, say), the Sharpe ratio is fat-tailed. The distributional shape of skill will not change this. Because the "False Strategy Theorem" is based on asymptotics of the normal distribution, and because we are looking at extremal values, the fat-tailedness of the sample Sharpes is likely to break the expected value used in the DSR computation. The expectation subtracted in the numerator of DSR is probably wrong.

    The other issue with this assumption is that skill is unlikely to be normally distributed, which is pretty much required if skill plus luck is normally distributed. Quants tend to test small variations on a number of different heterogenous strategies. Normality seems like a very poor model for how skill is distributed. The authors of the original paper ignore this issue, but it seems like it is addressed in the followup paper by López de Prado and Lewis. Needless to say the followup paper requires some results from the original paper that are demonstrably wrong.

  2. The definition of the DSR is quite odd. The numerator $\hat{\zeta}_n - \sqrt{\sigma_s^2 + \sigma_l^2}f(n)$ should be approximately zero mean if the expected value of the sample Sharpe ratios is zero, but larger otherwise. How much larger is hard to say, but it does not seem that it should depend only on $\sigma_l$, which is what we divide it by. In fact, it seems that the intent is to measure how many "luck units" away we are from what we suspect should be zero. The problem is that the maximal sample Sharpe $\hat{\zeta}_n$ has both skill and luck in it, and we cannot just pretend that we maximized over skill and then had luck-sized noise on top of that.

  3. This also raises the issue of what null exactly is putatively being tested here. The authors do not state this clearly, in either the original 2014 paper or the 2018 followup. There are a few obvious choices: the unconditional null, which is that the expected value of all the sample Sharpe ratios is zero; and the conditional null which is that the skill of the selected strategy is zero, symbolically $\zeta_n = 0$, where $n$ is the index of the strategy with the largest observed sample Sharpe ratio. (We do not know, or exactly care, if $\zeta_n$ is the largest of the skill values.) Lastly there is a conditional familywise null which is that the skill of all strategies is zero (or non-positive), symbolically $\zeta_i = 0$ for all $i$. We suspect the authors intended to test the conditional null, as the other two nulls can be tested with standard MHT corrections like Bonferroni. The two other nulls also do not seem super useful, since they are not strictly about the skill of the selected strategy.

  4. You might be wondering how the authors proved that the DSR is uniform under the null hypothesis if they did not state the null hypothesis. I regret to inform there is no proof of this statement. Instead, the efficacy of DSR for controlling type I errors rests on the reputation of the authors and the editors of the Journal of Portfolio Management.

Disproving Uniformity of the DSR

The fact that I could not immediately grasp the justification for the DSR is not real cause for concern. After all, there are many, many things I do not understand. However the absence of a proof, or even a hand-wave justification, is troubling from my point of view. The authors and editors did not seem to think a proof was necessary.

I now believe that no proof was offered because there is no proof, as the statement is clearly false. Consider the simplified case where there is no spread in skill, $\sigma_s=0$. In this case all strategies have the same skill which we assume to be zero. (Note that if this were true there would be no reason to select among the strategies, but this is the limiting case where spread of luck is much larger than spread of skill.) The three nulls happen to coincide here, so it does not matter which one the original authors intended. Suppose that $\sigma_l=1$ by choice of units, so that DSR is defined as $\Phi\left(\hat{\zeta}_n - f(n)\right)$. For large $n$ the maximum of $n$ i.i.d. Gaussians tends to a Gumbel distribution. This is simply not normally distributed, and the DSR should not be uniform under the null.

We can simulate this very easily. We simulate $n$ independent Gaussians, select the maximal one, subtract the magical value, take the CDF, and get something that should be uniform if we believe the DSR is a p-value. We run this simulation $10^5$ times and plot the empirical CDF of these putative p-values. If the DSR were uniform this would be a straight line segment from $(0,0)$ to $(1,1)$. It clearly is not. Note that we are simulating $n=50000$ strategies here, so the problem is not the asymptotics: these are the asymptotics. In fact, using larger $n$ does not improve the situation. This is evident from other testing we have done and is obvious from the asymptotics, since the variance of the maximum of $n$ Gaussians is shrinking in $n$. This even figures into the 2014 paper of this constellation, but apparently did not occur to authors of the DSR paper.

exp_max_sr <- function(N) {
    mascher <- 0.5772156649
    (1 - mascher) * qnorm((1/N), lower.tail=FALSE) + mascher * qnorm(exp(-1)/N, lower.tail=FALSE)
}
nstrat <- 50000
limval <- exp_max_sr(nstrat)
set.seed(1234)
simvals <- replicate(1e5,{
  srs <- rnorm(nstrat, mean=0, sd=1)
  dsr <- pnorm(max(srs) - limval)
})
library(tibble)
library(dplyr)
library(ggplot2)
ph <- tibble(sample=simvals) %>%
  ggplot(aes(x=sample)) +
  stat_ecdf() +
  labs(title='Empirical CDF of DSR Values',x='DSR',y='Empirical CDF',caption=paste0('Over 100K simulations of ',nstrat,' tested strategies, with no skill spread.'))
print(ph)

Note that conservatively these simulations require 20 lines of code to execute and plot, and took 15 minutes to write and execute. That the original authors never bothered to check this is mind-boggling. The first DSR paper does contain simulations, but only to check how the mean value of $\hat{\zeta}_n$ compares to the asymptotic value as one varies $n$. While this is an interesting question, it rather misses the mark. The problem with DSR, as it turns out, is not that $f(n)$ is inaccurate for small $n$; rather the issue is that the DSR does not make any sense.

As one can see from the plot above, in some situations we should expect that the probability that DSR exceeds $1-\alpha$ will often be much smaller than $\alpha$. To illustrate the issue, we simulate using numbers from the original DSR paper. As in the original paper, we assume 250 trading days per year, let $\sigma_s^2 + \sigma_l^2 = 0.5$ in "annualized units", let $T = 1250$ days (5 years at this cadence), assume returns are normally distributed, and consider either $n = 88$ or $n = 89$. First we write a DSR function and confirm that if $\hat{\zeta}_n=2.5$ in annualized units that this DSR function spits out 0.9505 when $n = 88$, as in the original paper.

exp_max_sr <- function(N) {
    mascher <- 0.5772156649
    (1 - mascher) * qnorm((1/N), lower.tail=FALSE) + mascher * qnorm(exp(-1)/N, lower.tail=FALSE)
}
dsr <- function(sr, T, N, Vsrs, Esrs=0, gamma3=0, gamma4=3, ...) {
    sr0 <- Esrs + sqrt(Vsrs) * exp_max_sr(N)
    serr <- sqrt((1 - gamma3 * sr + (gamma4 - 1)/4*sr**2) / (T-1))
    pnorm((sr - sr0) / serr, ...)
}

dpy <- 250
zeta_n <- 2.5
T <- 1250
n <- 88
sigmasq_tot <- 0.5
# compute and print
print(dsr(zeta_n / sqrt(dpy), T=T, N=n, Vsrs=sigmasq_tot / dpy))
[1] 0.950491
print(dsr(zeta_n / sqrt(dpy), T=T, N=n+1, Vsrs=sigmasq_tot / dpy))
[1] 0.949839

I indeed get 0.9505, as in the original paper. Moreover, when $n=89$, the DSR falls slightly under 0.95 and, if we are to believe the narrative of the original paper, a conscientious investor would fail to reject the null and not invest in the strategy. Our code matches the code in the paper by visual inspection and spits out the same values, and we can proceed.

Now we will perform a number of simulations with these values of $T, n$ and noise. In each of them we will first draw from normally distributed skill, then add some amount of luck to that. We will select the "strategy" with the highest combined skill plus luck. We will compute the DSR on the simulated Sharpe of this strategy. We then get the actual skill and the DSR value. This will allow us to test the conditional null by focusing on cases where the actual skill is either less than zero, or very close to it.

Below we perform the simulations under these values of $T, n, \sigma^2_s$.

#' perform a number of simulations of normal skill plus normal luck,
#' compute the DSR on the results, return the SNR of the strategy
#' selected for having maximal sharpe, along with that maximal sharpe and the DSR statistic.
sim_machine <- function(T, n, sigmasq_tot, nsims=100000, dpy=250) {
  require(tibble,quietly=TRUE)
  sigma_luck_day <- sqrt(1/T)
  sigma_tot_day <- sqrt(sigmasq_tot / dpy)
  sigma_skill_day <- sqrt(sigma_tot_day^2 - sigma_luck_day^2)

  simvals <- replicate(nsims, {
    skills <- rnorm(n, mean=0, sd=sigma_skill_day)
    srs <- rnorm(length(skills), mean=skills, sd=sigma_luck_day)
    maxsr <- max(srs)
    nidx <- order(srs, decreasing=TRUE)[1]
    selskill <- skills[nidx]
    dsrv <- dsr(maxsr, T, N=n, Vsrs=sigma_tot_day^2)
    c(skill=selskill, dsr=dsrv, maxsr=maxsr)
  })
  as_tibble(t(simvals))
}

T <- 1250
n <- 88
sigmasq_tot <- 0.5
set.seed(1234)
results <- sim_machine(T=T, n=n, sigmasq_tot=sigmasq_tot, nsims=1e6)

Now we compute the empirical rejection rates. To avoid "bottoming out", we pretend we are interested in a type I rate of 0.10, so our decision threshold is to reject when $DSR > 0.9$. Here we tabulate the empirical rejection proportion for the entire simulation, which is under the unconditional null, as well as for those small number (less than 1%!) of cases where the skill of the selected strategy is non-positive, which is the conditional null. We compute 95% confidence intervals on the rejection rate based on our simulations, and tabulate these below.

We see only one rejection at the nominal 0.10 level under the conditional null, which we observe over 3000 times in 1 million simulations. For the conditional null the actual rejection rate is around 100 times smaller than the nominal 0.10 rate promised by the DSR. Under the unconditional null the empirical rejection rate is more like 4%, not the nominal 10%. We note, however that under this unconditional null there are many cases where the skill of the selected strategy is quite large, larger than 1.5 in annualized units. And yet the rejection rate is still small.

#' estimate the rejection rates along with confidence intervals on same
#' assumes the skill has already been grouped into skill_group variable.
compute_rejrates <- function(results, conf.level=0.95, dsr_thresh=0.95) {
  results %>%
    group_by(skill_group) %>%
      summarize(Rejections=sum(dsr > dsr_thresh), Total=n()) %>%
    ungroup() %>%
    mutate(`Rejection Proportion`=Rejections/Total) %>%
    mutate(conf.lo=mapply(function(x, n)  {prop.test(x, n, conf.level=conf.level)$conf.int[1]}, Rejections, Total)) %>%
    mutate(conf.hi=mapply(function(x, n)  {prop.test(x, n, conf.level=conf.level)$conf.int[2]}, Rejections, Total))
}
library(knitr)
bind_rows(results %>%
  mutate(skill_group="unconditional null"),
results %>%
  dplyr::filter(skill < 0) %>%
  mutate(skill_group="conditional null")) %>%
  compute_rejrates(dsr_thresh=0.9) %>%
  rename(`Group`=skill_group) %>%
  knitr::kable(caption='Empirical rejection rates for simulations using 0.9 DSR threshold.', format='markdown', digits=3)

Table: Empirical rejection rates for simulations using 0.9 DSR threshold.

Group Rejections Total Rejection Proportion conf.lo conf.hi
conditional null 1 3361 0.000 0.000 0.002
unconditional null 41627 1000000 0.042 0.041 0.042

To get a feel for how the DSR rejects at the nominal 0.10 level, we group cases by their underlying skill, rounding to the nearest 0.1 in annualized units. We compute the empirical rejection rates of the DSR along with (pointwise, not familywise) 95% confidence intervals, which we plot here. We see that the empirical rejection rate does not exceed the nominal level until the skill is around 1.5 in annualized units. The DSR has apparently very low power when the skill is less than 2 in annualized units.

rejrates <- results %>%
  mutate(skill_group=round(sqrt(250)*skill, 1)) %>%
  compute_rejrates(dsr_thresh=0.90)
ph <- rejrates %>%
  ggplot(aes(skill_group, `Rejection Proportion`, ymin=conf.lo, ymax=conf.hi)) + 
  geom_point() + 
  geom_hline(yintercept=0.1,linetype="dotted") + 
  geom_errorbar(alpha=0.5) +
  labs(x='SNR of Selected Strategy (/sqrt(yr))',
       y='Empirical Rejection Rate',
       title='Empirical Rejection Rates of DSR at Nominal 0.10 Level',
       caption='Over 1 million simulations.')
print(ph)

Steelmanning the DSR

Note that in our simulations we found that the type I rate of DSR seems to be quite smaller than the nominal rate. Toying with the parameters, we have not found cases where the empirical rejection rate exceeds the nominal rate, which is to say we suspect that the DSR is uniformly conservative. It is not uncommon in statistical practice to employ a statistic that depends on some unobserved nuisance parameter. In that case one usually identifies a least favorable configuration, which is some value of the unobserved nuisance parameter at which the type I rate is maximized under the null, then calibrate the procedure to that case. The guarantees of the procedure then become an upper bound on the type I rate. Could this rescue the DSR?

First note that the original authors do not make this claim. López de Prado and Lewis do not make that claim either, instead they use DSR as if it were uniform under the null.

Second, it is easy to establish that the distribution of the DSR under the null depends not on an unobserved nuisance parameter, but on the observed value of $n$. The DSR simply has more spread for smaller $n$. This means that for a fixed value of the skill of the selected strategy, the DSR is more likely to reject for small $n$ than for large $n$. We confirm this experimentally below by repeating the above experiments but setting $n=1000$. We plot the conditional empirical rejection rates at the nominal 0.10 level for both sets of simulations and see a much higher rejection rate for the case where $n=88$. Presumably the "least favorable configuration", then, would be $n=1$, where we do not trust $f(n)$ to be any good. However, this variable rejection rate is going to be a serious problem if one attempts to use the DSR result for different normally distributed clusters of skill, as do López de Prado and Lewis. Their proposed solution is built on very shaky foundations, and I would not trust it at all: it will preferentially surface small clusters over large clusters for no good reason.

results2 <- sim_machine(T=T, n=1000, sigmasq_tot=sigmasq_tot, nsims=1e6)
rejrates2 <- results2 %>%
  mutate(skill_group=round(sqrt(250)*skill, 1)) %>%
  compute_rejrates(dsr_thresh=0.90)
ph <- bind_rows(rejrates %>% mutate(n=88),
          rejrates2 %>% mutate(n=1000)) %>%
  mutate(experiment=paste0("n=",n)) %>%
  ggplot(aes(skill_group, `Rejection Proportion`, ymin=conf.lo, ymax=conf.hi, 
             color=experiment, group=experiment)) +
  geom_point() + 
  geom_hline(yintercept=0.1,linetype="dotted") + 
  geom_errorbar(alpha=0.5) +
  labs(x='SNR of Selected Strategy (/sqrt(yr))',
       y='Empirical Rejection Rate',
       title='Empirical Rejection Rates of DSR at Nominal 0.10 Level',
       color='Strategies Tested',
       caption='Over 1 million simulations each.')
print(ph)

In one last-ditch effort to steelman the DSR, perhaps we should just assume that one always uses it for fixed values of $n$ and $T$, and then performs simulations like the ones I have done here to get the proper threshold value of DSR to reject at a given type I rate. There are so many things wrong with this approach, however:

  1. It requires the user to run a bunch of simulations to fix a broken statistic? Is it not the responsibility of the original authors to do this?
  2. If you fix the $n$ and $T$, then the DSR is monotonic in $\hat{\zeta}_n$. So if you are going to run these simulations, why waste your time with computing $f(n)$ and the normal CDF? Instead just find threshold values for $\hat{\zeta}_n$ under the conditional null you seek to test, you get the same results. Note that this is just the in-sample Sharpe ratio that these authors have spent five plus papers telling us was biased and useless.

No, there is no rescuing the DSR. Stop using it, it makes no sense, and it does not do what it was advertised to do.

Is DSR a Fraud?

Bailey et al ask a similar question in their paper on "backtest overfitting". A fraud is an intentional act of deception or a misrepresentation of facts to gain an unfair advantage. I will stress however, there is no evidence whatsoever of intentional deception here. The DSR is not a fraud, but it is a misrepresentation of the truth. In cases like these I employ Hanlon's razor: "Never attribute to malice that which is adequately explained by ignorance". Writing and publishing the DSR paper(s) without spending even five minutes thinking about whether it is true, or 30 minutes testing it, is in my opinion outright statistical malpractice. The paper should be retracted, and I will present my case to JPM, though I doubt it will be.

References


Appendix: What Should You Do Instead?

This blogpost is not about me, but I have done research on this problem. I would be remiss if I did not point readers to my preprint, which is ostensibly about a procedure which does maintain nominal coverage under the conditional null. The paper snowballed however and also contains several tests for the unconditional null, with some analysis of the false discovery rate, etc. The tests are available in the SharpeR R package as sr_conditional_test and sr_max_test. By no means do I consider the topic closed, however. I think the conditional procedure can be improved to have higher power, and am slowly researching and developing a fix.

Click to read and post comments

Apr 12, 2021

Achieved Signal Noise Ratio via Cross Validation

Some of the research on the problem of "overfitting" of quantitative strategies (including my own) might be better described as research on "over-optimism". That is because the analysis tends to view the strategies as something one stumbles upon, without any in-sample tinkering. If that is the case, the Sharpe ratio is nearly unbiased for the signal-noise ratio, and the only statistical sin is selecting the best strategy among many without some kind of multiple hypothesis test correction. However, strategies tend to be generated based on some in-sample overfitting beyond just selection. As an example, suppose you observed $n$ days of returns on $k$ assets in your universe, then constructed the Markowitz portfolio based on that data. If your strategy is to hold that Markowitz portfolio, it is a bit trickier to de-bias the in-sample Sharpe ratio.

For the specific problem of estimating what I call the achieved signal-noise ratio of the Markowitz portfolio, one can use the Sharpe Ratio Information Criterion of Paulsen and Söhl. However, I suspect most practicing quants would fall back to cross validation. Cross validation is a folk remedy prescribed to cure all ills, probably beneficial but of unknown efficacy. In the usual $m$ fold cross validation, one splits the data into $m$ equally sized pieces, fits the Markowitz portfolio on all but one of those $m$ pieces, then simulates returns on that piece. This is repeated holding out each of the $m$ validation sets. The result is a simulated time series over the $n$ days of data, which one then computes the Sharpe ratio on. I commonly used a fancier version of cross validation, called walk forward, where one only estimated the portfolio on data prior to the validation set, which results in a slightly truncated resultant time series. The $m$-fold and walk-forward cross validation techniques are illustrated below for the case of $m=5$ folds.

Cross Validation is Broken?

Although I used cross-validation in this way for years, I never tested it until now. It seemed "obvious" that it would provide unbiased results. It turns out I was wrong, as simple simulations will show.

In our set up, cross validation is designed to estimate the achieved signal-noise ratio. "Achieved" means we are considering the signal-noise ratio of the sample Markowitz portfolio. This is a quantity that is both random (depends on the sample) and unobservable (depends on the population mean and covariance). In the simulations below I perform $10$-fold regular and walk-forward cross validation, constructing the Markowitz portfolio on training data, then generating a single time series of returns, computing the Sharpe on those returns. I also compute the achieved signal-noise ratio of the Markowitz portfolio on the whole sample. I vary the maximal signal-noise ratio of the population, as well as the number of assets. For every setting of the population parameters I perform 50000 simulations, then average the cross validated Sharpes and the achieved signal-noise ratios over the simulations.

Below I plot the bias of the Sharpes estimated via regular and walk-forward cross validation, defined as the Sharpe minus the achieved signal-noise ratio. We see that, in annual terms, both cross validation techniques are severely biased, and walk-forward gets worse as the maximal signal-noise ratio increases. The bias is negative, which is to say we _under_estimate the achieved signal-noise ratio.

suppressMessages({
  library(dplyr)
  library(tidyr)
  library(tibble)
  library(future.apply)
})

# trading days per year
ope <- 252
# compute markowitz portfolio
comp_mp <- function(X) { solve(cov(X),colMeans(X)) }
# compute sr
comp_sr <- function(x,na.rm=TRUE) { mean(x,na.rm=na.rm) / sd(x,na.rm=na.rm) }
# run simulations
simit <- function(n,k,zeta,nsim=1000,nfolds=10) {
  require(future.apply)
  won <- seq(n)
  cvidx <- (won %% nfolds) + 1
  mu <- zeta / sqrt(k)
  vals <- future_replicate(nsim,{
    X <- matrix(rnorm(n*k,mean=mu),ncol=k)
    kfret <- rep(0,n)
    wfret <- rep(NA_real_,n)
    for (folnum in unique(cvidx)) { 
      oosi <- cvidx==folnum
      oos <- X[oosi,]
      iis <- X[!oosi,]
      mp <- comp_mp(iis)
      kfret[oosi] <- oos %*% mp
      # walk forward
      iisi <- cvidx < folnum
      if (any(iisi)) {
        iis <- X[iisi,]
        mp <- comp_mp(iis)
        wfret[oosi] <- oos %*% mp
      }
    }
    cv_sr <- comp_sr(kfret)
    wf_sr <- comp_sr(wfret,na.rm=TRUE)
    achieved_snr <- sum(mu * mp) / sqrt(sum(mp^2))
    c(cv_sr,wf_sr,achieved_snr,sqrt(ope)*mean(kfret),sd(kfret))
  })
  setNames(data.frame(sqrt(ope) * t(vals)),c('CV','walkforward','achieved','kf_mean','kf_sd'))
}

nday <- ope*5
nsim <- 50000

params <- crossing(tibble(zeta=seq(0.125,1.875,length.out=7)/sqrt(ope)),tibble(k=c(5,10)))

set.seed(1234)
plan(multicore)
resu <- params %>%
  group_by(zeta,k) %>%
    summarize(foo=list(simit(n=nday,k=k,zeta=zeta,nsim=nsim))) %>%
  ungroup() %>%
  unnest(foo)
plan(sequential)
# aggregate the results:
mresu <- resu %>%
  tidyr::gather(key=series,value=value,-zeta,-k,-achieved) %>%
  mutate(bias=value - achieved) %>%
  group_by(zeta,k,series) %>%
    summarize(emp_bias=mean(bias),
              emp_sbias=sd(bias),
              count=n()) %>%
  ungroup() %>%
  mutate(emp_biase=emp_sbias / sqrt(count)) 

ph <- mresu %>%
  dplyr::filter(series %in% c('CV','walkforward')) %>%
  mutate(showser=case_when(series=='CV'  ~ 'cross val. SR',
                           series=='walkforward' ~ 'w.f. cross val. SR',
                           TRUE ~ 'error')) %>%
  ggplot(aes(sqrt(ope)*zeta,emp_bias,
             linetype=showser,
             group=interaction(k,showser))) +
  geom_hline(yintercept=0,linetype=3,alpha=0.8) + 
  geom_point(aes(shape=showser),alpha=0.9) +
  geom_errorbar(aes(ymin=emp_bias - emp_biase,ymax=emp_bias + emp_biase),width=0.1) + 
  geom_line(alpha=0.5) +
  facet_wrap(~k,labeller=label_both) +
  labs(x=expression(zeta['*']~~(yr^{-1/2})),
       y=expression(bias~~(yr^{-1/2})),
       shape='series',
       linetype='series')
print(ph)

Where is the bias?

Despite the relative simplicity of the simulations, I was convinced they contained a bug. How could cross-validation be so broken? I tried increasing the number of folds, which only made the problem worse! In my debugging I noticed that the estimate of the mean return of the Markowitz portfolio seemed unbiased. How can one have an unbiased estimate of the mean, but a biased estimate of the Sharpe ratio? The answer to that question is that the sample mean and sample standard deviation are not independent under cross validation.

We illustrate that here by performing simulations where the population mean is the zero vector. For each simulation we compute the numerator and denominator of the cross-validated Sharpe ratio, namely the mean and standard deviation of the simulated returns on validation sets. We then scatter the means versus the standard deviations. There is a clear correlation here. I believe this is a well-known effect in cross-validation. Moreover, if your standard deviation is positively correlated with your mean, it will clearly bias your Sharpe ratio. This is simple to understand intuitively, we leave it as an exercise to show how that bias is a function of the correlation.

nday <- ope*5
nsim <- 10000

set.seed(1234)
plan(multiprocess)
atz <- simit(n=nday,k=5,zeta=0,nsim=nsim)
plan(sequential)

# scatter em:
ph <- atz %>%
  ggplot(aes(kf_sd,kf_mean)) +
  geom_point(alpha=0.02) +
    stat_smooth() + 
    geom_hline(yintercept=0,linetype=3) + 
    geom_rug(alpha=0.02,sides='rb') + 
  labs(x=expression(sd~of~rets~~(yr^{-1/2})),
       y=expression(mean~of~rets~~(yr^{-1})))
print(ph)

What can be done?

If you intended to trade the Markowitz portfolio, you could probably debias the cross-validated estimates by some mathematical wizardry. However, most quants deploy more complicated trading strategies, which would be harder to analyze. My only suggestion at the moment is to instead use an average of Sharpes approach: perform cross-validation as usual, but instead of computing a single time series of returns, compute the Sharpe ratio on each validation set, then average them. In a future blog post I will show that this has far less bias for our toy problem.

Click to read and post comments

Apr 11, 2021

A Sign Sharpe

Consider the following challenge: estimate the Signal-Noise ratio of an asset given a single observation of returns of that asset, $x$.

The task seems impossible: you can easily estimate the mean, but how are you going to estimate the standard deviation with just one observation? Consider, however, that for most assets the expected value of returns is very small compared to the volatility, so if you wanted to estimate the variance of returns, $x^2$ would be a good guess. Thus $\left|x\right|$ would be a passable estimate for the standard deviation. Given that $x$ is an unbiased estimate of the mean, then $$ \frac{x}{\left|x\right|} = \operatorname{sign}(x) $$ is our candidate estimator of the Signal-Noise ratio.

It turns out this will work, up to scaling. Suppose that returns are Gaussian. Then $$ E\left[ \operatorname{sign}(x) \right] = 2\Phi\left(\frac{\mu}{\sigma}\right) - 1 \approx \sqrt{\frac{2}{\pi}} \frac{\mu}{\sigma}. $$ Thus $\sqrt{\frac{\pi}{2}} \operatorname{sign}(x)$ is a nearly unbiased estimator of the Signal-Noise ratio.

Since this estimator has only two possible values, it is going to be very noisy. However, if you had $n$ observations $x_t$, you could average them together as $$ \check{\zeta} = \frac{1}{n}\sqrt{\frac{\pi}{2}} \sum_{1 \le t \le n}\operatorname{sign}\left(x_t\right). $$ Call this the sign Sharpe ratio. Note that it is just a rescaled version of the cringe-worthy "winning days percentage" statistic some portfolio managers advertise.

There are two obvious problems with the sign Sharpe ratio.

  1. It is easy to show that the standard error of $\check{\zeta}$ is $\sqrt{\frac{\pi}{2n}}$, which is about 20% bigger than that of the moment-based Sharpe ratio.
  2. It is only nearly unbiased for Gaussian returns, and can have serious bias for skewed returns.

The latter point is a serious problem. One can easily construct returns streams with absurdly high or low values of winning days percentage, and somewhat arbitrary signal-noise ratio. By using the Edgeworth expansion, one can show that the expected value of $\check{\zeta}$ is approximately $$ E\left[ \check{\zeta} \right] \approx \zeta - \frac{\gamma_1}{6}, $$ where $\zeta=\mu/\sigma$ is the Signal-Noise ratio, and $\gamma_1$ is the standardized skewness of returns. Of course, if you somehow knew the skew of returns, you could correct for them. More interesting would be to find a way to estimate $\gamma_1$ from a single observation, though I would argue that $c\operatorname{sign}\left(x\right)$ is probably the best candidate.

Let's confirm these findings experimentally. Consider the case where $x_t$ are drawn from a Beta distribution, shifted to have a fixed mean. We fix the Signal-Noise ratio to be 0.1, and tweak the two shape parameters of the Beta distribution, $\alpha$ and $\beta$, to vary the skewness of returns. We plot the empirical mean value of $\check{\zeta}$ for $n=10^7$, along with the approximate theoretical value, versus the skewness. We see very good agreement between the two except for the case where $\alpha$ and $\beta$ are small, where the higher order moments which we have discarded are no longer negligible. We would note that this is also likely to be a problem for real world asset returns.

library(dplyr)
library(tidyr)
library(tibble)
library(ggplot2)

# generate shifted beta variates with a fixed SNR zeta
genb <- function(n,zeta,alpha,beta) {
  mu <- alpha / (alpha+beta)
  sg <- sqrt(alpha*beta/((alpha+beta+1)*(alpha+beta)^2))
  x <- (sg*zeta - mu) + rbeta(n,shape1=alpha,shape2=beta)
}
signsr <- function(x) { sqrt(pi/2) * mean(sign(x)) }

zeta <- 0.1
set.seed(12345)
sims <- crossing(tibble(alpha=c(5,50,500)),tibble(beta=c(10,100,1000))) %>%
  group_by(alpha,beta) %>%
    summarize(simzet=signsr(genb(n=10000000,zeta=zeta,alpha=alpha,beta=beta)),
              skew=2*(beta-alpha)*sqrt(beta+alpha+1)/((alpha+beta+2)*sqrt(alpha*beta))) %>%
  ungroup() %>%
  mutate(esign=zeta - skew / 6)

# plot them:
sims %>%
  ggplot(aes(skew,simzet)) + 
  geom_point(aes(color=factor(alpha),shape=factor(beta))) + 
  geom_line(aes(y=esign),linetype=3,alpha=0.5) +
  labs(x=expression(gamma[1]),
       y='mean of sign SR',
       title='Empirical and approximate theoretical mean value of sign SR',
       color=expression(alpha),shape=expression(beta))

Is it useful?

The sign Sharpe ratio does not seem useful for estimating the Signal-Noise ratio: it is too noisy, and is confounded by higher order moments. However, the challenge of estimating the Sharpe from individual observations was motivated by difficulties bounding the out-of-sample Sharpe ratio from the-in-sample Sharpe ratio using something like VC dimension or Rademacher complexity. The problem is that the standard deviation in the Sharpe computation depends on all the observations. As a side effect, if you increase a single day's returns, it can cause the Sharpe to either increase or decrease. Because of this dependence on the whole sample, the Sharpe ratio can not easily be analyzed by these complexity measures.

Click to read and post comments

Jan 08, 2020

A Sharper Sharpe, Again

Previously on this blog we had performed a fair amount of testing of the "drawdown-based estimator" of the signal-noise ratio, as proposed by Damien Challet. All that analysis was based on the 1.1 version of the sharpeRratio package, written by Challet himself. There was a bug (or bugs) in that package that caused the estimator to be biased, which could also appear as improved efficiency over the traditional "moment-based" estimator due to Sharpe (or Gosset, rather) via shrinkage to zero. Here we analyze the 1.2 version of the package, which presumably fixes this issue.

Checking for bias

Here I perform some simulations to check for bias of the estimator. I draw 128 days of daily returns from a $t$ distribution with $\nu=4$ degrees of freedom. I then compute: the moment-based Sharpe ratio; the moment-based Sharpe ratio, but debiased using higher order moments; the drawdown estimator from the 1.2 version of the package; the drawdown estimator from the 1.2 version of the package, but feeding $\nu$ to the estimator. I do this for many draws of returns. I repeat for 256 days of data, and for the population Signal-Noise ratio varying from 0.30 to 1.5 in "annualized units" (per square root year), assuming 252 trading days per year. I use future.apply to run the simulations in parallel.

suppressMessages({
  library(dplyr)
  library(tidyr)
  library(tibble)
  library(SharpeR)
  library(sharpeRratio)
  library(future.apply)
})
# only works for scalar pzeta:
onesim <- function(nday,pzeta=0.1,nu=4) {
  x <- pzeta + sqrt(1 - (2/nu)) * rt(nday,df=nu)
  srv <- SharpeR::as.sr(x,higher_order=TRUE)
  # mental note: this is much more awkward than it should be,
  # let's make it easier in SharpeR!
  #ssr <- mean(x) / sd(x)
  # moment based:
  ssr <- srv$sr
  # debiased
  ssr_b <- ssr - SharpeR::sr_bias(snr=ssr,n=nday,cumulants=srv$cumulants)

  sim <- sharpeRratio::estimateSNR(x)
  # this cheats and gives the true nu to the estimator
  cht <- sharpeRratio::estimateSNR(x,nu=nu)
  c(ssr,ssr_b,sim$SNR,cht$SNR)
}
repsim <- function(nrep,nday,pzeta=0.1,nu=4) {
  dummy <- invisible(capture.output(jumble <- replicate(nrep,onesim(nday=nday,pzeta=pzeta,nu=nu)),file='/dev/null'))
  retv <- t(jumble)
  colnames(retv) <- c('sr','sr_unbiased','ddown','ddown_cheat')
  invisible(as.data.frame(retv))
}
manysim <- function(nrep,nday,pzeta,nu=4,nnodes=5) {
  if (nrep > 2*nnodes) {
    # do in parallel.
    nper <- table(1 + ((0:(nrep-1) %% nnodes))) 
    plan(multisession, workers = 2)  
    retv <- future_lapply(nper,FUN=function(aper) repsim(aper,nday=nday,pzeta=pzeta,nu=nu)) %>%
      bind_rows()
    plan(sequential)
  } else {
    retv <- repsim(nrep=nrep,nday=nday,pzeta=pzeta,nu=nu)
  }
  retv 
}
# summarizing function
sim_summary <- function(retv) {
  retv %>%
    tidyr::gather(key=metric,value=value,-pzeta,-nday) %>%
        dplyr::filter(!is.na(value)) %>%
    group_by(pzeta,nday,metric) %>%
    summarize(meanvalue=mean(value),
              serr=sd(value) / sqrt(n()),
              rmse=sqrt(mean((pzeta - value)^2)),
              nsims=n()) %>%
    ungroup() %>%
    arrange(pzeta,nday,metric)
}

ope <- 252
pzeta <- seq(0.30,1.5,by=0.30) / sqrt(ope)

params <- tidyr::crossing(tibble::tribble(~nday,128,256),
                          tibble::tibble(pzeta=pzeta))

nrep <- 1000
set.seed(1234)
system.time({
  results <- params %>%
    group_by(nday,pzeta) %>%
      summarize(sims=list(manysim(nrep=nrep,nnodes=7,pzeta=pzeta,nday=nday))) %>%
    ungroup() %>%
    tidyr::unnest(cols=c(sims))
})
   user  system elapsed 
  3.926   0.165 217.285 

I compute the mean of each estimator over the 1,000 draws, divide that mean estimate by the true Signal-Noise Ratio, then plot versus the annualized SNR. I plot errobars at plus and minus one standard error around the mean. The ratio should be one, any deviation from which is geometric bias in the estimator. Previously this plot showed the drawdown estimator consistently estimating a value around 70% of the true value, a problem which seems to have been fixed, as it now shows values around 95% of the true value. The moment estimator shows a slight positive bias, which is decreasing in sample size, as described by Bao and Miller and Gehr. The higher order moment correction mitigates this effect somewhat for the moment estimator.

library(ggplot2)
ph <- results %>% 
  sim_summary() %>%
  mutate(metric=case_when(.$metric=='ddown' ~ 'drawdown estimator v1.1',
                          .$metric=='ddown_two' ~ 'drawdown estimator v1.2',
                          .$metric=='ddown_cheat' ~ 'drawdown estimator v1.2, nu given',
                          .$metric=='sr_unbiased' ~ 'moment estimator, debiased',
                          .$metric=='sr' ~ 'moment estimator (SR)',
                          TRUE ~ 'error')) %>%
  mutate(bias = meanvalue / pzeta,
         zeta_pa=sqrt(ope) * pzeta,
         serr = serr) %>%
  ggplot(aes(zeta_pa,bias,color=metric,ymin=bias-serr,ymax=bias+serr)) + 
  geom_line() + geom_point() + geom_errorbar(alpha=0.5,width=0.05) + 
  geom_hline(yintercept=1,linetype=2,alpha=0.5) + 
  facet_wrap(~nday,labeller=label_both) +
  scale_y_log10() + 
  labs(x='Signal-noise ratio (per square root year)',
       y='empirical expected value of estimator, divided by actual value',
       color='estimator',
       title='geometric bias of SR estimators')
print(ph)

I now plot the 'relative efficiency' as in Figure 4 of version 6 of Challet's paper. This is the ratio of the mean square error of the moment-estimator to the mean square error of the drawdown-estimator, again as a function of the true (annualized) signal-noise ratio, with different lines for the number of days simulated. Challet's plot shows this line as approximately 5, while we see values of around 1.25 or so. That is, we see only modest improvements in efficiency for the drawdown estimator, and not the putative huge gains in efficiency in the paper.

library(ggplot2)
ph <- results %>% 
  sim_summary() %>%
    dplyr::filter(metric %in% c('sr','ddown')) %>%
  dplyr::select(-meanvalue,-serr,-nsims) %>%
  tidyr::spread(key=metric,value=rmse) %>%
  mutate(eff=(sr/ddown)^2) %>%
  mutate(zeta_pa=sqrt(ope) * pzeta) %>%
  ggplot(aes(zeta_pa,eff,color=factor(nday))) + 
  geom_line() + geom_point() + 
  geom_hline(yintercept=1,linetype=3) + 
  labs(x='Signal-noise ratio (per square root year)',
       y='relative efficiency of drawdown to moment estimator',
       color='num days',
       title='Efficiency of SR estimators')
print(ph)

Thus it appears that the 1.2 version of the package fixes the bias issues in the initial release.

Click to read and post comments
Next → Page 1 of 5

Copyright © 2018-2026, Steven E. Pav.  
The above references an opinion and is for information purposes only. It is not offered as investment advice.