Exercise · m09a_chain.py
An option chain from the NSE F&O bhavcopy; PCR, max pain, OI build-up
not run yet
Not saved: sign up to keep your code"""M09A exercise 1: an option chain from the NSE F&O bhavcopy; PCR, max pain, OI build-up
(notes §9A.2-9A.5).
Input is a DataFrame with the UDiFF bhavcopy columns used below (one row per contract):
TckrSymb, FinInstrmTp ('IDO' index option, 'STO' stock option, 'IDF'/'STF' futures),
XpryDt ('YYYY-MM-DD'), StrkPric, OptnTp ('CE'/'PE'), ClsPric, PrvsClsgPric,
UndrlygPric, OpnIntrst, ChngInOpnIntrst, TtlTradgVol.
OpnIntrst and ChngInOpnIntrst are in units of the underlying (contracts x lot size);
TtlTradgVol is in contracts (notes §9A.2). PCR on volume is a ratio, so units cancel.
The test fixture is a real extract: Nifty options expiring 27 Oct 2026, from the
26 Oct 2026 bhavcopy, plus some rows of other expiries, futures and Bank Nifty that your
filter must ignore.
Run (tests import this file; set COURSE_IMPL=solutions to run the reference):
cd exercises && .venv/bin/python -m pytest modules/M09A-indian-options-desk/tests/test_m09a_chain.py
"""
from __future__ import annotations
import numpy as np
import pandas as pd
OPTION_TYPES = ("IDO", "STO")
LABELS = ("long_buildup", "short_buildup", "short_covering", "long_unwinding", "neutral")
def build_chain(bhav: pd.DataFrame, symbol: str, expiry: str) -> pd.DataFrame:
"""Chain for one underlying and one expiry, indexed by strike (ascending float,
index name 'strike').
Columns, in this order, all float: ce_oi, pe_oi, ce_doi, pe_doi, ce_vol, pe_vol,
ce_close, pe_close, ce_prev, pe_prev (from OpnIntrst, ChngInOpnIntrst, TtlTradgVol,
ClsPric, PrvsClsgPric). A strike listed for only one side gets 0 for that side's
OI/volume/change and NaN for its prices. Keep only rows with TckrSymb == symbol,
FinInstrmTp in OPTION_TYPES and XpryDt == expiry. Raise ValueError if nothing matches.
"""
raise NotImplementedError
def underlying_price(bhav: pd.DataFrame, symbol: str, expiry: str) -> float:
"""UndrlygPric reported on the option rows of (symbol, expiry); ValueError if none."""
raise NotImplementedError
def pcr(chain: pd.DataFrame, basis: str = "oi") -> float:
"""Put-call ratio, notes (9A.3): sum(pe_oi)/sum(ce_oi) for basis='oi',
sum(pe_vol)/sum(ce_vol) for basis='volume'. Other basis: ValueError."""
raise NotImplementedError
def writer_payout(chain: pd.DataFrame, x: float) -> float:
"""Total payout to option holders if the expiry settlement is x, notes (9A.4):
W(x) = sum_K [ce_oi(K) (x - K)^+ + pe_oi(K) (K - x)^+].
OI is in units, so W is in rupees."""
raise NotImplementedError
def max_pain(chain: pd.DataFrame) -> float:
"""Max pain = argmin of writer_payout over the listed strikes (Proposition 9A.3 shows
this is also the minimiser over all real x). Ties go to the lowest strike."""
raise NotImplementedError
def classify_buildup(dprice: float, doi: float) -> str:
"""Broker labels from the signs of the price change and OI change (notes §9A.3, M07 §7.5):
(+,+) 'long_buildup', (-,+) 'short_buildup', (+,-) 'short_covering',
(-,-) 'long_unwinding'; 'neutral' if either change is zero or NaN."""
raise NotImplementedError
def buildup_labels(chain: pd.DataFrame) -> pd.DataFrame:
"""DataFrame indexed like chain with columns ce_label, pe_label: classify_buildup applied
to (close - prev, doi) for calls and for puts."""
raise NotImplementedError
No runs yet
The stub's functions raise
NotImplementedError until you write them. Fill in one, press
Run tests, and watch its tests turn green.Reference solution
Available after your first run, with an account approved for code runs.