use super::SVJCalibrator;
use super::SVJParams;
use super::loss::bates_call_price;
use crate::LossMetric;
use crate::OptionType;
use crate::traits::Calibrator;
const HESTON_REF: [f64; 9] = [
25.095178, 20.976171, 17.106937, 13.548230, 10.361869, 7.604362, 5.317953, 3.519953, 2.193310,
];
const STRIKES: [f64; 9] = [80.0, 85.0, 90.0, 95.0, 100.0, 105.0, 110.0, 115.0, 120.0];
#[test]
fn bates_pricer_matches_heston_reference() {
let p = SVJParams {
v0: 0.04,
kappa: 1.5,
theta: 0.04,
sigma_v: 0.3,
rho: -0.7,
lambda: 0.0,
mu_j: 0.0,
sigma_j: 0.01,
};
for (i, &k) in STRIKES.iter().enumerate() {
let price = bates_call_price(&p, 100.0, k, 0.05, 0.0, 1.0);
assert!(
(price - HESTON_REF[i]).abs() < 0.1,
"Heston K={k}: got {price:.6}, expected {:.6}",
HESTON_REF[i]
);
}
}
#[test]
fn svj_calibrate_recovers_heston_prices() {
let n = STRIKES.len();
let calibrator = SVJCalibrator::new(
Some(SVJParams {
v0: 0.06,
kappa: 2.0,
theta: 0.06,
sigma_v: 0.4,
rho: -0.5,
lambda: 0.1,
mu_j: 0.0,
sigma_j: 0.1,
}),
HESTON_REF.to_vec().into(),
vec![100.0; n].into(),
STRIKES.to_vec().into(),
0.05,
Some(0.0),
1.0,
OptionType::Call,
false,
);
let result = calibrator.calibrate(None).unwrap();
assert!(
result.loss.get(LossMetric::Rmse) < 0.5,
"SVJ→Heston RMSE={:.6}",
result.loss.get(LossMetric::Rmse)
);
println!(
"SVJ→Heston: v0={:.4}, kappa={:.4}, theta={:.4}, sigma_v={:.4}, rho={:.4}, lambda={:.4}",
result.v0, result.kappa, result.theta, result.sigma_v, result.rho, result.lambda
);
}
#[test]
fn svj_bates_little_trap_long_maturity_high_rho() {
let p = SVJParams {
v0: 0.04,
kappa: 2.0,
theta: 0.04,
sigma_v: 0.3,
rho: -0.9,
lambda: 0.3,
mu_j: -0.05,
sigma_j: 0.15,
};
let call = bates_call_price(&p, 100.0, 100.0, 0.05, 0.0, 5.0);
assert!(
call.is_finite() && call > 0.0 && call < 100.0,
"SVJ bates_cf Trap form: finite positive bounded call required at T=5y, ρ=-0.9, got {call}"
);
}
#[test]
fn test_svj_calibrate() {
let s = vec![
100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0,
];
let k = vec![
80.0, 85.0, 90.0, 95.0, 100.0, 105.0, 110.0, 115.0, 120.0, 125.0, 130.0, 135.0,
];
let c_market = vec![
21.5, 17.9, 14.2, 11.0, 8.2, 6.0, 4.3, 3.1, 2.2, 1.6, 1.2, 0.9,
];
let r = 0.01;
let q = Some(0.0);
let tau = 0.5;
let option_type = OptionType::Call;
let calibrator = SVJCalibrator::new(
Some(SVJParams {
v0: 0.04,
kappa: 1.5,
theta: 0.04,
sigma_v: 0.5,
rho: -0.7,
lambda: 0.5,
mu_j: -0.05,
sigma_j: 0.1,
}),
c_market.into(),
s.into(),
k.into(),
r,
q,
tau,
option_type,
true,
);
let result = calibrator.calibrate(None).unwrap();
println!(
"SVJ result: v0={}, kappa={}, theta={}, sigma_v={}, rho={}, lambda={}, mu_j={}, sigma_j={}",
result.v0,
result.kappa,
result.theta,
result.sigma_v,
result.rho,
result.lambda,
result.mu_j,
result.sigma_j,
);
println!("Loss: {:?}", result.loss);
}