Strasmore Research
Learn Matt ConnorBy Matt Connor

What Is the Sharpe Ratio? Formula and Math

The Sharpe ratio is average excess return divided by the standard deviation of excess returns. Get the formula right, then see where the number misleads.

The Sharpe ratio measures how much return an investment delivered for each unit of volatility it put you through. The formula is short: average the returns above a risk-free rate, then divide by the standard deviation of those same excess returns. Both halves of that fraction are choices, and this page shows how far the answer moves when you change them.

The Sharpe ratio formula, in plain English

Pick one sampling period and stay in it, a day or a month. For each period, subtract the risk-free return for that period from the investment's return. What is left is the excess return: the part you were paid for taking risk. Average the excess returns across the sample, then divide by their standard deviation, which is the typical distance between a single period's excess return and that average.

Sharpe = average excess return / standard deviation of excess returns

Two details separate a careful calculation from a sloppy one. The denominator is the standard deviation of the excess series rather than of the raw return series (with a constant rate the two match, and with a rate that moves every period they do not). The standard deviation is the sample version, dividing by n minus 1.

What is a good Sharpe ratio?

No threshold makes a number good, and a ratio computed over a single calendar year is a sample rather than a property of the asset. Context comes from comparison. The panel below ranks six household names over the same 2025 window, using the same assumed 4.25% annual risk-free rate for every one of them.

QueryAnnualized Sharpe ratio by name, calendar 2025, assumed 4.25% risk-free rate
The exact SQL behind every number
WITH
    daily AS
    (
        SELECT
            ticker                                               AS symbol,
            toDate(toTimeZone(window_start, 'America/New_York')) AS session_date,
            argMax(toFloat64(close), window_start)               AS close_px
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'NVDA', 'KO')
          AND window_start >= toDateTime('2024-12-24 00:00:00')
          AND window_start <  toDateTime('2026-01-01 05:00:00')
          AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
             + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
          AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
             + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
        GROUP BY symbol, session_date
    ),
    stepped AS
    (
        SELECT
            symbol,
            session_date,
            close_px,
            lagInFrame(close_px, 1) OVER (PARTITION BY symbol ORDER BY session_date
                                          ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_px
        FROM daily
    ),
    excess AS
    (
        SELECT
            symbol,
            close_px / prev_px - 1                 AS raw_ret,
            close_px / prev_px - 1 - 0.0425 / 252  AS ex_ret
        FROM stepped
        WHERE prev_px > 0
          AND session_date >= toDate('2025-01-01')
    )
SELECT
    symbol,
    round(avg(ex_ret) * 252 * 100, 2)                      AS ann_excess_return_pct,
    round(stddevSamp(raw_ret) * sqrt(252) * 100, 2)        AS ann_volatility_pct,
    round(avg(ex_ret) / stddevSamp(ex_ret) * sqrt(252), 2) AS sharpe_ratio
FROM excess
GROUP BY symbol
HAVING count() > 200
ORDER BY sharpe_ratio DESC
Run this yourself

NVDA leads the group at 0.83 on 49.4% annualized volatility. The last row, AAPL, scores 0.28 on 32.32%. Read the volatility column on its own and it tells you little about the ranking: a calm name with a flat year and a jumpy name with a strong year can land in the same place, which is the entire point of dividing one by the other. The low volatility anomaly is the long-running observation that calmer names have historically scored better here than their raw returns alone suggest.

How do you annualize a Sharpe ratio?

A ratio built from daily returns cannot be compared with one built from monthly returns until both sit on the same clock. Scaling a mean and a standard deviation follows two different rules. Across n periods the mean excess return grows with n, while the standard deviation of the sum grows with the square root of n. Divide one by the other and a factor of the square root of n survives.

Those are the annualizing multipliers in common use: the square root of 252 for daily returns, about 15.87, on a 252-session trading year; the square root of 52 for weekly, about 7.21; the square root of 12 for monthly, about 3.46; the square root of 4 for quarterly, exactly 2.

The step assumes returns are independent from one period to the next. Streaks break that assumption, and the same asset over the same window can annualize to different numbers from different sampling frequencies. The panel runs all three on one year of SPY.

QueryOne year of SPY, three sampling frequencies, one annualized Sharpe ratio
The exact SQL behind every number
WITH
    daily AS
    (
        SELECT
            toDate(toTimeZone(window_start, 'America/New_York')) AS session_date,
            argMax(toFloat64(close), window_start)               AS close_px
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ticker = 'SPY'
          AND window_start >= toDateTime('2024-12-24 00:00:00')
          AND window_start <  toDateTime('2026-01-01 05:00:00')
          AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
             + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
          AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
             + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
        GROUP BY session_date
    ),
    freqs AS
    (
        SELECT
            arrayJoin([('Daily', 252), ('Weekly', 52), ('Monthly', 12)]) AS pair,
            pair.1                                                       AS sampling,
            pair.2                                                       AS periods_per_year
    ),
    bucketed AS
    (
        SELECT
            f.sampling                         AS sampling,
            f.periods_per_year                 AS periods_per_year,
            multiIf(f.sampling = 'Daily',  d.session_date,
                    f.sampling = 'Weekly', toMonday(d.session_date),
                                           toStartOfMonth(d.session_date)) AS period_key,
            argMax(d.close_px, d.session_date) AS period_close
        FROM daily AS d
        CROSS JOIN freqs AS f
        GROUP BY sampling, periods_per_year, period_key
    ),
    stepped AS
    (
        SELECT
            sampling,
            periods_per_year,
            period_key,
            period_close,
            lagInFrame(period_close, 1) OVER (PARTITION BY sampling ORDER BY period_key
                                              ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_close
        FROM bucketed
    ),
    per_period AS
    (
        SELECT
            sampling,
            periods_per_year,
            period_close / prev_close - 1 - 0.0425 / periods_per_year AS ex_ret
        FROM stepped
        WHERE prev_close > 0
          AND period_key >= toDate('2025-01-01')
    )
SELECT
    sampling,
    round(avg(ex_ret) / stddevSamp(ex_ret), 4)                          AS per_period_sharpe,
    round(avg(ex_ret) / stddevSamp(ex_ret) * sqrt(periods_per_year), 2) AS annualized_sharpe
FROM per_period
GROUP BY sampling, periods_per_year
ORDER BY periods_per_year DESC
Run this yourself

Daily sampling annualizes to 0.68, and Monthly sampling of that same year gives 1.04. The per-period column is the other half of the lesson. Before annualizing, the daily figure is 0.0431, a number that looks like nothing at all and carries exactly the same information.

Work the math yourself in a terminal

The arithmetic is small enough to check by hand. The block below is a self-contained script: it carries its own data, imports only the Python standard library, and installs nothing. Paste it into any machine with python3 and it prints the same numbers every time. The twelve monthly returns are illustrative figures for a made-up portfolio, not market data.

python3 - <<'PY'
from math import sqrt
from statistics import mean, stdev

# 12 illustrative monthly returns, as decimals: 0.021 = +2.1%
rets = [0.021, -0.008, 0.034, 0.012, -0.025, 0.018,
        0.007, 0.029, -0.014, 0.022, 0.005, 0.016]

rf_annual = 0.0425                       # assumed T-bill yield, annual
rf_period = (1 + rf_annual) ** (1 / 12) - 1

excess = [r - rf_period for r in rets]
m = mean(excess)
s = stdev(excess)                        # sample stdev, divides by n-1
sharpe = m / s
se = sqrt((1 + 0.5 * sharpe ** 2) / len(rets))

print('observations       :', len(rets))
print('mean excess/month  : %.5f' % m)
print('stdev/month        : %.5f' % s)
print('monthly Sharpe     : %.4f' % sharpe)
print('annualized Sharpe  : %.3f' % (sharpe * sqrt(12)))
print('std error, annual  : %.3f' % (se * sqrt(12)))
PY

It reports a mean excess return near 0.63% a month, a monthly standard deviation near 1.77%, a monthly Sharpe of 0.3536, and 1.225 after the square-root-of-12 step. The last line is the one worth staring at: twelve observations put the standard error of that estimate near 1.03 in annualized terms, roughly the size of the estimate itself. A "1.2 Sharpe" measured over one year of monthly data sits comfortably inside a range running from clearly negative to above 3. The formula in the script is the standard independent-and-identically-distributed approximation, and serial correlation in real returns widens the interval further. How monthly returns are measured covers the sampling choices that feed a series like this.

Which risk-free rate belongs in the numerator?

The risk-free rate is the return available over the same window without taking market risk. The standard proxy is short-dated US Treasury bills, usually the 1-month or 3-month bill, quoted as an annual yield and converted down to the period you are working in. One practical trap: the price chart of a bill ETF is not its return. Those funds pay their income out monthly, so the price series alone understates the yield by most of it. Take the quoted bill yield for the window instead.

The panel holds one return series fixed and sweeps the assumed annual rate from zero to five percent.

QuerySame SPY 2025 returns, six assumed risk-free rates
The exact SQL behind every number
WITH
    daily AS
    (
        SELECT
            toDate(toTimeZone(window_start, 'America/New_York')) AS session_date,
            argMax(toFloat64(close), window_start)               AS close_px
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ticker = 'SPY'
          AND window_start >= toDateTime('2024-12-24 00:00:00')
          AND window_start <  toDateTime('2026-01-01 05:00:00')
          AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
             + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
          AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
             + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
        GROUP BY session_date
    ),
    stepped AS
    (
        SELECT
            session_date,
            close_px,
            lagInFrame(close_px, 1) OVER (ORDER BY session_date
                                          ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_px
        FROM daily
    ),
    rets AS
    (
        SELECT close_px / prev_px - 1 AS raw_ret
        FROM stepped
        WHERE prev_px > 0
          AND session_date >= toDate('2025-01-01')
    ),
    rates AS
    (
        SELECT
            arrayJoin([(0.00, '0.00%'), (0.01, '1.00%'), (0.02, '2.00%'),
                       (0.03, '3.00%'), (0.04, '4.00%'), (0.05, '5.00%')]) AS pair,
            pair.1                                                         AS rf_annual,
            pair.2                                                         AS assumed_risk_free
    )
SELECT
    assumed_risk_free,
    round(avg(raw_ret - rf_annual / 252) * 252 * 100, 2)              AS ann_excess_return_pct,
    round(stddevSamp(raw_ret - rf_annual / 252) * sqrt(252) * 100, 2) AS ann_volatility_pct,
    round(avg(raw_ret - rf_annual / 252)
          / stddevSamp(raw_ret - rf_annual / 252) * sqrt(252), 2)     AS sharpe_ratio
FROM rets
CROSS JOIN rates
GROUP BY rf_annual, assumed_risk_free
ORDER BY rf_annual
Run this yourself

The same 2025 series scores 0.91 against a zero rate and 0.64 against an assumed 5.00% rate. The volatility column stays where it is, 18.62% in the first row and 18.62% in the last: subtracting a constant from every period shifts the average and leaves the spread alone. A Sharpe ratio quoted without its rate assumption is missing an input you need to reproduce it.

Why a negative Sharpe ratio cannot be ranked

When the average excess return is negative, the ratio inverts its own meaning. A negative numerator divided by a larger denominator lands closer to zero, so the more volatile of two losing positions prints the better-looking score. Nothing in that ordering is usable. Treat a negative Sharpe as one bit of information, the return trailed cash over the window, and compare the numerator and the denominator separately from there.

Sharpe versus Sortino: what the denominator punishes

Standard deviation counts distance from the average in both directions. A month far above the average adds as much to the denominator as a month equally far below it, so a portfolio is charged for its best periods. The Sortino ratio swaps in downside deviation: the root mean square of the shortfalls below a target, with everything above the target entered as zero.

QuerySharpe against Sortino, six names, calendar 2025
The exact SQL behind every number
WITH
    daily AS
    (
        SELECT
            ticker                                               AS symbol,
            toDate(toTimeZone(window_start, 'America/New_York')) AS session_date,
            argMax(toFloat64(close), window_start)               AS close_px
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'NVDA', 'KO')
          AND window_start >= toDateTime('2024-12-24 00:00:00')
          AND window_start <  toDateTime('2026-01-01 05:00:00')
          AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
             + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
          AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
             + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
        GROUP BY symbol, session_date
    ),
    stepped AS
    (
        SELECT
            symbol,
            session_date,
            close_px,
            lagInFrame(close_px, 1) OVER (PARTITION BY symbol ORDER BY session_date
                                          ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_px
        FROM daily
    ),
    excess AS
    (
        SELECT
            symbol,
            close_px / prev_px - 1 - 0.0425 / 252 AS ex_ret
        FROM stepped
        WHERE prev_px > 0
          AND session_date >= toDate('2025-01-01')
    )
SELECT
    symbol,
    round(avg(ex_ret) / stddevSamp(ex_ret) * sqrt(252), 2)                     AS sharpe_ratio,
    round(avg(ex_ret) / sqrt(avg(pow(least(ex_ret, 0.0), 2))) * sqrt(252), 2)  AS sortino_ratio,
    round(countIf(ex_ret < 0) * 100.0 / count(), 1)                            AS down_day_pct
FROM excess
GROUP BY symbol
HAVING count() > 200 AND countIf(ex_ret < 0) > 0
ORDER BY sortino_ratio DESC
Run this yourself

NVDA carries the highest Sortino of the six at 1.18 against a Sharpe of 0.83, with 46.4% of its sessions landing below the daily risk-free hurdle. Where the two columns separate most, the return series was lopsided: the gains arrived in a few large steps that standard deviation charged against the score and downside deviation ignored. Neither ratio describes the worst stretch an investor actually held through, which is what maximum drawdown measures.

How noisy is a Sharpe ratio estimate?

The panel below runs one calculation on one asset, a calendar year at a time, holding the assumed rate fixed at 4.25% throughout, leaving the return series as the only thing that changes between rows.

QuerySPY annualized Sharpe ratio, year by year, fixed 4.25% assumed rate
The exact SQL behind every number
WITH
    daily AS
    (
        SELECT
            toDate(toTimeZone(window_start, 'America/New_York')) AS session_date,
            argMax(toFloat64(close), window_start)               AS close_px
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ticker = 'SPY'
          AND window_start >= toDateTime('2015-12-24 00:00:00')
          AND window_start <  toDateTime('2026-01-01 05:00:00')
          AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
             + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
          AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
             + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
        GROUP BY session_date
    ),
    stepped AS
    (
        SELECT
            session_date,
            close_px,
            lagInFrame(close_px, 1) OVER (ORDER BY session_date
                                          ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_px
        FROM daily
    ),
    rets AS
    (
        SELECT
            session_date,
            close_px / prev_px - 1                AS raw_ret,
            close_px / prev_px - 1 - 0.0425 / 252 AS ex_ret
        FROM stepped
        WHERE prev_px > 0
          AND session_date >= toDate('2016-01-01')
    )
SELECT
    toString(toYear(session_date))                         AS year,
    round(avg(ex_ret) / stddevSamp(ex_ret) * sqrt(252), 2) AS sharpe_ratio,
    round(stddevSamp(raw_ret) * sqrt(252) * 100, 2)        AS ann_volatility_pct
FROM rets
GROUP BY year
ORDER BY year
Run this yourself

The series opens at 0.44 in 2016 and ends at 0.68 in 2025, and the line between those two points travels a long way for an asset whose composition barely changed. One year of daily returns is a thin base for a figure quoted to two decimals, and a Sharpe ratio without its measurement window attached cannot be compared with anything.

Why backtest Sharpe ratios run high

Two mechanical errors inflate a strategy's Sharpe before a single share trades. The first is look-ahead: the rule uses information that was not available at the moment it claims to have acted, a closing price applied to a decision made at noon, or a restated fundamental figure treated as though it were known the day the period ended. Look-ahead bias in backtesting walks through the places it hides. The second is survivorship: a test universe assembled from the names that still exist today, with the delisted ones absent from the sample entirely. Both lift the numerator and calm the denominator, the exact combination this ratio rewards.

A third is subtler. Try enough parameter sets against one history and the best Sharpe among them is the maximum of many noisy draws, which sits higher than any single honest estimate. Position-sizing rules inherit that error directly, since they take the estimated edge as an input: Kelly criterion position sizing shows what an overstated edge does to a bet size.

FAQ

What is a good Sharpe ratio?

There is no universal cutoff. An annualized Sharpe near 1 over a long window is commonly described as strong for a single asset, though the figure moves with the measurement window, the sampling frequency, the assumed risk-free rate, and the asset itself. Compare like windows with like windows.

How do you annualize a Sharpe ratio?

Multiply the per-period Sharpe by the square root of the number of periods in a year: about 15.87 for daily returns on a 252-session year, 7.21 for weekly, 3.46 for monthly, and exactly 2 for quarterly. The mean scales with the number of periods and the standard deviation with the square root of it, which leaves the square root factor behind.

What is the difference between the Sharpe ratio and the Sortino ratio?

They share a numerator, the average excess return. Sharpe divides by the standard deviation of every period's excess return. Sortino divides by downside deviation, which counts only the periods that fell below a target and enters the rest as zero. Sortino leaves upside volatility unpunished.

Can the Sharpe ratio be negative?

Yes, whenever the average return falls short of the risk-free rate over the window. Two negative Sharpe ratios cannot be ranked against each other: adding volatility to a losing series pushes the number toward zero, making the worse holding print the better score.

What risk-free rate is used in the Sharpe ratio?

Short-dated US Treasury bills, usually the 1-month or 3-month, matched to the period of the returns being measured. Whichever one is used, state it alongside the ratio: the sweep above shows a single unchanged return series scoring differently across a five-point range of assumptions.


Every panel here carries the SQL that produced it. Open one to see the exact window, the assumed rate, and the aggregate behind the number. To run the same calculation on a name and a window you care about, ask for it in plain English on the Strasmore terminal.

#sharpe ratio#risk-adjusted return#volatility#portfolio metrics#sortino