Skip to content
All library documents

Pricing Zero-Coupon Bonds in the Hull–White Short-Rate Model

Article Quant Q&A · Author: TRex

Summary

The document explains how to price a zero-coupon bond at an intermediate time in the one-factor Hull–White model and debug a QuantLib simulation. The bond value is expressed as a time-dependent factor multiplied by an exponential function of the current short rate. The factor depends on the initial yield curve, the mean-reversion speed, volatility, and the time remaining to maturity.

The accepted answer identifies errors in the questioner's formula: the current simulated rate must be used in the final exponential, and the drift adjustment must account for the initial forward rate and curve-consistent terms. It gives a corrected expression and reports that averaging simulated bond-price paths shows prices moving toward par as maturity approaches. That illustration changes the volatility and forward-rate assumptions, so it is not a direct validation of the original setup. The exchange does not assess simulation convergence, calibration, or the suitability of the chosen model parameters.

Key ideas

  • A Hull–White zero-coupon bond price depends on the current short rate and the initial term structure.
  • The bond-price loading on the short rate is determined by mean reversion and time to maturity.
  • A pricing formula must distinguish the simulated rate at time t from the initial forward rate.
  • A sign error or an inconsistent curve adjustment can materially distort simulated prices.
  • The reported path average illustrates pull to par but does not establish calibration or convergence.

Tags

Full text
# Quantlib: How do I price a ZC bond using the Hull White model?


# Quantlib: How do I price a ZC bond using the Hull White model?












I am trying to use QuantLib to model short rate and looks like QL has some material here http://gouthamanbalaraman.com/blog/hull-white-simulation-quantlib-python.html

I have been able to simulate using the actual term structure, although in the attached link for simplicity GB has assumed flat forwards. Which is what I have used below.

I am calculating the price of the zero coupon bond at a time t using the below code

```
def price(T, t, r0):
    tau = T - t
    B = (1 - np.exp(-a * tau)) / a
    A = np.exp(-r0 * tau - B * r0 - sigma ** 2 / (4 * a ** 3) *
               (np.exp(-a * T) - np.exp(-a * t)) * (np.exp(2 * a * t) - 1))
    return A * np.exp(-r0 * B)
```

but I feel my zc bond prices are all over the place. Any clues what an I doing wrong here?

Does QuantLib provide a wrapper to calculate the zc prices using the HW model by any chance?

The complete code is below

```
import QuantLib as ql
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

sigma = 0.1
a = 0.1
length = 30 # in years
steps_per_year = 12
timestep = length * steps_per_year
forward_rate = 0.05
day_count = ql.Thirty360()
todays_date = ql.Date(15, 1, 2015)

ql.Settings.instance().evaluationDate = todays_date

spot_curve = ql.FlatForward(todays_date, ql.QuoteHandle(ql.SimpleQuote(forward_rate)), day_count)
spot_curve_handle = ql.YieldTermStructureHandle(spot_curve)

hw_process = ql.HullWhiteProcess(spot_curve_handle, a, sigma)
rng = ql.GaussianRandomSequenceGenerator(ql.UniformRandomSequenceGenerator(timestep, ql.UniformRandomGenerator()))
seq = ql.GaussianPathGenerator(hw_process, length, timestep, rng, False)

def generate_paths(n_scenarios):
    arr = np.zeros((n_scenarios, timestep+1))
    for i in range(n_scenarios):
        sample_path = seq.next()
        path = sample_path.value()
        time = [path.time(j) for j in range(len(path))]
        value = [path[j] for j in range(len(path))]
        arr[i, :] = np.array(value)
    return np.array(time), arr

n_scenarios = 1
time, paths = generate_paths(n_scenarios)
rates = pd.DataFrame(paths).T

#price a zero coupon bond

n_years = length
num_steps = timestep
dt = 1 / steps_per_year
prices = np.empty_like(rates)

def price(T, t, r0):
    tau = T - t
    B = (1 - np.exp(-a * tau)) / a
    A = np.exp(-r0 * tau - B * r0 - sigma ** 2 / (4 * a ** 3) *
               (np.exp(-a * T) - np.exp(-a * t)) * (np.exp(2 * a * t) - 1))
    return A * np.exp(-r0 * B)

prices[0] = price(n_years,dt*0,rates.values[0])

for steps in range(1,num_steps+1):
    prices[steps] = price(n_years, dt * steps, rates.values[steps])

plt.plot(prices)
plt.show()

```
```

## Answer by StackG (score 5, accepted)

https://quant.stackexchange.com/a/57256

Here is the price in HW[4] for a ZCB at time $t$:

\begin{align} P(t,T) &= A(t,T) e^{-B(t,T) r(t)}\\ A(t,T) &= {\frac {P(0,T)} {P(0,t)}} \exp \Bigl( B(t,T)F(0,t) - {\frac {\sigma^2} {4a}} B(t,T)^2(1-e^{-2at})\Bigr)\\ B(t,T) &= {\frac {1-e^{-a(T-t)}} {a}} \end{align}

You seem to be simulating to rate $r(t)$ at time $t$ and putting that into your function argument `r0`. I'm not exactly sure what `r0` is supposed to be, but note that the equations above include $F(0,t)$ - which is just a constant in your simulation (as you've used a `FlatForward` curve which I think should replace most of your `r0` terms, but the final line should read `return A * np.exp(-rt * B)`, where `rt` is the current value of rate. There is also a sign error on the `B * r0` term, which should be `+` instead of `-`

When I make these changes (and slightly reduce the rates vol, as 10% for 30Y is a long time and a lot of vol for a short rates model), and average the prices along the paths, I see the following, which nicely shows the bond's pull-to-par as its maturity approaches:

Full modified code:

```
import QuantLib as ql
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

sigma = 0.01
a = 0.1
length = 30 # in years
steps_per_year = 12
timestep = length * steps_per_year
forward_rate = 0.01
day_count = ql.Thirty360()
todays_date = ql.Date(15, 1, 2015)

ql.Settings.instance().evaluationDate = todays_date

spot_curve = ql.FlatForward(todays_date, ql.QuoteHandle(ql.SimpleQuote(forward_rate)), day_count)
spot_curve_handle = ql.YieldTermStructureHandle(spot_curve)

hw_process = ql.HullWhiteProcess(spot_curve_handle, a, sigma)
rng = ql.GaussianRandomSequenceGenerator(ql.UniformRandomSequenceGenerator(timestep, ql.UniformRandomGenerator()))
seq = ql.GaussianPathGenerator(hw_process, length, timestep, rng, False)

def generate_paths(n_scenarios):
    arr = np.zeros((n_scenarios, timestep+1))
    for i in range(n_scenarios):
        sample_path = seq.next()
        path = sample_path.value()
        time = [path.time(j) for j in range(len(path))]
        value = [path[j] for j in range(len(path))]
        arr[i, :] = np.array(value)
    return np.array(time), arr

n_scenarios = 1000
time, paths = generate_paths(n_scenarios)
rates = pd.DataFrame(paths).T

#price a zero coupon bond
n_years = length
num_steps = timestep
dt = 1 / steps_per_year
prices = np.empty_like(rates)

def price(T, t, f0, rt):
    tau = T - t
    B = (1 - np.exp(-a * tau)) / a
    A = np.exp(-f0 * tau + B * f0 - sigma ** 2 / (4 * a ** 3) *
               (np.exp(-a * T) - np.exp(-a * t)) * (np.exp(2 * a * t) - 1))
    return A * np.exp(-rt * B)

prices[0] = price(n_years,dt*0, forward_rate, rates.values[0])

for steps in range(1,num_steps+1):
    prices[steps] = price(n_years, dt * steps, forward_rate, rates.values[steps])

fig = plt.figure()
plt.figure(figsize=(16,10))

plt.subplot(2, 1, 1)
plt.title("All ZCB paths")
plt.plot(prices)

plt.subplot(2, 1, 2)

ave = [np.mean(x) for x in prices]
plt.plot(ave)
plt.title("Average ZCB paths")

plt.show()
```

[4]: Hull, J., White, A., “Pricing Interest-Rate-Derivative Securities”, Review of Financial Studies, Volume 3, Issue 4, pp. 573-592, 1990

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.