Build an option chain

An option chain is just a filtered, pivoted view of the NFO instrument master for one underlying and one expiry, then joined with live quotes.

def get_option_chain(kite, nfo_df: pd.DataFrame, underlying: str, expiry: str) -> pd.DataFrame:
    chain = nfo_df[
        (nfo_df.name == underlying) &
        (nfo_df.expiry == expiry) &
        (nfo_df.instrument_type.isin(["CE", "PE"]))
    ].copy()

    symbols = [f"NFO:{s}" for s in chain.tradingsymbol]
    quotes = kite.quote(symbols)

    chain["ltp"] = chain.tradingsymbol.apply(lambda s: quotes[f"NFO:{s}"]["last_price"])
    chain["oi"] = chain.tradingsymbol.apply(lambda s: quotes[f"NFO:{s}"]["oi"])
    chain["volume"] = chain.tradingsymbol.apply(lambda s: quotes[f"NFO:{s}"]["volume"])

    calls = chain[chain.instrument_type == "CE"].set_index("strike")[["ltp", "oi", "volume"]]
    puts = chain[chain.instrument_type == "PE"].set_index("strike")[["ltp", "oi", "volume"]]

    return calls.join(puts, lsuffix="_call", rsuffix="_put").sort_index()
chain = get_option_chain(kite, nfo_df, "NIFTY", "2024-12-26")
print(chain.loc[24000:25000])

Output shape (strike-indexed, calls and puts side by side):

        ltp_call  oi_call  volume_call  ltp_put  oi_put  volume_put
strike
24000      612.4   145000       89000     42.1   210000      156000
24500      312.8   198000      142000    142.5   187000      132000
25000      112.3   167000       98000    342.6   134000       87000

Finding ATM strike

def find_atm_strike(chain: pd.DataFrame, spot: float) -> float:
    return min(chain.index, key=lambda k: abs(k - spot))

Rate limit awareness

kite.quote() accepts up to 500 instruments per call — a full NIFTY option chain (one expiry) is usually well under that, so one call suffices. Requesting *multiple* expiries' full chains in a tight loop is where you'll hit rate limits — batch and cache aggressively (chapter 20).

Next: 034 — Track index spot values