Skip to content

Commit 956bb52

Browse files
committed
Scale the zero-dispersion floor with the sample size
The first guard compared the standard deviation against eps x scale, which is the residue of a single rounding rather than of the whole sum. Measured over constant series spanning values 1e-7..1e3 and lengths 3..10000, the residue reaches 1.96 eps x scale, so the original threshold still let a flat series through at other lengths: it was calibrated on one series and tested on that same series. n eps x scale keeps a margin of at least 3.9x at every length measured, and a real series with sigma=1e-12 sits more than ten orders of magnitude above it, so nothing legitimate is caught. The test now sweeps values x lengths rather than asserting one point, and was run against the unfixed function, where it fails.
1 parent 06511a5 commit 956bb52

2 files changed

Lines changed: 35 additions & 2 deletions

File tree

openbb_platform/extensions/quantitative/openbb_quantitative/helpers.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def deflated_sharpe_stats(
119119
variance.
120120
"""
121121
# pylint: disable=import-outside-toplevel
122-
from numpy import asarray, e as np_e, sqrt
122+
from numpy import asarray, e as np_e, finfo, sqrt
123123
from scipy.stats import norm
124124

125125
if trials < 1:
@@ -130,7 +130,18 @@ def deflated_sharpe_stats(
130130
raise ValueError("need at least 3 observations")
131131
mu = returns.mean() - rfr
132132
sd = returns.std() # population, consistent with the reference implementation
133-
if sd == 0:
133+
# `sd == 0` is exact and a constant series does not reach it: its standard
134+
# deviation is floating-point residue rather than a true zero, so a flat 0.1%
135+
# series has sd ~1e-19 and divides out to a Sharpe of ~1e16 -- finite, and past
136+
# every isfinite guard after this one. Deflating that returned 1.0, i.e. certainty
137+
# of a real edge, for the one input carrying no information about one. Compare
138+
# against the resolution of a float at the scale of the data instead, so a
139+
# genuinely low-volatility series still gets a number.
140+
# The residue grows with the number of terms summed, so the floor is n eps rather
141+
# than eps: measured at most 1.96 eps x scale over constant series spanning values
142+
# 1e-7..1e3 and lengths 3..10000, while a real series with sigma=1e-12 sits more
143+
# than ten orders of magnitude above n eps x scale.
144+
if not sd > n * finfo(float).eps * abs(returns).max():
134145
raise ValueError("zero-variance returns")
135146
sr = mu / sd
136147
skew_ = (((returns - returns.mean()) / sd) ** 3).mean()

openbb_platform/extensions/quantitative/tests/test_deflated_sharpe.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,25 @@ def test_invalid_inputs_raise():
5656
deflated_sharpe_stats(RETURNS.head(2), trials=10)
5757
with pytest.raises(ValueError):
5858
deflated_sharpe_stats(pd.Series([0.01] * 10), trials=10)
59+
60+
61+
def test_zero_dispersion_raises_across_values_and_lengths():
62+
"""A constant series carries no information about an edge, at any scale.
63+
64+
`sd == 0` is exact and a constant series does not reach it: its standard
65+
deviation is floating-point residue rather than a true zero, so the ratio came
66+
out finite (~1e16) and reached the deflation arithmetic, which answered 1.0.
67+
The residue depends on both the value and the length, so this is checked over a
68+
grid: a guard calibrated on a single series passes while still leaking elsewhere.
69+
"""
70+
for value in (1e-7, 1e-4, 0.001, 0.01, 1.0, 100.0):
71+
for n in (3, 10, 250, 5000):
72+
with pytest.raises(ValueError, match="zero-variance"):
73+
deflated_sharpe_stats(pd.Series([value] * n), trials=4)
74+
75+
# The guard is relative to the scale of the data: a real but very quiet series
76+
# still gets a number.
77+
import numpy as np
78+
79+
quiet = pd.Series(np.random.default_rng(1).normal(0, 1e-8, 250))
80+
assert 0 <= deflated_sharpe_stats(quiet, trials=4)["deflated_sharpe_ratio"] <= 1

0 commit comments

Comments
 (0)