use super::caplet::black_forward_caplet;
use super::caplet::black_forward_floorlet;
use super::volatility::VolatilityModel;
use crate::traits::FloatExt;
fn g_function<T: FloatExt>(s: T, n: T, delta: T) -> T {
if s.abs() <= T::from_f64_fast(1e-12) {
return n * delta;
}
let discount = T::one() / (T::one() + s * delta);
(T::one() - discount.powf(n)) / s
}
pub fn hagan_linear_tsr_convexity_factor<T: FloatExt>(s0: T, swap_years: T, fixed_freq: T) -> T {
let delta = T::one() / fixed_freq;
let n = swap_years * fixed_freq;
let bump = s0.abs() * T::from_f64_fast(1e-4);
let eps = if bump > T::from_f64_fast(1e-8) {
bump
} else {
T::from_f64_fast(1e-8)
};
let g0 = g_function(s0, n, delta);
if g0.abs() < T::from_f64_fast(1e-14) {
return T::zero();
}
let g_plus = g_function(s0 + eps, n, delta);
let g_minus = g_function(s0 - eps, n, delta);
let g_prime = (g_plus - g_minus) / (T::from_f64_fast(2.0) * eps);
s0 * g_prime / g0
}
pub fn hagan_linear_tsr_convexity_adjustment<T: FloatExt>(
s0: T,
sigma_black: T,
t_fix: T,
swap_years: T,
fixed_freq: T,
payment_delay: T,
) -> T {
let lambda = hagan_linear_tsr_convexity_factor(s0, swap_years, fixed_freq);
let variance = sigma_black * sigma_black * t_fix;
let natural = s0 * s0 * variance * lambda;
let delay = if payment_delay > T::zero() {
s0 * variance * payment_delay * s0 / (T::one() + s0 * payment_delay)
} else {
T::zero()
};
natural + delay
}
#[derive(Debug, Clone)]
pub struct CmsCaplet<T: FloatExt, V: VolatilityModel<T>> {
pub strike: T,
pub notional: T,
pub accrual_factor: T,
pub discount_factor: T,
pub forward_cms: T,
pub t_fix: T,
pub swap_years: T,
pub fixed_freq: T,
pub payment_delay: T,
pub vol: V,
}
impl<T: FloatExt, V: VolatilityModel<T>> CmsCaplet<T, V> {
pub fn price(&self) -> T {
price_cms_payoff(
self.strike,
self.notional,
self.accrual_factor,
self.discount_factor,
self.forward_cms,
self.t_fix,
self.swap_years,
self.fixed_freq,
self.payment_delay,
&self.vol,
true,
)
}
}
#[derive(Debug, Clone)]
pub struct CmsFloorlet<T: FloatExt, V: VolatilityModel<T>> {
pub strike: T,
pub notional: T,
pub accrual_factor: T,
pub discount_factor: T,
pub forward_cms: T,
pub t_fix: T,
pub swap_years: T,
pub fixed_freq: T,
pub payment_delay: T,
pub vol: V,
}
impl<T: FloatExt, V: VolatilityModel<T>> CmsFloorlet<T, V> {
pub fn price(&self) -> T {
price_cms_payoff(
self.strike,
self.notional,
self.accrual_factor,
self.discount_factor,
self.forward_cms,
self.t_fix,
self.swap_years,
self.fixed_freq,
self.payment_delay,
&self.vol,
false,
)
}
}
#[allow(clippy::too_many_arguments)]
fn price_cms_payoff<T: FloatExt, V: VolatilityModel<T> + ?Sized>(
strike: T,
notional: T,
accrual: T,
discount: T,
s0: T,
t_fix: T,
swap_years: T,
fixed_freq: T,
payment_delay: T,
vol: &V,
is_caplet: bool,
) -> T {
let sigma = vol.implied_volatility(s0, strike, t_fix);
let ca =
hagan_linear_tsr_convexity_adjustment(s0, sigma, t_fix, swap_years, fixed_freq, payment_delay);
let s_adj = s0 + ca;
let forward_value = if is_caplet {
black_forward_caplet(s_adj, strike, t_fix, sigma)
} else {
black_forward_floorlet(s_adj, strike, t_fix, sigma)
};
notional * accrual * discount * forward_value
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn convexity_factor_is_negative_for_standard_swap() {
let lambda = hagan_linear_tsr_convexity_factor(0.04_f64, 10.0, 2.0);
assert!(lambda < 0.0, "lambda should be negative, got {lambda}");
assert!(lambda.is_finite());
}
#[test]
fn convexity_factor_goes_to_zero_for_short_swap() {
let lambda_long = hagan_linear_tsr_convexity_factor(0.04_f64, 20.0, 2.0).abs();
let lambda_short = hagan_linear_tsr_convexity_factor(0.04_f64, 1.0, 2.0).abs();
assert!(
lambda_long > lambda_short,
"longer swap must show stronger convexity, long={lambda_long} short={lambda_short}"
);
}
#[test]
fn payment_delay_adds_positive_adjustment() {
let nat = hagan_linear_tsr_convexity_adjustment(0.04_f64, 0.3, 2.0, 10.0, 2.0, 0.0);
let delayed = hagan_linear_tsr_convexity_adjustment(0.04_f64, 0.3, 2.0, 10.0, 2.0, 0.5);
assert!(delayed > nat, "delay must push adjustment upwards");
}
}