use crate::error::{IntegrateError, IntegrateResult};
use crate::sde::{SdeSolution};
use crate::sde::euler_maruyama::euler_maruyama;
use crate::sde::milstein::scalar_milstein;
use crate::sde::runge_kutta_sde::platen_scheme;
use crate::sde::SdeProblem;
use scirs2_core::ndarray::{array, Array1, Array2};
use scirs2_core::random::prelude::StdRng;
pub fn geometric_brownian_motion(
mu: f64,
sigma: f64,
s0: f64,
t_span: [f64; 2],
dt: f64,
rng: &mut StdRng,
) -> IntegrateResult<SdeSolution> {
if sigma <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"GBM volatility sigma must be > 0, got {}",
sigma
)));
}
if s0 <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"GBM initial value s0 must be > 0, got {}",
s0
)));
}
scalar_milstein(
move |_t, x| mu * x,
move |_t, x| sigma * x,
s0,
t_span,
dt,
rng,
)
}
pub fn ornstein_uhlenbeck(
theta: f64,
mu: f64,
sigma: f64,
x0: f64,
t_span: [f64; 2],
dt: f64,
rng: &mut StdRng,
) -> IntegrateResult<SdeSolution> {
if theta <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"OU theta (mean reversion speed) must be > 0, got {}",
theta
)));
}
if sigma <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"OU sigma must be > 0, got {}",
sigma
)));
}
scalar_milstein(
move |_t, x| theta * (mu - x),
move |_t, _x| sigma,
x0,
t_span,
dt,
rng,
)
}
pub fn cir_process(
kappa: f64,
theta: f64,
sigma: f64,
r0: f64,
t_span: [f64; 2],
dt: f64,
rng: &mut StdRng,
) -> IntegrateResult<SdeSolution> {
if kappa <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"CIR kappa must be > 0, got {}",
kappa
)));
}
if theta < 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"CIR theta must be >= 0, got {}",
theta
)));
}
if sigma <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"CIR sigma must be > 0, got {}",
sigma
)));
}
if r0 < 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"CIR initial value r0 must be >= 0, got {}",
r0
)));
}
let prob = SdeProblem::new(
array![r0],
t_span,
1,
move |_t, x| array![kappa * (theta - x[0])],
move |_t, x| {
let mut g = Array2::zeros((1, 1));
g[[0, 0]] = sigma * x[0].max(0.0).sqrt();
g
},
);
let sol_raw = platen_scheme(&prob, dt, rng)?;
let reflected: Vec<Array1<f64>> = sol_raw.x.into_iter().map(|mut xi| {
xi[0] = xi[0].max(0.0);
xi
}).collect();
Ok(SdeSolution {
t: sol_raw.t,
x: reflected,
})
}
pub fn arithmetic_brownian_motion(
mu: f64,
sigma: f64,
x0: f64,
t_span: [f64; 2],
dt: f64,
rng: &mut StdRng,
) -> IntegrateResult<SdeSolution> {
if sigma <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"ABM sigma must be > 0, got {}",
sigma
)));
}
scalar_milstein(
move |_t, _x| mu,
move |_t, _x| sigma,
x0,
t_span,
dt,
rng,
)
}
pub fn vasicek(
a: f64,
b: f64,
sigma: f64,
r0: f64,
t_span: [f64; 2],
dt: f64,
rng: &mut StdRng,
) -> IntegrateResult<SdeSolution> {
if b <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"Vasicek b (mean reversion speed) must be > 0, got {}",
b
)));
}
if sigma <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"Vasicek sigma must be > 0, got {}",
sigma
)));
}
scalar_milstein(
move |_t, x| a - b * x,
move |_t, _x| sigma,
r0,
t_span,
dt,
rng,
)
}
#[allow(clippy::too_many_arguments)]
pub fn heston(
mu: f64,
kappa: f64,
theta: f64,
sigma_v: f64,
rho: f64,
s0: f64,
v0: f64,
t_span: [f64; 2],
dt: f64,
rng: &mut StdRng,
) -> IntegrateResult<SdeSolution> {
if s0 <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"Heston s0 must be > 0, got {}",
s0
)));
}
if v0 < 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"Heston v0 must be >= 0, got {}",
v0
)));
}
if rho.abs() > 1.0 {
return Err(IntegrateError::InvalidInput(format!(
"Heston rho must be in [-1, 1], got {}",
rho
)));
}
if sigma_v <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"Heston sigma_v must be > 0, got {}",
sigma_v
)));
}
if kappa <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"Heston kappa must be > 0, got {}",
kappa
)));
}
let rho_perp = (1.0 - rho * rho).sqrt();
let prob = SdeProblem::new(
array![s0, v0],
t_span,
2,
move |_t, x| {
let s = x[0];
let v = x[1].max(0.0);
array![mu * s, kappa * (theta - v)]
},
move |_t, x| {
let s = x[0].max(0.0);
let v = x[1].max(0.0);
let sqrt_v = v.sqrt();
let mut g = Array2::zeros((2, 2));
g[[0, 0]] = sqrt_v * s;
g[[1, 0]] = sigma_v * sqrt_v * rho;
g[[0, 1]] = 0.0;
g[[1, 1]] = sigma_v * sqrt_v * rho_perp;
g
},
);
let sol_raw = euler_maruyama(&prob, dt, rng)?;
let reflected: Vec<Array1<f64>> = sol_raw.x.into_iter().map(|mut xi| {
xi[1] = xi[1].max(0.0);
xi
}).collect();
Ok(SdeSolution {
t: sol_raw.t,
x: reflected,
})
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::random::prelude::seeded_rng;
#[test]
fn test_gbm_positive() {
let mut rng = seeded_rng(42);
let sol = geometric_brownian_motion(0.05, 0.2, 100.0, [0.0, 1.0], 0.01, &mut rng).expect("geometric_brownian_motion should succeed");
for xi in &sol.x {
assert!(xi[0] > 0.0, "GBM must stay positive");
}
}
#[test]
fn test_gbm_weak_mean() {
let mu = 0.1_f64;
let sigma = 0.2_f64;
let s0 = 1.0_f64;
let t1 = 1.0_f64;
let analytic = s0 * (mu * t1).exp();
let n_paths = 500;
let dt = 0.005;
let mut sum = 0.0;
for seed in 0..n_paths as u64 {
let mut rng = seeded_rng(seed + 100);
let sol = geometric_brownian_motion(mu, sigma, s0, [0.0, t1], dt, &mut rng).expect("geometric_brownian_motion should succeed");
sum += sol.x_final().expect("solution has state")[0];
}
let mean = sum / n_paths as f64;
assert!(
(mean - analytic).abs() / analytic < 0.05,
"GBM mean {:.4} vs analytic {:.4}",
mean,
analytic
);
}
#[test]
fn test_ou_mean_reversion() {
let theta = 2.0_f64;
let mu_ou = 1.0_f64;
let sigma = 0.3_f64;
let x0 = 0.0_f64;
let t1 = 3.0_f64;
let analytic_mean = x0 * (-theta * t1).exp() + mu_ou * (1.0 - (-theta * t1).exp());
let n_paths = 400;
let dt = 0.01;
let mut sum = 0.0;
for seed in 0..n_paths as u64 {
let mut rng = seeded_rng(seed + 200);
let sol = ornstein_uhlenbeck(theta, mu_ou, sigma, x0, [0.0, t1], dt, &mut rng).expect("ornstein_uhlenbeck should succeed");
sum += sol.x_final().expect("solution has state")[0];
}
let mean = sum / n_paths as f64;
assert!(
(mean - analytic_mean).abs() < 0.1,
"OU mean {:.4} vs analytic {:.4}",
mean,
analytic_mean
);
}
#[test]
fn test_cir_non_negative() {
let mut rng = seeded_rng(7);
let sol = cir_process(1.0, 1.0, 0.3, 1.0, [0.0, 5.0], 0.005, &mut rng).expect("cir_process should succeed");
for xi in &sol.x {
assert!(xi[0] >= 0.0, "CIR must be non-negative, got {}", xi[0]);
}
}
#[test]
fn test_cir_mean_reversion() {
let kappa = 1.0_f64;
let theta = 0.5_f64;
let sigma = 0.2_f64;
let r0 = 0.1_f64;
let t1 = 5.0_f64;
let analytic_mean = r0 * (-kappa * t1).exp() + theta * (1.0 - (-kappa * t1).exp());
let n_paths = 400;
let dt = 0.01;
let mut sum = 0.0;
for seed in 0..n_paths as u64 {
let mut rng = seeded_rng(seed + 300);
let sol = cir_process(kappa, theta, sigma, r0, [0.0, t1], dt, &mut rng).expect("cir_process should succeed");
sum += sol.x_final().expect("solution has state")[0];
}
let mean = sum / n_paths as f64;
assert!(
(mean - analytic_mean).abs() < 0.1,
"CIR mean {:.4} vs analytic {:.4}",
mean,
analytic_mean
);
}
#[test]
fn test_abm_mean() {
let mu = 0.5_f64;
let sigma = 1.0_f64;
let x0 = 0.0_f64;
let t1 = 2.0_f64;
let analytic_mean = x0 + mu * t1;
let n_paths = 400;
let dt = 0.01;
let mut sum = 0.0;
for seed in 0..n_paths as u64 {
let mut rng = seeded_rng(seed + 400);
let sol = arithmetic_brownian_motion(mu, sigma, x0, [0.0, t1], dt, &mut rng).expect("arithmetic_brownian_motion should succeed");
sum += sol.x_final().expect("solution has state")[0];
}
let mean = sum / n_paths as f64;
assert!(
(mean - analytic_mean).abs() < 0.2,
"ABM mean {:.4} vs analytic {:.4}",
mean,
analytic_mean
);
}
#[test]
fn test_vasicek_basic() {
let mut rng = seeded_rng(5);
let sol = vasicek(0.1, 1.0, 0.05, 0.05, [0.0, 10.0], 0.01, &mut rng).expect("vasicek should succeed");
assert!(!sol.is_empty());
}
#[test]
fn test_heston_non_negative_variance() {
let mut rng = seeded_rng(42);
let sol = heston(
0.05, 2.0, 0.04, 0.2, -0.7,
100.0, 0.04,
[0.0, 1.0], 0.005, &mut rng,
).expect("heston should succeed");
for xi in &sol.x {
assert!(xi[0] > 0.0, "Heston S must be positive");
assert!(xi[1] >= 0.0, "Heston v must be non-negative");
}
}
#[test]
fn test_gbm_invalid_sigma() {
let mut rng = seeded_rng(0);
assert!(geometric_brownian_motion(-0.05, -0.2, 100.0, [0.0, 1.0], 0.01, &mut rng).is_err());
}
#[test]
fn test_gbm_invalid_s0() {
let mut rng = seeded_rng(0);
assert!(geometric_brownian_motion(0.05, 0.2, -1.0, [0.0, 1.0], 0.01, &mut rng).is_err());
}
#[test]
fn test_cir_invalid_params() {
let mut rng = seeded_rng(0);
assert!(cir_process(-1.0, 0.5, 0.3, 1.0, [0.0, 1.0], 0.01, &mut rng).is_err());
}
}