Options trading offers a unique window into market psychology. By analyzing where investors are placing their bets β and how much they're wagering β we can extract powerful insights into the collective mood of the market.
In this article, Iβll walk you through a Python script that scrapes options chain data from Yahoo Finance using the yfinance
library, saves the data into structured CSVs, and performs market sentiment analysis and Max Pain price estimation β all with a single function call.
π οΈ Tools Weβll Use
Weβll rely on a few core Python libraries:
-
yfinance
: To fetch real-time financial data from Yahoo Finance -
csv
: To export options data for further analysis -
math
,datetime
,collections
: For calculations and data structuring
π¦ The Core Function: fetch_and_save_options()
At the heart of this project is the fetch_and_save_options(symbol, expiration_date)
function. Hereβs what it does:
- Fetches Options Data for a given stock symbol and expiration date
- Writes Calls and Puts to separate CSV files
- Analyzes Market Sentiment using Put/Call Ratios
- Estimates Max Pain Price, a concept used to gauge where the underlying asset might gravitate towards by expiration
Letβs break down the key components.
1οΈβ£ Fetching and Validating Options Data
stock = yf.Ticker(symbol)
if expiration_date not in stock.options:
print(f"Invalid expiration date.")
return
options = stock.option_chain(expiration_date)
We begin by pulling the full option chain for a symbol and validating the expiration date to avoid API errors.
2οΈβ£ Structuring the Data
Both calls and puts are parsed into structured dictionaries. This allows us to write them easily to CSVs:
calls_data = []
puts_data = []
We also handle missing or NaN values gracefully to avoid data corruption.
3οΈβ£ Writing to CSV
Data is written to two CSV files: one for calls and another for puts.
with open(f"{symbol}_calls.csv", "w") as file:
writer = csv.DictWriter(file, fieldnames=calls_data[0].keys())
writer.writeheader()
writer.writerows(calls_data)
This makes the dataset portable and easy to load into Excel, pandas, or visualization tools.
4οΈβ£ Market Sentiment Analysis via Put/Call Ratios
The script calculates:
- Total Call & Put Open Interest
- Total Call & Put Volume
- Put/Call Ratio (by OI and Volume)
These metrics help infer sentiment:
if pcr_oi < 0.7 and pcr_volume < 0.7:
sentiment = "π Bullish sentiment expected."
elif pcr_oi > 1.3 and pcr_volume > 1.3:
sentiment = "π Bearish sentiment expected."
else:
sentiment = "π€ Neutral or uncertain."
This gives you an immediate snapshot of whether investors are leaning bullish, bearish, or undecided.
5οΈβ£ Estimating the Max Pain Price
Max Pain Theory suggests that the stock price tends to move toward the strike price that causes the maximum financial loss for options holders β and hence, the least payout by options writers.
We calculate the loss at each strike:
for strike in sorted(strike_prices):
total_loss = 0
for s in strike_prices:
...
if total_loss < min_loss:
min_loss = total_loss
max_pain_strike = strike
And print the strike with the minimum combined payout:
print(f"π° Estimated 'Max Pain' price: ${max_pain_strike:.2f}")
π§ͺ Example Usage
fetch_and_save_options("LLY", "2025-08-15")
Running this line will:
- Save two CSV files:
LLY_calls.csv
andLLY_puts.csv
- Print total open interest and volume
- Show Put/Call Ratios and sentiment
- Estimate the Max Pain price
π‘ Why This Matters
For traders and investors, understanding where money is flowing in the options market can offer an edge β whether it's to:
- Confirm your investment thesis
- Avoid getting caught on the wrong side of sentiment
- Gauge where market participants expect the stock to go
And with Python doing the heavy lifting, you can focus on strategy, not spreadsheets.
Code
import yfinance as yf
import csv
import math
from datetime import datetime
from collections import defaultdict
def fetch_and_save_options(symbol, expiration_date):
stock = yf.Ticker(symbol)
if expiration_date not in stock.options:
print(f"Invalid expiration date. Available options dates: {stock.options}")
return
options = stock.option_chain(expiration_date)
calls_data = []
puts_data = []
total_call_oi = 0
total_put_oi = 0
total_call_volume = 0
total_put_volume = 0
# For Max Pain calculation
strike_prices = set()
call_oi_by_strike = defaultdict(int)
put_oi_by_strike = defaultdict(int)
# --- Process Calls ---
for call in options.calls.itertuples():
volume = 0 if call.volume is None or (isinstance(call.volume, float) and math.isnan(call.volume)) else call.volume
open_interest = 0 if call.openInterest is None or (isinstance(call.openInterest, float) and math.isnan(call.openInterest)) else call.openInterest
iv = 0 if call.impliedVolatility is None or (isinstance(call.impliedVolatility, float) and math.isnan(call.impliedVolatility)) else call.impliedVolatility
calls_data.append({
"Contract Name": call.contractSymbol,
"Last Trade Date (EDT)": call.lastTradeDate.strftime("%m/%d/%Y %I:%M %p") if call.lastTradeDate else "",
"Strike": call.strike,
"Last Price": call.lastPrice,
"Bid": call.bid,
"Ask": call.ask,
"Change": call.change,
"% Change": call.percentChange,
"Volume": volume,
"Open Interest": open_interest,
"Implied Volatility": f"{iv * 100:.2f}%"
})
total_call_volume += volume
total_call_oi += open_interest
call_oi_by_strike[call.strike] += open_interest
strike_prices.add(call.strike)
# --- Process Puts ---
for put in options.puts.itertuples():
volume = 0 if put.volume is None or (isinstance(put.volume, float) and math.isnan(put.volume)) else put.volume
open_interest = 0 if put.openInterest is None or (isinstance(put.openInterest, float) and math.isnan(put.openInterest)) else put.openInterest
iv = 0 if put.impliedVolatility is None or (isinstance(put.impliedVolatility, float) and math.isnan(put.impliedVolatility)) else put.impliedVolatility
puts_data.append({
"Contract Name": put.contractSymbol,
"Last Trade Date (EDT)": put.lastTradeDate.strftime("%m/%d/%Y %I:%M %p") if put.lastTradeDate else "",
"Strike": put.strike,
"Last Price": put.lastPrice,
"Bid": put.bid,
"Ask": put.ask,
"Change": put.change,
"% Change": put.percentChange,
"Volume": volume,
"Open Interest": open_interest,
"Implied Volatility": f"{iv * 100:.2f}%"
})
total_put_volume += volume
total_put_oi += open_interest
put_oi_by_strike[put.strike] += open_interest
strike_prices.add(put.strike)
# --- Write CSVs ---
with open(f"{symbol}_calls.csv", "w", newline="") as file:
writer = csv.DictWriter(file, fieldnames=calls_data[0].keys())
writer.writeheader()
writer.writerows(calls_data)
with open(f"{symbol}_puts.csv", "w", newline="") as file:
writer = csv.DictWriter(file, fieldnames=puts_data[0].keys())
writer.writeheader()
writer.writerows(puts_data)
print(f"β
Options data for {symbol} on {expiration_date} saved to CSV files.")
# --- Market Sentiment Analysis ---
print("\n--- π Market Sentiment Analysis ---")
print(f"Total Call Open Interest: {total_call_oi}")
print(f"Total Put Open Interest: {total_put_oi}")
print(f"Total Call Volume: {total_call_volume}")
print(f"Total Put Volume: {total_put_volume}")
pcr_oi = total_put_oi / total_call_oi if total_call_oi != 0 else float('inf')
pcr_volume = total_put_volume / total_call_volume if total_call_volume != 0 else float('inf')
print(f"Put/Call Ratio (OI): {pcr_oi:.2f}")
print(f"Put/Call Ratio (Volume): {pcr_volume:.2f}")
if pcr_oi < 0.7 and pcr_volume < 0.7:
sentiment = "π Bullish sentiment expected."
elif pcr_oi > 1.3 and pcr_volume > 1.3:
sentiment = "π Bearish sentiment expected."
else:
sentiment = "π€ Market sentiment appears neutral or uncertain."
print(f"Conclusion: {sentiment}")
# --- Max Pain Price Calculation ---
print("\n--- π‘ Expected Price Estimate (Max Pain Theory) ---")
min_loss = float("inf")
max_pain_strike = None
for strike in sorted(strike_prices):
total_loss = 0
for s in strike_prices:
call_oi = call_oi_by_strike[s]
put_oi = put_oi_by_strike[s]
if s > strike: # ITM calls
total_loss += (s - strike) * call_oi
elif s < strike: # ITM puts
total_loss += (strike - s) * put_oi
if total_loss < min_loss:
min_loss = total_loss
max_pain_strike = strike
if max_pain_strike is not None:
print(f"π° Estimated 'Max Pain' price: ${max_pain_strike:.2f}")
print("This is the strike price where the fewest options would be in-the-money.")
else:
print("β οΈ Could not calculate expected price due to insufficient data.")
# π§ͺ Example usage
fetch_and_save_options("LLY", "2025-08-15")
π Next Steps
This script is just a foundation. Here are a few ideas to take it further:
- Visualize open interest and volume per strike using
matplotlib
orplotly
- Automate daily downloads for multiple tickers
- Integrate technical indicators for a broader analysis
π Final Thoughts
Options data hides valuable clues about market psychology. With just a few lines of Python, we can tap into that information and make smarter trading decisions. If youβre serious about finance and data, itβs time to let your code do some of the thinking.
Top comments (0)