1use libm::exp;
34use rayon::prelude::*;
35
36use crate::core::utils::ContractStyle;
37use super::asian::{self, AsianStrikeType, AveragingType};
38use super::accumulator::AccumulatorPayoff;
39use super::autocallable::AutocallablePayoff;
40use super::barrier::{BarrierDirection, KnockType};
41use super::heston::HestonParams;
42use super::local_vol::LocalVol;
43use super::processes::{BlackScholesProcess, HestonProcess, HestonScheme, VolDynamics};
44use super::vanilla_option::{AsianPayoff, BarrierPayoff, EquityOption, VanillaPayoff};
45use super::utils::Model;
46use crate::core::montecarlo::process::{StochasticProcess, StochasticProcess1D};
47use crate::core::trade::PutOrCall;
48use crate::core::montecarlo::{path_normals, pseudo_normals, sobol_normals, PathDraws};
49use crate::core::data_models::EquityOptionData;
50use crate::core::errors::RustyQLibError;
51
52pub use crate::core::montecarlo::process::DiscretizationScheme;
55
56pub use crate::core::montecarlo::paths::Sampler;
59
60#[derive(Debug, Clone, Copy, PartialEq)]
66pub struct MonteCarloConfig {
67 pub paths: usize,
68 pub time_steps: usize,
71 pub scheme: DiscretizationScheme,
72 pub sampler: Sampler,
73 pub seed: u64,
74}
75
76pub const LOCAL_VOL_MIN_STEPS: usize = 100;
77pub const HESTON_MIN_STEPS: usize = 250;
80pub const HESTON_QE_MIN_STEPS: usize = 25;
84pub const PATH_DEPENDENT_MIN_STEPS: usize = 100;
86
87impl Default for MonteCarloConfig {
88 fn default() -> Self {
89 MonteCarloConfig {
90 paths: 100_000,
91 time_steps: 1,
92 scheme: DiscretizationScheme::Exact,
93 sampler: Sampler::Sobol,
94 seed: 42,
95 }
96 }
97}
98
99impl MonteCarloConfig {
100 pub fn from_data(data: &EquityOptionData) -> Result<Self, RustyQLibError> {
103 let defaults = MonteCarloConfig::default();
104 let scheme = match data.mc_scheme.as_deref() {
105 Some(s) => s
106 .parse::<DiscretizationScheme>()
107 .map_err(|e| RustyQLibError::invalid_input("mc_scheme", e))?,
108 None => defaults.scheme,
109 };
110 let default_steps = match scheme {
112 DiscretizationScheme::Exact => 1,
113 _ => 252,
114 };
115 let sampler = match data.mc_sampler.as_deref() {
116 Some(s) => s
117 .parse::<Sampler>()
118 .map_err(|e| RustyQLibError::invalid_input("mc_sampler", e))?,
119 None => defaults.sampler,
120 };
121 Ok(MonteCarloConfig {
122 paths: data.simulation.unwrap_or(defaults.paths as u64) as usize,
123 time_steps: data.mc_time_steps.unwrap_or(default_steps),
124 scheme,
125 sampler,
126 seed: data.mc_seed.unwrap_or(defaults.seed),
127 })
128 }
129
130 pub fn validate(&self) -> Result<(), RustyQLibError> {
134 if self.paths == 0 {
135 return Err(RustyQLibError::invalid_input(
136 "paths",
137 "Monte Carlo needs at least one path",
138 ));
139 }
140 if self.time_steps == 0 {
141 return Err(RustyQLibError::invalid_input(
142 "mc_time_steps",
143 "Monte Carlo needs at least one time step",
144 ));
145 }
146 Ok(())
147 }
148}
149
150#[derive(Debug, Clone, Copy)]
157pub struct McStats {
158 pub pv: f64,
159 pub std_err: f64,
160 pub paths: usize,
161 pub steps: usize,
162}
163
164fn stats(sum: f64, sum_sq: f64, n: usize, steps: usize, offset: f64) -> McStats {
165 let (mean, std_err) = crate::core::montecarlo::mean_std_err(sum, sum_sq, n);
166 McStats { pv: mean + offset, std_err, paths: n, steps }
167}
168
169#[derive(Debug, Clone, Copy)]
172struct MarketParams {
173 s0: f64,
174 strike: f64,
175 r: f64,
176 q: f64,
177 sigma: f64,
178 t: f64,
179}
180
181fn market_params(option: &EquityOption) -> MarketParams {
182 MarketParams {
183 s0: option.market.spot.value(),
184 strike: option.base.strike_price,
185 r: option.risk_free_rate(),
186 q: option.carry_yield(),
187 sigma: option.volatility(),
188 t: option.time_to_maturity(),
189 }
190}
191
192fn dividends_per_step(option: &EquityOption, t: f64, steps: usize) -> Option<Vec<f64>> {
195 if option.market.cash_dividends.is_empty() {
196 return None;
197 }
198 let dt = t / steps as f64;
199 let mut buckets = vec![0.0; steps];
200 for (date, amount) in &option.market.cash_dividends {
201 let td = (*date - option.market.valuation_date).num_days() as f64 / 365.0;
202 if td > 0.0 && td <= t {
203 let idx = (((td / dt).ceil() as usize).max(1) - 1).min(steps - 1);
204 buckets[idx] += amount;
205 }
206 }
207 Some(buckets)
208}
209
210fn escrowed_spot(option: &EquityOption, p: &MarketParams) -> f64 {
218 let dr = p.r - option.risk_free_rate();
219 let mut pv = 0.0;
220 for (date, amount) in &option.market.cash_dividends {
221 let td = (*date - option.market.valuation_date).num_days() as f64 / 365.0;
222 if td > 0.0 && td <= p.t {
223 pv += amount * option.market.discount_curve.df(td) * ((p.q - dr) * td).exp();
226 }
227 }
228 p.s0 - pv
229}
230
231pub fn npv(option: &EquityOption) -> f64 {
232 npv_with_stats(option).pv
233}
234
235pub fn npv_with_stats(option: &EquityOption) -> McStats {
237 assert!(option.volatility() >= 0.0);
238 assert!(option.time_to_maturity() >= 0.0);
239 assert!(option.market.spot.mid() >= 0.0);
240 if let Some(barrier) = option.payoff.as_any().downcast_ref::<BarrierPayoff>() {
241 assert!(
242 !(barrier.rebate != 0.0 && barrier.rebate_at_hit),
243 "at-hit rebates need the touch time: price on the Analytical engine (Monte Carlo supports the at-expiry rebate convention)"
244 );
245 }
246 price(option, &market_params(option))
247}
248
249fn price(option: &EquityOption, p: &MarketParams) -> McStats {
250 match option.payoff.exercise_style() {
251 ContractStyle::American | ContractStyle::Bermudan(_) => american_npv(option, p),
252 _ => european_npv(option, p),
253 }
254}
255
256pub(crate) fn pathwise_delta_vega(option: &EquityOption) -> Option<(f64, f64)> {
280 let vanilla = option.payoff.as_any().downcast_ref::<VanillaPayoff>()?;
281 if !matches!(vanilla.exercise_style, ContractStyle::European) {
282 return None;
283 }
284 if option.model != Model::Gbm {
285 return None;
286 }
287 let cfg = option.mc_cfg();
288 if effective_steps(cfg, &option.model) > 1 {
289 return None;
290 }
291 let p = market_params(option);
292 let df = exp(-p.r * p.t);
293 let drift = (p.r - p.q - 0.5 * p.sigma * p.sigma) * p.t;
294 let sqrt_t = p.t.sqrt();
295 let vol_sqrt_t = p.sigma * sqrt_t;
296 let s0 = escrowed_spot(option, &p);
297 let z = match cfg.sampler {
298 Sampler::Sobol => sobol_normals(cfg.paths),
299 Sampler::PseudoRandom => pseudo_normals(cfg.paths, cfg.seed),
300 };
301 let sign = match vanilla.put_or_call {
302 PutOrCall::Call => 1.0,
303 PutOrCall::Put => -1.0,
304 };
305 let partials: Vec<(f64, f64)> = z
306 .par_chunks(PATH_CHUNK)
307 .map(|chunk| {
308 let (mut delta_sum, mut vega_sum) = (0.0, 0.0);
309 for z in chunk {
310 let s_t = s0 * exp(drift + vol_sqrt_t * z);
311 let in_the_money = match vanilla.put_or_call {
312 PutOrCall::Call => s_t > p.strike,
313 PutOrCall::Put => s_t < p.strike,
314 };
315 if in_the_money {
316 delta_sum += sign * s_t / s0;
317 vega_sum += sign * s_t * (sqrt_t * z - p.sigma * p.t);
318 }
319 }
320 (delta_sum, vega_sum)
321 })
322 .collect();
323 let (delta_sum, vega_sum) =
324 partials.into_iter().fold((0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1));
325 let n = cfg.paths as f64;
326 Some((df * delta_sum / n, df * vega_sum / n))
327}
328
329#[derive(Debug, Clone, Copy)]
331pub(crate) struct AadGreeks {
332 pub delta: f64,
333 pub vega: f64,
335 pub rho: f64,
336}
337
338pub(crate) fn aad_greeks(option: &EquityOption) -> Option<AadGreeks> {
358 if !matches!(option.payoff.exercise_style(), ContractStyle::European) {
359 return None;
360 }
361 match option.model {
362 Model::Gbm => gbm_aad_greeks(option),
363 Model::Heston(_) => heston_aad_greeks(option),
364 Model::LocalVol => None,
365 }
366}
367
368fn gbm_aad_greeks(option: &EquityOption) -> Option<AadGreeks> {
372 use crate::core::aad::{Tape, Var};
373 let cfg = option.mc_cfg();
374 let p = market_params(option);
375 let steps = if option.payoff.is_path_dependent() {
376 effective_steps(cfg, &option.model).max(PATH_DEPENDENT_MIN_STEPS)
377 } else {
378 effective_steps(cfg, &option.model).max(1)
379 };
380 {
382 let tape = Tape::new();
383 let probe: Vec<Var> = (0..steps.max(2)).map(|_| tape.var(p.s0)).collect();
384 option.payoff.path_payoff_var(&probe, p.strike)?;
385 }
386 let dt = p.t / steps as f64;
387 let draws = PathDraws::new(cfg.sampler, cfg.seed, steps, dt);
388 let divs = dividends_per_step(option, p.t, steps);
389 let chunks = cfg.paths.div_ceil(PATH_CHUNK);
390 let partials: Vec<(f64, f64, f64)> = (0..chunks)
391 .into_par_iter()
392 .map(|chunk| {
393 let mut z = vec![0.0; steps];
394 let mut w = vec![0.0; steps];
395 let mut dw = vec![0.0; steps];
396 let tape = Tape::new();
397 let mut path_vars: Vec<Var> = Vec::with_capacity(steps);
398 let (mut delta_sum, mut vega_sum, mut rho_sum) = (0.0, 0.0, 0.0);
399 for i in chunk * PATH_CHUNK..((chunk + 1) * PATH_CHUNK).min(cfg.paths) {
400 draws.fill(i, &mut z, &mut w, &mut dw);
401 tape.clear();
402 path_vars.clear();
403 let s0 = tape.var(p.s0);
404 let sigma = tape.var(p.sigma);
405 let r = tape.var(p.r);
406 let mut s = s0;
407 for (step_idx, dwi) in dw.iter().enumerate() {
408 s = match cfg.scheme {
409 DiscretizationScheme::Exact => {
411 let exponent =
412 (r - p.q) * dt - sigma * sigma * (0.5 * dt) + sigma * *dwi;
413 s * exponent.exp()
414 }
415 DiscretizationScheme::Euler => {
416 (s * ((r - p.q) * dt + sigma * *dwi + 1.0)).maxf(0.0)
417 }
418 DiscretizationScheme::Milstein => {
419 let correction =
420 sigma * sigma * (0.5 * (dwi * dwi - dt));
421 (s * ((r - p.q) * dt + sigma * *dwi + correction + 1.0))
422 .maxf(0.0)
423 }
424 };
425 if let Some(divs) = &divs {
426 if divs[step_idx] != 0.0 {
427 s = (s - divs[step_idx]).maxf(1e-8);
428 }
429 }
430 path_vars.push(s);
431 }
432 let payoff = option
433 .payoff
434 .path_payoff_var(&path_vars, p.strike)
435 .expect("the probe above guaranteed AAD support");
436 let discounted = payoff * (-(r * p.t)).exp();
437 let g = discounted.grad();
438 delta_sum += g.wrt(s0);
439 vega_sum += g.wrt(sigma);
440 rho_sum += g.wrt(r);
441 }
442 (delta_sum, vega_sum, rho_sum)
443 })
444 .collect();
445 let (delta_sum, vega_sum, rho_sum) = partials
446 .into_iter()
447 .fold((0.0, 0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1, a.2 + b.2));
448 let n = cfg.paths as f64;
449 Some(AadGreeks { delta: delta_sum / n, vega: vega_sum / n, rho: rho_sum / n })
450}
451
452fn heston_aad_greeks(option: &EquityOption) -> Option<AadGreeks> {
466 use crate::core::aad::{Tape, Var};
467 let hp = *option.heston_params();
468 let cfg = option.mc_cfg();
469 let p = market_params(option);
470 let steps = cfg.time_steps.max(HESTON_MIN_STEPS);
471 {
473 let tape = Tape::new();
474 let probe: Vec<Var> = (0..steps.max(2)).map(|_| tape.var(p.s0)).collect();
475 option.payoff.path_payoff_var(&probe, p.strike)?;
476 }
477 let dt = p.t / steps as f64;
478 let sqrt_dt = dt.sqrt();
479 let rho_perp = (1.0 - hp.rho * hp.rho).sqrt();
480 let divs = dividends_per_step(option, p.t, steps);
481 let chunks = cfg.paths.div_ceil(PATH_CHUNK);
482 let partials: Vec<(f64, f64, f64)> = (0..chunks)
483 .into_par_iter()
484 .map(|chunk| {
485 let mut z = vec![0.0; 2 * steps];
486 let tape = Tape::new();
487 let mut path_vars: Vec<Var> = Vec::with_capacity(steps);
488 let (mut delta_sum, mut vega_sum, mut rho_sum) = (0.0, 0.0, 0.0);
489 for i in chunk * PATH_CHUNK..((chunk + 1) * PATH_CHUNK).min(cfg.paths) {
490 path_normals(cfg.seed, (i / 2) as u64, &mut z);
491 let sign = if i % 2 == 0 { 1.0 } else { -1.0 };
492 tape.clear();
493 path_vars.clear();
494 let s0 = tape.var(p.s0);
495 let vol_shift = tape.var(0.0);
496 let r = tape.var(p.r);
497 let sqrt_v0 = vol_shift + hp.v0.sqrt();
498 let sqrt_theta = vol_shift + hp.theta.sqrt();
499 let theta_var = sqrt_theta * sqrt_theta;
500 let mut s = s0;
501 let mut v = sqrt_v0 * sqrt_v0;
502 for j in 0..steps {
503 let dw_s = sqrt_dt * sign * z[2 * j];
504 let dw_v =
505 hp.rho * dw_s + rho_perp * sqrt_dt * sign * z[2 * j + 1];
506 let v_pos = v.maxf(1e-12);
508 let sqrt_v = v_pos.sqrt();
509 s = s * ((r - p.q) * dt - v_pos * (0.5 * dt) + sqrt_v * dw_s).exp();
510 if let Some(divs) = &divs {
511 if divs[j] != 0.0 {
512 s = (s - divs[j]).maxf(1e-8);
513 }
514 }
515 v = v + (theta_var - v_pos) * (hp.kappa * dt)
516 + sqrt_v * (hp.vol_of_vol * dw_v);
517 path_vars.push(s);
518 }
519 let payoff = option
520 .payoff
521 .path_payoff_var(&path_vars, p.strike)
522 .expect("the probe above guaranteed AAD support");
523 let discounted = payoff * (-(r * p.t)).exp();
524 let g = discounted.grad();
525 delta_sum += g.wrt(s0);
526 vega_sum += g.wrt(vol_shift);
527 rho_sum += g.wrt(r);
528 }
529 (delta_sum, vega_sum, rho_sum)
530 })
531 .collect();
532 let (delta_sum, vega_sum, rho_sum) = partials
533 .into_iter()
534 .fold((0.0, 0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1, a.2 + b.2));
535 let n = cfg.paths as f64;
536 Some(AadGreeks { delta: delta_sum / n, vega: vega_sum / n, rho: rho_sum / n })
537}
538
539pub(crate) fn npv_with(
542 option: &EquityOption,
543 d_spot: f64,
544 d_vol: f64,
545 d_rate: f64,
546 d_time: f64,
547) -> f64 {
548 let p = market_params(option);
549 price(
550 option,
551 &MarketParams {
552 s0: p.s0 + d_spot,
553 sigma: p.sigma + d_vol,
554 r: p.r + d_rate,
555 t: (p.t - d_time).max(1e-6),
556 ..p
557 },
558 )
559 .pv
560}
561
562fn bs_process<'a>(option: &'a EquityOption, p: &MarketParams) -> BlackScholesProcess<'a> {
567 let vol = match option.model {
568 Model::Gbm => VolDynamics::Const(p.sigma),
569 Model::LocalVol => VolDynamics::Local(LocalVol::new(
570 &option.market.vol_surface,
571 &option.market.discount_curve,
572 option.market.spot.value(),
575 option.carry_yield(),
576 p.sigma - option.volatility(),
578 )),
579 Model::Heston(_) => unreachable!("Heston paths are generated by the dedicated routes"),
580 };
581 BlackScholesProcess::new(p.r - p.q, vol)
582}
583
584fn effective_steps(cfg: &MonteCarloConfig, model: &Model) -> usize {
585 match model {
586 Model::LocalVol => cfg.time_steps.max(LOCAL_VOL_MIN_STEPS),
587 Model::Heston(_) => {
588 let floor = match cfg.scheme {
591 DiscretizationScheme::Exact => HESTON_QE_MIN_STEPS,
592 _ => HESTON_MIN_STEPS,
593 };
594 cfg.time_steps.max(floor)
595 }
596 Model::Gbm => cfg.time_steps,
597 }
598}
599
600const PATH_CHUNK: usize = 4096;
604
605fn run_paths<F>(paths: usize, steps: usize, draws: &PathDraws, eval: F) -> (f64, f64)
608where
609 F: Fn(&[f64], &mut Vec<f64>) -> f64 + Sync,
610{
611 let chunks = paths.div_ceil(PATH_CHUNK);
612 let partials: Vec<(f64, f64)> = (0..chunks)
613 .into_par_iter()
614 .map(|chunk| {
615 let mut z = vec![0.0; steps];
616 let mut w = vec![0.0; steps];
617 let mut dw = vec![0.0; steps];
618 let mut scratch = Vec::new();
619 let (mut sum, mut sum_sq) = (0.0, 0.0);
620 for i in chunk * PATH_CHUNK..((chunk + 1) * PATH_CHUNK).min(paths) {
621 draws.fill(i, &mut z, &mut w, &mut dw);
622 let v = eval(&dw, &mut scratch);
623 sum += v;
624 sum_sq += v * v;
625 }
626 (sum, sum_sq)
627 })
628 .collect();
629 partials.into_iter().fold((0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1))
630}
631
632fn european_npv(option: &EquityOption, p: &MarketParams) -> McStats {
635 if option.model .is_heston() {
636 return heston_european_npv(option, p);
637 }
638 if option.payoff.is_path_dependent() {
639 return if let Some(barrier) = option
643 .payoff
644 .as_any()
645 .downcast_ref::<BarrierPayoff>()
646 .filter(|b| b.barrier2.is_none() && b.rebate == 0.0)
647 {
648 barrier_npv(option, barrier, p)
649 } else if let Some(asian) = option.payoff.as_any().downcast_ref::<AsianPayoff>() {
650 asian_npv(option, asian, p)
651 } else if let Some(auto) = option.payoff.as_any().downcast_ref::<AutocallablePayoff>() {
652 autocall_npv(option, auto, p)
653 } else if let Some(accu) = option.payoff.as_any().downcast_ref::<AccumulatorPayoff>() {
654 accumulator_npv(option, accu, p)
655 } else {
656 generic_path_npv(option, p)
657 };
658 }
659 let cfg = option.mc_cfg();
660 let steps = effective_steps(cfg, &option.model);
661 let df = exp(-p.r * p.t);
662 if steps <= 1 {
663 let drift = (p.r - p.q - 0.5 * p.sigma * p.sigma) * p.t;
665 let vol_sqrt_t = p.sigma * p.t.sqrt();
666 let s0 = escrowed_spot(option, p);
667 let z = match cfg.sampler {
668 Sampler::Sobol => sobol_normals(cfg.paths),
669 Sampler::PseudoRandom => pseudo_normals(cfg.paths, cfg.seed),
670 };
671 let partials: Vec<(f64, f64)> = z
672 .par_chunks(PATH_CHUNK)
673 .map(|chunk| {
674 let (mut sum, mut sum_sq) = (0.0, 0.0);
675 for z in chunk {
676 let v = df
677 * option.payoff.payoff(s0 * exp(drift + vol_sqrt_t * z), p.strike);
678 sum += v;
679 sum_sq += v * v;
680 }
681 (sum, sum_sq)
682 })
683 .collect();
684 let (sum, sum_sq) =
685 partials.into_iter().fold((0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1));
686 return stats(sum, sum_sq, cfg.paths, 1, 0.0);
687 }
688 let dt = p.t / steps as f64;
689 let process = bs_process(option, p);
690 let draws = PathDraws::new(cfg.sampler, cfg.seed, steps, dt);
691 let divs = dividends_per_step(option, p.t, steps);
692 let (sum, sum_sq) = run_paths(cfg.paths, steps, &draws, |dw, _| {
693 let mut s = p.s0;
694 for (i, d) in dw.iter().enumerate() {
695 s = process.evolve(cfg.scheme, i as f64 * dt, s, dt, *d);
696 if let Some(divs) = &divs {
697 s = (s - divs[i]).max(1e-8);
698 }
699 }
700 df * option.payoff.payoff(s, p.strike)
701 });
702 stats(sum, sum_sq, cfg.paths, steps, 0.0)
703}
704
705fn generic_path_npv(option: &EquityOption, p: &MarketParams) -> McStats {
708 let cfg = option.mc_cfg();
709 let steps = effective_steps(cfg, &option.model).max(PATH_DEPENDENT_MIN_STEPS);
710 let dt = p.t / steps as f64;
711 let df = exp(-p.r * p.t);
712 let process = bs_process(option, p);
713 let draws = PathDraws::new(cfg.sampler, cfg.seed, steps, dt);
714 let divs = dividends_per_step(option, p.t, steps);
715 let (sum, sum_sq) = run_paths(cfg.paths, steps, &draws, |dw, path| {
716 path.clear();
717 let mut s = p.s0;
718 for (i, d) in dw.iter().enumerate() {
719 s = process.evolve(cfg.scheme, i as f64 * dt, s, dt, *d);
720 if let Some(divs) = &divs {
721 s = (s - divs[i]).max(1e-8);
722 }
723 path.push(s);
724 }
725 df * option.payoff.path_payoff(path, p.strike)
726 });
727 stats(sum, sum_sq, cfg.paths, steps, 0.0)
728}
729
730fn asian_npv(option: &EquityOption, asian: &AsianPayoff, p: &MarketParams) -> McStats {
737 let cfg = option.mc_cfg();
738 let use_control_variate = asian.averaging == AveragingType::Arithmetic
739 && asian.strike_type == AsianStrikeType::FixedStrike
740 && option.model == Model::Gbm
741 && cfg.scheme == DiscretizationScheme::Exact
742 && option.market.cash_dividends.is_empty();
743 if !use_control_variate {
744 return generic_path_npv(option, p);
745 }
746 let steps = effective_steps(cfg, &option.model).max(PATH_DEPENDENT_MIN_STEPS);
747 let dt = p.t / steps as f64;
748 let drift_dt = (p.r - p.q - 0.5 * p.sigma * p.sigma) * dt;
749 let df = exp(-p.r * p.t);
750 let draws = PathDraws::new(cfg.sampler, cfg.seed, steps, dt);
751 let (sum, sum_sq) = run_paths(cfg.paths, steps, &draws, |dw, _| {
752 let mut s = p.s0;
753 let mut sum_s = 0.0;
754 let mut log_sum = 0.0;
755 for d in dw {
756 s *= exp(drift_dt + p.sigma * d);
757 sum_s += s;
758 log_sum += s.ln();
759 }
760 let arithmetic = sum_s / steps as f64;
761 let geometric = (log_sum / steps as f64).exp();
762 df * (option.payoff.payoff(arithmetic, p.strike)
763 - option.payoff.payoff(geometric, p.strike))
764 });
765 let geo_closed = asian::geometric_asian_price(
766 p.s0,
767 p.strike,
768 p.r,
769 p.q,
770 p.sigma,
771 p.t,
772 Some(steps),
773 *option.payoff.put_or_call(),
774 );
775 stats(sum, sum_sq, cfg.paths, steps, geo_closed)
776}
777
778fn barrier_npv(option: &EquityOption, barrier: &BarrierPayoff, p: &MarketParams) -> McStats {
783 let cfg = option.mc_cfg();
784 let steps = effective_steps(cfg, &option.model).max(PATH_DEPENDENT_MIN_STEPS);
785 let dt = p.t / steps as f64;
786 let down = barrier.direction == BarrierDirection::Down;
787 let out = barrier.knock == KnockType::Out;
788 let h = barrier.barrier;
789 let knocked_at_start = if down { p.s0 <= h } else { p.s0 >= h };
790 if knocked_at_start && out {
791 return McStats { pv: 0.0, std_err: 0.0, paths: cfg.paths, steps };
792 }
793 let df = exp(-p.r * p.t);
794 let process = bs_process(option, p);
795 let draws = PathDraws::new(cfg.sampler, cfg.seed, steps, dt);
796 let divs = dividends_per_step(option, p.t, steps);
797 let (sum, sum_sq) = run_paths(cfg.paths, steps, &draws, |dw, _| {
798 let mut s = p.s0;
799 let mut survival = if knocked_at_start { 0.0 } else { 1.0 };
800 for (i, d) in dw.iter().enumerate() {
801 let sigma = process.vol(s, i as f64 * dt);
804 let mut s_next = process.step_with_vol(cfg.scheme, i as f64 * dt, s, dt, *d, sigma);
805 if let Some(divs) = &divs {
806 s_next = (s_next - divs[i]).max(1e-8);
807 }
808 if survival > 0.0 {
809 let crossed = if down { s_next <= h } else { s_next >= h };
810 if crossed {
811 survival = 0.0;
812 } else {
813 let (a, b) = if down {
815 ((s / h).ln(), (s_next / h).ln())
816 } else {
817 ((h / s).ln(), (h / s_next).ln())
818 };
819 survival *= 1.0 - (-2.0 * a * b / (sigma * sigma * dt)).exp();
820 }
821 }
822 s = s_next;
823 }
824 let vanilla_leg = option.payoff.payoff(s, p.strike);
825 let weight = if out { survival } else { 1.0 - survival };
826 df * weight * vanilla_leg
827 });
828 stats(sum, sum_sq, cfg.paths, steps, 0.0)
829}
830
831fn observation_grid(
837 option: &EquityOption,
838 n_obs: usize,
839 observation_times: Option<&Vec<f64>>,
840 t: f64,
841 r: f64,
842 steps: usize,
843) -> (Vec<usize>, Vec<f64>) {
844 let dr = r - option.risk_free_rate();
845 let n_obs = n_obs.max(1);
846 let (obs_idx, obs_times): (Vec<usize>, Vec<f64>) = match observation_times {
847 Some(times) => {
848 let mut idx = Vec::with_capacity(times.len());
849 let mut prev: i64 = 0;
850 for &tm in times {
851 let i = ((tm / t) * steps as f64).round().max(1.0) as i64;
854 let i = i.max(prev + 1).min(steps as i64);
855 idx.push(i as usize - 1);
856 prev = i;
857 }
858 (idx, times.clone())
859 }
860 None => {
861 let dt = t / steps as f64;
862 let idx: Vec<usize> = (1..=n_obs).map(|m| m * steps / n_obs - 1).collect();
863 let times = idx.iter().map(|&i| (i + 1) as f64 * dt).collect();
864 (idx, times)
865 }
866 };
867 let dfs = obs_times
868 .iter()
869 .map(|&tm| option.market.discount_curve.df(tm) * exp(-dr * tm))
870 .collect();
871 (obs_idx, dfs)
872}
873
874fn autocall_npv(option: &EquityOption, auto: &AutocallablePayoff, p: &MarketParams) -> McStats {
880 let cfg = option.mc_cfg();
881 let n_obs = auto.observations.max(1);
882 let steps = effective_steps(cfg, &option.model).max(PATH_DEPENDENT_MIN_STEPS).div_ceil(n_obs) * n_obs;
883 let dt = p.t / steps as f64;
884 let (obs_idx, dfs) =
885 observation_grid(option, n_obs, auto.observation_times.as_ref(), p.t, p.r, steps);
886 let divs = dividends_per_step(option, p.t, steps);
887 let process = bs_process(option, p);
888 let draws = PathDraws::new(cfg.sampler, cfg.seed, steps, dt);
889 let (sum, sum_sq) = run_paths(cfg.paths, steps, &draws, |dw, path| {
890 path.clear();
891 let mut s = p.s0;
892 for (i, d) in dw.iter().enumerate() {
893 s = process.evolve(cfg.scheme, i as f64 * dt, s, dt, *d);
894 if let Some(divs) = &divs {
895 s = (s - divs[i]).max(1e-8);
896 }
897 path.push(s);
898 }
899 auto.path_value(path, &obs_idx, &dfs)
900 });
901 stats(sum, sum_sq, cfg.paths, steps, 0.0)
902}
903
904fn accumulator_npv(option: &EquityOption, accu: &AccumulatorPayoff, p: &MarketParams) -> McStats {
911 let cfg = option.mc_cfg();
912 let n_obs = accu.observations.max(1);
913 let steps = effective_steps(cfg, &option.model).max(PATH_DEPENDENT_MIN_STEPS).div_ceil(n_obs) * n_obs;
914 let dt = p.t / steps as f64;
915 let (obs_idx, dfs) = observation_grid(option, n_obs, None, p.t, p.r, steps);
916 let divs = dividends_per_step(option, p.t, steps);
917 let process = bs_process(option, p);
918 let draws = PathDraws::new(cfg.sampler, cfg.seed, steps, dt);
919 let (sum, sum_sq) = run_paths(cfg.paths, steps, &draws, |dw, path| {
920 path.clear();
921 let mut s = p.s0;
922 for (i, d) in dw.iter().enumerate() {
923 s = process.evolve(cfg.scheme, i as f64 * dt, s, dt, *d);
924 if let Some(divs) = &divs {
925 s = (s - divs[i]).max(1e-8);
926 }
927 path.push(s);
928 }
929 accu.path_value(path, &obs_idx, &dfs, p.strike)
930 });
931 stats(sum, sum_sq, cfg.paths, steps, 0.0)
932}
933
934fn heston_european_npv(option: &EquityOption, p: &MarketParams) -> McStats {
942 let hp = option.heston_params().with_vol_shift(p.sigma - option.volatility());
943 let cfg = option.mc_cfg();
944 let steps = if option.payoff.is_path_dependent() {
948 effective_steps(cfg, &option.model).max(PATH_DEPENDENT_MIN_STEPS)
949 } else {
950 effective_steps(cfg, &option.model)
951 };
952 let dt = p.t / steps as f64;
953 let df = exp(-p.r * p.t);
954
955 if let Some(barrier) = option.payoff.as_any().downcast_ref::<BarrierPayoff>() {
956 let down = barrier.direction == BarrierDirection::Down;
957 let out = barrier.knock == KnockType::Out;
958 let h = barrier.barrier;
959 let knocked_at_start = if down { p.s0 <= h } else { p.s0 >= h };
960 if knocked_at_start && out {
961 return McStats { pv: 0.0, std_err: 0.0, paths: cfg.paths, steps };
962 }
963 let (sum, sum_sq) = run_heston_paths(option, p, &hp, steps, dt, |spots, vols| {
964 let mut survival = if knocked_at_start { 0.0 } else { 1.0 };
965 let mut s_prev = p.s0;
966 for (i, &s_next) in spots.iter().enumerate() {
967 if survival > 0.0 {
968 let crossed = if down { s_next <= h } else { s_next >= h };
969 if crossed {
970 survival = 0.0;
971 } else {
972 let (a, b) = if down {
973 ((s_prev / h).ln(), (s_next / h).ln())
974 } else {
975 ((h / s_prev).ln(), (h / s_next).ln())
976 };
977 let sigma = vols[i].max(1e-8);
978 survival *= 1.0 - (-2.0 * a * b / (sigma * sigma * dt)).exp();
979 }
980 }
981 s_prev = s_next;
982 }
983 let weight = if out { survival } else { 1.0 - survival };
984 df * weight * option.payoff.payoff(s_prev, p.strike)
985 });
986 return stats(sum, sum_sq, cfg.paths, steps, 0.0);
987 }
988
989 if let Some(auto) = option.payoff.as_any().downcast_ref::<AutocallablePayoff>() {
990 let n_obs = auto.observations.max(1);
991 let steps = steps.div_ceil(n_obs) * n_obs;
992 let dt = p.t / steps as f64;
993 let (obs_idx, dfs) =
994 observation_grid(option, n_obs, auto.observation_times.as_ref(), p.t, p.r, steps);
995 let (sum, sum_sq) = run_heston_paths(option, p, &hp, steps, dt, |spots, _| {
996 auto.path_value(spots, &obs_idx, &dfs)
997 });
998 return stats(sum, sum_sq, cfg.paths, steps, 0.0);
999 }
1000
1001 if let Some(accu) = option.payoff.as_any().downcast_ref::<AccumulatorPayoff>() {
1002 let n_obs = accu.observations.max(1);
1003 let steps = steps.div_ceil(n_obs) * n_obs;
1004 let dt = p.t / steps as f64;
1005 let (obs_idx, dfs) = observation_grid(option, n_obs, None, p.t, p.r, steps);
1006 let (sum, sum_sq) = run_heston_paths(option, p, &hp, steps, dt, |spots, _| {
1007 accu.path_value(spots, &obs_idx, &dfs, p.strike)
1008 });
1009 return stats(sum, sum_sq, cfg.paths, steps, 0.0);
1010 }
1011
1012 let path_dependent = option.payoff.is_path_dependent();
1013 let (sum, sum_sq) = run_heston_paths(option, p, &hp, steps, dt, |spots, _| {
1014 let v = if path_dependent {
1015 option.payoff.path_payoff(spots, p.strike)
1016 } else {
1017 option.payoff.payoff(*spots.last().unwrap(), p.strike)
1018 };
1019 df * v
1020 });
1021 stats(sum, sum_sq, cfg.paths, steps, 0.0)
1022}
1023
1024fn run_heston_paths<F>(
1030 option: &EquityOption,
1031 p: &MarketParams,
1032 hp: &HestonParams,
1033 steps: usize,
1034 dt: f64,
1035 eval: F,
1036) -> (f64, f64)
1037where
1038 F: Fn(&[f64], &[f64]) -> f64 + Sync,
1039{
1040 let cfg = option.mc_cfg();
1041 let scheme = match cfg.scheme {
1044 DiscretizationScheme::Exact => HestonScheme::QuadraticExponential,
1045 _ => HestonScheme::FullTruncation,
1046 };
1047 let process = HestonProcess { drift_rate: p.r - p.q, params: *hp, scheme };
1048 let sqrt_dt = dt.sqrt();
1049 let divs = dividends_per_step(option, p.t, steps);
1050 let chunks = cfg.paths.div_ceil(PATH_CHUNK);
1051 let partials: Vec<(f64, f64)> = (0..chunks)
1052 .into_par_iter()
1053 .map(|chunk| {
1054 let mut z = vec![0.0; 2 * steps];
1055 let mut spots = vec![0.0; steps];
1056 let mut vols = vec![0.0; steps];
1057 let (mut sum, mut sum_sq) = (0.0, 0.0);
1058 for i in chunk * PATH_CHUNK..((chunk + 1) * PATH_CHUNK).min(cfg.paths) {
1059 path_normals(cfg.seed, (i / 2) as u64, &mut z);
1061 let sign = if i % 2 == 0 { 1.0 } else { -1.0 };
1062 let mut x = [p.s0, hp.v0];
1063 let mut x_next = [0.0; 2];
1064 for j in 0..steps {
1065 let dw = [sign * sqrt_dt * z[2 * j], sign * sqrt_dt * z[2 * j + 1]];
1067 vols[j] = x[1].max(0.0).sqrt();
1068 process.evolve(j as f64 * dt, &x, dt, &dw, &mut x_next);
1069 if let Some(divs) = &divs {
1070 x_next[0] = (x_next[0] - divs[j]).max(1e-8);
1071 }
1072 x = x_next;
1073 spots[j] = x[0];
1074 }
1075 let value = eval(&spots, &vols);
1076 sum += value;
1077 sum_sq += value * value;
1078 }
1079 (sum, sum_sq)
1080 })
1081 .collect();
1082 partials.into_iter().fold((0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1))
1083}
1084
1085const LSMC_DEFAULT_STEPS: usize = 50;
1088const LSMC_BASIS: usize = 4;
1089
1090fn lsmc_basis(x: f64) -> [f64; LSMC_BASIS] {
1095 [1.0, x, x * x, x * x * x]
1096}
1097
1098fn exercise_mask(option: &EquityOption, t: f64, steps: usize) -> Vec<bool> {
1101 match option.payoff.exercise_style() {
1102 ContractStyle::Bermudan(times) => {
1103 let mut mask = vec![false; steps.saturating_sub(1)];
1104 for g in crate::core::utils::times_to_grid_steps(times, t, steps) {
1105 if g < steps {
1106 mask[g - 1] = true;
1107 }
1108 }
1109 mask
1110 }
1111 _ => vec![true; steps.saturating_sub(1)],
1112 }
1113}
1114
1115fn american_npv(option: &EquityOption, p: &MarketParams) -> McStats {
1124 let cfg = option.mc_cfg();
1125 if option.model.is_heston() {
1126 return heston_american_npv(option, p);
1127 }
1128 let steps = if cfg.time_steps > 1 { cfg.time_steps } else { LSMC_DEFAULT_STEPS }
1129 .max(if option.model == Model::LocalVol { LOCAL_VOL_MIN_STEPS } else { 1 });
1130 let dt = p.t / steps as f64;
1131 let allowed = exercise_mask(option, p.t, steps);
1132 let disc = exp(-p.r * dt);
1133 let process = bs_process(option, p);
1134 let seed_regression = cfg.seed ^ 0xA11C_E5ED;
1135 let seed_valuation = cfg.seed ^ 0xB0B5_1EED;
1136
1137 let simulate = |draws: &PathDraws, index: usize, bufs: &mut (Vec<f64>, Vec<f64>, Vec<f64>), path: &mut Vec<f64>| {
1138 let (z, w, dw) = bufs;
1139 draws.fill(index, z, w, dw);
1140 path.clear();
1141 let mut s = p.s0;
1142 for (i, d) in dw.iter().enumerate() {
1143 s = process.evolve(cfg.scheme, i as f64 * dt, s, dt, *d);
1144 path.push(s);
1145 }
1146 };
1147
1148 let reg_draws = PathDraws::pseudo(seed_regression, dt);
1150 let spots: Vec<Vec<f64>> = (0..cfg.paths)
1151 .into_par_iter()
1152 .map_init(
1153 || (vec![0.0; steps], vec![0.0; steps], vec![0.0; steps]),
1154 |bufs, i| {
1155 let mut path = Vec::with_capacity(steps);
1156 simulate(®_draws, i, bufs, &mut path);
1157 path
1158 },
1159 )
1160 .collect();
1161
1162 let mut cashflow: Vec<f64> =
1163 spots.iter().map(|path| option.payoff.payoff(path[steps - 1], p.strike)).collect();
1164 let mut betas: Vec<Option<[f64; LSMC_BASIS]>> = vec![None; steps.saturating_sub(1)];
1165 for step_idx in (0..steps - 1).rev() {
1166 for cf in cashflow.iter_mut() {
1167 *cf *= disc;
1168 }
1169 if !allowed[step_idx] {
1170 continue;
1173 }
1174 let itm: Vec<usize> = (0..spots.len())
1175 .filter(|&i| option.payoff.payoff(spots[i][step_idx], p.strike) > 0.0)
1176 .collect();
1177 if itm.len() < LSMC_BASIS {
1178 continue;
1179 }
1180 let rows: Vec<([f64; LSMC_BASIS], f64)> = itm
1181 .iter()
1182 .map(|&i| {
1183 let s = spots[i][step_idx];
1184 (lsmc_basis(s / p.s0), cashflow[i])
1185 })
1186 .collect();
1187 let Some(beta) = least_squares(&rows) else { continue };
1188 for &i in &itm {
1189 let s = spots[i][step_idx];
1190 let pay = option.payoff.payoff(s, p.strike);
1191 let continuation = dot(&beta, &lsmc_basis(s / p.s0));
1192 if pay > continuation {
1193 cashflow[i] = pay;
1194 }
1195 }
1196 betas[step_idx] = Some(beta);
1197 }
1198 drop(spots);
1199 drop(cashflow);
1200
1201 let val_draws = PathDraws::pseudo(seed_valuation, dt);
1203 let partials: Vec<(f64, f64)> = (0..cfg.paths.div_ceil(PATH_CHUNK))
1204 .into_par_iter()
1205 .map(|chunk| {
1206 let mut bufs = (vec![0.0; steps], vec![0.0; steps], vec![0.0; steps]);
1207 let mut path = Vec::with_capacity(steps);
1208 let (mut c_sum, mut c_sum_sq) = (0.0, 0.0);
1209 for i in chunk * PATH_CHUNK..((chunk + 1) * PATH_CHUNK).min(cfg.paths) {
1210 simulate(&val_draws, i, &mut bufs, &mut path);
1211 let mut value = 0.0;
1212 let mut exercised = false;
1213 for k in 0..steps - 1 {
1214 let s = path[k];
1215 let pay = option.payoff.payoff(s, p.strike);
1216 if pay > 0.0 {
1217 if let Some(beta) = &betas[k] {
1218 let continuation = dot(beta, &lsmc_basis(s / p.s0));
1219 if pay > continuation {
1220 value = pay * disc.powi(k as i32 + 1);
1221 exercised = true;
1222 break;
1223 }
1224 }
1225 }
1226 }
1227 if !exercised {
1228 value = option.payoff.payoff(path[steps - 1], p.strike)
1229 * disc.powi(steps as i32);
1230 }
1231 c_sum += value;
1232 c_sum_sq += value * value;
1233 }
1234 (c_sum, c_sum_sq)
1235 })
1236 .collect();
1237 let (sum, sum_sq) = partials.into_iter().fold((0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1));
1238 stats(sum, sum_sq, cfg.paths, steps, 0.0)
1239}
1240
1241const HESTON_LSMC_BASIS: usize = 6;
1244
1245fn heston_lsmc_basis(x: f64, v: f64) -> [f64; HESTON_LSMC_BASIS] {
1250 [1.0, x, x * x, x * x * x, v, x * v]
1251}
1252
1253fn heston_american_npv(option: &EquityOption, p: &MarketParams) -> McStats {
1263 let hp = option.heston_params().with_vol_shift(p.sigma - option.volatility());
1264 let cfg = option.mc_cfg();
1265 let scheme = match cfg.scheme {
1266 DiscretizationScheme::Exact => HestonScheme::QuadraticExponential,
1267 _ => HestonScheme::FullTruncation,
1268 };
1269 let floor = match scheme {
1270 HestonScheme::QuadraticExponential => HESTON_QE_MIN_STEPS,
1271 HestonScheme::FullTruncation => HESTON_MIN_STEPS,
1272 };
1273 let steps = if cfg.time_steps > 1 { cfg.time_steps } else { LSMC_DEFAULT_STEPS }.max(floor);
1274 let dt = p.t / steps as f64;
1275 let allowed = exercise_mask(option, p.t, steps);
1276 let disc = exp(-p.r * dt);
1277 let process = HestonProcess { drift_rate: p.r - p.q, params: hp, scheme };
1278 let sqrt_dt = dt.sqrt();
1279 let seed_regression = cfg.seed ^ 0xA11C_E5ED;
1280 let seed_valuation = cfg.seed ^ 0xB0B5_1EED;
1281
1282 let simulate = |seed: u64, i: usize, z: &mut [f64], spots: &mut [f64], vars: &mut [f64]| {
1285 path_normals(seed, (i / 2) as u64, z);
1286 let sign = if i % 2 == 0 { 1.0 } else { -1.0 };
1287 let mut x = [p.s0, hp.v0];
1288 let mut x_next = [0.0; 2];
1289 for j in 0..steps {
1290 let dw = [sign * sqrt_dt * z[2 * j], sign * sqrt_dt * z[2 * j + 1]];
1291 process.evolve(j as f64 * dt, &x, dt, &dw, &mut x_next);
1292 x = x_next;
1293 spots[j] = x[0];
1294 vars[j] = x[1].max(0.0);
1295 }
1296 };
1297
1298 let paths_sv: Vec<(Vec<f64>, Vec<f64>)> = (0..cfg.paths)
1300 .into_par_iter()
1301 .map_init(
1302 || vec![0.0; 2 * steps],
1303 |z, i| {
1304 let mut spots = vec![0.0; steps];
1305 let mut vars = vec![0.0; steps];
1306 simulate(seed_regression, i, z, &mut spots, &mut vars);
1307 (spots, vars)
1308 },
1309 )
1310 .collect();
1311
1312 let mut cashflow: Vec<f64> = paths_sv
1313 .iter()
1314 .map(|(spots, _)| option.payoff.payoff(spots[steps - 1], p.strike))
1315 .collect();
1316 let mut betas: Vec<Option<[f64; HESTON_LSMC_BASIS]>> = vec![None; steps.saturating_sub(1)];
1317 for step_idx in (0..steps - 1).rev() {
1318 for cf in cashflow.iter_mut() {
1319 *cf *= disc;
1320 }
1321 if !allowed[step_idx] {
1322 continue;
1323 }
1324 let itm: Vec<usize> = (0..paths_sv.len())
1325 .filter(|&i| option.payoff.payoff(paths_sv[i].0[step_idx], p.strike) > 0.0)
1326 .collect();
1327 if itm.len() < HESTON_LSMC_BASIS {
1328 continue;
1329 }
1330 let rows: Vec<([f64; HESTON_LSMC_BASIS], f64)> = itm
1331 .iter()
1332 .map(|&i| {
1333 let (spots, vars) = &paths_sv[i];
1334 (heston_lsmc_basis(spots[step_idx] / p.s0, vars[step_idx]), cashflow[i])
1335 })
1336 .collect();
1337 let Some(beta) = least_squares(&rows) else { continue };
1338 for &i in &itm {
1339 let (spots, vars) = &paths_sv[i];
1340 let s = spots[step_idx];
1341 let pay = option.payoff.payoff(s, p.strike);
1342 let continuation = dot(&beta, &heston_lsmc_basis(s / p.s0, vars[step_idx]));
1343 if pay > continuation {
1344 cashflow[i] = pay;
1345 }
1346 }
1347 betas[step_idx] = Some(beta);
1348 }
1349 drop(paths_sv);
1350 drop(cashflow);
1351
1352 let partials: Vec<(f64, f64)> = (0..cfg.paths.div_ceil(PATH_CHUNK))
1354 .into_par_iter()
1355 .map(|chunk| {
1356 let mut z = vec![0.0; 2 * steps];
1357 let mut spots = vec![0.0; steps];
1358 let mut vars = vec![0.0; steps];
1359 let (mut c_sum, mut c_sum_sq) = (0.0, 0.0);
1360 for i in chunk * PATH_CHUNK..((chunk + 1) * PATH_CHUNK).min(cfg.paths) {
1361 simulate(seed_valuation, i, &mut z, &mut spots, &mut vars);
1362 let mut value = 0.0;
1363 let mut exercised = false;
1364 for k in 0..steps - 1 {
1365 let pay = option.payoff.payoff(spots[k], p.strike);
1366 if pay > 0.0 {
1367 if let Some(beta) = &betas[k] {
1368 let continuation =
1369 dot(beta, &heston_lsmc_basis(spots[k] / p.s0, vars[k]));
1370 if pay > continuation {
1371 value = pay * disc.powi(k as i32 + 1);
1372 exercised = true;
1373 break;
1374 }
1375 }
1376 }
1377 }
1378 if !exercised {
1379 value = option.payoff.payoff(spots[steps - 1], p.strike)
1380 * disc.powi(steps as i32);
1381 }
1382 c_sum += value;
1383 c_sum_sq += value * value;
1384 }
1385 (c_sum, c_sum_sq)
1386 })
1387 .collect();
1388 let (sum, sum_sq) = partials.into_iter().fold((0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1));
1389 stats(sum, sum_sq, cfg.paths, steps, 0.0)
1390}
1391
1392fn dot<const K: usize>(a: &[f64; K], b: &[f64; K]) -> f64 {
1393 a.iter().zip(b).map(|(x, y)| x * y).sum()
1394}
1395
1396fn least_squares<const K: usize>(rows: &[([f64; K], f64)]) -> Option<[f64; K]> {
1401 let mut m = [[0.0; K]; K];
1402 let mut rhs = [0.0; K];
1403 for (basis, y) in rows {
1404 for i in 0..K {
1405 for j in 0..K {
1406 m[i][j] += basis[i] * basis[j];
1407 }
1408 rhs[i] += basis[i] * y;
1409 }
1410 }
1411 for col in 0..K {
1412 let pivot =
1413 (col..K).max_by(|&i, &j| m[i][col].abs().partial_cmp(&m[j][col].abs()).unwrap())?;
1414 if m[pivot][col].abs() < 1e-10 {
1415 return None;
1416 }
1417 m.swap(col, pivot);
1418 rhs.swap(col, pivot);
1419 for row in col + 1..K {
1420 let f = m[row][col] / m[col][col];
1421 for c in col..K {
1422 m[row][c] -= f * m[col][c];
1423 }
1424 rhs[row] -= f * rhs[col];
1425 }
1426 }
1427 let mut beta = [0.0; K];
1428 for row in (0..K).rev() {
1429 let mut acc = rhs[row];
1430 for c in row + 1..K {
1431 acc -= m[row][c] * beta[c];
1432 }
1433 beta[row] = acc / m[row][row];
1434 }
1435 Some(beta)
1436}
1437