🌬️ Winds of Change in Options Analysis: Visualizing Market Signals with Python & Google Charts
Dmitry Romanoff

Dmitry Romanoff @dm8ry

Joined:
Sep 18, 2022

🌬️ Winds of Change in Options Analysis: Visualizing Market Signals with Python & Google Charts

Publish Date: Jun 1
0 0

"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):
    ...
Enter fullscreen mode Exit fullscreen mode

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:

  1. ATM Strike (At-The-Money) – The strike with the highest open interest
  2. Expected Move – A volatility-based projection of price movement
  3. 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 = ...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

  1. Get two CSVs (calls.csv and puts.csv) with columns like: Strike, Open Interest, and Implied Volatility
  2. Run the Python script (see below)
  3. Open options_visualization.html in your browser
python options_analysis.py
Enter fullscreen mode Exit fullscreen mode

🧠 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")


Enter fullscreen mode Exit fullscreen mode

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. 🌬️📈

Winds of Change in Options Analysis: Visualizing Market Signals with Python & Google Charts

Comments 0 total

    Add comment