Markets: A Rigorous Course

Exercise · m09a_implied_move.py

The straddle-implied move, event variance from the term structure, IV rank and IV percentile

Conventions: Black-76 on the forward F, continuously compounded rate r, tau in years, sigma annualised. Total implied variance of an expiry is sigma^2 * tau.

Provided: b76(F, K, tau, r, sigma, kind="call"), the Black-76 price of a European call or put on a forward F (toolkit.options.black76_price, which you wrote in M08; intrinsic value discounted at tau <= 0).

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

Tests (9)

  • test_closed_form_matches_black76
  • test_approximation_error_is_x2_over_24
  • test_rbi_eve_straddle_and_inversion
  • test_expected_abs_move_monte_carlo
  • test_event_variance_recovers_construction
  • test_event_variance_real_term_structures
  • test_iv_rank_and_percentile
  • test_iv_rank_has_no_lookahead
  • test_percentile_uniform_under_iid

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

not run yet

Not saved: sign up to keep your code
"""M09A exercise 2: the straddle-implied move, event variance from the term structure,
IV rank and IV percentile (notes §9A.7-9A.10).

Conventions: Black-76 on the forward F, continuously compounded rate r, tau in years,
sigma annualised. Total implied variance of an expiry is sigma^2 * tau.

Provided: b76(F, K, tau, r, sigma, kind="call"), the Black-76 price of a European call or
put on a forward F (toolkit.options.black76_price, which you wrote in M08; intrinsic value
discounted at tau <= 0).

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

from __future__ import annotations

import math

import numpy as np
import pandas as pd
from scipy.optimize import brentq
from scipy.stats import norm

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

SQRT_2_OVER_PI = math.sqrt(2.0 / math.pi)


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

def straddle_exact(F: float, K: float, tau: float, r: float, sigma: float) -> float:
    """Black-76 call + put at strike K."""
    raise NotImplementedError


def atm_straddle_closed_form(F: float, tau: float, r: float, sigma: float) -> float:
    """Exact ATM-forward straddle (K = F), notes (9A.7):
        2 e^{-r tau} F (2 N(sigma sqrt(tau) / 2) - 1)."""
    raise NotImplementedError


def atm_straddle_approx(F: float, tau: float, r: float, sigma: float) -> float:
    """Linear approximation, notes (9A.8): e^{-r tau} F sigma sqrt(tau) sqrt(2/pi)."""
    raise NotImplementedError


def expected_abs_move(S: float, sigma: float, tau: float) -> float:
    """E|S_T - S| when S_T - S ~ N(0, (S sigma)^2 tau): S sigma sqrt(tau) sqrt(2/pi)."""
    raise NotImplementedError


def prob_move_within(m: float, S: float, sigma: float, tau: float) -> float:
    """P(|S_T - S| < m) in the same normal model: 2 N(m / (S sigma sqrt(tau))) - 1."""
    raise NotImplementedError


def straddle_implied_vol(straddle: float, F: float, K: float, tau: float, r: float) -> float:
    """The sigma with straddle_exact(F, K, tau, r, sigma) == straddle, found by Brent's method
    on [1e-4, 5]. Raise ValueError if straddle is not strictly between the prices at the
    two ends of the bracket."""
    raise NotImplementedError


def event_variance(sigma1: float, tau1: float, sigma2: float, tau2: float) -> tuple[float, float]:
    """Two expiries 0 < tau1 < tau2 that both contain one scheduled event. Model:
        sigma_i^2 tau_i = sigma_d^2 tau_i + e   (i = 1, 2).
    Return (e, sigma_d), notes (9A.13):
        e = tau1 tau2 (sigma1^2 - sigma2^2) / (tau2 - tau1),
        sigma_d = sqrt((sigma2^2 tau2 - sigma1^2 tau1) / (tau2 - tau1))  (NaN if the radicand <= 0).
    e may be negative. Raise ValueError unless 0 < tau1 < tau2."""
    raise NotImplementedError


def event_variance_before_after(sigma_b: float, tau_b: float, sigma_a: float, tau_a: float,
                                sigma_d: float) -> float:
    """One expiry before the event (tau_b) and one after (tau_a), ordinary volatility sigma_d
    between them, notes (9A.14):
        e = sigma_a^2 tau_a - sigma_b^2 tau_b - sigma_d^2 (tau_a - tau_b).
    Raise ValueError unless 0 <= tau_b < tau_a."""
    raise NotImplementedError


def event_move(e: float) -> dict:
    """dict(sd=sqrt(e), mean_abs=sqrt(e) * sqrt(2/pi)); ValueError if e <= 0."""
    raise NotImplementedError


def iv_rank(iv: pd.Series, window: int = 252) -> pd.Series:
    """IV rank, notes (9A.10): (IV_t - min) / (max - min), min and max over the trailing
    `window` observations INCLUDING t. NaN for the first window-1 points and when max == min."""
    raise NotImplementedError


def iv_percentile(iv: pd.Series, window: int = 252) -> pd.Series:
    """IV percentile, notes (9A.11): the fraction of the window-1 observations before t
    (t-window+1 .. t-1) that are strictly below IV_t. NaN for the first window-1 points."""
    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.