use super::*;
use crate::traits::PricerExt;
use crate::traits::TimeExt;
fn paper_pricer() -> HestonStochCorrPricer {
HestonStochCorrPricer::new(
100.0, 0.0, 100.0, 0.02, 2.1, 0.03, 0.2, -0.4, 3.4, -0.6, 0.1, 0.4, 1.0 / 12.0, )
}
#[test]
fn carr_madan_reduces_to_heston_short_dated() {
use crate::pricing::heston::HestonPricer;
let (rho, kappa, theta, sigma, v0, s, r) = (-0.7, 2.0, 0.04, 0.3, 0.04, 100.0, 0.03);
for tau in [0.02, 0.005, 0.002] {
let heston = HestonPricer::new(
s,
v0,
s,
r,
None,
rho,
kappa,
theta,
sigma,
Some(0.0),
Some(tau),
None,
None,
);
let heston_call = heston.calculate_call_put().0;
let hscm = HestonStochCorrPricer::new(
s, r, s, v0, kappa, theta, sigma, rho, 10.0, rho, 1e-10, 0.0, tau,
);
let hscm_call = hscm.price_call_carr_madan();
let reldiff = (heston_call - hscm_call).abs() / heston_call;
assert!(
reldiff < 0.01,
"HSCM(σ_ρ→0) must match Heston at τ={tau}: Heston={heston_call:.6}, HSCM={hscm_call:.6}, reldiff={reldiff:.4}"
);
}
}
#[test]
fn char_func_at_zero_is_one() {
let pricer = paper_pricer();
let phi0 = pricer.char_func(0.0);
assert!(
(phi0.norm() - 1.0).abs() < 0.01,
"φ(0) = {phi0}, expected ~1.0"
);
}
#[test]
fn char_func_is_finite_and_bounded() {
let pricer = paper_pricer();
for u in [0.1, 1.0, 5.0, 10.0, 20.0] {
let phi = pricer.char_func(u);
assert!(phi.re.is_finite() && phi.im.is_finite(), "φ({u}) = {phi}");
assert!(phi.norm() <= 1.0 + 0.02, "φ({u}) norm > 1: {}", phi.norm());
}
}
#[test]
fn carr_madan_price_is_positive() {
let pricer = HestonStochCorrPricer::new(
100.0, 0.03, 100.0, 0.04, 2.0, 0.04, 0.3, -0.7, 5.0, -0.5, 0.2, 0.3, 0.5,
);
let call = pricer.price_call_carr_madan();
assert!(call > 0.0, "call price should be positive: {call}");
let (call2, put) = pricer.calculate_call_put();
assert!((call - call2).abs() < 1e-10);
assert!(put > 0.0, "put price should be positive: {put}");
}
#[test]
fn put_call_parity() {
let pricer = HestonStochCorrPricer::new(
100.0, 0.05, 95.0, 0.04, 2.0, 0.04, 0.3, -0.7, 5.0, -0.5, 0.2, 0.3, 0.5,
);
let (call, put) = pricer.calculate_call_put();
let tau = pricer.tau().unwrap();
let parity_rhs = pricer.s - pricer.k * (-pricer.r * tau).exp();
let parity_lhs = call - put;
assert!(
(parity_lhs - parity_rhs).abs() < 0.5,
"put-call parity violated: C-P={parity_lhs:.4}, S-K·e^(-rτ)={parity_rhs:.4}"
);
}
#[test]
fn put_call_parity_with_dividend_yield() {
let mut pricer = HestonStochCorrPricer::new(
100.0, 0.05, 95.0, 0.04, 2.0, 0.04, 0.3, -0.7, 5.0, -0.5, 0.2, 0.3, 0.5,
);
pricer.q = Some(0.03); let (call, put) = pricer.calculate_call_put();
let tau = pricer.tau().unwrap();
let q = pricer.q.unwrap();
let parity_rhs = pricer.s * (-q * tau).exp() - pricer.k * (-pricer.r * tau).exp();
let parity_lhs = call - put;
assert!(
(parity_lhs - parity_rhs).abs() < 0.5,
"put-call parity with q={q} violated: C-P={parity_lhs:.4} vs S·e^(-qτ)-K·e^(-rτ)={parity_rhs:.4}"
);
}
#[test]
fn hscm_model_pricer_uses_dividend_yield() {
use crate::traits::ModelPricer;
let model = HscmModel {
v0: 0.04,
kappa_v: 2.0,
theta_v: 0.04,
sigma_v: 0.3,
rho0: -0.7,
kappa_r: 5.0,
mu_r: -0.5,
sigma_r: 0.2,
rho2: 0.3,
};
let s = 100.0;
let k = 100.0;
let r = 0.05;
let tau = 0.5;
let p_no_div = model.price_call(s, k, r, 0.0, tau);
let p_with_div = model.price_call(s, k, r, 0.05, tau);
assert!(
p_with_div < p_no_div - 0.1,
"HscmModel must respect dividend yield: q=0 → {p_no_div:.4}, q=0.05 → {p_with_div:.4}"
);
}
#[test]
fn reduces_to_heston_when_sigma_r_zero() {
let pricer = HestonStochCorrPricer::new(
100.0, 0.03, 95.0, 0.04, 2.0, 0.04, 0.3, -0.7, 5.0, -0.7, 1e-10, 0.0, 0.5,
);
let call = pricer.price_call_carr_madan();
assert!(call > 5.0 && call < 30.0, "unexpected call price: {call}");
}
#[test]
fn compare_with_standard_heston() {
use crate::pricing::heston::HestonPricer;
let rho = -0.7;
let kappa = 2.0;
let theta = 0.04;
let sigma = 0.3;
let v0 = 0.04;
let s = 100.0;
let r = 0.03;
let k = 100.0;
let tau = 0.5;
let heston = HestonPricer::new(
s,
v0,
k,
r,
None,
rho,
kappa,
theta,
sigma,
Some(0.0),
Some(tau),
None,
None,
);
let (h_call, _) = heston.calculate_call_put();
let hscm = HestonStochCorrPricer::new(
s, r, k, v0, kappa, theta, sigma, rho, 10.0, rho, 1e-10, 0.0, tau,
);
let hscm_call = hscm.price_call_carr_madan();
println!(
"Heston call: {h_call:.4}, HSCM call: {hscm_call:.4}, diff: {:.4}",
(h_call - hscm_call).abs()
);
assert!(
(h_call - hscm_call).abs() / h_call < 0.15,
"HSCM should be close to Heston: H={h_call:.4} vs HSCM={hscm_call:.4}"
);
}
#[test]
fn price_multiple_strikes() {
let pricer = HestonStochCorrPricer::new(
100.0, 0.03, 100.0, 0.04, 2.0, 0.04, 0.3, -0.7, 5.0, -0.5, 0.2, 0.3, 0.5,
);
let strikes = [80.0, 90.0, 100.0, 110.0, 120.0];
let prices: Vec<f64> = strikes
.iter()
.map(|&k| pricer.price_call_at_strike(k))
.collect();
for i in 1..prices.len() {
assert!(
prices[i] <= prices[i - 1] + 0.01,
"call prices not monotone: C({})={:.4} > C({})={:.4}",
strikes[i],
prices[i],
strikes[i - 1],
prices[i - 1]
);
}
}