1use std::collections::HashMap;
43
44use crate::core::results::{Greeks, PricingResult};
45use crate::equity::blackscholes::BlackScholesPricer;
46use crate::equity::utils::PricingEngine;
47use crate::equity::vanilla_option::EquityOption;
48use crate::equity::{baw, binomial, finite_difference, heston, montecarlo};
49
50#[derive(Debug, Clone, Copy)]
56struct BumpPolicy {
57 hs1: f64,
59 hs2: f64,
62 hv: f64,
64 hv_volga: f64,
66 hr: f64,
68 ht: f64,
70}
71
72fn maturity_bump(option: &EquityOption) -> f64 {
73 (1.0 / 365.0_f64).min(0.5 * option.time_to_maturity())
74}
75
76enum Route {
78 Grid,
79 Tree,
80 Analytic,
81 Bump(BumpPolicy),
82}
83
84fn route(option: &EquityOption) -> Route {
85 match option.engine {
86 PricingEngine::MonteCarlo(_) => {
87 let s = option.market.spot.value();
88 Route::Bump(BumpPolicy {
89 hs1: s * 0.01,
90 hs2: s * 0.01,
91 hv: 0.01,
92 hv_volga: 0.01,
93 hr: 1e-4,
94 ht: maturity_bump(option),
95 })
96 }
97 PricingEngine::FiniteDifference(_) => Route::Grid,
98 PricingEngine::BaroneAdesiWhaley | PricingEngine::BjerksundStensland => {
99 let s = option.effective_spot();
101 Route::Bump(BumpPolicy {
102 hs1: s * 1e-4,
103 hs2: s * 1e-4,
104 hv: 1e-4,
105 hv_volga: 1e-3,
106 hr: 1e-4,
107 ht: maturity_bump(option),
108 })
109 }
110 _ if option.analytic_heston() => {
111 let s = option.market.spot.value();
112 Route::Bump(BumpPolicy {
113 hs1: s * 1e-4,
114 hs2: s * 1e-3,
115 hv: 1e-4,
116 hv_volga: 1e-2,
117 hr: 1e-5,
118 ht: maturity_bump(option),
119 })
120 }
121 PricingEngine::Binomial(_) => Route::Tree,
122 _ => Route::Analytic,
123 }
124}
125
126struct Repricer<'a> {
139 option: &'a EquityOption,
140 cache: HashMap<[u64; 4], f64>,
141 baw_kernels: Option<HashMap<[u64; 3], baw::SpotKernel>>,
143}
144
145impl<'a> Repricer<'a> {
146 fn new(option: &'a EquityOption) -> Self {
147 let baw_kernels = matches!(option.engine, PricingEngine::BaroneAdesiWhaley)
148 .then(HashMap::new);
149 Repricer { option, cache: HashMap::new(), baw_kernels }
150 }
151
152 fn v(&mut self, ds: f64, dv: f64, dr: f64, dt: f64) -> f64 {
153 let key = [ds.to_bits(), dv.to_bits(), dr.to_bits(), dt.to_bits()];
154 if let Some(&cached) = self.cache.get(&key) {
155 return cached;
156 }
157 let value = match &mut self.baw_kernels {
158 Some(kernels) => {
159 let kernel = kernels
160 .entry([dv.to_bits(), dr.to_bits(), dt.to_bits()])
161 .or_insert_with(|| baw::SpotKernel::new(self.option, dv, dr, dt));
162 kernel.value(self.option.effective_spot() + ds)
163 }
164 None => self.option.price_with(ds, dv, dr, -dt),
165 };
166 self.cache.insert(key, value);
167 value
168 }
169}
170
171fn bump_delta(r: &mut Repricer, p: &BumpPolicy) -> f64 {
174 let h = p.hs1;
175 (r.v(h, 0.0, 0.0, 0.0) - r.v(-h, 0.0, 0.0, 0.0)) / (2.0 * h)
176}
177
178fn bump_gamma(r: &mut Repricer, p: &BumpPolicy) -> f64 {
179 let h = p.hs2;
180 (r.v(h, 0.0, 0.0, 0.0) - 2.0 * r.v(0.0, 0.0, 0.0, 0.0) + r.v(-h, 0.0, 0.0, 0.0)) / (h * h)
181}
182
183fn bump_vega(r: &mut Repricer, p: &BumpPolicy) -> f64 {
184 let h = p.hv;
185 (r.v(0.0, h, 0.0, 0.0) - r.v(0.0, -h, 0.0, 0.0)) / (2.0 * h)
186}
187
188fn bump_theta(r: &mut Repricer, p: &BumpPolicy) -> f64 {
189 let h = p.ht;
191 -(r.v(0.0, 0.0, 0.0, h) - r.v(0.0, 0.0, 0.0, -h)) / (2.0 * h)
192}
193
194fn bump_rho(r: &mut Repricer, p: &BumpPolicy) -> f64 {
195 let h = p.hr;
196 (r.v(0.0, 0.0, h, 0.0) - r.v(0.0, 0.0, -h, 0.0)) / (2.0 * h)
197}
198
199fn bump_vanna(r: &mut Repricer, p: &BumpPolicy) -> f64 {
200 let (hs, hv) = (p.hs1, p.hv);
201 (r.v(hs, hv, 0.0, 0.0) - r.v(-hs, hv, 0.0, 0.0) - r.v(hs, -hv, 0.0, 0.0)
202 + r.v(-hs, -hv, 0.0, 0.0))
203 / (4.0 * hs * hv)
204}
205
206fn bump_charm(r: &mut Repricer, p: &BumpPolicy) -> f64 {
207 let (hs, ht) = (p.hs1, p.ht);
209 -(r.v(hs, 0.0, 0.0, ht) - r.v(-hs, 0.0, 0.0, ht) - r.v(hs, 0.0, 0.0, -ht)
210 + r.v(-hs, 0.0, 0.0, -ht))
211 / (4.0 * hs * ht)
212}
213
214fn bump_zomma(r: &mut Repricer, p: &BumpPolicy) -> f64 {
215 let (hs, hv) = (p.hs2, p.hv);
216 let gamma_at = |r: &mut Repricer, dv: f64| {
217 (r.v(hs, dv, 0.0, 0.0) - 2.0 * r.v(0.0, dv, 0.0, 0.0) + r.v(-hs, dv, 0.0, 0.0))
218 / (hs * hs)
219 };
220 (gamma_at(r, hv) - gamma_at(r, -hv)) / (2.0 * hv)
221}
222
223fn bump_volga(r: &mut Repricer, p: &BumpPolicy) -> f64 {
224 let h = p.hv_volga;
225 (r.v(0.0, h, 0.0, 0.0) - 2.0 * r.v(0.0, 0.0, 0.0, 0.0) + r.v(0.0, -h, 0.0, 0.0)) / (h * h)
226}
227
228macro_rules! greek {
231 ($name:ident, $stencil:ident, $grid:path, $tree:path, $analytic:ident) => {
232 pub fn $name(option: &EquityOption) -> f64 {
233 match route(option) {
234 Route::Grid => $grid(option),
235 Route::Tree => $tree(option),
236 Route::Analytic => BlackScholesPricer::new().$analytic(option),
237 Route::Bump(p) => $stencil(&mut Repricer::new(option), &p),
238 }
239 }
240 };
241}
242
243greek!(gamma, bump_gamma, finite_difference::gamma, binomial::gamma, gamma);
244greek!(theta, bump_theta, finite_difference::theta, binomial::theta, theta);
245greek!(vanna, bump_vanna, finite_difference::vanna, binomial::vanna, vanna);
246
247fn native_delta(option: &EquityOption) -> Option<f64> {
256 match option.engine {
257 PricingEngine::MonteCarlo(_) => montecarlo::pathwise_delta_vega(option)
258 .map(|(delta, _)| delta)
259 .or_else(|| montecarlo::aad_greeks(option).map(|g| g.delta)),
260 _ if option.analytic_heston() => heston::native_vanilla_delta(option),
261 _ => None,
262 }
263}
264
265fn native_vega(option: &EquityOption) -> Option<f64> {
266 match option.engine {
267 PricingEngine::MonteCarlo(_) => montecarlo::pathwise_delta_vega(option)
268 .map(|(_, vega)| vega)
269 .or_else(|| montecarlo::aad_greeks(option).map(|g| g.vega)),
270 _ => None,
271 }
272}
273
274fn native_rho(option: &EquityOption) -> Option<f64> {
275 match option.engine {
276 PricingEngine::MonteCarlo(_)
279 if montecarlo::pathwise_delta_vega(option).is_none() =>
280 {
281 montecarlo::aad_greeks(option).map(|g| g.rho)
282 }
283 _ => None,
284 }
285}
286
287pub fn delta(option: &EquityOption) -> f64 {
288 match route(option) {
289 Route::Grid => finite_difference::delta(option),
290 Route::Tree => binomial::delta(option),
291 Route::Analytic => BlackScholesPricer::new().delta(option),
292 Route::Bump(p) => native_delta(option)
293 .unwrap_or_else(|| bump_delta(&mut Repricer::new(option), &p)),
294 }
295}
296
297pub fn vega(option: &EquityOption) -> f64 {
298 match route(option) {
299 Route::Grid => finite_difference::vega(option),
300 Route::Tree => binomial::vega(option),
301 Route::Analytic => BlackScholesPricer::new().vega(option),
302 Route::Bump(p) => native_vega(option)
303 .unwrap_or_else(|| bump_vega(&mut Repricer::new(option), &p)),
304 }
305}
306
307pub fn rho(option: &EquityOption) -> f64 {
308 match route(option) {
309 Route::Grid => finite_difference::rho(option),
310 Route::Tree => binomial::rho(option),
311 Route::Analytic => BlackScholesPricer::new().rho(option),
312 Route::Bump(p) => native_rho(option)
313 .unwrap_or_else(|| bump_rho(&mut Repricer::new(option), &p)),
314 }
315}
316greek!(charm, bump_charm, finite_difference::charm, binomial::charm, charm);
317greek!(zomma, bump_zomma, finite_difference::zomma, binomial::zomma, zomma);
318greek!(volga, bump_volga, finite_difference::volga, binomial::volga, volga);
319
320pub fn gamma_p(option: &EquityOption) -> f64 {
322 let delta = delta(option);
323 if delta == 0.0 {
324 f64::NAN
325 } else {
326 option.market.spot.value() * gamma(option) / delta
327 }
328}
329
330pub fn pricing_result(option: &EquityOption) -> PricingResult {
337 match route(option) {
338 Route::Tree => binomial::pricing_result(option),
339 Route::Grid => finite_difference::pricing_result(option),
340 Route::Analytic => {
341 let pricer = BlackScholesPricer::new();
342 PricingResult {
343 pv: pricer.npv(option),
344 greeks: Greeks {
345 delta: pricer.delta(option),
346 gamma: pricer.gamma(option),
347 vega: pricer.vega(option),
348 theta: pricer.theta(option),
349 rho: pricer.rho(option),
350 vanna: pricer.vanna(option),
351 charm: pricer.charm(option),
352 gamma_p: pricer.gamma_p(option),
353 zomma: pricer.zomma(option),
354 },
355 std_err: None,
356 }
357 }
358 Route::Bump(p) => {
359 let (pv, std_err) = match option.engine {
360 PricingEngine::MonteCarlo(_) => {
361 let stats = montecarlo::npv_with_stats(option);
362 (stats.pv, Some(stats.std_err))
363 }
364 _ => (option.price_with(0.0, 0.0, 0.0, 0.0), None),
365 };
366 let pathwise = match option.engine {
369 PricingEngine::MonteCarlo(_) => montecarlo::pathwise_delta_vega(option),
370 _ => None,
371 };
372 let adjoint = match option.engine {
373 PricingEngine::MonteCarlo(_) if pathwise.is_none() => {
374 montecarlo::aad_greeks(option)
375 }
376 _ => None,
377 };
378 let r = &mut Repricer::new(option);
379 let delta = pathwise
380 .map(|(delta, _)| delta)
381 .or(adjoint.map(|g| g.delta))
382 .or_else(|| {
383 option.analytic_heston().then(|| heston::native_vanilla_delta(option)).flatten()
384 })
385 .unwrap_or_else(|| bump_delta(r, &p));
386 let vega = pathwise
387 .map(|(_, vega)| vega)
388 .or(adjoint.map(|g| g.vega))
389 .unwrap_or_else(|| bump_vega(r, &p));
390 let rho = adjoint.map(|g| g.rho).unwrap_or_else(|| bump_rho(r, &p));
391 let gamma = bump_gamma(r, &p);
392 let gamma_p = if delta == 0.0 {
393 f64::NAN
394 } else {
395 option.market.spot.value() * gamma / delta
396 };
397 PricingResult {
398 pv,
399 greeks: Greeks {
400 delta,
401 gamma,
402 vega,
403 theta: bump_theta(r, &p),
404 rho,
405 vanna: bump_vanna(r, &p),
406 charm: bump_charm(r, &p),
407 gamma_p,
408 zomma: bump_zomma(r, &p),
409 },
410 std_err,
411 }
412 }
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::core::trade::PutOrCall;
420 use crate::core::traits::Instrument;
421 use crate::equity::builder::EquityOptionBuilder;
422 use crate::equity::utils::{Engine, Model};
423 use chrono::NaiveDate;
424
425 fn option(engine: Engine, put_or_call: PutOrCall) -> EquityOption {
426 EquityOptionBuilder::new()
427 .symbol("ACME")
428 .spot(100.0)
429 .strike(100.0)
430 .flat_vol(0.25)
431 .flat_rate(0.03)
432 .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 5).unwrap())
433 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 4).unwrap())
434 .vanilla(put_or_call)
435 .engine(engine)
436 .build()
437 .expect("option must build")
438 }
439
440 #[test]
441 fn mc_pathwise_delta_and_vega_match_the_analytic_values() {
442 for pc in [PutOrCall::Call, PutOrCall::Put] {
443 let mc = option(Engine::MonteCarlo, pc);
444 let bs = option(Engine::BlackScholes, pc);
445 let (d, v) = montecarlo::pathwise_delta_vega(&mc).expect("pathwise must apply");
447 assert_eq!(d, mc.delta(), "accessor must use the pathwise estimator");
448 assert_eq!(v, mc.vega(), "accessor must use the pathwise estimator");
449 assert!((d - bs.delta()).abs() < 5e-3, "{pc:?} delta {d} vs {}", bs.delta());
451 assert!((v - bs.vega()).abs() < 0.2, "{pc:?} vega {v} vs {}", bs.vega());
452 let result = mc.price().unwrap();
454 assert_eq!(result.greeks.delta, d);
455 assert_eq!(result.greeks.vega, v);
456 }
457 }
458
459 #[test]
460 fn mc_pathwise_declines_out_of_scope_and_the_adjoint_takes_over() {
461 let mut mc = option(Engine::MonteCarlo, PutOrCall::Call);
464 if let PricingEngine::MonteCarlo(cfg) = &mut mc.engine {
465 cfg.time_steps = 12;
466 }
467 assert!(montecarlo::pathwise_delta_vega(&mc).is_none());
468 let adjoint = montecarlo::aad_greeks(&mc).expect("AAD must cover multi-step vanilla");
469 assert_eq!(mc.delta(), adjoint.delta, "accessor must use the adjoint estimator");
470 assert_eq!(mc.vega(), adjoint.vega);
471 assert_eq!(mc.rho(), adjoint.rho);
472 let bs = option(Engine::BlackScholes, PutOrCall::Call);
474 assert!((adjoint.delta - bs.delta()).abs() < 1e-2, "{} vs {}", adjoint.delta, bs.delta());
475 assert!((adjoint.vega - bs.vega()).abs() < 0.5, "{} vs {}", adjoint.vega, bs.vega());
476 assert!((adjoint.rho - bs.rho()).abs() < 0.5, "{} vs {}", adjoint.rho, bs.rho());
477 let result = mc.price().unwrap();
479 assert_eq!(result.greeks.delta, adjoint.delta);
480 assert_eq!(result.greeks.vega, adjoint.vega);
481 assert_eq!(result.greeks.rho, adjoint.rho);
482 }
483
484 #[test]
485 fn aad_covers_continuous_path_dependents_and_declines_discontinuous() {
486 use crate::core::utils::ContractStyle;
487 use crate::equity::asian::{AsianStrikeType, AveragingType};
488 use crate::equity::vanilla_option::AsianPayoff;
489 let mut asian = option(Engine::MonteCarlo, PutOrCall::Call);
491 asian.payoff = Box::new(AsianPayoff {
492 put_or_call: PutOrCall::Call,
493 exercise_style: ContractStyle::European,
494 averaging: AveragingType::Arithmetic,
495 strike_type: AsianStrikeType::FixedStrike,
496 });
497 let adjoint = montecarlo::aad_greeks(&asian).expect("AAD must cover Asians");
498 assert_eq!(asian.delta(), adjoint.delta);
499 let h = asian.market.spot.value() * 0.01;
501 let bump = (asian.price_with(h, 0.0, 0.0, 0.0) - asian.price_with(-h, 0.0, 0.0, 0.0))
502 / (2.0 * h);
503 assert!((adjoint.delta - bump).abs() < 0.03, "adjoint {} vs bump {bump}", adjoint.delta);
504 assert!(adjoint.vega > 0.0 && adjoint.rho > 0.0);
505
506 use crate::equity::barrier::{BarrierDirection, KnockType};
509 use crate::equity::vanilla_option::BarrierPayoff;
510 let mut barrier = option(Engine::MonteCarlo, PutOrCall::Call);
511 barrier.payoff = Box::new(BarrierPayoff {
512 put_or_call: PutOrCall::Call,
513 exercise_style: ContractStyle::European,
514 direction: BarrierDirection::Up,
515 knock: KnockType::Out,
516 barrier: 130.0,
517 barrier2: None,
518 rebate: 0.0,
519 rebate_at_hit: false,
520 });
521 assert!(montecarlo::aad_greeks(&barrier).is_none());
522 }
523
524 #[test]
525 fn heston_native_delta_matches_the_bump_stencil() {
526 use crate::equity::heston::HestonParams;
527 let params =
528 HestonParams { v0: 0.0625, kappa: 1.5, theta: 0.0625, vol_of_vol: 0.4, rho: -0.6 };
529 let mut call = option(Engine::BlackScholes, PutOrCall::Call);
530 call.model = Model::Heston(params);
531 let mut put = option(Engine::BlackScholes, PutOrCall::Put);
532 put.model = Model::Heston(params);
533 let h = call.market.spot.value() * 1e-4;
535 let stencil =
536 (call.price_with(h, 0.0, 0.0, 0.0) - call.price_with(-h, 0.0, 0.0, 0.0)) / (2.0 * h);
537 assert!(
538 (call.delta() - stencil).abs() < 1e-6,
539 "native {} vs stencil {stencil}",
540 call.delta()
541 );
542 assert!((call.delta() - put.delta() - 1.0).abs() < 1e-9);
544 assert_eq!(call.price().unwrap().greeks.delta, call.delta());
546 }
547
548 #[test]
549 fn baw_boundary_kernel_is_bit_identical_to_the_direct_reprice() {
550 let baw_put = EquityOptionBuilder::new()
552 .symbol("ACME")
553 .spot(100.0)
554 .strike(100.0)
555 .flat_vol(0.25)
556 .flat_rate(0.05)
557 .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 5).unwrap())
558 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 4).unwrap())
559 .vanilla(PutOrCall::Put)
560 .american()
561 .engine(Engine::BaroneAdesiWhaley)
562 .build()
563 .expect("option must build");
564 let h = baw_put.effective_spot() * 1e-4;
568 let direct_delta = (baw_put.price_with(h, 0.0, 0.0, 0.0)
569 - baw_put.price_with(-h, 0.0, 0.0, 0.0))
570 / (2.0 * h);
571 assert_eq!(baw_put.delta(), direct_delta);
572 let direct_gamma = (baw_put.price_with(h, 0.0, 0.0, 0.0)
573 - 2.0 * baw_put.price_with(0.0, 0.0, 0.0, 0.0)
574 + baw_put.price_with(-h, 0.0, 0.0, 0.0))
575 / (h * h);
576 assert_eq!(baw_put.gamma(), direct_gamma);
577 }
578}