use std::f64::consts::PI;
pub struct VasicekModel {
pub a: f64,
pub b: f64,
pub sigma: f64,
pub r0: f64,
}
impl VasicekModel {
pub fn new(a: f64, b: f64, sigma: f64, r0: f64) -> Self {
VasicekModel { a, b, sigma, r0 }
}
pub fn expected_rate(&self, t: f64) -> f64 {
self.b + (self.r0 - self.b) * (-self.a * t).exp()
}
pub fn variance(&self, t: f64) -> f64 {
self.sigma * self.sigma / (2.0 * self.a) * (1.0 - (-2.0 * self.a * t).exp())
}
pub fn bond_price(&self, t: f64) -> f64 {
let b_t = (1.0 - (-self.a * t).exp()) / self.a;
let a_t = ((self.b - self.sigma*self.sigma / (2.0*self.a*self.a)) * (b_t - t)
- self.sigma*self.sigma * b_t*b_t / (4.0*self.a)).exp();
a_t * (-b_t * self.r0).exp()
}
pub fn simulate(&self, dt: f64, steps: usize, seed: u64) -> Vec<f64> {
let mut r = self.r0;
let mut path = vec![r];
let mut state = seed;
let sqrt_dt = dt.sqrt();
for _ in 0..steps {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
let u1 = (state >> 11) as f64 / (1u64 << 53) as f64;
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
let u2 = (state >> 11) as f64 / (1u64 << 53) as f64;
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos();
r += self.a * (self.b - r) * dt + self.sigma * sqrt_dt * z;
path.push(r);
}
path
}
}
pub struct CirModel {
pub a: f64,
pub b: f64,
pub sigma: f64,
pub r0: f64,
}
impl CirModel {
pub fn new(a: f64, b: f64, sigma: f64, r0: f64) -> Self {
CirModel { a, b, sigma, r0 }
}
pub fn feller_condition(&self) -> bool {
2.0 * self.a * self.b >= self.sigma * self.sigma
}
pub fn expected_rate(&self, t: f64) -> f64 {
self.r0 * (-self.a * t).exp() + self.b * (1.0 - (-self.a * t).exp())
}
pub fn bond_price(&self, t: f64) -> f64 {
let gamma = (self.a*self.a + 2.0*self.sigma*self.sigma).sqrt();
let denom = (gamma + self.a) * ((-gamma * t).exp() - 1.0) + 2.0 * gamma;
let b_t = 2.0 * ((-gamma * t).exp() - 1.0) / denom;
let a_t_ln = 2.0 * self.a * self.b / (self.sigma * self.sigma)
* (2.0 * gamma * (((self.a + gamma) * t / 2.0).exp()) / denom).ln();
a_t_ln.exp() * (-b_t * self.r0).exp()
}
pub fn simulate(&self, dt: f64, steps: usize, seed: u64) -> Vec<f64> {
let mut r = self.r0.max(0.0);
let mut path = vec![r];
let mut state = seed;
let sqrt_dt = dt.sqrt();
for _ in 0..steps {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
let u1 = ((state >> 11) as f64 / (1u64 << 53) as f64).max(1e-10);
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
let u2 = (state >> 11) as f64 / (1u64 << 53) as f64;
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos();
let dr = self.a * (self.b - r) * dt + self.sigma * r.max(0.0).sqrt() * sqrt_dt * z;
r = (r + dr).max(0.0);
path.push(r);
}
path
}
}
pub struct HullWhiteModel {
pub a: f64,
pub sigma: f64,
pub r0: f64,
pub theta_params: Vec<(f64, f64)>,
}
impl HullWhiteModel {
pub fn new(a: f64, sigma: f64, r0: f64) -> Self {
HullWhiteModel { a, sigma, r0, theta_params: Vec::new() }
}
pub fn theta_at(&self, t: f64) -> f64 {
self.theta_params.iter()
.filter(|(ti, _)| *ti <= t)
.last()
.map(|(_, th)| *th)
.unwrap_or(self.a * 0.05) }
pub fn add_theta(mut self, t: f64, theta: f64) -> Self {
self.theta_params.push((t, theta));
self.theta_params.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
self
}
pub fn calibrate_to_yield_curve(a: f64, sigma: f64, r0: f64,
curve_points: &[(f64, f64)]) -> Self {
let mut hw = HullWhiteModel::new(a, sigma, r0);
for window in curve_points.windows(2) {
let (t1, r1) = window[0];
let (t2, r2) = window[1];
let fwd = (r2*t2 - r1*t1) / (t2 - t1); let t_mid = (t1 + t2) / 2.0;
let theta = a * fwd + sigma*sigma / (2.0*a) * (1.0 - (-2.0*a*t_mid).exp());
hw.theta_params.push((t_mid, theta));
}
hw
}
pub fn bond_price(&self, t: f64) -> f64 {
let b_t = (1.0 - (-self.a * t).exp()) / self.a;
let theta_avg = if self.theta_params.is_empty() { self.a * 0.05 }
else { self.theta_params.iter().map(|(_, th)| th).sum::<f64>() / self.theta_params.len() as f64 };
let ln_a = theta_avg / self.a * (b_t - t)
- self.sigma*self.sigma / (4.0*self.a) * b_t*b_t;
ln_a.exp() * (-b_t * self.r0).exp()
}
pub fn simulate(&self, dt: f64, steps: usize, seed: u64) -> Vec<f64> {
let mut r = self.r0;
let mut path = vec![r];
let mut state = seed;
let sqrt_dt = dt.sqrt();
let mut t = 0.0;
for _ in 0..steps {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
let u1 = ((state >> 11) as f64 / (1u64 << 53) as f64).max(1e-10);
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
let u2 = (state >> 11) as f64 / (1u64 << 53) as f64;
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos();
let theta = self.theta_at(t);
r += (theta - self.a * r) * dt + self.sigma * sqrt_dt * z;
path.push(r);
t += dt;
}
path
}
}
pub fn vasicek_caplet_price(model: &VasicekModel, strike: f64, t: f64, tau: f64) -> f64 {
let p_t = model.bond_price(t);
let p_ttau = model.bond_price(t + tau);
let b_tau = (1.0 - (-model.a * tau).exp()) / model.a;
let sigma_p = model.sigma * b_tau * ((1.0 - (-2.0*model.a*t).exp()) / (2.0*model.a)).sqrt();
if sigma_p < 1e-10 { return 0.0; }
let h = (p_ttau / (p_t * (1.0 + strike * tau))).ln() / sigma_p + sigma_p / 2.0;
let nd1 = norm_cdf(h);
let nd2 = norm_cdf(h - sigma_p);
p_ttau * nd1 - p_t * (1.0 + strike * tau) * nd2
}
fn norm_cdf(x: f64) -> f64 {
0.5 * (1.0 + libm_erf(x / std::f64::consts::SQRT_2))
}
fn libm_erf(x: f64) -> f64 {
let t = 1.0 / (1.0 + 0.3275911 * x.abs());
let poly = t * (0.254829592 + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
let result = 1.0 - poly * (-(x*x)).exp();
if x >= 0.0 { result } else { -result }
}