"The winds of change are blowing, and with them come opportunities for those willing to listen."
In a world where markets shift faster than headlines scroll, the ability to analyze and visualize data in meaningful ways is more valuable than ever. Recently, I found myself staring at CSV files filled with options data — messy, inconsistent, and hard to interpret. But as the wind shifted, so did the opportunity: what if we could turn that noise into actionable insight?
This post is about that transformation — cleaning options data, computing key metrics like Max Pain and Expected Move, and visualizing them using Google Charts.
Let’s dive in! 💡
📌 The Problem
Options data often comes in unwieldy formats — mixed delimiters, percentage strings, and missing headers. Before any serious analysis can begin, we must tame the data.
def clean_options_data(filepath):
...
This function handles:
- Automatic delimiter detection
- Cleaning column headers
- Replacing missing values
- Converting implied volatility and strike prices to numeric form
🔧 Pro tip: Always validate the presence of key columns like Implied Volatility
before continuing.
🔍 Metrics That Matter: ATM Strike, Expected Move & Max Pain
Once the data is cleaned, we extract three vital metrics:
- ATM Strike (At-The-Money) – The strike with the highest open interest
- Expected Move – A volatility-based projection of price movement
- Max Pain – The strike price where option holders lose the most money (often where prices gravitate at expiry)
atm_strike = ...
expected_move = ...
max_pain_strike = ...
These numbers give us a sneak peek into market expectations and where the battle lines are drawn between bulls and bears.
📊 From Code to Chart: Google Charts for the Win
Instead of staring at raw numbers, let’s make them visual.
We use Google Charts to create interactive, browser-based visuals:
<script type="text/javascript">
google.charts.load('current', {packages: ['corechart']});
...
</script>
This script renders:
- A Column Chart of Open Interest by Strike
- A Line Chart of Implied Volatility by Strike
And yes, all of this is done via a single .html
file that can be opened in any browser — no dashboards, no logins.
🚀 Winds of Change = Winds of Opportunity
This project started with a CSV and ended with a shareable visualization — a reminder that the smallest initiatives can spark the biggest opportunities.
In an era where markets evolve and tools change rapidly, being able to adapt and harness those winds is what gives you the edge. Whether you're a trader, a data scientist, or just a curious mind, these types of DIY tools put the power back in your hands.
🧪 Try It Yourself
To try this out:
- Get two CSVs (
calls.csv
andputs.csv
) with columns like:Strike
,Open Interest
, andImplied Volatility
- Run the Python script (see below)
- Open
options_visualization.html
in your browser
python options_analysis.py
🧠 Final Thoughts
This script isn’t a trading recommendation engine, but it is a powerful foundation. The tools you build for yourself don’t have to be flashy — they just need to work for you. And often, it's the DIY solutions that unlock the clearest views.
💬 Got ideas for how to expand this? Overlay real-time data? Add Vega/Delta charts? Let’s connect in the comments.
🔗 Source Code
Find the full source code:
import pandas as pd
import numpy as np
import json
def clean_options_data(filepath):
df = pd.read_csv(filepath, sep=None, engine='python')
df.columns = df.columns.str.strip()
df.replace('-', np.nan, inplace=True)
if 'Implied Volatility' not in df.columns:
print("❌ 'Implied Volatility' column not found. Check headers.")
return None
df['Implied Volatility'] = df['Implied Volatility'].str.replace('%', '', regex=False)
df['Implied Volatility'] = pd.to_numeric(df['Implied Volatility'], errors='coerce') / 100
df['Strike'] = pd.to_numeric(df['Strike'], errors='coerce')
df['Open Interest'] = pd.to_numeric(df['Open Interest'], errors='coerce')
df.dropna(subset=['Strike', 'Open Interest', 'Implied Volatility'], inplace=True)
return df
def max_pain(calls_df, puts_df):
strikes = sorted(set(calls_df['Strike']).union(set(puts_df['Strike'])))
total_pain = []
for strike in strikes:
call_pain = ((calls_df['Strike'] - strike).clip(lower=0) * calls_df['Open Interest']).sum()
put_pain = ((strike - puts_df['Strike']).clip(lower=0) * puts_df['Open Interest']).sum()
total_pain.append((strike, call_pain + put_pain))
pain_df = pd.DataFrame(total_pain, columns=['Strike', 'Total Pain'])
return pain_df.loc[pain_df['Total Pain'].idxmin(), 'Strike']
def to_py(val):
"""Convert numpy types to native Python types."""
if pd.isna(val):
return None
elif isinstance(val, (np.integer, np.int64)):
return int(val)
elif isinstance(val, (np.floating, np.float64)):
return float(val)
return val
# Load CSVs
calls = clean_options_data("calls.csv")
puts = clean_options_data("puts.csv")
if calls is None or puts is None:
print("Fix your CSV headers and retry.")
exit()
# Metrics
atm_strike = float(calls.loc[calls['Open Interest'].idxmax(), 'Strike'])
atm_iv = float(calls.loc[calls['Strike'] == atm_strike, 'Implied Volatility'].mean())
days_to_expiry = 75
expected_move = atm_strike * atm_iv * np.sqrt(days_to_expiry / 365)
max_pain_strike = float(max_pain(calls, puts))
# Chart Data
chart_data = {
"atm_strike": atm_strike,
"expected_move": expected_move,
"max_pain_strike": max_pain_strike,
"open_interest": [
["Strike", "Calls OI", "Puts OI"]
] + [
[to_py(row['Strike']), to_py(row['Open Interest']), 0] for _, row in calls.iterrows()
] + [
[to_py(row['Strike']), 0, to_py(row['Open Interest'])] for _, row in puts.iterrows()
],
"implied_volatility": [
["Strike", "Calls IV", "Puts IV"]
] + [
[to_py(row['Strike']), to_py(row['Implied Volatility']), 0] for _, row in calls.iterrows()
] + [
[to_py(row['Strike']), 0, to_py(row['Implied Volatility'])] for _, row in puts.iterrows()
]
}
# Create HTML with embedded data
html_template = f"""
<!DOCTYPE html>
<html>
<head>
<title>Options Analysis with Google Charts</title>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
const chartData = {json.dumps(chart_data)};
google.charts.load('current', {{packages: ['corechart']}});
google.charts.setOnLoadCallback(drawCharts);
function drawCharts() {{
drawOpenInterestChart();
drawIVChart();
}}
function drawOpenInterestChart() {{
var data = google.visualization.arrayToDataTable(chartData.open_interest);
var options = {{
title: 'Open Interest by Strike',
hAxis: {{ title: 'Strike' }},
vAxis: {{ title: 'Open Interest' }},
legend: {{ position: 'top' }},
}};
var chart = new google.visualization.ColumnChart(document.getElementById('open_interest_chart'));
chart.draw(data, options);
}}
function drawIVChart() {{
var data = google.visualization.arrayToDataTable(chartData.implied_volatility);
var options = {{
title: 'Implied Volatility by Strike',
hAxis: {{ title: 'Strike' }},
vAxis: {{ title: 'Implied Volatility' }},
legend: {{ position: 'top' }},
}};
var chart = new google.visualization.LineChart(document.getElementById('iv_chart'));
chart.draw(data, options);
}}
</script>
</head>
<body>
<h2>📊 Options Analysis Visualization</h2>
<p>ATM Strike: <b>{atm_strike:.2f}</b></p>
<p>Expected Move (±): <b>{expected_move:.2f}</b></p>
<p>Max Pain Strike: <b>{max_pain_strike:.2f}</b></p>
<div id="open_interest_chart" style="width: 100%; height: 500px;"></div>
<div id="iv_chart" style="width: 100%; height: 500px;"></div>
</body>
</html>
"""
# Save HTML
with open("options_visualization.html", "w") as f:
f.write(html_template)
print("✅ Visualization created: options_visualization.html")
The markets may be uncertain, but one thing is clear: the winds of change always bring new opportunities — if you're ready to catch them. 🌬️📈