Score option chains using open interest, OI/volume, and strike distance to highlight active contracts in Python.
Most options data is noise. However, there are a few contracts that can tell a story worth knowing.
If we zero in on where open interest and volume surge together, we can spot which strikes attract serious capital.
We produce a composite score aiming to identify such capital by ranking contracts by size, stickiness, and distance from the current price.
The approach gives us a live heatmap of where big money is actually betting and which contracts pull in the largest flows.
The complete Python notebook for the analysis is provided below.
1. Finding Where the Big Money Plays
Large, unusual trades set the tone for price action. Institutions show intent not in single trades, but in where size persists and grows over time.
Traditional scanners show volume spikes or unusual activity, but miss the subtle footprints left by accumulation, sticky open interest, or far-out bets.
To cut through the noise, we derive a composite score. This method ranks every contract using three inputs:
- Open Interest: measures size. Contracts with more open positions signal where the crowd commits.
- OI/Volume Ratio: measures stickiness. High ratios flag contracts where traders open positions and hold, not just churn for liquidity.
- Strike Distance (OTM): measures risk appetite. Big money concentrates near-the-money but sometimes places significant OTM bets.
The Scoring Formula
We normalize open interest and OI/volume within each expiry using a z-score:
Here:
- OIi is the open interest for contract i
- (OI/Volume)i is the open interest-to-volume ratio for contract i
- μ and σ are the mean and standard deviation for each expiry date.
We then rank these z-scores on a percentile basis within each expiry.
This keeps contracts with abnormal size or stickiness at the top, regardless of overall market mood.
Strike distance is scaled as:
K_OTM is a scaling factor.
Shorter-dated options are penalized using:
DTEi is days to expiry.
The final composite score for each contract:

Weights are tuned for practical signal:
- wOI=0.4
- wOV=0.4
- wOTM=0.2
Practical Example
Suppose there is a call option with:
- High open interest relative to peers
- OI/volume ratio in the top decile
- Strike close to spot
- 30 days to expiry
This contract scores near the top. It reflects big money building a position.
By contrast, a weekly put with low OI and high churn scores low, even if it trades heavy volume.
2. Identifying Big Money Options in Python
2.1 Get the Data and Compute Score
We use yfinance to pull the full option chain, apply liquidity filters, and rank contracts by the composite score.
To make the code adjustable, we state the parameters at the beginning of the workflow.
Step 1: Set Parameters and Pull Data
Each parameter is explained in the code comments below, from liquidity filters to scoring weights.
import pandas as pd
import numpy as np
import yfinance as yf
from datetime import date
from math import erf, log, sqrt
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
# PARAMETERS
TICKER = "NVDA" # Ticker symbol; set to any liquid underlying
TOP_N = 50 # Number of top contracts to return; higher values include more noise
K_OTM = 2.0 # Out-of-the-money scaling; higher = more penalty for far strikes
MAX_DTE = 360 # Maximum days to expiry; lower to focus on short-term flows
MIN_VOLUME = 10 # Minimum daily volume; raises threshold for “active” contracts
MIN_OI = 100 # Minimum open interest; filters out illiquid/ignored contracts
CAP_OI_VOL = 100 # Cap for OI/Volume ratio; prevents outliers from distorting scores
W_OI_Z = 0.4 # Weight on open interest z-score; higher = more focus on total size
W_OV_Z = 0.4 # Weight on OI/volume z-score; higher = more focus on stickiness
W_OTM = 0.2 # Weight on OTM (strike distance); higher = more penalty for contracts far from spot
We use NVDA as an example and loop through every listed expiry, for both calls and puts.
We keep only fields needed for scoring: strike, open interest, volume, last price, and IV.
# fetch full chain with IV
tkr = yf.Ticker(TICKER)
spot = tkr.history(period="1d")["Close"].iloc[-1]
today = pd.to_datetime(date.today())
rows = []
for exp in tkr.options:
oc = tkr.option_chain(exp)
for side, df in (("call", oc.calls), ("put", oc.puts)):
part = df[[
"contractSymbol",
"strike",
"openInterest",
"volume",
"lastPrice",
"impliedVolatility"
]].copy()
part["side"] = side
part["exp"] = pd.to_datetime(exp)
part["snap"] = today
part["spot"] = spot
rows.append(part)
raw = pd.concat(rows, ignore_index=True)
Step 2: Apply Filters and Compute Score Inputs
We then remove contracts with short expiry, low OI, or low volume.
Each contract gets features for days-to-expiry, OTM weighting, and the OI/Volume ratio.
We then normalize OI and OI/Volume for each expiry using z-scores, then percentile rank within expiry.
The composite score is finally computed.
Newsletter