use std::f64::consts::PI;
use crate::core::trade::PutOrCall;
use crate::equity::heston::Cpx;
pub const CALIBRATION_TERMS: usize = 160;
pub const DEFAULT_TERMS: usize = 2048;
pub(crate) struct CosPricer {
a: f64,
b: f64,
weights: Vec<f64>,
df: f64,
forward: f64,
}
impl CosPricer {
pub(crate) fn new(cf: &dyn Fn(Cpx) -> Cpx, r: f64, t: f64, n: usize) -> Self {
let psi = |h: f64| cf(Cpx::real(h)).ln();
let rough = psi(1e-2);
let c2_rough = (-2.0 * rough.re / 1e-4).abs().max(1e-10);
let h = (0.05 / c2_rough.sqrt()).clamp(1e-4, 1e-1);
let p1 = psi(h);
let p2 = psi(2.0 * h);
let c1 = p1.im / h;
let c4 = (2.0 * (p2.re - 4.0 * p1.re) / h.powi(4)).max(0.0);
let c2 = (-2.0 * p1.re / (h * h) + c4 * h * h / 12.0).abs().max(1e-10);
let width = 10.0 * (c2 + c4.sqrt()).sqrt();
let (a, b) = (c1 - width, c1 + width);
let bma = b - a;
let weights = (0..n)
.map(|k| {
let u = k as f64 * PI / bma;
let e = Cpx::new((u * a).cos(), -(u * a).sin());
let w = cf(Cpx::real(u)).mul(e).re;
if k == 0 { 0.5 * w } else { w }
})
.collect();
let forward = cf(Cpx::new(0.0, -1.0)).re;
CosPricer { a, b, weights, df: (-r * t).exp(), forward }
}
pub(crate) fn call(&self, strike: f64) -> f64 {
if strike < self.forward {
return self.raw_put(strike) + self.df * (self.forward - strike);
}
self.raw_call(strike)
}
pub(crate) fn put(&self, strike: f64) -> f64 {
if strike > self.forward {
return self.raw_call(strike) + self.df * (strike - self.forward);
}
self.raw_put(strike)
}
fn raw_call(&self, strike: f64) -> f64 {
let lnk = strike.ln();
if lnk >= self.b {
return 0.0; }
self.sum(strike, lnk.max(self.a), self.b, true)
}
fn raw_put(&self, strike: f64) -> f64 {
let lnk = strike.ln();
if lnk <= self.a {
return 0.0;
}
self.sum(strike, self.a, lnk.min(self.b), false)
}
pub(crate) fn price(&self, strike: f64, put_or_call: PutOrCall) -> f64 {
match put_or_call {
PutOrCall::Call => self.call(strike),
PutOrCall::Put => self.put(strike),
}
}
fn sum(&self, strike: f64, c: f64, d: f64, call: bool) -> f64 {
let bma = self.b - self.a;
let (ec, ed) = (c.exp(), d.exp());
let (yc, yd) = (c - self.a, d - self.a);
let mut total = 0.0;
for (k, w) in self.weights.iter().enumerate() {
let omega = k as f64 * PI / bma;
let (sin_c, cos_c) = (omega * yc).sin_cos();
let (sin_d, cos_d) = (omega * yd).sin_cos();
let chi =
(cos_d * ed - cos_c * ec + omega * (sin_d * ed - sin_c * ec)) / (1.0 + omega * omega);
let psi = if k == 0 { d - c } else { (sin_d - sin_c) / omega };
let payoff_coeff = if call { chi - strike * psi } else { strike * psi - chi };
total += w * payoff_coeff;
}
self.df * total * 2.0 / bma
}
}
pub(crate) fn group_by_maturity(maturities: impl Iterator<Item = f64>) -> Vec<(f64, Vec<usize>)> {
let mut groups: Vec<(f64, Vec<usize>)> = Vec::new();
for (i, t) in maturities.enumerate() {
match groups.iter_mut().find(|(gt, _)| *gt == t) {
Some((_, idxs)) => idxs.push(i),
None => groups.push((t, vec![i])),
}
}
groups
}
#[cfg(test)]
mod tests {
use super::*;
use crate::equity::bates::{
bates_double_exp_price, bates_price, BatesDoubleExpParams, BatesParams, KouJumps,
MertonJumps,
};
use crate::equity::blackscholes::bs_price;
use crate::equity::heston::{heston_price, HestonParams};
const S: f64 = 100.0;
const R: f64 = 0.03;
const Q: f64 = 0.01;
fn gbm_cf(u: Cpx, sigma: f64, t: f64) -> Cpx {
let drift = S.ln() + (R - Q - 0.5 * sigma * sigma) * t;
crate::equity::heston::I
.mul(u)
.scale(drift)
.sub(u.mul(u).scale(0.5 * sigma * sigma * t))
.exp()
}
fn heston() -> HestonParams {
HestonParams { v0: 0.09, kappa: 2.0, theta: 0.09, vol_of_vol: 0.4, rho: -0.7 }
}
#[test]
fn cos_reproduces_black_scholes() {
for t in [0.1, 1.0, 3.0] {
for sigma in [0.1, 0.3, 0.6] {
let pricer = CosPricer::new(&|u| gbm_cf(u, sigma, t), R, t, DEFAULT_TERMS);
for k in [60.0, 90.0, 100.0, 110.0, 150.0] {
let call = bs_price(S, k, R, Q, sigma, t, PutOrCall::Call);
let put = bs_price(S, k, R, Q, sigma, t, PutOrCall::Put);
assert!(
(pricer.call(k) - call).abs() < 1e-8,
"call K={k} t={t} sigma={sigma}: cos {} bs {call}",
pricer.call(k)
);
assert!(
(pricer.put(k) - put).abs() < 1e-8,
"put K={k} t={t} sigma={sigma}: cos {} bs {put}",
pricer.put(k)
);
}
}
}
}
#[test]
fn cos_agrees_with_the_heston_integration_oracle() {
let params = [
heston(),
HestonParams { v0: 0.04, kappa: 1.0, theta: 0.04, vol_of_vol: 0.9, rho: -0.9 },
HestonParams { v0: 0.16, kappa: 3.0, theta: 0.09, vol_of_vol: 0.2, rho: 0.3 },
];
for hp in ¶ms {
for t in [0.25, 1.0, 2.0] {
let pricer = CosPricer::new(
&|u| crate::equity::heston::characteristic_fn(u, S, R, Q, t, hp),
R,
t,
DEFAULT_TERMS,
);
for k in [70.0, 90.0, 100.0, 110.0, 140.0] {
let oracle = heston_price(S, k, R, Q, t, hp, PutOrCall::Call);
let cos = pricer.call(k);
assert!(
(cos - oracle).abs() < 5e-6,
"K={k} t={t} hp={hp:?}: cos {cos} oracle {oracle}"
);
}
}
}
}
#[test]
fn cos_agrees_with_both_bates_oracles() {
let merton = BatesParams {
heston: heston(),
jumps: MertonJumps { intensity: 0.5, mean_jump: -0.1, jump_vol: 0.2 },
};
let kou = BatesDoubleExpParams {
heston: heston(),
jumps: KouJumps { intensity: 0.7, p_up: 0.4, eta_up: 12.0, eta_down: 8.0 },
};
let t = 1.0;
let merton_pricer =
CosPricer::new(&|u| crate::equity::bates::ln_price_cf(u, S, R, Q, t, &merton), R, t, DEFAULT_TERMS);
let kou_pricer = CosPricer::new(
&|u| crate::equity::bates::ln_price_cf_double_exp(u, S, R, Q, t, &kou),
R,
t,
DEFAULT_TERMS,
);
for k in [80.0, 100.0, 120.0] {
let m_oracle = bates_price(S, k, R, Q, t, &merton, PutOrCall::Call);
let k_oracle = bates_double_exp_price(S, k, R, Q, t, &kou, PutOrCall::Call);
assert!(
(merton_pricer.call(k) - m_oracle).abs() < 5e-6,
"merton K={k}: cos {} oracle {m_oracle}",
merton_pricer.call(k)
);
assert!(
(kou_pricer.call(k) - k_oracle).abs() < 5e-6,
"kou K={k}: cos {} oracle {k_oracle}",
kou_pricer.call(k)
);
}
}
#[test]
fn put_call_parity_holds_to_high_precision() {
let t = 1.0;
let hp = heston();
let pricer = CosPricer::new(
&|u| crate::equity::heston::characteristic_fn(u, S, R, Q, t, &hp),
R,
t,
DEFAULT_TERMS,
);
let df = (-R * t).exp();
let forward = S * ((R - Q) * t).exp();
for k in [80.0, 100.0, 125.0] {
let parity = pricer.call(k) - pricer.put(k) - df * (forward - k);
assert!(parity.abs() < 1e-9, "parity violation {parity} at K={k}");
}
}
#[test]
fn maturity_grouping_preserves_indices() {
let groups = group_by_maturity([1.0, 0.5, 1.0, 2.0, 0.5].into_iter());
assert_eq!(
groups,
vec![(1.0, vec![0, 2]), (0.5, vec![1, 4]), (2.0, vec![3])]
);
}
}