Unusual Options Activity: Last Session
Unusual options activity ranked from the full US options tape: which underlyings traded far above their own 20-session average, and how calls and puts split.
Unusual options activity means one underlying trading far more option contracts in a session than its own recent normal. This board ranks it for the last completed options session, Jul 21: total contracts traded against that same underlying's average over the prior 20 sessions. The options tape settles behind the equity tape, so the session named here is the newest complete one in our data rather than today's.
One thing to be clear about before the table: this is a volume screen. Most published unusual-options alerts compare a day's volume against open interest, the count of contracts still outstanding after the close. The options tape we store carries trades, not positions, and there is no open-interest field anywhere in it. Every ratio below compares an underlying's volume against its own volume history. Options volume vs. open interest covers what each measurement answers.
Unusual options activity: the last completed session
The exact SQL behind every number
WITH tape AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
sum(toFloat64(volume)) AS vol
FROM global_markets.options_minute_aggs
WHERE window_start >= toDateTime(today() - 45, 'America/New_York')
GROUP BY d
),
ranked AS (
SELECT d, vol, row_number() OVER (ORDER BY d DESC) AS raw_rn
FROM tape
),
cal AS (
SELECT d, vol, rn, sum(if(rn BETWEEN 2 AND 21, 1, 0)) OVER () AS baseline_sessions
FROM (
SELECT d, vol, row_number() OVER (ORDER BY d DESC) AS rn
FROM ranked
WHERE vol >= 0.75 * (SELECT quantileExact(0.5)(vol) FROM ranked WHERE raw_rn > 1)
)
),
day_root AS (
SELECT substring(ticker, 3, length(ticker) - 17) AS root,
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
sum(toFloat64(volume)) AS vol
FROM global_markets.options_minute_aggs
WHERE window_start >= toDateTime(today() - 45, 'America/New_York')
GROUP BY root, d
),
scored AS (
SELECT r.root AS root,
sumIf(r.vol, c.rn = 1) AS last_vol,
avgIf(r.vol, c.rn BETWEEN 2 AND 21) AS base_vol,
maxIf(r.vol, c.rn BETWEEN 2 AND 21) AS prior_high,
countIf(c.rn BETWEEN 2 AND 21) AS root_sessions,
max(c.baseline_sessions) AS baseline_session_count,
maxIf(toYYYYMMDD(c.d), c.rn = 1) AS session_id,
maxIf(formatDateTime(c.d, '%b %e'), c.rn = 1) AS session_label
FROM day_root r INNER JOIN cal c ON r.d = c.d
WHERE c.rn <= 21
AND r.root NOT IN ('SPCX')
AND r.root NOT IN ('KORU','SOXL','SOXS','TQQQ','SQQQ','NVDL','NVDS','NVD','TSLL','TSLQ','TSLZ','SPXL','SPXS','UPRO','SPXU','LABU','LABD','FAS','FAZ','TNA','TZA','YINN','YANG','UDOW','SDOW','BOIL','KOLD','UCO','SCO','USD','SSO','SDS','QLD','QID','ERX','ERY','DRN','DRV','CURE','SOXY','MUU','SNXX','UVXY','SVXY','UVIX','SVIX','BULZ','WEBL','WEBS','DPST','DRIP','GUSH','AGQ','ZSL','BITX','ETHU','MSTX','MSTU','CONL','DUST','JNUG','JDST','NUGT')
GROUP BY r.root
HAVING last_vol >= 25000 AND base_vol >= 5000 AND root_sessions >= 18
)
SELECT root AS ticker,
round(last_vol / base_vol, 1) AS vol_ratio,
round(last_vol / 1000, 1) AS session_volume_k,
round(base_vol / 1000, 1) AS baseline_volume_k,
round(prior_high / 1000, 1) AS prior_high_volume_k,
session_label,
session_id,
baseline_session_count
FROM scored
ORDER BY vol_ratio DESC, ticker ASC
LIMIT 10Three columns carry the reading. The ratio says how far above its own normal the underlying traded. The baseline says what normal was, and a large multiple off a thin chain is a smaller event than a modest one off a busy chain. The 20-session high says whether the session beat anything in the previous month. The last three columns are the audit trail: the session the board is built from, printed as a label and as a plain number, plus the count of eligible sessions standing behind the baseline, which has to come to 20 for the page to publish.
ASST2 tops the board at 6.7x its own average: 76 thousand contracts against a baseline of 11.3 thousand, with a prior 20-session high of 201.1 thousand.
- MMM at 6.3x: 64.9 thousand contracts against a 10.4 thousand baseline.
- DHR at 5.2x, 28.6 thousand contracts on a 5.5 thousand baseline.
- GM at 3.5x, 58.8 thousand contracts against a 20-session high of 57.2 thousand.
- CVS at 3.2x on 59.3 thousand contracts.
The last row of the board, HAL, still traded 2.6x its own average. Multiple and size are separate readings, which is why both sit on the board. The share-market version of the same idea lives at unusual volume stocks this week, and the arithmetic itself is worked through in relative volume.
Calls or puts: the side of the tape
The split between call and put volume is most of what an alert is really pointing at. Same board, same session, sorted by call share:
The exact SQL behind every number
WITH tape AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
sum(toFloat64(volume)) AS vol
FROM global_markets.options_minute_aggs
WHERE window_start >= toDateTime(today() - 45, 'America/New_York')
GROUP BY d
),
ranked AS (
SELECT d, vol, row_number() OVER (ORDER BY d DESC) AS raw_rn
FROM tape
),
cal AS (
SELECT d, vol, rn, sum(if(rn BETWEEN 2 AND 21, 1, 0)) OVER () AS baseline_sessions
FROM (
SELECT d, vol, row_number() OVER (ORDER BY d DESC) AS rn
FROM ranked
WHERE vol >= 0.75 * (SELECT quantileExact(0.5)(vol) FROM ranked WHERE raw_rn > 1)
)
),
day_root AS (
SELECT substring(ticker, 3, length(ticker) - 17) AS root,
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
sum(toFloat64(volume)) AS vol,
sumIf(toFloat64(volume), substring(ticker, length(ticker) - 8, 1) = 'C') AS calls,
sumIf(toFloat64(volume), substring(ticker, length(ticker) - 8, 1) = 'P') AS puts
FROM global_markets.options_minute_aggs
WHERE window_start >= toDateTime(today() - 45, 'America/New_York')
GROUP BY root, d
),
scored AS (
SELECT r.root AS root,
round(sumIf(r.vol, c.rn = 1) / avgIf(r.vol, c.rn BETWEEN 2 AND 21), 1) AS vol_ratio,
sumIf(r.calls, c.rn = 1) AS calls,
sumIf(r.puts, c.rn = 1) AS puts,
maxIf(toYYYYMMDD(c.d), c.rn = 1) AS session_id
FROM day_root r INNER JOIN cal c ON r.d = c.d
WHERE c.rn <= 21
AND r.root NOT IN ('SPCX')
AND r.root NOT IN ('KORU','SOXL','SOXS','TQQQ','SQQQ','NVDL','NVDS','NVD','TSLL','TSLQ','TSLZ','SPXL','SPXS','UPRO','SPXU','LABU','LABD','FAS','FAZ','TNA','TZA','YINN','YANG','UDOW','SDOW','BOIL','KOLD','UCO','SCO','USD','SSO','SDS','QLD','QID','ERX','ERY','DRN','DRV','CURE','SOXY','MUU','SNXX','UVXY','SVXY','UVIX','SVIX','BULZ','WEBL','WEBS','DPST','DRIP','GUSH','AGQ','ZSL','BITX','ETHU','MSTX','MSTU','CONL','DUST','JNUG','JDST','NUGT')
GROUP BY r.root
HAVING sumIf(r.vol, c.rn = 1) >= 25000
AND avgIf(r.vol, c.rn BETWEEN 2 AND 21) >= 5000
AND countIf(c.rn BETWEEN 2 AND 21) >= 18
),
board AS (
SELECT root, vol_ratio, calls, puts, session_id
FROM scored
ORDER BY vol_ratio DESC, root ASC
LIMIT 10
)
SELECT root AS ticker,
round(calls / 1000, 1) AS call_volume_k,
round(puts / 1000, 1) AS put_volume_k,
round(100.0 * calls / (calls + puts), 0) AS call_share_pct,
session_id
FROM board
WHERE calls + puts > 0
ORDER BY call_share_pct DESC, ticker ASCAt the call-heavy end, ASST2 put 100% of its contracts into calls: 76 thousand calls against 0 thousand puts. At the other end of the same table, WBD ran 25% calls, 78.6 thousand against 239.7 thousand puts.
That split is worth reading and easy to over-read. Every printed contract has a buyer and a seller, and the tape does not record which side initiated. Heavy call volume can be an outright long position or a covered call written against stock the seller already owns. Buying and selling call options walks through what each side of that trade takes on. Where the tape is loud, option prices usually are too, which is the subject of the highest implied volatility stocks board.
What the session's contracts were made of
The exact SQL behind every number
WITH tape AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
sum(toFloat64(volume)) AS vol
FROM global_markets.options_minute_aggs
WHERE window_start >= toDateTime(today() - 45, 'America/New_York')
GROUP BY d
),
ranked AS (
SELECT d, vol, row_number() OVER (ORDER BY d DESC) AS raw_rn
FROM tape
),
cal AS (
SELECT d, vol, rn, sum(if(rn BETWEEN 2 AND 21, 1, 0)) OVER () AS baseline_sessions
FROM (
SELECT d, vol, row_number() OVER (ORDER BY d DESC) AS rn
FROM ranked
WHERE vol >= 0.75 * (SELECT quantileExact(0.5)(vol) FROM ranked WHERE raw_rn > 1)
)
),
bars AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(volume) AS v,
toDate(concat('20', substring(ticker, length(ticker) - 14, 2), '-',
substring(ticker, length(ticker) - 12, 2), '-',
substring(ticker, length(ticker) - 10, 2))) AS expiry
FROM global_markets.options_minute_aggs
WHERE window_start >= toDateTime(today() - 12, 'America/New_York')
AND toDate(toTimeZone(window_start, 'America/New_York')) IN (SELECT d FROM cal WHERE rn = 1)
),
scored AS (
SELECT multiIf(expiry - d <= 0, 1, expiry - d <= 7, 2, expiry - d <= 30, 3,
expiry - d <= 90, 4, expiry - d <= 365, 5, 6) AS bk,
v, d
FROM bars
)
SELECT arrayElement(['same day', '1 to 7 days', '8 to 30 days', '31 to 90 days', '91 to 365 days', 'over a year'], bk) AS dte_bucket,
round(sum(v) / 1e6, 2) AS contracts_m,
round(100.0 * sum(v) / sum(sum(v)) OVER (), 1) AS pct_of_volume,
toYYYYMMDD(max(d)) AS session_id
FROM scored
GROUP BY bk
ORDER BY bk ASCSame-day expiries took 29.7% of the whole session, 16.85 million contracts. Another 30.8% expired inside a week. At the far end, contracts with more than a year left took 2.1%.
That mix changes how an unusual-activity headline should read. Most of the volume behind any board like this is short-dated, and a same-day contract settles by the closing bell rather than being carried. 0DTE options covers that end of the chain.
Was this an expiration session?
Options volume rises across the whole market on monthly expiration, when the largest block of listed contracts comes off the board at once. A ratio screen run on an expiration day flags half the market, so the panel below labels every session in the window.
The exact SQL behind every number
WITH tape AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
sum(toFloat64(volume)) AS vol
FROM global_markets.options_minute_aggs
WHERE window_start >= toDateTime(today() - 45, 'America/New_York')
GROUP BY d
),
ranked AS (
SELECT d, vol, row_number() OVER (ORDER BY d DESC) AS raw_rn
FROM tape
),
cal AS (
SELECT d, vol, rn, sum(if(rn BETWEEN 2 AND 21, 1, 0)) OVER () AS baseline_sessions
FROM (
SELECT d, vol, row_number() OVER (ORDER BY d DESC) AS rn
FROM ranked
WHERE vol >= 0.75 * (SELECT quantileExact(0.5)(vol) FROM ranked WHERE raw_rn > 1)
)
),
w AS (
SELECT d, vol,
toStartOfMonth(d) + toIntervalDay(((5 - toDayOfWeek(toStartOfMonth(d)) + 7) % 7) + 14) AS third_friday
FROM cal
WHERE rn <= 25
),
marked AS (
SELECT d, vol,
(d = max(if(d <= third_friday, d, toDate('1970-01-01'))) OVER (PARTITION BY toStartOfMonth(d)))
AND (third_friday <= max(d) OVER ()) AS is_expiry
FROM w
),
latest AS (
SELECT d, vol, is_expiry,
max(if(is_expiry, d, toDate('1970-01-01'))) OVER () AS last_expiry_d
FROM marked
)
SELECT formatDateTime(d, '%b %e') AS session,
round(vol / 1e6, 1) AS contracts_m,
multiIf(is_expiry, 'monthly expiration', 'ordinary') AS session_type,
round(max(if(d = last_expiry_d, vol, 0)) OVER () / 1e6, 1) AS monthly_expiry_m,
toYYYYMMDD(d) AS session_id
FROM latest
ORDER BY d ASCThe newest session in this panel is the one the board reports, the same date in both tables, and its label reads ordinary. It printed 56.8 million contracts across the whole US tape. The most recent monthly expiration inside the same window printed 76.8 million. The 20-session baseline behind every ratio on the board normally contains exactly one monthly expiration, so the denominator carries the same lump from session to session.
Does unusual options activity lead the stock?
The question worth answering is whether a heavy options session is followed by a heavy move in the underlying. Measured over the last 200 days: every name-session clearing the same volume floors, bucketed by its ratio, matched against the absolute change in the underlying's close on the next session. The typical column is each name's own median daily change across the window, so every bucket is compared with itself.
The exact SQL behind every number
WITH daily AS (
SELECT underlying_symbol AS sym,
date AS d,
sum(volume) AS vol,
max(underlying_close) AS px
FROM global_markets.options_greeks
WHERE date >= today() - 200
AND underlying_close > 0
AND underlying_symbol NOT IN ('SPCX')
AND underlying_symbol NOT IN ('KORU','SOXL','SOXS','TQQQ','SQQQ','NVDL','NVDS','NVD','TSLL','TSLQ','TSLZ','SPXL','SPXS','UPRO','SPXU','LABU','LABD','FAS','FAZ','TNA','TZA','YINN','YANG','UDOW','SDOW','BOIL','KOLD','UCO','SCO','USD','SSO','SDS','QLD','QID','ERX','ERY','DRN','DRV','CURE','SOXY','MUU','SNXX','UVXY','SVXY','UVIX','SVIX','BULZ','WEBL','WEBS','DPST','DRIP','GUSH','AGQ','ZSL','BITX','ETHU','MSTX','MSTU','CONL','DUST','JNUG','JDST','NUGT')
AND underlying_symbol NOT IN (SELECT ticker FROM global_markets.stocks_splits
WHERE execution_date BETWEEN today() - 230 AND today())
GROUP BY sym, d
),
seq AS (
SELECT sym, d, vol, px,
avg(vol) OVER (PARTITION BY sym ORDER BY d ROWS BETWEEN 20 PRECEDING AND 1 PRECEDING) AS base,
count() OVER (PARTITION BY sym ORDER BY d ROWS BETWEEN 20 PRECEDING AND 1 PRECEDING) AS base_n,
any(px) OVER (PARTITION BY sym ORDER BY d ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_px,
any(px) OVER (PARTITION BY sym ORDER BY d ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_px
FROM daily
),
moves AS (
SELECT sym, vol, base, base_n,
if(next_px > 0, 100 * abs(next_px / px - 1), -1) AS next_abs,
if(next_px > 0, 100 * (next_px / px - 1), -999) AS next_signed,
if(prev_px > 0, 100 * abs(px / prev_px - 1), -1) AS own_abs
FROM seq
),
typical AS (
SELECT sym, quantileExact(0.5)(own_abs) AS typ
FROM moves
WHERE own_abs >= 0
GROUP BY sym
)
SELECT arrayElement(['5x or more', '3x to 5x', '2x to 3x', '1x to 2x', 'below 1x'], bk) AS rvol_bucket,
count() AS event_count,
round(quantileExact(0.5)(next_abs), 2) AS median_next_move_pct,
round(quantileExact(0.5)(typ), 2) AS median_typical_move_pct,
round(quantileExact(0.5)(next_abs) - quantileExact(0.5)(typ), 2) AS gap_pp,
round(quantileExact(0.5)(next_signed), 2) AS median_next_signed_pct
FROM (
SELECT m.sym AS sym,
multiIf(m.vol / m.base >= 5, 1, m.vol / m.base >= 3, 2, m.vol / m.base >= 2, 3,
m.vol / m.base >= 1, 4, 5) AS bk,
m.next_abs AS next_abs,
m.next_signed AS next_signed,
t.typ AS typ
FROM moves m INNER JOIN typical t ON m.sym = t.sym
WHERE m.base_n = 20 AND m.base >= 5000 AND m.vol >= 25000 AND m.next_abs >= 0
)
GROUP BY bk
ORDER BY bk ASCFollowing a session at five times its own baseline or more, the median absolute change on the next session was 2.61%, against 1.98% for those same names on an ordinary day, a gap of 0.63 percentage points. At the quiet end of the table the two columns read 1.89% and 2.11%, a gap of -0.22 points.
The honest answer is small, and it is about size rather than direction. Heavier options sessions are followed by slightly bigger moves in absolute terms, and the gap at the median is a fraction of a percentage point. The median signed change on the session after a five-times day was -0.35%, and after a below-baseline day -0.06%. Neither is the move an alert headline implies. Rarity is worth holding in view too: 638 name-sessions cleared five times their own baseline across the study window, against 11466 that traded below it.
How this screen is measured
A screen with hidden rules reads as data and is actually opinion, so every rule is here.
- Source. The full US options minute tape. The underlying is parsed out of each contract's OCC symbol, so index option roots and same-day expiries are both included alongside ordinary stock and fund chains.
- The ratio. The last completed session's total contract volume divided by the same underlying's average across the prior 20 eligible sessions. At least 18 of those 20 must carry volume. The board prints the window length in a column of its own, and a window that comes up short of 20 holds the page rather than publishing a shorter average under a 20-session label.
- Floors. 25,000 contracts on the session and a 5,000-contract baseline. Without them the board fills with chains where one order is a 10x reading.
- Session eligibility. Every panel on this page reads its sessions from one 45-day window of the same tape, under one rule: a session counts once its market-wide volume reaches 75% of the median of the older sessions in that window. The newest day is left out of the median it has to clear, so a partly-loaded day cannot pull its own bar down to meet it. The tape arrives at the front edge in pieces, and a day carrying less than three quarters of a normal session stays off the board until it fills. Each panel prints the session it landed on, so all four can be read against each other.
- Exclusions. Leveraged and inverse funds are removed by name: a 3x product's chain moves with its multiplier and would crowd the list every week. One reused ticker symbol is excluded as well, since vendor history splices two companies under it.
- No open interest. There is no open-interest field in the warehouse, so nothing on this page compares volume against outstanding positions. Where a competitor's screen says volume beat open interest, ours says volume beat its own 20-session average.
- Expiration days. Monthly expiration lifts options volume market-wide. The session panel labels the condition, and a board built on an expiration session should be read against other expiration sessions.
The follow-through study uses a second table, the daily options greeks file, which carries a clean underlying symbol and the underlying's close. It excludes same-day expiries and index options and settles a few sessions later than the minute tape, so its universe is narrower than the board's. Names with a stock split inside the study window are removed, since a split fakes a huge move.
FAQ
What is unusual options activity?
An underlying trading far more option contracts in one session than its own recent average. On this page the threshold is arithmetic rather than editorial: the session's contract volume divided by the same underlying's average over the prior 20 sessions, with a floor of 25,000 contracts on the session and 5,000 on the baseline.
Does unusual options activity predict stock moves?
Weakly, and not in a direction. Over the last 200 days of our data, sessions at five times an underlying's own options-volume baseline were followed by a median absolute move of 2.61%, against 1.98% for the same names on an ordinary day. The median signed change on that next session was -0.35%, so the volume said something about how far the stock moved and very little about which way.
What time is options volume data updated?
Contract volume prints live on the consolidated options tape while the session runs. The stored tape behind this page settles with a lag, so the board reports the newest complete session rather than an in-progress one, and stamps its date. Nothing here is a real-time feed.
Why does this unusual options activity scanner not use open interest?
Open interest is published by the clearinghouse after the close, and there is no open-interest field in the data we hold. Comparing a session's volume against the same underlying's own volume history is measurable directly from the tape, and it answers a similar question: is this chain busier than usual.
Every number above is a stored, versioned query. Open the SQL under any panel to audit the measurement, or run the same screen over any window you like on the Strasmore terminal.