Tutorial
Sharpe ratio: formula, what counts as good, and how to calculate it in Python
Over the year to 9 October 2026, the Sharpe ratio rated a near-record US500 at 0.88, under the rule-of-thumb mark of 1, and silver at 0.57 despite a 51.6% drawdown.
On this page
- The Sharpe ratio formula
- What is a good Sharpe ratio?
- Which risk-free rate to use
- How to calculate the Sharpe ratio in Python
- What the Sharpe ratio hides: max drawdown, Sortino and Calmar
- Why the same data gives different Sharpe ratios
- Sharpe ratio vs Sortino ratio, Calmar, Treynor and information ratio
- Questions
Key takeaways
- The Sharpe ratio is annualized return minus the risk-free rate, divided by annualized volatility: 0.88 for US500 over the year to 9 October 2026, against that day’s 4.25% Treasury bill yield.
- The usual rule of thumb calls 1 good, 2 very good and 3 excellent, but a one-year Sharpe ratio has a standard error of about 1.0, too wide to separate US:KO’s 1.37 from US500’s 0.88.
- A Sharpe ratio turns negative when the mean return falls short of the risk-free rate: BTCUSD scored -0.73 for the year to 9 October 2026, when its price fell 32.1%.
- Volatility does not show how losses arrive: silver scored 0.57 while falling 51.6% from its January high, and US:XOM, top of the eight at 1.53, fell 20.7%.
- Unlabeled choices move a Sharpe ratio: US500 scored 0.88 from the 9 October 2025 close and 1.12 from the next day’s.
The Sharpe ratio, named after the economist William F. Sharpe, measures return per unit of risk: an asset’s annualized return minus the risk-free rate, divided by the annualized volatility of its returns. A Sharpe ratio of 1 means one percentage point of return above cash for each point of volatility, which lets traders compare assets, funds and strategies that carry different risk. US500, the US 500 large-cap benchmark, closed 9 October 2026 near its record yet scored 0.88 for the year, against the 4.25% a 3-month Treasury bill yielded that day.
Gold scored 0.16: its daily returns beat the bill by only 4.4 points a year, against 27.5% volatility. Below, one script scores eight assets on Sharpe, Sortino, max drawdown and Calmar; a second recomputes US500 and BTCUSD ten ways.
The Sharpe ratio formula
Worked example: US500 over the year to 9 October 2026:
Sharpe ratio = (Rp − Rf) ÷ σp
- Rp
- Annualized return: the mean daily return × 252, or × 365 for an asset that trades every day.
- Rf
- Risk-free rate: the 3-month Treasury bill yield, 4.25% on 9 October 2026.
- σp
- Annualized volatility: the standard deviation of daily returns × √252, or × √365.
The return is the mean daily return times 252, not the price change, and for a volatile asset the two drift apart: silver averaged 38.2% a year against a 23.1% price gain, which would score 0.32 instead of 0.57. For US500 the gap is small, 0.88 against 0.90.
Annualizing treats daily returns as independent: the mean scales with the number of periods, the standard deviation with its square root. Weekday markets use 252 by convention, BTCUSD 365; the trading days guide shows how far real calendars stray from 252.
A Sharpe ratio is a slope
Plot volatility across and mean return up, then draw a line from the risk-free rate to each asset. Each line’s slope is that asset’s Sharpe ratio:
Risk and return, 9 October 2025 to 9 October 2026
- Asset, with its Sharpe ratio
- 3-month Treasury bill, 4.25%
Across: annualized volatility of daily returns. Up: annualized mean daily return (mean × 252, or × 365 for BTCUSD).
US:XOM and US:KO have the steepest rising lines; EURUSD and BTCUSD sit below the bill, so theirs point down. Slope matters more than height because mixing an asset with cash slides it along its own line: a mix kept at half US:KO and half bills at 4.25% would have averaged 17.2% with 9.5% volatility, beating US500 on both, and still scored 1.37.
What is a good Sharpe ratio?
The bands repeated across finance sites call a Sharpe ratio below 1 weak, 1 to 2 good, 2 to 3 very good and above 3 excellent. They are a rule of thumb, not a standard:
One-year Sharpe ratios against the usual rule-of-thumb bands
- -1.39EURUSD
- -0.73BTCUSD
- 0.16XAUUSD
- 0.57XAGUSD
- 0.88US500
- 1.0Good
- 1.37US:KO
- 1.53US:XOM
- 2.0Very good
- 3.0Excellent
Is 1.2 a good Sharpe ratio?
The bands call 1.2 good, but one year of data cannot tell it from 0.2 or 2.2. A Sharpe ratio from 251 daily returns has a standard error of about 1.0: the 95% range around US500’s 0.88 runs from -1.08 to 2.85. Even US:KO’s 1.37 is not statistically different from that 0.88: a Jobson-Korkie test with Memmel’s correction gives z = 0.32, far below 1.96.
At 0.88 the error falls to 0.71 with two years, 0.50 with four and 0.32 with ten: halving it takes four times the data. The approximation (Lo, 2002) assumes independent, normal returns, so skewed silver’s true error is larger.
What does a 2.5 Sharpe ratio mean?
A Sharpe ratio of 2.5 means 2.5 points of return above cash per point of volatility: rare over a year, where none of these eight reached 2, but common over a quarter. The trailing 63-session ratio was above 2.5 on 12% of the year’s US500 sessions and 30% of gold’s, whose one-year figure was 0.16:
Sharpe ratio of the trailing 63 sessions
- US500, one-year Sharpe 0.88
- XAUUSD, one-year Sharpe 0.16
63-session Sharpe ratio, annualized
Can a Sharpe ratio be negative?
Yes, whenever the mean return falls short of the risk-free rate: BTCUSD scored -0.73 after a 32.1% fall. EURUSD’s -1.39 subtracts the US bill rate from a spot return that leaves out the interest euros earn; against a zero rate it is -0.57. Below zero the ranking can mislead, because more volatility makes a shortfall look smaller: BTCUSD outscores EURUSD despite a far bigger loss.
Which risk-free rate to use
The risk-free rate is what cash earns without risk: the yield on a short government bill in the asset’s currency, over the same window. The script reads it from the bond snapshot endpoint for US:3M, the 3-month US Treasury bill at the short end of the yield curve, which returned "rate": 4.25 for "date": "2026-10-09".
The snapshot returns the latest observation, so the script applies 9 October’s 4.25% to every day, an approximation; set RISK_FREE=4.25 to reproduce these numbers later. A zero rate, a common shortcut, lifts US500 from 0.88 to 1.21.
How to calculate the Sharpe ratio in Python
sharpe.py pulls a year of daily closes for eight assets from the aggregate bars endpoints and prints Sharpe, Sortino, max drawdown and Calmar, then beta, R², Treynor and the information ratio for two stocks against US500. It needs Python 3.9 or newer, pip install pandas requests and a TICKERLAYER_API_KEY environment variable, and makes 9 requests.
"""Sharpe, Sortino, max drawdown and Calmar ratios from one year of daily closes."""
import math
import os
import pandas as pd
import requests
BASE_URL = "https://api.tickerlayer.com"
API_KEY = os.environ["TICKERLAYER_API_KEY"]
START, END = "2025-10-09", "2026-10-09" # first and last daily close
ASSETS = { # symbol: (asset class, return periods a year)
"US500": ("indices", 252),
"US:KO": ("stocks", 252),
"US:XOM": ("stocks", 252),
"US:GS": ("stocks", 252),
"XAUUSD": ("commodities", 252),
"XAGUSD": ("commodities", 252),
"EURUSD": ("forex", 252),
"BTCUSD": ("crypto", 365), # trades every day of the year
}
def get(path, **params):
"""GET a REST path. Returns None when the key's plan does not include it (403)."""
resp = requests.get(BASE_URL + path, params=params, headers={"x-api-key": API_KEY}, timeout=30)
if resp.status_code == 403:
return None
resp.raise_for_status()
return resp.json()
def risk_free():
"""Annual risk-free rate as a decimal, and where it came from."""
if os.environ.get("RISK_FREE"): # a fixed rate in percent, e.g. RISK_FREE=4.25
return float(os.environ["RISK_FREE"]) / 100, "RISK_FREE"
bill = get("/bond/snapshot/US:3M") # latest 3-month Treasury bill yield
if bill is None:
raise SystemExit("This key has no bond data: set RISK_FREE, for example RISK_FREE=4.25")
return bill["rate"] / 100, f"US:3M on {bill['date']}"
def daily_closes(asset, symbol, start=START, end=END):
"""Daily closes from start to end, or None when the plan does not include the asset."""
day_before = (pd.Timestamp(start) - pd.Timedelta(days=1)).strftime("%Y-%m-%d")
path = f"/{asset}/agg/{symbol}/1/day/{day_before}/{end}" # one extra day, trimmed below
params, rows = {"sort": "asc", "limit": 5000, "offset": 0}, []
while True:
body = get(path, **params)
if body is None:
return None
rows += body["results"]
if body.get("next_offset") is None:
break
params["offset"] = body["next_offset"]
if not rows:
raise SystemExit(f"No bars for {symbol}: check the symbol and the dates")
dates = pd.to_datetime([r["t"] for r in rows], unit="ms")
closes = pd.Series([r["c"] for r in rows], index=dates)
if asset != "crypto":
closes = closes[closes.index.dayofweek < 5] # weekday sessions only
return closes[closes.index >= start]
def sharpe(returns, rf, n):
"""Annualized Sharpe ratio of periodic returns, with n periods a year."""
excess = returns - rf / n
return excess.mean() / excess.std() * math.sqrt(n)
def metrics(closes, rf, n):
"""Return, mean, volatility, Sharpe, Sortino, max drawdown and Calmar of daily closes."""
r = closes.pct_change().dropna()
excess = r - rf / n
downside = math.sqrt((excess.clip(upper=0) ** 2).mean()) # shortfall below the risk-free rate
drawdown = closes / closes.cummax() - 1
one_year = closes.iloc[-1] / closes.iloc[0] - 1
return {
"return": one_year,
"mean": r.mean() * n,
"vol": r.std() * math.sqrt(n),
"sharpe": sharpe(r, rf, n),
"sortino": excess.mean() / downside * math.sqrt(n),
"max_dd": drawdown.min(),
"calmar": one_year / -drawdown.min(),
}
def main():
rf, source = risk_free()
print(f"Risk-free rate {rf:.2%} ({source}), daily closes {START} to {END}\n")
print(f"{'symbol':<8}{'return':>8}{'mean':>8}{'vol':>7}"
f"{'sharpe':>8}{'sortino':>9}{'max dd':>8}{'calmar':>8}")
closes = {}
for symbol, (asset, n) in ASSETS.items():
c = daily_closes(asset, symbol)
if c is None:
print(f"{symbol:<8}not in this key's plan, skipped")
continue
closes[symbol] = c
m = metrics(c, rf, n)
print(f"{symbol:<8}{m['return']:>8.1%}{m['mean']:>8.1%}{m['vol']:>7.1%}{m['sharpe']:>8.2f}"
f"{m['sortino']:>9.2f}{m['max_dd']:>8.1%}{m['calmar']:>8.2f}")
if "US500" not in closes:
return
print("\nAgainst US500")
for symbol in ("US:KO", "US:GS"):
if symbol not in closes:
continue
both = pd.concat([closes[symbol], closes["US500"]], axis=1, join="inner")
returns = both.pct_change().dropna() # same dates for stock and index
stock, market = returns.iloc[:, 0], returns.iloc[:, 1]
beta = stock.cov(market) / market.var()
treynor = (stock.mean() * 252 - rf) / beta
active = stock - market
info = active.mean() / active.std() * math.sqrt(252)
print(f"{symbol:<8}beta {beta:5.2f} R2 {stock.corr(market) ** 2:4.2f} "
f"Treynor {treynor:6.2f} information ratio {info:5.2f}")
if __name__ == "__main__":
main()Risk-free rate 4.25% (US:3M on 2026-10-09), daily closes 2025-10-09 to 2026-10-09
symbol return mean vol sharpe sortino max dd calmar
US500 16.0% 15.7% 13.0% 0.88 1.27 -9.1% 1.76
US:KO 32.7% 30.2% 19.0% 1.37 2.29 -8.5% 3.84
US:XOM 49.6% 43.8% 25.9% 1.53 2.26 -20.7% 2.40
US:GS 14.8% 19.0% 32.2% 0.46 0.67 -23.4% 0.63
XAUUSD 5.2% 8.7% 27.5% 0.16 0.22 -25.4% 0.20
XAGUSD 23.1% 38.2% 59.1% 0.57 0.75 -51.6% 0.45
EURUSD -3.1% -2.9% 5.1% -1.39 -1.88 -6.5% -0.48
BTCUSD -32.1% -28.6% 44.8% -0.73 -1.03 -51.8% -0.62
Against US500
US:KO beta -0.23 R2 0.02 Treynor -1.15 information ratio 0.59
US:GS beta 1.60 R2 0.42 Treynor 0.09 information ratio 0.13return is the price change from first close to last, used by Calmar; mean is the annualized average daily return, used by Sharpe and Sortino.
For gold, silver and EURUSD the script keeps one close per weekday, 262 each, so a weekend move lands in Monday’s return. US bars are raw prices: no split fell in the window, so they need no split adjustment, but dividends are left out, which understates the ratios of payers such as US:KO and US:XOM.
What the Sharpe ratio hides: max drawdown, Sortino and Calmar
Volatility treats gains and losses alike and ignores the order in which they arrive. Maximum drawdown measures the fall a holder sat through: the largest drop from a running peak close to a later trough, the figure the golden cross backtest prints next to annual growth.
Drawdown from the highest close so far
- XAGUSD, Sharpe 0.57
- US:XOM, Sharpe 1.53
- US500, Sharpe 0.88
% below the highest close since 9 October 2025
US:XOM, the highest Sharpe ratio of the eight at 1.53, lost 20.7% between 30 March and 29 June and was still 1.5% below that peak on 9 October. Silver, at 0.57, fell 51.6%, including 24.2% on 30 January alone. Sortino and Calmar put losses back into the ratio:
Sortino ratio = (Rp − Rf) ÷ σdMax drawdown = min over t of (Pt ÷ highest P so far − 1)Calmar ratio = one-year return ÷ |max drawdown|
- σd
- Downside deviation: the square root of the mean squared shortfall of daily returns below the risk-free rate (days above it count as zero), × √252. The other common recipe, the standard deviation of below-target days only, gives US:KO 2.58 instead of 2.29.
- Pt
- The close on day t; the highest P so far is the running peak since the start of the window.
| Symbol | Sharpe | Max drawdown | Calmar |
|---|---|---|---|
| US500 | 0.88 | -9.1% | 1.76 |
| US:KO | 1.37 | -8.5% | 3.84 |
| US:XOM | 1.53 | -20.7% | 2.40 |
| US:GS | 0.46 | -23.4% | 0.63 |
| XAUUSD | 0.16 | -25.4% | 0.20 |
| XAGUSD | 0.57 | -51.6% | 0.45 |
| EURUSD | -1.39 | -6.5% | -0.48 |
| BTCUSD | -0.73 | -51.8% | -0.62 |
Rank by Calmar instead of Sharpe and the order changes: US:KO (3.84) overtakes US:XOM (2.40), and US:GS (0.63) overtakes silver (0.45).
Why the same data gives different Sharpe ratios
A published Sharpe ratio often omits how it was computed, and the choices can move it further than the 0.49 that separates US:KO from US500. sharpe_traps.py recomputes US500 and BTCUSD ten ways:
Same data, different Sharpe ratio
- Start date.
US500scores 0.88 from the 9 October 2025 close and 1.12 from the next: the return between them, -2.71% on 10 October, was the window’s worst. - Sampling. Weekly closes, each week’s last session, give 1.18 but start after that fall too: frequency moves the ratio by 0.06, the start date by 0.24.
- Window.
US500scores 1.94 over six months; BTCUSD flips from -0.73 over the year to 2.81 over three months. - Annualization. Stock settings, 252 periods and √252, turn BTCUSD’s -28.6% mean and 44.8% volatility into -19.8% and 37.2%: -0.65 instead of -0.73.
"""Same asset, different Sharpe ratio: start date, sampling, window, annualization and Rf.
Save next to sharpe.py, which provides the helpers.
"""
import math
from sharpe import daily_closes, risk_free, sharpe
def since(closes, start):
"""Daily returns of the closes from `start` on."""
return closes[closes.index >= start].pct_change().dropna()
rf, source = risk_free()
us500 = daily_closes("indices", "US500")
btc = daily_closes("crypto", "BTCUSD")
if us500 is None or btc is None:
raise SystemExit("This script needs the Indices and Crypto feeds")
weekly = us500.resample("W-FRI").last().pct_change().dropna() # each week's last close
rows = [
("US500", "daily, from the 2025-10-09 close", sharpe(since(us500, "2025-10-09"), rf, 252)),
("US500", "daily, from the 2025-10-10 close", sharpe(since(us500, "2025-10-10"), rf, 252)),
("US500", "weekly, from the 2025-10-10 close", sharpe(weekly, rf, 52)),
("US500", "last 6 months", sharpe(since(us500, "2026-04-09"), rf, 252)),
("US500", "last 3 months", sharpe(since(us500, "2026-07-09"), rf, 252)),
("US500", "risk-free rate 0%", sharpe(since(us500, "2025-10-09"), 0.0, 252)),
("BTCUSD", "daily, 365 days a year", sharpe(since(btc, "2025-10-09"), rf, 365)),
("BTCUSD", "daily, 252 days a year", sharpe(since(btc, "2025-10-09"), rf, 252)),
("BTCUSD", "last 6 months", sharpe(since(btc, "2026-04-09"), rf, 365)),
("BTCUSD", "last 3 months", sharpe(since(btc, "2026-07-09"), rf, 365)),
]
print(f"Risk-free rate {rf:.2%} ({source})")
for symbol, label, value in rows:
print(f"{symbol:<8}{label:<35}{value:6.2f}")
daily = since(us500, "2025-10-09")
sr = sharpe(daily, rf, 252)
se = math.sqrt((1 + sr**2 / 252 / 2) * 252 / len(daily)) # Lo (2002), independent returns
print(f"\nUS500 one-year Sharpe {sr:.2f}, standard error {se:.2f}, "
f"95% range {sr - 1.96 * se:.2f} to {sr + 1.96 * se:.2f}")Risk-free rate 4.25% (US:3M on 2026-10-09)
US500 daily, from the 2025-10-09 close 0.88
US500 daily, from the 2025-10-10 close 1.12
US500 weekly, from the 2025-10-10 close 1.18
US500 last 6 months 1.94
US500 last 3 months 0.90
US500 risk-free rate 0% 1.21
BTCUSD daily, 365 days a year -0.73
BTCUSD daily, 252 days a year -0.65
BTCUSD last 6 months 0.82
BTCUSD last 3 months 2.81
US500 one-year Sharpe 0.88, standard error 1.00, 95% range -1.08 to 2.85The choices moved US500 by up to 1.06, about one standard error. None is wrong, but each needs its window, sampling and rate printed beside it.
Sharpe ratio vs Sortino ratio, Calmar, Treynor and information ratio
Each divides a return by a different measure of risk. Sharpe, Sortino and Calmar stand alone; Treynor and the information ratio need a benchmark, here US500:
| Feature | Divides | Example | Misleads when |
|---|---|---|---|
| Sharpe ratio | Excess return ÷ volatility | US500 0.88, US:KO 1.37 | Losses come in rare, deep falls: silver 0.57 with a 51.6% drawdown |
| Sortino ratio | Excess return ÷ downside deviation | US:KO 2.29 | Down days are few, so the divisor is tiny |
| Calmar ratio | Return ÷ maximum drawdown | US:KO 3.84, silver 0.45 | One bad stretch sets the divisor |
| Treynor ratio | Excess return ÷ beta | US:GS 0.09, US500 0.11 | Beta is near zero: US:KO’s -0.23 gives -1.15 |
| Information ratio | Return over a benchmark ÷ tracking error | US:KO 0.59 | The benchmark explains little: R² 0.02 for US:KO |
Both benchmark ratios assume US500 explains the stock. It explained 42% of US:GS’s daily moves, so its Treynor ratio of 0.09, 9.2% excess return per unit of beta against the index’s 11.5%, means something; for US:KO, at 2%, the -1.15 does not. The stock beta tutorial computes beta and R² over its own window.
Before you publish a Sharpe ratio
- Mean of daily returns, not the price change: silver’s price change gives 0.32 instead of 0.57
- √252 for weekday markets, √365 for crypto: BTCUSD is -0.73, or -0.65 on 252
- Window and first close stated: US500 scores 0.88 from 9 October 2025, 1.12 from the next day
- Risk-free rate in the asset’s currency: EURUSD is -1.39 against the US bill, -0.57 against zero
- Sortino convention named: US:KO is 2.29 with every day in the downside deviation, 2.58 with below-target days only
- Dividends stated: these US bars are price-only
- Max drawdown and standard error (about 1.0 for one year) published next to the ratio
Questions
What is a good Sharpe ratio?
By the usual rule of thumb, below 1 is weak, 1 to 2 good, 2 to 3 very good and above 3 excellent, though a one-year figure carries a standard error of about 1.0.
How do you calculate the Sharpe ratio?
Divide the mean of daily excess returns (each return minus the daily risk-free rate) by their standard deviation and multiply by √252, or √365 for crypto. In pandas: excess.mean() / excess.std() * math.sqrt(252).
Is 1.2 a good Sharpe ratio?
By the bands it is good, but one year of daily data cannot tell 1.2 from 0.2 or 2.2: in the year to 9 October 2026, US:KO’s 1.37 and US500’s 0.88 were not statistically different.
What does a 2.5 Sharpe ratio mean?
It means 2.5 points of return above cash for each point of volatility: rare over a year, common over a quarter. BTCUSD scored 2.81 over the three months to 9 October 2026 and -0.73 over the year.
Can a Sharpe ratio be negative?
Yes, whenever the mean return is below the risk-free rate. BTCUSD scored -0.73 for the year to 9 October 2026, after a 32.1% fall.
What is the difference between the Sharpe ratio and the Sortino ratio?
Sharpe divides excess return by the volatility of all returns; Sortino divides it by the downside deviation, so gains do not count as risk. In the year to 9 October 2026, US:KO scored 1.37 on Sharpe and 2.29 on Sortino.