rustyqlib/core/montecarlo/
variance_reduction.rs1pub fn moment_match(draws: &mut [f64]) {
11 let n = draws.len() as f64;
12 let mean = draws.iter().sum::<f64>() / n;
13 let var = draws.iter().map(|z| (z - mean) * (z - mean)).sum::<f64>() / n;
14 let std = var.sqrt();
15 if std > 0.0 {
16 for z in draws.iter_mut() {
17 *z = (*z - mean) / std;
18 }
19 }
20}
21
22pub fn control_variate_estimate(
31 payoffs: &[f64],
32 controls: &[f64],
33 control_mean: f64,
34) -> (f64, f64, f64) {
35 assert_eq!(payoffs.len(), controls.len());
36 let n = payoffs.len() as f64;
37 assert!(n > 1.0, "need at least two paths");
38 let mean_y = payoffs.iter().sum::<f64>() / n;
39 let mean_c = controls.iter().sum::<f64>() / n;
40 let mut cov = 0.0;
41 let mut var_c = 0.0;
42 for (y, c) in payoffs.iter().zip(controls) {
43 cov += (y - mean_y) * (c - mean_c);
44 var_c += (c - mean_c) * (c - mean_c);
45 }
46 let beta = if var_c > 0.0 { cov / var_c } else { 0.0 };
47
48 let mut sum = 0.0;
49 let mut sum_sq = 0.0;
50 for (y, c) in payoffs.iter().zip(controls) {
51 let adjusted = y - beta * (c - control_mean);
52 sum += adjusted;
53 sum_sq += adjusted * adjusted;
54 }
55 let mean = sum / n;
56 let var = (sum_sq / n - mean * mean).max(0.0);
57 (mean, (var / n).sqrt(), beta)
58}
59
60#[cfg(test)]
61mod tests {
62 use super::super::rng::path_normals;
63 use super::*;
64
65 #[test]
66 fn moment_matching_standardizes_exactly() {
67 let mut z = vec![0.0; 1001];
68 path_normals(3, 0, &mut z);
69 moment_match(&mut z);
70 let n = z.len() as f64;
71 let mean: f64 = z.iter().sum::<f64>() / n;
72 let var: f64 = z.iter().map(|x| x * x).sum::<f64>() / n;
73 assert!(mean.abs() < 1e-12 && (var - 1.0).abs() < 1e-12);
74 }
75
76 #[test]
77 fn control_variate_removes_correlated_noise() {
78 let n = 4000;
81 let mut c = vec![0.0; n];
82 let mut eps = vec![0.0; n];
83 path_normals(11, 0, &mut c);
84 path_normals(11, 1, &mut eps);
85 let payoffs: Vec<f64> =
86 c.iter().zip(&eps).map(|(ci, ei)| 3.0 + 2.0 * ci + 0.1 * ei).collect();
87
88 let (est, se, beta) = control_variate_estimate(&payoffs, &c, 0.0);
89 assert!((beta - 2.0).abs() < 0.05, "beta {beta}");
90 assert!((est - 3.0).abs() < 0.01, "estimate {est}");
91 let mean_y: f64 = payoffs.iter().sum::<f64>() / n as f64;
93 let var_y: f64 =
94 payoffs.iter().map(|y| (y - mean_y) * (y - mean_y)).sum::<f64>() / n as f64;
95 let raw_se = (var_y / n as f64).sqrt();
96 assert!(se < 0.1 * raw_se, "cv se {se} vs raw {raw_se}");
97 }
98
99 #[test]
100 fn zero_variance_control_degrades_gracefully() {
101 let payoffs = [1.0, 2.0, 3.0];
102 let controls = [5.0, 5.0, 5.0];
103 let (est, _, beta) = control_variate_estimate(&payoffs, &controls, 5.0);
104 assert_eq!(beta, 0.0);
105 assert!((est - 2.0).abs() < 1e-12);
106 }
107}