fast_sde/mc/
mc_engine.rs

1// src/mc/mc_engine.rs
2use crate::rng;
3use rayon::prelude::*;
4use std::f64;
5use crate::mc::payoffs::Payoff;
6use crate::analytics::bs_analytic;
7use crate::error::{SdeError, SdeResult, validation::*};
8use bitflags::bitflags;
9
10bitflags! {
11    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12    pub struct GreeksConfig: u32 {
13        const NONE  = 0;
14        const DELTA = 1 << 0;
15        const VEGA  = 1 << 1;
16        const RHO   = 1 << 2;
17        const GAMMA = 1 << 3;
18    }
19}
20
21#[derive(Clone)]
22pub struct McConfig {
23    pub paths: usize,
24    pub steps: usize,
25    pub s0: f64,
26    pub r: f64,
27    pub sigma: f64,
28    pub t: f64,
29    pub use_antithetic: bool,
30    pub use_control_variate: bool,
31    pub seed: u64,
32    pub payoff: Payoff,
33    pub greeks: GreeksConfig,
34    pub epsilon: Option<f64>,  // For finite difference Greeks (default: 1e-3 * s0)
35}
36
37impl McConfig {
38    /// Validate the Monte Carlo configuration
39    pub fn validate(&self) -> SdeResult<()> {
40        validate_paths(self.paths)?;
41        validate_steps(self.steps)?;
42        validate_positive("s0", self.s0)?;
43        validate_finite("r", self.r)?;
44        validate_positive("sigma", self.sigma)?;
45        validate_positive("t", self.t)?;
46        
47        if let Some(eps) = self.epsilon {
48            validate_positive("epsilon", eps)?;
49            if eps > self.s0 * 0.1 {
50                return Err(SdeError::InvalidParameters {
51                    parameter: "epsilon".to_string(),
52                    value: eps,
53                    constraint: format!("should be much smaller than spot price ({})", self.s0),
54                });
55            }
56        }
57        
58        Ok(())
59    }
60}
61
62impl Default for McConfig {
63    fn default() -> Self {
64        McConfig {
65            paths: 1_000_000,
66            steps: 1,
67            s0: 100.0,
68            r: 0.01,
69            sigma: 0.2,
70            t: 1.0,
71            use_antithetic: true,
72            use_control_variate: true,
73            seed: 12345,
74            payoff: Payoff::EuropeanCall { k: 100.0 },
75            greeks: GreeksConfig::NONE,
76            epsilon: None,
77        }
78    }
79}
80
81/// Monte Carlo pricing for options under Geometric Brownian Motion
82/// 
83/// # Math Framework
84/// 
85/// Simulates the GBM SDE:
86/// ```text
87/// dS_t = r S_t dt + σ S_t dW_t
88/// ```
89/// 
90/// With exact solution:
91/// ```text
92/// S_T = S_0 * exp((r - σ²/2)T + σ√T * Z)
93/// ```
94/// where Z ~ N(0,1).
95/// 
96/// # Variance Reduction Techniques
97/// 
98/// 1. **Antithetic Variates**: For each path with normal draw Z, also simulate
99///    path with -Z and average the payoffs. Reduces variance for smooth payoffs.
100/// 
101/// 2. **Control Variates**: Uses European call as control with known expectation.
102///    Estimator: Y - b(X - E\[X\]) where:
103///    - Y = target payoff (e.g., Asian call)  
104///    - X = control payoff (European call)
105///    - b = Cov(Y,X)/Var(X) (optimal coefficient)
106/// 
107/// # Returns
108/// 
109/// Returns `(price, variance_estimate)` where:
110/// - `price`: Discounted expected payoff
111/// - `variance_estimate`: Sample variance of the estimator (for confidence intervals)
112/// 
113/// # Errors
114/// 
115/// Returns `SdeError` for:
116/// - Invalid configuration parameters
117/// - Numerical instability (negative variance, non-finite results)
118pub fn mc_price_option_gbm(cfg: &McConfig) -> SdeResult<(f64, f64)> {
119    // Validate configuration
120    cfg.validate()?;
121    let n = cfg.paths;
122    let dt = cfg.t / cfg.steps as f64;
123    let sqrt_dt = dt.sqrt();
124    let discount = (-cfg.r * cfg.t).exp();
125
126    let (sum_payoff_path, sum_control_path, sum_payoff_times_control_path, sum_control_sq_path, sum_european_analytic_price_path, sum_payoff_sq_path) = (0..n)
127        .into_par_iter()
128        .map(|i| {
129            let mut rng = rng::seed_rng_from_u64(cfg.seed + i as u64);
130
131            // Generate asset price path using exact GBM solution
132            // S_{t+dt} = S_t * exp((r - σ²/2)dt + σ√dt * Z_t)
133            // where Z_t ~ N(0,1) are independent normal draws
134            let mut path_prices = Vec::with_capacity(cfg.steps + 1);
135            path_prices.push(cfg.s0);
136
137            let mut current_s = cfg.s0;
138            for _ in 0..cfg.steps {
139                let z = rng::get_normal_draw(&mut rng);
140                // Apply exact GBM step: drift + diffusion
141                current_s *= ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * dt + cfg.sigma * sqrt_dt * z).exp();
142                path_prices.push(current_s);
143            }
144            
145            // Calculate the payoff for this path
146            let payoff_raw = cfg.payoff.calculate(&path_prices);
147            
148            // Control Variate Setup
149            // For variance reduction, we use a control variate with known expectation
150            let mut control_var_raw = 0.0;
151            let mut european_analytic_price = 0.0;
152
153            match cfg.payoff {
154                Payoff::EuropeanCall { k } => {
155                    // For European calls, the control is itself (perfect control)
156                    control_var_raw = Payoff::EuropeanCall { k }.calculate(&path_prices);
157                    european_analytic_price = bs_analytic::bs_call_price(cfg.s0, k, cfg.r, cfg.sigma, cfg.t);
158                },
159                Payoff::AsianCall { k } => {
160                    // For Asian calls, use European call on terminal price as control
161                    // Theory: Both depend on final price, providing positive correlation
162                    let st_final = *path_prices.last().unwrap();
163                    control_var_raw = Payoff::EuropeanCall { k }.calculate(&vec![st_final]);
164                    european_analytic_price = bs_analytic::bs_call_price(cfg.s0, k, cfg.r, cfg.sigma, cfg.t);
165                },
166                _ => {
167                    // For barrier and other exotic options, control variates are more complex
168                    // Future enhancement: implement specific controls for each payoff type
169                }
170            }
171
172            let mut payoff_path = payoff_raw;
173            let mut control_var_path = control_var_raw;
174
175            // Antithetic Variates Implementation
176            // Generate second path with negated normal draws for variance reduction
177            if cfg.use_antithetic {
178                let mut path_prices2 = Vec::with_capacity(cfg.steps + 1);
179                path_prices2.push(cfg.s0);
180
181                let mut current_s2 = cfg.s0;
182                for _ in 0..cfg.steps {
183                    // Use -Z instead of Z for antithetic path
184                    // Theory: E[f(Z) + f(-Z)]/2 has lower variance than E[f(Z)] for symmetric f
185                    let z2 = -rng::get_normal_draw(&mut rng);
186                    current_s2 *= ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * dt + cfg.sigma * sqrt_dt * z2).exp();
187                    path_prices2.push(current_s2);
188                }
189                
190                let payoff2_raw = cfg.payoff.calculate(&path_prices2);
191                
192                let mut control_var2_raw = 0.0;
193                match cfg.payoff {
194                    Payoff::EuropeanCall { k } => {
195                        control_var2_raw = Payoff::EuropeanCall { k }.calculate(&path_prices2);
196                    },
197                    Payoff::AsianCall { k } => {
198                        let st2_final = *path_prices2.last().unwrap();
199                        control_var2_raw = Payoff::EuropeanCall { k }.calculate(&vec![st2_final]);
200                    },
201                    _ => {}
202                }
203                
204                // Average the original and antithetic payoffs
205                // This is the antithetic variate estimator: (Y₁ + Y₂)/2
206                payoff_path = 0.5 * (payoff_raw + payoff2_raw);
207                control_var_path = 0.5 * (control_var_raw + control_var2_raw);
208            }
209
210            (payoff_path, control_var_path, payoff_path * control_var_path, control_var_path * control_var_path, european_analytic_price, payoff_path * payoff_path)
211        })
212        .reduce(|| (0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
213                |a, b| (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3, a.4 + b.4, a.5 + b.5));
214
215    // Compute sample statistics for control variate method
216    let mean_payoff = sum_payoff_path / n as f64;
217    let mean_control = sum_control_path / n as f64;
218    let mean_payoff_times_control = sum_payoff_times_control_path / n as f64;
219    let mean_control_sq = sum_control_sq_path / n as f64;
220    let mean_european_analytic_price = sum_european_analytic_price_path / n as f64;
221    let mean_payoff_sq = sum_payoff_sq_path / n as f64;
222
223    let estimated_price;
224    let mut variance_of_estimate;
225
226    // Control Variate Method Implementation
227    // Estimator: Y - b(X - E[X]) where b minimizes variance
228    if cfg.use_control_variate {
229        // Optimal control variate coefficient: b* = Cov(Y,X) / Var(X)
230        // This minimizes Var(Y - b(X - E[X]))
231        let cov_payoff_control = mean_payoff_times_control - mean_payoff * mean_control;
232        let var_control = mean_control_sq - mean_control * mean_control;
233
234        // Avoid division by zero if control has no variance
235        let b = if var_control > 1e-10 { cov_payoff_control / var_control } else { 0.0 };
236
237        let controlled_payoffs_sum = (0..n)
238            .into_par_iter()
239            .map(|i| {
240                let mut rng = rng::seed_rng_from_u64(cfg.seed + i as u64);
241                let mut path_prices = Vec::with_capacity(cfg.steps + 1);
242                path_prices.push(cfg.s0);
243
244                let mut current_s = cfg.s0;
245                for _ in 0..cfg.steps {
246                    let z = rng::get_normal_draw(&mut rng);
247                    current_s *= ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * dt + cfg.sigma * sqrt_dt * z).exp();
248                    path_prices.push(current_s);
249                }
250
251                let payoff_raw = cfg.payoff.calculate(&path_prices);
252                
253                let mut control_var_raw = 0.0;
254                match cfg.payoff {
255                    Payoff::EuropeanCall { k } => {
256                        control_var_raw = Payoff::EuropeanCall { k }.calculate(&path_prices);
257                    },
258                    Payoff::AsianCall { k } => {
259                        let st_final = *path_prices.last().unwrap();
260                        control_var_raw = Payoff::EuropeanCall { k }.calculate(&vec![st_final]);
261                    },
262                    _ => {}
263                }
264
265                let mut payoff_path = payoff_raw;
266                let mut control_var_path = control_var_raw;
267
268                if cfg.use_antithetic {
269                    let mut path_prices2 = Vec::with_capacity(cfg.steps + 1);
270                    path_prices2.push(cfg.s0);
271
272                    let mut current_s2 = cfg.s0;
273                    for _ in 0..cfg.steps {
274                        let z2 = -rng::get_normal_draw(&mut rng);
275                        current_s2 *= ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * dt + cfg.sigma * sqrt_dt * z2).exp();
276                        path_prices2.push(current_s2);
277                    }
278
279                    let payoff2_raw = cfg.payoff.calculate(&path_prices2);
280                    
281                    let mut control_var2_raw = 0.0;
282                    match cfg.payoff {
283                        Payoff::EuropeanCall { k } => {
284                            control_var2_raw = Payoff::EuropeanCall { k }.calculate(&path_prices2);
285                        },
286                        Payoff::AsianCall { k } => {
287                            let st2_final = *path_prices2.last().unwrap();
288                            control_var2_raw = Payoff::EuropeanCall { k }.calculate(&vec![st2_final]);
289                        },
290                        _ => {}
291                    }
292                    payoff_path = 0.5 * (payoff_raw + payoff2_raw);
293                    control_var_path = 0.5 * (control_var_raw + control_var2_raw);
294                }
295
296                discount * (payoff_path - b * (control_var_path - mean_european_analytic_price))
297            })
298            .sum::<f64>();
299        
300        let mean_controlled_payoff = controlled_payoffs_sum / n as f64;
301        let sum_controlled_payoff_sq = (0..n)
302            .into_par_iter()
303            .map(|i| {
304                let mut rng = rng::seed_rng_from_u64(cfg.seed + i as u64);
305                let mut path_prices = Vec::with_capacity(cfg.steps + 1);
306                path_prices.push(cfg.s0);
307
308                let mut current_s = cfg.s0;
309                for _ in 0..cfg.steps {
310                    let z = rng::get_normal_draw(&mut rng);
311                    current_s *= ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * dt + cfg.sigma * sqrt_dt * z).exp();
312                    path_prices.push(current_s);
313                }
314
315                let payoff_raw = cfg.payoff.calculate(&path_prices);
316                
317                let mut control_var_raw = 0.0;
318                match cfg.payoff {
319                    Payoff::EuropeanCall { k } => {
320                        control_var_raw = Payoff::EuropeanCall { k }.calculate(&path_prices);
321                    },
322                    Payoff::AsianCall { k } => {
323                        let st_final = *path_prices.last().unwrap();
324                        control_var_raw = Payoff::EuropeanCall { k }.calculate(&vec![st_final]);
325                    },
326                    _ => {}
327                }
328
329                let mut payoff_path = payoff_raw;
330                let mut control_var_path = control_var_raw;
331
332                if cfg.use_antithetic {
333                    let mut path_prices2 = Vec::with_capacity(cfg.steps + 1);
334                    path_prices2.push(cfg.s0);
335
336                    let mut current_s2 = cfg.s0;
337                    for _ in 0..cfg.steps {
338                        let z2 = -rng::get_normal_draw(&mut rng);
339                        current_s2 *= ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * dt + cfg.sigma * sqrt_dt * z2).exp();
340                        path_prices2.push(current_s2);
341                    }
342
343                    let payoff2_raw = cfg.payoff.calculate(&path_prices2);
344                    
345                    let mut control_var2_raw = 0.0;
346                    match cfg.payoff {
347                        Payoff::EuropeanCall { k } => {
348                            control_var2_raw = Payoff::EuropeanCall { k }.calculate(&path_prices2);
349                        },
350                        Payoff::AsianCall { k } => {
351                            let st2_final = *path_prices2.last().unwrap();
352                            control_var2_raw = Payoff::EuropeanCall { k }.calculate(&vec![st2_final]);
353                        },
354                        _ => {}
355                    }
356                    payoff_path = 0.5 * (payoff_raw + payoff2_raw);
357                    control_var_path = 0.5 * (control_var_raw + control_var2_raw);
358                }
359
360                let controlled_payoff = discount * (payoff_path - b * (control_var_path - mean_european_analytic_price));
361                controlled_payoff * controlled_payoff
362            })
363            .sum::<f64>() / n as f64;
364
365        estimated_price = mean_controlled_payoff;
366        variance_of_estimate = (sum_controlled_payoff_sq - mean_controlled_payoff * mean_controlled_payoff) / (n as f64 * (n as f64 - 1.0));
367        
368        // Handle numerical precision issues that can cause negative variance
369        if variance_of_estimate < 0.0 {
370            if variance_of_estimate > -1e-10 {
371                // Small negative due to floating point precision - set to zero
372                variance_of_estimate = 0.0;
373            } else {
374                return Err(SdeError::NumericalInstability {
375                    method: "Control Variate Monte Carlo".to_string(),
376                    reason: format!("Variance estimate became significantly negative: {}", variance_of_estimate),
377                });
378            }
379        }
380
381    } else {
382        estimated_price = discount * mean_payoff;
383        variance_of_estimate = (mean_payoff_sq - mean_payoff * mean_payoff) * discount.powi(2) / (n as f64 * (n as f64 - 1.0));
384        
385        // Handle numerical precision issues for regular MC too
386        if variance_of_estimate < 0.0 {
387            if variance_of_estimate > -1e-10 {
388                variance_of_estimate = 0.0;
389            } else {
390                return Err(SdeError::NumericalInstability {
391                    method: "Monte Carlo".to_string(),
392                    reason: format!("Variance estimate became significantly negative: {}", variance_of_estimate),
393                });
394            }
395        }
396    }
397    
398    // Final validation of results
399    if !estimated_price.is_finite() {
400        return Err(SdeError::NumericalInstability {
401            method: "Monte Carlo".to_string(),
402            reason: format!("Price estimate is not finite: {}", estimated_price),
403        });
404    }
405    
406    if !variance_of_estimate.is_finite() {
407        return Err(SdeError::NumericalInstability {
408            method: "Monte Carlo".to_string(),
409            reason: format!("Variance estimate is not finite: {}", variance_of_estimate),
410        });
411    }
412
413    Ok((estimated_price, variance_of_estimate))
414}
415
416/// Monte Carlo Delta calculation using pathwise derivative method
417/// 
418/// # Mathematical Framework
419/// 
420/// For European call under GBM, the pathwise derivative is:
421/// ```text
422/// ∂/∂S₀ max(S_T - K, 0) = 1_{S_T > K} * ∂S_T/∂S₀
423/// ```
424/// 
425/// Where:
426/// - `1_{S_T > K}` is the indicator function (1 if in-the-money, 0 otherwise)
427/// - `∂S_T/∂S₀ = S_T/S₀` for the exact GBM solution
428/// 
429/// # Algorithm
430/// 
431/// 1. Simulate terminal stock price: S_T = S₀ * exp((r - σ²/2)T + σ√T * Z)
432/// 2. Compute pathwise delta: δ_path = 1_{S_T > K} * S_T/S₀  
433/// 3. Apply discount factor: Δ = e^(-rT) * E\[δ_path\]
434/// 
435/// # Accuracy
436/// 
437/// Pathwise method provides unbiased estimates for smooth payoffs.
438/// Typical relative error: < 0.1% with sufficient paths.
439pub fn mc_delta_european_call_gbm_pathwise(cfg: &McConfig) -> f64 {
440    let n = cfg.paths;
441    let discount = (-cfg.r * cfg.t).exp();
442
443    let k = match cfg.payoff {
444        Payoff::EuropeanCall { k } => k,
445        _ => {
446            return 0.0;
447        }
448    };
449
450    (0..n)
451        .into_par_iter()
452        .map(|i| {
453            let mut rng = rng::seed_rng_from_u64(cfg.seed + i as u64);
454            let z = rng::get_normal_draw(&mut rng);
455
456            let st = cfg.s0 * ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * cfg.t + cfg.sigma * cfg.t.sqrt() * z).exp();
457            
458            let mut delta_path = 0.0;
459            if st > k {
460                delta_path = st / cfg.s0;
461            }
462
463            if cfg.use_antithetic {
464                let z2 = -z;
465                let st2 = cfg.s0 * ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * cfg.t + cfg.sigma * cfg.t.sqrt() * z2).exp();
466                let mut delta_path2 = 0.0;
467                if st2 > k {
468                    delta_path2 = st2 / cfg.s0;
469                }
470                delta_path = 0.5 * (delta_path + delta_path2);
471            }
472            delta_path
473        })
474        .reduce(|| 0.0, |a,b| a + b) / n as f64 * discount
475}
476
477/// Monte Carlo Vega calculation using pathwise derivative method
478/// 
479/// # Mathematical Framework
480/// 
481/// For European call under GBM, the pathwise derivative w.r.t. volatility σ is:
482/// ```text
483/// ∂/∂σ max(S_T - K, 0) = 1_{S_T > K} * ∂S_T/∂σ
484/// ```
485/// 
486/// Where:
487/// ```text
488/// ∂S_T/∂σ = S_T * (-σT + W_T)
489/// ```
490/// and W_T is the Brownian motion at time T.
491/// 
492/// # Algorithm
493/// 
494/// 1. Simulate: S_T = S₀ * exp((r - σ²/2)T + σW_T) with W_T = √T * Z
495/// 2. Compute: ∂S_T/∂σ = S_T * (-σT + W_T)
496/// 3. Pathwise vega: ν_path = 1_{S_T > K} * ∂S_T/∂σ
497/// 4. Apply discount: ν = e^(-rT) * E\[ν_path\]
498/// 
499/// # Note
500/// 
501/// For single-step European options, W_T = √T * Z where Z ~ N(0,1).
502pub fn mc_vega_european_call_gbm_pathwise(cfg: &McConfig) -> f64 {
503    let n = cfg.paths;
504    let discount = (-cfg.r * cfg.t).exp();
505    let sqrt_t = cfg.t.sqrt();
506    
507    let k = match cfg.payoff {
508        Payoff::EuropeanCall { k } => k,
509        _ => {
510            return 0.0;
511        }
512    };
513
514    // For single-step European option, we accumulate W_T directly
515    (0..n)
516        .into_par_iter()
517        .map(|i| {
518            let mut rng = rng::seed_rng_from_u64(cfg.seed + i as u64);
519            let z = rng::get_normal_draw(&mut rng);
520            let w_t = sqrt_t * z;  // W_T = sqrt(T) * Z where Z ~ N(0,1)
521            
522            let st = cfg.s0 * ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * cfg.t + cfg.sigma * w_t).exp();
523            
524            let mut vega_path = 0.0;
525            if st > k {
526                // dS_T/dsigma = S_T * (-sigma * T + W_T)
527                let ds_dsigma = st * (-cfg.sigma * cfg.t + w_t);
528                vega_path = ds_dsigma;
529            }
530            
531            if cfg.use_antithetic {
532                let z2 = -z;
533                let w_t2 = sqrt_t * z2;
534                let st2 = cfg.s0 * ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * cfg.t + cfg.sigma * w_t2).exp();
535                
536                let mut vega_path2 = 0.0;
537                if st2 > k {
538                    let ds_dsigma2 = st2 * (-cfg.sigma * cfg.t + w_t2);
539                    vega_path2 = ds_dsigma2;
540                }
541                vega_path = 0.5 * (vega_path + vega_path2);
542            }
543            vega_path
544        })
545        .reduce(|| 0.0, |a,b| a + b) / n as f64 * discount
546}
547
548/// Monte Carlo Rho calculation using pathwise derivative method
549/// 
550/// # Mathematical Framework
551/// 
552/// For European call, Rho is the derivative w.r.t. risk-free rate r:
553/// ```text
554/// ∂/∂r [e^(-rT) * max(S_T - K, 0)] = ∂/∂r [e^(-rT)] * payoff + e^(-rT) * ∂payoff/∂r
555/// ```
556/// 
557/// This gives:
558/// ```text
559/// ρ = -T * e^(-rT) * payoff + e^(-rT) * 1_{S_T > K} * ∂S_T/∂r
560/// ```
561/// 
562/// Where:
563/// ```text
564/// ∂S_T/∂r = S_T * T  (from the drift term in GBM)
565/// ```
566/// 
567/// # Algorithm
568/// 
569/// 1. Simulate terminal price S_T
570/// 2. Compute payoff and indicator function
571/// 3. Apply Rho formula: ρ_path = -T * payoff + 1_{S_T > K} * S_T * T
572/// 4. Discount: ρ = e^(-rT) * E\[ρ_path\]
573pub fn mc_rho_european_call_gbm_pathwise(cfg: &McConfig) -> f64 {
574    let n = cfg.paths;
575    let discount = (-cfg.r * cfg.t).exp();
576    let sqrt_t = cfg.t.sqrt();
577    
578    let k = match cfg.payoff {
579        Payoff::EuropeanCall { k } => k,
580        _ => {
581            return 0.0;
582        }
583    };
584
585    (0..n)
586        .into_par_iter()
587        .map(|i| {
588            let mut rng = rng::seed_rng_from_u64(cfg.seed + i as u64);
589            let z = rng::get_normal_draw(&mut rng);
590            
591            let st = cfg.s0 * ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * cfg.t + cfg.sigma * sqrt_t * z).exp();
592            
593            let payoff = (st - k).max(0.0);
594            let indicator = if st > k { 1.0 } else { 0.0 };
595            
596            // Rho = -T * e^(-rT) * payoff + e^(-rT) * indicator * dS_T/dr
597            // where dS_T/dr = S_T * T
598            let ds_dr = st * cfg.t;
599            let mut rho_path = -cfg.t * payoff + indicator * ds_dr;
600            
601            if cfg.use_antithetic {
602                let z2 = -z;
603                let st2 = cfg.s0 * ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * cfg.t + cfg.sigma * sqrt_t * z2).exp();
604                
605                let payoff2 = (st2 - k).max(0.0);
606                let indicator2 = if st2 > k { 1.0 } else { 0.0 };
607                let ds_dr2 = st2 * cfg.t;
608                let rho_path2 = -cfg.t * payoff2 + indicator2 * ds_dr2;
609                
610                rho_path = 0.5 * (rho_path + rho_path2);
611            }
612            rho_path
613        })
614        .reduce(|| 0.0, |a,b| a + b) / n as f64 * discount
615}
616
617/// Monte Carlo Gamma calculation using central finite difference
618/// 
619/// # Mathematical Framework
620/// 
621/// Gamma (Γ) is the second derivative of option price w.r.t. underlying price:
622/// ```text
623/// Γ = ∂²V/∂S₀²
624/// ```
625/// 
626/// Since pathwise Gamma has issues with discontinuous payoffs (kink at S_T = K),
627/// we use central finite difference on Delta:
628/// ```text
629/// Γ ≈ [Δ(S₀ + ε) - Δ(S₀ - ε)] / (2ε)
630/// ```
631/// 
632/// # Algorithm
633/// 
634/// 1. Compute Delta at S₀ + ε using pathwise method
635/// 2. Compute Delta at S₀ - ε using same random seeds (common random numbers)
636/// 3. Apply central difference formula
637/// 
638/// # Parameters
639/// 
640/// - Default ε = 0.1% of spot price (balances bias vs. variance)
641/// - Uses same RNG seeds for variance reduction
642/// 
643/// # Note
644/// 
645/// This is a simple implementation. The batched version below is more efficient
646/// as it uses common random numbers within a single parallel loop.
647pub fn mc_gamma_european_call_gbm_finite_diff(cfg: &McConfig) -> f64 {
648    // Use provided epsilon or default to 1e-3 * s0
649    let epsilon = cfg.epsilon.unwrap_or(1e-3 * cfg.s0);
650    
651    // Create configs for spot up and spot down
652    let mut cfg_up = cfg.clone();
653    cfg_up.s0 = cfg.s0 + epsilon;
654    
655    let mut cfg_down = cfg.clone();
656    cfg_down.s0 = cfg.s0 - epsilon;
657    
658    // Compute Delta at both spot levels using the same seed for common random numbers
659    let delta_up = mc_delta_european_call_gbm_pathwise(&cfg_up);
660    let delta_down = mc_delta_european_call_gbm_pathwise(&cfg_down);
661    
662    // Central finite difference for Gamma
663    (delta_up - delta_down) / (2.0 * epsilon)
664}
665
666/// Efficient batched Gamma calculation with common random numbers
667/// 
668/// # Mathematical Framework
669/// 
670/// Same as simple finite difference but optimized for performance:
671/// ```text
672/// Γ ≈ [Δ(S₀ + ε) - Δ(S₀ - ε)] / (2ε)
673/// ```
674/// 
675/// # Optimization Strategy
676/// 
677/// Instead of calling Delta function twice, this implementation:
678/// 1. Uses single parallel loop over paths
679/// 2. Applies same random draw Z to both (S₀ + ε) and (S₀ - ε) scenarios
680/// 3. Computes both deltas simultaneously for maximum variance reduction
681/// 
682/// # Common Random Numbers
683/// 
684/// Critical for variance reduction: same Brownian path used for both
685/// spot scenarios ensures that:
686/// ```text
687/// Var[Δ(S₀ + ε) - Δ(S₀ - ε)] << Var[Δ(S₀ + ε)] + Var[Δ(S₀ - ε)]
688/// ```
689/// 
690/// # Performance
691/// 
692/// ~2x faster than calling Delta function separately due to:
693/// - Single RNG initialization per path
694/// - Better cache locality
695/// - Reduced parallel overhead
696pub fn mc_gamma_european_call_gbm_finite_diff_batched(cfg: &McConfig) -> f64 {
697    let n = cfg.paths;
698    let discount = (-cfg.r * cfg.t).exp();
699    let sqrt_t = cfg.t.sqrt();
700    
701    let k = match cfg.payoff {
702        Payoff::EuropeanCall { k } => k,
703        _ => {
704            return 0.0;
705        }
706    };
707    
708    // Use provided epsilon or default to 1e-3 * s0
709    let epsilon = cfg.epsilon.unwrap_or(1e-3 * cfg.s0);
710    let s0_up = cfg.s0 + epsilon;
711    let s0_down = cfg.s0 - epsilon;
712    
713    // Compute deltas for both spot scenarios using common random numbers
714    let (sum_delta_up, sum_delta_down) = (0..n)
715        .into_par_iter()
716        .map(|i| {
717            // Use the same RNG seed for both scenarios to ensure common random numbers
718            let mut rng = rng::seed_rng_from_u64(cfg.seed + i as u64);
719            let z = rng::get_normal_draw(&mut rng);
720            
721            // Compute terminal stock prices for both spot scenarios
722            let st_up = s0_up * ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * cfg.t + cfg.sigma * sqrt_t * z).exp();
723            let st_down = s0_down * ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * cfg.t + cfg.sigma * sqrt_t * z).exp();
724            
725            // Pathwise delta for spot up
726            let delta_up = if st_up > k { st_up / s0_up } else { 0.0 };
727            
728            // Pathwise delta for spot down
729            let delta_down = if st_down > k { st_down / s0_down } else { 0.0 };
730            
731            if cfg.use_antithetic {
732                let z2 = -z;
733                
734                let st_up2 = s0_up * ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * cfg.t + cfg.sigma * sqrt_t * z2).exp();
735                let st_down2 = s0_down * ((cfg.r - 0.5 * cfg.sigma * cfg.sigma) * cfg.t + cfg.sigma * sqrt_t * z2).exp();
736                
737                let delta_up2 = if st_up2 > k { st_up2 / s0_up } else { 0.0 };
738                let delta_down2 = if st_down2 > k { st_down2 / s0_down } else { 0.0 };
739                
740                (0.5 * (delta_up + delta_up2), 0.5 * (delta_down + delta_down2))
741            } else {
742                (delta_up, delta_down)
743            }
744        })
745        .reduce(|| (0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1));
746    
747    let mean_delta_up = sum_delta_up / n as f64 * discount;
748    let mean_delta_down = sum_delta_down / n as f64 * discount;
749    
750    // Central finite difference for Gamma
751    (mean_delta_up - mean_delta_down) / (2.0 * epsilon)
752}