Stocks With the Biggest IV-RV Gap
Which stocks have the biggest IV-RV gap? We rank 30-day implied volatility against the realized volatility that followed, with the full method shown.
Stocks with the biggest IV-RV gap are the names whose 30-day implied volatility sat far above the realized volatility that actually arrived over the following month. This page ranks that spread, which options traders call the variance risk premium. It answers a different question from our other three volatility leaderboards: highest implied volatility stocks ranks the level of implied volatility, highest IV rank stocks ranks that level against a name's own history, and highest option premiums ranks the dollar price of the contract. A stock can lead all three and still show no gap at all, if it moved as much as its options implied.
What the IV-RV gap measures
Implied volatility is the annualized move an option's price implies for the stock underneath it, backed out of the option price. Realized volatility is the annualized move the stock actually printed, measured from its closing prices after the fact. One is a forecast. The other is the scoreboard. Our historical volatility vs implied volatility primer walks through both definitions slowly.
The gap is the first minus the second, in annualized volatility points. A name with 30-day IV of 40% whose following month realized 28% carries a 12-point gap. Positive means the options were priced for more motion than arrived. Negative means the stock out-moved its own option market. The average across broad equity samples has been positive over long stretches, and that persistence is what gave the spread a name of its own.
The method, in full
A ranking like this is only as credible as its definition, so here is the whole recipe before any number.
- The implied leg: per-contract daily IV for call contracts with 20 to 45 days to expiry, struck within 5% of the stock's close that day, keeping only rows where the solver returned a positive volatility. Moneyness is measured against the same daily closing series that feeds the realized leg, so one price defines both the strike band and the returns. Each name's reading is the plain average of those contract days.
- The realized leg: close-to-close daily log returns, sample standard deviation, multiplied by the square root of 252 to annualize. Both legs print in the same units.
- The forward shift: a 30-day implied reading covers the next 30 days, so the realized window is pushed forward one month. The implied leg spans October 2025 through June 2026. The realized leg spans November 2025 through July 2026.
- The filters: a name needs at least 120 days with a qualifying quote and 1,000 qualifying contract days, which keeps the list to actively traded option classes, and its close has to sit above $10. Any ticker with a stock split in the window is dropped, since an unadjusted split prints as a fake 50% session.
Stocks with the biggest IV-RV gap, October 2025 to June 2026
| symbol | implied_vol_pct | realized_vol_pct | gap_pct |
|---|---|---|---|
| VKTX | 79.2 | 58.3 | 20.9 |
| TTD | 71.8 | 53.9 | 17.9 |
| UPST | 83.8 | 68.6 | 15.2 |
| VXX | 71.7 | 58 | 13.7 |
| CORZ | 87.6 | 74.2 | 13.4 |
| UUUU | 101.4 | 88.1 | 13.3 |
| LYFT | 63.3 | 50.2 | 13.1 |
| WBD | 35.7 | 22.7 | 13 |
| DPST | 78.5 | 65.5 | 13 |
| DUOL | 79.2 | 67.1 | 12.1 |
| GME | 48.5 | 36.4 | 12.1 |
| KVUE | 32.9 | 20.8 | 12.1 |
The exact SQL behind every number
WITH
split_tickers AS
(
SELECT DISTINCT ticker
FROM global_markets.stocks_splits
WHERE execution_date >= '2025-10-01'
AND execution_date < '2026-08-01'
),
px AS
(
SELECT
ticker AS symbol,
date AS session,
max(toFloat64(close)) AS px
FROM global_markets.stocks_daily_aggs
WHERE date >= '2025-10-01'
AND date < '2026-08-01'
AND ticker NOT IN (SELECT ticker FROM split_tickers)
AND ticker NOT IN ('SPCX')
GROUP BY symbol, session
),
iv_rows AS
(
SELECT
g.symbol AS symbol,
g.session AS session,
g.iv AS iv
FROM
(
SELECT
underlying_symbol AS symbol,
toDate(date) AS session,
toFloat64(strike_price) AS strike,
implied_volatility AS iv
FROM global_markets.options_greeks
WHERE date >= '2025-10-01'
AND date < '2026-07-01'
AND lower(toString(option_type)) IN ('c', 'call')
AND days_to_expiry BETWEEN 20 AND 45
AND implied_volatility > 0
) AS g
INNER JOIN px AS p ON p.symbol = g.symbol AND p.session = g.session
WHERE p.px > 10
AND abs(g.strike / p.px - 1) < 0.05
),
implied AS
(
SELECT
symbol,
round(100 * avg(iv), 1) AS implied_vol_pct
FROM iv_rows
GROUP BY symbol
HAVING countDistinct(session) >= 120
AND count() >= 1000
),
daily_returns AS
(
SELECT
symbol,
arrayJoin(arrayMap((a, b) -> log(tupleElement(a, 2) / tupleElement(b, 2)),
arraySlice(series, 2),
arraySlice(series, 1, length(series) - 1))) AS ret
FROM
(
SELECT
symbol,
arraySort(groupArray((session, px))) AS series
FROM px
WHERE session >= '2025-11-01'
AND symbol IN (SELECT symbol FROM implied)
GROUP BY symbol
)
),
realized AS
(
SELECT
symbol,
round(100 * sqrt(252) * stddevSamp(ret), 1) AS realized_vol_pct
FROM daily_returns
GROUP BY symbol
HAVING count() >= 120
)
SELECT
implied.symbol AS symbol,
implied.implied_vol_pct AS implied_vol_pct,
realized.realized_vol_pct AS realized_vol_pct,
round(implied.implied_vol_pct - realized.realized_vol_pct, 1) AS gap_pct
FROM implied
INNER JOIN realized ON realized.symbol = implied.symbol
WHERE implied.implied_vol_pct > realized.realized_vol_pct
ORDER BY gap_pct DESC
LIMIT 12The widest name on the list, VKTX, averaged 79.2% implied against 58.3% realized, a gap of 20.9 points. All 12 names shown carried positive gaps, down to 12.1 points for KVUE at the bottom. Read the bars in pairs: the implied bar is the forecast the option market published, the realized bar is what the tape delivered over the month after.
A worked example: SPY, month by month
One name over many months shows the mechanics better than any cross-section. The panel below pairs each month's average 30-day IV on SPY, the S&P 500 ETF, with the realized volatility of the month that came next.
| month | implied_vol_pct | realized_vol_pct | gap_pct |
|---|---|---|---|
| 2024-07 | 12.9 | 19.2 | -6.3 |
| 2024-08 | 15 | 13.8 | 1.2 |
| 2024-09 | 13.6 | 11.2 | 2.4 |
| 2024-10 | 16.4 | 11.8 | 4.6 |
| 2024-11 | 13 | 14.1 | -1.1 |
| 2024-12 | 11.6 | 13.9 | -2.3 |
| 2025-01 | 13.9 | 13.2 | 0.7 |
| 2025-02 | 12.7 | 20.7 | -8 |
| 2025-03 | 17.7 | 51.9 | -34.2 |
| 2025-04 | 26.4 | 16.8 | 9.6 |
| 2025-05 | 17.4 | 10.2 | 7.2 |
| 2025-06 | 14.7 | 6.6 | 8.1 |
| 2025-07 | 14.4 | 12 | 2.4 |
| 2025-08 | 12.6 | 7.1 | 5.5 |
| 2025-09 | 11.3 | 13.8 | -2.5 |
| 2025-10 | 15.3 | 15.4 | -0.1 |
| 2025-11 | 15.5 | 8.4 | 7.1 |
| 2025-12 | 12.4 | 10.3 | 2.1 |
| 2026-01 | 13.4 | 13.4 | 0 |
| 2026-02 | 15.3 | 18.2 | -2.9 |
The exact SQL behind every number
WITH
px AS
(
SELECT
date AS session,
max(toFloat64(close)) AS px
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2024-07-01'
AND date < '2026-08-01'
GROUP BY session
),
iv_rows AS
(
SELECT
g.session AS session,
g.iv AS iv
FROM
(
SELECT
toDate(date) AS session,
toFloat64(strike_price) AS strike,
implied_volatility AS iv
FROM global_markets.options_greeks
WHERE underlying_symbol = 'SPY'
AND date >= '2024-07-01'
AND date < '2026-07-01'
AND lower(toString(option_type)) IN ('c', 'call')
AND days_to_expiry BETWEEN 20 AND 45
AND implied_volatility > 0
) AS g
INNER JOIN px AS p ON p.session = g.session
WHERE abs(g.strike / p.px - 1) < 0.05
),
daily_returns AS
(
SELECT
arrayJoin(arrayMap((a, b) -> (tupleElement(a, 1), log(tupleElement(a, 2) / tupleElement(b, 2))),
arraySlice(series, 2),
arraySlice(series, 1, length(series) - 1))) AS ret
FROM
(
SELECT arraySort(groupArray((session, px))) AS series
FROM px
)
),
realized AS
(
SELECT
toStartOfMonth(tupleElement(ret, 1)) AS rv_month,
round(100 * sqrt(252) * stddevSamp(tupleElement(ret, 2)), 1) AS realized_vol_pct
FROM daily_returns
GROUP BY rv_month
HAVING count() >= 15
),
implied AS
(
SELECT
iv_month,
addMonths(iv_month, 1) AS forward_month,
implied_vol_pct
FROM
(
SELECT
toStartOfMonth(session) AS iv_month,
round(100 * avg(iv), 1) AS implied_vol_pct
FROM iv_rows
GROUP BY iv_month
HAVING countDistinct(session) >= 15
)
)
SELECT
formatDateTime(implied.iv_month, '%Y-%m') AS month,
implied.implied_vol_pct AS implied_vol_pct,
realized.realized_vol_pct AS realized_vol_pct,
round(implied.implied_vol_pct - realized.realized_vol_pct, 1) AS gap_pct
FROM implied
INNER JOIN realized ON realized.rv_month = implied.forward_month
ORDER BY monthThat is 24 monthly pairs on one name, starting at 2024-07. In the last month in view, 2026-06, implied averaged 14.7% against a realized 12.1%. Follow the two lines and the arithmetic of the gap becomes concrete: the implied line is fairly smooth, the realized line spikes, and the gap_pct column carries the sign of each month.
How often is the IV-RV gap positive?
A top-twelve list is selected on the outcome it reports, so it cannot tell you how common a positive gap is. The panel below drops the ranking and counts instead. For every month in the window it pairs each qualifying name's implied reading with the realized volatility of the next month, then reports the share of names where the implied number came out higher.
| month | names_measured | positive_gap_share_pct | median_gap_pct |
|---|---|---|---|
| 2025-10 | 432 | 69.9 | 5.2 |
| 2025-11 | 320 | 87.8 | 9.3 |
| 2025-12 | 369 | 33.9 | -4 |
| 2026-01 | 395 | 34.4 | -5.1 |
| 2026-02 | 319 | 59.9 | 2.6 |
| 2026-03 | 338 | 72.5 | 6.7 |
| 2026-04 | 358 | 63.7 | 3.2 |
| 2026-05 | 396 | 38.6 | -1.9 |
| 2026-06 | 357 | 58.8 | 2.4 |
The exact SQL behind every number
WITH
split_tickers AS
(
SELECT DISTINCT ticker
FROM global_markets.stocks_splits
WHERE execution_date >= '2025-10-01'
AND execution_date < '2026-08-01'
),
px AS
(
SELECT
ticker AS symbol,
date AS session,
max(toFloat64(close)) AS px
FROM global_markets.stocks_daily_aggs
WHERE date >= '2025-10-01'
AND date < '2026-08-01'
AND ticker NOT IN (SELECT ticker FROM split_tickers)
AND ticker NOT IN ('SPCX')
GROUP BY symbol, session
),
iv_rows AS
(
SELECT
g.symbol AS symbol,
g.session AS session,
g.iv AS iv
FROM
(
SELECT
underlying_symbol AS symbol,
toDate(date) AS session,
toFloat64(strike_price) AS strike,
implied_volatility AS iv
FROM global_markets.options_greeks
WHERE date >= '2025-10-01'
AND date < '2026-07-01'
AND lower(toString(option_type)) IN ('c', 'call')
AND days_to_expiry BETWEEN 20 AND 45
AND implied_volatility > 0
) AS g
INNER JOIN px AS p ON p.symbol = g.symbol AND p.session = g.session
WHERE p.px > 10
AND abs(g.strike / p.px - 1) < 0.05
),
implied AS
(
SELECT
symbol,
iv_month,
addMonths(iv_month, 1) AS forward_month,
implied_vol_pct
FROM
(
SELECT
symbol,
toStartOfMonth(session) AS iv_month,
round(100 * avg(iv), 1) AS implied_vol_pct
FROM iv_rows
GROUP BY symbol, iv_month
HAVING countDistinct(session) >= 15
AND count() >= 150
)
),
daily_returns AS
(
SELECT
symbol,
arrayJoin(arrayMap((a, b) -> (tupleElement(a, 1), log(tupleElement(a, 2) / tupleElement(b, 2))),
arraySlice(series, 2),
arraySlice(series, 1, length(series) - 1))) AS ret
FROM
(
SELECT
symbol,
arraySort(groupArray((session, px))) AS series
FROM px
WHERE symbol IN (SELECT symbol FROM implied)
GROUP BY symbol
)
),
realized AS
(
SELECT
symbol,
toStartOfMonth(tupleElement(ret, 1)) AS rv_month,
round(100 * sqrt(252) * stddevSamp(tupleElement(ret, 2)), 1) AS realized_vol_pct
FROM daily_returns
GROUP BY symbol, rv_month
HAVING count() >= 15
)
SELECT
formatDateTime(implied.iv_month, '%Y-%m') AS month,
count() AS names_measured,
round(100 * countIf(implied.implied_vol_pct > realized.realized_vol_pct) / count(), 1) AS positive_gap_share_pct,
round(quantileDeterministic(0.5)(implied.implied_vol_pct - realized.realized_vol_pct,
cityHash64(implied.symbol)), 1) AS median_gap_pct
FROM implied
INNER JOIN realized ON realized.symbol = implied.symbol
AND realized.rv_month = implied.forward_month
GROUP BY month
ORDER BY monthIn the first month of the window, 2025-10, the gap came out positive for 69.9% of the 432 names that cleared the filters, with a median of 5.2 points. By 2026-06 the positive share was 58.8% across 357 names. The line across all 9 months matters more than any single reading: the share is not a constant, and each dip marks a month when realized volatility came in above implied for much of the list.
What a persistent gap does not imply
Sort the same monthly pairs the other way and the left tail appears.
| label | implied_vol_pct | realized_vol_pct | gap_pct |
|---|---|---|---|
| SOXL Jun 2026 | 136.9 | 243.2 | -106.3 |
| SLV Jan 2026 | 47.8 | 139.7 | -91.9 |
| APP Feb 2026 | 72.2 | 137.6 | -65.4 |
| SNDK Jul 2026 | 104.6 | 159.7 | -55.1 |
| DELL Feb 2026 | 46.3 | 92.2 | -45.9 |
| HOOD Feb 2026 | 59.6 | 102.1 | -42.5 |
| IBIT Feb 2026 | 40.2 | 81.8 | -41.6 |
| CRWV Feb 2026 | 93.4 | 130.8 | -37.4 |
| MU Jun 2026 | 89.1 | 126.4 | -37.3 |
| SOXX Jun 2026 | 45.1 | 82.1 | -37 |
The exact SQL behind every number
WITH
split_tickers AS
(
SELECT DISTINCT ticker
FROM global_markets.stocks_splits
WHERE execution_date >= '2025-10-01'
AND execution_date < '2026-08-01'
),
px AS
(
SELECT
ticker AS symbol,
date AS session,
max(toFloat64(close)) AS px
FROM global_markets.stocks_daily_aggs
WHERE date >= '2025-10-01'
AND date < '2026-08-01'
AND ticker NOT IN (SELECT ticker FROM split_tickers)
AND ticker NOT IN ('SPCX')
GROUP BY symbol, session
),
iv_rows AS
(
SELECT
g.symbol AS symbol,
g.session AS session,
g.iv AS iv
FROM
(
SELECT
underlying_symbol AS symbol,
toDate(date) AS session,
toFloat64(strike_price) AS strike,
implied_volatility AS iv
FROM global_markets.options_greeks
WHERE date >= '2025-10-01'
AND date < '2026-07-01'
AND lower(toString(option_type)) IN ('c', 'call')
AND days_to_expiry BETWEEN 20 AND 45
AND implied_volatility > 0
) AS g
INNER JOIN px AS p ON p.symbol = g.symbol AND p.session = g.session
WHERE p.px > 10
AND abs(g.strike / p.px - 1) < 0.05
),
implied AS
(
SELECT
symbol,
iv_month,
addMonths(iv_month, 1) AS forward_month,
implied_vol_pct
FROM
(
SELECT
symbol,
toStartOfMonth(session) AS iv_month,
round(100 * avg(iv), 1) AS implied_vol_pct
FROM iv_rows
GROUP BY symbol, iv_month
HAVING countDistinct(session) >= 18
AND count() >= 500
)
),
daily_returns AS
(
SELECT
symbol,
arrayJoin(arrayMap((a, b) -> (tupleElement(a, 1), log(tupleElement(a, 2) / tupleElement(b, 2))),
arraySlice(series, 2),
arraySlice(series, 1, length(series) - 1))) AS ret
FROM
(
SELECT
symbol,
arraySort(groupArray((session, px))) AS series
FROM px
WHERE symbol IN (SELECT symbol FROM implied)
GROUP BY symbol
)
),
realized AS
(
SELECT
symbol,
toStartOfMonth(tupleElement(ret, 1)) AS rv_month,
round(100 * sqrt(252) * stddevSamp(tupleElement(ret, 2)), 1) AS realized_vol_pct
FROM daily_returns
GROUP BY symbol, rv_month
HAVING count() >= 15
)
SELECT
concat(implied.symbol, ' ', formatDateTime(implied.forward_month, '%b %Y')) AS label,
implied.implied_vol_pct AS implied_vol_pct,
realized.realized_vol_pct AS realized_vol_pct,
round(implied.implied_vol_pct - realized.realized_vol_pct, 1) AS gap_pct
FROM implied
INNER JOIN realized ON realized.symbol = implied.symbol
AND realized.rv_month = implied.forward_month
WHERE realized.realized_vol_pct > implied.implied_vol_pct
ORDER BY gap_pct ASC
LIMIT 10The most negative pair in the window is SOXL Jun 2026: 136.9% implied against 243.2% realized, a gap of -106.3 points. Each of the 10 rows is a liquid option class where one month's move dwarfed the size its options were priced for. Four things sit between a positive average spread and what any account would experience.
- Costs come off the top. Every leg pays the bid-ask spread and a commission in both directions, and the thinner the option class, the larger that haul against a 10-point gap. Our guide to what it costs to trade options puts numbers on it.
- The average is not the path. A positive mean across hundreds of names and nine months says nothing about one position in one month.
- The tail is fat on one side. The panel above is the left edge of the distribution, and a short option's loss in such a month runs to many times the premium taken in.
- Overnight gaps ignore exit plans. A stock can reopen far from its last print with nothing tradeable in between, so the worst outcome on a short position is not bounded by where a trader intended to get out.
FAQ
What is the IV-RV gap?
It is the difference between an option market's 30-day implied volatility and the realized volatility the stock went on to print over the next month, measured in annualized percentage points. A positive gap means the options were priced for a larger move than the one that arrived. Traders also call it the variance risk premium.
Is the biggest IV-RV gap the same as the highest implied volatility?
No. A name at 90% IV that then realizes 85% has a 5-point gap, while a name at 25% IV that realizes 15% has a 10-point gap. The level is ranked in highest implied volatility stocks. The spread is what this page ranks.
How is realized volatility calculated here?
From close-to-close daily log returns over the forward month, taking the sample standard deviation and multiplying by the square root of 252, the approximate count of trading sessions in a year. That puts it in the same annualized units as implied volatility.
Does a persistent positive gap mean selling options makes money?
No. A positive average across many names and months is a property of the sample, not a result for any single trade. Costs and the fat left tail shown above sit between the measured spread and any account's outcome.
Method notes and caveats
- The implied leg uses call contracts only. Put-side IV at the same strike and expiry tracks close to it for most names, and averaging the two blends two quote surfaces into one number.
- IV rows are per contract per day, so a name with a dense strike ladder contributes more contract days than a thin one. The averages here are unweighted across qualifying contracts, and a contract counts whether or not it traded that session.
- Both legs read the same daily closing series, which is why a name with no price history in the window drops out of the ranking entirely rather than carrying an implied reading with no realized partner.
- Daily price aggregates carry a one to two day ingest lag at the front edge, which is why the window closes in July 2026 rather than at today's date.
- Sourcing these series yourself is covered in where to get historical implied volatility data.
Every panel here ships with the exact SQL beneath it. Open one, move the date window or widen the days-to-expiry band, and the ranking recomputes on the Strasmore terminal.