use super::Extrapolator;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SmithWilson {
pub ultimate_forward_rate: f64,
pub convergence_speed: f64,
pub last_liquid_point: f64,
}
impl SmithWilson {
#[must_use]
pub fn new(ufr: f64, alpha: f64, llp: f64) -> Self {
assert!(alpha > 0.0, "Alpha must be positive");
assert!(llp > 0.0, "LLP must be positive");
Self {
ultimate_forward_rate: ufr,
convergence_speed: alpha,
last_liquid_point: llp,
}
}
#[must_use]
pub fn eiopa_eur() -> Self {
Self::new(0.0345, 0.126, 20.0)
}
#[must_use]
pub fn eiopa_gbp() -> Self {
Self::new(0.0345, 0.100, 50.0)
}
#[must_use]
pub fn eiopa_usd() -> Self {
Self::new(0.0345, 0.100, 30.0)
}
#[must_use]
pub fn eiopa_chf() -> Self {
Self::new(0.0345, 0.100, 25.0)
}
#[must_use]
pub fn ufr(&self) -> f64 {
self.ultimate_forward_rate
}
#[must_use]
pub fn alpha(&self) -> f64 {
self.convergence_speed
}
#[must_use]
pub fn llp(&self) -> f64 {
self.last_liquid_point
}
#[inline]
#[allow(dead_code)]
pub(crate) fn kernel(&self, t: f64, u: f64) -> f64 {
let alpha = self.convergence_speed;
let min_tu = t.min(u);
let term1 = (-alpha * (t + u)).exp();
let term2 = (alpha * min_tu).exp() - (-alpha * min_tu).exp();
alpha * min_tu - 0.5 * term1 * term2
}
#[inline]
#[allow(dead_code)]
pub(crate) fn convergence_weight(&self, t: f64) -> f64 {
if t <= self.last_liquid_point {
return 0.0;
}
let tau = t - self.last_liquid_point;
let alpha = self.convergence_speed;
1.0 - (-alpha * tau).exp()
}
}
impl Extrapolator for SmithWilson {
fn extrapolate(&self, t: f64, last_t: f64, last_value: f64, _last_derivative: f64) -> f64 {
if t <= last_t {
return last_value;
}
let alpha = self.convergence_speed;
let tau = t - last_t;
let convergence = 1.0 - (-alpha * tau).exp();
let ufr_implied = (last_value * last_t + self.ultimate_forward_rate * tau) / t;
last_value + convergence * (ufr_implied - last_value)
}
fn name(&self) -> &'static str {
"Smith-Wilson"
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_smith_wilson_creation() {
let sw = SmithWilson::new(0.042, 0.1, 20.0);
assert_relative_eq!(sw.ufr(), 0.042, epsilon = 1e-10);
assert_relative_eq!(sw.alpha(), 0.1, epsilon = 1e-10);
assert_relative_eq!(sw.llp(), 20.0, epsilon = 1e-10);
}
#[test]
fn test_eiopa_eur_parameters() {
let sw = SmithWilson::eiopa_eur();
assert_relative_eq!(sw.ufr(), 0.0345, epsilon = 1e-10);
assert_relative_eq!(sw.alpha(), 0.126, epsilon = 1e-10);
assert_relative_eq!(sw.llp(), 20.0, epsilon = 1e-10);
}
#[test]
fn test_eiopa_gbp_parameters() {
let sw = SmithWilson::eiopa_gbp();
assert_relative_eq!(sw.ufr(), 0.0345, epsilon = 1e-10);
assert_relative_eq!(sw.alpha(), 0.100, epsilon = 1e-10);
assert_relative_eq!(sw.llp(), 50.0, epsilon = 1e-10);
}
#[test]
fn test_eiopa_usd_parameters() {
let sw = SmithWilson::eiopa_usd();
assert_relative_eq!(sw.ufr(), 0.0345, epsilon = 1e-10);
assert_relative_eq!(sw.alpha(), 0.100, epsilon = 1e-10);
assert_relative_eq!(sw.llp(), 30.0, epsilon = 1e-10);
}
#[test]
fn test_smith_wilson_at_llp() {
let sw = SmithWilson::new(0.042, 0.1, 20.0);
let last_t = 20.0;
let last_value = 0.035;
let last_deriv = 0.001;
let value = sw.extrapolate(last_t, last_t, last_value, last_deriv);
assert_relative_eq!(value, last_value, epsilon = 1e-10);
}
#[test]
fn test_smith_wilson_convergence_towards_ufr() {
let ufr = 0.042;
let sw = SmithWilson::new(ufr, 0.1, 20.0);
let last_t = 20.0;
let last_value = 0.035; let last_deriv = 0.001;
let value_30 = sw.extrapolate(30.0, last_t, last_value, last_deriv);
let value_60 = sw.extrapolate(60.0, last_t, last_value, last_deriv);
let value_100 = sw.extrapolate(100.0, last_t, last_value, last_deriv);
let value_150 = sw.extrapolate(150.0, last_t, last_value, last_deriv);
assert!(value_30 > last_value, "30Y should be above LLP value");
assert!(value_60 > value_30, "60Y should be above 30Y");
assert!(value_100 > value_60, "100Y should be above 60Y");
assert!(
(value_150 - ufr).abs() < 0.005,
"150Y should be within 50bp of UFR"
);
}
#[test]
fn test_smith_wilson_convergence_from_above() {
let ufr = 0.03;
let sw = SmithWilson::new(ufr, 0.1, 20.0);
let last_t = 20.0;
let last_value = 0.045; let last_deriv = -0.001;
let value_30 = sw.extrapolate(30.0, last_t, last_value, last_deriv);
let value_60 = sw.extrapolate(60.0, last_t, last_value, last_deriv);
let value_100 = sw.extrapolate(100.0, last_t, last_value, last_deriv);
assert!(value_30 < last_value, "30Y should be below LLP value");
assert!(value_60 < value_30, "60Y should be below 30Y");
assert!(value_100 < value_60, "100Y should be below 60Y");
assert!((value_100 - ufr).abs() < (last_value - ufr).abs());
}
#[test]
fn test_smith_wilson_higher_alpha_faster_convergence() {
let ufr = 0.042;
let sw_slow = SmithWilson::new(ufr, 0.05, 20.0);
let sw_fast = SmithWilson::new(ufr, 0.20, 20.0);
let last_t = 20.0;
let last_value = 0.03;
let last_deriv = 0.001;
let slow_40 = sw_slow.extrapolate(40.0, last_t, last_value, last_deriv);
let fast_40 = sw_fast.extrapolate(40.0, last_t, last_value, last_deriv);
assert!(
(fast_40 - ufr).abs() < (slow_40 - ufr).abs(),
"Higher alpha should converge faster: slow_40={}, fast_40={}, ufr={}",
slow_40,
fast_40,
ufr
);
}
#[test]
fn test_smith_wilson_name() {
let sw = SmithWilson::new(0.042, 0.1, 20.0);
assert_eq!(sw.name(), "Smith-Wilson");
}
#[test]
fn test_smith_wilson_eiopa_convergence_criterion() {
let sw = SmithWilson::eiopa_eur();
let last_t = 20.0;
let last_value = 0.030; let last_deriv = 0.0;
let value_60 = sw.extrapolate(60.0, last_t, last_value, last_deriv);
let distance_to_ufr = (value_60 - sw.ufr()).abs();
let initial_distance = (last_value - sw.ufr()).abs();
assert!(
distance_to_ufr < initial_distance * 0.5,
"At LLP+40Y, should be at least 50% closer to UFR"
);
}
#[test]
#[should_panic(expected = "Alpha must be positive")]
fn test_smith_wilson_invalid_alpha() {
let _ = SmithWilson::new(0.042, 0.0, 20.0);
}
#[test]
#[should_panic(expected = "LLP must be positive")]
fn test_smith_wilson_invalid_llp() {
let _ = SmithWilson::new(0.042, 0.1, 0.0);
}
#[test]
fn test_kernel_function() {
let sw = SmithWilson::new(0.042, 0.1, 20.0);
let h_10_20 = sw.kernel(10.0, 20.0);
let h_20_10 = sw.kernel(20.0, 10.0);
assert_relative_eq!(h_10_20, h_20_10, epsilon = 1e-10);
let h_10_10 = sw.kernel(10.0, 10.0);
assert!(h_10_10 > 0.0);
}
#[test]
fn test_convergence_weight() {
let sw = SmithWilson::new(0.042, 0.1, 20.0);
let w_llp = sw.convergence_weight(20.0);
assert_relative_eq!(w_llp, 0.0, epsilon = 1e-10);
let w_30 = sw.convergence_weight(30.0);
let w_50 = sw.convergence_weight(50.0);
let w_100 = sw.convergence_weight(100.0);
assert!(w_30 > 0.0);
assert!(w_50 > w_30);
assert!(w_100 > w_50);
let w_500 = sw.convergence_weight(500.0);
assert!(w_500 > 0.99);
}
}