1use crate::error::{IntegrateError, IntegrateResult};
16use scirs2_core::random::prelude::*;
17use scirs2_core::random::{Normal, Uniform};
18use scirs2_core::Distribution;
19
20pub fn merton_jump_diffusion(
66 mu: f64,
67 sigma: f64,
68 lambda: f64,
69 mu_j: f64,
70 sigma_j: f64,
71 s0: f64,
72 t_span: (f64, f64),
73 dt: f64,
74 rng: &mut StdRng,
75) -> IntegrateResult<Vec<(f64, f64)>> {
76 if sigma <= 0.0 {
77 return Err(IntegrateError::InvalidInput(format!(
78 "sigma must be > 0, got {sigma}"
79 )));
80 }
81 if lambda < 0.0 {
82 return Err(IntegrateError::InvalidInput(format!(
83 "lambda must be >= 0, got {lambda}"
84 )));
85 }
86 if sigma_j < 0.0 {
87 return Err(IntegrateError::InvalidInput(format!(
88 "sigma_j must be >= 0, got {sigma_j}"
89 )));
90 }
91 if s0 <= 0.0 {
92 return Err(IntegrateError::InvalidInput(format!(
93 "s0 must be > 0, got {s0}"
94 )));
95 }
96 validate_t_span(t_span)?;
97 validate_dt(dt)?;
98
99 let k_bar = (mu_j + 0.5 * sigma_j * sigma_j).exp() - 1.0;
101 let mu_comp = mu - lambda * k_bar;
103
104 let normal = Normal::new(0.0, 1.0).map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
105 let uniform =
106 Uniform::new(0.0_f64, 1.0_f64).map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
107
108 let n_steps = ((t_span.1 - t_span.0) / dt).ceil() as usize;
109 let mut path = Vec::with_capacity(n_steps + 1);
110 path.push((t_span.0, s0));
111 let mut s = s0;
112 let mut t = t_span.0;
113
114 for _ in 0..n_steps {
115 let step = (t_span.1 - t).min(dt);
116 let sqrt_step = step.sqrt();
117 let z: f64 = normal.sample(rng);
118 let dw = z * sqrt_step;
119
120 let ds_cont = mu_comp * s * step + sigma * s * dw;
122
123 let n_jumps = poisson_sample(lambda * step, rng, &uniform)?;
125 let mut jump_factor = 1.0;
126 for _ in 0..n_jumps {
127 let z_j: f64 = normal.sample(rng);
128 let log_jump = mu_j + sigma_j * z_j;
129 jump_factor *= log_jump.exp(); }
131 s = ((s + ds_cont) * jump_factor).max(1e-10);
133 t += step;
134 path.push((t, s));
135 }
136 Ok(path)
137}
138
139pub fn compound_poisson_process<F>(
186 lambda: f64,
187 jump_dist: F,
188 t_span: (f64, f64),
189 rng: &mut StdRng,
190) -> IntegrateResult<Vec<(f64, f64)>>
191where
192 F: Fn(&mut StdRng) -> f64,
193{
194 if lambda <= 0.0 {
195 return Err(IntegrateError::InvalidInput(format!(
196 "lambda must be > 0, got {lambda}"
197 )));
198 }
199 validate_t_span(t_span)?;
200
201 let uniform =
203 Uniform::new(0.0_f64, 1.0_f64).map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
204
205 let mut path = Vec::new();
206 path.push((t_span.0, 0.0));
207 let mut x = 0.0_f64;
208 let mut t = t_span.0;
209
210 loop {
211 let u: f64 = uniform.sample(rng);
213 let inter = -u.ln() / lambda;
214 t += inter;
215 if t >= t_span.1 {
216 break;
217 }
218 let jump = jump_dist(rng);
219 x += jump;
220 path.push((t, x));
221 }
222 path.push((t_span.1, x));
223 Ok(path)
224}
225
226pub fn kou_double_exponential(
257 mu: f64,
258 sigma: f64,
259 lambda: f64,
260 p: f64,
261 eta1: f64,
262 eta2: f64,
263 s0: f64,
264 t_span: (f64, f64),
265 dt: f64,
266 rng: &mut StdRng,
267) -> IntegrateResult<Vec<(f64, f64)>> {
268 if sigma <= 0.0 {
269 return Err(IntegrateError::InvalidInput(format!(
270 "sigma must be > 0, got {sigma}"
271 )));
272 }
273 if lambda < 0.0 {
274 return Err(IntegrateError::InvalidInput(format!(
275 "lambda must be >= 0, got {lambda}"
276 )));
277 }
278 if !(0.0 < p && p < 1.0) {
279 return Err(IntegrateError::InvalidInput(format!(
280 "p must be in (0,1), got {p}"
281 )));
282 }
283 if eta1 <= 1.0 {
284 return Err(IntegrateError::InvalidInput(format!(
285 "eta1 must be > 1, got {eta1}"
286 )));
287 }
288 if eta2 <= 0.0 {
289 return Err(IntegrateError::InvalidInput(format!(
290 "eta2 must be > 0, got {eta2}"
291 )));
292 }
293 if s0 <= 0.0 {
294 return Err(IntegrateError::InvalidInput(format!(
295 "s0 must be > 0, got {s0}"
296 )));
297 }
298 validate_t_span(t_span)?;
299 validate_dt(dt)?;
300
301 let k_bar = p * eta1 / (eta1 - 1.0) + (1.0 - p) * eta2 / (eta2 + 1.0) - 1.0;
304 let mu_comp = mu - lambda * k_bar;
305
306 let normal = Normal::new(0.0, 1.0).map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
307 let uniform =
308 Uniform::new(0.0_f64, 1.0_f64).map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
309
310 let n_steps = ((t_span.1 - t_span.0) / dt).ceil() as usize;
311 let mut path = Vec::with_capacity(n_steps + 1);
312 path.push((t_span.0, s0));
313 let mut s = s0;
314 let mut t = t_span.0;
315
316 for _ in 0..n_steps {
317 let step = (t_span.1 - t).min(dt);
318 let sqrt_step = step.sqrt();
319 let z: f64 = normal.sample(rng);
320 let dw = z * sqrt_step;
321
322 let ds_cont = mu_comp * s * step + sigma * s * dw;
323 let n_jumps = poisson_sample(lambda * step, rng, &uniform)?;
324 let mut log_jump_sum: f64 = 0.0;
325 for _ in 0..n_jumps {
326 let u_type: f64 = uniform.sample(rng);
327 let u_mag: f64 = uniform.sample(rng);
328 if u_type < p {
329 log_jump_sum += -u_mag.ln() / eta1;
331 } else {
332 log_jump_sum -= -u_mag.ln() / eta2;
334 }
335 }
336 let jump_mult = log_jump_sum.exp();
337 s = ((s + ds_cont) * jump_mult).max(1e-10);
338 t += step;
339 path.push((t, s));
340 }
341 Ok(path)
342}
343
344pub struct JumpDiffusionProblem<Drift, Diffusion, JumpSampler>
357where
358 Drift: Fn(f64, f64) -> f64,
359 Diffusion: Fn(f64, f64) -> f64,
360 JumpSampler: Fn(&mut StdRng) -> f64,
361{
362 pub drift: Drift,
364 pub diffusion: Diffusion,
366 pub jump_intensity: f64,
368 pub jump_sampler: JumpSampler,
370 pub x0: f64,
372 pub t_span: (f64, f64),
374}
375
376impl<Drift, Diffusion, JumpSampler> JumpDiffusionProblem<Drift, Diffusion, JumpSampler>
377where
378 Drift: Fn(f64, f64) -> f64,
379 Diffusion: Fn(f64, f64) -> f64,
380 JumpSampler: Fn(&mut StdRng) -> f64,
381{
382 pub fn new(
384 drift: Drift,
385 diffusion: Diffusion,
386 jump_intensity: f64,
387 jump_sampler: JumpSampler,
388 x0: f64,
389 t_span: (f64, f64),
390 ) -> IntegrateResult<Self> {
391 if jump_intensity < 0.0 {
392 return Err(IntegrateError::InvalidInput(format!(
393 "jump_intensity must be >= 0, got {jump_intensity}"
394 )));
395 }
396 validate_t_span(t_span)?;
397 Ok(Self {
398 drift,
399 diffusion,
400 jump_intensity,
401 jump_sampler,
402 x0,
403 t_span,
404 })
405 }
406
407 pub fn solve(&self, dt: f64, rng: &mut StdRng) -> IntegrateResult<Vec<(f64, f64)>> {
411 validate_dt(dt)?;
412 let normal =
413 Normal::new(0.0, 1.0).map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
414 let uniform = Uniform::new(0.0_f64, 1.0_f64)
415 .map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
416
417 let n_steps = ((self.t_span.1 - self.t_span.0) / dt).ceil() as usize;
418 let mut path = Vec::with_capacity(n_steps + 1);
419 path.push((self.t_span.0, self.x0));
420 let mut x = self.x0;
421 let mut t = self.t_span.0;
422
423 for _ in 0..n_steps {
424 let step = (self.t_span.1 - t).min(dt);
425 let sqrt_step = step.sqrt();
426 let z: f64 = normal.sample(rng);
427 let fx = (self.drift)(x, t);
428 let gx = (self.diffusion)(x, t);
429 let n_jumps = poisson_sample(self.jump_intensity * step, rng, &uniform)?;
430 let jump_sum: f64 = (0..n_jumps).map(|_| (self.jump_sampler)(rng)).sum();
431 x = x + fx * step + gx * z * sqrt_step + jump_sum;
432 t += step;
433 path.push((t, x));
434 }
435 Ok(path)
436 }
437}
438
439fn poisson_sample(lambda: f64, rng: &mut StdRng, uniform: &Uniform<f64>) -> IntegrateResult<usize> {
446 if lambda < 0.0 {
447 return Err(IntegrateError::InvalidInput(format!(
448 "Poisson lambda must be >= 0, got {lambda}"
449 )));
450 }
451 if lambda == 0.0 {
452 return Ok(0);
453 }
454 if lambda > 30.0 {
456 let normal = Normal::new(lambda, lambda.sqrt())
457 .map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
458 let sample: f64 = normal.sample(rng);
459 return Ok(sample.round().max(0.0) as usize);
460 }
461 let threshold = (-lambda).exp();
463 let mut count = 0usize;
464 let mut product = uniform.sample(rng);
465 while product > threshold {
466 product *= uniform.sample(rng);
467 count += 1;
468 }
469 Ok(count)
470}
471
472fn validate_t_span(t_span: (f64, f64)) -> IntegrateResult<()> {
473 if t_span.0 >= t_span.1 {
474 Err(IntegrateError::InvalidInput(format!(
475 "t_span must satisfy t0 < t1, got {:?}",
476 t_span
477 )))
478 } else {
479 Ok(())
480 }
481}
482
483fn validate_dt(dt: f64) -> IntegrateResult<()> {
484 if dt <= 0.0 {
485 Err(IntegrateError::InvalidInput(format!(
486 "dt must be > 0, got {dt}"
487 )))
488 } else {
489 Ok(())
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496 use scirs2_core::random::prelude::seeded_rng;
497
498 #[test]
499 fn test_merton_positive_price() {
500 let mut rng = seeded_rng(42);
501 let path =
502 merton_jump_diffusion(0.05, 0.2, 1.0, -0.1, 0.2, 100.0, (0.0, 1.0), 0.01, &mut rng)
503 .expect("merton_jump_diffusion should succeed");
504 assert!(!path.is_empty());
505 assert!(
506 path.iter().all(|&(_, s)| s > 0.0),
507 "Merton S must stay positive"
508 );
509 }
510
511 #[test]
512 fn test_merton_no_jumps() {
513 let mut rng = seeded_rng(1);
515 let path =
516 merton_jump_diffusion(0.05, 0.2, 0.0, 0.0, 0.0, 100.0, (0.0, 1.0), 0.01, &mut rng)
517 .expect("merton_jump_diffusion should succeed");
518 assert!(path.iter().all(|&(_, s)| s > 0.0));
519 }
520
521 #[test]
522 fn test_merton_invalid_sigma() {
523 let mut rng = seeded_rng(0);
524 assert!(merton_jump_diffusion(
525 0.05,
526 -0.2,
527 1.0,
528 0.0,
529 0.1,
530 100.0,
531 (0.0, 1.0),
532 0.01,
533 &mut rng
534 )
535 .is_err());
536 }
537
538 #[test]
539 fn test_merton_invalid_s0() {
540 let mut rng = seeded_rng(0);
541 assert!(
542 merton_jump_diffusion(0.05, 0.2, 1.0, 0.0, 0.1, -1.0, (0.0, 1.0), 0.01, &mut rng)
543 .is_err()
544 );
545 }
546
547 #[test]
548 fn test_compound_poisson_basic() {
549 let mut rng = seeded_rng(7);
550 let normal =
551 Normal::new(0.0_f64, 1.0).expect("Normal::new should succeed with valid params");
552 let path = compound_poisson_process(2.0, |r| normal.sample(r), (0.0, 5.0), &mut rng)
553 .expect("compound_poisson_process should succeed");
554 assert!(path.len() >= 2);
556 assert!((path[0].0).abs() < 1e-12, "starts at t0");
557 assert!((path[0].1).abs() < 1e-12, "starts at 0");
558 assert!(
559 (path.last().expect("path is non-empty").0 - 5.0).abs() < 1e-12,
560 "ends at t1"
561 );
562 }
563
564 #[test]
565 fn test_compound_poisson_mean_jumps() {
566 let lambda = 3.0;
568 let t_end = 2.0;
569 let n_paths = 500;
570 let mut total_jumps = 0usize;
571 for seed in 0..n_paths as u64 {
572 let mut rng = seeded_rng(seed + 1000);
573 let path = compound_poisson_process(
574 lambda,
575 |_r| 1.0, (0.0, t_end),
577 &mut rng,
578 )
579 .expect("compound_poisson_process should succeed");
580 total_jumps += path.len().saturating_sub(2);
582 }
583 let mean_jumps = total_jumps as f64 / n_paths as f64;
584 let expected = lambda * t_end;
585 assert!(
586 (mean_jumps - expected).abs() / expected < 0.15,
587 "mean jumps {mean_jumps:.3} vs expected {expected:.3}"
588 );
589 }
590
591 #[test]
592 fn test_compound_poisson_invalid_lambda() {
593 let mut rng = seeded_rng(0);
594 assert!(compound_poisson_process(0.0, |_| 1.0, (0.0, 1.0), &mut rng).is_err());
595 assert!(compound_poisson_process(-1.0, |_| 1.0, (0.0, 1.0), &mut rng).is_err());
596 }
597
598 #[test]
599 fn test_kou_positive_price() {
600 let mut rng = seeded_rng(42);
601 let path = kou_double_exponential(
602 0.05,
603 0.2,
604 1.0,
605 0.4,
606 5.0,
607 3.0,
608 100.0,
609 (0.0, 1.0),
610 0.01,
611 &mut rng,
612 )
613 .expect("kou_double_exponential should succeed");
614 assert!(
615 path.iter().all(|&(_, s)| s > 0.0),
616 "Kou S must stay positive"
617 );
618 }
619
620 #[test]
621 fn test_kou_invalid_params() {
622 let mut rng = seeded_rng(0);
623 assert!(kou_double_exponential(
625 0.05,
626 0.2,
627 1.0,
628 0.4,
629 0.5,
630 3.0,
631 100.0,
632 (0.0, 1.0),
633 0.01,
634 &mut rng
635 )
636 .is_err());
637 assert!(kou_double_exponential(
639 0.05,
640 0.2,
641 1.0,
642 0.0,
643 5.0,
644 3.0,
645 100.0,
646 (0.0, 1.0),
647 0.01,
648 &mut rng
649 )
650 .is_err());
651 }
652
653 #[test]
654 fn test_jump_diffusion_problem() {
655 let mut rng = seeded_rng(99);
656 let normal =
657 Normal::new(0.0_f64, 0.5).expect("Normal::new should succeed with valid params");
658 let prob = JumpDiffusionProblem::new(
659 |x, _t| 0.05 * x,
660 |x, _t| 0.2 * x,
661 1.0,
662 move |r| normal.sample(r),
663 100.0,
664 (0.0, 1.0),
665 )
666 .expect("JumpDiffusionProblem::new should succeed");
667 let path = prob
668 .solve(0.01, &mut rng)
669 .expect("prob.solve should succeed");
670 assert!(!path.is_empty());
671 assert!((path[0].0 - 0.0).abs() < 1e-12);
672 assert!((path[0].1 - 100.0).abs() < 1e-12);
673 }
674
675 #[test]
676 fn test_poisson_sample_zero_rate() {
677 let uniform =
678 Uniform::new(0.0_f64, 1.0).expect("Uniform::new should succeed with valid range");
679 let mut rng = seeded_rng(0);
680 let n = poisson_sample(0.0, &mut rng, &uniform).expect("poisson_sample should succeed");
681 assert_eq!(n, 0);
682 }
683
684 #[test]
685 fn test_validate_t_span() {
686 assert!(validate_t_span((0.0, 1.0)).is_ok());
687 assert!(validate_t_span((1.0, 0.0)).is_err());
688 assert!(validate_t_span((1.0, 1.0)).is_err());
689 }
690}