1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// RustQuant: A Rust library for quantitative finance tools.
// Copyright (C) 2023 https://github.com/avhz
// Dual licensed under Apache 2.0 and MIT.
// See:
// - LICENSE-APACHE.md
// - LICENSE-MIT.md
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// TODO: Update Hull-White pricer to accept dates instead of time to maturity.
//! Hull-White model for zero-coupon bond prices.
//!
//! The risk-neutral short rate follows the process:
//!
//! dr = (theta(t) - a*r_t)dt + sigma * dW_t
//!
//! It incorporates a mean-reversion factor into the drift term:
//!
//! - `theta(t)`: is the rate at which it gets pulled.
//! - `a`: is the level to which it gets pulled.
//! - `r_t`: short rate at time t
//! - `sigma`: is the diffusion coefficient.
//! - `t`: time to check price at
//! - `maturity`: time at bond maturity
use crate::instruments::Instrument;
use crate::math::integrate;
use crate::time::{DayCountConvention, DayCounter};
use time::OffsetDateTime;
/// Struct containing the Hull-White model parameters.
pub struct HullWhite {
a: f64,
theta_t: fn(f64) -> f64,
sigma: f64,
r_t: f64,
/// `evaluation_date` - Valuation date.
pub evaluation_date: Option<OffsetDateTime>,
/// `expiration_date` - Expiry date.
pub expiration_date: OffsetDateTime,
}
impl HullWhite {
// TODO make dependenont t,T
fn B(&self) -> f64 {
assert!(self.a > 0.0);
(1.0 / self.a) * (1.0 - (-self.a).exp())
}
// TODO make dependenont t,T
fn A(&self) -> f64 {
assert!(self.a > 0.0);
let today = OffsetDateTime::now_utc();
let t = (self.evaluation_date.unwrap_or(today).year() - today.year()) as f64;
let T = (self.expiration_date.year() - today.year()) as f64;
let first = -1.0 * integrate(|u| (self.theta_t)(u) * self.B(), t, T);
let second = ((self.sigma).powi(2) / (2.0 * (self.a).powi(2))) * (self.B() - self.tau());
let third = ((self.sigma).powi(2) / (4.0 * self.a)) * (self.B()).powi(2);
(first - second - third).exp()
}
fn tau(&self) -> f64 {
DayCounter::day_count_factor(
self.evaluation_date.unwrap_or(OffsetDateTime::now_utc()),
self.expiration_date,
&DayCountConvention::Actual365,
)
}
}
impl Instrument for HullWhite {
fn price(&self) -> f64 {
assert!(self.a > 0.0);
assert!(self.expiration_date >= self.evaluation_date.unwrap_or(OffsetDateTime::now_utc()));
self.A() * (-1.0 * self.B() * self.r_t).exp()
}
fn error(&self) -> Option<f64> {
None
}
fn valuation_date(&self) -> time::OffsetDateTime {
self.evaluation_date.unwrap_or(OffsetDateTime::now_utc())
}
fn instrument_type(&self) -> &'static str {
"Zero Coupon Bond"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hw_zero_coupon_bond() {
let hw_bond = HullWhite {
a: 2.0,
theta_t: |_x| 0.5,
sigma: 0.3,
r_t: 0.05,
evaluation_date: None,
expiration_date: OffsetDateTime::now_utc() + time::Duration::days(365 * 10),
};
let _price = hw_bond.price();
// TODO check price against actual
// But this implementation is analytic, so should be right
}
}