Reward per unit of risk
A strategy that earns 12% with 20% volatility is not better than one that earns 6% with 5%. The Sharpe ratio puts them on one scale: the average return above the risk-free rate divided by the volatility, (mean - rf) / std, annualised by sqrt(periods). You met the formula in the topic on mean and variance. This topic is about its use: how to read a Sharpe ratio, improve on it, and above all how much to trust it. A Sharpe of 0.5 is mediocre, 1 is good, 2 is excellent and suspicious.
Volatility counts upside and downside equally, yet nobody complains about a surprise gain. The Sortino ratio keeps only the bad side: it replaces the standard deviation with the downside deviation, sqrt(mean(min(r - mar, 0)²)), where mar is the minimum acceptable return. Days above mar contribute zero, but they still count in the average. A strategy with big positive jumps and small losses scores much better on Sortino than on Sharpe.
import math
import numpy as np
import pandas as pd
returns = np.array([0.004, -0.002, 0.001, 0.003, -0.004, 0.002, 0.006, -0.001, -0.010, 0.004, 0.002, -0.003])
sharpe = returns.mean() / returns.std(ddof=1) * math.sqrt(252)
print(round(sharpe, 3)) # 0.598
mar = 0.0 # a day below zero is a bad day
downside = math.sqrt(np.mean(np.minimum(returns - mar, 0) ** 2))
sortino = (returns.mean() - mar) / downside * math.sqrt(252)
print(round(downside, 5), round(sortino, 3)) # 0.00329 0.804 the up days no longer count as riskWatching it move
One Sharpe ratio for the whole history hides the story. A rolling Sharpe ratio recomputes it on the last window returns, so you see the strategy's good and bad regimes. With pandas it is the rolling mean divided by the rolling standard deviation, annualised. Notice how wildly short windows jump: a month of data is nowhere near enough to rank strategies.
series = pd.Series(returns)
rolling = series.rolling(5)
rolling_sharpe = rolling.mean() / rolling.std() * math.sqrt(252)
print(rolling_sharpe.round(2).tolist())
# [nan, nan, nan, nan, 1.89, 0.0, 6.96, 4.97, -3.66, 0.51, 0.51, -4.69] a 5-day Sharpe ratio is mostly noiseHow much of it is luck?
A Sharpe ratio is an estimate, and estimates have error. For a per-period Sharpe ratio sr measured on n returns, the standard error is about sqrt((1 + 0.5 * sr²) / n). With our 12 days it is 0.29 per day against a Sharpe of 0.04: the error is seven times the estimate. Annualised, the same 12 days give 0.6 ± 4.6. For an annual Sharpe S observed over years, the t statistic is S * sqrt(years), so you need (1.96 / S)² years to be 95% sure it is not zero: 15 years for a Sharpe of 0.5, 4 years for a Sharpe of 1. Short backtests of modest strategies prove nothing.
per_period = returns.mean() / returns.std(ddof=1) # the per-day Sharpe ratio
std_error = math.sqrt((1 + 0.5 * per_period ** 2) / len(returns))
print(round(per_period, 4), round(std_error, 4)) # 0.0376 0.2888 the error is 7 times the estimate
print(round(std_error * math.sqrt(252), 3)) # 4.584 annualised: 0.6 +/- 4.6
# Years needed for an annual Sharpe ratio to reach t = 1.96
for annual in (0.5, 1.0, 2.0):
print(annual, round((1.96 / annual) ** 2, 1))
# 0.5 15.4
# 1.0 3.8
# 2.0 1.0# A yearly Sharpe of 1.0 observed over 4 years (1,008 days): how precise is it?
per_day = 1.0 / math.sqrt(252)
error_per_day = math.sqrt((1 + 0.5 * per_day ** 2) / 1008)
print(round(per_day, 4), round(error_per_day, 4), round(error_per_day * math.sqrt(252), 3)) # 0.063 0.0315 0.5 so 1.0 +/- 0.5- Sharpe =
(mean - rf) / std * sqrt(periods); Sortino replaces the std by the downside deviationsqrt(mean(min(r - mar, 0)²)). - A rolling Sharpe (
rolling.mean() / rolling.std()) shows how the ratio changes; short windows are mostly noise. - Its standard error is
sqrt((1 + 0.5 * sr²) / n)per period; the t statistic of an annual Sharpe isS * sqrt(years): a Sharpe of 1 needs about 4 years.
Next up: drawdowns, the losses from a peak that investors actually feel.
Lab · what's graded
- Penalise only the bad days with
sortino_ratio25 pts - Watch the ratio change over time with
rolling_sharpe25 pts - Size the luck in a Sharpe ratio with
sharpe_standard_error25 pts - Ask whether it is real with
sharpe_t_stat25 pts