use std::f64::consts::PI;
use ndarray::Array1;
use ndrustfft::FftHandler;
use ndrustfft::ndfft;
use ndrustfft::ndifft;
use num_complex::Complex64;
use super::FourierModelExt;
pub fn frft(x: &Array1<Complex64>, gamma: f64) -> Array1<Complex64> {
let n = x.len();
assert!(n.is_power_of_two(), "FRFT length must be a power of two");
let m = 2 * n;
let mut y = Array1::<Complex64>::zeros(m);
let mut z = Array1::<Complex64>::zeros(m);
for j in 0..n {
let jj = j as f64;
let phase = PI * jj * jj * gamma;
y[j] = x[j] * Complex64::new(0.0, -phase).exp();
z[j] = Complex64::new(0.0, phase).exp();
}
for j in 1..n {
let jj = j as f64;
let phase = PI * jj * jj * gamma;
z[m - j] = Complex64::new(0.0, phase).exp();
}
let handler = FftHandler::<f64>::new(m);
let mut fy = Array1::<Complex64>::zeros(m);
let mut fz = Array1::<Complex64>::zeros(m);
ndfft(&y, &mut fy, &handler, 0);
ndfft(&z, &mut fz, &handler, 0);
let prod = &fy * &fz;
let mut conv = Array1::<Complex64>::zeros(m);
ndifft(&prod, &mut conv, &handler, 0);
let mut out = Array1::<Complex64>::zeros(n);
for k in 0..n {
let kk = k as f64;
let phase = PI * kk * kk * gamma;
out[k] = Complex64::new(0.0, -phase).exp() * conv[k];
}
out
}
#[derive(Debug, Clone)]
pub struct FrftCarrMadanPricer {
pub n: usize,
pub alpha: f64,
pub eta: f64,
pub lambda: f64,
}
impl Default for FrftCarrMadanPricer {
fn default() -> Self {
Self {
n: 4096,
alpha: 0.75,
eta: 0.25,
lambda: 6.0 / 4096.0,
}
}
}
impl FrftCarrMadanPricer {
pub fn new(n: usize, alpha: f64, eta: f64, lambda: f64) -> Self {
assert!(n.is_power_of_two(), "n must be a power of two");
assert!(eta > 0.0 && lambda > 0.0, "eta, lambda must be positive");
Self {
n,
alpha,
eta,
lambda,
}
}
pub fn cumulant_sized(model: &impl FourierModelExt, t: f64, l_factor: f64) -> Self {
let cumulants = model.cumulants(t);
if !cumulants.c2.is_finite() || cumulants.c2 <= 0.0 {
return Self::default();
}
let c4_term = if cumulants.c4.is_finite() && cumulants.c4 >= 0.0 {
cumulants.c4.sqrt()
} else {
0.0
};
let half_width = l_factor * (cumulants.c2.abs() + c4_term).sqrt();
if !half_width.is_finite() || half_width <= 0.0 {
return Self::default();
}
let n = 4096_usize;
let v_max = 2.0 * (80.0 / cumulants.c2.abs()).sqrt();
let eta = (v_max / n as f64).clamp(0.005, 0.25);
Self {
n,
alpha: 0.75,
eta,
lambda: 2.0 * half_width / n as f64,
}
}
pub fn price_call_surface(
&self,
model: &impl FourierModelExt,
s: f64,
r: f64,
t: f64,
) -> (Array1<f64>, Array1<f64>) {
let n = self.n;
let alpha = self.alpha;
let eta = self.eta;
let lambda = self.lambda;
let gamma = eta * lambda / (2.0 * PI);
let cumulants = model.cumulants(t);
let ln_s = s.ln();
let mu_t = ln_s + cumulants.c1;
let b = mu_t - 0.5 * n as f64 * lambda;
let i_unit = Complex64::i();
let disc = (-r * t).exp();
let mut input = Array1::<Complex64>::zeros(n);
for j in 0..n {
let v_j = eta * j as f64;
let simpson = if j == 0 {
eta / 3.0
} else if j % 2 == 1 {
eta * 4.0 / 3.0
} else {
eta * 2.0 / 3.0
};
let xi = Complex64::new(v_j, -(alpha + 1.0));
let phi = model.chf(t, xi) * (i_unit * xi * ln_s).exp();
let denom = Complex64::new(alpha * alpha + alpha - v_j * v_j, (2.0 * alpha + 1.0) * v_j);
let psi = disc * phi / denom;
input[j] = (-i_unit * v_j * b).exp() * psi * simpson;
}
let output = frft(&input, gamma);
let mut log_strikes = Array1::<f64>::zeros(n);
let mut prices = Array1::<f64>::zeros(n);
for u in 0..n {
let k_u = b + lambda * u as f64;
log_strikes[u] = k_u;
prices[u] = ((-alpha * k_u).exp() * output[u].re / PI).max(0.0);
}
(log_strikes, prices)
}
pub fn price_call(&self, model: &impl FourierModelExt, s: f64, k: f64, r: f64, t: f64) -> f64 {
let (log_strikes, prices) = self.price_call_surface(model, s, r, t);
let target = k.ln();
let n = log_strikes.len();
if target < log_strikes[0] || target > log_strikes[n - 1] {
return f64::NAN;
}
for i in 0..n - 1 {
if log_strikes[i] <= target && target <= log_strikes[i + 1] {
let w = (target - log_strikes[i]) / (log_strikes[i + 1] - log_strikes[i]);
return prices[i] * (1.0 - w) + prices[i + 1] * w;
}
}
unreachable!("FRFT Carr-Madan interpolation fall-through despite grid bracketing");
}
pub fn price_put(
&self,
model: &impl FourierModelExt,
s: f64,
k: f64,
r: f64,
q: f64,
t: f64,
) -> f64 {
let call = self.price_call(model, s, k, r, t);
call - s * (-q * t).exp() + k * (-r * t).exp()
}
}
#[cfg(test)]
mod tests {
use num_complex::Complex64;
use super::super::bsm::BSMFourier;
use super::super::heston::HestonFourier;
use super::super::pricer::GilPelaezPricer;
use super::*;
#[test]
fn frft_reduces_to_dft_at_unit_gamma() {
let n = 16usize;
let x = Array1::from_shape_fn(n, |j| Complex64::new(j as f64, 0.5 * j as f64));
let frft_out = frft(&x, 1.0 / n as f64);
let mut dft = Array1::<Complex64>::zeros(n);
for (k, slot) in dft.iter_mut().enumerate() {
let mut acc = Complex64::new(0.0, 0.0);
for j in 0..n {
let ang = -2.0 * std::f64::consts::PI * (j * k) as f64 / n as f64;
acc += x[j] * Complex64::new(0.0, ang).exp();
}
*slot = acc;
}
for k in 0..n {
assert!(
(frft_out[k] - dft[k]).norm() < 1e-9,
"FRFT(γ=1/N)[{k}] = {} vs DFT = {}",
frft_out[k],
dft[k]
);
}
}
#[test]
fn frft_carr_madan_bsm_reference() {
let model = BSMFourier {
sigma: 0.15,
r: 0.05,
q: 0.01,
};
let pricer = FrftCarrMadanPricer::cumulant_sized(&model, 1.0, 12.0);
let price = pricer.price_call(&model, 100.0, 100.0, 0.05, 1.0);
let expected = 7.94871378854164;
assert!(
(price - expected).abs() < 1e-4,
"FRFT Carr-Madan BSM: got={price}, expected={expected}"
);
}
#[test]
fn frft_carr_madan_heston_matches_gil_pelaez() {
let model = HestonFourier {
v0: 0.04,
kappa: 1.5,
theta: 0.04,
sigma: 0.3,
rho: -0.7,
r: 0.03,
q: 0.0,
};
let s = 100.0;
let r = 0.03;
let t = 1.0;
let pricer = FrftCarrMadanPricer::cumulant_sized(&model, t, 12.0);
for k in [80.0, 90.0, 100.0, 110.0, 120.0] {
let frft_price = pricer.price_call(&model, s, k, r, t);
let gp_price = GilPelaezPricer::price_call(&model, s, k, r, 0.0, t);
assert!(
(frft_price - gp_price).abs() < 5e-2,
"Heston K={k}: FRFT={frft_price} vs Gil-Pelaez={gp_price}"
);
}
}
#[test]
fn frft_out_of_grid_returns_nan() {
let model = BSMFourier {
sigma: 0.15,
r: 0.05,
q: 0.0,
};
let pricer = FrftCarrMadanPricer::cumulant_sized(&model, 1.0, 12.0);
let deep = pricer.price_call(&model, 100.0, 1e12, 0.05, 1.0);
assert!(deep.is_nan(), "out-of-grid strike must be NaN, got {deep}");
}
}