Markets: A Rigorous Course

Exercise · m09_strategies.py

Multi-leg option positions, their Greeks, and variance-swap replication

A position is a list of Leg objects. Each leg is a European call, put, or the underlying itself ("stock"; for Nifty, think of a future with zero carry), with a signed quantity (+ long, - short) and its own expiry T (years from today, t = 0). All valuation uses one flat Black-Scholes volatility sigma and rate r, no dividends.

Greek conventions (same as M08): delta per 1 unit of S, gamma per unit S^2, vega per 1.00 (i.e. 100 vol points) of sigma, theta per YEAR of calendar time (dV/dt, so a long option has negative theta).

Provided from toolkit.options (you wrote BS in M08): bs_price(S, K, tau, r, sigma, kind="call") intrinsic value when tau <= 0 bs_greeks(S, K, tau, r, sigma, kind="call") dict(delta, gamma, vega, theta, rho) Leg(kind, K=0.0, T=0.0, qty=1.0) frozen dataclass; leg.is_option kind is "call", "put" or "stock". A stock leg is worth qty * (S - K): with the default K = 0 that is qty * S, the share held outright (M09A uses K as a futures trade price).

Run: cd exercises && .venv/bin/python -m pytest modules/M09-volatility-trading/tests/test_m09_strategies.py

Tests (11)

  • test_position_greeks_match_finite_differences
  • test_single_leg_matches_bs
  • test_straddle_vega_is_twice_call_vega
  • test_iron_condor_payoff_bounds_and_breakevens
  • test_bull_spread_max_profit_and_loss
  • test_covered_call_is_synthetic_short_put
  • test_calendar_and_short_straddle_greek_signs
  • test_ratio_spread_unbounded_upside_loss
  • test_atm_gamma_scaling
  • test_variance_swap_strike_flat_vol_recovers_sigma_squared
  • test_variance_swap_strike_exceeds_atm_variance_under_skew

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

not run yet

Not saved: sign up to keep your code
"""M09 exercise 3: multi-leg option positions, their Greeks, and variance-swap replication
(notes §9.6-9.8).

A position is a list of Leg objects. Each leg is a European call, put, or the
underlying itself ("stock"; for Nifty, think of a future with zero carry), with a
signed quantity (+ long, - short) and its own expiry T (years from today, t = 0).
All valuation uses one flat Black-Scholes volatility sigma and rate r, no dividends.

Greek conventions (same as M08): delta per 1 unit of S, gamma per unit S^2,
vega per 1.00 (i.e. 100 vol points) of sigma, theta per YEAR of calendar time
(dV/dt, so a long option has negative theta).

Provided from toolkit.options (you wrote BS in M08):
    bs_price(S, K, tau, r, sigma, kind="call")    intrinsic value when tau <= 0
    bs_greeks(S, K, tau, r, sigma, kind="call")   dict(delta, gamma, vega, theta, rho)
    Leg(kind, K=0.0, T=0.0, qty=1.0)              frozen dataclass; leg.is_option
kind is "call", "put" or "stock". A stock leg is worth qty * (S - K): with the default
K = 0 that is qty * S, the share held outright (M09A uses K as a futures trade price).

Run:  cd exercises && .venv/bin/python -m pytest modules/M09-volatility-trading/tests/test_m09_strategies.py
"""

from __future__ import annotations

import math

import numpy as np

from toolkit.options import Leg, bs_greeks, bs_price  # noqa: F401  (provided, M08)


# ------------------------------------------------------------------ positions

def position_value(legs, S, t, r, sigma) -> float:
    """Value at calendar time t (years from today) and spot S: sum of qty * leg value.

    An option leg with T - t <= 0 is worth its intrinsic value; a stock leg is worth S - K
    (S with the default K = 0).
    """
    raise NotImplementedError


def position_greeks(legs, S, t, r, sigma) -> dict:
    """dict(delta, gamma, vega, theta): sum of qty * leg Greeks at time t.

    Stock leg: delta = 1, other Greeks 0. Every option leg has T - t > 0.
    """
    raise NotImplementedError


def payoff_at_expiry(legs, S_T) -> np.ndarray:
    """Vectorised terminal payoff: sum over legs of qty * intrinsic; stock legs pay S_T - K.

    Assumes all option legs share one expiry (do not use for calendars).
    """
    raise NotImplementedError


def expiry_pnl(legs, S_T, S0, r, sigma) -> np.ndarray:
    """P&L at expiry (undiscounted): payoff - initial cost * e^{rT}, T = max option-leg expiry.

    Initial cost = position_value(legs, S0, 0, r, sigma). A net credit is a negative cost.
    """
    raise NotImplementedError


def breakevens(legs, S0, r, sigma, lo, hi, n=200_001) -> list[float]:
    """Spot levels in [lo, hi] where expiry_pnl changes sign: evaluate on np.linspace(lo, hi, n),
    find strict sign changes, refine each by linear interpolation. Return sorted list."""
    raise NotImplementedError


# ------------------------------------------------------------------ builders (notes §9.7)

def bull_call_spread(K1, K2, T, qty=1.0):
    """[long qty call K1, short qty call K2], K1 < K2."""
    raise NotImplementedError


def straddle(K, T, qty=1.0):
    """[call K, put K], both with quantity qty (qty < 0 for a short straddle)."""
    raise NotImplementedError


def strangle(Kp, Kc, T, qty=1.0):
    """[put Kp, call Kc], both quantity qty, Kp < Kc (qty < 0 for short)."""
    raise NotImplementedError


def iron_condor(K1, K2, K3, K4, T, qty=1.0):
    """SHORT iron condor: [+qty put K1, -qty put K2, -qty call K3, +qty call K4], K1<K2<=K3<K4."""
    raise NotImplementedError


def call_calendar(K, T_near, T_far, qty=1.0):
    """LONG calendar: [-qty call (K, T_near), +qty call (K, T_far)]."""
    raise NotImplementedError


def call_ratio_spread(K1, K2, T, ratio=2.0, qty=1.0):
    """[+qty call K1, -ratio*qty call K2], K1 < K2."""
    raise NotImplementedError


def covered_call(K, T, qty=1.0):
    """[+qty stock, -qty call K]."""
    raise NotImplementedError


# ------------------------------------------------------------------ expiry gamma

def atm_gamma_approx(S, sigma, tau) -> float:
    """Leading-order ATM gamma 1 / (sigma S sqrt(2 pi tau)). Notes eq. (9.26)."""
    raise NotImplementedError


# ------------------------------------------------------------------ variance swap

def variance_swap_strike(strikes, otm_prices, F, T, r) -> float:
    """Discretised fair variance (VIX-style, notes eq. 9.25):

        K_var = (2 e^{rT} / T) * sum_i (Delta K_i / K_i^2) Q(K_i) - (1/T) (F / K_0 - 1)^2

    strikes ascending; Q(K_i) = put price if K_i < K_0, call if K_i > K_0, average of call
    and put at K_0, where K_0 is the largest strike <= F. Delta K_i = (K_{i+1} - K_{i-1}) / 2
    in the interior and the one-sided difference at the two ends.
    Returns annualised variance (not vol).
    """
    raise NotImplementedError


def otm_strip(strikes, F, T, r, sigma_of_K):
    """OTM option prices Q(K_i) on `strikes` from a vol function sigma_of_K(K), priced with BS
    off spot S = F e^{-rT}: put below K_0, call above, average of the two at K_0
    (K_0 = largest strike <= F). Returns a numpy array."""
    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.