1use std::io;
27use std::str::FromStr;
28use chrono::{Local, NaiveDate};
29use libm::exp;
30use rayon::prelude::*;
31
32use crate::core::utils::ContractStyle;
33use super::asian::{self, AsianStrikeType, AveragingType};
34use super::autocallable::AutocallablePayoff;
35use super::barrier::{BarrierDirection, KnockType};
36use super::heston::HestonParams;
37use super::local_vol::LocalVol;
38use super::vanila_option::{AsianPayoff, BarrierPayoff, EquityOption, EquityOptionBase, VanillaPayoff};
39use super::utils::{Engine, LongShort, Payoff};
40use crate::core::trade::PutOrCall;
41use crate::utils::RNG::{
42 path_normals, pseudo_normals, sobol_normals, BrownianBridge, QmcSequence,
43};
44use crate::core::quotes::Quote;
45use crate::core::curves::{Compounding, YieldCurve};
46use crate::core::daycount::DayCountConvention;
47use crate::core::data_models::EquityOptionData;
48use crate::core::vols::VolSurface;
49use crate::core::traits::Instrument;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum DiscretizationScheme {
57 Exact,
58 Euler,
59 Milstein,
60}
61
62impl FromStr for DiscretizationScheme {
63 type Err = String;
64 fn from_str(s: &str) -> Result<Self, Self::Err> {
65 match s.trim().to_lowercase().as_str() {
66 "exact" => Ok(DiscretizationScheme::Exact),
67 "euler" => Ok(DiscretizationScheme::Euler),
68 "milstein" => Ok(DiscretizationScheme::Milstein),
69 other => Err(format!("Invalid discretization scheme '{other}'")),
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum Sampler {
81 Sobol,
82 PseudoRandom,
83}
84
85impl FromStr for Sampler {
86 type Err = String;
87 fn from_str(s: &str) -> Result<Self, Self::Err> {
88 match s.trim().to_lowercase().as_str() {
89 "sobol" | "quasi" => Ok(Sampler::Sobol),
90 "pseudo" | "pseudorandom" | "pseudo_random" => Ok(Sampler::PseudoRandom),
91 other => Err(format!("Invalid sampler '{other}'")),
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum McModel {
101 Gbm,
102 LocalVol,
103 Heston,
104}
105
106impl FromStr for McModel {
107 type Err = String;
108 fn from_str(s: &str) -> Result<Self, Self::Err> {
109 match s.trim().to_lowercase().as_str() {
110 "gbm" | "blackscholes" | "bs" => Ok(McModel::Gbm),
111 "local_vol" | "localvol" | "lv" => Ok(McModel::LocalVol),
112 "heston" => Ok(McModel::Heston),
113 other => Err(format!("Invalid mc_model '{other}'")),
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy)]
119pub struct MonteCarloConfig {
120 pub paths: usize,
121 pub time_steps: usize,
124 pub scheme: DiscretizationScheme,
125 pub sampler: Sampler,
126 pub model: McModel,
127 pub seed: u64,
128}
129
130pub const LOCAL_VOL_MIN_STEPS: usize = 100;
131pub const HESTON_MIN_STEPS: usize = 250;
132pub const PATH_DEPENDENT_MIN_STEPS: usize = 100;
134
135impl Default for MonteCarloConfig {
136 fn default() -> Self {
137 MonteCarloConfig {
138 paths: 100_000,
139 time_steps: 1,
140 scheme: DiscretizationScheme::Exact,
141 sampler: Sampler::Sobol,
142 model: McModel::Gbm,
143 seed: 42,
144 }
145 }
146}
147
148impl MonteCarloConfig {
149 pub fn from_data(data: &EquityOptionData) -> Self {
150 let defaults = MonteCarloConfig::default();
151 let scheme = data
152 .mc_scheme
153 .as_deref()
154 .map(|s| s.parse::<DiscretizationScheme>().expect("Invalid mc_scheme"))
155 .unwrap_or(defaults.scheme);
156 let default_steps = match scheme {
158 DiscretizationScheme::Exact => 1,
159 _ => 252,
160 };
161 let model = data
162 .mc_model
163 .as_deref()
164 .map(|s| s.parse::<McModel>().expect("Invalid mc_model"))
165 .unwrap_or(defaults.model);
166 MonteCarloConfig {
167 paths: data.simulation.unwrap_or(defaults.paths as u64) as usize,
168 time_steps: data.mc_time_steps.unwrap_or(default_steps),
169 scheme,
170 sampler: data
171 .mc_sampler
172 .as_deref()
173 .map(|s| s.parse::<Sampler>().expect("Invalid mc_sampler"))
174 .unwrap_or(defaults.sampler),
175 model,
176 seed: data.mc_seed.unwrap_or(defaults.seed),
177 }
178 }
179}
180
181#[derive(Debug, Clone, Copy)]
188pub struct McStats {
189 pub pv: f64,
190 pub std_err: f64,
191 pub paths: usize,
192 pub steps: usize,
193}
194
195fn stats(sum: f64, sum_sq: f64, n: usize, steps: usize, offset: f64) -> McStats {
196 let nf = n as f64;
197 let mean = sum / nf;
198 let var = (sum_sq / nf - mean * mean).max(0.0);
199 McStats { pv: mean + offset, std_err: (var / nf).sqrt(), paths: n, steps }
200}
201
202#[derive(Debug, Clone, Copy)]
205struct MarketParams {
206 s0: f64,
207 strike: f64,
208 r: f64,
209 q: f64,
210 sigma: f64,
211 t: f64,
212}
213
214fn market_params(option: &EquityOption) -> MarketParams {
215 MarketParams {
216 s0: option.base.underlying_price.value(),
217 strike: option.base.strike_price,
218 r: option.base.risk_free_rate(),
219 q: option.base.carry_yield(),
220 sigma: option.base.volatility(),
221 t: option.time_to_maturity(),
222 }
223}
224
225fn dividends_per_step(option: &EquityOption, t: f64, steps: usize) -> Option<Vec<f64>> {
228 if option.base.cash_dividends.is_empty() {
229 return None;
230 }
231 let dt = t / steps as f64;
232 let mut buckets = vec![0.0; steps];
233 for (date, amount) in &option.base.cash_dividends {
234 let td = (*date - option.base.valuation_date).num_days() as f64 / 365.0;
235 if td > 0.0 && td <= t {
236 let idx = (((td / dt).ceil() as usize).max(1) - 1).min(steps - 1);
237 buckets[idx] += amount;
238 }
239 }
240 Some(buckets)
241}
242
243fn escrowed_spot(option: &EquityOption, p: &MarketParams) -> f64 {
251 let dr = p.r - option.base.risk_free_rate();
252 let mut pv = 0.0;
253 for (date, amount) in &option.base.cash_dividends {
254 let td = (*date - option.base.valuation_date).num_days() as f64 / 365.0;
255 if td > 0.0 && td <= p.t {
256 pv += amount * option.base.discount_curve.df(td) * ((p.q - dr) * td).exp();
259 }
260 }
261 p.s0 - pv
262}
263
264pub fn npv(option: &EquityOption) -> f64 {
265 npv_with_stats(option).pv
266}
267
268pub fn npv_with_stats(option: &EquityOption) -> McStats {
270 assert!(option.base.volatility() >= 0.0);
271 assert!(option.base.time_to_maturity() >= 0.0);
272 assert!(option.base.underlying_price.value >= 0.0);
273 price(option, &market_params(option))
274}
275
276fn price(option: &EquityOption, p: &MarketParams) -> McStats {
277 match option.payoff.exercise_style() {
278 ContractStyle::American => american_npv(option, p),
279 _ => european_npv(option, p),
280 }
281}
282
283pub fn delta(option: &EquityOption) -> f64 {
286 let p = market_params(option);
287 let h = p.s0 * 0.01;
288 (price(option, &MarketParams { s0: p.s0 + h, ..p }).pv
289 - price(option, &MarketParams { s0: p.s0 - h, ..p }).pv)
290 / (2.0 * h)
291}
292
293pub fn gamma(option: &EquityOption) -> f64 {
294 let p = market_params(option);
295 let h = p.s0 * 0.01;
296 (price(option, &MarketParams { s0: p.s0 + h, ..p }).pv - 2.0 * price(option, &p).pv
297 + price(option, &MarketParams { s0: p.s0 - h, ..p }).pv)
298 / (h * h)
299}
300
301pub fn vega(option: &EquityOption) -> f64 {
302 let p = market_params(option);
303 let h = 0.01;
304 (price(option, &MarketParams { sigma: p.sigma + h, ..p }).pv
305 - price(option, &MarketParams { sigma: p.sigma - h, ..p }).pv)
306 / (2.0 * h)
307}
308
309pub fn theta(option: &EquityOption) -> f64 {
310 let p = market_params(option);
312 let h = (1.0 / 365.0_f64).min(0.5 * p.t);
313 -(price(option, &MarketParams { t: p.t + h, ..p }).pv
314 - price(option, &MarketParams { t: p.t - h, ..p }).pv)
315 / (2.0 * h)
316}
317
318pub fn rho(option: &EquityOption) -> f64 {
319 let p = market_params(option);
320 let h = 1e-4;
321 (price(option, &MarketParams { r: p.r + h, ..p }).pv
322 - price(option, &MarketParams { r: p.r - h, ..p }).pv)
323 / (2.0 * h)
324}
325
326enum PathVol<'a> {
329 Const(f64),
330 Local(LocalVol<'a>),
331}
332
333impl PathVol<'_> {
334 fn vol(&self, s: f64, t: f64) -> f64 {
335 match self {
336 PathVol::Const(v) => *v,
337 PathVol::Local(lv) => lv.vol(s, t),
338 }
339 }
340}
341
342fn path_vol<'a>(option: &'a EquityOption, p: &MarketParams) -> PathVol<'a> {
343 match option.mc.model {
344 McModel::Gbm => PathVol::Const(p.sigma),
345 McModel::LocalVol => PathVol::Local(LocalVol::new(
346 &option.base.vol_surface,
347 &option.base.discount_curve,
348 option.base.underlying_price.value(),
351 option.base.carry_yield(),
352 p.sigma - option.base.volatility(),
354 )),
355 McModel::Heston => unreachable!("Heston paths are generated by the dedicated routes"),
356 }
357}
358
359fn effective_steps(cfg: &MonteCarloConfig) -> usize {
360 match cfg.model {
361 McModel::LocalVol => cfg.time_steps.max(LOCAL_VOL_MIN_STEPS),
362 McModel::Heston => cfg.time_steps.max(HESTON_MIN_STEPS),
363 McModel::Gbm => cfg.time_steps,
364 }
365}
366
367enum PathDraws {
374 Pseudo { seed: u64, sqrt_dt: f64 },
375 Qmc { seq: QmcSequence, bridge: BrownianBridge },
376}
377
378impl PathDraws {
379 fn new(cfg: &MonteCarloConfig, steps: usize, dt: f64) -> Self {
380 match cfg.sampler {
381 Sampler::Sobol => PathDraws::Qmc {
382 seq: QmcSequence::new(steps, cfg.seed),
383 bridge: BrownianBridge::new(steps, dt),
384 },
385 Sampler::PseudoRandom => PathDraws::Pseudo { seed: cfg.seed, sqrt_dt: dt.sqrt() },
386 }
387 }
388
389 fn pseudo(seed: u64, dt: f64) -> Self {
390 PathDraws::Pseudo { seed, sqrt_dt: dt.sqrt() }
391 }
392
393 fn fill(&self, index: usize, z: &mut [f64], w: &mut [f64], dw: &mut [f64]) {
395 match self {
396 PathDraws::Pseudo { seed, sqrt_dt } => {
397 path_normals(*seed, (index / 2) as u64, z);
398 let sign = if index % 2 == 0 { 1.0 } else { -1.0 };
399 for (d, zi) in dw.iter_mut().zip(z.iter()) {
400 *d = sign * sqrt_dt * zi;
401 }
402 }
403 PathDraws::Qmc { seq, bridge } => {
404 seq.normals(index as u64 + 1, z);
405 bridge.increments(z, w, dw);
406 }
407 }
408 }
409}
410
411fn step(scheme: DiscretizationScheme, s: f64, dt: f64, dw: f64, drift: f64, sigma: f64) -> f64 {
412 let next = match scheme {
413 DiscretizationScheme::Exact => s * exp((drift - 0.5 * sigma * sigma) * dt + sigma * dw),
414 DiscretizationScheme::Euler => s * (1.0 + drift * dt + sigma * dw),
415 DiscretizationScheme::Milstein => {
416 s * (1.0 + drift * dt + sigma * dw + 0.5 * sigma * sigma * (dw * dw - dt))
417 }
418 };
419 next.max(0.0)
420}
421
422const PATH_CHUNK: usize = 4096;
426
427fn run_paths<F>(paths: usize, steps: usize, draws: &PathDraws, eval: F) -> (f64, f64)
430where
431 F: Fn(&[f64], &mut Vec<f64>) -> f64 + Sync,
432{
433 let chunks = paths.div_ceil(PATH_CHUNK);
434 let partials: Vec<(f64, f64)> = (0..chunks)
435 .into_par_iter()
436 .map(|chunk| {
437 let mut z = vec![0.0; steps];
438 let mut w = vec![0.0; steps];
439 let mut dw = vec![0.0; steps];
440 let mut scratch = Vec::new();
441 let (mut sum, mut sum_sq) = (0.0, 0.0);
442 for i in chunk * PATH_CHUNK..((chunk + 1) * PATH_CHUNK).min(paths) {
443 draws.fill(i, &mut z, &mut w, &mut dw);
444 let v = eval(&dw, &mut scratch);
445 sum += v;
446 sum_sq += v * v;
447 }
448 (sum, sum_sq)
449 })
450 .collect();
451 partials.into_iter().fold((0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1))
452}
453
454fn european_npv(option: &EquityOption, p: &MarketParams) -> McStats {
457 if option.mc.model == McModel::Heston {
458 return heston_european_npv(option, p);
459 }
460 if option.payoff.is_path_dependent() {
461 return if let Some(barrier) = option.payoff.as_any().downcast_ref::<BarrierPayoff>() {
465 barrier_npv(option, barrier, p)
466 } else if let Some(asian) = option.payoff.as_any().downcast_ref::<AsianPayoff>() {
467 asian_npv(option, asian, p)
468 } else if let Some(auto) = option.payoff.as_any().downcast_ref::<AutocallablePayoff>() {
469 autocall_npv(option, auto, p)
470 } else {
471 generic_path_npv(option, p)
472 };
473 }
474 let cfg = &option.mc;
475 let steps = effective_steps(cfg);
476 let df = exp(-p.r * p.t);
477 if steps <= 1 {
478 let drift = (p.r - p.q - 0.5 * p.sigma * p.sigma) * p.t;
480 let vol_sqrt_t = p.sigma * p.t.sqrt();
481 let s0 = escrowed_spot(option, p);
482 let z = match cfg.sampler {
483 Sampler::Sobol => sobol_normals(cfg.paths),
484 Sampler::PseudoRandom => pseudo_normals(cfg.paths, cfg.seed),
485 };
486 let partials: Vec<(f64, f64)> = z
487 .par_chunks(PATH_CHUNK)
488 .map(|chunk| {
489 let (mut sum, mut sum_sq) = (0.0, 0.0);
490 for z in chunk {
491 let v = df
492 * option.payoff.payoff(s0 * exp(drift + vol_sqrt_t * z), p.strike);
493 sum += v;
494 sum_sq += v * v;
495 }
496 (sum, sum_sq)
497 })
498 .collect();
499 let (sum, sum_sq) =
500 partials.into_iter().fold((0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1));
501 return stats(sum, sum_sq, cfg.paths, 1, 0.0);
502 }
503 let dt = p.t / steps as f64;
504 let vol_model = path_vol(option, p);
505 let draws = PathDraws::new(cfg, steps, dt);
506 let divs = dividends_per_step(option, p.t, steps);
507 let drift = p.r - p.q;
508 let (sum, sum_sq) = run_paths(cfg.paths, steps, &draws, |dw, _| {
509 let mut s = p.s0;
510 for (i, d) in dw.iter().enumerate() {
511 let sigma = vol_model.vol(s, i as f64 * dt);
512 s = step(cfg.scheme, s, dt, *d, drift, sigma);
513 if let Some(divs) = &divs {
514 s = (s - divs[i]).max(1e-8);
515 }
516 }
517 df * option.payoff.payoff(s, p.strike)
518 });
519 stats(sum, sum_sq, cfg.paths, steps, 0.0)
520}
521
522fn generic_path_npv(option: &EquityOption, p: &MarketParams) -> McStats {
525 let cfg = &option.mc;
526 let steps = effective_steps(cfg).max(PATH_DEPENDENT_MIN_STEPS);
527 let dt = p.t / steps as f64;
528 let df = exp(-p.r * p.t);
529 let vol_model = path_vol(option, p);
530 let draws = PathDraws::new(cfg, steps, dt);
531 let drift = p.r - p.q;
532 let divs = dividends_per_step(option, p.t, steps);
533 let (sum, sum_sq) = run_paths(cfg.paths, steps, &draws, |dw, path| {
534 path.clear();
535 let mut s = p.s0;
536 for (i, d) in dw.iter().enumerate() {
537 let sigma = vol_model.vol(s, i as f64 * dt);
538 s = step(cfg.scheme, s, dt, *d, drift, sigma);
539 if let Some(divs) = &divs {
540 s = (s - divs[i]).max(1e-8);
541 }
542 path.push(s);
543 }
544 df * option.payoff.path_payoff(path, p.strike)
545 });
546 stats(sum, sum_sq, cfg.paths, steps, 0.0)
547}
548
549fn asian_npv(option: &EquityOption, asian: &AsianPayoff, p: &MarketParams) -> McStats {
556 let cfg = &option.mc;
557 let use_control_variate = asian.averaging == AveragingType::Arithmetic
558 && asian.strike_type == AsianStrikeType::FixedStrike
559 && cfg.model == McModel::Gbm
560 && cfg.scheme == DiscretizationScheme::Exact
561 && option.base.cash_dividends.is_empty();
562 if !use_control_variate {
563 return generic_path_npv(option, p);
564 }
565 let steps = effective_steps(cfg).max(PATH_DEPENDENT_MIN_STEPS);
566 let dt = p.t / steps as f64;
567 let drift_dt = (p.r - p.q - 0.5 * p.sigma * p.sigma) * dt;
568 let df = exp(-p.r * p.t);
569 let draws = PathDraws::new(cfg, steps, dt);
570 let (sum, sum_sq) = run_paths(cfg.paths, steps, &draws, |dw, _| {
571 let mut s = p.s0;
572 let mut sum_s = 0.0;
573 let mut log_sum = 0.0;
574 for d in dw {
575 s *= exp(drift_dt + p.sigma * d);
576 sum_s += s;
577 log_sum += s.ln();
578 }
579 let arithmetic = sum_s / steps as f64;
580 let geometric = (log_sum / steps as f64).exp();
581 df * (option.payoff.payoff(arithmetic, p.strike)
582 - option.payoff.payoff(geometric, p.strike))
583 });
584 let geo_closed = asian::geometric_asian_price(
585 p.s0,
586 p.strike,
587 p.r,
588 p.q,
589 p.sigma,
590 p.t,
591 Some(steps),
592 *option.payoff.put_or_call(),
593 );
594 stats(sum, sum_sq, cfg.paths, steps, geo_closed)
595}
596
597fn barrier_npv(option: &EquityOption, barrier: &BarrierPayoff, p: &MarketParams) -> McStats {
602 let cfg = &option.mc;
603 let steps = effective_steps(cfg).max(PATH_DEPENDENT_MIN_STEPS);
604 let dt = p.t / steps as f64;
605 let down = barrier.direction == BarrierDirection::Down;
606 let out = barrier.knock == KnockType::Out;
607 let h = barrier.barrier;
608 let knocked_at_start = if down { p.s0 <= h } else { p.s0 >= h };
609 if knocked_at_start && out {
610 return McStats { pv: 0.0, std_err: 0.0, paths: cfg.paths, steps };
611 }
612 let df = exp(-p.r * p.t);
613 let vol_model = path_vol(option, p);
614 let draws = PathDraws::new(cfg, steps, dt);
615 let drift = p.r - p.q;
616 let divs = dividends_per_step(option, p.t, steps);
617 let (sum, sum_sq) = run_paths(cfg.paths, steps, &draws, |dw, _| {
618 let mut s = p.s0;
619 let mut survival = if knocked_at_start { 0.0 } else { 1.0 };
620 for (i, d) in dw.iter().enumerate() {
621 let sigma = vol_model.vol(s, i as f64 * dt);
622 let mut s_next = step(cfg.scheme, s, dt, *d, drift, sigma);
623 if let Some(divs) = &divs {
624 s_next = (s_next - divs[i]).max(1e-8);
625 }
626 if survival > 0.0 {
627 let crossed = if down { s_next <= h } else { s_next >= h };
628 if crossed {
629 survival = 0.0;
630 } else {
631 let (a, b) = if down {
633 ((s / h).ln(), (s_next / h).ln())
634 } else {
635 ((h / s).ln(), (h / s_next).ln())
636 };
637 survival *= 1.0 - (-2.0 * a * b / (sigma * sigma * dt)).exp();
638 }
639 }
640 s = s_next;
641 }
642 let vanilla_leg = option.payoff.payoff(s, p.strike);
643 let weight = if out { survival } else { 1.0 - survival };
644 df * weight * vanilla_leg
645 });
646 stats(sum, sum_sq, cfg.paths, steps, 0.0)
647}
648
649fn autocall_npv(option: &EquityOption, auto: &AutocallablePayoff, p: &MarketParams) -> McStats {
655 let cfg = &option.mc;
656 let n_obs = auto.observations.max(1);
657 let steps = effective_steps(cfg).max(PATH_DEPENDENT_MIN_STEPS).div_ceil(n_obs) * n_obs;
658 let dt = p.t / steps as f64;
659 let obs_idx: Vec<usize> = (1..=n_obs).map(|m| m * steps / n_obs - 1).collect();
660 let dr = p.r - option.base.risk_free_rate();
661 let dfs: Vec<f64> = obs_idx
662 .iter()
663 .map(|&i| {
664 let tm = (i + 1) as f64 * dt;
665 option.base.discount_curve.df(tm) * exp(-dr * tm)
666 })
667 .collect();
668 let divs = dividends_per_step(option, p.t, steps);
669 let vol_model = path_vol(option, p);
670 let draws = PathDraws::new(cfg, steps, dt);
671 let drift = p.r - p.q;
672 let (sum, sum_sq) = run_paths(cfg.paths, steps, &draws, |dw, path| {
673 path.clear();
674 let mut s = p.s0;
675 for (i, d) in dw.iter().enumerate() {
676 let sigma = vol_model.vol(s, i as f64 * dt);
677 s = step(cfg.scheme, s, dt, *d, drift, sigma);
678 if let Some(divs) = &divs {
679 s = (s - divs[i]).max(1e-8);
680 }
681 path.push(s);
682 }
683 auto.path_value(path, &obs_idx, &dfs)
684 });
685 stats(sum, sum_sq, cfg.paths, steps, 0.0)
686}
687
688fn heston_european_npv(option: &EquityOption, p: &MarketParams) -> McStats {
695 let hp = option
696 .heston
697 .expect("heston parameters are required when mc_model = heston")
698 .with_vol_shift(p.sigma - option.base.volatility());
699 let cfg = &option.mc;
700 let steps = effective_steps(cfg);
701 let dt = p.t / steps as f64;
702 let df = exp(-p.r * p.t);
703
704 if let Some(barrier) = option.payoff.as_any().downcast_ref::<BarrierPayoff>() {
705 let down = barrier.direction == BarrierDirection::Down;
706 let out = barrier.knock == KnockType::Out;
707 let h = barrier.barrier;
708 let knocked_at_start = if down { p.s0 <= h } else { p.s0 >= h };
709 if knocked_at_start && out {
710 return McStats { pv: 0.0, std_err: 0.0, paths: cfg.paths, steps };
711 }
712 let (sum, sum_sq) = run_heston_paths(option, p, &hp, steps, dt, |spots, vols| {
713 let mut survival = if knocked_at_start { 0.0 } else { 1.0 };
714 let mut s_prev = p.s0;
715 for (i, &s_next) in spots.iter().enumerate() {
716 if survival > 0.0 {
717 let crossed = if down { s_next <= h } else { s_next >= h };
718 if crossed {
719 survival = 0.0;
720 } else {
721 let (a, b) = if down {
722 ((s_prev / h).ln(), (s_next / h).ln())
723 } else {
724 ((h / s_prev).ln(), (h / s_next).ln())
725 };
726 let sigma = vols[i].max(1e-8);
727 survival *= 1.0 - (-2.0 * a * b / (sigma * sigma * dt)).exp();
728 }
729 }
730 s_prev = s_next;
731 }
732 let weight = if out { survival } else { 1.0 - survival };
733 df * weight * option.payoff.payoff(s_prev, p.strike)
734 });
735 return stats(sum, sum_sq, cfg.paths, steps, 0.0);
736 }
737
738 if let Some(auto) = option.payoff.as_any().downcast_ref::<AutocallablePayoff>() {
739 let n_obs = auto.observations.max(1);
740 let steps = steps.div_ceil(n_obs) * n_obs;
741 let dt = p.t / steps as f64;
742 let obs_idx: Vec<usize> = (1..=n_obs).map(|m| m * steps / n_obs - 1).collect();
743 let dr = p.r - option.base.risk_free_rate();
744 let dfs: Vec<f64> = obs_idx
745 .iter()
746 .map(|&i| {
747 let tm = (i + 1) as f64 * dt;
748 option.base.discount_curve.df(tm) * exp(-dr * tm)
749 })
750 .collect();
751 let (sum, sum_sq) = run_heston_paths(option, p, &hp, steps, dt, |spots, _| {
752 auto.path_value(spots, &obs_idx, &dfs)
753 });
754 return stats(sum, sum_sq, cfg.paths, steps, 0.0);
755 }
756
757 let path_dependent = option.payoff.is_path_dependent();
758 let (sum, sum_sq) = run_heston_paths(option, p, &hp, steps, dt, |spots, _| {
759 let v = if path_dependent {
760 option.payoff.path_payoff(spots, p.strike)
761 } else {
762 option.payoff.payoff(*spots.last().unwrap(), p.strike)
763 };
764 df * v
765 });
766 stats(sum, sum_sq, cfg.paths, steps, 0.0)
767}
768
769fn run_heston_paths<F>(
772 option: &EquityOption,
773 p: &MarketParams,
774 hp: &HestonParams,
775 steps: usize,
776 dt: f64,
777 eval: F,
778) -> (f64, f64)
779where
780 F: Fn(&[f64], &[f64]) -> f64 + Sync,
781{
782 let cfg = &option.mc;
783 let drift = p.r - p.q;
784 let sqrt_dt = dt.sqrt();
785 let rho = hp.rho;
786 let rho_perp = (1.0 - rho * rho).sqrt();
787 let divs = dividends_per_step(option, p.t, steps);
788 let chunks = cfg.paths.div_ceil(PATH_CHUNK);
789 let partials: Vec<(f64, f64)> = (0..chunks)
790 .into_par_iter()
791 .map(|chunk| {
792 let mut z = vec![0.0; 2 * steps];
793 let mut spots = vec![0.0; steps];
794 let mut vols = vec![0.0; steps];
795 let (mut sum, mut sum_sq) = (0.0, 0.0);
796 for i in chunk * PATH_CHUNK..((chunk + 1) * PATH_CHUNK).min(cfg.paths) {
797 path_normals(cfg.seed, (i / 2) as u64, &mut z);
799 let sign = if i % 2 == 0 { 1.0 } else { -1.0 };
800 let mut s = p.s0;
801 let mut v = hp.v0;
802 for j in 0..steps {
803 let z_s = sign * z[2 * j];
804 let z_v = rho * z_s + rho_perp * sign * z[2 * j + 1];
805 let v_pos = v.max(0.0);
806 let sqrt_v = v_pos.sqrt();
807 s *= exp((drift - 0.5 * v_pos) * dt + sqrt_v * sqrt_dt * z_s);
808 if let Some(divs) = &divs {
809 s = (s - divs[j]).max(1e-8);
810 }
811 v += hp.kappa * (hp.theta - v_pos) * dt
812 + hp.vol_of_vol * sqrt_v * sqrt_dt * z_v;
813 spots[j] = s;
814 vols[j] = sqrt_v;
815 }
816 let value = eval(&spots, &vols);
817 sum += value;
818 sum_sq += value * value;
819 }
820 (sum, sum_sq)
821 })
822 .collect();
823 partials.into_iter().fold((0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1))
824}
825
826const LSMC_DEFAULT_STEPS: usize = 50;
829const LSMC_BASIS: usize = 4;
830
831fn lsmc_basis(x: f64) -> [f64; LSMC_BASIS] {
836 [1.0, x, x * x, x * x * x]
837}
838
839fn american_npv(option: &EquityOption, p: &MarketParams) -> McStats {
845 let cfg = &option.mc;
846 if cfg.model == McModel::Heston {
847 panic!("American exercise under the Heston model is not supported yet");
848 }
849 let steps = if cfg.time_steps > 1 { cfg.time_steps } else { LSMC_DEFAULT_STEPS }
850 .max(if cfg.model == McModel::LocalVol { LOCAL_VOL_MIN_STEPS } else { 1 });
851 let dt = p.t / steps as f64;
852 let disc = exp(-p.r * dt);
853 let vol_model = path_vol(option, p);
854 let drift = p.r - p.q;
855 let seed_regression = cfg.seed ^ 0xA11C_E5ED;
856 let seed_valuation = cfg.seed ^ 0xB0B5_1EED;
857
858 let simulate = |draws: &PathDraws, index: usize, bufs: &mut (Vec<f64>, Vec<f64>, Vec<f64>), path: &mut Vec<f64>| {
859 let (z, w, dw) = bufs;
860 draws.fill(index, z, w, dw);
861 path.clear();
862 let mut s = p.s0;
863 for (i, d) in dw.iter().enumerate() {
864 let sigma = vol_model.vol(s, i as f64 * dt);
865 s = step(cfg.scheme, s, dt, *d, drift, sigma);
866 path.push(s);
867 }
868 };
869
870 let reg_draws = PathDraws::pseudo(seed_regression, dt);
872 let spots: Vec<Vec<f64>> = (0..cfg.paths)
873 .into_par_iter()
874 .map_init(
875 || (vec![0.0; steps], vec![0.0; steps], vec![0.0; steps]),
876 |bufs, i| {
877 let mut path = Vec::with_capacity(steps);
878 simulate(®_draws, i, bufs, &mut path);
879 path
880 },
881 )
882 .collect();
883
884 let mut cashflow: Vec<f64> =
885 spots.iter().map(|path| option.payoff.payoff(path[steps - 1], p.strike)).collect();
886 let mut betas: Vec<Option<[f64; LSMC_BASIS]>> = vec![None; steps.saturating_sub(1)];
887 for step_idx in (0..steps - 1).rev() {
888 for cf in cashflow.iter_mut() {
889 *cf *= disc;
890 }
891 let itm: Vec<usize> = (0..spots.len())
892 .filter(|&i| option.payoff.payoff(spots[i][step_idx], p.strike) > 0.0)
893 .collect();
894 if itm.len() < LSMC_BASIS {
895 continue;
896 }
897 let rows: Vec<([f64; LSMC_BASIS], f64)> = itm
898 .iter()
899 .map(|&i| {
900 let s = spots[i][step_idx];
901 let pay = option.payoff.payoff(s, p.strike);
902 (lsmc_basis(s / p.s0), cashflow[i])
903 })
904 .collect();
905 let Some(beta) = least_squares(&rows) else { continue };
906 for &i in &itm {
907 let s = spots[i][step_idx];
908 let pay = option.payoff.payoff(s, p.strike);
909 let continuation = dot(&beta, &lsmc_basis(s / p.s0));
910 if pay > continuation {
911 cashflow[i] = pay;
912 }
913 }
914 betas[step_idx] = Some(beta);
915 }
916 drop(spots);
917 drop(cashflow);
918
919 let val_draws = PathDraws::pseudo(seed_valuation, dt);
921 let partials: Vec<(f64, f64)> = (0..cfg.paths.div_ceil(PATH_CHUNK))
922 .into_par_iter()
923 .map(|chunk| {
924 let mut bufs = (vec![0.0; steps], vec![0.0; steps], vec![0.0; steps]);
925 let mut path = Vec::with_capacity(steps);
926 let (mut c_sum, mut c_sum_sq) = (0.0, 0.0);
927 for i in chunk * PATH_CHUNK..((chunk + 1) * PATH_CHUNK).min(cfg.paths) {
928 simulate(&val_draws, i, &mut bufs, &mut path);
929 let mut value = 0.0;
930 let mut exercised = false;
931 for k in 0..steps - 1 {
932 let s = path[k];
933 let pay = option.payoff.payoff(s, p.strike);
934 if pay > 0.0 {
935 if let Some(beta) = &betas[k] {
936 let continuation = dot(beta, &lsmc_basis(s / p.s0));
937 if pay > continuation {
938 value = pay * disc.powi(k as i32 + 1);
939 exercised = true;
940 break;
941 }
942 }
943 }
944 }
945 if !exercised {
946 value = option.payoff.payoff(path[steps - 1], p.strike)
947 * disc.powi(steps as i32);
948 }
949 c_sum += value;
950 c_sum_sq += value * value;
951 }
952 (c_sum, c_sum_sq)
953 })
954 .collect();
955 let (sum, sum_sq) = partials.into_iter().fold((0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1));
956 stats(sum, sum_sq, cfg.paths, steps, 0.0)
957}
958
959fn dot(a: &[f64; LSMC_BASIS], b: &[f64; LSMC_BASIS]) -> f64 {
960 a.iter().zip(b).map(|(x, y)| x * y).sum()
961}
962
963fn least_squares(rows: &[([f64; LSMC_BASIS], f64)]) -> Option<[f64; LSMC_BASIS]> {
966 let k = LSMC_BASIS;
967 let mut m = [[0.0; LSMC_BASIS + 1]; LSMC_BASIS];
968 for (basis, y) in rows {
969 for i in 0..k {
970 for j in 0..k {
971 m[i][j] += basis[i] * basis[j];
972 }
973 m[i][k] += basis[i] * y;
974 }
975 }
976 for col in 0..k {
977 let pivot =
978 (col..k).max_by(|&i, &j| m[i][col].abs().partial_cmp(&m[j][col].abs()).unwrap())?;
979 if m[pivot][col].abs() < 1e-10 {
980 return None;
981 }
982 m.swap(col, pivot);
983 for row in col + 1..k {
984 let f = m[row][col] / m[col][col];
985 for c in col..=k {
986 m[row][c] -= f * m[col][c];
987 }
988 }
989 }
990 let mut beta = [0.0; LSMC_BASIS];
991 for row in (0..k).rev() {
992 let mut acc = m[row][k];
993 for c in row + 1..k {
994 acc -= m[row][c] * beta[c];
995 }
996 beta[row] = acc / m[row][row];
997 }
998 Some(beta)
999}
1000
1001pub fn option_pricing() {
1004 println!("Welcome to the Monte Carlo Option pricer.");
1005 println!("(Step 1/7) What is the current price of the underlying asset?");
1006 let mut curr_price = String::new();
1007 io::stdin()
1008 .read_line(&mut curr_price)
1009 .expect("Failed to read line");
1010
1011 println!("(Step 2/7) Do you want a call option ('C') or a put option ('P') ?");
1012 let mut side_input = String::new();
1013 io::stdin()
1014 .read_line(&mut side_input)
1015 .expect("Failed to read line");
1016
1017 let side: PutOrCall;
1018 match side_input.trim() {
1019 "C" | "c" | "Call" | "call" => side = PutOrCall::Call,
1020 "P" | "p" | "Put" | "put" => side = PutOrCall::Put,
1021 _ => panic!("Invalide side argument! Side has to be either 'C' or 'P'."),
1022 }
1023
1024 println!("Stike price:");
1025 let mut strike = String::new();
1026 io::stdin()
1027 .read_line(&mut strike)
1028 .expect("Failed to read line");
1029
1030 println!("Expected annualized volatility in %:");
1031 println!("E.g.: Enter 50% chance as 0.50 ");
1032 let mut vol = String::new();
1033 io::stdin()
1034 .read_line(&mut vol)
1035 .expect("Failed to read line");
1036
1037 println!("Risk-free rate in %:");
1038 let mut rf = String::new();
1039 io::stdin().read_line(&mut rf).expect("Failed to read line");
1040
1041 println!("Maturity date in YYYY-MM-DD format:");
1042 let mut expiry = String::new();
1043 io::stdin()
1044 .read_line(&mut expiry)
1045 .expect("Failed to read line");
1046 let future_date = NaiveDate::parse_from_str(&expiry.trim(), "%Y-%m-%d").expect("Invalid date format");
1047 println!("Dividend yield on this stock:");
1048 let mut div = String::new();
1049 io::stdin()
1050 .read_line(&mut div)
1051 .expect("Failed to read line");
1052
1053 let valuation_date = Local::now().date_naive();
1054 let discount_curve = YieldCurve::flat(
1055 rf.trim().parse::<f64>().unwrap(),
1056 valuation_date,
1057 DayCountConvention::Act365,
1058 Compounding::Continuous,
1059 )
1060 .expect("Invalid risk free rate");
1061 let vol_surface = VolSurface::flat(
1062 vol.trim().parse::<f64>().unwrap(),
1063 valuation_date,
1064 DayCountConvention::Act365,
1065 )
1066 .expect("Invalid volatility");
1067 let curr_quote = Quote::new(curr_price.trim().parse::<f64>().unwrap());
1068 let option = EquityOptionBase {
1069 symbol: "ABC".to_string(),
1070 currency: None,
1071 exchange: None,
1072 name: None,
1073 cusip: None,
1074 isin: None,
1075 settlement_type: Some("ABC".to_string()),
1076 entry_price: 0.0,
1077 long_short: LongShort::LONG,
1078 underlying_price: curr_quote,
1079 current_price: Quote::new(0.0),
1080 strike_price: strike.trim().parse::<f64>().unwrap(),
1081 vol_surface,
1082 maturity_date: future_date,
1083 discount_curve,
1084 dividend_yield: div.trim().parse::<f64>().unwrap(),
1085 borrow_cost: 0.0,
1086 cash_dividends: vec![],
1087 futures_settlement: None,
1088 valuation_date,
1089 multiplier: 1.0,
1090 };
1091 println!("{:?}", option.time_to_maturity());
1092 let payoff = Box::new(VanillaPayoff {
1093 put_or_call: side,
1094 exercise_style: crate::core::utils::ContractStyle::European,
1095 });
1096 let equityoption = EquityOption {
1097 base: option,
1098 payoff: payoff,
1099 engine: Engine::MonteCarlo,
1100 mc: MonteCarloConfig::default(),
1101 fd: crate::equity::finite_difference::FdConfig::default(),
1102 heston: None,
1103 };
1104
1105 let result = npv_with_stats(&equityoption);
1106 println!("Theoretical Price ${} (std err {})", result.pv, result.std_err);
1107 let mut wait = String::new();
1108 io::stdin()
1109 .read_line(&mut wait)
1110 .expect("Failed to read line");
1111}