17 — Backtest & Validate

Sharpe Ratio & Risk-Adjusted Returns#

PerformanceStatistical 1 min read
◆ The PatternInterpreting Sharpe, Sortino, Calmar — return per unit of risk

Raw returns are meaningless without risk context. The Sharpe ratio measures excess return per unit of total volatility. Sortino only penalises downside volatility. Calmar divides return by maximum drawdown — the worst-case measure.

Sharpe = (Rp − Rf) / σp
Sharpe = annualised excess return / annualised std dev. >1 is good, >2 is excellent.
Sortino = (Rp − Rf) / σdownside
Sortino = only penalises negative volatility. Better for asymmetric return distributions.
Calmar = Rannual / |MaxDrawdown|
Calmar = return divided by worst-case loss. Sensitive to tail risk.
// Interactive — adjust return and volatility, see ratios
Return %12%
Volatility %15%
Sharpe—
Pattern bridge: The Sharpe ratio is signal-to-noise for finance. In ML, the same concept appears as comparing model runs — is the improvement larger than the noise? Both ask: is this real or random?
Worked example

A strategy with an annual Sharpe ratio of 1, measured on three years of monthly returns. Its standard error is about 0.59, so the 95% interval runs from −0.15 to 2.15. Three years cannot tell a good strategy from a useless one — or from an excellent one.

Where it misleads
  • Autocorrelated returns (smoothed or illiquid assets) make the usual √12 annualisation overstate the Sharpe ratio (Lo 2002).
  • The ratio treats upside and downside volatility alike; skewed strategies (option selling) look better than they are.
  • Comparing the best of many strategies by Sharpe needs a correction for the number tried.
In code
sr_annual, months = 1.0, 36                         # a Sharpe of 1, from three years of monthly returns
sr_m = sr_annual / np.sqrt(12)
se_m = np.sqrt((1 + 0.5 * sr_m ** 2) / months)       # Lo (2002), i.i.d. returns
se_annual = se_m * np.sqrt(12)
ci = (sr_annual - 1.96 * se_annual, sr_annual + 1.96 * se_annual)
Python, in your browser — edit the code and run it again

Assumes import numpy as np and import pandas as pd (and SciPy where imported). Each snippet simulates or makes up its own data, as the comments say.

The same idea with a library
# Python — risk-adjusted ratios
import numpy as np

returns = df['returns']
rf = 0.05 / 252  # daily risk-free rate

# Sharpe (annualised)
sharpe = (returns.mean() - rf) / returns.std() * np.sqrt(252)

# Sortino (downside only)
down = returns[returns < 0].std()
sortino = (returns.mean() - rf) / down * np.sqrt(252)

# Calmar
cum = (1 + returns).cumprod()
dd = (cum / cum.cummax() - 1).min()
calmar = returns.mean() * 252 / abs(dd)
Sources
  • A. W. Lo, “The Statistics of Sharpe Ratios”, Financial Analysts Journal 58(4), 2002
  • W. F. Sharpe, “The Sharpe Ratio”, Journal of Portfolio Management 21(1), 1994
  • D. H. Bailey & M. López de Prado, “The Sharpe Ratio Efficient Frontier”, Journal of Risk 15(2), 2012

Last reviewed October 2026 · How this site checks its content · Report a mistake

Linked from 2
← Previous
Sampling & Class Imbalance
Open in the full reader, with the topic sidebar