The deflated Sharpe ratio in Python: how many strategies did you try?
Try enough strategies and one of them will look good by luck. The Sharpe ratio of the best of 50 variants isn't the Sharpe ratio of a strategy. It's the maximum of 50 noisy numbers, and a maximum is biased upward.
The deflated Sharpe ratio (DSR), introduced by David H. Bailey and Marcos López de Prado in "The Deflated Sharpe Ratio: Correcting for Selection Bias, Backtest Overfitting and Non-Normality" (Journal of Portfolio Management 40(5), 2014; preprint on SSRN), corrects for that. Given how many variants you tried, how much their Sharpe ratios varied, how long the sample is, and how skewed and fat-tailed the returns are, it estimates the probability that the selected strategy's true Sharpe ratio is above what the best of that many zero-skill strategies would show by chance.
This page explains the idea, gives a short Python implementation, checks it against the worked example in the paper, and covers the hard part: counting your trials. All backtest figures here are hypothetical and come from simulated returns.
Why the best backtest is biased
Simulate 50 strategies with no skill at all and look at the best one:
import numpy as np
from scipy import stats
rng = np.random.default_rng(1)
n_obs, n_trials = 1250, 50 # about five years of daily returns, 50 variants
returns = rng.normal(0, 0.01, (n_obs, n_trials)) # true Sharpe ratio of every variant is 0
sharpes = returns.mean(axis=0) / returns.std(axis=0, ddof=1) # per-day Sharpe ratios
best = sharpes.argmax()
round(sharpes[best] * np.sqrt(252), 2) # -> 0.93 (hypothetical, zero-skill noise)
None of these strategies has any skill, yet the best one shows an annualized Sharpe ratio of 0.93 (hypothetical, on simulated noise). Report only that one and it looks like a modest discovery. The more variants you try, the higher the best one will be.
The formula
The DSR is built on the probabilistic Sharpe ratio (PSR) from Bailey and López de Prado, "The Sharpe Ratio Efficient Frontier" (Journal of Risk 15(2), 2012). PSR is the probability that the true Sharpe ratio is above a benchmark SR*, given an observed Sharpe ratio SR over T observations with skewness γ₃ and kurtosis γ₄:
PSR(SR*) = Φ( (SR − SR*) · √(T − 1) / √(1 − γ₃·SR + (γ₄ − 1)/4 · SR²) )
Φ is the standard normal CDF. The DSR is the PSR with the benchmark set to the Sharpe ratio you'd expect from the best of N zero-skill trials:
SR₀ = √V · ( (1 − γ)·Φ⁻¹(1 − 1/N) + γ·Φ⁻¹(1 − 1/(N·e)) )
DSR = PSR(SR₀)
V is the variance of the Sharpe ratios across your N trials, γ ≈ 0.5772 is the Euler–Mascheroni constant, and e is Euler's number. Three details matter in code:
- Sharpe ratios are per period, not annualized, and T is the number of periods.
- γ₄ is plain kurtosis, which is 3 for normal returns, not excess kurtosis (0 for normal returns).
- V and N describe all the trials, not just the winner. You can't compute the DSR without them.
Python implementation
EULER_GAMMA = 0.5772156649015329
def probabilistic_sharpe_ratio(sr, sr_benchmark, n_obs, skew, kurtosis):
"""P(true Sharpe > sr_benchmark). Per-period Sharpe ratios; kurtosis is 3 for normal returns."""
denom = np.sqrt(1 - skew * sr + (kurtosis - 1) / 4 * sr**2)
return stats.norm.cdf((sr - sr_benchmark) * np.sqrt(n_obs - 1) / denom)
def expected_max_sharpe(n_trials, sr_variance):
"""Expected best per-period Sharpe ratio among n_trials zero-skill strategies."""
z = stats.norm.ppf
return np.sqrt(sr_variance) * (
(1 - EULER_GAMMA) * z(1 - 1 / n_trials) + EULER_GAMMA * z(1 - 1 / (n_trials * np.e))
)
def deflated_sharpe_ratio(sr, n_obs, skew, kurtosis, n_trials, sr_variance):
sr0 = expected_max_sharpe(n_trials, sr_variance)
return probabilistic_sharpe_ratio(sr, sr0, n_obs, skew, kurtosis)
Check it against the paper
The paper's numerical example has an annualized Sharpe ratio of 2.5 over five years of daily data (T = 1,250, 250 observations a year), N = 100 trials, a variance of 1/2 for the annualized Sharpe ratios across trials, skewness −3 and kurtosis 10. It reports SR₀ ≈ 0.1132 (per day) and DSR ≈ 0.9004. Converting to per-day units:
sr = 2.5 / np.sqrt(250)
sr_variance = 0.5 / 250
round(expected_max_sharpe(100, sr_variance), 4) # -> 0.1132
round(deflated_sharpe_ratio(sr, 1250, -3, 10, 100, sr_variance), 4) # -> 0.9004
Both match. In the paper's example, a DSR of 0.90 falls short of the 95% confidence level it applies, so the paper concludes that this Sharpe ratio of 2.5 isn't a significant discovery at that level once 100 trials are counted.
And on the simulated strategies
Back to the 50 zero-skill variants. Without the correction, the best one looks fairly convincing; with it, it doesn't:
r = returns[:, best]
skew, kurt = stats.skew(r), stats.kurtosis(r, fisher=False) # fisher=False: 3 for normal returns
round(probabilistic_sharpe_ratio(sharpes[best], 0, n_obs, skew, kurt), 2) # -> 0.98
round(deflated_sharpe_ratio(sharpes[best], n_obs, skew, kurt, n_trials, sharpes.var(ddof=1)), 2) # -> 0.32
Measured against zero, the best variant gets a 98% probability of a positive Sharpe ratio. Measured against what the best of 50 noise strategies should show, the probability is 32%.
Watch the kurtosis argument: scipy.stats.kurtosis returns excess kurtosis unless you pass fisher=False, and pandas' .kurt() always returns excess kurtosis. Passing excess kurtosis understates the fat-tail correction.
How good is the approximation?
The SR₀ formula approximates the expected maximum of N normal draws. You can check it by simulation, since the per-day Sharpe estimate of a zero-skill strategy has a standard deviation of about 1/√T:
draws = rng.normal(0, np.sqrt(1 / n_obs), (20_000, n_trials))
round(draws.max(axis=1).mean(), 4) # -> 0.0635 (simulated)
round(expected_max_sharpe(n_trials, 1 / n_obs), 4) # -> 0.0644 (formula)
For 50 trials the formula is within about 1.5% of the simulated value, slightly on the conservative side. The paper derives the approximation in an appendix and checks it with its own Monte Carlo experiments.
What counts as a trial
N is the number of distinct things you tried before picking the one you report. That includes:
- every parameter combination in a grid search, kept or not;
- every universe, date range, horizon and rebalancing rule you looked at;
- every feature set and model;
- the variants you "just looked at once" and dropped.
If you can't reconstruct the count, you're probably undercounting. Two practical habits help: log every backtest run (parameters and resulting returns) automatically, and decide on the experiments before you run them.
Trials that are near-copies of each other (a 20-day versus a 21-day moving average) aren't fully independent, and the formula assumes independent trials. The paper discusses estimating an effective number of independent trials; treat the raw count as an upper bound and the number of genuinely different ideas as a lower bound, and report the DSR for both.
Related tools
- A higher bar for t-statistics. Harvey, Liu and Zhu, "... and the Cross-Section of Expected Returns" (Review of Financial Studies 29(1), 2016), argue that after decades of factor mining, a new factor should clear a t-statistic of about 3.0, not 2.0.
- Multiple-testing corrections. Bonferroni divides the significance level by the number of tests; Holm's step-down method controls the same error rate and is never less powerful.
- Probability of backtest overfitting (PBO). Bailey, Borwein, López de Prado and Zhu, "The Probability of Backtest Overfitting" (Journal of Computational Finance, 2017), estimate how often the in-sample winner underperforms out of sample, using combinatorially symmetric cross-validation.
- Pre-registration. Write down the hypothesis, the test and the pass threshold before you see the data. It's the cheapest fix of all, because it makes N small and known.
Checking a return series for this
Backtest Auditor (in development) reports a deflated Sharpe ratio when you give it a return series and the number of variants you tried, alongside other statistical checks. It will be available after release. A DSR above a threshold doesn't mean a strategy will make money; it means the result is less likely to be a product of selection alone.
Related on this site: lookahead bias in pandas, survivorship bias and reused tickers, and our help articles.