use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::random::prelude::*;
use scirs2_core::random::{Normal, Uniform};
use scirs2_core::Distribution;
pub fn merton_jump_diffusion(
mu: f64,
sigma: f64,
lambda: f64,
mu_j: f64,
sigma_j: f64,
s0: f64,
t_span: (f64, f64),
dt: f64,
rng: &mut StdRng,
) -> IntegrateResult<Vec<(f64, f64)>> {
if sigma <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"sigma must be > 0, got {sigma}"
)));
}
if lambda < 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"lambda must be >= 0, got {lambda}"
)));
}
if sigma_j < 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"sigma_j must be >= 0, got {sigma_j}"
)));
}
if s0 <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"s0 must be > 0, got {s0}"
)));
}
validate_t_span(t_span)?;
validate_dt(dt)?;
let k_bar = (mu_j + 0.5 * sigma_j * sigma_j).exp() - 1.0;
let mu_comp = mu - lambda * k_bar;
let normal = Normal::new(0.0, 1.0).map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
let uniform =
Uniform::new(0.0_f64, 1.0_f64).map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
let n_steps = ((t_span.1 - t_span.0) / dt).ceil() as usize;
let mut path = Vec::with_capacity(n_steps + 1);
path.push((t_span.0, s0));
let mut s = s0;
let mut t = t_span.0;
for _ in 0..n_steps {
let step = (t_span.1 - t).min(dt);
let sqrt_step = step.sqrt();
let z: f64 = normal.sample(rng);
let dw = z * sqrt_step;
let ds_cont = mu_comp * s * step + sigma * s * dw;
let n_jumps = poisson_sample(lambda * step, rng, &uniform)?;
let mut jump_factor = 1.0;
for _ in 0..n_jumps {
let z_j: f64 = normal.sample(rng);
let log_jump = mu_j + sigma_j * z_j;
jump_factor *= log_jump.exp(); }
s = ((s + ds_cont) * jump_factor).max(1e-10);
t += step;
path.push((t, s));
}
Ok(path)
}
pub fn compound_poisson_process<F>(
lambda: f64,
jump_dist: F,
t_span: (f64, f64),
rng: &mut StdRng,
) -> IntegrateResult<Vec<(f64, f64)>>
where
F: Fn(&mut StdRng) -> f64,
{
if lambda <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"lambda must be > 0, got {lambda}"
)));
}
validate_t_span(t_span)?;
let uniform =
Uniform::new(0.0_f64, 1.0_f64).map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
let mut path = Vec::new();
path.push((t_span.0, 0.0));
let mut x = 0.0_f64;
let mut t = t_span.0;
loop {
let u: f64 = uniform.sample(rng);
let inter = -u.ln() / lambda;
t += inter;
if t >= t_span.1 {
break;
}
let jump = jump_dist(rng);
x += jump;
path.push((t, x));
}
path.push((t_span.1, x));
Ok(path)
}
pub fn kou_double_exponential(
mu: f64,
sigma: f64,
lambda: f64,
p: f64,
eta1: f64,
eta2: f64,
s0: f64,
t_span: (f64, f64),
dt: f64,
rng: &mut StdRng,
) -> IntegrateResult<Vec<(f64, f64)>> {
if sigma <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"sigma must be > 0, got {sigma}"
)));
}
if lambda < 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"lambda must be >= 0, got {lambda}"
)));
}
if !(0.0 < p && p < 1.0) {
return Err(IntegrateError::InvalidInput(format!(
"p must be in (0,1), got {p}"
)));
}
if eta1 <= 1.0 {
return Err(IntegrateError::InvalidInput(format!(
"eta1 must be > 1, got {eta1}"
)));
}
if eta2 <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"eta2 must be > 0, got {eta2}"
)));
}
if s0 <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"s0 must be > 0, got {s0}"
)));
}
validate_t_span(t_span)?;
validate_dt(dt)?;
let k_bar = p * eta1 / (eta1 - 1.0) + (1.0 - p) * eta2 / (eta2 + 1.0) - 1.0;
let mu_comp = mu - lambda * k_bar;
let normal = Normal::new(0.0, 1.0).map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
let uniform =
Uniform::new(0.0_f64, 1.0_f64).map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
let n_steps = ((t_span.1 - t_span.0) / dt).ceil() as usize;
let mut path = Vec::with_capacity(n_steps + 1);
path.push((t_span.0, s0));
let mut s = s0;
let mut t = t_span.0;
for _ in 0..n_steps {
let step = (t_span.1 - t).min(dt);
let sqrt_step = step.sqrt();
let z: f64 = normal.sample(rng);
let dw = z * sqrt_step;
let ds_cont = mu_comp * s * step + sigma * s * dw;
let n_jumps = poisson_sample(lambda * step, rng, &uniform)?;
let mut log_jump_sum: f64 = 0.0;
for _ in 0..n_jumps {
let u_type: f64 = uniform.sample(rng);
let u_mag: f64 = uniform.sample(rng);
if u_type < p {
log_jump_sum += -u_mag.ln() / eta1;
} else {
log_jump_sum -= -u_mag.ln() / eta2;
}
}
let jump_mult = log_jump_sum.exp();
s = ((s + ds_cont) * jump_mult).max(1e-10);
t += step;
path.push((t, s));
}
Ok(path)
}
pub struct JumpDiffusionProblem<Drift, Diffusion, JumpSampler>
where
Drift: Fn(f64, f64) -> f64,
Diffusion: Fn(f64, f64) -> f64,
JumpSampler: Fn(&mut StdRng) -> f64,
{
pub drift: Drift,
pub diffusion: Diffusion,
pub jump_intensity: f64,
pub jump_sampler: JumpSampler,
pub x0: f64,
pub t_span: (f64, f64),
}
impl<Drift, Diffusion, JumpSampler> JumpDiffusionProblem<Drift, Diffusion, JumpSampler>
where
Drift: Fn(f64, f64) -> f64,
Diffusion: Fn(f64, f64) -> f64,
JumpSampler: Fn(&mut StdRng) -> f64,
{
pub fn new(
drift: Drift,
diffusion: Diffusion,
jump_intensity: f64,
jump_sampler: JumpSampler,
x0: f64,
t_span: (f64, f64),
) -> IntegrateResult<Self> {
if jump_intensity < 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"jump_intensity must be >= 0, got {jump_intensity}"
)));
}
validate_t_span(t_span)?;
Ok(Self {
drift,
diffusion,
jump_intensity,
jump_sampler,
x0,
t_span,
})
}
pub fn solve(&self, dt: f64, rng: &mut StdRng) -> IntegrateResult<Vec<(f64, f64)>> {
validate_dt(dt)?;
let normal =
Normal::new(0.0, 1.0).map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
let uniform = Uniform::new(0.0_f64, 1.0_f64)
.map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
let n_steps = ((self.t_span.1 - self.t_span.0) / dt).ceil() as usize;
let mut path = Vec::with_capacity(n_steps + 1);
path.push((self.t_span.0, self.x0));
let mut x = self.x0;
let mut t = self.t_span.0;
for _ in 0..n_steps {
let step = (self.t_span.1 - t).min(dt);
let sqrt_step = step.sqrt();
let z: f64 = normal.sample(rng);
let fx = (self.drift)(x, t);
let gx = (self.diffusion)(x, t);
let n_jumps = poisson_sample(self.jump_intensity * step, rng, &uniform)?;
let jump_sum: f64 = (0..n_jumps).map(|_| (self.jump_sampler)(rng)).sum();
x = x + fx * step + gx * z * sqrt_step + jump_sum;
t += step;
path.push((t, x));
}
Ok(path)
}
}
fn poisson_sample(lambda: f64, rng: &mut StdRng, uniform: &Uniform<f64>) -> IntegrateResult<usize> {
if lambda < 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"Poisson lambda must be >= 0, got {lambda}"
)));
}
if lambda == 0.0 {
return Ok(0);
}
if lambda > 30.0 {
let normal = Normal::new(lambda, lambda.sqrt())
.map_err(|e| IntegrateError::InvalidInput(e.to_string()))?;
let sample: f64 = normal.sample(rng);
return Ok(sample.round().max(0.0) as usize);
}
let threshold = (-lambda).exp();
let mut count = 0usize;
let mut product = uniform.sample(rng);
while product > threshold {
product *= uniform.sample(rng);
count += 1;
}
Ok(count)
}
fn validate_t_span(t_span: (f64, f64)) -> IntegrateResult<()> {
if t_span.0 >= t_span.1 {
Err(IntegrateError::InvalidInput(format!(
"t_span must satisfy t0 < t1, got {:?}",
t_span
)))
} else {
Ok(())
}
}
fn validate_dt(dt: f64) -> IntegrateResult<()> {
if dt <= 0.0 {
Err(IntegrateError::InvalidInput(format!(
"dt must be > 0, got {dt}"
)))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::random::prelude::seeded_rng;
#[test]
fn test_merton_positive_price() {
let mut rng = seeded_rng(42);
let path =
merton_jump_diffusion(0.05, 0.2, 1.0, -0.1, 0.2, 100.0, (0.0, 1.0), 0.01, &mut rng)
.expect("merton_jump_diffusion should succeed");
assert!(!path.is_empty());
assert!(
path.iter().all(|&(_, s)| s > 0.0),
"Merton S must stay positive"
);
}
#[test]
fn test_merton_no_jumps() {
let mut rng = seeded_rng(1);
let path =
merton_jump_diffusion(0.05, 0.2, 0.0, 0.0, 0.0, 100.0, (0.0, 1.0), 0.01, &mut rng)
.expect("merton_jump_diffusion should succeed");
assert!(path.iter().all(|&(_, s)| s > 0.0));
}
#[test]
fn test_merton_invalid_sigma() {
let mut rng = seeded_rng(0);
assert!(merton_jump_diffusion(
0.05,
-0.2,
1.0,
0.0,
0.1,
100.0,
(0.0, 1.0),
0.01,
&mut rng
)
.is_err());
}
#[test]
fn test_merton_invalid_s0() {
let mut rng = seeded_rng(0);
assert!(
merton_jump_diffusion(0.05, 0.2, 1.0, 0.0, 0.1, -1.0, (0.0, 1.0), 0.01, &mut rng)
.is_err()
);
}
#[test]
fn test_compound_poisson_basic() {
let mut rng = seeded_rng(7);
let normal =
Normal::new(0.0_f64, 1.0).expect("Normal::new should succeed with valid params");
let path = compound_poisson_process(2.0, |r| normal.sample(r), (0.0, 5.0), &mut rng)
.expect("compound_poisson_process should succeed");
assert!(path.len() >= 2);
assert!((path[0].0).abs() < 1e-12, "starts at t0");
assert!((path[0].1).abs() < 1e-12, "starts at 0");
assert!(
(path.last().expect("path is non-empty").0 - 5.0).abs() < 1e-12,
"ends at t1"
);
}
#[test]
fn test_compound_poisson_mean_jumps() {
let lambda = 3.0;
let t_end = 2.0;
let n_paths = 500;
let mut total_jumps = 0usize;
for seed in 0..n_paths as u64 {
let mut rng = seeded_rng(seed + 1000);
let path = compound_poisson_process(
lambda,
|_r| 1.0, (0.0, t_end),
&mut rng,
)
.expect("compound_poisson_process should succeed");
total_jumps += path.len().saturating_sub(2);
}
let mean_jumps = total_jumps as f64 / n_paths as f64;
let expected = lambda * t_end;
assert!(
(mean_jumps - expected).abs() / expected < 0.15,
"mean jumps {mean_jumps:.3} vs expected {expected:.3}"
);
}
#[test]
fn test_compound_poisson_invalid_lambda() {
let mut rng = seeded_rng(0);
assert!(compound_poisson_process(0.0, |_| 1.0, (0.0, 1.0), &mut rng).is_err());
assert!(compound_poisson_process(-1.0, |_| 1.0, (0.0, 1.0), &mut rng).is_err());
}
#[test]
fn test_kou_positive_price() {
let mut rng = seeded_rng(42);
let path = kou_double_exponential(
0.05,
0.2,
1.0,
0.4,
5.0,
3.0,
100.0,
(0.0, 1.0),
0.01,
&mut rng,
)
.expect("kou_double_exponential should succeed");
assert!(
path.iter().all(|&(_, s)| s > 0.0),
"Kou S must stay positive"
);
}
#[test]
fn test_kou_invalid_params() {
let mut rng = seeded_rng(0);
assert!(kou_double_exponential(
0.05,
0.2,
1.0,
0.4,
0.5,
3.0,
100.0,
(0.0, 1.0),
0.01,
&mut rng
)
.is_err());
assert!(kou_double_exponential(
0.05,
0.2,
1.0,
0.0,
5.0,
3.0,
100.0,
(0.0, 1.0),
0.01,
&mut rng
)
.is_err());
}
#[test]
fn test_jump_diffusion_problem() {
let mut rng = seeded_rng(99);
let normal =
Normal::new(0.0_f64, 0.5).expect("Normal::new should succeed with valid params");
let prob = JumpDiffusionProblem::new(
|x, _t| 0.05 * x,
|x, _t| 0.2 * x,
1.0,
move |r| normal.sample(r),
100.0,
(0.0, 1.0),
)
.expect("JumpDiffusionProblem::new should succeed");
let path = prob
.solve(0.01, &mut rng)
.expect("prob.solve should succeed");
assert!(!path.is_empty());
assert!((path[0].0 - 0.0).abs() < 1e-12);
assert!((path[0].1 - 100.0).abs() < 1e-12);
}
#[test]
fn test_poisson_sample_zero_rate() {
let uniform =
Uniform::new(0.0_f64, 1.0).expect("Uniform::new should succeed with valid range");
let mut rng = seeded_rng(0);
let n = poisson_sample(0.0, &mut rng, &uniform).expect("poisson_sample should succeed");
assert_eq!(n, 0);
}
#[test]
fn test_validate_t_span() {
assert!(validate_t_span((0.0, 1.0)).is_ok());
assert!(validate_t_span((1.0, 0.0)).is_err());
assert!(validate_t_span((1.0, 1.0)).is_err());
}
}