Markets: A Rigorous Course

Exercise · m09a_chain.py

An option chain from the NSE F&O bhavcopy; PCR, max pain, OI build-up

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

Tests (6)

  • test_toy_writer_payout_and_max_pain
  • test_payout_is_convex_piecewise_linear
  • test_build_chain_filters_and_columns
  • test_real_chain_statistics
  • test_classify
  • test_buildup_labels_real

Theory: M09A notes. Run with Ctrl/⌘+Enter.

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.

Create a free account

Reading is open to everyone. An account keeps your progress, answers, notes and code drafts, and gives you the study plan, the journals and the simulator. Running code needs an account approved for it.