Markets: A Rigorous Course

Exercise · m09a_margin_greeks.py

Portfolio Greeks of multi-leg positions, a SPAN-like scenario margin, and adjustments viewed as changes of Greeks

A position is a list of Leg objects on one Nifty-like forward F. Options are European, priced with Black-76 at one flat volatility. A 'fut' leg is a future bought/sold at price K (its value today is qty * (F - K); carry and daily settlement are ignored).

Greek conventions (as in M08/M09): delta per unit of F, gamma per unit F^2, vega per 1.00 of sigma, theta per YEAR of calendar time (dV/dt; a long option has negative theta). qty is signed and in units of the underlying (one Nifty lot = 65 units as of 5 Nov 2026).

The scenario parameters (scan range 6%, vol shift 4 points, extreme move 2x scan at 35%) are illustrative defaults, not NSE Clearing's published values; read those from the daily SPAN risk-parameter files.

Provided (toolkit.options; you wrote Black-76 in M08): Leg(kind, K, T, qty) frozen dataclass; kind 'call', 'put' or 'fut' (K = strike, or the futures trade price) b76(F, K, tau, r, sigma, kind) Black-76 price; intrinsic value at tau <= 0 b76_greeks(F, K, tau, r, sigma, kind) dict(delta, gamma, vega, theta, rho), per unit leg_value(leg, F, t, r, sigma) below

Run: cd exercises && .venv/bin/python -m pytest modules/M09A-indian-options-desk/tests/test_m09a_margin_greeks.py

Tests (8)

  • test_greeks_match_finite_differences
  • test_greeks_are_additive
  • test_scenarios_shape_and_order
  • test_condor_needs_less_margin_and_is_capped
  • test_margin_is_subadditive
  • test_fixed_margin_values
  • test_delta_hedge_and_roll
  • test_adjustment_trades_and_cost

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

not run yet

Not saved: sign up to keep your code
"""M09A exercise 3: portfolio Greeks of multi-leg positions, a SPAN-like scenario margin,
and adjustments viewed as changes of Greeks (notes §9A.12-9A.13).

A position is a list of Leg objects on one Nifty-like forward F. Options are European,
priced with Black-76 at one flat volatility. A 'fut' leg is a future bought/sold at price K
(its value today is qty * (F - K); carry and daily settlement are ignored).

Greek conventions (as in M08/M09): delta per unit of F, gamma per unit F^2, vega per 1.00
of sigma, theta per YEAR of calendar time (dV/dt; a long option has negative theta).
qty is signed and in units of the underlying (one Nifty lot = 65 units as of 5 Nov 2026).

The scenario parameters (scan range 6%, vol shift 4 points, extreme move 2x scan at 35%)
are illustrative defaults, not NSE Clearing's published values; read those from the
daily SPAN risk-parameter files.

Provided (toolkit.options; you wrote Black-76 in M08):
    Leg(kind, K, T, qty)                       frozen dataclass; kind 'call', 'put' or 'fut'
                                               (K = strike, or the futures trade price)
    b76(F, K, tau, r, sigma, kind)             Black-76 price; intrinsic value at tau <= 0
    b76_greeks(F, K, tau, r, sigma, kind)      dict(delta, gamma, vega, theta, rho), per unit
    leg_value(leg, F, t, r, sigma)             below

Run:  cd exercises && .venv/bin/python -m pytest modules/M09A-indian-options-desk/tests/test_m09a_margin_greeks.py
"""

from __future__ import annotations

import math
from dataclasses import replace

import numpy as np

from toolkit.options import Leg  # noqa: F401  (provided)
from toolkit.options import black76_greeks as b76_greeks  # noqa: F401  (provided, M08)
from toolkit.options import black76_price as b76  # noqa: F401  (provided, M08)


# ------------------------------------------------------------------ provided

def leg_value(leg: Leg, F: float, t: float, r: float, sigma: float) -> float:
    if not leg.is_option:  # 'fut': a future traded at K is worth qty * (F - K)
        return leg.qty * (F - leg.K)
    return leg.qty * b76(F, leg.K, leg.T - t, r, sigma, leg.kind)


# ------------------------------------------------------------------ exercises

def position_value(legs, F, t, r, sigma) -> float:
    """Sum of leg_value over legs at forward F, calendar time t (years from today)."""
    raise NotImplementedError


def position_greeks(legs, F, t, r, sigma) -> dict:
    """dict(delta, gamma, vega, theta) = qty-weighted sum of b76_greeks over option legs
    (time to expiry T - t); a 'fut' leg adds qty to delta and nothing else. (9A.16)"""
    raise NotImplementedError


def scenario_losses(legs, F, r, sigma, scan, vol_shift, extreme_mult=2.0, extreme_weight=0.35) -> np.ndarray:
    """Array of 16 losses (positive = loss) relative to the value today, revalued instantly
    (t = 0), notes (9A.17):
      * scenarios 0-6: forward F (1 + m scan) for m in (0, +1/3, -1/3, +2/3, -2/3, +1, -1)
        at volatility sigma + vol_shift;
      * scenarios 7-13: the same seven moves at max(sigma - vol_shift, 0.01);
      * scenarios 14-15: F (1 + extreme_mult scan) and F (1 - extreme_mult scan) at sigma,
        each loss multiplied by extreme_weight."""
    raise NotImplementedError


def scenario_margin(legs, F, r, sigma, scan=0.06, vol_shift=0.04, extreme_mult=2.0, extreme_weight=0.35) -> float:
    """max(0, largest scenario loss)."""
    raise NotImplementedError


def exposure_margin(legs, F, elm_rate=0.02, extra_expiry_elm=0.0) -> float:
    """(elm_rate + extra_expiry_elm) * F * (sum of |qty| over 'fut' legs and SHORT option
    legs). Long options attract no exposure margin in this model."""
    raise NotImplementedError


def total_margin(legs, F, r, sigma, scan=0.06, vol_shift=0.04, elm_rate=0.02, extra_expiry_elm=0.0) -> float:
    """scenario_margin(...) + exposure_margin(...)."""
    raise NotImplementedError


def strangle_to_condor(legs, wing: float):
    """Return a new list: legs followed by protective wings. For every SHORT option leg add a
    long leg of the same kind, same T, quantity -qty, struck `wing` further out of the money
    (put: K - wing; call: K + wing)."""
    raise NotImplementedError


def delta_hedge(legs, F, t, r, sigma):
    """Return legs + [Leg('fut', F, 0.0, q)] with q chosen so the position delta is zero."""
    raise NotImplementedError


def roll(legs, old_T: float, new_T: float):
    """Replace every option leg whose T equals old_T (to 1e-12) by the same leg with T=new_T."""
    raise NotImplementedError


def adjustment_trades(before, after):
    """Trades turning `before` into `after`: aggregate each book by key
    (kind, K, T) (use T = 0.0 for 'fut'), take qty_after - qty_before, drop zeros, and return
    Leg(kind, K, T, dq) objects sorted by key."""
    raise NotImplementedError


def adjustment_cost(before, after, half_spread=0.5, fee_per_order=20.0) -> float:
    """sum over adjustment_trades of |qty| * half_spread (points per unit) plus
    fee_per_order for each trade."""
    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.