1use crate::core::interpolation::interp_pairs;
29use crate::core::montecarlo::path_rng;
30use crate::equity::heston::HestonParams;
31use crate::equity::local_vol::LocalVol;
32use crate::equity::processes::qe_variance_step;
33use crate::core::trade::PutOrCall;
34use rand::Rng;
35use rand_distr::StandardNormal;
36
37struct SlvStepper {
53 hp: HestonParams,
54 rho_bar: f64,
55 drift: f64,
56 dt: f64,
57}
58
59impl SlvStepper {
60 fn new(hp: &HestonParams, r: f64, q: f64, dt: f64) -> Self {
61 SlvStepper {
62 hp: *hp,
63 rho_bar: (1.0 - hp.rho * hp.rho).sqrt(),
64 drift: r - q,
65 dt,
66 }
67 }
68
69 fn step(&self, lev: f64, s: f64, v: f64, z_perp: f64, z_v: f64) -> (f64, f64) {
72 let hp = &self.hp;
73 let vp = v.max(0.0);
74 let v_next = qe_variance_step(hp, vp, self.dt, z_v);
75 let vbar = 0.5 * (vp + v_next);
76 let integrated = (hp.rho / hp.vol_of_vol)
77 * (v_next - vp - hp.kappa * hp.theta * self.dt + hp.kappa * vbar * self.dt)
78 + self.rho_bar * (vbar * self.dt).sqrt() * z_perp;
79 let s_next =
80 s * ((self.drift - 0.5 * lev * lev * vbar) * self.dt + lev * integrated).exp();
81 (s_next, v_next)
82 }
83}
84
85#[derive(Debug, Clone, Copy)]
87pub struct SlvConfig {
88 pub paths: usize,
90 pub steps: usize,
92 pub bins: usize,
94 pub seed: u64,
95}
96
97impl Default for SlvConfig {
98 fn default() -> Self {
99 Self { paths: 20_000, steps: 50, bins: 20, seed: 42 }
100 }
101}
102
103#[derive(Debug, Clone)]
106pub struct ConditionalVariance {
107 times: Vec<f64>,
108 slices: Vec<Vec<(f64, f64)>>,
110}
111
112impl ConditionalVariance {
113 pub fn value(&self, s: f64, t: f64) -> f64 {
117 let idx = match self.times.iter().rposition(|&ti| ti <= t + 1e-12) {
118 Some(i) => i,
119 None => 0,
120 };
121 let slice = &self.slices[idx];
122 if slice.len() == 1 {
123 return slice[0].1;
124 }
125 interp_pairs(slice, s)
126 }
127}
128
129pub struct Slv<'a> {
131 pub heston: HestonParams,
132 pub cond_var: ConditionalVariance,
133 local_vol: &'a LocalVol<'a>,
134 s0: f64,
135 r: f64,
136 q: f64,
137 dt: f64,
138}
139
140impl<'a> Slv<'a> {
141 pub fn leverage(&self, s: f64, t: f64) -> f64 {
143 self.local_vol.vol(s, t) / self.cond_var.value(s, t).max(1e-8).sqrt()
144 }
145
146 pub fn price_vanilla(
149 &self,
150 strike: f64,
151 t: f64,
152 put_or_call: PutOrCall,
153 paths: usize,
154 seed: u64,
155 ) -> f64 {
156 let steps = (t / self.dt).round().max(1.0) as usize;
157 let dt = t / steps as f64;
158 let stepper = SlvStepper::new(&self.heston, self.r, self.q, dt);
159 let mut sum = 0.0;
160 for i in 0..paths {
161 let mut rng = path_rng(seed, i as u64);
162 let mut s = self.s0;
163 let mut v: f64 = self.heston.v0;
164 for k in 0..steps {
165 let tk = k as f64 * dt;
166 let z1: f64 = rng.sample(StandardNormal);
167 let z2: f64 = rng.sample(StandardNormal);
168 let lev = self.leverage(s, tk);
169 (s, v) = stepper.step(lev, s, v, z1, z2);
170 }
171 sum += match put_or_call {
172 PutOrCall::Call => (s - strike).max(0.0),
173 PutOrCall::Put => (strike - s).max(0.0),
174 };
175 }
176 (-self.r * t).exp() * sum / paths as f64
177 }
178}
179
180pub fn calibrate<'a>(
184 local_vol: &'a LocalVol<'a>,
185 heston: &HestonParams,
186 s0: f64,
187 r: f64,
188 q: f64,
189 horizon: f64,
190 cfg: &SlvConfig,
191) -> Slv<'a> {
192 heston.validate().expect("invalid Heston parameters");
193 assert!(horizon > 0.0 && cfg.steps > 0 && cfg.bins >= 2 && cfg.paths >= cfg.bins * 10);
194 let dt = horizon / cfg.steps as f64;
195 let n = cfg.paths;
196 let stepper = SlvStepper::new(heston, r, q, dt);
197
198 let mut spots = vec![s0; n];
199 let mut vars = vec![heston.v0; n];
200 let mut times = Vec::with_capacity(cfg.steps);
201 let mut slices = Vec::with_capacity(cfg.steps);
202
203 for k in 0..cfg.steps {
204 let t = k as f64 * dt;
205 let slice: Vec<(f64, f64)> = if k == 0 {
207 vec![(s0, heston.v0)]
208 } else {
209 let mut order: Vec<usize> = (0..n).collect();
210 order.sort_by(|&a, &b| spots[a].total_cmp(&spots[b]));
211 let per_bin = n / cfg.bins;
212 (0..cfg.bins)
213 .map(|b| {
214 let lo = b * per_bin;
215 let hi = if b == cfg.bins - 1 { n } else { lo + per_bin };
216 let members = &order[lo..hi];
217 let ms = members.iter().map(|&i| spots[i]).sum::<f64>()
218 / members.len() as f64;
219 let mv = members.iter().map(|&i| vars[i]).sum::<f64>()
220 / members.len() as f64;
221 (ms, mv)
222 })
223 .collect()
224 };
225 times.push(t);
226 slices.push(slice.clone());
227
228 let cond = |s: f64| -> f64 {
230 if slice.len() == 1 { slice[0].1 } else { interp_pairs(&slice, s) }
231 };
232 for i in 0..n {
233 let mut rng = path_rng(cfg.seed.wrapping_add(0x51_1e * k as u64 + 1), i as u64);
234 let z1: f64 = rng.sample(StandardNormal);
235 let z2: f64 = rng.sample(StandardNormal);
236 let lev = local_vol.vol(spots[i], t) / cond(spots[i]).max(1e-8).sqrt();
237 (spots[i], vars[i]) = stepper.step(lev, spots[i], vars[i], z1, z2);
238 }
239 }
240
241 Slv {
242 heston: *heston,
243 cond_var: ConditionalVariance { times, slices },
244 local_vol,
245 s0,
246 r,
247 q,
248 dt,
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::core::curves::YieldCurve;
256 use crate::core::curves::Compounding;
257 use crate::core::daycount::DayCountConvention;
258 use crate::core::vols::VolSurface;
259 use crate::equity::blackscholes::{bs_price, implied_vol_from_price};
260 use chrono::NaiveDate;
261
262 const S0: f64 = 100.0;
263 const R: f64 = 0.02;
264
265 fn reference() -> NaiveDate {
266 NaiveDate::from_ymd_opt(2026, 1, 1).unwrap()
267 }
268
269 fn curve() -> YieldCurve {
270 YieldCurve::flat(R, reference(), DayCountConvention::Act365, Compounding::Continuous)
271 .unwrap()
272 }
273
274 fn mixing_heston() -> HestonParams {
275 HestonParams { v0: 1.0, kappa: 2.0, theta: 1.0, vol_of_vol: 0.8, rho: -0.6 }
278 }
279
280 #[test]
281 fn conditional_variance_lookup_interpolates_and_clamps() {
282 let cv = ConditionalVariance {
283 times: vec![0.0, 0.5],
284 slices: vec![vec![(100.0, 1.0)], vec![(90.0, 1.2), (110.0, 0.8)]],
285 };
286 assert_eq!(cv.value(50.0, 0.1), 1.0); assert!((cv.value(100.0, 0.7) - 1.0).abs() < 1e-12); assert_eq!(cv.value(50.0, 0.7), 1.2); assert_eq!(cv.value(150.0, 0.7), 0.8);
290 }
291
292 #[test]
293 fn flat_surface_slv_reprices_black_scholes() {
294 let surface =
297 VolSurface::flat(0.2, reference(), DayCountConvention::Act365).unwrap();
298 let yc = curve();
299 let lv = LocalVol::new(&surface, &yc, S0, 0.0, 0.0);
300 let cfg = SlvConfig { paths: 16_000, steps: 40, bins: 20, seed: 7 };
301 let slv = calibrate(&lv, &mixing_heston(), S0, R, 0.0, 1.0, &cfg);
302
303 let mc = slv.price_vanilla(100.0, 1.0, PutOrCall::Call, 32_000, 11);
304 let bs = bs_price(S0, 100.0, R, 0.0, 0.2, 1.0, PutOrCall::Call);
305 assert!((mc - bs).abs() < 0.02 * bs, "slv {mc} vs bs {bs}");
306 }
307
308 #[test]
309 fn skewed_surface_slv_reprices_the_smile() {
310 let strikes = [80.0, 90.0, 100.0, 110.0, 120.0];
314 let smile = |k: f64| 0.2 - 0.1 * (k / S0 - 1.0) + 0.15 * (k / S0 - 1.0).powi(2);
315 let smiles: Vec<Vec<(f64, f64)>> = (0..3)
316 .map(|_| strikes.iter().map(|&k| (k, smile(k))).collect())
317 .collect();
318 let surface = VolSurface::from_strike_smiles(
319 &[
320 crate::core::curves::Tenor::YearFraction(0.25),
321 crate::core::curves::Tenor::YearFraction(0.75),
322 crate::core::curves::Tenor::YearFraction(1.5),
323 ],
324 &smiles,
325 reference(),
326 DayCountConvention::Act365,
327 )
328 .unwrap();
329 let yc = curve();
330 let lv = LocalVol::new(&surface, &yc, S0, 0.0, 0.0);
331 let cfg = SlvConfig { paths: 16_000, steps: 40, bins: 20, seed: 3 };
332 let slv = calibrate(&lv, &mixing_heston(), S0, R, 0.0, 1.0, &cfg);
333
334 for k in [90.0, 100.0, 110.0] {
335 let t = 0.75;
336 let mc = slv.price_vanilla(k, t, PutOrCall::Call, 32_000, 5);
337 let iv = implied_vol_from_price(S0, k, R, 0.0, t, mc, PutOrCall::Call)
338 .expect("inverting the SLV price");
339 let market = smile(k);
340 assert!(
341 (iv - market).abs() < 0.01,
342 "K = {k}: slv implied {iv:.4} vs market {market:.4}"
343 );
344 }
345 }
346
347 #[test]
348 fn zero_vol_of_vol_degenerates_to_pure_local_vol() {
349 let surface =
351 VolSurface::flat(0.25, reference(), DayCountConvention::Act365).unwrap();
352 let yc = curve();
353 let lv = LocalVol::new(&surface, &yc, S0, 0.0, 0.0);
354 let degenerate = HestonParams { v0: 1.0, kappa: 1.0, theta: 1.0, vol_of_vol: 1e-6, rho: 0.0 };
355 let cfg = SlvConfig { paths: 4_000, steps: 20, bins: 10, seed: 9 };
356 let slv = calibrate(&lv, °enerate, S0, R, 0.0, 1.0, &cfg);
357 for s in [80.0, 100.0, 125.0] {
359 let lev = slv.leverage(s, 0.5);
360 let sigma = lv.vol(s, 0.5);
361 assert!((lev - sigma).abs() < 1e-3, "S = {s}: {lev} vs {sigma}");
362 }
363 }
364}