use argmin::core::CostFunction;
use argmin::core::Executor;
use argmin::core::State;
use argmin::solver::neldermead::NelderMead;
use crate::pricing::sabr::hagan_implied_vol;
#[derive(Debug, Clone)]
pub struct SabrCapletCalibrationResult {
pub alpha: f64,
pub beta: f64,
pub nu: f64,
pub rho: f64,
pub rmse: f64,
pub residuals: Vec<f64>,
pub converged: bool,
}
impl SabrCapletCalibrationResult {
pub fn to_model(&self) -> crate::pricing::sabr::SabrModel {
crate::pricing::sabr::SabrModel {
alpha: self.alpha,
beta: self.beta,
nu: self.nu,
rho: self.rho,
}
}
}
impl crate::traits::ToModel for SabrCapletCalibrationResult {
type Model = crate::pricing::sabr::SabrModel;
fn to_model(&self, _r: f64, _q: f64) -> Self::Model {
SabrCapletCalibrationResult::to_model(self)
}
}
impl crate::traits::CalibrationResult for SabrCapletCalibrationResult {
fn rmse(&self) -> f64 {
self.rmse
}
fn converged(&self) -> bool {
self.converged
}
}
impl crate::traits::Calibrator for SabrCapletCalibrator {
type InitialGuess = (f64, f64, f64);
type Output = SabrCapletCalibrationResult;
fn calibrate(&self, initial: Option<Self::InitialGuess>) -> Self::Output {
let mut this = self.clone();
if let Some(g) = initial {
this.initial_guess = Some(g);
}
SabrCapletCalibrator::calibrate(&this)
}
}
#[derive(Debug, Clone)]
pub struct SabrCapletCalibrator {
pub forward: f64,
pub expiry: f64,
pub beta: f64,
pub strikes: Vec<f64>,
pub market_vols: Vec<f64>,
pub weights: Option<Vec<f64>>,
pub initial_guess: Option<(f64, f64, f64)>,
pub max_iters: u64,
pub sd_tolerance: f64,
}
impl SabrCapletCalibrator {
pub fn new(
forward: f64,
expiry: f64,
beta: f64,
strikes: Vec<f64>,
market_vols: Vec<f64>,
) -> Self {
assert_eq!(
strikes.len(),
market_vols.len(),
"strikes and market_vols must have equal length"
);
Self {
forward,
expiry,
beta,
strikes,
market_vols,
weights: None,
initial_guess: None,
max_iters: 600,
sd_tolerance: 1e-10,
}
}
pub fn calibrate(&self) -> SabrCapletCalibrationResult {
let weights = self
.weights
.clone()
.unwrap_or_else(|| vec![1.0; self.strikes.len()]);
let (a0, nu0, rho0) = self.initial_guess.unwrap_or_else(|| {
let atm = self
.market_vols
.iter()
.zip(self.strikes.iter())
.min_by(|a, b| {
(a.1 - self.forward)
.abs()
.partial_cmp(&(b.1 - self.forward).abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(v, _)| *v)
.unwrap_or(0.2);
(atm * self.forward.powf(1.0 - self.beta), 0.4, -0.2)
});
let problem = SabrCapletCost {
forward: self.forward,
expiry: self.expiry,
beta: self.beta,
strikes: self.strikes.clone(),
market_vols: self.market_vols.clone(),
weights: weights.clone(),
};
let simplex = vec![
vec![a0, nu0, rho0],
vec![a0 * 1.3, nu0, rho0],
vec![a0, nu0 * 1.5, rho0],
vec![a0, nu0, (rho0 + 0.2).clamp(-0.99, 0.99)],
];
let mut converged = true;
let best = match NelderMead::new(simplex.clone()).with_sd_tolerance(self.sd_tolerance) {
Ok(solver) => match Executor::new(problem.clone(), solver)
.configure(|s| s.max_iters(self.max_iters))
.run()
{
Ok(res) => res
.state
.get_best_param()
.cloned()
.unwrap_or_else(|| simplex[0].clone()),
Err(_) => {
converged = false;
simplex[0].clone()
}
},
Err(_) => {
converged = false;
simplex[0].clone()
}
};
let alpha = best[0].abs().max(1e-8);
let nu = best[1].abs().max(1e-8);
let rho = best[2].clamp(-0.9999, 0.9999);
let residuals: Vec<f64> = self
.strikes
.iter()
.zip(self.market_vols.iter())
.map(|(&k, &v_mkt)| {
let v_model = hagan_implied_vol(k, self.forward, self.expiry, alpha, self.beta, nu, rho);
v_model - v_mkt
})
.collect();
let ssr: f64 = residuals
.iter()
.zip(weights.iter())
.map(|(r, w)| (w.sqrt() * r).powi(2))
.sum();
let rmse = (ssr / residuals.len().max(1) as f64).sqrt();
SabrCapletCalibrationResult {
alpha,
beta: self.beta,
nu,
rho,
rmse,
residuals,
converged,
}
}
}
#[derive(Clone)]
struct SabrCapletCost {
forward: f64,
expiry: f64,
beta: f64,
strikes: Vec<f64>,
market_vols: Vec<f64>,
weights: Vec<f64>,
}
impl CostFunction for SabrCapletCost {
type Param = Vec<f64>;
type Output = f64;
fn cost(&self, x: &Self::Param) -> Result<f64, argmin::core::Error> {
let alpha = x[0].abs().max(1e-8);
let nu = x[1].abs().max(1e-8);
let rho = x[2].clamp(-0.9999, 0.9999);
let mut sse = 0.0;
for ((&k, &v_mkt), &w) in self
.strikes
.iter()
.zip(self.market_vols.iter())
.zip(self.weights.iter())
{
let v_model = hagan_implied_vol(k, self.forward, self.expiry, alpha, self.beta, nu, rho);
let diff = v_model - v_mkt;
sse += w * diff * diff;
}
Ok(sse)
}
}