use crate::errors::QlResult;
use crate::math::comparison::close;
use crate::termstructures::volatility::VolatilityType;
use crate::types::{Rate, Real, Time};
use crate::{fail, require};
fn unsafe_sabr_lognormal_volatility(
strike: Rate,
forward: Rate,
expiry_time: Time,
alpha: Real,
beta: Real,
nu: Real,
rho: Real,
) -> Real {
let one_minus_beta = 1.0 - beta;
let a = (forward * strike).powf(one_minus_beta);
let sqrt_a = a.sqrt();
let log_m = if !close(forward, strike) {
(forward / strike).ln()
} else {
let epsilon = (forward - strike) / strike;
epsilon - 0.5 * epsilon * epsilon
};
let z = (nu / alpha) * sqrt_a * log_m;
let b = 1.0 - 2.0 * rho * z + z * z;
let c = one_minus_beta * one_minus_beta * log_m * log_m;
let tmp = (b.sqrt() + z - rho) / (1.0 - rho);
let xx = tmp.ln();
let denominator = sqrt_a * (1.0 + c / 24.0 + c * c / 1920.0);
let time_factor = 1.0
+ expiry_time
* (one_minus_beta * one_minus_beta * alpha * alpha / (24.0 * a)
+ 0.25 * rho * beta * nu * alpha / sqrt_a
+ (2.0 - 3.0 * rho * rho) * (nu * nu / 24.0));
const M: Real = 10.0;
let multiplier = if (z * z).abs() > Real::EPSILON * M {
z / xx
} else {
1.0 - 0.5 * rho * z - (3.0 * rho * rho - 2.0) * z * z / 12.0
};
(alpha / denominator) * multiplier * time_factor
}
#[allow(clippy::too_many_arguments)]
pub fn unsafe_sabr_volatility(
strike: Rate,
forward: Rate,
expiry_time: Time,
alpha: Real,
beta: Real,
nu: Real,
rho: Real,
volatility_type: VolatilityType,
) -> QlResult<Real> {
match volatility_type {
VolatilityType::ShiftedLognormal => Ok(unsafe_sabr_lognormal_volatility(
strike,
forward,
expiry_time,
alpha,
beta,
nu,
rho,
)),
VolatilityType::Normal => {
fail!("normal (Bachelier) SABR volatility is not yet ported (deferred to #586)")
}
}
}
#[allow(clippy::neg_cmp_op_on_partial_ord)]
pub fn validate_sabr_parameters(alpha: Real, beta: Real, nu: Real, rho: Real) -> QlResult<()> {
require!(alpha > 0.0, "alpha must be positive: {alpha} not allowed");
require!(
(0.0..=1.0).contains(&beta),
"beta must be in [0.0, 1.0]: {beta} not allowed"
);
require!(nu >= 0.0, "nu must be non negative: {nu} not allowed");
require!(
rho * rho < 1.0,
"rho square must be less than one: {rho} not allowed"
);
Ok(())
}
#[allow(clippy::too_many_arguments, clippy::neg_cmp_op_on_partial_ord)]
pub fn sabr_volatility(
strike: Rate,
forward: Rate,
expiry_time: Time,
alpha: Real,
beta: Real,
nu: Real,
rho: Real,
volatility_type: VolatilityType,
) -> QlResult<Real> {
require!(
strike > 0.0,
"strike must be positive: {strike} not allowed"
);
require!(
forward > 0.0,
"at the money forward rate must be positive: {forward} not allowed"
);
require!(
expiry_time >= 0.0,
"expiry time must be non-negative: {expiry_time} not allowed"
);
validate_sabr_parameters(alpha, beta, nu, rho)?;
unsafe_sabr_volatility(
strike,
forward,
expiry_time,
alpha,
beta,
nu,
rho,
volatility_type,
)
}
#[cfg(test)]
mod tests {
use super::*;
const FORWARD: Real = 0.039;
const EXPIRY: Real = 1.0;
const ALPHA: Real = 0.3;
const BETA: Real = 0.6;
const NU: Real = 0.02;
const RHO: Real = 0.01;
#[test]
fn reproduces_the_cpp_31_strike_vol_table() {
let strikes: [Real; 31] = [
0.03, 0.032, 0.034, 0.036, 0.038, 0.04, 0.042, 0.044, 0.046, 0.048, 0.05, 0.052, 0.054,
0.056, 0.058, 0.06, 0.062, 0.064, 0.066, 0.068, 0.07, 0.072, 0.074, 0.076, 0.078, 0.08,
0.082, 0.084, 0.086, 0.088, 0.09,
];
let expected: [Real; 31] = [
1.16725837321531,
1.15226075991385,
1.13829711098834,
1.12524190877505,
1.11299079244474,
1.10145609357162,
1.09056348513411,
1.08024942745106,
1.07045919457758,
1.06114533019077,
1.05226642581503,
1.04378614411707,
1.03567243073732,
1.0278968727451,
1.02043417226345,
1.01326171139321,
1.00635919013311,
0.999708323124949,
0.993292584155381,
0.987096989695393,
0.98110791455717,
0.975312934134512,
0.969700688771689,
0.964260766651027,
0.958983602256592,
0.953860388001395,
0.948882997029509,
0.944043915545469,
0.939336183299237,
0.934753341079515,
0.930289384251337,
];
for (strike, expected_vol) in strikes.into_iter().zip(expected) {
let vol = sabr_volatility(
strike,
FORWARD,
EXPIRY,
ALPHA,
BETA,
NU,
RHO,
VolatilityType::ShiftedLognormal,
)
.unwrap();
assert!(
(vol - expected_vol).abs() <= 1e-12,
"strike {strike}: expected {expected_vol}, got {vol}"
);
}
}
#[test]
fn atm_hits_the_close_epsilon_branch_continuously() {
let atm = sabr_volatility(
FORWARD,
FORWARD,
EXPIRY,
ALPHA,
BETA,
NU,
RHO,
VolatilityType::ShiftedLognormal,
)
.unwrap();
assert!(
atm.is_finite() && atm > 0.0,
"atm vol not finite positive: {atm}"
);
let just_off = sabr_volatility(
FORWARD + 1e-13,
FORWARD,
EXPIRY,
ALPHA,
BETA,
NU,
RHO,
VolatilityType::ShiftedLognormal,
)
.unwrap();
assert!(
(atm - just_off).abs() < 1e-9,
"epsilon branch discontinuous: atm {atm} vs just_off {just_off}"
);
}
#[test]
fn validate_accepts_the_inclusive_beta_boundaries() {
assert!(validate_sabr_parameters(ALPHA, 0.0, NU, RHO).is_ok());
assert!(validate_sabr_parameters(ALPHA, 1.0, NU, RHO).is_ok());
}
#[test]
fn validate_rejects_out_of_range_parameters() {
assert!(validate_sabr_parameters(0.0, BETA, NU, RHO).is_err());
assert!(validate_sabr_parameters(-0.1, BETA, NU, RHO).is_err());
assert!(validate_sabr_parameters(ALPHA, -0.001, NU, RHO).is_err());
assert!(validate_sabr_parameters(ALPHA, 1.001, NU, RHO).is_err());
assert!(validate_sabr_parameters(ALPHA, BETA, -0.001, RHO).is_err());
assert!(validate_sabr_parameters(ALPHA, BETA, NU, 1.0).is_err());
assert!(validate_sabr_parameters(ALPHA, BETA, NU, -1.0).is_err());
}
#[test]
fn sabr_volatility_rejects_bad_inputs() {
for (strike, forward, expiry) in [
(0.0, FORWARD, EXPIRY),
(-0.01, FORWARD, EXPIRY),
(0.04, 0.0, EXPIRY),
(0.04, -0.01, EXPIRY),
(0.04, FORWARD, -1.0),
] {
assert!(
sabr_volatility(
strike,
forward,
expiry,
ALPHA,
BETA,
NU,
RHO,
VolatilityType::ShiftedLognormal,
)
.is_err(),
"expected Err for strike {strike}, forward {forward}, expiry {expiry}"
);
}
}
#[test]
fn normal_volatility_type_is_deferred_to_586() {
let err = sabr_volatility(
0.04,
FORWARD,
EXPIRY,
ALPHA,
BETA,
NU,
RHO,
VolatilityType::Normal,
)
.unwrap_err();
assert!(
err.message().contains("#586"),
"deferral error should name #586: {}",
err.message()
);
}
}