use crate::surface::EXPIRY_MATCH_TOL;
pub(crate) fn interpolate_theta_forward(
tenors: &[f64],
thetas: &[f64],
forwards: &[f64],
expiry: f64,
) -> (f64, f64) {
debug_assert!(!tenors.is_empty(), "tenors must not be empty");
debug_assert_eq!(tenors.len(), thetas.len());
debug_assert_eq!(tenors.len(), forwards.len());
let n = tenors.len();
for (i, &t) in tenors.iter().enumerate() {
if (expiry - t).abs() < EXPIRY_MATCH_TOL {
return (thetas[i], forwards[i]);
}
}
if expiry < tenors[0] {
let theta = thetas[0] * expiry / tenors[0];
return (theta, forwards[0]);
}
if expiry > tenors[n - 1] {
let theta = thetas[n - 1] * expiry / tenors[n - 1];
return (theta, forwards[n - 1]);
}
let right = tenors.partition_point(|&t| t < expiry);
let left = right - 1;
let alpha = (expiry - tenors[left]) / (tenors[right] - tenors[left]);
let theta = (1.0 - alpha) * thetas[left] + alpha * thetas[right];
let forward = (forwards[left].ln() * (1.0 - alpha) + forwards[right].ln() * alpha).exp();
(theta, forward)
}
#[cfg(test)]
mod tests {
use super::interpolate_theta_forward;
use approx::assert_abs_diff_eq;
fn multi_tenor() -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let tenors = vec![0.25, 0.5, 1.0, 2.0];
let thetas = vec![0.04, 0.08, 0.16, 0.32];
let forwards = vec![100.0, 102.0, 105.0, 110.0];
(tenors, thetas, forwards)
}
#[test]
fn single_element_exact() {
let (theta, fwd) = interpolate_theta_forward(&[0.5], &[0.08], &[100.0], 0.5);
assert_abs_diff_eq!(theta, 0.08, epsilon = 1e-14);
assert_abs_diff_eq!(fwd, 100.0, epsilon = 1e-14);
}
#[test]
fn single_element_before() {
let (theta, fwd) = interpolate_theta_forward(&[1.0], &[0.16], &[105.0], 0.5);
assert_abs_diff_eq!(theta, 0.08, epsilon = 1e-14);
assert_abs_diff_eq!(fwd, 105.0, epsilon = 1e-14);
}
#[test]
fn single_element_after() {
let (theta, fwd) = interpolate_theta_forward(&[0.5], &[0.08], &[100.0], 1.0);
assert_abs_diff_eq!(theta, 0.16, epsilon = 1e-14);
assert_abs_diff_eq!(fwd, 100.0, epsilon = 1e-14);
}
#[test]
fn multi_exact_match() {
let (t, th, f) = multi_tenor();
let (theta, fwd) = interpolate_theta_forward(&t, &th, &f, 1.0);
assert_abs_diff_eq!(theta, 0.16, epsilon = 1e-14);
assert_abs_diff_eq!(fwd, 105.0, epsilon = 1e-14);
}
#[test]
fn multi_extrapolate_left() {
let (t, th, f) = multi_tenor();
let (theta, fwd) = interpolate_theta_forward(&t, &th, &f, 0.1);
assert_abs_diff_eq!(theta, 0.016, epsilon = 1e-14);
assert_abs_diff_eq!(fwd, 100.0, epsilon = 1e-14);
}
#[test]
fn multi_extrapolate_right() {
let (t, th, f) = multi_tenor();
let (theta, fwd) = interpolate_theta_forward(&t, &th, &f, 4.0);
assert_abs_diff_eq!(theta, 0.64, epsilon = 1e-14);
assert_abs_diff_eq!(fwd, 110.0, epsilon = 1e-14);
}
#[test]
fn multi_interpolation_theta_linear() {
let (t, th, f) = multi_tenor();
let (theta, _) = interpolate_theta_forward(&t, &th, &f, 0.75);
assert_abs_diff_eq!(theta, 0.12, epsilon = 1e-14);
}
#[test]
fn multi_interpolation_forward_log_linear() {
let (t, th, f) = multi_tenor();
let (_, fwd) = interpolate_theta_forward(&t, &th, &f, 0.75);
let expected = (102.0_f64 * 105.0).sqrt();
assert_abs_diff_eq!(fwd, expected, epsilon = 1e-10);
}
#[test]
fn exact_match_within_tolerance() {
let (t, th, f) = multi_tenor();
let (theta, fwd) = interpolate_theta_forward(&t, &th, &f, 0.5 + 5e-11);
assert_abs_diff_eq!(theta, 0.08, epsilon = 1e-14);
assert_abs_diff_eq!(fwd, 102.0, epsilon = 1e-14);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "tenors must not be empty")]
fn panics_on_empty_input() {
interpolate_theta_forward(&[], &[], &[], 0.5);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic]
fn panics_on_mismatched_lengths() {
interpolate_theta_forward(&[0.5, 1.0], &[0.08], &[100.0, 105.0], 0.75);
}
}