Meme Stocks#
The Question#
What happens when retail traders discover options on highly shorted stocks?
Stocks analyzed: GameStop (GME), AMC, BlackBerry (BB), Bed Bath & Beyond (BBBY), and others from the 2021 meme stock frenzy.
Key metrics we’re tracking:
Options activity relative to shares outstanding
Volume patterns in calls vs puts
How options trading compares to stock trading
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['figure.figsize'] = (9,5)
plt.rcParams['font.size'] = 11
plt.rcParams['legend.fontsize'] = 11
FILENAME = '../data/meme_stocks.xlsx'
SHEET = 'meme stock data'
ts = pd.read_excel(FILENAME, sheet_name=SHEET, header=[0, 1], index_col=0)
ts.tail().style.format('{:.1f}').format_index('{:%Y-%m-%d}')
| GME US Equity | AMC US Equity | AAPL US Equity | TSLA US Equity | NVDA US Equity | SPY US Equity | |||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| PX_LAST | OPEN_INT_TOTAL_CALL | OPEN_INT_TOTAL_PUT | VOLUME | VOLUME_TOTAL_CALL | VOLUME_TOTAL_PUT | EQY_SH_OUT | PX_LAST | OPEN_INT_TOTAL_CALL | OPEN_INT_TOTAL_PUT | VOLUME | VOLUME_TOTAL_CALL | VOLUME_TOTAL_PUT | EQY_SH_OUT | PX_LAST | OPEN_INT_TOTAL_CALL | OPEN_INT_TOTAL_PUT | VOLUME | VOLUME_TOTAL_CALL | VOLUME_TOTAL_PUT | EQY_SH_OUT | PX_LAST | OPEN_INT_TOTAL_CALL | OPEN_INT_TOTAL_PUT | VOLUME | VOLUME_TOTAL_CALL | VOLUME_TOTAL_PUT | EQY_SH_OUT | PX_LAST | OPEN_INT_TOTAL_CALL | OPEN_INT_TOTAL_PUT | VOLUME | VOLUME_TOTAL_CALL | VOLUME_TOTAL_PUT | EQY_SH_OUT | PX_LAST | OPEN_INT_TOTAL_CALL | OPEN_INT_TOTAL_PUT | VOLUME | VOLUME_TOTAL_CALL | VOLUME_TOTAL_PUT | EQY_SH_OUT | |
| date | ||||||||||||||||||||||||||||||||||||||||||
| 2025-06-24 | 23.3 | 996503.0 | 530788.0 | 11660172.0 | 135776.0 | 28848.0 | 447.3 | 3.0 | 629523.0 | 146802.0 | 4379887.0 | 36549.0 | 4166.0 | 433.1 | 200.3 | 2903818.0 | 2087842.0 | 54064033.0 | 554893.0 | 276554.0 | 14935.8 | 340.5 | 4231080.0 | 4009088.0 | 114736245.0 | 1096515.0 | 825473.0 | 3221.0 | 147.9 | 9641076.0 | 8768880.0 | 187566121.0 | 1573615.0 | 943560.0 | 24400.0 | 606.8 | 5292474.0 | 11305608.0 | 67735293.0 | 4021862.0 | 5257330.0 | 1023.2 |
| 2025-06-25 | 23.6 | 1013588.0 | 535743.0 | 9840749.0 | 98805.0 | 27869.0 | 447.3 | 3.0 | 638125.0 | 147400.0 | 4968114.0 | 26970.0 | 5303.0 | 433.1 | 201.6 | 2970777.0 | 2123396.0 | 39525730.0 | 438941.0 | 215690.0 | 14935.8 | 327.6 | 4395663.0 | 4118931.0 | 119845050.0 | 1703670.0 | 1186768.0 | 3221.0 | 154.3 | 10137395.0 | 9131950.0 | 269146471.0 | 3863998.0 | 1539360.0 | 24400.0 | 607.1 | 5426166.0 | 11495609.0 | 62114767.0 | 3811191.0 | 4158585.0 | 1029.9 |
| 2025-06-26 | 23.9 | 1036220.0 | 538544.0 | 8777965.0 | 130822.0 | 39860.0 | 447.3 | 3.0 | 649393.0 | 148795.0 | 4237091.0 | 38928.0 | 4006.0 | 433.1 | 201.0 | 3065468.0 | 2163559.0 | 50799121.0 | 601823.0 | 392877.0 | 14935.8 | 325.8 | 4466995.0 | 4202770.0 | 80440907.0 | 1158862.0 | 886635.0 | 3221.0 | 155.0 | 10316490.0 | 9414999.0 | 198145746.0 | 2074735.0 | 1351663.0 | 24400.0 | 611.9 | 5528473.0 | 12030509.0 | 78548361.0 | 3609760.0 | 4683545.0 | 1028.5 |
| 2025-06-27 | 23.6 | 1088638.0 | 551234.0 | 11638237.0 | 192027.0 | 46109.0 | 447.3 | 3.1 | 674242.0 | 153354.0 | 9739939.0 | 53032.0 | 8412.0 | 433.1 | 201.1 | 3162125.0 | 2190106.0 | 73188571.0 | 697983.0 | 322696.0 | 14935.8 | 323.6 | 4576464.0 | 4481144.0 | 89067049.0 | 1668487.0 | 1625142.0 | 3221.0 | 157.8 | 10509692.0 | 9756422.0 | 263234539.0 | 2628172.0 | 1618135.0 | 24400.0 | 614.9 | 5603665.0 | 12312087.0 | 86258398.0 | 4693802.0 | 5119717.0 | 1030.1 |
| 2025-06-30 | 24.4 | 988156.0 | 526013.0 | 10439313.0 | 170159.0 | 29771.0 | 447.3 | 3.1 | 645698.0 | 150747.0 | 6322139.0 | 59150.0 | 13676.0 | 433.1 | 205.2 | 3072744.0 | 2171894.0 | 91912816.0 | 1272592.0 | 365953.0 | 14935.8 | 317.7 | 4130528.0 | 3916484.0 | 76695081.0 | 681068.0 | 611189.0 | 3221.0 | 158.0 | 9984979.0 | 9123520.0 | 194580316.0 | 1292108.0 | 724481.0 | 24400.0 | 617.9 | 5524009.0 | 12421425.0 | 92502541.0 | 3853371.0 | 4346607.0 | 1024.9 |
Options Activity: The Scale of the Phenomenon#
How Big Is the Options Market?#
We’re measuring open interest (total outstanding option contracts) as a percentage of shares outstanding.
SHARES_PER_CONTRACT = 100
SCALE_BB_SHARES_OUT = 1e6
open_call = ts.xs('OPEN_INT_TOTAL_CALL', axis=1, level=1)
open_put = ts.xs('OPEN_INT_TOTAL_PUT', axis=1, level=1)
sh_out = ts.xs('EQY_SH_OUT', axis=1, level=1)
open_call_pct = open_call.mul(SHARES_PER_CONTRACT).div(sh_out.mul(SCALE_BB_SHARES_OUT))
open_put_pct = open_put.mul(SHARES_PER_CONTRACT).div(sh_out.mul(SCALE_BB_SHARES_OUT))
Total Options Activity#
Combined calls + puts: Shows the full scale of options open interest relative to the shares outstanding.
open_opt_pct = open_call_pct + open_put_pct
ax = open_opt_pct.dropna().rolling(WINDOW).mean().plot(title='Total Option Interest as Ratio of Shares Outstanding')
ax.axhline(y=1, color='black', linestyle='--', linewidth=1)
plt.show()
Volumes#
Call Volume Ratio: Call option volume as % of stock volume.
When this exceeds the 1% line, it means call option trading is unusually heavy relative to stock trading.
WINDOW = 21
FLDS = [
'VOLUME',
'VOLUME_TOTAL_CALL',
'VOLUME_TOTAL_PUT'
]
volumes = ts.xs(FLDS[0], axis=1, level=1)
volumes_call = ts.xs(FLDS[1], axis=1, level=1)
volumes_put = ts.xs(FLDS[2], axis=1, level=1)
volume_c_pct = (volumes_call * SHARES_PER_CONTRACT / volumes).rolling(WINDOW).mean()
volume_p_pct = volumes_put * SHARES_PER_CONTRACT / volumes
ax = volume_c_pct.drop(columns=['SPY US Equity']).plot(title='Call Volume as Ratio of Stock Volume')
ax.axhline(y=1, color='black', linestyle='--', linewidth=1)
plt.show()
Stock Prices#
import math
import matplotlib.pyplot as plt
def plot_field(ts, field, window=10,
figsize=None, sharex=False, sharey=False):
"""
Extract one field from a MultiIndex-ts DataFrame and plot each stock.
Parameters
----------
ts : pd.DataFrame
Time-indexed DataFrame whose columns are a MultiIndex
(level0=stock tickers, level1=fields).
field : str
The second-level field name to extract (e.g. 'volume', 'price', etc.).
window : int, default=10
Rolling window size for smoothing.
figsize : tuple, optional
Figure size; if None it defaults to (4*n, 3*n).
sharex, sharey : bool, default=False
Whether subplots share x-/y-axes.
Returns
-------
fig, axes : matplotlib Figure and flattened Axes array
"""
# 1) extract the field across all tickers
try:
df = ts.xs(field, axis=1, level=1)
except KeyError:
raise KeyError(f"Field {field!r} not found in ts.columns")
# 2) set up grid
tickers = df.columns.tolist()
m = len(tickers)
n = math.ceil(math.sqrt(m))
fig, axes = plt.subplots(n, n,
figsize=figsize or (4*n, 3*n),
sharex=sharex,
sharey=sharey)
axes = axes.flatten()
# 3) pull the default color cycle
colors = plt.rcParams['axes.prop_cycle'].by_key()['color']
# 4) loop & plot
for i, (ax, ticker) in enumerate(zip(axes, tickers)):
sm = df[ticker].rolling(window=window, min_periods=1).mean()
ax.plot(sm.index, sm.values,
color=colors[i % len(colors)])
ax.axhline(1, linestyle='--', linewidth=1, color='black')
ax.set_title(ticker)
# 5) hide any unused axes
for ax in axes[m:]:
ax.set_visible(False)
plt.tight_layout()
return fig, axes
FLD = 'PX_LAST'
fig, axes = plot_field(ts, FLD, window=WINDOW)
plt.show()
FLD = 'OPEN_INT_TOTAL_CALL'
fig, axes = plot_field(ts, FLD, window=WINDOW)
#plt.show()
Stock Volume vs. Call Open Interest#
What this shows: How many shares trade relative to call option positions.
Lower ratios suggest options activity is very large compared to stock trading.
fig, axes = plot_field_ratio(ts, 'VOLUME', 'EQY_SH_OUT', window=WINDOW,scale=1/1e6)
plt.show()
Call vs. Put Volume Ratio#
Sentiment indicator: Values above 1.0 show more call than put volume (bullish bias).
Meme stocks typically show very high call/put ratios during rallies.
fig, axes = plot_field_ratio(ts, 'VOLUME', 'OPEN_INT_TOTAL_CALL', window=WINDOW,scale=1/100)
Call Volume as % of Stock Volume#
Scale of speculation: When call volume exceeds 10-20% of stock volume, it indicates massive options speculation.
fig, axes = plot_field_ratio(ts, 'VOLUME_TOTAL_CALL', 'VOLUME_TOTAL_PUT', window=WINDOW)
Put Volume as % of Stock Volume#
Bearish activity: Generally much lower than call volume for meme stocks - retail traders were primarily betting on upward moves.
Extra Analysis#
Peak Options Activity Summary#
# Summary of peak options activity for each stock
summary_stats = pd.DataFrame()
# Peak call open interest as % of shares outstanding
summary_stats['Peak Call OI %'] = open_call_pct.max()
# Peak put open interest as % of shares outstanding
summary_stats['Peak Put OI %'] = open_put_pct.max()
# Peak total options activity
summary_stats['Peak Total OI %'] = open_opt_pct.max()
# Peak call volume as % of stock volume
peak_call_vol = (volumes_call * 100 / volumes).max()
summary_stats['Peak Call Vol %'] = peak_call_vol
# Clean up and sort
summary_stats = summary_stats.round(2)
summary_stats = summary_stats.sort_values('Peak Total OI %', ascending=False)
print("Peak Options Activity by Stock:")
print("=" * 40)
summary_stats
Peak Options Activity by Stock:
========================================
| Peak Call OI % | Peak Put OI % | Peak Total OI % | Peak Call Vol % | |
|---|---|---|---|---|
| AMC US Equity | 4.36 | 5.11 | 9.48 | 1.74 |
| TSLA US Equity | 1.40 | 2.68 | 4.08 | 6.15 |
| GME US Equity | 0.77 | 3.02 | 3.73 | 5.16 |
| SPY US Equity | 1.37 | 1.97 | 3.11 | 13.07 |
| NVDA US Equity | 0.11 | 0.09 | 0.20 | 3.85 |
| AAPL US Equity | 0.07 | 0.07 | 0.14 | 5.79 |
Options Activity vs. Price Volatility#
# Calculate price volatility and compare with options activity
prices = ts.xs('PX_LAST', axis=1, level=1)
returns = prices.pct_change()
volatility = returns.rolling(WINDOW).std() * np.sqrt(252) * 100 # Annualized vol
# Create scatter plot of options activity vs volatility
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
for stock in open_opt_pct.columns:
if stock in volatility.columns:
# Get data where both exist
opt_data = open_opt_pct[stock].dropna()
vol_data = volatility[stock].dropna()
# Align the data
common_dates = opt_data.index.intersection(vol_data.index)
if len(common_dates) > 10: # Only plot if we have enough data
ax.scatter(opt_data[common_dates], vol_data[common_dates],
alpha=0.6, label=stock, s=20)
ax.set_xlabel('Total Options Open Interest (% of Shares Outstanding)')
ax.set_ylabel('Annualized Volatility (%)')
ax.set_title('Options Activity vs. Price Volatility')
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.show()
Key Takeaways#
- Massive options activity: Open interest often exceeds 5-10% of shares outstanding
- Bullish bias: Call volume dominates put volume by huge margins
- Retail-driven: Options volume can exceed 20%+ of stock volume during peaks
- High volatility: Options activity correlates with extreme price swings
The 2021 meme stock phenomenon showed how options markets can amplify retail sentiment and create feedback loops between options activity and stock prices.
fig, axes = plot_field_ratio(ts, 'VOLUME_TOTAL_CALL', 'VOLUME', window=WINDOW,scale=100)
plt.show()
fig, axes = plot_field_ratio(ts, 'VOLUME_TOTAL_PUT', 'VOLUME', window=WINDOW,scale=100)
plt.show()