Strasmore Research
Learn · Matt ConnorBy Matt Connor ·

The Low-Volatility Anomaly

The low-volatility anomaly says more risk does not reliably pay. We line up two years of realized volatility against the return each large-cap stock delivered.

Finance theory holds that an investor who accepts more risk should be paid more return for carrying it. The low-volatility anomaly is the long-running observation that this often fails. Over many stretches of market history, calmer stocks have kept pace with or beaten wilder ones while putting their holders through far smaller swings. This post lines up realized volatility against total return for 25 large, familiar stocks over roughly the past two years, and the relationship comes out loose and noisy rather than the tidy upward line the textbook draws.

What the low-volatility anomaly is

Volatility here means annualized volatility: the standard deviation of a stock's daily returns, scaled up to a yearly figure. A number near 16% is calm for a stock, and a figure above 50% marks a name that routinely moves several percent in a day. The Capital Asset Pricing Model, the framework taught in every finance course, predicts a straight upward line, where higher volatility earns higher expected return.

Decades of academic work found the opposite tilt. Low-volatility portfolios have historically delivered returns comparable to, and sometimes better than, high-volatility ones, at a fraction of the risk. That gap between the theory and the record is the anomaly. It is one of the most-studied patterns in markets, and it sits behind the "min-vol" and "low-vol" funds sold today.

Volatility against return, stock by stock

The chart below takes 25 household large caps, computes each one's annualized volatility and its total return over the same window, and sorts them from calmest to wildest. If the textbook held, the return bars would climb in step with the volatility bars from left to right.

QueryAnnualized volatility vs total return, 25 large caps, calmest to wildest (~2 years)
The exact SQL behind every number
WITH d AS (
    SELECT ticker,
           toDate(toTimeZone(window_start, 'America/New_York')) AS dt,
           argMax(toFloat64(close), toTimeZone(window_start, 'America/New_York')) AS c
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('SPY','KO','JNJ','PG','PEP','WMT','MCD','MO','VZ','XOM','JPM','HD','COST','UNH','MRK','LLY','HON','LMT','CAT','TXN','ORCL','NVDA','TSLA','AMD','NFLX')
      AND window_start >= now() - INTERVAL 800 DAY
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
    GROUP BY ticker, dt
),
r AS (
    SELECT ticker, dt, c,
           c / lagInFrame(c) OVER (PARTITION BY ticker ORDER BY dt) - 1 AS ret
    FROM d
)
SELECT ticker,
       round(stddevSamp(ret) * sqrt(252) * 100, 1) AS annual_vol_pct,
       round((exp(sum(log(1 + ret))) - 1) * 100, 1) AS total_return_pct
FROM r
WHERE ret IS NOT NULL AND ret > -0.5 AND ret < 0.5
GROUP BY ticker
ORDER BY annual_vol_pct

They do not. The calmest name in the set is the S&P 500 itself, ticker SPY, at 16% annualized volatility for a 45.9% total return. Reading across, some of the calmest single stocks posted strong gains while several of the most volatile names landed near flat or in the red. The return bars scatter. They do not track the volatility bars in any straight-line way, which is the anomaly in one picture: the extra volatility on the right did not buy a dependable step up in return.

What the extra volatility actually bought

Grouping the 25 names into thirds by volatility sharpens the point. Each third gets a median return and a range from its worst performer to its best, so the spread of outcomes is visible next to the average.

QueryVolatility thirds: median return, and the range from worst to best name in each
The exact SQL behind every number
WITH d AS (
    SELECT ticker,
           toDate(toTimeZone(window_start, 'America/New_York')) AS dt,
           argMax(toFloat64(close), toTimeZone(window_start, 'America/New_York')) AS c
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('SPY','KO','JNJ','PG','PEP','WMT','MCD','MO','VZ','XOM','JPM','HD','COST','UNH','MRK','LLY','HON','LMT','CAT','TXN','ORCL','NVDA','TSLA','AMD','NFLX')
      AND window_start >= now() - INTERVAL 800 DAY
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
    GROUP BY ticker, dt
),
r AS (
    SELECT ticker, dt,
           c / lagInFrame(c) OVER (PARTITION BY ticker ORDER BY dt) - 1 AS ret
    FROM d
),
pername AS (
    SELECT ticker,
           stddevSamp(ret) * sqrt(252) * 100 AS vol,
           (exp(sum(log(1 + ret))) - 1) * 100 AS tr
    FROM r
    WHERE ret IS NOT NULL AND ret > -0.5 AND ret < 0.5
    GROUP BY ticker
),
ranked AS (
    SELECT ticker, vol, tr, ntile(3) OVER (ORDER BY vol) AS bucket FROM pername
)
SELECT multiIf(bucket = 1, '1. Calmest third', bucket = 2, '2. Middle third', '3. Wildest third') AS vol_group,
       round(median(vol), 1) AS median_vol_pct,
       round(median(tr), 1) AS median_return_pct,
       round(min(tr), 1) AS worst_return_pct,
       round(max(tr), 1) AS best_return_pct,
       round(max(tr) - min(tr), 0) AS range_pct
FROM ranked
GROUP BY bucket
ORDER BY bucket

Over this particular window the wildest third did post the highest median return, 93.4% against 18.8% for the calmest third. That is honest, and it matters. A two-year window is short, and the past two years rewarded a handful of large, volatile technology and momentum names. What the wildest third also bought was dispersion. Its returns ran from -17% to +242.1%, a spread of 259 percentage points. The calmest third fit inside a spread of 90 points. High volatility widened the cone of outcomes far more than it lifted the center of it. An investor in the wild group had a real shot at the best return in the table and an equally real shot at the worst.

The path matters as much as the endpoint

A single total-return number hides the ride. Tracking $100 invested at the start of the window through each week shows what volatility feels like to hold.

QueryGrowth of $100: a calm name (KO), the market (SPY), a wild name (NVDA), weekly
The exact SQL behind every number
WITH d AS (
    SELECT ticker,
           toDate(toTimeZone(window_start, 'America/New_York')) AS dt,
           argMax(toFloat64(close), toTimeZone(window_start, 'America/New_York')) AS c
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('KO','SPY','NVDA')
      AND window_start >= now() - INTERVAL 800 DAY
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
    GROUP BY ticker, dt
),
r AS (
    SELECT ticker, dt,
           c / lagInFrame(c) OVER (PARTITION BY ticker ORDER BY dt) - 1 AS ret
    FROM d
),
f AS (
    SELECT ticker, dt, ret FROM r WHERE ret IS NOT NULL AND ret > -0.5 AND ret < 0.5
),
cum AS (
    SELECT ticker, dt,
           100 * exp(sum(log(1 + ret)) OVER (PARTITION BY ticker ORDER BY dt)) AS idx
    FROM f
),
wk AS (
    SELECT ticker, toMonday(dt) AS week, argMax(idx, dt) AS wv
    FROM cum GROUP BY ticker, week
)
SELECT week,
       round(maxIf(wv, ticker = 'KO'), 1) AS ko_calm,
       round(maxIf(wv, ticker = 'SPY'), 1) AS spy_market,
       round(maxIf(wv, ticker = 'NVDA'), 1) AS nvda_wild
FROM wk
GROUP BY week
ORDER BY week

The calm stock here is Coca-Cola (KO), the wild one is Nvidia (NVDA), and the middle line is the market. By the last week, $100 in KO had grown to $131.9, the S&P 500 to $145.9, and NVDA to $231.8. NVDA won the window and did it on a visibly jagged path, giving back double-digit percentages more than once along the way. KO trailed both other lines while barely wobbling. Neither outcome fits the textbook. The calm name did not beat the market this time, and the volatile name paid off in spite of the anomaly, not in line with it. That is the texture of a short window. The anomaly is a tendency measured over decades, not a promise about any two-year stretch.

Why the anomaly is real but not a law

The strongest version of the low-vol finding is modest: over long horizons, buying calmer stocks has not cost investors return, and it has sharply cut their risk. It is a statement about averages across thousands of names and many years, not a rule that governs 25 stocks over 24 months. Windows like this one, where volatile momentum names ran hard, are exactly why the pattern is called an anomaly rather than a law. For a fuller look at which large caps have actually been calmest, see the calmest large-cap stocks. The flip side, holding a few big volatile winners, carries its own concentration risk, and the temperament the anomaly rewards sits close to the one behind buying when others are fearful.

FAQ

What is the low-volatility anomaly?

It is the long-observed pattern that low-volatility stocks have earned returns as high as, or higher than, high-volatility stocks over long horizons, at much lower risk. That runs against the textbook prediction that more risk earns more return.

Do low-volatility stocks really beat high-volatility stocks?

Over multi-decade samples the research finds they roughly match or beat them on a risk-adjusted basis. Over any short window the result can flip. In the two-year window measured here the most volatile third of names posted the highest median return, with by far the widest range of outcomes.

Is low-volatility investing a good strategy?

Low-volatility investing has historically cut risk without sacrificing much return, which is why "min-vol" funds exist. It is not a guarantee of outperformance in any given year, and it can lag sharply when speculative names lead.

How is stock volatility measured?

The common measure is annualized volatility: the standard deviation of daily percentage returns, multiplied by the square root of 252 (the number of trading days in a year). A higher figure means larger typical daily moves in either direction.


Every panel above is a stored, inspectable query. Open the SQL under any chart to check the math, or run the same volatility-versus-return test on your own basket of names on the Strasmore terminal.