use crate::models::common::black_scholes::bs_call_forward;
#[derive(Clone, Debug)]
pub struct DupireLocalVol {
pub expiries: Vec<f64>,
pub strikes: Vec<f64>,
pub variance: Vec<Vec<f64>>,
}
pub fn build(
expiries: &[f64],
strikes: &[f64],
vols: &[Vec<f64>],
spot: f64,
rd: f64,
rf: f64,
) -> DupireLocalVol {
let n_t = expiries.len();
let n_k = strikes.len();
assert!(n_t >= 3, "need ≥ 3 expiries for Dupire FD");
assert!(n_k >= 3, "need ≥ 3 strikes for Dupire FD");
assert_eq!(vols.len(), n_t, "vols rows vs expiries");
for row in vols {
assert_eq!(row.len(), n_k, "vols cols vs strikes");
}
for w in expiries.windows(2) {
assert!(w[1] > w[0], "expiries must be strictly increasing");
}
for w in strikes.windows(2) {
assert!(w[1] > w[0], "strikes must be strictly increasing");
}
assert!(spot > 0.0, "spot must be positive");
let mut c = vec![vec![0.0_f64; n_k]; n_t];
for i in 0..n_t {
let t = expiries[i];
let fwd = spot * ((rd - rf) * t).exp();
let disc = (-rd * t).exp();
for j in 0..n_k {
c[i][j] = bs_call_forward(fwd, strikes[j], vols[i][j], t, disc);
}
}
let floor = 1.0e-8_f64;
let mut variance = vec![vec![0.0_f64; n_k]; n_t];
for i in 0..n_t {
let t = expiries[i];
for j in 0..n_k {
let k = strikes[j];
let dc_dt = fd_t(&c, expiries, i, j);
let dc_dk = fd_k(&c[i], strikes, j);
let d2c_dk2 = fd_kk(&c[i], strikes, j);
let numer = dc_dt + (rd - rf) * k * dc_dk + rf * c[i][j];
let denom = k * k * d2c_dk2;
let var = if denom > floor && numer > 0.0 {
2.0 * numer / denom
} else {
vols[i][j] * vols[i][j]
};
let sigma_floor = 0.01_f64; let sigma_cap = 2.0_f64; variance[i][j] = var.clamp(sigma_floor * sigma_floor, sigma_cap * sigma_cap);
let _ = t;
}
}
DupireLocalVol {
expiries: expiries.to_vec(),
strikes: strikes.to_vec(),
variance,
}
}
impl DupireLocalVol {
pub fn local_variance(&self, t: f64, k: f64) -> f64 {
let (i0, i1, wt) = bracket(&self.expiries, t);
let (j0, j1, wk) = bracket(&self.strikes, k);
let v00 = self.variance[i0][j0];
let v01 = self.variance[i0][j1];
let v10 = self.variance[i1][j0];
let v11 = self.variance[i1][j1];
let v0 = v00 + wk * (v01 - v00);
let v1 = v10 + wk * (v11 - v10);
v0 + wt * (v1 - v0)
}
pub fn local_vol(&self, t: f64, k: f64) -> f64 {
self.local_variance(t, k).sqrt()
}
pub fn t_range(&self) -> (f64, f64) {
(self.expiries[0], *self.expiries.last().unwrap())
}
pub fn k_range(&self) -> (f64, f64) {
(self.strikes[0], *self.strikes.last().unwrap())
}
}
fn bracket(xs: &[f64], x: f64) -> (usize, usize, f64) {
if x <= xs[0] {
return (0, 0, 0.0);
}
if x >= *xs.last().unwrap() {
let n = xs.len() - 1;
return (n, n, 0.0);
}
for i in 1..xs.len() {
if xs[i] >= x {
let lo = i - 1;
let hi = i;
let w = (x - xs[lo]) / (xs[hi] - xs[lo]);
return (lo, hi, w);
}
}
let n = xs.len() - 1;
(n, n, 0.0)
}
fn fd_t(c: &[Vec<f64>], expiries: &[f64], i: usize, j: usize) -> f64 {
let n = expiries.len();
if i == 0 {
(c[1][j] - c[0][j]) / (expiries[1] - expiries[0])
} else if i == n - 1 {
(c[n - 1][j] - c[n - 2][j]) / (expiries[n - 1] - expiries[n - 2])
} else {
(c[i + 1][j] - c[i - 1][j]) / (expiries[i + 1] - expiries[i - 1])
}
}
fn fd_k(row: &[f64], strikes: &[f64], j: usize) -> f64 {
let n = strikes.len();
if j == 0 {
(row[1] - row[0]) / (strikes[1] - strikes[0])
} else if j == n - 1 {
(row[n - 1] - row[n - 2]) / (strikes[n - 1] - strikes[n - 2])
} else {
(row[j + 1] - row[j - 1]) / (strikes[j + 1] - strikes[j - 1])
}
}
fn fd_kk(row: &[f64], strikes: &[f64], j: usize) -> f64 {
let n = strikes.len();
if j == 0 {
return fd_kk(row, strikes, 1);
}
if j == n - 1 {
return fd_kk(row, strikes, n - 2);
}
let h1 = strikes[j] - strikes[j - 1];
let h2 = strikes[j + 1] - strikes[j];
let hs = h1 + h2;
2.0 * (row[j + 1] * h1 - row[j] * hs + row[j - 1] * h2) / (h1 * h2 * hs)
}
#[cfg(test)]
mod tests {
use super::*;
fn flat_vol_grid(sigma: f64) -> (Vec<f64>, Vec<f64>, Vec<Vec<f64>>) {
let expiries = vec![0.25, 0.5, 1.0, 1.5, 2.0];
let strikes = vec![0.8, 0.9, 1.0, 1.1, 1.2, 1.3];
let n_t = expiries.len();
let n_k = strikes.len();
let vols = vec![vec![sigma; n_k]; n_t];
(expiries, strikes, vols)
}
#[test]
fn flat_vol_recovers_itself() {
let sigma0 = 0.20_f64;
let (exp, k, vols) = flat_vol_grid(sigma0);
let lv = build(&exp, &k, &vols, 1.0, 0.0, 0.0);
for i in 1..exp.len() - 1 {
for j in 1..k.len() - 1 {
let var = lv.variance[i][j];
let sigma = var.sqrt();
assert!(
(sigma - sigma0).abs() < 0.01,
"grid({}, {}): σ_LV = {}, expected {}",
i,
j,
sigma,
sigma0
);
}
}
}
#[test]
fn flat_vol_with_nonzero_rates_reproduces_itself() {
let sigma0 = 0.15_f64;
let (exp, k, vols) = flat_vol_grid(sigma0);
let lv = build(&exp, &k, &vols, 1.0, 0.05, 0.02);
for i in 1..exp.len() - 1 {
for j in 1..k.len() - 1 {
let sigma = lv.variance[i][j].sqrt();
assert!(
(sigma - sigma0).abs() < 0.01,
"grid({}, {}): σ_LV = {}, expected {}",
i,
j,
sigma,
sigma0
);
}
}
}
#[test]
fn bilinear_interpolation_at_grid_points_and_midcell() {
let sigma0 = 0.20_f64;
let (exp, k, vols) = flat_vol_grid(sigma0);
let lv = build(&exp, &k, &vols, 1.0, 0.0, 0.0);
let v_at = lv.local_variance(exp[2], k[3]);
assert!((v_at - lv.variance[2][3]).abs() < 1e-15);
let t_mid = 0.5 * (exp[1] + exp[2]);
let k_mid = 0.5 * (k[2] + k[3]);
let v_mid = lv.local_variance(t_mid, k_mid);
let lo = lv.variance[1][2]
.min(lv.variance[1][3])
.min(lv.variance[2][2])
.min(lv.variance[2][3]);
let hi = lv.variance[1][2]
.max(lv.variance[1][3])
.max(lv.variance[2][2])
.max(lv.variance[2][3]);
assert!(v_mid >= lo - 1e-12 && v_mid <= hi + 1e-12);
}
#[test]
fn smiled_market_gives_positive_finite_lv() {
let expiries = vec![0.25_f64, 0.5, 1.0, 1.5, 2.0];
let strikes = vec![0.8_f64, 0.9, 1.0, 1.1, 1.2, 1.3];
let atm = 0.20_f64;
let mut vols = vec![vec![0.0; strikes.len()]; expiries.len()];
for (i, _) in expiries.iter().enumerate() {
for (j, &kk) in strikes.iter().enumerate() {
let m = kk.ln();
vols[i][j] = atm * (1.0 + 0.5 * m * m);
}
}
let lv = build(&expiries, &strikes, &vols, 1.0, 0.03, 0.01);
for i in 1..expiries.len() - 1 {
for j in 1..strikes.len() - 1 {
let var = lv.variance[i][j];
assert!(var.is_finite() && var > 0.0, "LV²({},{}) = {}", i, j, var);
let sigma = var.sqrt();
assert!(sigma > 0.01 && sigma < 2.0);
}
}
}
#[test]
fn bracket_clamps_at_grid_boundaries() {
let xs = vec![1.0, 2.0, 3.0];
assert_eq!(bracket(&xs, 0.5), (0, 0, 0.0));
assert_eq!(bracket(&xs, 3.5), (2, 2, 0.0));
let (lo, hi, w) = bracket(&xs, 1.5);
assert_eq!((lo, hi), (0, 1));
assert!((w - 0.5).abs() < 1e-15);
}
}