Every fund factsheet shows a Sharpe ratio. The better ones also show Sortino — and when the two tell different stories, that gap is information.
Sharpe divides excess return by total volatility: upside surprises are punished exactly like losses. Sortino divides by downside deviation only — volatility you would happily take (big up days) costs you nothing.
Here is both, in five lines:
import numpy as np r = np.random.default_rng(14).normal(0.0004, 0.01, 252) - 0.02 / 252 # daily excess returns sharpe = r.mean() / r.std(ddof=1) * np.sqrt(252) downside = np.sqrt(np.mean(np.minimum(r, 0.0) ** 2)) * np.sqrt(252) sortino = r.mean() * 252 / downside print(f"Sharpe {sharpe:.2f} Sortino {sortino:.2f}")
Output: Sharpe 1.04 Sortino 1.55.
Three things worth internalising:
-
For symmetric (normal-ish) returns, Sortino ≈ √2 × Sharpe. For a zero-mean normal distribution, the expected squared downside is exactly half the variance — so the ratio of the two ratios is mechanically √2 ≈ 1.41. Our simulated series lands at 1.55/1.04 ≈ 1.49: close to √2, because the draw is roughly symmetric. A Sortino/Sharpe ratio near 1.4 tells you nothing about skill — it is what randomness produces.
-
The interesting funds are the ones far from √2. A trend-following CTA with a long right tail (many small losses, a few huge wins) shows Sortino ≫ √2 × Sharpe. A short-volatility strategy — steady small gains, rare disasters — shows the opposite until the disaster is in the sample. If Sortino/Sharpe ≈ 2.5, ask about skew before you ask about alpha.
-
Downside deviation uses min(r, 0), not a filter. A common bug is
r[r < 0].std()— dropping the non-negative days changes the denominator's sample size and inflates the ratio. The convention divides the squared shortfalls by all n observations, which is whatnp.meandoes above.
Both ratios annualise with √252 — keep the numerator and denominator on the same clock, or you will manufacture performance out of unit errors.