npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

deflated-sharpe

v0.1.0

Published

Deflated Sharpe Ratio (Bailey & Lopez de Prado) — corrects a backtest's Sharpe ratio for selection bias and non-normality. No dependencies.

Readme

deflated-sharpe

A dependency-free TypeScript implementation of the Deflated Sharpe Ratio (Bailey & López de Prado, 2014) — the correction that tells you whether your backtest found an edge or found the luckiest of your attempts.

import { deflatedSharpe } from "deflated-sharpe";

// 437 trade returns, expressed in R (or %, or anything scale-free).
// 60 is the number of variants you tried before settling on this one.
const result = deflatedSharpe(tradeReturns, 60);

result.sharpe;              // 0.0350  — what you measured
result.threshold;           // 0.1121  — what pure search would have produced
result.probabilisticSharpe; // 0.794   — looks fine, if you ignore the search
result.deflatedSharpe;      // 0.035   — once you account for it
result.survives;            // false

The problem

If you test 100 strategies, roughly 5 will clear a 95% significance bar by chance alone. That is not a flaw in your process — it is what "95%" means.

So the Sharpe ratio you report is not the Sharpe ratio you found. It is the maximum of N draws, and the maximum of N draws is biased upward by construction. The more carefully you searched, the more inflated it is.

Two corrections are needed, and the DSR applies both:

1. Selection bias. expectedMaxSharpe(σ, N) gives the threshold SR₀ — the highest Sharpe you would expect across N trials if none of them had any edge. Beating zero is not the test. Beating SR₀ is.

2. Non-normality. Trade returns are skewed and fat-tailed. A trend-following system has many small losses and a few enormous wins; a short-volatility system has the reverse. The ordinary Sharpe ratio assumes normality and overstates confidence when the skew runs against you. probabilisticSharpe takes skewness and kurtosis as explicit inputs.

The result is a probability: how confident you can be that the true Sharpe exceeds the threshold. The conventional bar is 0.95.


Install

npm install deflated-sharpe

No dependencies. Node ≥ 18, or any bundler. Works in the browser.


Usage

The whole calculation

import { deflatedSharpe } from "deflated-sharpe";

const result = deflatedSharpe(returns, trials);
if (result === null) {
  // Fewer than 3 observations, or zero variance.
  // An infinite Sharpe ratio from a constant series is how a backtest
  // reports certainty it does not have, so this returns null instead.
}

Report a range, not a number

trials is the one input you cannot measure. It is a judgement call, and the verdict depends on it. So show the reader how sensitive the answer is:

for (const n of [1, 10, 50, 100, 500]) {
  console.log(n, deflatedSharpe(returns, n)?.deflatedSharpe.toFixed(3));
}
// 1    1.000
// 10   0.974
// 50   0.846
// 100  0.753
// 500  0.499

A result that is 1.000 at N=1 and 0.499 at N=500 is not a finding. It is a statement about how hard you looked.

The pieces separately

import { moments, probabilisticSharpe, expectedMaxSharpe } from "deflated-sharpe";

const m = moments(returns);          // { sharpe, n, skewness, kurtosis }
probabilisticSharpe(m!, 0);          // P(true Sharpe > 0), non-normality only
expectedMaxSharpe(0.04, 250);        // the threshold 250 trials alone produces

Counting trials honestly

This is where the method is usually defeated, so it is worth being blunt.

Count every variant you looked at, including the ones you discarded in the first minute. A parameter you swept and rejected still consumed a draw from the same distribution. If you tried 9 trailing-stop widths across 2 data windows and 10 bar sizes across 3 exit rules, that is 108 — not "one strategy".

Count also:

  • variants you tried in an earlier session and abandoned
  • markets you added or removed from the universe
  • date ranges you shifted
  • anything a colleague tried on the same data and told you about

Under-counting is the default failure. When in doubt, take the higher number and see whether the result still stands.


A worked example, from a real project

This library was extracted from a systematic trading project where it was used on the project's own results. It is worth reporting what happened, because the interesting case is not the one it approved.

The best result the project had found was a configuration measured at +0.119R per trade. It survived three separate data splits. It looked like the strongest finding in months of work. It was then tested on a window that had never been touched, and came back at −0.081R.

Run after the fact, with an honest trial count of 60:

| | | |---|---| | measured Sharpe per trade | 0.0350 | | threshold SR₀ at N=60 | 0.1121 | | P(Sharpe > 0), non-normality only | 0.794 | | deflated | 0.035 |

The measured Sharpe was not merely below the threshold — it was below a third of it. Pure search across 60 variants would have been expected to produce a better number than the one that had been celebrated, with no edge present at all.

Applied across the whole project, not one return-based finding survived. The two results that did survive were both measurements of structure rather than of edge: a cost-scaling exponent and a correlation figure. Both were arithmetic, and arithmetic does not get selected.

That is the honest use of this tool. It does not help you find an edge. It tells you when you have not found one — which is more often than anyone wants, and cheaper to learn before the money is committed than after.


Design notes

The Sharpe ratio is per observation, not annualised. Annualising requires assuming how many observations a year contains, and that assumption varies with timeframe and instrument count. The DSR only cares about the ratio of the Sharpe to its own standard error, and both scale identically — so leaving it per-observation removes a free parameter without changing any answer.

Unstable inputs return NaN or null, never a plausible-looking number. moments returns null for a constant or too-short series. probabilisticSharpe returns NaN when the variance term goes non-positive, which happens with extreme tails on few observations. normalInv throws outside (0, 1). A threshold computed from an undefined quantile is exactly the kind of number that looks real and is not.

Accuracy. normalCdf uses Abramowitz & Stegun 7.1.26, accurate to ~1.5e-7 absolute; normalInv uses Acklam's algorithm with one Halley refinement. A round trip is therefore accurate to ~1e-6, not machine epsilon — which the tests assert honestly rather than to a tolerance the implementation cannot meet. This is far tighter than the precision of the inputs: a Sharpe ratio estimated from a few hundred trades is uncertain in the second decimal.

When you have measured the cross-trial dispersion, pass it. deflatedSharpe(returns, trials, sigmaSharpe) takes the standard deviation of the Sharpe estimates across your N attempts. Omitted, it falls back to the standard error of a single estimate — which understates the true dispersion and so makes the threshold too lenient. Worth remembering when the verdict comes back "survives".


API

| export | purpose | |---|---| | deflatedSharpe(returns, trials, sigmaSharpe?) | the whole calculation | | moments(returns) | Sharpe, n, skewness, kurtosis — or null | | probabilisticSharpe(moments, threshold) | P(true Sharpe > threshold) | | expectedMaxSharpe(sigmaSharpe, trials) | the selection-bias threshold SR₀ | | normalCdf(x) / normalInv(p) | the normal distribution helpers |

Full types are exported: ReturnMoments, DeflatedSharpeResult.


Reference

Bailey, D. H. and López de Prado, M. (2014). The Deflated Sharpe Ratio: Correcting for Selection Bias, Backtest Overfitting, and Non-Normality. Journal of Portfolio Management, 40(5), 94–107. SSRN 2460551

Licence

MIT