Indian Options Archive

Guides /

Rebuilding a historical NIFTY option chain, with implied volatility, from 1-minute data

Turn 1-minute option bars into the option chain as it stood at any past minute: strikes, prices, volume, open interest, put-call ratio, and implied volatility computed with Black-Scholes.

A live option chain is a snapshot: every strike of one expiry, calls on one side and puts on the other. With 1-minute history you can rebuild that snapshot for any minute in the past, and add the fields a trading screen computes for you, such as implied volatility.

The snapshot

import pandas as pd

opt = pd.read_parquet("NIFTY_OPT_1min_2025-10-14_exp2025-10-20.parquet")
opt["cp"] = opt["right"].str[0].str.upper()
t = pd.Timestamp("2025-10-14 11:00")

# last known bar for each contract at or before t
snap = (opt[opt["datetime"] <= t].sort_values("datetime")
        .groupby(["strike_price", "cp"]).last())
chain = snap[["close", "volume", "open_interest"]].unstack("cp")
chain.columns = [f"{c}_{side}" for c, side in chain.columns]
print(chain.loc[24800:25600])

Using the last bar at or before the snapshot time matters. Illiquid strikes may not trade every minute, and taking only rows stamped exactly 11:00 would leave holes.

Put-call ratio

pcr_oi = snap.xs("P", level="cp")["open_interest"].sum() / snap.xs("C", level="cp")["open_interest"].sum()
print(round(pcr_oi, 2))

The files report open interest as it stood at that minute. Note that the volume in this snapshot is the volume of that one bar, not the cumulative day volume a live screen shows; sum the bars up to t if you want the running total.

Implied volatility

The files hold traded prices, so implied volatility is something you compute. With the index level, strike, time to expiry and an interest rate, invert Black-Scholes for each option:

import numpy as np
from scipy.optimize import brentq
from scipy.stats import norm

def bs_price(S, K, T, r, sigma, cp):
    d1 = (np.log(S / K) + (r + sigma ** 2 / 2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    if cp == "C":
        return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
    return K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)

def implied_vol(price, S, K, T, r, cp):
    intrinsic = max(0.0, S - K) if cp == "C" else max(0.0, K - S)
    if price <= intrinsic + 0.05 or T <= 0:
        return np.nan
    return brentq(lambda s: bs_price(S, K, T, r, s, cp) - price, 1e-4, 5.0)

spot = pd.read_parquet("NIFTY_SPOT_1min_2025-10-14.parquet").set_index("timestamp")["close"]
S = spot.asof(t)
expiry = pd.Timestamp("2025-10-20 15:30")
T = (expiry - t).total_seconds() / (365 * 24 * 3600)
r = 0.06

for k in range(24900, 25401, 100):
    for cp in "CP":
        px = snap.loc[(float(k), cp), "close"]
        print(k, cp, px, round(implied_vol(px, S, k, T, r, cp) * 100, 1))

Refinements

Data for this guide

Or start with the free samples.

More guides