The data
The returns are invented for illustration, not market data. They are 60 monthly returns drawn with a fixed seed from a normal distribution with mean 0.75% and volatility a month. The bill pays a year, 0.25% a month, as in lesson 4.6.2.
So the model behind the data has an excess return of 0.5% a month, or 6% a year. Its annual volatility is 12% and its true Sharpe ratio 0.5. A desk never knows the truth; here, as in case study 4.6.5, each estimate can be checked against it.
The excess returns are what the Sharpe ratio is built from, with the strategy's return in month . The 60 months include 20 losing months; the worst is and the best . The mean excess return is 1.049% a month and the sample standard deviation is 3.539%: 12.59% and 12.26% a year.
Try it
The first lines generate the returns exactly as later cells do: seed 598, then 60 normal draws. The first line printed shows the first six months, the worst, the best and the number of losing months.
Then the monthly mean and standard deviation of the excess returns, and both annualised: times 12 and times .
The Sharpe ratio and its standard error
Lesson 4.6.2 defined the Sharpe ratio as the excess return per unit of volatility. Here it is written , and its estimate from data , so that stays the sample standard deviation.
Rule: Sample Sharpe ratio
is the mean monthly excess return and its sample standard deviation, dividing by (lesson 5.8.1). The mean scales by 12 and the volatility by (lesson 4.6.2), which needs independent months (case study 5.5.5).
Here , and the annual . The truth is 0.1443 a month and 0.5 a year: the estimate is twice the true value.
Is that unusual? It is a ratio of two estimates, so it has a standard error of its own.
Rule: Standard error of a Sharpe ratio
Stated here, for independent normal returns; is the number of months and the number of years. The annual standard error is times the monthly one, close to unless the Sharpe ratio is large.
With the sample's in the formula, the monthly standard error is 0.1319 and the annual one 0.4569, near . A 95% interval for the annual Sharpe ratio is : from 0.1313 to 1.9224 (lesson 5.8.2). It holds the true 0.5, and it nearly reaches 0.
A simulation checks the formula. Over 100,000 seeded five-year samples from the model, the sample Sharpe ratio averages 0.5045. Its standard deviation is 0.4601, close to the formula's 0.4495 at the true 0.5.
In 12.65% of those samples it is 1.03 or more, and in 13.44% it is negative. So a strategy with a true Sharpe ratio of 0.5 shows 1.03 in about one five-year backtest in eight. It trails the bill in about one in seven.
Try it
The first lines rebuild the returns and print both Sharpe ratios and the true monthly one. Then both standard errors, and the 95% interval.
Then 100,000 seeded samples of 60 months from the model. The cell prints the mean and standard deviation of their annual Sharpe ratios, the formula at 0.5 and the two shares.
A t test of no skill
"No skill" means a mean excess return of 0. That is lesson 5.8.3's t test with , and its statistic is the Sharpe ratio in disguise.
Rule: The t statistic of a Sharpe ratio
Compared with . Testing a Sharpe ratio of 0 and testing a mean excess return of 0 are the same test. The statistic grows with the number of years, not of months.
Here with 59 degrees of freedom, so the two-sided p-value is 0.0252. That is significant at the 5% level but not at 1%, where would need 2.6618. The one-sided p-value, for "the strategy beats the bill", is 0.0126.
Try it
The cell computes three ways: from the mean and , from the monthly Sharpe ratio, and from the annual one. Then the p-values, SciPy's ttest_1samp as a check, and the cut-off at 1%.
A bootstrap interval
The formula assumes normal, independent months. The bootstrap of lesson 5.8.2 drops the first assumption: resample the 60 months with replacement, recompute the annual Sharpe ratio, and repeat 10,000 times.
The 2.5% and 97.5% quantiles give the interval 0.1423 to 2.0546, against the formula's 0.1313 to 1.9224. The bootstrap Sharpe ratios have standard deviation 0.4871, and 1.21% of them are 0 or below.
The interval leans a little to the right, but the two methods agree. The data rule out 0 at the 5% level only just, and they cannot tell 0.5 from 1.5.
Try it
idx picks 60 months with replacement for each of 10,000 rows, with a fixed seed. Each row gives one annual Sharpe ratio.
The cell prints the percentile interval, the standard deviation of the bootstrap ratios and the share at or below 0.
How many years does a Sharpe ratio need?
With , a sample Sharpe ratio is significant at the 5% level once , that is after years. Lesson 5.8.3's sample-size formula, with , gives the years for an 80% chance of detecting a true Sharpe ratio.
| Sharpe ratio | 0.25 | 0.5 | 1 | 2 |
|---|---|---|---|---|
| years for a sample value to be significant | 61.5 | 15.4 | 3.8 | 1.0 |
| years to detect a true value 80% of the time | 125.6 | 31.4 | 7.8 | 2.0 |
A Sharpe ratio of 0.5, respectable for a fund, needs 15.4 years of data to be told apart from 0. It needs 31.4 years to be found reliably. The small term in the standard error changes this little: 15.5 years instead of 15.4.
Sample Sharpe ratios that luck alone produces, against the length of the record. A ratio is significant at 5% once it clears the shaded funnel: after 3.8 years for 1, 15.4 for 0.5. The backtest clears it only just.
Try it
Each line of the loop is one column of the table: the years for significance and the years for 80% power. The last line adds the term for a Sharpe ratio of 0.5.
The mean is the uncertain part
Case study 5.4.6 noted that a volatility can be estimated far better than a mean return. These five years show how much better.
- The mean. The annual mean excess return is 12.59%, with standard error percentage points: the of case study 4.6.5 and lesson 5.8.2. Its 95% t interval runs from 1.62% to 23.56%, around a true 6%.
- The volatility. The annual volatility is 12.26%. Lesson 5.8.2's chi-squared interval runs from 10.39% to 14.95%, around a true 12%.
The mean's standard error is 44% of the estimate itself; the volatility's is about 9%, roughly for normal data. So the uncertainty of a Sharpe ratio is almost all in its numerator.
More frequent data helps only the volatility. With 1,260 daily returns over the same five years, the volatility's relative error falls to about 2%. The mean's standard error stays points: only more years shrink it.
Try it
The first lines give the annual mean, its standard error and its t interval. Then the chi-squared interval for , annualised by .
The last line prints the mean's relative error, the volatility's for 60 months and for 1,260 days, and the true model's .
Twenty strategies tried
The strategy was the best of 20 ideas backtested on the same five years. Suppose none of them has any skill: lesson 5.8.3 warned what happens.
In 5,000 seeded batches of 20 independent strategies with no skill, the best Sharpe ratio has median 0.83. It reaches the backtest's 1.03 in 21.18% of batches, against 1.22% for a single strategy. In 64% of batches at least one of the 20 is significant at the 5% level.
The exact version agrees. One strategy with no skill reaches with probability 0.0126, the one-sided p-value, so the best of 20 does with probability .
The Sharpe ratio of one strategy with no skill, and of the best of 20. A ratio that is rare for one strategy is common for the best of many.
A Bonferroni correction for 20 tries asks each for , two-sided: , an annual Sharpe ratio of 1.41 over five years. The backtest's 1.03 does not pass.
Desks therefore record how many strategies were tried, and keep data aside to test the winner on, as case study 4.6.5 advised. Adjusted measures such as the "deflated Sharpe ratio" build the count in.
Try it
The array holds 5,000 batches of 20 strategies of 60 months, each with mean 0. Each row's largest annual Sharpe ratio is the best of its batch.
The lines print the median best, the two shares that reach 1.03 and the share of batches with a significant strategy. Then come the exact formula and the Bonferroni cut-off in and in Sharpe ratio, and the of the quiz below.
Try it
The first line is the Sharpe ratio of the raw returns, without subtracting the bill. The second is the factor by which overstates an annual Sharpe ratio when the lag-one correlation is 0.2. It uses case study 5.5.5's annual variance, .
The significance of a Sharpe ratio
Write two functions for an array r of per-period returns and a per-period risk-free rate rf (default 0). periods is the number of periods a year: 12, the default, for monthly data and 252 for daily. Work with the excess returns r - rf and the sample standard deviation dividing by .
sharpe_stats(r, rf=0.0, periods=12)returns the tuple(sharpe, se, t_stat, p_value).sharpeis the annualised Sharpe ratio, times the per-period one.seis its standard error, with SR the per-period sample Sharpe ratio.t_statis times the per-period Sharpe ratio, andp_valueits two-sided p-value from .
bootstrap_interval(r, rf=0.0, periods=12, level=0.95, n_boot=10_000, seed=0)returns(low, high).- Draw
idx = np.random.default_rng(seed).integers(0, n, size=(n_boot, n)), so each row is one resample of the excess returns. - Compute each row's annualised Sharpe ratio. Return the
(1 - level)/2and(1 + level)/2quantiles, withnp.quantile's default method.
- Draw
Both raise ValueError if r has fewer than 3 returns, if every excess return is the same (no volatility), or if periods is not positive. bootstrap_interval also raises it if level is not strictly between 0 and 1. The tests use this case study's returns, a daily series, and each bad input.
See the tests
import numpy as np
from scipy.stats import ttest_1samp
R = np.random.default_rng(598).normal(0.0075, 0.12 / np.sqrt(12), 60)
RF = 0.03 / 12
DAILY = np.random.default_rng(252).normal(0.0004, 0.01, 756)
def _raises(f, *args, **kwargs):
try:
f(*args, **kwargs)
except ValueError:
return True
return False
def _stats(*args, **kwargs):
got = sharpe_stats(*args, **kwargs)
assert got is not None, "sharpe_stats returned None (did you forget to return?)"
return got
def test_case_study():
"""The case study's 60 months: Sharpe ratio about 1.0268, standard error 0.4569, t 2.2961, p 0.0252"""
got = _stats(R, RF)
if abs(got[0] - 1.0355134955) < 1e-6:
raise AssertionError("got a Sharpe ratio of 1.0355: that divides by n. Use the sample standard deviation with ddof=1")
if abs(got[0] - 0.2964254726) < 1e-6:
raise AssertionError("got 0.2964: that is the monthly Sharpe ratio. Multiply by the square root of periods")
expect_close(got, (1.026847958263304, 0.45693194906708584, 2.296101837233615, 0.02523944563138756), what="sharpe_stats(r, rf)", rel=1e-9)
def test_risk_free_rate():
"""Without subtracting the bill the ratio is about 1.2716: rf matters"""
expect_close(_stats(R)[0], 1.2715658855220144, what="sharpe_stats(r) with rf = 0", rel=1e-9)
def test_matches_t_test():
"""The t statistic and p-value equal SciPy's one-sample t test of a zero mean excess return"""
res = ttest_1samp(R - RF, 0)
got = _stats(R, RF)
expect_close(got[2:], (float(res.statistic), float(res.pvalue)), what="t_stat and p_value", rel=1e-9)
def test_losing_strategy():
"""A losing strategy has a negative Sharpe ratio and t, and a two-sided p-value of about 0.00125"""
expect_close(_stats(-R, RF), (-1.5162838127807248, 0.4681445343133967, -3.390513678560265, 0.0012496623447016545), what="sharpe_stats(-r, rf)", rel=1e-9)
def test_daily():
"""Three years of daily returns, periods = 252: Sharpe ratio about 0.1141, standard error about 0.5774"""
expect_close(_stats(DAILY, 0.03 / 252, 252), (0.11414190728482858, 0.577357731379311, 0.19769958269013926, 0.8433333305780878), what="sharpe_stats(daily, rf, 252)", rel=1e-9)
def test_bootstrap_case_study():
"""With seed 5852 the bootstrap interval is about 0.1423 to 2.0546, as in the case study"""
got = bootstrap_interval(R, RF, seed=5852)
assert got is not None, "bootstrap_interval returned None (did you forget to return?)"
expect_close(got, (0.14227536992923157, 2.054623742026796), what="bootstrap_interval(r, rf, seed=5852)", rel=1e-9)
def test_bootstrap_defaults():
"""With the default seed 0 the interval is about 0.1551 to 2.0358"""
expect_close(bootstrap_interval(R, RF), (0.15505232661625515, 2.0358276113556757), what="bootstrap_interval(r, rf)", rel=1e-9)
def test_bootstrap_level():
"""A 90% interval with seed 1 is about 0.2917 to 1.8604, narrower than the 95% one"""
expect_close(bootstrap_interval(R, RF, level=0.9, seed=1), (0.29172254684002374, 1.8603905606518771), what="bootstrap_interval(r, rf, level=0.9, seed=1)", rel=1e-9)
def test_bootstrap_daily():
"""Daily returns, periods = 252, 2,000 resamples with seed 3: about −0.9979 to 1.2302"""
got = bootstrap_interval(DAILY, 0.03 / 252, 252, n_boot=2000, seed=3)
expect_close(got, (-0.9978506995692057, 1.2302088480070756), what="bootstrap_interval(daily, rf, 252, n_boot=2000, seed=3)", rel=1e-9)
def test_bad_inputs():
"""Fewer than 3 returns, no volatility, or periods of 0 raise ValueError in both functions"""
for f in (sharpe_stats, bootstrap_interval):
assert _raises(f, [0.01, 0.02]), f"{f.__name__} with 2 returns should raise ValueError"
assert _raises(f, [0.01] * 12), f"{f.__name__} with every return 0.01 should raise ValueError"
assert _raises(f, R, RF, 0), f"{f.__name__} with periods = 0 should raise ValueError"
def test_bad_level():
"""A level of 0, 1 or 1.5 raises ValueError in bootstrap_interval"""
for level in (0, 1, 1.5):
assert _raises(bootstrap_interval, R, RF, level=level), f"bootstrap_interval(level={level}) should raise ValueError"
Summary card
- The sample Sharpe ratio is of the excess returns, times for monthly data. Its standard error is about per period, about a year: 0.4569 for five years.
- The t test of no skill is : 2.2961 here, . Years matter, not the number of returns.
- A bootstrap interval, 0.1423 to 2.0546, agrees with the formula's 0.1313 to 1.9224: five years cannot tell 0.5 from 1.5.
- A sample Sharpe ratio of 0.5 needs years to be significant, and a true 0.5 needs 31.4 years to be detected 80% of the time.
- The mean return carries the uncertainty: 12.59% with a 95% interval of 1.62% to 23.56%, while the volatility is pinned to 10.39% to 14.95%.
- The best of 20 strategies with no skill reaches 1.03 about 21% of the time. Report how many were tried; Bonferroni would ask for 1.41.