|
| 1 | +//! Dupire local volatility from call price surface |
| 2 | +//! σ_loc^2(K,T) = [ ∂C/∂T + (r - q) K ∂C/∂K + q C ] / [ 0.5 K^2 ∂²C/∂K² ] |
| 3 | +
|
| 4 | +use impl_new_derive::ImplNew; |
| 5 | +use ndarray::{Array2, Axis}; |
| 6 | + |
| 7 | +/// Dupire local volatility surface calculator |
| 8 | +#[derive(ImplNew, Clone, Debug)] |
| 9 | +pub struct Dupire { |
| 10 | + /// Strikes (ascending), length N_K |
| 11 | + pub ks: Vec<f64>, |
| 12 | + /// Maturities in years (ascending), length N_T |
| 13 | + pub ts: Vec<f64>, |
| 14 | + /// Call price surface with shape (N_T, N_K), row = fixed T_j, col = K_i, values are present call prices C(K_i, T_j) |
| 15 | + pub calls: Array2<f64>, |
| 16 | + /// Risk-free rate |
| 17 | + pub r: f64, |
| 18 | + /// Dividend yield |
| 19 | + pub q: f64, |
| 20 | + /// Small number to stabilize division when ∂²C/∂K² is near zero |
| 21 | + pub eps: f64, |
| 22 | + /// Optional pre-calculated ∂C/∂K with shape (N_T, N_K) (overrides finite-difference computation) |
| 23 | + pub dc_dk: Option<Array2<f64>>, |
| 24 | + /// Optional pre-calculated ∂²C/∂K² with shape (N_T, N_K) (overrides finite-difference computation) |
| 25 | + pub d2c_dk2: Option<Array2<f64>>, |
| 26 | + /// Optional pre-calculated ∂C/∂T with shape (N_T, N_K) (overrides finite-difference computation) |
| 27 | + pub dc_dt: Option<Array2<f64>>, |
| 28 | +} |
| 29 | + |
| 30 | +impl Dupire { |
| 31 | + /// Compute local volatility surface on the same (T, K) grid as the input call surface. |
| 32 | + /// |
| 33 | + /// Returns Array2 (N_T, N_K) with σ_loc(K_i, T_j); boundaries in K use NaN where the second derivative is ill-defined. |
| 34 | + #[must_use] |
| 35 | + pub fn local_vol_surface(&self) -> Array2<f64> { |
| 36 | + assert_eq!( |
| 37 | + self.calls.dim().0, |
| 38 | + self.ts.len(), |
| 39 | + "calls rows must match ts length" |
| 40 | + ); |
| 41 | + assert_eq!( |
| 42 | + self.calls.dim().1, |
| 43 | + self.ks.len(), |
| 44 | + "calls cols must match ks length" |
| 45 | + ); |
| 46 | + |
| 47 | + let nt = self.ts.len(); |
| 48 | + let nk = self.ks.len(); |
| 49 | + |
| 50 | + let mut sigma = Array2::<f64>::from_elem((nt, nk), f64::NAN); |
| 51 | + |
| 52 | + for j in 0..nt { |
| 53 | + for i in 1..nk - 1 { |
| 54 | + let k_im1 = self.ks[i - 1]; |
| 55 | + let k_i = self.ks[i]; |
| 56 | + let k_ip1 = self.ks[i + 1]; |
| 57 | + |
| 58 | + let c_im1 = self.calls[[j, i - 1]]; |
| 59 | + let c_i = self.calls[[j, i]]; |
| 60 | + let c_ip1 = self.calls[[j, i + 1]]; |
| 61 | + |
| 62 | + // ∂C/∂K at (j,i) using non-uniform 3-pt central stencil |
| 63 | + let h_i = k_i - k_im1; |
| 64 | + let h_ip1 = k_ip1 - k_i; |
| 65 | + let dcdk = (-h_ip1 / (h_i * (h_i + h_ip1))) * c_im1 |
| 66 | + + ((h_ip1 - h_i) / (h_i * h_ip1)) * c_i |
| 67 | + + (h_i / (h_ip1 * (h_i + h_ip1))) * c_ip1; |
| 68 | + |
| 69 | + // ∂²C/∂K² at (j,i) using non-uniform 3-pt stencil |
| 70 | + let denom_left = h_i * (h_i + h_ip1); |
| 71 | + let denom_mid = h_i * h_ip1; |
| 72 | + let denom_right = h_ip1 * (h_i + h_ip1); |
| 73 | + let d2cdk2 = 2.0 * (c_im1 / denom_left - c_i / denom_mid + c_ip1 / denom_right); |
| 74 | + |
| 75 | + // ∂C/∂T at (j,i) using central difference in T (non-uniform aware) |
| 76 | + let dcdt = if j == 0 { |
| 77 | + let dt = self.ts[1] - self.ts[0]; |
| 78 | + if dt.abs() < f64::EPSILON { |
| 79 | + f64::NAN |
| 80 | + } else { |
| 81 | + (self.calls[[1, i]] - self.calls[[0, i]]) / dt |
| 82 | + } |
| 83 | + } else if j == nt - 1 { |
| 84 | + let dt = self.ts[nt - 1] - self.ts[nt - 2]; |
| 85 | + if dt.abs() < f64::EPSILON { |
| 86 | + f64::NAN |
| 87 | + } else { |
| 88 | + (self.calls[[nt - 1, i]] - self.calls[[nt - 2, i]]) / dt |
| 89 | + } |
| 90 | + } else { |
| 91 | + let dt = self.ts[j + 1] - self.ts[j - 1]; |
| 92 | + if dt.abs() < f64::EPSILON { |
| 93 | + f64::NAN |
| 94 | + } else { |
| 95 | + (self.calls[[j + 1, i]] - self.calls[[j - 1, i]]) / dt |
| 96 | + } |
| 97 | + }; |
| 98 | + |
| 99 | + let denom = 0.5 * k_i * k_i * d2cdk2; |
| 100 | + let numer = dcdt + (self.r - self.q) * k_i * dcdk + self.q * c_i; |
| 101 | + |
| 102 | + if denom.abs() > self.eps && denom.is_finite() && numer.is_finite() { |
| 103 | + let s2 = numer / denom; |
| 104 | + if s2.is_sign_positive() { |
| 105 | + sigma[[j, i]] = s2.max(0.0).sqrt(); |
| 106 | + } |
| 107 | + } |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + sigma |
| 112 | + } |
| 113 | + |
| 114 | + /// Compute local volatility surface using pre-calculated partial derivatives ∂C/∂K, ∂²C/∂K², ∂C/∂T. |
| 115 | + /// Requires `dc_dk`, `d2c_dk2`, and `dc_dt` fields to be populated. |
| 116 | + /// |
| 117 | + /// Returns Array2 (N_T, N_K) with σ_loc(K_i, T_j). |
| 118 | + #[must_use] |
| 119 | + pub fn local_vol_surface_from_custom_derivatives(&self) -> Array2<f64> { |
| 120 | + let dc_dk = self.dc_dk.as_ref().expect("dc_dk must be provided for custom derivatives"); |
| 121 | + let d2c_dk2 = self.d2c_dk2.as_ref().expect("d2c_dk2 must be provided for custom derivatives"); |
| 122 | + let dc_dt = self.dc_dt.as_ref().expect("dc_dt must be provided for custom derivatives"); |
| 123 | + |
| 124 | + let nt = self.ts.len(); |
| 125 | + let nk = self.ks.len(); |
| 126 | + |
| 127 | + assert_eq!(dc_dk.dim(), (nt, nk), "dc_dk must have shape (N_T, N_K)"); |
| 128 | + assert_eq!(d2c_dk2.dim(), (nt, nk), "d2c_dk2 must have shape (N_T, N_K)"); |
| 129 | + assert_eq!(dc_dt.dim(), (nt, nk), "dc_dt must have shape (N_T, N_K)"); |
| 130 | + |
| 131 | + let mut sigma = Array2::<f64>::from_elem((nt, nk), f64::NAN); |
| 132 | + |
| 133 | + for j in 0..nt { |
| 134 | + for i in 0..nk { |
| 135 | + let k_i = self.ks[i]; |
| 136 | + let c_i = self.calls[[j, i]]; |
| 137 | + |
| 138 | + let dcdk = dc_dk[[j, i]]; |
| 139 | + let d2cdk2 = d2c_dk2[[j, i]]; |
| 140 | + let dcdt = dc_dt[[j, i]]; |
| 141 | + |
| 142 | + let denom = 0.5 * k_i * k_i * d2cdk2; |
| 143 | + let numer = dcdt + (self.r - self.q) * k_i * dcdk + self.q * c_i; |
| 144 | + |
| 145 | + if denom.abs() > self.eps && denom.is_finite() && numer.is_finite() { |
| 146 | + let s2 = numer / denom; |
| 147 | + if s2.is_sign_positive() { |
| 148 | + sigma[[j, i]] = s2.max(0.0).sqrt(); |
| 149 | + } |
| 150 | + } |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + sigma |
| 155 | + } |
| 156 | + |
| 157 | + /// Convenience: compute local volatility for a single maturity slice at time index j. |
| 158 | + #[must_use] |
| 159 | + pub fn local_vol_slice(&self, j: usize) -> Vec<f64> { |
| 160 | + assert!(j < self.ts.len()); |
| 161 | + let row = self.calls.index_axis(Axis(0), j); |
| 162 | + let mut out = vec![f64::NAN; self.ks.len()]; |
| 163 | + |
| 164 | + if self.ks.len() < 3 { |
| 165 | + return out; |
| 166 | + } |
| 167 | + |
| 168 | + for i in 1..self.ks.len() - 1 { |
| 169 | + let k_im1 = self.ks[i - 1]; |
| 170 | + let k_i = self.ks[i]; |
| 171 | + let k_ip1 = self.ks[i + 1]; |
| 172 | + |
| 173 | + let c_im1 = row[i - 1]; |
| 174 | + let c_i = row[i]; |
| 175 | + let c_ip1 = row[i + 1]; |
| 176 | + |
| 177 | + let h_i = k_i - k_im1; |
| 178 | + let h_ip1 = k_ip1 - k_i; |
| 179 | + |
| 180 | + let dcdk = (-h_ip1 / (h_i * (h_i + h_ip1))) * c_im1 |
| 181 | + + ((h_ip1 - h_i) / (h_i * h_ip1)) * c_i |
| 182 | + + (h_i / (h_ip1 * (h_i + h_ip1))) * c_ip1; |
| 183 | + |
| 184 | + let denom_left = h_i * (h_i + h_ip1); |
| 185 | + let denom_mid = h_i * h_ip1; |
| 186 | + let denom_right = h_ip1 * (h_i + h_ip1); |
| 187 | + let d2cdk2 = 2.0 * (c_im1 / denom_left - c_i / denom_mid + c_ip1 / denom_right); |
| 188 | + |
| 189 | + let dcdt = if j == 0 { |
| 190 | + let dt = self.ts[1] - self.ts[0]; |
| 191 | + if dt.abs() < f64::EPSILON { |
| 192 | + f64::NAN |
| 193 | + } else { |
| 194 | + (self.calls[[1, i]] - self.calls[[0, i]]) / dt |
| 195 | + } |
| 196 | + } else if j == self.ts.len() - 1 { |
| 197 | + let dt = self.ts[j] - self.ts[j - 1]; |
| 198 | + if dt.abs() < f64::EPSILON { |
| 199 | + f64::NAN |
| 200 | + } else { |
| 201 | + (self.calls[[j, i]] - self.calls[[j - 1, i]]) / dt |
| 202 | + } |
| 203 | + } else { |
| 204 | + let dt = self.ts[j + 1] - self.ts[j - 1]; |
| 205 | + if dt.abs() < f64::EPSILON { |
| 206 | + f64::NAN |
| 207 | + } else { |
| 208 | + (self.calls[[j + 1, i]] - self.calls[[j - 1, i]]) / dt |
| 209 | + } |
| 210 | + }; |
| 211 | + |
| 212 | + let denom = 0.5 * k_i * k_i * d2cdk2; |
| 213 | + let numer = dcdt + (self.r - self.q) * k_i * dcdk + self.q * c_i; |
| 214 | + |
| 215 | + if denom.abs() > self.eps && denom.is_finite() && numer.is_finite() { |
| 216 | + let s2 = numer / denom; |
| 217 | + if s2.is_sign_positive() { |
| 218 | + out[i] = s2.max(0.0).sqrt(); |
| 219 | + } |
| 220 | + } |
| 221 | + } |
| 222 | + |
| 223 | + out |
| 224 | + } |
| 225 | + |
| 226 | + /// Compute local volatility for a single maturity slice using pre-calculated partial derivatives. |
| 227 | + /// Requires `dc_dk`, `d2c_dk2`, and `dc_dt` fields to be populated. |
| 228 | + #[must_use] |
| 229 | + pub fn local_vol_slice_from_custom_derivatives(&self, j: usize) -> Vec<f64> { |
| 230 | + assert!(j < self.ts.len()); |
| 231 | + |
| 232 | + let dc_dk = self.dc_dk.as_ref().expect("dc_dk must be provided for custom derivatives"); |
| 233 | + let d2c_dk2 = self.d2c_dk2.as_ref().expect("d2c_dk2 must be provided for custom derivatives"); |
| 234 | + let dc_dt = self.dc_dt.as_ref().expect("dc_dt must be provided for custom derivatives"); |
| 235 | + |
| 236 | + let row = self.calls.index_axis(Axis(0), j); |
| 237 | + let mut out = vec![f64::NAN; self.ks.len()]; |
| 238 | + |
| 239 | + for i in 0..self.ks.len() { |
| 240 | + let k_i = self.ks[i]; |
| 241 | + let c_i = row[i]; |
| 242 | + |
| 243 | + let dcdk = dc_dk[[j, i]]; |
| 244 | + let d2cdk2 = d2c_dk2[[j, i]]; |
| 245 | + let dcdt = dc_dt[[j, i]]; |
| 246 | + |
| 247 | + let denom = 0.5 * k_i * k_i * d2cdk2; |
| 248 | + let numer = dcdt + (self.r - self.q) * k_i * dcdk + self.q * c_i; |
| 249 | + |
| 250 | + if denom.abs() > self.eps && denom.is_finite() && numer.is_finite() { |
| 251 | + let s2 = numer / denom; |
| 252 | + if s2.is_sign_positive() { |
| 253 | + out[i] = s2.max(0.0).sqrt(); |
| 254 | + } |
| 255 | + } |
| 256 | + } |
| 257 | + |
| 258 | + out |
| 259 | + } |
| 260 | +} |
0 commit comments