Bond and Derivative Pricing Classes Guide¶
The Hull-White model's affine structure means that zero-coupon bond prices, bond options, caps, floors, and swaptions all have closed-form or semi-closed-form expressions. This guide describes the pricing classes in the companion bond_derivative_pricing_classes.py module, mapping each method to its mathematical formula. The classes build on the HullWhite model class and its named functions (\(A\), \(B\), \(\theta\)) to compute prices and Greeks for the main interest rate derivatives.
Prerequisites
- Hull-White Model Class Guide (model class and named functions)
- Named Functions Implementation Guide (\(A\), \(B\), \(\theta\) computation)
- Bond Price Formula (derivation of \(P = e^{A + Br}\))
- Caplet-Floorlet Formula (caplet pricing theory)
Learning Objectives
By the end of this guide, you will be able to:
- Compute zero-coupon bond prices using \(P(t, T) = \exp(A(t,T) + B(t,T)\,r_t)\)
- Price European bond options using the Hull-White closed-form formula
- Price caplets and floorlets via the bond option equivalence
- Price swaptions using the Jamshidian decomposition
- Understand the class hierarchy and how methods delegate to the model class
Zero-Coupon Bond Pricing¶
Formula¶
In the Hull-White model, the zero-coupon bond price is
where \(B(t, T) = -(1 - e^{-\lambda(T-t)})/\lambda\) and \(A(t, T)\) is computed by numerical integration involving \(\theta\).
Implementation¶
```python class BondPricer: def init(self, hw_model): self.hw = hw_model
def zcb_price(self, t, T, r_t):
A = self.hw.compute_A(T - t, T)
B = self.hw.compute_B(T - t)
return np.exp(A + B * r_t)
```
For time-0 pricing, the initial short rate \(r_0\) is extracted from the market curve: \(r_0 = -\ln P^M(0, \delta)/\delta\) for a small \(\delta\).
Bond Option Pricing¶
European option on a zero-coupon bond¶
A European call option with strike \(K\), expiry \(T_0\), on a ZCB maturing at \(T_1 > T_0\) has the closed-form price:
where
and the bond price volatility is
Implementation¶
python
def bond_option(self, T0, T1, K, r0, option_type='call'):
P_T0 = self.zcb_price(0, T0, r0)
P_T1 = self.zcb_price(0, T1, r0)
sigma_P = (self.hw.sigma / self.hw.lambd) * (1 - np.exp(-self.hw.lambd * (T1 - T0))) \
* np.sqrt((1 - np.exp(-2 * self.hw.lambd * T0)) / (2 * self.hw.lambd))
d1 = np.log(P_T1 / (K * P_T0)) / sigma_P + sigma_P / 2
d2 = d1 - sigma_P
if option_type == 'call':
return P_T1 * norm.cdf(d1) - K * P_T0 * norm.cdf(d2)
else:
return K * P_T0 * norm.cdf(-d2) - P_T1 * norm.cdf(-d1)
Caplet and Floorlet Pricing¶
Bond option equivalence¶
A caplet with reset \(T_i\), payment \(T_{i+1} = T_i + \delta\), and strike \(K\) is equivalent to a put option on a zero-coupon bond:
This identity converts caplet pricing to bond option pricing, which has the closed-form formula above.
Implementation¶
```python def caplet(self, T_i, delta, K, r0): bond_strike = 1.0 / (1.0 + K * delta) return (1 + K * delta) * self.bond_option(T_i, T_i + delta, bond_strike, r0, 'put')
def cap(self, reset_dates, delta, K, r0): return sum(self.caplet(T_i, delta, K, r0) for T_i in reset_dates) ```
Floorlets are analogous, using call options on the bond.
Swaption Pricing¶
Jamshidian decomposition¶
A European payer swaption with expiry \(T_0\) on a swap with fixed rate \(K\) and payment dates \(T_1, \ldots, T_n\) decomposes into a portfolio of bond options. The key insight: at expiry \(T_0\), the swap value is a sum of bond prices, and since bond prices are monotone in \(r_{T_0}\), there exists a critical rate \(r^*\) at which the swap is exactly at the money.
Step 1. Find \(r^*\) solving:
where \(c_i = K\delta\) for \(i < n\) and \(c_n = 1 + K\delta\).
Step 2. The swaption price is:
Implementation¶
```python def swaption(self, T0, payment_dates, K, delta, r0): n = len(payment_dates) coupons = [K * delta] * (n - 1) + [1 + K * delta]
# Step 1: find r* by root-finding
def swap_value(r_star):
return sum(c * self.zcb_price(T0, T_i, r_star)
for c, T_i in zip(coupons, payment_dates)) - 1.0
r_star = brentq(swap_value, -0.1, 0.5)
# Step 2: sum of bond puts
strikes = [self.zcb_price(T0, T_i, r_star) for T_i in payment_dates]
return sum(c * self.bond_option(T0, T_i, Ki, r0, 'put')
for c, T_i, Ki in zip(coupons, payment_dates, strikes))
```
Receiver Swaptions
For a receiver swaption, replace puts with calls in the decomposition. The Jamshidian trick works because all bond prices are monotonically decreasing in \(r\), which is guaranteed by the one-factor structure.
Summary¶
| Derivative | Method | Mathematical basis |
|---|---|---|
| ZCB | zcb_price |
\(P = e^{A + Br}\) |
| Bond option | bond_option |
Black-Scholes-like with \(\sigma_P\) |
| Caplet/floorlet | caplet / floorlet |
Bond option equivalence |
| Cap/floor | cap / floor |
Sum of caplets/floorlets |
| Swaption | swaption |
Jamshidian decomposition |
For the tree-based and Monte Carlo pricing engines (used for path-dependent derivatives), see Tree and Monte Carlo Engines Guide. For the calibration pipeline that uses these pricing classes, see Calibration Pipeline Guide.
Exercises¶
Exercise 1. Using the BondPricer class with Hull-White parameters \(\sigma = 0.01\), \(\lambda = 0.05\), and a flat market curve at 3\%, compute \(B(0, 10)\) and \(A(0, 10)\). Verify that the model zero-coupon bond price \(P^{\text{HW}}(0, 10)\) matches \(e^{-0.03 \times 10}\) to within the numerical tolerance of the trapezoidal integration.
Solution to Exercise 1
With \(\sigma = 0.01\), \(\lambda = 0.05\), flat curve at 3%, and \(r_0 = 0.03\):
For the flat curve, \(\theta(t) = r + \frac{\sigma^2}{2\lambda^2}(1 - e^{-2\lambda t})\). The function \(A(0, 10)\) is computed by numerical integration:
The analytical part evaluates to:
The model bond price should satisfy the recovery condition:
Since \(\theta(t)\) is calibrated to match the market curve, this must equal:
The numerical integration with 250 grid points introduces errors of order \(10^{-6}\), so the match should hold to at least 5 significant figures: \(|P^{\text{HW}} - P^M|/P^M < 10^{-5}\).
Exercise 2. The bond price volatility \(\sigma_P\) is given by
Compute \(\sigma_P\) for a bond option with \(T_0 = 2\), \(T_1 = 5\), \(\sigma = 0.015\), \(\lambda = 0.03\). Explain why \(\sigma_P\) increases with \(T_1 - T_0\) but eventually saturates.
Solution to Exercise 2
With \(T_0 = 2\), \(T_1 = 5\), \(\sigma = 0.015\), \(\lambda = 0.03\):
Why \(\sigma_P\) increases with \(T_1 - T_0\): The factor \((1 - e^{-\lambda(T_1 - T_0)})\) increases as \(T_1 - T_0\) grows. A longer-maturity bond has greater price sensitivity to the short rate (larger \(|B|\)), so its volatility is higher.
Why it saturates: As \(T_1 - T_0 \to \infty\), \(e^{-\lambda(T_1 - T_0)} \to 0\) and \((1 - e^{-\lambda(T_1 - T_0)}) \to 1\). The saturation occurs because mean reversion limits the bond's sensitivity: \(B(t, T) \to -1/\lambda\) as \(T - t \to \infty\). Beyond the mean-reversion horizon \(1/\lambda\), extending the bond maturity adds no further rate sensitivity, so \(\sigma_P\) plateaus at \(\frac{\sigma}{\lambda}\sqrt{\frac{1 - e^{-2\lambda T_0}}{2\lambda}}\).
Exercise 3. Verify the caplet-bond-put equivalence numerically. Using parameters \(\sigma = 0.01\), \(\lambda = 0.05\), \(r_0 = 0.03\), flat curve at 3\%, price a caplet with \(T_i = 1\), \(\delta = 0.5\), \(K = 3\%\) by: (a) calling caplet(T_i, delta, K, r0), and (b) manually computing \((1 + K\delta) \times \text{Put}(T_i, T_i + \delta, 1/(1+K\delta))\). Confirm the two approaches give identical results.
Solution to Exercise 3
Parameters: \(\sigma = 0.01\), \(\lambda = 0.05\), \(r_0 = 0.03\), flat curve at 3%, \(T_i = 1\), \(\delta = 0.5\), \(K = 0.03\).
(a) Using the caplet method: caplet(1, 0.5, 0.03, 0.03) internally computes the bond strike \(K_B = 1/(1 + 0.03 \times 0.5) = 1/1.015 = 0.98522\) and calls the bond put with \(T_0 = 1\), \(T_1 = 1.5\), strike \(K_B = 0.98522\), then multiplies by \((1 + K\delta) = 1.015\).
(b) Manual computation: The bond put price uses the formula with:
Then:
The caplet price is \((1 + K\delta) \times \text{Put}(1, 1.5, K_B)\). Both approaches (a) and (b) give identical results because the caplet method is simply a wrapper that performs exactly this bond-put computation internally.
Exercise 4. In the Jamshidian swaption decomposition, explain why the critical rate \(r^*\) exists and is unique. For a payer swaption with \(T_0 = 1\), annual payments at \(T_1 = 2, T_2 = 3\), and \(K = 4\%\), write down the equation for \(r^*\) explicitly in terms of the Hull-White bond price formula. What happens to \(r^*\) if \(K\) is very large? Very small?
Solution to Exercise 4
Existence and uniqueness of \(r^*\): In the one-factor Hull-White model, the bond price \(P(T_0, T_i; r) = \exp(A(T_0, T_i) + B(T_0, T_i) \cdot r)\) is a strictly decreasing function of \(r\) (since \(B(T_0, T_i) < 0\) for \(T_i > T_0\)). Therefore the swap value at \(T_0\):
is a sum of strictly decreasing exponentials minus 1, hence strictly decreasing in \(r\). Since \(S(r) \to +\infty\) as \(r \to -\infty\) and \(S(r) \to -1\) as \(r \to +\infty\), by the intermediate value theorem there exists exactly one \(r^*\) solving \(S(r^*) = 0\).
For the specific case \(T_0 = 1\), \(T_1 = 2\), \(T_2 = 3\), \(K = 4\%\), \(\delta = 1\):
- \(c_1 = K\delta = 0.04\)
- \(c_2 = 1 + K\delta = 1.04\)
The equation for \(r^*\) is:
If \(K\) is very large, the coupon payments dominate, and the sum \(S(r)\) is large even for moderate \(r\), so \(r^*\) must be very large to bring the bond prices down enough. If \(K\) is very small (close to zero), the swap is essentially a zero-coupon instrument and \(r^*\) satisfies \(P(T_0, T_n; r^*) \approx 1\), which means \(r^* \approx 0\) (or the rate implied by \(A(T_0, T_n) + B(T_0, T_n) \cdot r^* = 0\)).
Exercise 5. Using the bond_option method, compute the price of a European call on a 5-year ZCB with strike \(K = 0.85\), option expiry \(T_0 = 2\), and parameters \(\sigma = 0.01\), \(\lambda = 0.05\), flat curve at 3\%. Then compute the corresponding put price using put-call parity: Put \(=\) Call \(- P(0, 5) + K \cdot P(0, 2)\). Verify that the direct put formula gives the same answer.
Solution to Exercise 5
Parameters: \(\sigma = 0.01\), \(\lambda = 0.05\), flat curve at 3%, \(T_0 = 2\), \(T_1 = 5\), \(K = 0.85\).
Bond prices: \(P(0, 2) = e^{-0.06} = 0.94176\), \(P(0, 5) = e^{-0.15} = 0.86071\).
Bond price volatility:
Call option:
Put-call parity:
The put is very small because the strike \(K = 0.85\) is well below the forward bond price \(P(0, 5)/P(0, 2) = 0.86071/0.94176 = 0.91397\). The direct put formula from bond_option(..., 'put') should return the same value \(\approx 0.00027\).
Exercise 6. Price a 5-year cap (annual resets, \(\delta = 1\), \(K = 3\%\)) using the Hull-White model with \(\sigma = 0.01\), \(\lambda = 0.05\), flat curve at 3\%. Decompose the cap price into individual caplet contributions. Which caplet is most expensive, and why? How does the answer change if \(\lambda\) is increased to 0.20?
Solution to Exercise 6
A 5-year cap with annual resets (\(\delta = 1\), \(K = 3\%\)) consists of 4 caplets (the first reset at \(T_0 = 1\) pays at \(T_1 = 2\), and so on through \(T_3 = 4\) paying at \(T_4 = 5\)).
Each caplet uses the bond-put equivalence: Caplet\((T_i, T_{i+1}, K) = (1 + K\delta) \times \text{Put}(T_i, T_{i+1}, 1/(1+K\delta))\).
With \(K_B = 1/1.03 = 0.97087\), and \((1 + K\delta) = 1.03\):
The bond price volatility for caplet \(i\) (reset \(T_i\), payment \(T_i + 1\)):
Since \(\sigma_{P,i}\) increases with \(T_i\) (through the \(\sqrt{(1 - e^{-0.1T_i})/0.1}\) factor), later caplets have higher bond volatility and therefore higher option value.
Approximate caplet prices (in basis points of notional):
| Caplet | Reset \(T_i\) | \(\sigma_{P,i}\) | Price (bps) |
|---|---|---|---|
| 1 | 1 | 0.00300 | \(\sim\) 1.2 |
| 2 | 2 | 0.00419 | \(\sim\) 1.7 |
| 3 | 3 | 0.00507 | \(\sim\) 2.0 |
| 4 | 4 | 0.00573 | \(\sim\) 2.3 |
The last caplet (reset at \(T = 4\)) is the most expensive because it has the highest bond volatility \(\sigma_{P,4}\). This is because the longer the time to reset, the more uncertainty accumulates in the short rate (the \(\sqrt{(1-e^{-2\lambda T_i})/(2\lambda)}\) factor grows with \(T_i\)), making the option more valuable.
Effect of increasing \(\lambda\) to 0.20: Higher mean reversion has two effects:
- The factor \((1 - e^{-\lambda})\) decreases slightly (bond sensitivity per unit tenor shrinks).
- The saturation of \(\sqrt{(1 - e^{-2\lambda T_i})/(2\lambda)}\) occurs faster, so the differences between caplet prices narrow.
The overall cap price decreases because stronger mean reversion reduces rate volatility at longer horizons. The most expensive caplet may shift to an earlier reset because the volatility benefit of later resets is diminished by the faster mean reversion.