scirs2_integrate/specialized/finance/utils/
simulation.rs1use crate::error::{IntegrateError, IntegrateResult as Result};
28use scirs2_core::random::Rng;
29use scirs2_core::random::{Distribution, Normal};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum VarianceReduction {
34 None,
36 Antithetic,
38}
39
40#[derive(Debug, Clone)]
42pub struct PathConfig {
43 pub initial_value: f64,
45 pub drift: f64,
47 pub dividend: f64,
49 pub volatility: f64,
51 pub time_horizon: f64,
53 pub n_steps: usize,
55 pub variance_reduction: VarianceReduction,
57}
58
59impl PathConfig {
60 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, variance_reduction: VarianceReduction::None,
76 }
77 }
78
79 pub fn with_steps(mut self, n_steps: usize) -> Self {
81 self.n_steps = n_steps;
82 self
83 }
84
85 pub fn with_variance_reduction(mut self, variance_reduction: VarianceReduction) -> Self {
87 self.variance_reduction = variance_reduction;
88 self
89 }
90
91 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
117pub struct PathGenerator {
119 config: PathConfig,
120}
121
122impl PathGenerator {
123 pub fn new(config: PathConfig) -> Self {
125 Self { config }
126 }
127
128 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 n_paths % 2 == 1 {
154 paths.push(self.generate_single_path(&mut rng, &normal)?);
155 }
156 }
157 }
158
159 Ok(paths)
160 }
161
162 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 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 value1 *= (drift_term + diffusion_term * z).exp();
206 path1.push(value1);
207
208 value2 *= (drift_term - diffusion_term * z).exp();
210 path2.push(value2);
211 }
212
213 Ok((path1, path2))
214 }
215
216 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 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
274pub trait RandomNumberGenerator {
276 fn generate_normal(&mut self) -> f64;
278
279 fn generate_normal_vec(&mut self, n: usize) -> Vec<f64> {
281 (0..n).map(|_| self.generate_normal()).collect()
282 }
283}
284
285pub struct StandardRng {
287 rng: scirs2_core::random::rngs::ThreadRng,
288 normal: Normal<f64>,
289}
290
291impl StandardRng {
292 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); 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); assert_eq!(path[0], 100.0); assert!(path.iter().all(|&v| v > 0.0)); }
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 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 assert!(terminal_values.iter().all(|&v| v > 0.0));
381
382 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 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 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); assert!((std_dev - 1.0).abs() < 0.05); }
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); }
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 let paths = generator.generate_paths(101).unwrap();
428 assert_eq!(paths.len(), 101);
429 }
430}