#[derive(Clone, Debug, PartialEq)]
pub struct PiecewiseConstant {
pub knots: Vec<f64>,
pub values: Vec<f64>,
}
impl PiecewiseConstant {
pub fn new(knots: Vec<f64>, values: Vec<f64>) -> Self {
assert!(knots.len() >= 2, "need at least one segment");
assert_eq!(
values.len() + 1,
knots.len(),
"values.len() must be knots.len() − 1"
);
assert!((knots[0]).abs() < 1.0e-15, "first knot must be 0");
for i in 1..knots.len() {
assert!(knots[i] > knots[i - 1], "knots must be strictly increasing");
}
Self { knots, values }
}
pub fn constant(t: f64, v: f64) -> Self {
Self::new(vec![0.0, t], vec![v])
}
pub fn n_segments(&self) -> usize {
self.values.len()
}
pub fn final_time(&self) -> f64 {
*self.knots.last().unwrap()
}
pub fn at(&self, t: f64) -> f64 {
assert!(t >= 0.0 && t <= self.final_time() + 1.0e-15);
if t <= self.knots[0] {
return self.values[0];
}
for k in 1..self.knots.len() {
if t <= self.knots[k] + 1.0e-15 {
return self.values[k - 1];
}
}
*self.values.last().unwrap()
}
}
fn exp_seg(c: f64, delta: f64) -> f64 {
let cd = c * delta;
if cd.abs() < 1.0e-8 {
delta * (1.0 + 0.5 * cd + cd * cd / 6.0 + cd * cd * cd / 24.0)
} else {
cd.exp_m1() / c
}
}
fn exp_seg_diff_kernel(gamma_sq: f64, delta: f64) -> f64 {
if (gamma_sq * delta).abs() < 1.0e-4 {
let g = gamma_sq;
let d = delta;
0.5 * d * d + (7.0 / 6.0) * g * d * d * d + (43.0 / 24.0) * g * g * d * d * d * d
} else {
let e6 = exp_seg(6.0 * gamma_sq, delta);
let e1 = exp_seg(gamma_sq, delta);
(e6 - e1) / (5.0 * gamma_sq)
}
}
fn aligned_segments(
gamma: &PiecewiseConstant,
omega1: &PiecewiseConstant,
expiry: f64,
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
assert_eq!(
gamma.knots, omega1.knots,
"γ and ω₁ schedules must share the same knots (align in caller)"
);
assert!(
gamma.knots.iter().any(|&t| (t - expiry).abs() < 1.0e-12),
"expiry {} must be one of the schedule knots {:?}",
expiry,
gamma.knots
);
let n: usize = gamma
.knots
.iter()
.take_while(|&&t| t <= expiry + 1.0e-12)
.count()
- 1;
let deltas: Vec<f64> = (0..n)
.map(|k| gamma.knots[k + 1] - gamma.knots[k])
.collect();
let gs: Vec<f64> = gamma.values[..n].to_vec();
let os: Vec<f64> = omega1.values[..n].to_vec();
(deltas, gs, os)
}
fn i_integral(deltas: &[f64], gamma: &[f64], omega1: &[f64]) -> f64 {
let mut g_prev = 0.0_f64;
let mut total = 0.0_f64;
for k in 0..deltas.len() {
let g_sq = gamma[k] * gamma[k];
let o_sq = omega1[k] * omega1[k];
total += o_sq * g_prev.exp() * exp_seg(g_sq, deltas[k]);
g_prev += g_sq * deltas[k];
}
total
}
fn lhs_double_integral(deltas: &[f64], gamma: &[f64], omega1: &[f64]) -> f64 {
let mut g_prev = 0.0_f64; let mut h_prev = 0.0_f64; let mut total = 0.0_f64;
for k in 0..deltas.len() {
let g_sq = gamma[k] * gamma[k];
let o_sq = omega1[k] * omega1[k];
let o4 = o_sq * o_sq;
let delta = deltas[k];
let piece_a = o_sq * g_prev.exp() * h_prev * exp_seg(g_sq, delta);
let piece_b = o4 * (6.0 * g_prev).exp() * exp_seg_diff_kernel(g_sq, delta);
total += piece_a + piece_b;
h_prev += o_sq * (5.0 * g_prev).exp() * exp_seg(5.0 * g_sq, delta);
g_prev += g_sq * delta;
}
total
}
fn rhs_at_gamma_tilde(gamma_tilde_sq: f64, expiry: f64, i_val: f64) -> f64 {
let x = gamma_tilde_sq * expiry;
let denom = x.exp_m1();
if denom.abs() < 1.0e-300 {
return 0.5 * i_val * i_val;
}
let inner = if x.abs() < 1.0e-3 {
let x2 = x * x;
2.5 * x2 + (35.0 / 6.0) * x2 * x + (215.0 / 24.0) * x2 * x2 + (1295.0 / 120.0) * x2 * x2 * x
} else {
(6.0 * x).exp_m1() / 6.0 - x.exp_m1()
};
(i_val * i_val) / (5.0 * denom * denom) * inner
}
pub fn effective_vol_vol(
gamma: &PiecewiseConstant,
omega1: &PiecewiseConstant,
expiry: f64,
) -> f64 {
let (deltas, gs, os) = aligned_segments(gamma, omega1, expiry);
let i_val = i_integral(&deltas, &gs, &os);
let lhs = lhs_double_integral(&deltas, &gs, &os);
let rhs_at_zero = 0.5 * i_val * i_val;
if lhs <= rhs_at_zero * (1.0 + 1.0e-12) {
return 0.0;
}
let mut lo = 0.0_f64;
let mut hi = 4.0_f64; let mut f_hi = rhs_at_gamma_tilde(hi, expiry, i_val) - lhs;
let mut expansions = 0;
while f_hi < 0.0 && expansions < 20 {
hi *= 2.0;
f_hi = rhs_at_gamma_tilde(hi, expiry, i_val) - lhs;
expansions += 1;
}
assert!(
f_hi >= 0.0,
"effective γ̃ root not bracketed (LHS = {}, RHS({}) = {})",
lhs,
hi,
f_hi + lhs
);
for _ in 0..60 {
let mid = 0.5 * (lo + hi);
let f_mid = rhs_at_gamma_tilde(mid, expiry, i_val) - lhs;
if f_mid > 0.0 {
hi = mid;
} else {
lo = mid;
}
if hi - lo < 1.0e-14 {
break;
}
}
(0.5 * (lo + hi)).sqrt()
}
pub fn effective_term_structure(
gamma: &PiecewiseConstant,
omega1: &PiecewiseConstant,
expiry: f64,
) -> f64 {
let (deltas, gs, os) = aligned_segments(gamma, omega1, expiry);
let i_val = i_integral(&deltas, &gs, &os);
let gamma_tilde = effective_vol_vol(gamma, omega1, expiry);
let x = gamma_tilde * gamma_tilde * expiry;
let omega_sq = if x.abs() < 1.0e-6 {
i_val / expiry
} else {
gamma_tilde * gamma_tilde * i_val / x.exp_m1()
};
assert!(
omega_sq >= 0.0,
"ω̃₁² = {} (γ̃ = {}, I = {})",
omega_sq,
gamma_tilde,
i_val
);
omega_sq.sqrt()
}
pub fn effective_correlation(
gamma: &PiecewiseConstant,
omega1: &PiecewiseConstant,
rho: &PiecewiseConstant,
expiry: f64,
) -> f64 {
assert_eq!(
gamma.knots, rho.knots,
"γ and ρ schedules must share the same knots"
);
let (deltas, gs, os) = aligned_segments(gamma, omega1, expiry);
let rs: Vec<f64> = rho.values[..deltas.len()].to_vec();
let mut integral = 0.0_f64;
for k in 0..deltas.len() {
let denom = os[k];
assert!(
denom.abs() > 1.0e-15,
"ω₁ must be non-zero on every segment (got {} at k={})",
denom,
k
);
integral += rs[k] * gs[k] / denom * deltas[k];
}
let gamma_tilde = effective_vol_vol(gamma, omega1, expiry);
let omega_tilde = effective_term_structure(gamma, omega1, expiry);
if gamma_tilde.abs() < 1.0e-15 {
return 0.0;
}
(omega_tilde / (gamma_tilde * expiry)) * integral
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constant_gamma_recovers_itself() {
for &gamma0 in &[0.01_f64, 0.1, 0.3, 0.5, 0.8, 1.0, 1.5] {
for &omega0 in &[0.05_f64, 0.15, 0.30] {
let g = PiecewiseConstant::constant(2.0, gamma0);
let w = PiecewiseConstant::constant(2.0, omega0);
let g_tilde = effective_vol_vol(&g, &w, 2.0);
assert!(
(g_tilde - gamma0).abs() < 1.0e-6,
"γ₀={}, ω={}: γ̃ = {}",
gamma0,
omega0,
g_tilde
);
}
}
}
#[test]
fn zero_gamma_gives_zero_effective() {
let g = PiecewiseConstant::new(vec![0.0, 1.0, 2.0], vec![0.0, 0.0]);
let w = PiecewiseConstant::new(vec![0.0, 1.0, 2.0], vec![0.10, 0.15]);
let g_tilde = effective_vol_vol(&g, &w, 2.0);
assert!(g_tilde.abs() < 1.0e-10, "γ̃ = {}", g_tilde);
}
#[test]
fn paper_table_1_schedule_invariants() {
let knots = vec![0.0, 0.5, 1.0, 2.0, 3.0, 5.0];
let gammas = vec![1.0_f64, 0.8, 0.5, 0.3, 0.2];
let g = PiecewiseConstant::new(knots.clone(), gammas.clone());
let w = PiecewiseConstant::new(knots.clone(), vec![0.15; 5]);
let gt_half = effective_vol_vol(&g, &w, 0.5);
assert!(
(gt_half - 1.0).abs() < 1.0e-6,
"T=0.5: γ̃ = {}, expected 1",
gt_half
);
let ts = [0.5_f64, 1.0, 2.0, 3.0, 5.0];
let mut prev = f64::INFINITY;
for &t in &ts {
let gt = effective_vol_vol(&g, &w, t);
assert!(
gt <= prev + 1.0e-12,
"γ̃ not monotone at T={}: prev={}, got={}",
t,
prev,
gt
);
prev = gt;
}
for t_idx in 1..knots.len() {
let t = knots[t_idx];
let mut int_g2 = 0.0;
for k in 0..t_idx {
int_g2 += gammas[k] * gammas[k] * (knots[k + 1] - knots[k]);
}
let lower = (int_g2 / t).sqrt();
let gt = effective_vol_vol(&g, &w, t);
assert!(
gt >= lower - 1.0e-10,
"Jensen floor violated at T={}: γ̃={}, √⟨γ²⟩={}",
t,
gt,
lower
);
}
}
#[test]
fn non_constant_gamma_has_omega_dependence() {
let knots = vec![0.0, 1.0, 2.0];
let g = PiecewiseConstant::new(knots.clone(), vec![1.0, 0.3]);
let w1 = PiecewiseConstant::new(knots.clone(), vec![0.10, 0.10]);
let w2 = PiecewiseConstant::new(knots, vec![0.10, 0.30]);
let gt1 = effective_vol_vol(&g, &w1, 2.0);
let gt2 = effective_vol_vol(&g, &w2, 2.0);
assert!(
(gt1 - gt2).abs() > 1.0e-4,
"γ̃ should differ for different ω schedules: {} vs {}",
gt1,
gt2
);
}
#[test]
fn omega_consistency_identity_holds() {
let knots = vec![0.0, 0.5, 1.0, 2.0, 3.0, 5.0];
let g = PiecewiseConstant::new(knots.clone(), vec![1.0, 0.8, 0.5, 0.3, 0.2]);
let w = PiecewiseConstant::new(knots.clone(), vec![0.15; 5]);
for &t_idx in &[1_usize, 3, 5] {
let t = knots[t_idx];
let g_tilde = effective_vol_vol(&g, &w, t);
let (deltas, gs, os) = aligned_segments(&g, &w, t);
let i_val = i_integral(&deltas, &gs, &os);
let x = g_tilde * g_tilde * t;
let omega1_sq = if x.abs() < 1.0e-6 {
i_val / t
} else {
g_tilde * g_tilde * i_val / x.exp_m1()
};
assert!(
omega1_sq > 0.0,
"T={}: γ̃ = {}, ω̃₁² = {}",
t,
g_tilde,
omega1_sq
);
}
}
#[test]
fn tiny_gamma_regression_no_blowup() {
let knots = vec![0.0, 0.5, 1.0];
let g = PiecewiseConstant::new(knots.clone(), vec![1.0e-3, 1.0e-3]);
let w = PiecewiseConstant::new(knots, vec![0.15, 0.15]);
let gt = effective_vol_vol(&g, &w, 1.0);
assert!(
gt.is_finite() && gt.abs() < 1.0e-2,
"tiny-γ effective: {}",
gt
);
assert!((gt - 1.0e-3).abs() < 1.0e-5, "tiny-γ value: γ̃ = {}", gt);
}
#[test]
#[should_panic(expected = "same knots")]
fn misaligned_knots_panic() {
let g = PiecewiseConstant::new(vec![0.0, 1.0, 2.0], vec![0.5, 0.5]);
let w = PiecewiseConstant::new(vec![0.0, 0.5, 2.0], vec![0.1, 0.1]);
let _ = effective_vol_vol(&g, &w, 2.0);
}
#[test]
fn effective_term_structure_identity_on_fully_constant_schedule() {
for &gamma0 in &[0.2_f64, 0.5, 1.0] {
for &omega0 in &[0.05_f64, 0.15, 0.30] {
let g = PiecewiseConstant::constant(2.0, gamma0);
let w = PiecewiseConstant::constant(2.0, omega0);
let omega_tilde = effective_term_structure(&g, &w, 2.0);
assert!(
(omega_tilde - omega0).abs() < 1.0e-10,
"(γ₀={}, ω₀={}): ω̃ = {}",
gamma0,
omega0,
omega_tilde
);
}
}
}
#[test]
fn effective_term_structure_near_identity_on_constant_omega() {
let knots = vec![0.0, 0.5, 1.0, 2.0];
let g = PiecewiseConstant::new(knots.clone(), vec![1.0, 0.5, 0.3]);
for &omega0 in &[0.05_f64, 0.15, 0.30] {
let w = PiecewiseConstant::new(knots.clone(), vec![omega0; 3]);
let omega_tilde = effective_term_structure(&g, &w, 2.0);
let rel = (omega_tilde - omega0).abs() / omega0;
assert!(
rel < 5.0e-2,
"ω₀={}: ω̃ = {} (rel err {:.4})",
omega0,
omega_tilde,
rel
);
}
}
#[test]
fn effective_term_structure_zero_gamma_is_rms() {
let knots = vec![0.0, 1.0, 2.0];
let g = PiecewiseConstant::new(knots.clone(), vec![0.0, 0.0]);
let w = PiecewiseConstant::new(knots.clone(), vec![0.1, 0.2]);
let omega_tilde = effective_term_structure(&g, &w, 2.0);
let expected = (0.025_f64).sqrt();
assert!(
(omega_tilde - expected).abs() < 1.0e-10,
"ω̃={}, expected {}",
omega_tilde,
expected
);
}
#[test]
fn effective_term_structure_brackets_omega_range() {
let knots = vec![0.0, 0.5, 1.0, 2.0];
let g = PiecewiseConstant::new(knots.clone(), vec![0.6, 0.5, 0.4]);
let w = PiecewiseConstant::new(knots.clone(), vec![0.10, 0.15, 0.20]);
let omega_tilde = effective_term_structure(&g, &w, 2.0);
assert!(
(0.10 - 1.0e-12..=0.20 + 1.0e-12).contains(&omega_tilde),
"ω̃ = {} not in [0.10, 0.20]",
omega_tilde
);
}
#[test]
fn effective_correlation_identity_on_fully_constant_schedule() {
for &rho0 in &[-0.8_f64, -0.3, 0.0, 0.2, 0.7] {
let g = PiecewiseConstant::constant(2.0, 0.5);
let w = PiecewiseConstant::constant(2.0, 0.15);
let r = PiecewiseConstant::constant(2.0, rho0);
let rho_tilde = effective_correlation(&g, &w, &r, 2.0);
assert!(
(rho_tilde - rho0).abs() < 1.0e-10,
"ρ₀={}: ρ̃ = {}",
rho0,
rho_tilde
);
}
}
#[test]
fn effective_correlation_flips_with_rho_sign() {
let knots = vec![0.0, 0.5, 1.0, 2.0];
let g = PiecewiseConstant::new(knots.clone(), vec![1.0, 0.7, 0.4]);
let w = PiecewiseConstant::new(knots.clone(), vec![0.10, 0.12, 0.14]);
let r_pos = PiecewiseConstant::new(knots.clone(), vec![0.20, 0.35, 0.50]);
let r_neg = PiecewiseConstant::new(knots.clone(), vec![-0.20, -0.35, -0.50]);
let p = effective_correlation(&g, &w, &r_pos, 2.0);
let n = effective_correlation(&g, &w, &r_neg, 2.0);
assert!(
(p + n).abs() < 1.0e-12,
"ρ flip asymmetry: + {} vs − {}",
p,
n
);
}
#[test]
fn effective_correlation_is_weighted_average() {
let knots = vec![0.0, 1.0, 2.0, 3.0];
let g = PiecewiseConstant::new(knots.clone(), vec![0.8, 0.5, 0.3]);
let w = PiecewiseConstant::new(knots.clone(), vec![0.15, 0.12, 0.10]);
let r = PiecewiseConstant::new(knots.clone(), vec![-0.7, -0.4, -0.1]);
let rho_tilde = effective_correlation(&g, &w, &r, 3.0);
assert!(
(-0.7 - 1.0e-12..=-0.1 + 1.0e-12).contains(&rho_tilde),
"ρ̃ = {} not in [−0.7, −0.1]",
rho_tilde
);
}
}