Skip to content

Commit 7e93913

Browse files
committed
Return nan from deflated_sharpe_ratio for returns with no dispersion
An equity curve growing at a constant rate has no Sharpe ratio, but it does not reach the deflation arithmetic as a nan. The standard deviation of its returns is floating-point residue rather than an exact zero, so it divides out to a Sharpe of ~1e13 -- finite, and therefore past the existing check. Deflating that returned 1.0: certainty of a real edge, from the one input that carries no information about one. These returns are ratios of floats, so the residue is of the order of an ulp of 1.0 rather than of the returns' own magnitude. Measured at 0.44-0.61 eps across constant rates from -1% to +5% and lengths 50-3000, against 4e7 eps for a real series with sigma=1e-8, so one eps separates them with seven orders of magnitude to spare. The test was run against the unfixed function and fails there, so it tests the guard rather than accompanying it.
1 parent cd7e7ab commit 7e93913

2 files changed

Lines changed: 34 additions & 0 deletions

File tree

β€Žbacktesting/lib.pyβ€Ž

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,18 @@ def deflated_sharpe_ratio(stats: pd.Series,
264264
if not sr or np.isnan(sr) or n_periods < 2:
265265
return np.nan
266266

267+
# A return series with no dispersion has no Sharpe ratio, but it does not arrive
268+
# here as a nan: an equity curve growing at a constant rate yields returns whose
269+
# standard deviation is floating-point residue rather than an exact zero, so it
270+
# divides out to a Sharpe of ~1e13 -- finite, and therefore past the check above.
271+
# Deflating that returned 1.0, i.e. certainty of a real edge, for the one input
272+
# carrying no information about one. These returns are ratios of floats, so the
273+
# residue is of the order of an ulp of 1.0: measured at 0.44-0.61 eps for constant
274+
# rates from -1% to +5% and lengths 50-3000, against 4e7 eps for a real series with
275+
# sigma=1e-8. One eps separates them with seven orders of magnitude to spare.
276+
if not returns.std(ddof=1) > np.finfo(float).eps * max(1.0, returns.abs().max()):
277+
return np.nan
278+
267279
# Expected maximum Sharpe ratio of `n_trials` skill-less trials
268280
# (Bailey & LΓ³pez de Prado 2014, eq. for E[max SR_n] under the null)
269281
norm = NormalDist()

β€Žbacktesting/test/_test.pyβ€Ž

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,28 @@ def test_deflated_sharpe_ratio(self):
10101010
with self.assertWarnsRegex(UserWarning, 'not Sharpe ratios'):
10111011
deflated_sharpe_ratio(stats, heatmap.rename('SQN'))
10121012

1013+
def test_deflated_sharpe_ratio_zero_dispersion(self):
1014+
# An equity curve growing at a constant rate has no dispersion, so no Sharpe
1015+
# ratio and no deflated one. Its standard deviation is floating-point residue
1016+
# rather than an exact zero, so the ratio comes out finite (~1e16) and reaches
1017+
# the deflation arithmetic, which answered 1.0 -- certainty of an edge, from
1018+
# the one input that cannot show one.
1019+
index = pd.date_range('2020-01-01', periods=250, freq='D')
1020+
for rate in (1.001, 1.0):
1021+
equity = pd.Series(np.full(250, 1e4) * rate ** np.arange(250), index=index)
1022+
stats = pd.Series({'Sharpe Ratio': 3.0,
1023+
'_equity_curve': pd.DataFrame({'Equity': equity})})
1024+
self.assertTrue(np.isnan(deflated_sharpe_ratio(stats, [.5, 1., 1.5, 2.])))
1025+
1026+
# The guard is relative to the scale of the data: a real but very quiet
1027+
# series still gets a number.
1028+
quiet = pd.Series(
1029+
1e4 * np.cumprod(1 + np.random.default_rng(1).normal(0, 1e-8, 250)),
1030+
index=index)
1031+
stats = pd.Series({'Sharpe Ratio': 3.0,
1032+
'_equity_curve': pd.DataFrame({'Equity': quiet})})
1033+
self.assertTrue(0 <= deflated_sharpe_ratio(stats, [.5, 1., 1.5, 2.]) <= 1)
1034+
10131035
def test_compute_stats(self):
10141036
stats = Backtest(GOOG, SmaCross).run()
10151037
only_long_trades = stats._trades[stats._trades.Size > 0]

0 commit comments

Comments
Β (0)