use crate::models::common::cir::CirProcess;
use crate::models::forex::fx_fmm::{FxFmmParams, compute_a_d, compute_a_f};
use num_complex::Complex64;
#[derive(Copy, Clone, Debug)]
pub struct ChfComponents {
pub a: Complex64,
pub b: Complex64, pub c: Complex64,
pub d_d: Complex64,
pub d_f: Complex64,
}
impl ChfComponents {
pub fn assemble(&self, x: f64, sigma: f64, v_d: f64, v_f: f64) -> Complex64 {
(self.a + self.b * x + self.c * sigma + self.d_d * v_d + self.d_f * v_f).exp()
}
}
#[derive(Clone, Debug)]
struct SubSegment {
tau: f64,
length: f64,
start_idx: usize,
a_d: f64,
a_f: f64,
}
pub struct FxFmm1ForwardChf<'a> {
params: &'a FxFmmParams,
pub expiry: f64,
sub_segments: Vec<SubSegment>,
psi_base_d: Vec<f64>,
psi_base_f: Vec<f64>,
phi_sigma_cir: CirProcess,
phi_d_cir: CirProcess,
phi_f_cir: CirProcess,
pub n_substeps_per_period: usize,
pub n_simpson_per_segment: usize,
}
impl<'a> FxFmm1ForwardChf<'a> {
pub fn new(params: &'a FxFmmParams, expiry: f64) -> Self {
Self::with_resolution(params, expiry, 1, 32)
}
pub fn with_resolution(
params: &'a FxFmmParams,
expiry: f64,
n_substeps_per_period: usize,
n_simpson_per_segment: usize,
) -> Self {
assert!(expiry > 0.0);
assert!(n_substeps_per_period >= 1);
assert!(
n_simpson_per_segment >= 2 && n_simpson_per_segment.is_multiple_of(2),
"n_simpson must be even and ≥ 2"
);
params.validate().expect("params must be valid");
let tenor = ¶ms.tenor;
let m = tenor.m();
let mut tau_boundaries: Vec<f64> = Vec::new();
for k in (0..=m - 1).rev() {
let tau_b = expiry - tenor.dates[k];
if tau_b > 0.0 && tau_b < expiry + 1e-12 {
tau_boundaries.push(tau_b);
}
}
if tau_boundaries
.last()
.is_none_or(|&t| (t - expiry).abs() > 1e-12)
{
tau_boundaries.push(expiry);
}
let mut sub_segments: Vec<SubSegment> = Vec::new();
let mut prev = 0.0_f64;
for &tau_j in &tau_boundaries {
let length = tau_j - prev;
if length <= 0.0 {
prev = tau_j;
continue;
}
let h = length / n_substeps_per_period as f64;
for i in 0..n_substeps_per_period {
let lo = prev + i as f64 * h;
let hi = if i + 1 == n_substeps_per_period {
tau_j
} else {
lo + h
};
let t_mid = expiry - 0.5 * (lo + hi);
let start_idx = tenor.eta(t_mid).min(m + 1);
let a_d = compute_a_d(params, t_mid, start_idx);
let a_f = compute_a_f(params, t_mid, start_idx);
sub_segments.push(SubSegment {
tau: hi,
length: hi - lo,
start_idx,
a_d,
a_f,
});
}
prev = tau_j;
}
let psi_base_d = params.domestic.psi_base(¶ms.tenor);
let psi_base_f = params.foreign.psi_base(¶ms.tenor);
let phi_d_cir = CirProcess {
kappa: params.domestic.lambda,
theta: params.domestic.v_0,
gamma: params.domestic.eta,
sigma_0: params.domestic.v_0,
};
let phi_f_cir = CirProcess {
kappa: params.foreign.lambda,
theta: params.foreign.v_0,
gamma: params.foreign.eta,
sigma_0: params.foreign.v_0,
};
Self {
params,
expiry,
sub_segments,
psi_base_d,
psi_base_f,
phi_sigma_cir: params.heston,
phi_d_cir,
phi_f_cir,
n_substeps_per_period,
n_simpson_per_segment,
}
}
pub fn params(&self) -> &'a FxFmmParams {
self.params
}
pub fn components(&self, u: Complex64) -> ChfComponents {
let p = self.params;
let b = Complex64::new(0.0, 1.0) * u;
let c = heston_c(
u,
p.heston.kappa,
p.heston.gamma,
p.correlations.rho_xi_sigma,
self.expiry,
);
let mut d_d = Complex64::new(0.0, 0.0);
let mut d_f = Complex64::new(0.0, 0.0);
let mut a = Complex64::new(0.0, 0.0);
let mut tau_prev = 0.0_f64;
for seg in &self.sub_segments {
let s_j = seg.length;
let tau_j = seg.tau;
let (d_d_new, delta_a_dd) = advance_d_riccati(
u,
p.domestic.lambda,
p.domestic.eta,
seg.a_d,
p.domestic.v_0,
d_d,
s_j,
);
let (d_f_new, delta_a_df) = advance_d_riccati(
u,
p.foreign.lambda,
p.foreign.eta,
seg.a_f,
p.foreign.v_0,
d_f,
s_j,
);
let delta_a_c = heston_a_increment(
u,
p.heston.kappa,
p.heston.theta,
p.heston.gamma,
p.correlations.rho_xi_sigma,
tau_prev,
tau_j,
);
let f_integral = self.simpson_f_integral(
tau_prev,
tau_j,
seg.start_idx,
self.n_simpson_per_segment.max(2),
);
let u2_plus_iu = u * u + Complex64::new(0.0, 1.0) * u;
let delta_a_f = -0.5 * u2_plus_iu * f_integral;
d_d = d_d_new;
d_f = d_f_new;
a += delta_a_c + delta_a_dd + delta_a_df + delta_a_f;
tau_prev = tau_j;
}
ChfComponents { a, b, c, d_d, d_f }
}
pub fn evaluate(&self, u: Complex64) -> Complex64 {
let p = self.params;
let comps = self.components(u);
let (pd_0t, pf_0t) = discount_factors_from_rates(p, self.expiry);
let x0 = (p.fx_0 * pf_0t / pd_0t).ln();
let v_d0 = p.domestic.v_0;
let v_f0 = p.foreign.v_0;
comps.assemble(x0, p.heston.sigma_0, v_d0, v_f0)
}
}
fn heston_c(u: Complex64, kappa: f64, gamma: f64, rho: f64, tau: f64) -> Complex64 {
if tau <= 0.0 {
return Complex64::new(0.0, 0.0);
}
let kappa_c = Complex64::new(kappa, 0.0);
let gamma_c = Complex64::new(gamma, 0.0);
let rho_c = Complex64::new(rho, 0.0);
let iu = Complex64::new(0.0, 1.0) * u;
let b2_minus_b = iu * iu - iu;
let x = kappa_c - gamma_c * rho_c * iu;
let d = (x * x - gamma_c * gamma_c * iu * (iu - Complex64::new(1.0, 0.0))).sqrt();
let e = (-d * tau).exp();
b2_minus_b * (Complex64::new(1.0, 0.0) - e) / (x + d - (x - d) * e)
}
fn heston_a_increment(
u: Complex64,
kappa: f64,
theta: f64,
gamma: f64,
rho: f64,
tau1: f64,
tau2: f64,
) -> Complex64 {
heston_a_closed_form(u, kappa, theta, gamma, rho, tau2)
- heston_a_closed_form(u, kappa, theta, gamma, rho, tau1)
}
fn heston_a_closed_form(
u: Complex64,
kappa: f64,
theta: f64,
gamma: f64,
rho: f64,
tau: f64,
) -> Complex64 {
if tau <= 0.0 {
return Complex64::new(0.0, 0.0);
}
let kappa_c = Complex64::new(kappa, 0.0);
let gamma_c = Complex64::new(gamma, 0.0);
let rho_c = Complex64::new(rho, 0.0);
let iu = Complex64::new(0.0, 1.0) * u;
let x = kappa_c - gamma_c * rho_c * iu;
let d = (x * x - gamma_c * gamma_c * iu * (iu - Complex64::new(1.0, 0.0))).sqrt();
let g = (x - d) / (x + d);
let e = (-d * tau).exp();
let kappa_theta = Complex64::new(kappa * theta, 0.0);
kappa_theta / (gamma_c * gamma_c)
* ((x - d) * tau
- 2.0 * ((Complex64::new(1.0, 0.0) - g * e) / (Complex64::new(1.0, 0.0) - g)).ln())
}
fn advance_d_riccati(
u: Complex64,
lambda: f64,
eta: f64,
a_j: f64,
v_0: f64,
d_prev: Complex64,
s: f64,
) -> (Complex64, Complex64) {
if s <= 0.0 {
return (d_prev, Complex64::new(0.0, 0.0));
}
let iu = Complex64::new(0.0, 1.0) * u;
let u2_plus_iu = u * u + iu;
let eta_sq = eta * eta;
let delta = (Complex64::new(lambda * lambda, 0.0)
+ Complex64::new(eta_sq * a_j, 0.0) * u2_plus_iu)
.sqrt();
let denom = Complex64::new(lambda, 0.0) + delta - Complex64::new(eta_sq, 0.0) * d_prev;
let num = Complex64::new(lambda, 0.0) - delta - Complex64::new(eta_sq, 0.0) * d_prev;
let ell = num / denom;
let e = (-delta * s).exp();
let one = Complex64::new(1.0, 0.0);
let chi = num * (one - e) / (Complex64::new(eta_sq, 0.0) * (one - ell * e));
let d_new = d_prev + chi;
let coef = Complex64::new(v_0 * lambda / eta_sq, 0.0);
let delta_a = coef
* ((Complex64::new(lambda, 0.0) - delta) * s - 2.0 * ((one - ell * e) / (one - ell)).ln());
(d_new, delta_a)
}
impl<'a> FxFmm1ForwardChf<'a> {
fn simpson_f_integral(&self, tau1: f64, tau2: f64, start_idx: usize, n: usize) -> Complex64 {
let h = (tau2 - tau1) / n as f64;
let mut acc = 0.0_f64;
for k in 0..=n {
let s = tau1 + k as f64 * h;
let t = self.expiry - s;
let val = self.f_at(t, start_idx);
let w = if k == 0 || k == n {
1.0
} else if k % 2 == 1 {
4.0
} else {
2.0
};
acc += w * val;
}
Complex64::new(acc * h / 3.0, 0.0)
}
fn f_at(&self, t: f64, start_idx: usize) -> f64 {
let tenor = &self.params.tenor;
let m = tenor.m();
if start_idx > m {
return 0.0;
}
let decay_d = &self.params.domestic.decay;
let decay_f = &self.params.foreign.decay;
let phi_xi = self.phi_sigma_cir.sqrt_mean(t);
let phi_d = self.phi_d_cir.sqrt_mean(t);
let phi_f = self.phi_f_cir.sqrt_mean(t);
let mut two_ab = 0.0_f64;
for j in start_idx..=m {
let idx = j - 1;
let gamma = decay_d.gamma(j, t, tenor);
two_ab += self.psi_base_d[idx] * gamma * self.params.correlations.rho_xi_d[idx];
}
two_ab *= 2.0 * phi_xi * phi_d;
let mut two_ac = 0.0_f64;
for j in start_idx..=m {
let idx = j - 1;
let gamma = decay_f.gamma(j, t, tenor);
two_ac += self.psi_base_f[idx] * gamma * self.params.correlations.rho_xi_f[idx];
}
two_ac *= 2.0 * phi_xi * phi_f;
let mut two_bc = 0.0_f64;
for j in start_idx..=m {
let idx_j = j - 1;
let gj = decay_d.gamma(j, t, tenor);
for k in start_idx..=m {
let idx_k = k - 1;
let gk = decay_f.gamma(k, t, tenor);
two_bc += self.psi_base_d[idx_j]
* gj
* self.psi_base_f[idx_k]
* gk
* self.params.correlations.cross_rate_corr[idx_j][idx_k];
}
}
two_bc *= 2.0 * phi_d * phi_f;
two_ab - two_ac - two_bc
}
}
fn discount_factors_from_rates(params: &FxFmmParams, expiry: f64) -> (f64, f64) {
let tenor = ¶ms.tenor;
let tn = tenor.dates[tenor.m()];
let mut p = 1.0_f64;
for k in 1..=tenor.m() {
p /= 1.0 + tenor.tau(k) * tenor.initial_rates[k - 1];
}
if expiry > tn + 1e-12 {
let r_flat = tenor.initial_rates[tenor.m() - 1];
p *= (-r_flat * (expiry - tn)).exp();
}
(p, p)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::common::cir::CirProcess;
use crate::models::forex::fx_fmm::{FmmSide, FxFmmCorrelations, FxFmmParams};
use crate::models::interestrate::fmm::{FmmTenor, LinearDecay};
fn identity_like_corr(n: usize) -> Vec<Vec<f64>> {
let mut m = vec![vec![0.0; n]; n];
for (i, row) in m.iter_mut().enumerate().take(n) {
for (j, v) in row.iter_mut().enumerate() {
*v = if i == j { 1.0 } else { 0.9 };
}
}
m
}
fn toy_params(tenor_dates: Vec<f64>, rates: Vec<f64>) -> FxFmmParams {
let tenor = FmmTenor::new(tenor_dates, rates.clone());
let m = tenor.m();
let side = |sigma_level: f64| FmmSide {
sigmas: vec![sigma_level; m],
lambda: 1.0,
eta: 0.1,
v_0: 1.0,
rate_corr: identity_like_corr(m),
decay: LinearDecay,
};
FxFmmParams {
fx_0: 1.35,
heston: CirProcess {
kappa: 0.5,
theta: 0.1,
gamma: 0.3,
sigma_0: 0.1,
},
tenor,
domestic: side(0.15),
foreign: side(0.15),
correlations: FxFmmCorrelations {
rho_xi_sigma: -0.4,
rho_xi_d: vec![-0.15; m],
rho_xi_f: vec![-0.15; m],
rho_sigma_d: vec![0.30; m],
rho_sigma_f: vec![0.30; m],
cross_rate_corr: vec![vec![0.25; m]; m],
},
}
}
#[test]
fn chf_at_zero_frequency_is_one() {
let p = toy_params(vec![0.0, 0.5, 1.0], vec![0.03, 0.03]);
let chf = FxFmm1ForwardChf::new(&p, 1.0);
let v = chf.evaluate(Complex64::new(0.0, 0.0));
assert!(
(v.re - 1.0).abs() < 1e-10 && v.im.abs() < 1e-10,
"φ(0) = {} + {}i",
v.re,
v.im
);
}
#[test]
fn reduces_to_pure_heston_when_fmm_vol_is_zero() {
let mut p = toy_params(vec![0.0, 0.5, 1.0], vec![0.03, 0.03]);
p.domestic.sigmas = vec![0.0; 2];
p.foreign.sigmas = vec![0.0; 2];
p.domestic.eta = 1e-12;
p.foreign.eta = 1e-12;
p.correlations.rho_xi_d = vec![0.0; 2];
p.correlations.rho_xi_f = vec![0.0; 2];
p.correlations.cross_rate_corr = vec![vec![0.0; 2]; 2];
let chf = FxFmm1ForwardChf::new(&p, 1.0);
let comps = chf.components(Complex64::new(0.5, 0.1));
assert!(
comps.d_d.norm() < 1e-6,
"D_d should be ~0 in zero-FMM-vol limit: {}",
comps.d_d
);
assert!(
comps.d_f.norm() < 1e-6,
"D_f should be ~0 in zero-FMM-vol limit: {}",
comps.d_f
);
}
#[test]
fn chf_modulus_decreases_with_maturity() {
let p = toy_params(vec![0.0, 0.5, 1.0, 1.5, 2.0], vec![0.03; 4]);
let u = Complex64::new(1.0, 0.0);
let mut prev = f64::INFINITY;
for &t in &[0.5_f64, 1.0, 1.5, 2.0] {
let chf = FxFmm1ForwardChf::new(&p, t);
let v = chf.evaluate(u).norm();
assert!(v < prev, "T={}: |φ|={} should be < prev {}", t, v, prev);
prev = v;
}
}
#[test]
fn sub_segment_count_reflects_subdivision() {
let p = toy_params(vec![0.0, 0.3, 0.7, 1.0], vec![0.03, 0.03, 0.03]);
let default_chf = FxFmm1ForwardChf::new(&p, 1.0);
assert_eq!(default_chf.sub_segments.len(), 3);
let fine_chf = FxFmm1ForwardChf::with_resolution(&p, 1.0, 4, 32);
assert_eq!(fine_chf.sub_segments.len(), 3 * 4);
}
}