Source code for tensorquantlib.finance.ir_derivatives

"""Interest rate derivatives: swaptions, caps, and floors.

Pricing under the Black (1976) model for European swaptions and caplets.
"""

from __future__ import annotations

import numpy as np
from scipy.stats import norm

# ---------------------------------------------------------------------------
# Cap / Floor pricing (Black 76)
# ---------------------------------------------------------------------------


[docs] def black76_caplet( forward: float, strike: float, T: float, sigma: float, df: float, notional: float = 1.0, tau: float = 0.25, ) -> float: """Price a single caplet using the Black (1976) model. Parameters ---------- forward : float Forward rate for the period. strike : float Cap strike rate. T : float Time to caplet expiry (option maturity). sigma : float Black implied volatility for the forward rate. df : float Discount factor to payment date. notional : float Notional amount. tau : float Day count fraction for the period. Returns ------- float Caplet price. """ if T <= 0: return max(forward - strike, 0.0) * tau * df * notional d1 = (np.log(forward / strike) + 0.5 * sigma**2 * T) / (sigma * np.sqrt(T)) d2 = d1 - sigma * np.sqrt(T) return float(df * tau * notional * (forward * norm.cdf(d1) - strike * norm.cdf(d2)))
[docs] def black76_floorlet( forward: float, strike: float, T: float, sigma: float, df: float, notional: float = 1.0, tau: float = 0.25, ) -> float: """Price a single floorlet using Black (1976). Parameters ---------- forward, strike, T, sigma, df, notional, tau Same as :func:`black76_caplet`. Returns ------- float Floorlet price. """ if T <= 0: return max(strike - forward, 0.0) * tau * df * notional d1 = (np.log(forward / strike) + 0.5 * sigma**2 * T) / (sigma * np.sqrt(T)) d2 = d1 - sigma * np.sqrt(T) return float(df * tau * notional * (strike * norm.cdf(-d2) - forward * norm.cdf(-d1)))
[docs] def cap_price( forwards: np.ndarray, strike: float, expiries: np.ndarray, sigma: float | np.ndarray, dfs: np.ndarray, notional: float = 1.0, tau: float = 0.25, ) -> float: """Price an interest rate cap (sum of caplets). Parameters ---------- forwards : array, shape (n,) Forward rates for each period. strike : float Cap strike. expiries : array, shape (n,) Expiry time for each caplet. sigma : float or array Black vol (flat or per-caplet). dfs : array, shape (n,) Discount factors to each payment date. notional : float Notional. tau : float Day count fraction. Returns ------- float Total cap price. """ sigmas = np.broadcast_to(np.asarray(sigma, dtype=float), forwards.shape) total = 0.0 for i in range(len(forwards)): total += black76_caplet(forwards[i], strike, expiries[i], sigmas[i], dfs[i], notional, tau) return total
[docs] def floor_price( forwards: np.ndarray, strike: float, expiries: np.ndarray, sigma: float | np.ndarray, dfs: np.ndarray, notional: float = 1.0, tau: float = 0.25, ) -> float: """Price an interest rate floor (sum of floorlets). Parameters same as :func:`cap_price`. """ sigmas = np.broadcast_to(np.asarray(sigma, dtype=float), forwards.shape) total = 0.0 for i in range(len(forwards)): total += black76_floorlet( forwards[i], strike, expiries[i], sigmas[i], dfs[i], notional, tau ) return total
# --------------------------------------------------------------------------- # Swaption pricing (Black 76) # ---------------------------------------------------------------------------
[docs] def swap_rate(dfs: np.ndarray, tau: float = 0.5) -> float: """Par swap rate from discount factors. S = (df_0 - df_n) / (tau * sum(df_i)) Parameters ---------- dfs : array, shape (n+1,) Discount factors at each payment date, including the start (dfs[0]). tau : float Day count fraction for each period. Returns ------- float Par swap rate. """ annuity = tau * np.sum(dfs[1:]) if annuity < 1e-15: return 0.0 return float((dfs[0] - dfs[-1]) / annuity)
[docs] def swaption_price( swap_r: float, strike: float, T_option: float, sigma: float, annuity: float, notional: float = 1.0, payer: bool = True, ) -> float: """Price a European swaption using Black (1976). Parameters ---------- swap_r : float Forward swap rate. strike : float Swaption strike. T_option : float Time to swaption expiry. sigma : float Black implied vol for the swap rate. annuity : float PV of the swap's fixed-leg annuity factor. notional : float Notional. payer : bool If True, price a payer swaption (right to pay fixed). If False, price a receiver swaption. Returns ------- float Swaption price. """ if T_option <= 0: if payer: return max(swap_r - strike, 0.0) * annuity * notional return max(strike - swap_r, 0.0) * annuity * notional d1 = (np.log(swap_r / strike) + 0.5 * sigma**2 * T_option) / (sigma * np.sqrt(T_option)) d2 = d1 - sigma * np.sqrt(T_option) if payer: price = annuity * notional * (swap_r * norm.cdf(d1) - strike * norm.cdf(d2)) else: price = annuity * notional * (strike * norm.cdf(-d2) - swap_r * norm.cdf(-d1)) return float(price)
[docs] def swaption_parity( payer: float, receiver: float, swap_r: float, strike: float, annuity: float, notional: float = 1.0, ) -> float: """Check put-call parity for swaptions. Payer - Receiver = (S - K) * A * N Returns the parity residual (should be ~0). """ expected = (swap_r - strike) * annuity * notional return float(payer - receiver - expected)