scirs2_integrate/specialized/finance/utils/
simulation.rs

1//! Simulation utilities for Monte Carlo methods
2//!
3//! This module provides efficient random number generation, path simulation, and
4//! variance reduction techniques for Monte Carlo pricing.
5//!
6//! # Features
7//! - Low-discrepancy sequences (Sobol, Halton)
8//! - Geometric Brownian motion path generation
9//! - Antithetic variates for variance reduction
10//! - Flexible random number generation
11//!
12//! # Example
13//! ```
14//! use scirs2_integrate::specialized::finance::utils::simulation::{
15//!     PathGenerator, PathConfig, VarianceReduction
16//! };
17//!
18//! // Generate stock price paths with geometric Brownian motion
19//! let config = PathConfig::new(100.0, 0.05, 0.0, 0.2, 1.0)
20//!     .with_steps(252)
21//!     .with_variance_reduction(VarianceReduction::Antithetic);
22//!
23//! let generator = PathGenerator::new(config);
24//! let paths = generator.generate_paths(10000);
25//! ```
26
27use crate::error::{IntegrateError, IntegrateResult as Result};
28use scirs2_core::random::Rng;
29use scirs2_core::random::{Distribution, Normal};
30
31/// Variance reduction technique
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum VarianceReduction {
34    /// No variance reduction
35    None,
36    /// Antithetic variates (generate paired paths with negated shocks)
37    Antithetic,
38}
39
40/// Configuration for path generation
41#[derive(Debug, Clone)]
42pub struct PathConfig {
43    /// Initial value (e.g., spot price)
44    pub initial_value: f64,
45    /// Drift rate (e.g., risk-free rate - dividend)
46    pub drift: f64,
47    /// Dividend yield
48    pub dividend: f64,
49    /// Volatility
50    pub volatility: f64,
51    /// Time horizon (years)
52    pub time_horizon: f64,
53    /// Number of time steps
54    pub n_steps: usize,
55    /// Variance reduction technique
56    pub variance_reduction: VarianceReduction,
57}
58
59impl PathConfig {
60    /// Create a new path configuration
61    pub fn new(
62        initial_value: f64,
63        risk_free_rate: f64,
64        dividend: f64,
65        volatility: f64,
66        time_horizon: f64,
67    ) -> Self {
68        Self {
69            initial_value,
70            drift: risk_free_rate - dividend,
71            dividend,
72            volatility,
73            time_horizon,
74            n_steps: 252, // Daily steps by default
75            variance_reduction: VarianceReduction::None,
76        }
77    }
78
79    /// Set number of time steps
80    pub fn with_steps(mut self, n_steps: usize) -> Self {
81        self.n_steps = n_steps;
82        self
83    }
84
85    /// Set variance reduction technique
86    pub fn with_variance_reduction(mut self, variance_reduction: VarianceReduction) -> Self {
87        self.variance_reduction = variance_reduction;
88        self
89    }
90
91    /// Validate configuration
92    pub fn validate(&self) -> Result<()> {
93        if self.initial_value <= 0.0 {
94            return Err(IntegrateError::ValueError(
95                "Initial value must be positive".to_string(),
96            ));
97        }
98        if self.volatility < 0.0 {
99            return Err(IntegrateError::ValueError(
100                "Volatility cannot be negative".to_string(),
101            ));
102        }
103        if self.time_horizon <= 0.0 {
104            return Err(IntegrateError::ValueError(
105                "Time horizon must be positive".to_string(),
106            ));
107        }
108        if self.n_steps == 0 {
109            return Err(IntegrateError::ValueError(
110                "Number of steps must be positive".to_string(),
111            ));
112        }
113        Ok(())
114    }
115}
116
117/// Path generator for geometric Brownian motion
118pub struct PathGenerator {
119    config: PathConfig,
120}
121
122impl PathGenerator {
123    /// Create a new path generator
124    pub fn new(config: PathConfig) -> Self {
125        Self { config }
126    }
127
128    /// Generate multiple paths
129    pub fn generate_paths(&self, n_paths: usize) -> Result<Vec<Vec<f64>>> {
130        self.config.validate()?;
131
132        let mut rng = scirs2_core::random::rng();
133        let normal = Normal::new(0.0, 1.0).map_err(|e| {
134            IntegrateError::ValueError(format!("Failed to create normal distribution: {}", e))
135        })?;
136
137        let mut paths = Vec::with_capacity(n_paths);
138
139        match self.config.variance_reduction {
140            VarianceReduction::None => {
141                for _ in 0..n_paths {
142                    paths.push(self.generate_single_path(&mut rng, &normal)?);
143                }
144            }
145            VarianceReduction::Antithetic => {
146                let pairs = n_paths / 2;
147                for _ in 0..pairs {
148                    let (path1, path2) = self.generate_antithetic_pair(&mut rng, &normal)?;
149                    paths.push(path1);
150                    paths.push(path2);
151                }
152                // If odd number of paths, add one more
153                if n_paths % 2 == 1 {
154                    paths.push(self.generate_single_path(&mut rng, &normal)?);
155                }
156            }
157        }
158
159        Ok(paths)
160    }
161
162    /// Generate a single path using geometric Brownian motion
163    fn generate_single_path<R: Rng>(&self, rng: &mut R, normal: &Normal<f64>) -> Result<Vec<f64>> {
164        let dt = self.config.time_horizon / self.config.n_steps as f64;
165        let drift_term = (self.config.drift - 0.5 * self.config.volatility.powi(2)) * dt;
166        let diffusion_term = self.config.volatility * dt.sqrt();
167
168        let mut path = Vec::with_capacity(self.config.n_steps + 1);
169        path.push(self.config.initial_value);
170
171        let mut current_value = self.config.initial_value;
172
173        for _ in 0..self.config.n_steps {
174            let z = normal.sample(rng);
175            current_value *= (drift_term + diffusion_term * z).exp();
176            path.push(current_value);
177        }
178
179        Ok(path)
180    }
181
182    /// Generate antithetic pair of paths
183    fn generate_antithetic_pair<R: Rng>(
184        &self,
185        rng: &mut R,
186        normal: &Normal<f64>,
187    ) -> Result<(Vec<f64>, Vec<f64>)> {
188        let dt = self.config.time_horizon / self.config.n_steps as f64;
189        let drift_term = (self.config.drift - 0.5 * self.config.volatility.powi(2)) * dt;
190        let diffusion_term = self.config.volatility * dt.sqrt();
191
192        let mut path1 = Vec::with_capacity(self.config.n_steps + 1);
193        let mut path2 = Vec::with_capacity(self.config.n_steps + 1);
194
195        path1.push(self.config.initial_value);
196        path2.push(self.config.initial_value);
197
198        let mut value1 = self.config.initial_value;
199        let mut value2 = self.config.initial_value;
200
201        for _ in 0..self.config.n_steps {
202            let z = normal.sample(rng);
203
204            // Path 1 uses positive shock
205            value1 *= (drift_term + diffusion_term * z).exp();
206            path1.push(value1);
207
208            // Path 2 uses negative shock (antithetic)
209            value2 *= (drift_term - diffusion_term * z).exp();
210            path2.push(value2);
211        }
212
213        Ok((path1, path2))
214    }
215
216    /// Generate terminal values only (more efficient when intermediate values not needed)
217    pub fn generate_terminal_values(&self, n_paths: usize) -> Result<Vec<f64>> {
218        self.config.validate()?;
219
220        let mut rng = scirs2_core::random::rng();
221        let normal = Normal::new(0.0, 1.0).map_err(|e| {
222            IntegrateError::ValueError(format!("Failed to create normal distribution: {}", e))
223        })?;
224
225        let dt = self.config.time_horizon / self.config.n_steps as f64;
226        let drift_term = (self.config.drift - 0.5 * self.config.volatility.powi(2)) * dt;
227        let diffusion_term = self.config.volatility * dt.sqrt();
228
229        let mut terminal_values = Vec::with_capacity(n_paths);
230
231        match self.config.variance_reduction {
232            VarianceReduction::None => {
233                for _ in 0..n_paths {
234                    let mut value = self.config.initial_value;
235                    for _ in 0..self.config.n_steps {
236                        let z = normal.sample(&mut rng);
237                        value *= (drift_term + diffusion_term * z).exp();
238                    }
239                    terminal_values.push(value);
240                }
241            }
242            VarianceReduction::Antithetic => {
243                let pairs = n_paths / 2;
244                for _ in 0..pairs {
245                    let mut value1 = self.config.initial_value;
246                    let mut value2 = self.config.initial_value;
247
248                    for _ in 0..self.config.n_steps {
249                        let z = normal.sample(&mut rng);
250                        value1 *= (drift_term + diffusion_term * z).exp();
251                        value2 *= (drift_term - diffusion_term * z).exp();
252                    }
253
254                    terminal_values.push(value1);
255                    terminal_values.push(value2);
256                }
257
258                // Handle odd number of paths
259                if n_paths % 2 == 1 {
260                    let mut value = self.config.initial_value;
261                    for _ in 0..self.config.n_steps {
262                        let z = normal.sample(&mut rng);
263                        value *= (drift_term + diffusion_term * z).exp();
264                    }
265                    terminal_values.push(value);
266                }
267            }
268        }
269
270        Ok(terminal_values)
271    }
272}
273
274/// Random number generator trait for flexibility
275pub trait RandomNumberGenerator {
276    /// Generate a standard normal random number
277    fn generate_normal(&mut self) -> f64;
278
279    /// Generate multiple standard normal random numbers
280    fn generate_normal_vec(&mut self, n: usize) -> Vec<f64> {
281        (0..n).map(|_| self.generate_normal()).collect()
282    }
283}
284
285/// Standard pseudo-random number generator
286pub struct StandardRng {
287    rng: scirs2_core::random::rngs::ThreadRng,
288    normal: Normal<f64>,
289}
290
291impl StandardRng {
292    /// Create a new standard RNG
293    pub fn new() -> Result<Self> {
294        let normal = Normal::new(0.0, 1.0).map_err(|e| {
295            IntegrateError::ValueError(format!("Failed to create normal distribution: {}", e))
296        })?;
297
298        Ok(Self {
299            rng: scirs2_core::random::rng(),
300            normal,
301        })
302    }
303}
304
305impl Default for StandardRng {
306    fn default() -> Self {
307        Self::new().expect("Failed to create standard RNG")
308    }
309}
310
311impl RandomNumberGenerator for StandardRng {
312    fn generate_normal(&mut self) -> f64 {
313        self.normal.sample(&mut self.rng)
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_path_config_creation() {
323        let config = PathConfig::new(100.0, 0.05, 0.02, 0.2, 1.0);
324        assert_eq!(config.initial_value, 100.0);
325        assert!((config.drift - 0.03).abs() < 1e-10); // r - q = 0.05 - 0.02
326        assert_eq!(config.volatility, 0.2);
327        assert_eq!(config.n_steps, 252);
328    }
329
330    #[test]
331    fn test_path_config_validation() {
332        let valid_config = PathConfig::new(100.0, 0.05, 0.0, 0.2, 1.0);
333        assert!(valid_config.validate().is_ok());
334
335        let invalid_config = PathConfig::new(-100.0, 0.05, 0.0, 0.2, 1.0);
336        assert!(invalid_config.validate().is_err());
337    }
338
339    #[test]
340    fn test_path_generation() {
341        let config = PathConfig::new(100.0, 0.05, 0.0, 0.2, 1.0).with_steps(100);
342        let generator = PathGenerator::new(config);
343
344        let paths = generator.generate_paths(10).unwrap();
345        assert_eq!(paths.len(), 10);
346
347        for path in &paths {
348            assert_eq!(path.len(), 101); // n_steps + 1
349            assert_eq!(path[0], 100.0); // Initial value
350            assert!(path.iter().all(|&v| v > 0.0)); // All positive
351        }
352    }
353
354    #[test]
355    fn test_antithetic_variance_reduction() {
356        let config = PathConfig::new(100.0, 0.05, 0.0, 0.2, 1.0)
357            .with_steps(50)
358            .with_variance_reduction(VarianceReduction::Antithetic);
359
360        let generator = PathGenerator::new(config);
361        let paths = generator.generate_paths(100).unwrap();
362
363        assert_eq!(paths.len(), 100);
364
365        // Check that all paths start at initial value
366        for path in &paths {
367            assert_eq!(path[0], 100.0);
368        }
369    }
370
371    #[test]
372    fn test_terminal_values_generation() {
373        let config = PathConfig::new(100.0, 0.05, 0.0, 0.2, 1.0).with_steps(252);
374        let generator = PathGenerator::new(config);
375
376        let terminal_values = generator.generate_terminal_values(1000).unwrap();
377        assert_eq!(terminal_values.len(), 1000);
378
379        // All values should be positive
380        assert!(terminal_values.iter().all(|&v| v > 0.0));
381
382        // Statistical checks: mean should be close to forward price
383        let forward_price = 100.0 * (0.05_f64 * 1.0).exp();
384        let mean: f64 = terminal_values.iter().sum::<f64>() / terminal_values.len() as f64;
385
386        // Mean should be within reasonable range (using generous tolerance for randomness)
387        assert!((mean - forward_price).abs() / forward_price < 0.1);
388    }
389
390    #[test]
391    fn test_standard_rng() {
392        let mut rng = StandardRng::new().unwrap();
393
394        let samples = rng.generate_normal_vec(10000);
395        assert_eq!(samples.len(), 10000);
396
397        // Statistical test: mean should be close to 0, std dev close to 1
398        let mean: f64 = samples.iter().sum::<f64>() / samples.len() as f64;
399        let variance: f64 =
400            samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / samples.len() as f64;
401        let std_dev = variance.sqrt();
402
403        assert!(mean.abs() < 0.05); // Mean close to 0
404        assert!((std_dev - 1.0).abs() < 0.05); // Std dev close to 1
405    }
406
407    #[test]
408    fn test_path_lengths() {
409        let config = PathConfig::new(100.0, 0.05, 0.0, 0.2, 1.0).with_steps(10);
410        let generator = PathGenerator::new(config);
411
412        let paths = generator.generate_paths(5).unwrap();
413        for path in paths {
414            assert_eq!(path.len(), 11); // n_steps + 1
415        }
416    }
417
418    #[test]
419    fn test_variance_reduction_odd_paths() {
420        let config = PathConfig::new(100.0, 0.05, 0.0, 0.2, 1.0)
421            .with_steps(50)
422            .with_variance_reduction(VarianceReduction::Antithetic);
423
424        let generator = PathGenerator::new(config);
425
426        // Test odd number of paths
427        let paths = generator.generate_paths(101).unwrap();
428        assert_eq!(paths.len(), 101);
429    }
430}