Prism Data Lab

Every analysis here ships its dataset and the code that produced every figure. Nothing on this site is investment advice.

MethodsUpdated 1,076 words, 5 minutes

Sharpe, Sortino, and max drawdown computed by hand on the S&P 500

Sharpe, Sortino, and maximum drawdown worked step by step in Python on ten years of FRED's S&P 500 series, with a 3-month Treasury risk-free rate, year by year.

A Sharpe ratio is a mean divided by a standard deviation, and yet two people computing it on the same series routinely get different numbers. The differences come from decisions: which returns, which risk-free rate, which frequency, which annualisation, which degrees of freedom. This article makes every one of those decisions explicit, computes Sharpe, Sortino, and maximum drawdown on ten years of a public price series, and prints the results year by year so the sensitivity is visible. The data and the script are attached.

The data

FRED carries the S&P 500 daily close (SP500) with a ten-year rolling window, which the series notes attribute to the licensing agreement with S&P Dow Jones Indices. The file retrieved 2026-09-05 runs from 2016-09-06 to 2026-09-04, giving 2,513 daily returns. It is a price index: the notes say plainly that it "does not contain dividends", so every return here is a price return and understates a total return by the dividend yield.

The risk-free rate is DGS3MO, the 3-month constant-maturity Treasury yield, daily, in percent. Its mean over the sample is 2.48 percent.

Correction, September 18, 2026: the file attached to this article no longer contains the S&P 500 index levels. The FRED series notes carry the line "Reproduction of S&P 500 in any form is prohibited except with the prior written permission of S&P Dow Jones Indices LLC", and publishing the daily closes as a downloadable CSV was exactly that. The file now holds return_simple, the daily simple return computed from the series, alongside the DGS3MO risk-free proxy, which is a Federal Reserve series and is unaffected. The index is produced by and copyright S&P Dow Jones Indices LLC. Every statistic in this article is invariant to the scale of the price path, so the script rebuilds a rebased path that starts at 1.0 on the first day of the sample and computes exactly what it computed before: we re-ran it against the new file and every number in both tables below, including the peak, trough and recovery dates, is the number published here. To rebuild from the index itself, run python code/sharpe-sortino-max-drawdown-by-hand.py --download, which pulls SP500 and DGS3MO from FRED and writes the returns without storing the levels. FRED carries only a ten-year rolling window, so a pull today begins later than the sample reported here.

The definitions, in code

px = rebased_level(df["return_simple"])            # the rebased path, 1.0 on the first day of the sample
r = px.pct_change().dropna()                       # simple daily returns
rf = (rf_annual.shift(1).ffill().reindex(r.index) / 100.0) / DAYS   # DAYS = 252
e = (r - rf).dropna()                              # daily excess returns
sharpe = e.mean() / e.std(ddof=1) * math.sqrt(DAYS)
downside = math.sqrt((np.minimum(e, 0.0) ** 2).mean())
sortino = e.mean() / downside * math.sqrt(DAYS)
dd = px / px.cummax() - 1.0                        # drawdown from the running peak
max_dd = dd.min()

Decisions, one by one. The daily risk-free rate is the previous day's 3-month yield divided by 252, which treats the yield as a simple annual rate; a compounding version changes the fourth decimal. The excess return is computed daily and then annualised by multiplying the mean-over-standard-deviation by the square root of 252, which is the convention Sharpe's 1994 article describes as appropriate when returns are roughly uncorrelated across periods. The standard deviation uses one degree of freedom (ddof=1); pandas defaults to that and NumPy does not, which is one of the classic sources of a small mismatch. The Sortino denominator is the root mean square of the negative excess returns with zeros for the positive days, the whole-sample count in the denominator, and no target other than the risk-free rate. Maximum drawdown is measured on the price level, not on excess returns, from the running peak.

The results

Output of python code/sharpe-sortino-max-drawdown-by-hand.py on 2026-09-05, sample 2016-09-06 to 2026-09-04:

statistic value
total price return 253.0%
annualised price return (geometric) 13.48%
annualised volatility (ddof=1) 18.09%
mean risk-free rate used (DGS3MO) 2.48%
Sharpe ratio (annualised, daily excess returns) 0.65
Sortino ratio (annualised) 0.91
maximum drawdown -33.9%
drawdown peak / trough / recovery 2020-02-19 / 2020-03-23 / 2020-08-18
worst day -11.98% on 2020-03-16
best day 9.52% on 2025-04-09

By calendar year, where each year's first return uses the prior year's last close:

calendar year trading days price return ann. vol Sharpe Sortino max drawdown within year
2016 (from Sep 6) 81 2.4% 10.5% 0.72 1.05 -4.6%
2017 251 19.4% 6.7% 2.56 3.98 -2.8%
2018 251 -6.2% 17.1% -0.41 -0.53 -19.8%
2019 252 28.9% 12.5% 1.93 2.77 -6.8%
2020 253 16.3% 34.4% 0.60 0.82 -33.9%
2021 252 26.9% 13.1% 1.88 2.79 -5.2%
2022 251 -19.4% 24.2% -0.86 -1.18 -25.4%
2023 250 24.2% 13.1% 1.33 2.02 -10.3%
2024 252 23.3% 12.6% 1.31 1.86 -8.5%
2025 250 16.4% 18.8% 0.68 1.01 -18.9%
2026 (to Sep 4) 170 12.8% 13.4% 1.12 1.65 -9.1%

The same ten years measured with monthly returns give a Sharpe of 0.75 rather than 0.65. Daily returns have skewness of -0.37 and excess kurtosis of 15.8.

What the numbers show

The ten-year Sharpe of 0.65 is an average over years that range from 2.56 (2017, a year with 6.7 percent volatility and a worst drawdown of 2.8 percent) to -0.86 (2022). The yearly column is the more honest picture: a single-number Sharpe for a decade compresses regimes that have little in common, and a fund quoting "a Sharpe of 1.3" over three good years is quoting something that this table shows can be followed by a -0.9.

Sortino exceeds Sharpe in every positive year, as it must when the downside deviation is smaller than the total deviation, and the ratio between them is not constant: 1.6 in 2017 and 1.4 in 2020. The gap says something about the shape of the return distribution, and nothing about which statistic is "right".

The maximum drawdown of 33.9 percent took 23 trading days to happen and 104 to recover, on a price basis. The 2022 drawdown of 25.4 percent within the year was slower on both sides. Drawdown is path-dependent in a way the other two statistics are not, which is why it cannot be annualised or compared across sample lengths: a longer sample can only have a deeper maximum drawdown, never a shallower one.

The monthly-versus-daily gap of 0.10 is the frequency decision at work. With a kurtosis of 15.8 the daily returns are far from normal, and the square-root-of-time annualisation that Sharpe's article conditions on serial independence is an approximation whose error is visible in that gap.

What they do not show

None of these are forecasts, and none are guidance about any asset. A price index without dividends understates the return of anyone who held the index, and the risk-free proxy is one of several possible choices. Two people who make different choices here will publish different Sharpe ratios for the same series, and both can be arithmetically correct. The script is attached so that the choices can be changed and the table regenerated.

This article is analysis and education, not investment, tax, or legal advice. Figures are cited to their source and dated; check them before relying on them.