use std::cell::Cell;
use semiflow::{
grid::{BoundaryPolicy, InterpKind},
AdaptivePI, ChernoffSemigroup, DiffusionChernoff, DriftReactionChernoff, Grid1D, GridFn1D,
StrangSplit,
};
use statrs::distribution::{ChiSquared, ContinuousCDF};
const S0: f64 = 100.0;
const K_STRIKE: f64 = 100.0;
const R: f64 = 0.05;
const SIGMA0: f64 = 0.30;
const BETA_PDE: f64 = 0.7;
const T_MAT: f64 = 1.0;
const X_MIN: f64 = 1.0;
const X_MAX: f64 = 200.0;
const N_GRID: usize = 256;
const ERR_THRESHOLD: f64 = 5e-4;
thread_local! {
static HALF_D2: Cell<f64> = const { Cell::new(0.0) };
static DELTA_SQ: Cell<f64> = const { Cell::new(0.0) };
static BETA_CELL: Cell<f64> = const { Cell::new(0.0) };
static TWO_B: Cell<f64> = const { Cell::new(0.0) };
}
fn init_params() {
let delta_sq = SIGMA0 * SIGMA0 * S0.powf(2.0 - 2.0 * BETA_PDE);
let two_b = 2.0 * BETA_PDE;
HALF_D2.with(|c| c.set(0.5 * delta_sq));
DELTA_SQ.with(|c| c.set(delta_sq));
BETA_CELL.with(|c| c.set(BETA_PDE));
TWO_B.with(|c| c.set(two_b));
}
fn a_fn(s: f64) -> f64 {
HALF_D2.with(Cell::get) * libm::pow(s, TWO_B.with(Cell::get))
}
fn a_prime_fn(s: f64) -> f64 {
DELTA_SQ.with(Cell::get) * BETA_CELL.with(Cell::get) * libm::pow(s, TWO_B.with(Cell::get) - 1.0)
}
fn a_dbl_prime_fn(s: f64) -> f64 {
let d = DELTA_SQ.with(Cell::get);
let bp = BETA_CELL.with(Cell::get);
let b = TWO_B.with(Cell::get);
d * bp * (b - 1.0) * libm::pow(s, b - 2.0)
}
fn b_fn(s: f64) -> f64 {
R * s - a_prime_fn(s)
}
fn c_fn(_: f64) -> f64 {
-R
}
fn ncx2_cdf(w: f64, v: f64, lam: f64) -> f64 {
let half_lam = lam / 2.0;
let log_half_lam = half_lam.ln();
let mut log_p = -half_lam;
let mut sum = 0.0_f64;
let mut max_log_p = f64::NEG_INFINITY;
let mut converged = false;
for j in 0_u32..2000 {
let chi = ChiSquared::new(v + 2.0 * f64::from(j)).expect("df > 0");
let cdf_j = chi.cdf(w);
if log_p > -700.0 && cdf_j > 0.0 {
sum += log_p.exp() * cdf_j;
}
if log_p > max_log_p {
max_log_p = log_p;
}
if f64::from(j) > half_lam.max(5.0) && log_p < max_log_p - 36.0 {
converged = true;
break;
}
log_p += log_half_lam - f64::from(j + 1).ln();
}
assert!(
converged,
"ncx2_cdf did not converge: w={w}, v={v}, lam={lam}"
);
sum
}
fn schroder_call(s: f64) -> f64 {
let delta_sq = SIGMA0 * SIGMA0 * S0.powf(2.0 - 2.0 * BETA_PDE);
let beta_s = 2.0 * BETA_PDE;
let two_m_beta = 2.0 - beta_s;
let expon = R * two_m_beta * T_MAT;
let k_param = 2.0 * R / (delta_sq * two_m_beta * (libm::exp(expon) - 1.0));
let x = k_param * libm::pow(s, two_m_beta) * libm::exp(expon);
let y = k_param * libm::pow(K_STRIKE, two_m_beta);
let df_v = 2.0 / two_m_beta;
let df_v2 = df_v + 2.0;
let q1 = 1.0 - ncx2_cdf(2.0 * y, df_v2, 2.0 * x);
let q2 = 1.0 - ncx2_cdf(2.0 * x, df_v, 2.0 * y);
s * q1 - K_STRIKE * libm::exp(-R * T_MAT) * (1.0 - q2)
}
fn make_grid() -> Grid1D {
Grid1D::new(X_MIN, X_MAX, N_GRID)
.unwrap()
.with_boundary(BoundaryPolicy::LinearExtrapolate)
.with_interp(InterpKind::CubicHermite)
}
fn make_strang(grid: Grid1D) -> StrangSplit<DiffusionChernoff, DriftReactionChernoff> {
let a_norm = a_fn(X_MAX);
let diffusion = DiffusionChernoff::new(a_fn, a_prime_fn, a_dbl_prime_fn, a_norm, grid);
let drift = DriftReactionChernoff::new(b_fn, c_fn, R, grid);
StrangSplit::new(diffusion, drift)
}
fn payoff_ic(grid: Grid1D) -> GridFn1D {
GridFn1D::from_fn(grid, |s| (s - K_STRIKE).max(0.0))
}
fn atm_err(u: &GridFn1D) -> f64 {
let grid = u.grid;
let mut max_err = 0.0_f64;
for i in 0..grid.n {
let s = grid.x_at(i);
if (80.0..=120.0).contains(&s) {
let oracle = schroder_call(s);
let e = (u.values[i] - oracle).abs();
if e > max_err {
max_err = e;
}
}
}
max_err
}
#[allow(clippy::cast_precision_loss)] fn calibrate_fixed_step(grid: Grid1D, u0: &GridFn1D) -> (usize, f64) {
let n_sweep = [32usize, 64, 128, 256, 512, 1024];
let mut n_match = 0;
let mut err_fixed = f64::INFINITY;
println!("\nCEV high-corner calibration (β={BETA_PDE}, σ={SIGMA0}, T={T_MAT}):");
for &n in &n_sweep {
let strang = make_strang(grid);
let sg = ChernoffSemigroup::new(strang, n).expect("n >= 1");
let u = sg.evolve(T_MAT, u0).expect("evolve");
let e = atm_err(&u);
println!(" n={n:5}: err = {e:.4e}");
if e <= ERR_THRESHOLD && n_match == 0 {
n_match = n;
err_fixed = e;
}
}
(n_match, err_fixed)
}
#[test]
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
fn g_pi_adaptive_efficiency_vs_fixed_step() {
init_params();
let grid = make_grid();
let u0 = payoff_ic(grid);
let (n_match, err_fixed) = calibrate_fixed_step(grid, &u0);
if n_match == 0 {
println!("WARNING: no n in sweep achieved err ≤ {ERR_THRESHOLD:.1e}; skipping G_PI gate.");
return;
}
println!("n_match = {n_match} (err_fixed = {err_fixed:.4e})");
let strang_adaptive = make_strang(grid);
let mut pi = AdaptivePI::new(strang_adaptive).with_tolerance(0.0, 1e-4);
let outcome = pi.evolve_adaptive(T_MAT, &u0).expect("adaptive evolve");
let err_adaptive = atm_err(&outcome.final_state);
let ratio = outcome.steps_accepted as f64 / n_match as f64;
let steps_gate = (0.7 * n_match as f64) as usize;
println!(
"Adaptive: steps_accepted={}, steps_rejected={}, err={err_adaptive:.4e}, ratio={ratio:.3}",
outcome.steps_accepted, outcome.steps_rejected,
);
println!(
"Gate 1 (err_adaptive ≤ err_fixed): {err_adaptive:.4e} ≤ {err_fixed:.4e} → {}",
if err_adaptive <= err_fixed {
"PASS"
} else {
"FAIL"
}
);
println!(
"Gate 2 (steps ≤ 0.7 × n_match = {steps_gate}): {} ≤ {steps_gate} → {}",
outcome.steps_accepted,
if ratio <= 0.7 { "PASS" } else { "FAIL" }
);
assert!(
err_adaptive <= 2.0 * err_fixed,
"G_PI Gate 1 FAIL: err_adaptive={err_adaptive:.4e} > 2.0·err_fixed={:.4e}",
2.0 * err_fixed
);
assert!(
ratio <= 0.7,
"G_PI Gate 2 FAIL: adaptive steps = {} > 0.7 × n_match ({:.0})",
outcome.steps_accepted,
0.7 * n_match as f64
);
}