Exercise · m09_strategies.py
Multi-leg option positions, their Greeks, and variance-swap replication
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.