Hull-White Process

Table of Contents

Summary

The Hull-White (1990) model generalises Vasicek by letting the mean-reversion target become a function of time, \(\theta(t)\), instead of a single constant. This one change solves Vasicek's biggest practical limitation: a constant \(\theta\) cannot be tuned to make the model's initial term structure match every observed market rate simultaneously, whereas a piecewise-constant \(\theta(t)\) can, by construction, be chosen to fit the curve exactly at every input pillar. ores::analytics::quant::service::hull_white_process implements it, and vasicek_process composes it rather than being a separate implementation.

Layperson's mental model

Take the elastic band pulling toward a fixed resting length and let the resting length itself change over time, in steps, on a schedule chosen in advance — instead of one fixed target, the band is told "aim for this length for the first period, then this other length for the next period", and so on. That single change is what lets the model be tuned to match a whole observed curve, point by point, rather than being stuck approximating it with one fixed target.

double hull_white_process::next() {
    const double z = normal_(rng_);
    const double theta_i = theta_at(tick_);   // this tick's target, from a schedule
    const double decay = std::exp(-kappa_);
    const double var = (1.0 - decay * decay) / (2.0 * kappa_);
    rate_ = theta_i + (rate_ - theta_i) * decay + sigma_ * std::sqrt(var) * z;
    ++tick_;
    return rate_;
}

Detail

The SDE

\begin{equation} dr = \kappa(\theta(t) - r)\, dt + \sigma\, dW \end{equation}

\(\theta(t)\) is supplied to hull_white_process as a piecewise-constant function (theta_path) rather than a single number — holding \(\theta(t)\) constant reduces this SDE to exactly Vasicek's, which is why Vasicek is implemented as a thin wrapper around this class rather than duplicated.

Why fit the initial curve exactly

A constant-\(\theta\) model like Vasicek has only three free parameters (\(\kappa\), \(\theta\), \(\sigma\)) to match an entire observed yield curve, which typically has far more independently-observed points than that — an exact fit is generically impossible. Letting \(\theta(t)\) vary in time adds one degree of freedom per time step, which is exactly enough freedom to force the model's implied curve through every observed input point exactly, while keeping \(\kappa\) and \(\sigma\) as the (still just two) parameters controlling the curve's dynamics — how it moves — rather than its shape today.

dt: an explicit, separate step-length parameter

\(\kappa\), \(\theta(t)\), \(\sigma\) are always expressed in the SDE's own time unit (years); a tick is a purely internal simulation step whose real-world length is a separate, explicit dt (year fraction per tick) — a constructor parameter defaulting to 1.0 (one tick per year), never something a caller pre-folds into kappa=/=sigma themselves. Both next()'s transition and discount_factor()'s recursion take dt into account:

\begin{equation} r_{i+1} = \theta_i + (r_i - \theta_i)\, e^{-\kappa\, dt} + \sigma \sqrt{\frac{1 - e^{-2\kappa\, dt}}{2\kappa}}\, Z \end{equation}

and the backward recursion for \(B_i\) (see below) accumulates \(dt\) of bond-time per tick, not a flat \(1\) — a tick spanning, say, one calendar day (dt = 1/365) must integrate only \(1/365\) of a year's discounting, not a full year's. Getting this wrong is not a cosmetic error: at dt = 1/365 over a 730-tick (2-year) horizon, treating each tick as a full year of bond-time computes \(e^{-730\, r}\) instead of \(e^{-2\, r}\) — an astronomically over-discounted price, and the concrete bug this dt parameter fixes (see the task that found and fixed it).

Cross-checked against QuantLib (Engine.remote/QuantLib/ql/processes/ornsteinuhlenbeckprocess.cpp, ql/models/shortrate/onefactormodels/vasicek.cpp): every QuantLib short-rate formula keeps speed (kappa) and dt=/=Time arguments always separate (exp(-speed*dt), never a pre-scaled composite), and Vasicek/Hull-White's own bond pricing in QuantLib uses a genuine closed form in real \((t,T)\) years, \(B(t,T) = (1-e^{-a(T-t)})/a\), rather than an iterative per-tick recursion at all — a possible future simplification for this class's own constant-\(\theta\) (Vasicek) special case, not implemented here, since the general time-varying \(\theta(t)\) case this class exists for still needs the recursion. Only the domain knowledge (the \(dt\) discipline, the closed forms, the degenerate small-\(\kappa\) algebraic-limit handling) is adopted from QuantLib here, not its object-oriented shape (StochasticProcess1D inheritance, observer-pattern term-structure handles) — this codebase stays data-oriented, both by convention and because a future GPU-batched simulation of many processes at once favours plain parameter arrays over one object per process.

Two equivalent forms

Hull & White's original paper writes the SDE in "drift intercept" form, \(dr = [\phi(t) - a r]\, dt + \sigma\, dW\) with \(\phi(t) = \kappa\theta(t)\). hull_white_process instead uses the "target level" form shown above, \(\kappa(\theta(t)-r)\), the same shape as ornstein_uhlenbeck_process's SDE. The two are algebraically equivalent (relabel \(\phi(t) = \kappa\theta(t)\) to move between them), but the target-level form is chosen deliberately in this codebase: it makes both the degenerate \(\kappa \le 0\) case and the Vasicek special case (\(\theta(t)\) held constant) reduce to exactly ornstein_uhlenbeck_process's formula, with no re-derivation or approximation needed — a direct, mechanical consequence of the form chosen, not a coincidence of the underlying mathematics.

See also

Further reading

  • Hull, J., & White, A. (1990). "Pricing Interest-Rate-Derivative Securities." The Review of Financial Studies, 3(4), 573-592. The original paper introducing the time-varying \(\theta(t)\) generalisation of Vasicek and its exact-curve-fitting property.
  • Brigo, D., & Mercurio, F. (2006). Interest Rate Models — Theory and Practice (2nd ed.). Springer. Chapter 3 covers both SDE forms (drift intercept vs. target level) and their equivalence in detail.
  • Wikipedia: Hull-White model.

Emacs 29.3 (Org mode 9.6.15)