Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Do Stocks Move as Much as Options Predict?

Do stocks move as much as options predict? We scored the earnings implied move against the realized move, print by print, across sixteen household large caps.

Do stocks move as much as options predict? For the large-cap earnings prints scored on this page, usually not: the implied move priced by options came in at or above the move the stock actually delivered on 53.4% of events. Financial media publishes an implied move ahead of every big report. Almost nobody grades it afterwards, so this page does, print by print, with the method written out first.

What the implied move actually is

An earnings implied move is the size of the one-off price change the options market is charging for across a company's report, measured from the last close before the release. It is a magnitude, not a direction. An implied move of 6% prices roughly the same premium into a 6% drop as into a 6% rally.

Two different numbers both get called "the implied move". They are close cousins.

The straddle price is the first one. A straddle is one call plus one put at the same strike and the same expiry (the strangle variant splits the strikes instead). Take the at-the-money strike, the listed strike nearest the current share price, in the first expiry that falls after the report date. Add the two premiums, then divide by the share price. If the stock trades at $200 and the pair costs $12, the implied move is 6%. That figure is a quote off the screen rather than a model output.

Implied volatility times the square root of time is the second. Implied volatility, or IV, is the annualized volatility input that makes an option pricing model agree with the option's market price. Annualized means per year, so a shorter window has to be scaled: IV times the square root of days divided by 365. An IV of 70% with the expiry 9 days out works out near 11%. Our expected move walkthrough takes that formula apart term by term, and the IV term structure guide covers how much the expiry you pick changes the answer.

How to reproduce tonight's implied move for your own ticker

Four steps, all of them visible in the SQL under the first panel:

  • Find the report date. The 8-K filings carrying Item 2.02, Results of Operations and Financial Condition, are the earnings releases themselves. Finding a stock's earnings date covers the calendar side of this.
  • Take the last trading session before that date, along with its closing share price.
  • In that session's chain, pick the nearest expiry after the report date, then the strike closest to the share price. Add the call premium and the put premium.
  • Divide by the share price. Afterwards, compare the result with the absolute percentage change in the closing price across the report.

The panel below runs exactly those steps for every NVDA report where the pre-report chain survives the filters, with both implied methods side by side and the realized move next to them.

QueryNVDA: implied move both ways versus the realized move, report by report
report_datereport_labelstraddle_implied_pctiv_root_t_pctrealized_pct
2025-02-26February 26, 202510.5713.275.12
2025-05-28May 28, 20256.928.672.72
2025-08-27August 27, 20256.488.120.88
2025-11-19November 19, 20257.659.60.4
2026-02-25February 25, 20266.097.644.13
2026-05-20May 20, 20266.227.770.5
2026-08-26August 26, 20265.97.47.01
The exact SQL behind every number
WITH
reports AS (
    SELECT
        toString(ticker)    AS sym,
        toDate(filing_date) AS report_date
    FROM global_markets.stocks_8k_text
    WHERE ticker = 'NVDA'
      AND filing_date >= '2021-09-01'
      AND filing_date <  '2026-09-20'
      AND (items_text ILIKE '%results of operations and financial condition%'
        OR items_text ILIKE '%item 2.02%')
    GROUP BY sym, report_date
),
sessions AS (
    SELECT
        toString(ticker) AS sym,
        toDate(date)     AS session_date,
        toFloat64(close) AS px
    FROM global_markets.stocks_daily_aggs
    WHERE ticker IN ('NVDA')
      AND date >= '2021-08-01'
      AND date <  '2026-09-27'
      AND close > 0
),
spans AS (
    SELECT
        r.sym         AS sym,
        r.report_date AS report_date,
        maxIf(s.session_date, s.session_date < r.report_date) AS pre_date,
        minIf(s.session_date, s.session_date > r.report_date) AS post_date
    FROM reports AS r
    INNER JOIN sessions AS s ON s.sym = r.sym
    WHERE s.session_date >= r.report_date - 8
      AND s.session_date <= r.report_date + 8
    GROUP BY r.sym, r.report_date
    HAVING countIf(s.session_date < r.report_date) > 0
       AND countIf(s.session_date > r.report_date) > 0
),
moves AS (
    SELECT
        sp.sym         AS sym,
        sp.report_date AS report_date,
        sp.pre_date    AS pre_date,
        round(100 * abs(b.px / a.px - 1), 2) AS realized_pct
    FROM spans AS sp
    INNER JOIN sessions AS a ON a.sym = sp.sym AND a.session_date = sp.pre_date
    INNER JOIN sessions AS b ON b.sym = sp.sym AND b.session_date = sp.post_date
),
greeks AS (
    SELECT
        toString(underlying_symbol)   AS sym,
        toDate(date)                  AS pre_date,
        toDate(expiration_date)       AS expiry,
        lower(option_type)            AS side,
        toFloat64(strike_price)       AS strike,
        toFloat64(option_close)       AS opt_px,
        toFloat64(underlying_close)   AS spot,
        toFloat64(implied_volatility) AS iv,
        toUInt16(days_to_expiry)      AS dte
    FROM global_markets.options_greeks
    WHERE underlying_symbol IN ('NVDA')
      AND date >= '2021-09-01'
      AND date <  '2026-09-20'
      AND iv_converged = 1
      AND volume > 0
      AND days_to_expiry BETWEEN 1 AND 45
      AND underlying_close > 0
      AND option_close > 0
      AND abs(toFloat64(strike_price) / toFloat64(underlying_close) - 1) < 0.05
),
chain AS (
    SELECT
        g.sym      AS sym,
        g.pre_date AS pre_date,
        g.expiry   AS expiry,
        g.side     AS side,
        g.strike   AS strike,
        g.opt_px   AS opt_px,
        g.spot     AS spot,
        g.iv       AS iv,
        g.dte      AS dte
    FROM greeks AS g
    INNER JOIN moves AS m ON m.sym = g.sym AND m.pre_date = g.pre_date
    WHERE g.expiry > m.report_date
),
front AS (
    SELECT sym, pre_date, min(expiry) AS expiry
    FROM chain
    GROUP BY sym, pre_date
),
straddles AS (
    SELECT
        c.sym       AS sym,
        c.pre_date  AS pre_date,
        c.strike    AS strike,
        any(c.spot) AS spot,
        max(c.dte)  AS dte,
        avgIf(c.opt_px, c.side IN ('call', 'c')) AS call_px,
        avgIf(c.opt_px, c.side IN ('put', 'p'))  AS put_px,
        avg(c.iv)   AS atm_iv
    FROM chain AS c
    INNER JOIN front AS f
        ON f.sym = c.sym AND f.pre_date = c.pre_date AND f.expiry = c.expiry
    GROUP BY c.sym, c.pre_date, c.strike
    HAVING countIf(c.side IN ('call', 'c')) > 0
       AND countIf(c.side IN ('put', 'p')) > 0
),
implied AS (
    SELECT
        sym,
        pre_date,
        argMin(round(100 * (call_px + put_px) / spot, 2), abs(strike / spot - 1)) AS straddle_pct,
        argMin(round(100 * atm_iv * sqrt(dte / 365), 2), abs(strike / spot - 1))  AS iv_root_t_pct
    FROM straddles
    GROUP BY sym, pre_date
)
SELECT
    toString(m.report_date) AS report_date,
    concat(monthName(m.report_date), ' ', toString(toDayOfMonth(m.report_date)), ', ', toString(toYear(m.report_date))) AS report_label,
    i.straddle_pct          AS straddle_implied_pct,
    i.iv_root_t_pct         AS iv_root_t_pct,
    m.realized_pct          AS realized_pct
FROM moves AS m
INNER JOIN implied AS i ON i.sym = m.sym AND i.pre_date = m.pre_date
WHERE i.straddle_pct > 0
ORDER BY m.report_date
Run this yourself

At the most recent print in the trace, August 26, 2026, the straddle implied 5.9% and the IV calculation implied 7.4%, against a realized move of 7.01%. The trace opens at February 26, 2025 and carries 7 reports. Two features stand out on the chart. The two implied series track each other, which is what a quote and a model of that same quote ought to do. The realized series is the spiky one: it crosses above the implied lines at some prints and sits under them at others.

Do stocks move as much as options predict? The scoreboard

Run the same method across a set of household large caps with liquid chains and the events stack up into a scoreboard. Each name gets its count of reports, the count where the realized move finished at or under implied, the median of each side, and the median of the realized-to-implied ratio. A median ratio of 0.60 says the typical print delivered 60% of what the straddle priced.

QueryImplied versus realized earnings moves, large caps scored print by print
symbolevent_countunder_implied_countmedian_implied_pctmedian_realized_pctmedian_ratiosample_fromsample_through
NVDA766.482.720.392025-02-262026-08-26
AMD758.61.980.222025-02-042026-08-04
ORCL7510.510.240.852025-03-102026-09-10
TSLA1495.923.310.562025-01-022026-07-22
AMZN746.826.040.892025-02-062026-07-30
AVGO748.356.030.782025-03-062026-09-02
NFLX747.756.420.722025-01-212026-07-16
KO732.973.461.232025-02-112026-07-28
MSFT734.797.21.642025-01-292026-07-29
QCOM736.667.391.192025-02-052026-07-29
WMT734.945.61.182025-02-202026-08-20
DIS726.527.611.192025-02-052026-08-05
META727.159.161.182025-01-292026-07-29
The exact SQL behind every number
WITH
reports AS (
    SELECT
        toString(ticker)    AS sym,
        toDate(filing_date) AS report_date
    FROM global_markets.stocks_8k_text
    WHERE ticker IN ('AAPL', 'AMD', 'AMZN', 'AVGO', 'DIS', 'GOOGL', 'JPM', 'KO', 'META', 'MSFT', 'NFLX', 'NVDA', 'ORCL', 'QCOM', 'TSLA', 'WMT')
      AND filing_date >= '2021-09-01'
      AND filing_date <  '2026-09-20'
      AND (items_text ILIKE '%results of operations and financial condition%'
        OR items_text ILIKE '%item 2.02%')
    GROUP BY sym, report_date
),
sessions AS (
    SELECT
        toString(ticker) AS sym,
        toDate(date)     AS session_date,
        toFloat64(close) AS px
    FROM global_markets.stocks_daily_aggs
    WHERE ticker IN ('AAPL', 'AMD', 'AMZN', 'AVGO', 'DIS', 'GOOGL', 'JPM', 'KO', 'META', 'MSFT', 'NFLX', 'NVDA', 'ORCL', 'QCOM', 'TSLA', 'WMT')
      AND date >= '2021-08-01'
      AND date <  '2026-09-27'
      AND close > 0
),
spans AS (
    SELECT
        r.sym         AS sym,
        r.report_date AS report_date,
        maxIf(s.session_date, s.session_date < r.report_date) AS pre_date,
        minIf(s.session_date, s.session_date > r.report_date) AS post_date
    FROM reports AS r
    INNER JOIN sessions AS s ON s.sym = r.sym
    WHERE s.session_date >= r.report_date - 8
      AND s.session_date <= r.report_date + 8
    GROUP BY r.sym, r.report_date
    HAVING countIf(s.session_date < r.report_date) > 0
       AND countIf(s.session_date > r.report_date) > 0
),
moves AS (
    SELECT
        sp.sym         AS sym,
        sp.report_date AS report_date,
        sp.pre_date    AS pre_date,
        round(100 * abs(b.px / a.px - 1), 2) AS realized_pct
    FROM spans AS sp
    INNER JOIN sessions AS a ON a.sym = sp.sym AND a.session_date = sp.pre_date
    INNER JOIN sessions AS b ON b.sym = sp.sym AND b.session_date = sp.post_date
),
greeks AS (
    SELECT
        toString(underlying_symbol)   AS sym,
        toDate(date)                  AS pre_date,
        toDate(expiration_date)       AS expiry,
        lower(option_type)            AS side,
        toFloat64(strike_price)       AS strike,
        toFloat64(option_close)       AS opt_px,
        toFloat64(underlying_close)   AS spot,
        toFloat64(implied_volatility) AS iv,
        toUInt16(days_to_expiry)      AS dte
    FROM global_markets.options_greeks
    WHERE underlying_symbol IN ('AAPL', 'AMD', 'AMZN', 'AVGO', 'DIS', 'GOOGL', 'JPM', 'KO', 'META', 'MSFT', 'NFLX', 'NVDA', 'ORCL', 'QCOM', 'TSLA', 'WMT')
      AND date >= '2021-09-01'
      AND date <  '2026-09-20'
      AND iv_converged = 1
      AND volume > 0
      AND days_to_expiry BETWEEN 1 AND 45
      AND underlying_close > 0
      AND option_close > 0
      AND abs(toFloat64(strike_price) / toFloat64(underlying_close) - 1) < 0.05
),
chain AS (
    SELECT
        g.sym      AS sym,
        g.pre_date AS pre_date,
        g.expiry   AS expiry,
        g.side     AS side,
        g.strike   AS strike,
        g.opt_px   AS opt_px,
        g.spot     AS spot,
        g.iv       AS iv,
        g.dte      AS dte
    FROM greeks AS g
    INNER JOIN moves AS m ON m.sym = g.sym AND m.pre_date = g.pre_date
    WHERE g.expiry > m.report_date
),
front AS (
    SELECT sym, pre_date, min(expiry) AS expiry
    FROM chain
    GROUP BY sym, pre_date
),
straddles AS (
    SELECT
        c.sym       AS sym,
        c.pre_date  AS pre_date,
        c.strike    AS strike,
        any(c.spot) AS spot,
        max(c.dte)  AS dte,
        avgIf(c.opt_px, c.side IN ('call', 'c')) AS call_px,
        avgIf(c.opt_px, c.side IN ('put', 'p'))  AS put_px,
        avg(c.iv)   AS atm_iv
    FROM chain AS c
    INNER JOIN front AS f
        ON f.sym = c.sym AND f.pre_date = c.pre_date AND f.expiry = c.expiry
    GROUP BY c.sym, c.pre_date, c.strike
    HAVING countIf(c.side IN ('call', 'c')) > 0
       AND countIf(c.side IN ('put', 'p')) > 0
),
implied AS (
    SELECT
        sym,
        pre_date,
        argMin(round(100 * (call_px + put_px) / spot, 2), abs(strike / spot - 1)) AS straddle_pct,
        argMin(round(100 * atm_iv * sqrt(dte / 365), 2), abs(strike / spot - 1))  AS iv_root_t_pct
    FROM straddles
    GROUP BY sym, pre_date
)
SELECT
    m.sym                                               AS symbol,
    toUInt32(count())                                   AS event_count,
    toUInt32(countIf(m.realized_pct <= i.straddle_pct)) AS under_implied_count,
    round(quantileDeterministic(0.5)(i.straddle_pct, cityHash64(m.sym, m.report_date)), 2) AS median_implied_pct,
    round(quantileDeterministic(0.5)(m.realized_pct, cityHash64(m.sym, m.report_date)), 2) AS median_realized_pct,
    round(quantileDeterministic(0.5)(m.realized_pct / i.straddle_pct, cityHash64(m.sym, m.report_date)), 2) AS median_ratio,
    toString(min(m.report_date))                        AS sample_from,
    toString(max(m.report_date))                        AS sample_through
FROM moves AS m
INNER JOIN implied AS i ON i.sym = m.sym AND i.pre_date = m.pre_date
WHERE i.straddle_pct > 0
GROUP BY m.sym
HAVING count() >= 5
ORDER BY under_implied_count / event_count DESC, symbol ASC
Run this yourself

13 names cleared the five-report minimum. NVDA tops the list: the realized move landed at or under implied on 6 of 7 reports, with a median implied move of 6.48% against a median realized move of 2.72%, a median ratio of 0.39. At the far end of the same list, META came in at or under implied on 2 of 7 reports, a median ratio of 1.18. The levels differ a lot across names, which is its own lesson: a 3% implied move on a consumer staple and a 9% implied move on a chipmaker are the market charging for different ranges, not different amounts of confidence. Meta's implied volatility profile shows how one large cap's levels sit through an earnings cycle.

How often does the realized move come in under implied?

The scoreboard reads name by name. Pooling every scored report and bucketing the ratio shows the shape of the whole sample at once.

QueryWhere the realized move landed relative to implied
ratio_bucketevent_countshare_pctcumulative_pct
under 0.50x333232
0.50 to 0.75x1312.644.7
0.75 to 1.00x98.753.4
1.00 to 1.50x2423.376.7
1.50 to 2.00x1312.689.3
over 2.00x1110.7100
The exact SQL behind every number
WITH
reports AS (
    SELECT
        toString(ticker)    AS sym,
        toDate(filing_date) AS report_date
    FROM global_markets.stocks_8k_text
    WHERE ticker IN ('AAPL', 'AMD', 'AMZN', 'AVGO', 'DIS', 'GOOGL', 'JPM', 'KO', 'META', 'MSFT', 'NFLX', 'NVDA', 'ORCL', 'QCOM', 'TSLA', 'WMT')
      AND filing_date >= '2021-09-01'
      AND filing_date <  '2026-09-20'
      AND (items_text ILIKE '%results of operations and financial condition%'
        OR items_text ILIKE '%item 2.02%')
    GROUP BY sym, report_date
),
sessions AS (
    SELECT
        toString(ticker) AS sym,
        toDate(date)     AS session_date,
        toFloat64(close) AS px
    FROM global_markets.stocks_daily_aggs
    WHERE ticker IN ('AAPL', 'AMD', 'AMZN', 'AVGO', 'DIS', 'GOOGL', 'JPM', 'KO', 'META', 'MSFT', 'NFLX', 'NVDA', 'ORCL', 'QCOM', 'TSLA', 'WMT')
      AND date >= '2021-08-01'
      AND date <  '2026-09-27'
      AND close > 0
),
spans AS (
    SELECT
        r.sym         AS sym,
        r.report_date AS report_date,
        maxIf(s.session_date, s.session_date < r.report_date) AS pre_date,
        minIf(s.session_date, s.session_date > r.report_date) AS post_date
    FROM reports AS r
    INNER JOIN sessions AS s ON s.sym = r.sym
    WHERE s.session_date >= r.report_date - 8
      AND s.session_date <= r.report_date + 8
    GROUP BY r.sym, r.report_date
    HAVING countIf(s.session_date < r.report_date) > 0
       AND countIf(s.session_date > r.report_date) > 0
),
moves AS (
    SELECT
        sp.sym         AS sym,
        sp.report_date AS report_date,
        sp.pre_date    AS pre_date,
        round(100 * abs(b.px / a.px - 1), 2) AS realized_pct
    FROM spans AS sp
    INNER JOIN sessions AS a ON a.sym = sp.sym AND a.session_date = sp.pre_date
    INNER JOIN sessions AS b ON b.sym = sp.sym AND b.session_date = sp.post_date
),
greeks AS (
    SELECT
        toString(underlying_symbol)   AS sym,
        toDate(date)                  AS pre_date,
        toDate(expiration_date)       AS expiry,
        lower(option_type)            AS side,
        toFloat64(strike_price)       AS strike,
        toFloat64(option_close)       AS opt_px,
        toFloat64(underlying_close)   AS spot,
        toFloat64(implied_volatility) AS iv,
        toUInt16(days_to_expiry)      AS dte
    FROM global_markets.options_greeks
    WHERE underlying_symbol IN ('AAPL', 'AMD', 'AMZN', 'AVGO', 'DIS', 'GOOGL', 'JPM', 'KO', 'META', 'MSFT', 'NFLX', 'NVDA', 'ORCL', 'QCOM', 'TSLA', 'WMT')
      AND date >= '2021-09-01'
      AND date <  '2026-09-20'
      AND iv_converged = 1
      AND volume > 0
      AND days_to_expiry BETWEEN 1 AND 45
      AND underlying_close > 0
      AND option_close > 0
      AND abs(toFloat64(strike_price) / toFloat64(underlying_close) - 1) < 0.05
),
chain AS (
    SELECT
        g.sym      AS sym,
        g.pre_date AS pre_date,
        g.expiry   AS expiry,
        g.side     AS side,
        g.strike   AS strike,
        g.opt_px   AS opt_px,
        g.spot     AS spot,
        g.iv       AS iv,
        g.dte      AS dte
    FROM greeks AS g
    INNER JOIN moves AS m ON m.sym = g.sym AND m.pre_date = g.pre_date
    WHERE g.expiry > m.report_date
),
front AS (
    SELECT sym, pre_date, min(expiry) AS expiry
    FROM chain
    GROUP BY sym, pre_date
),
straddles AS (
    SELECT
        c.sym       AS sym,
        c.pre_date  AS pre_date,
        c.strike    AS strike,
        any(c.spot) AS spot,
        max(c.dte)  AS dte,
        avgIf(c.opt_px, c.side IN ('call', 'c')) AS call_px,
        avgIf(c.opt_px, c.side IN ('put', 'p'))  AS put_px,
        avg(c.iv)   AS atm_iv
    FROM chain AS c
    INNER JOIN front AS f
        ON f.sym = c.sym AND f.pre_date = c.pre_date AND f.expiry = c.expiry
    GROUP BY c.sym, c.pre_date, c.strike
    HAVING countIf(c.side IN ('call', 'c')) > 0
       AND countIf(c.side IN ('put', 'p')) > 0
),
implied AS (
    SELECT
        sym,
        pre_date,
        argMin(round(100 * (call_px + put_px) / spot, 2), abs(strike / spot - 1)) AS straddle_pct,
        argMin(round(100 * atm_iv * sqrt(dte / 365), 2), abs(strike / spot - 1))  AS iv_root_t_pct
    FROM straddles
    GROUP BY sym, pre_date
)
SELECT
    bucket                                                           AS ratio_bucket,
    toUInt32(n)                                                      AS event_count,
    round(100 * n / sum(n) OVER (), 1)                               AS share_pct,
    round(100 * sum(n) OVER (ORDER BY sort_key) / sum(n) OVER (), 1) AS cumulative_pct
FROM
(
    SELECT
        multiIf(r <= 0.50, 1, r <= 0.75, 2, r <= 1.00, 3, r <= 1.50, 4, r <= 2.00, 5, 6) AS sort_key,
        multiIf(r <= 0.50, 'under 0.50x',
                r <= 0.75, '0.50 to 0.75x',
                r <= 1.00, '0.75 to 1.00x',
                r <= 1.50, '1.00 to 1.50x',
                r <= 2.00, '1.50 to 2.00x',
                'over 2.00x') AS bucket,
        count()               AS n
    FROM
    (
        SELECT m.realized_pct / i.straddle_pct AS r
        FROM moves AS m
        INNER JOIN implied AS i ON i.sym = m.sym AND i.pre_date = m.pre_date
        WHERE i.straddle_pct > 0
    )
    GROUP BY sort_key, bucket
)
ORDER BY sort_key
Run this yourself

32% of reports delivered less than half the implied move, the under 0.50x bucket, 33 events. Adding the buckets up to the 1.00x line, 53.4% of prints finished at or under the implied move. The far tail, over 2.00x, holds 10.7% of the sample, 11 events where the stock travelled more than twice what the straddle priced.

Why the gap exists

The premium on an earnings straddle is payment for a range of outcomes, and most outcomes land near the middle of that range. A straddle carried through a print needs the tail, not the median, to cover its cost. The distribution above is what that asymmetry looks like when you count it.

The second half of the mechanism sits inside the option price. Ahead of a report, front-expiry IV carries an extra premium for the scheduled event. Once the numbers are public that premium leaves the option, usually inside a single session, and it leaves faster than the share price move can make up for it. This is IV crush, and it is why a long option can lose value on a day the stock moved the way its holder wanted. How earnings move option greeks follows delta, gamma, theta and vega across the same event.

Does the pattern hold year to year?

A pooled statistic can hide a single unusual season. Splitting the sample by calendar year keeps that visible.

QueryUnder-implied share and median ratio, year by year
yearevent_countunder_implied_pctmedian_ratio
20255761.40.78
20264643.51.12
The exact SQL behind every number
WITH
reports AS (
    SELECT
        toString(ticker)    AS sym,
        toDate(filing_date) AS report_date
    FROM global_markets.stocks_8k_text
    WHERE ticker IN ('AAPL', 'AMD', 'AMZN', 'AVGO', 'DIS', 'GOOGL', 'JPM', 'KO', 'META', 'MSFT', 'NFLX', 'NVDA', 'ORCL', 'QCOM', 'TSLA', 'WMT')
      AND filing_date >= '2021-09-01'
      AND filing_date <  '2026-09-20'
      AND (items_text ILIKE '%results of operations and financial condition%'
        OR items_text ILIKE '%item 2.02%')
    GROUP BY sym, report_date
),
sessions AS (
    SELECT
        toString(ticker) AS sym,
        toDate(date)     AS session_date,
        toFloat64(close) AS px
    FROM global_markets.stocks_daily_aggs
    WHERE ticker IN ('AAPL', 'AMD', 'AMZN', 'AVGO', 'DIS', 'GOOGL', 'JPM', 'KO', 'META', 'MSFT', 'NFLX', 'NVDA', 'ORCL', 'QCOM', 'TSLA', 'WMT')
      AND date >= '2021-08-01'
      AND date <  '2026-09-27'
      AND close > 0
),
spans AS (
    SELECT
        r.sym         AS sym,
        r.report_date AS report_date,
        maxIf(s.session_date, s.session_date < r.report_date) AS pre_date,
        minIf(s.session_date, s.session_date > r.report_date) AS post_date
    FROM reports AS r
    INNER JOIN sessions AS s ON s.sym = r.sym
    WHERE s.session_date >= r.report_date - 8
      AND s.session_date <= r.report_date + 8
    GROUP BY r.sym, r.report_date
    HAVING countIf(s.session_date < r.report_date) > 0
       AND countIf(s.session_date > r.report_date) > 0
),
moves AS (
    SELECT
        sp.sym         AS sym,
        sp.report_date AS report_date,
        sp.pre_date    AS pre_date,
        round(100 * abs(b.px / a.px - 1), 2) AS realized_pct
    FROM spans AS sp
    INNER JOIN sessions AS a ON a.sym = sp.sym AND a.session_date = sp.pre_date
    INNER JOIN sessions AS b ON b.sym = sp.sym AND b.session_date = sp.post_date
),
greeks AS (
    SELECT
        toString(underlying_symbol)   AS sym,
        toDate(date)                  AS pre_date,
        toDate(expiration_date)       AS expiry,
        lower(option_type)            AS side,
        toFloat64(strike_price)       AS strike,
        toFloat64(option_close)       AS opt_px,
        toFloat64(underlying_close)   AS spot,
        toFloat64(implied_volatility) AS iv,
        toUInt16(days_to_expiry)      AS dte
    FROM global_markets.options_greeks
    WHERE underlying_symbol IN ('AAPL', 'AMD', 'AMZN', 'AVGO', 'DIS', 'GOOGL', 'JPM', 'KO', 'META', 'MSFT', 'NFLX', 'NVDA', 'ORCL', 'QCOM', 'TSLA', 'WMT')
      AND date >= '2021-09-01'
      AND date <  '2026-09-20'
      AND iv_converged = 1
      AND volume > 0
      AND days_to_expiry BETWEEN 1 AND 45
      AND underlying_close > 0
      AND option_close > 0
      AND abs(toFloat64(strike_price) / toFloat64(underlying_close) - 1) < 0.05
),
chain AS (
    SELECT
        g.sym      AS sym,
        g.pre_date AS pre_date,
        g.expiry   AS expiry,
        g.side     AS side,
        g.strike   AS strike,
        g.opt_px   AS opt_px,
        g.spot     AS spot,
        g.iv       AS iv,
        g.dte      AS dte
    FROM greeks AS g
    INNER JOIN moves AS m ON m.sym = g.sym AND m.pre_date = g.pre_date
    WHERE g.expiry > m.report_date
),
front AS (
    SELECT sym, pre_date, min(expiry) AS expiry
    FROM chain
    GROUP BY sym, pre_date
),
straddles AS (
    SELECT
        c.sym       AS sym,
        c.pre_date  AS pre_date,
        c.strike    AS strike,
        any(c.spot) AS spot,
        max(c.dte)  AS dte,
        avgIf(c.opt_px, c.side IN ('call', 'c')) AS call_px,
        avgIf(c.opt_px, c.side IN ('put', 'p'))  AS put_px,
        avg(c.iv)   AS atm_iv
    FROM chain AS c
    INNER JOIN front AS f
        ON f.sym = c.sym AND f.pre_date = c.pre_date AND f.expiry = c.expiry
    GROUP BY c.sym, c.pre_date, c.strike
    HAVING countIf(c.side IN ('call', 'c')) > 0
       AND countIf(c.side IN ('put', 'p')) > 0
),
implied AS (
    SELECT
        sym,
        pre_date,
        argMin(round(100 * (call_px + put_px) / spot, 2), abs(strike / spot - 1)) AS straddle_pct,
        argMin(round(100 * atm_iv * sqrt(dte / 365), 2), abs(strike / spot - 1))  AS iv_root_t_pct
    FROM straddles
    GROUP BY sym, pre_date
)
SELECT
    toString(toYear(m.report_date))                                     AS year,
    toUInt32(count())                                                   AS event_count,
    round(100 * countIf(m.realized_pct <= i.straddle_pct) / count(), 1) AS under_implied_pct,
    round(quantileDeterministic(0.5)(m.realized_pct / i.straddle_pct, cityHash64(m.sym, m.report_date)), 2) AS median_ratio
FROM moves AS m
INNER JOIN implied AS i ON i.sym = m.sym AND i.pre_date = m.pre_date
WHERE i.straddle_pct > 0
GROUP BY year
ORDER BY year
Run this yourself

The panel splits the scored sample into 2 calendar years. The earlier row, 2025, holds 57 scored reports, 61.4% of them at or under implied. The later row, 2026, holds 46 reports, 43.5% at or under implied, with a median ratio of 1.12. Those event counts carry the coverage story as much as the calendar one: the scored window opens where the per-contract chain data opens and closes with this month's filings, and the year rows are uneven across that span.

When realized blows through implied

The overshoots are the whole reason an implied move is not a ceiling. These are the widest ones in the sample.

QueryThe ten widest overshoots: realized versus implied
eventimplied_pctrealized_pctratio
ORCL September 9, 20259.337.684.05
AMZN July 30, 20266.9719.822.84
AMD May 5, 20268.623.382.72
TSLA July 22, 20265.9215.632.64
AAPL July 30, 20263.678.662.36
QCOM April 29, 20268.3719.722.36
AMD February 3, 20268.3518.712.24
KO February 11, 20252.966.442.18
TSLA April 2, 20263.557.462.1
DIS May 7, 20256.7914.052.07
The exact SQL behind every number
WITH
reports AS (
    SELECT
        toString(ticker)    AS sym,
        toDate(filing_date) AS report_date
    FROM global_markets.stocks_8k_text
    WHERE ticker IN ('AAPL', 'AMD', 'AMZN', 'AVGO', 'DIS', 'GOOGL', 'JPM', 'KO', 'META', 'MSFT', 'NFLX', 'NVDA', 'ORCL', 'QCOM', 'TSLA', 'WMT')
      AND filing_date >= '2021-09-01'
      AND filing_date <  '2026-09-20'
      AND (items_text ILIKE '%results of operations and financial condition%'
        OR items_text ILIKE '%item 2.02%')
    GROUP BY sym, report_date
),
sessions AS (
    SELECT
        toString(ticker) AS sym,
        toDate(date)     AS session_date,
        toFloat64(close) AS px
    FROM global_markets.stocks_daily_aggs
    WHERE ticker IN ('AAPL', 'AMD', 'AMZN', 'AVGO', 'DIS', 'GOOGL', 'JPM', 'KO', 'META', 'MSFT', 'NFLX', 'NVDA', 'ORCL', 'QCOM', 'TSLA', 'WMT')
      AND date >= '2021-08-01'
      AND date <  '2026-09-27'
      AND close > 0
),
spans AS (
    SELECT
        r.sym         AS sym,
        r.report_date AS report_date,
        maxIf(s.session_date, s.session_date < r.report_date) AS pre_date,
        minIf(s.session_date, s.session_date > r.report_date) AS post_date
    FROM reports AS r
    INNER JOIN sessions AS s ON s.sym = r.sym
    WHERE s.session_date >= r.report_date - 8
      AND s.session_date <= r.report_date + 8
    GROUP BY r.sym, r.report_date
    HAVING countIf(s.session_date < r.report_date) > 0
       AND countIf(s.session_date > r.report_date) > 0
),
moves AS (
    SELECT
        sp.sym         AS sym,
        sp.report_date AS report_date,
        sp.pre_date    AS pre_date,
        round(100 * abs(b.px / a.px - 1), 2) AS realized_pct
    FROM spans AS sp
    INNER JOIN sessions AS a ON a.sym = sp.sym AND a.session_date = sp.pre_date
    INNER JOIN sessions AS b ON b.sym = sp.sym AND b.session_date = sp.post_date
),
greeks AS (
    SELECT
        toString(underlying_symbol)   AS sym,
        toDate(date)                  AS pre_date,
        toDate(expiration_date)       AS expiry,
        lower(option_type)            AS side,
        toFloat64(strike_price)       AS strike,
        toFloat64(option_close)       AS opt_px,
        toFloat64(underlying_close)   AS spot,
        toFloat64(implied_volatility) AS iv,
        toUInt16(days_to_expiry)      AS dte
    FROM global_markets.options_greeks
    WHERE underlying_symbol IN ('AAPL', 'AMD', 'AMZN', 'AVGO', 'DIS', 'GOOGL', 'JPM', 'KO', 'META', 'MSFT', 'NFLX', 'NVDA', 'ORCL', 'QCOM', 'TSLA', 'WMT')
      AND date >= '2021-09-01'
      AND date <  '2026-09-20'
      AND iv_converged = 1
      AND volume > 0
      AND days_to_expiry BETWEEN 1 AND 45
      AND underlying_close > 0
      AND option_close > 0
      AND abs(toFloat64(strike_price) / toFloat64(underlying_close) - 1) < 0.05
),
chain AS (
    SELECT
        g.sym      AS sym,
        g.pre_date AS pre_date,
        g.expiry   AS expiry,
        g.side     AS side,
        g.strike   AS strike,
        g.opt_px   AS opt_px,
        g.spot     AS spot,
        g.iv       AS iv,
        g.dte      AS dte
    FROM greeks AS g
    INNER JOIN moves AS m ON m.sym = g.sym AND m.pre_date = g.pre_date
    WHERE g.expiry > m.report_date
),
front AS (
    SELECT sym, pre_date, min(expiry) AS expiry
    FROM chain
    GROUP BY sym, pre_date
),
straddles AS (
    SELECT
        c.sym       AS sym,
        c.pre_date  AS pre_date,
        c.strike    AS strike,
        any(c.spot) AS spot,
        max(c.dte)  AS dte,
        avgIf(c.opt_px, c.side IN ('call', 'c')) AS call_px,
        avgIf(c.opt_px, c.side IN ('put', 'p'))  AS put_px,
        avg(c.iv)   AS atm_iv
    FROM chain AS c
    INNER JOIN front AS f
        ON f.sym = c.sym AND f.pre_date = c.pre_date AND f.expiry = c.expiry
    GROUP BY c.sym, c.pre_date, c.strike
    HAVING countIf(c.side IN ('call', 'c')) > 0
       AND countIf(c.side IN ('put', 'p')) > 0
),
implied AS (
    SELECT
        sym,
        pre_date,
        argMin(round(100 * (call_px + put_px) / spot, 2), abs(strike / spot - 1)) AS straddle_pct,
        argMin(round(100 * atm_iv * sqrt(dte / 365), 2), abs(strike / spot - 1))  AS iv_root_t_pct
    FROM straddles
    GROUP BY sym, pre_date
)
SELECT
    concat(m.sym, ' ', monthName(m.report_date), ' ', toString(toDayOfMonth(m.report_date)), ', ', toString(toYear(m.report_date))) AS event,
    i.straddle_pct                            AS implied_pct,
    m.realized_pct                            AS realized_pct,
    round(m.realized_pct / i.straddle_pct, 2) AS ratio
FROM moves AS m
INNER JOIN implied AS i ON i.sym = m.sym AND i.pre_date = m.pre_date
WHERE i.straddle_pct > 0
ORDER BY m.realized_pct / i.straddle_pct DESC
LIMIT 10
Run this yourself

The widest miss is ORCL September 9, 2025: the straddle priced 9.3%, and the two closes around the filing spanned 37.68%, which is 4.05 times the implied number. Even the tenth and last entry on the list, DIS May 7, 2025, ran to 2.07 times implied. A tail this fat is why the median is the honest summary of the ratio and an average is not.

What this sample can and cannot say

  • The universe is the sixteen household large caps with liquid listed options named in the SQL above, and 13 of them cleared the five-report minimum. Nothing here carries over to small caps, where a wide at-the-money quote can swamp the calculation.
  • The scored window is much shorter than the price history behind it. A report only enters the sample when the session before it carries a traded, converged at-the-money chain in the per-contract options data, and what survives that filter falls in the calendar years 2025 through 2026. For NVDA the scored span is 2025-02-26 to 2026-08-26. A handful of prints per name is a small sample, and the year rows above are smaller still.
  • Report dates come from each company's own 8-K filings tagged Item 2.02. A release that arrives without such a filing is absent from the sample, and an Item 2.02 filing that is not a quarterly report gets scored as though it were one.
  • The reaction window spans two closes, the session before the filing date and the session after it. The filing record carries a date and not a timestamp, so a company reporting before the open and one reporting after the close both land inside that window. It gives the realized side slightly more room than a strict single session would.
  • The expiry is whichever listed expiry falls first after the report, capped at 45 days out, with the strike inside 5% of the share price and the contract required to have traded that day with a converged IV. A longer expiry prices a longer horizon, which lifts the implied number.
  • Medians rather than averages throughout, given the tail above.

FAQ

What is the implied move for an earnings report?

It is the size of the price change the options market is charging for across the report, quoted as a percentage of the share price and read from the last close before the release. The quickest version is the at-the-money straddle in the first expiry after the report date, divided by the share price.

How do I calculate the expected move from an option price?

Add the at-the-money call and put premiums for the expiry just after the event, then divide by the share price. The model route is implied volatility times the square root of days to expiry over 365, which lands close to the straddle read on the same contracts.

Do options overprice earnings moves?

In this sample the realized move finished at or under the implied move on 53.4% of scored reports. The rest overshot, some of them by multiples, so an implied move reads as a central estimate rather than a cap.

Why does implied volatility fall after earnings?

Front-expiry IV carries an event premium while the report is still ahead of it. Once results are public the scheduled uncertainty is gone and that premium leaves the option, usually within a session. The IV crush explainer walks through the mechanics with data.

Does a bigger implied move mean a bigger realized move?

A bigger implied move means the options market is charging for a wider range, nothing more. Whether the stock fills that range is a separate question, and the ratio in the scoreboard and distribution panels above is the measurement of it.


Every panel here carries the SQL that produced it, so the scoreboard can be re-run with a different universe, a different expiry rule, or a longer reaction window. To price the implied move for a ticker you follow, ask for it in plain English on the Strasmore terminal.