From 68349eb0ea496100c95789f7401a22a819d507a4 Mon Sep 17 00:00:00 2001 From: Daniel Boros Date: Mon, 22 Jul 2024 00:20:31 +0200 Subject: [PATCH 01/11] feat: rework api and improve performance --- .vscode/settings.json | 1 + .zed/settings.json | 15 +++++ Cargo.toml | 3 + src/lib.rs | 2 + src/stochastic.rs | 4 ++ src/stochastic/diffusions.rs | 1 + src/stochastic/diffusions/ou.rs | 35 ++++++++++ src/stochastic/traits.rs | 111 ++++++++++++++++++++++++++++++++ src/stochastic/types.rs | 1 + 9 files changed, 173 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 .zed/settings.json create mode 100644 src/stochastic.rs create mode 100644 src/stochastic/diffusions.rs create mode 100644 src/stochastic/diffusions/ou.rs create mode 100644 src/stochastic/traits.rs create mode 100644 src/stochastic/types.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ff116c2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{ "rust-analyzer.cargo.features": ["f32"] } diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..f65869a --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,15 @@ +// Folder-specific settings +// +// For a full list of overridable settings, and general information on folder-specific settings, +// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings +{ + "lsp": { + "rust-analyzer": { + "initialization_options": { + "cargo": { + "features": ["f32"] + } + } + } + } +} diff --git a/Cargo.toml b/Cargo.toml index d16deee..9161df0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,6 @@ ndrustfft = "0.4.2" name = "stochastic_rs" crate-type = ["cdylib", "lib"] path = "src/lib.rs" + +[features] +f32 = [] diff --git a/src/lib.rs b/src/lib.rs index 36b3c04..d69e71a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,8 @@ pub mod prelude; +pub mod stochastic; + pub mod diffusions; pub mod jumps; pub mod models; diff --git a/src/stochastic.rs b/src/stochastic.rs new file mode 100644 index 0000000..e4cbad9 --- /dev/null +++ b/src/stochastic.rs @@ -0,0 +1,4 @@ +pub mod diffusions; + +pub mod traits; +pub mod types; diff --git a/src/stochastic/diffusions.rs b/src/stochastic/diffusions.rs new file mode 100644 index 0000000..fe0a0f7 --- /dev/null +++ b/src/stochastic/diffusions.rs @@ -0,0 +1 @@ +pub mod ou; diff --git a/src/stochastic/diffusions/ou.rs b/src/stochastic/diffusions/ou.rs new file mode 100644 index 0000000..0e69b49 --- /dev/null +++ b/src/stochastic/diffusions/ou.rs @@ -0,0 +1,35 @@ +use crate::stochastic::traits::{Process, ProcessF32}; + +pub struct OU { + pub theta: f64, + pub mu: f64, + pub sigma: f64, +} + +impl Process for OU { + fn drift(&self, x: f64, _t: f64) -> f64 { + self.theta * (self.mu - x) + } + + fn diffusion(&self, _x: f64, _t: f64) -> f64 { + self.sigma + } +} + +#[cfg(feature = "f32")] +pub struct OUF32 { + pub theta: f32, + pub mu: f32, + pub sigma: f32, +} + +#[cfg(feature = "f32")] +impl ProcessF32 for OUF32 { + fn drift(&self, x: f32, _t: f32) -> f32 { + self.theta * (self.mu - x) + } + + fn diffusion(&self, _x: f32, _t: f32) -> f32 { + self.sigma + } +} diff --git a/src/stochastic/traits.rs b/src/stochastic/traits.rs new file mode 100644 index 0000000..10c60cf --- /dev/null +++ b/src/stochastic/traits.rs @@ -0,0 +1,111 @@ +use ndarray::{Array1, Array2}; +use ndarray_rand::rand_distr::Normal; +use ndarray_rand::RandomExt; +use rayon::prelude::*; + +pub trait Process: Send + Sync { + fn drift(&self, x: f64, t: f64) -> f64; + + fn diffusion(&self, x: f64, t: f64) -> f64; + + fn sample(&self, n: usize, x_0: f64, t_0: f64, t: f64) -> Array1 { + let dt = (t - t_0) / n as f64; + let mut x = Array1::zeros(n); + x[0] = x_0; + let noise = Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()); + let times = Array1::linspace(t_0, t, n); + + noise.into_iter().enumerate().for_each(|(idx, dw)| { + x[idx + 1] = + x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; + }); + + x + } + + fn sample_as_vec(&self, n: usize, x_0: f64, t_0: f64, t: f64) -> Vec { + self.sample(n, x_0, t_0, t).to_vec() + } + + fn sample_parallel(&self, m: usize, n: usize, x_0: f64, t_0: f64, t: f64) -> Array2> { + let mut xs = Array2::>::default((m, n)); + + xs.par_iter_mut().for_each(|x| { + *x = self.sample(n, x_0, t_0, t); + }); + + xs + } + + fn sample_parallel_as_vec( + &self, + m: usize, + n: usize, + x_0: f64, + t_0: f64, + t: f64, + ) -> Vec> { + self + .sample_parallel(m, n, x_0, t_0, t) + .into_par_iter() + .map(|x| x.to_vec()) + .collect() + } +} + +#[cfg(feature = "f32")] +pub trait ProcessF32: Send + Sync { + #[cfg(feature = "f32")] + fn drift(&self, x: f32, t: f32) -> f32; + + #[cfg(feature = "f32")] + fn diffusion(&self, x: f32, t: f32) -> f32; + + #[cfg(feature = "f32")] + fn sample(&self, n: usize, x_0: f32, t_0: f32, t: f32) -> Array1 { + let dt = (t - t_0) / n as f32; + let mut x = Array1::zeros(n); + x[0] = x_0; + let noise = Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()); + let times = Array1::linspace(t_0, t, n); + + noise.into_iter().enumerate().for_each(|(idx, dw)| { + x[idx + 1] = + x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; + }); + + x + } + + #[cfg(feature = "f32")] + fn sample_as_vec(&self, n: usize, x_0: f32, t_0: f32, t: f32) -> Vec { + self.sample(n, x_0, t_0, t).to_vec() + } + + #[cfg(feature = "f32")] + fn sample_parallel(&self, m: usize, n: usize, x_0: f32, t_0: f32, t: f32) -> Array2> { + let mut xs = Array2::>::default((m, n)); + + xs.par_iter_mut().for_each(|x| { + *x = self.sample(n, x_0, t_0, t); + }); + + xs + } + + #[cfg(feature = "f32")] + fn sample_parallel_as_vec( + &self, + m: usize, + n: usize, + x_0: f32, + t_0: f32, + t: f32, + ) -> Vec> { + self + .sample_parallel(m, n, x_0, t_0, t) + .into_par_iter() + .map(|x| x.to_vec()) + .collect() + } +} diff --git a/src/stochastic/types.rs b/src/stochastic/types.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/stochastic/types.rs @@ -0,0 +1 @@ + From c911300858ea9148de0e0729a8cba0bd145646dd Mon Sep 17 00:00:00 2001 From: Daniel Boros Date: Mon, 22 Jul 2024 00:43:26 +0200 Subject: [PATCH 02/11] feat: add hust param --- src/models/duffie_kan.rs | 2 +- src/stochastic/diffusions/ou.rs | 5 ++- src/stochastic/traits.rs | 56 ++++++++++++++++++++++++--------- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/models/duffie_kan.rs b/src/models/duffie_kan.rs index 2744b63..77c79b0 100644 --- a/src/models/duffie_kan.rs +++ b/src/models/duffie_kan.rs @@ -35,7 +35,7 @@ use crate::processes::correlated::correlated_bms; /// ``` /// let (r_path, x_path) = duffie_kan(0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1000, Some(0.05), Some(0.02), Some(1.0)); /// ``` -#[allow(clippy::many_single_char_names)] +#[allow(clippy::too_many_arguments)] pub fn duffie_kan( alpha: f64, beta: f64, diff --git a/src/stochastic/diffusions/ou.rs b/src/stochastic/diffusions/ou.rs index 0e69b49..90b1e6d 100644 --- a/src/stochastic/diffusions/ou.rs +++ b/src/stochastic/diffusions/ou.rs @@ -1,4 +1,7 @@ -use crate::stochastic::traits::{Process, ProcessF32}; +use crate::stochastic::traits::Process; + +#[cfg(feature = "f32")] +use crate::stochastic::traits::ProcessF32; pub struct OU { pub theta: f64, diff --git a/src/stochastic/traits.rs b/src/stochastic/traits.rs index 10c60cf..6eda19f 100644 --- a/src/stochastic/traits.rs +++ b/src/stochastic/traits.rs @@ -8,11 +8,16 @@ pub trait Process: Send + Sync { fn diffusion(&self, x: f64, t: f64) -> f64; - fn sample(&self, n: usize, x_0: f64, t_0: f64, t: f64) -> Array1 { + fn sample(&self, n: usize, x_0: f64, t_0: f64, t: f64, hurst: Option) -> Array1 { let dt = (t - t_0) / n as f64; let mut x = Array1::zeros(n); x[0] = x_0; - let noise = Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()); + let noise = if hurst.is_some() && hurst.unwrap() != 0.5 { + // TODO: add fractional Gaussian noise + Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()) + } else { + Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()) + }; let times = Array1::linspace(t_0, t, n); noise.into_iter().enumerate().for_each(|(idx, dw)| { @@ -23,15 +28,23 @@ pub trait Process: Send + Sync { x } - fn sample_as_vec(&self, n: usize, x_0: f64, t_0: f64, t: f64) -> Vec { - self.sample(n, x_0, t_0, t).to_vec() + fn sample_as_vec(&self, n: usize, x_0: f64, t_0: f64, t: f64, hurst: Option) -> Vec { + self.sample(n, x_0, t_0, t, hurst).to_vec() } - fn sample_parallel(&self, m: usize, n: usize, x_0: f64, t_0: f64, t: f64) -> Array2> { + fn sample_parallel( + &self, + m: usize, + n: usize, + x_0: f64, + t_0: f64, + t: f64, + hurst: Option, + ) -> Array2> { let mut xs = Array2::>::default((m, n)); xs.par_iter_mut().for_each(|x| { - *x = self.sample(n, x_0, t_0, t); + *x = self.sample(n, x_0, t_0, t, hurst); }); xs @@ -44,9 +57,10 @@ pub trait Process: Send + Sync { x_0: f64, t_0: f64, t: f64, + hurst: Option, ) -> Vec> { self - .sample_parallel(m, n, x_0, t_0, t) + .sample_parallel(m, n, x_0, t_0, t, hurst) .into_par_iter() .map(|x| x.to_vec()) .collect() @@ -62,11 +76,16 @@ pub trait ProcessF32: Send + Sync { fn diffusion(&self, x: f32, t: f32) -> f32; #[cfg(feature = "f32")] - fn sample(&self, n: usize, x_0: f32, t_0: f32, t: f32) -> Array1 { + fn sample(&self, n: usize, x_0: f32, t_0: f32, t: f32, hurst: Option) -> Array1 { let dt = (t - t_0) / n as f32; let mut x = Array1::zeros(n); x[0] = x_0; - let noise = Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()); + let noise = if hurst.is_some() && hurst.unwrap() != 0.5 { + // TODO: add fractional Gaussian noise + Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()) + } else { + Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()) + }; let times = Array1::linspace(t_0, t, n); noise.into_iter().enumerate().for_each(|(idx, dw)| { @@ -78,16 +97,24 @@ pub trait ProcessF32: Send + Sync { } #[cfg(feature = "f32")] - fn sample_as_vec(&self, n: usize, x_0: f32, t_0: f32, t: f32) -> Vec { - self.sample(n, x_0, t_0, t).to_vec() + fn sample_as_vec(&self, n: usize, x_0: f32, t_0: f32, t: f32, hurst: Option) -> Vec { + self.sample(n, x_0, t_0, t, hurst).to_vec() } #[cfg(feature = "f32")] - fn sample_parallel(&self, m: usize, n: usize, x_0: f32, t_0: f32, t: f32) -> Array2> { + fn sample_parallel( + &self, + m: usize, + n: usize, + x_0: f32, + t_0: f32, + t: f32, + hurst: Option, + ) -> Array2> { let mut xs = Array2::>::default((m, n)); xs.par_iter_mut().for_each(|x| { - *x = self.sample(n, x_0, t_0, t); + *x = self.sample(n, x_0, t_0, t, hurst); }); xs @@ -101,9 +128,10 @@ pub trait ProcessF32: Send + Sync { x_0: f32, t_0: f32, t: f32, + hurst: Option, ) -> Vec> { self - .sample_parallel(m, n, x_0, t_0, t) + .sample_parallel(m, n, x_0, t_0, t, hurst) .into_par_iter() .map(|x| x.to_vec()) .collect() From 512c1d007190149741717e4118f3d23aa7d2f392 Mon Sep 17 00:00:00 2001 From: Daniel Boros Date: Mon, 22 Jul 2024 19:49:37 +0200 Subject: [PATCH 03/11] feat: improve array methods --- Cargo.toml | 6 +++- rustfmt.toml | 2 +- src/stochastic/diffusions/ou.rs | 24 ++++++++++--- src/stochastic/traits.rs | 63 +++++++++++++++++++++------------ 4 files changed, 65 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9161df0..23f5108 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,9 @@ +cargo-features = ["edition2024"] + [package] name = "stochastic-rs" version = "0.5.2" -edition = "2021" +edition = "2024" license = "MIT" description = "A Rust library for stochastic processes" homepage = "https://github.com/dancixx/stochastic-rs" @@ -10,6 +12,7 @@ repository = "https://github.com/dancixx/stochastic-rs" readme = "README.md" keywords = ["stochastic", "process", "random", "simulation", "monte-carlo"] + [dependencies] linreg = "0.2.0" ndarray = { version = "0.15.6", features = [ @@ -24,6 +27,7 @@ indicatif = "0.17.7" plotly = "0.8.4" ndarray-rand = "0.14.0" ndrustfft = "0.4.2" +# nalgebra = { version = "0.33.0", default-features = true, features = ["rand"] } [dev-dependencies] diff --git a/rustfmt.toml b/rustfmt.toml index 6f2e075..b196eaa 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1 @@ -tab_spaces = 2 \ No newline at end of file +tab_spaces = 2 diff --git a/src/stochastic/diffusions/ou.rs b/src/stochastic/diffusions/ou.rs index 90b1e6d..80a2b1c 100644 --- a/src/stochastic/diffusions/ou.rs +++ b/src/stochastic/diffusions/ou.rs @@ -1,15 +1,20 @@ use crate::stochastic::traits::Process; -#[cfg(feature = "f32")] -use crate::stochastic::traits::ProcessF32; - pub struct OU { pub theta: f64, pub mu: f64, pub sigma: f64, } -impl Process for OU { +impl OU { + #[must_use] + #[inline(always)] + pub fn new(theta: f64, mu: f64, sigma: f64) -> Self { + Self { theta, mu, sigma } + } +} + +impl Process for OU { fn drift(&self, x: f64, _t: f64) -> f64 { self.theta * (self.mu - x) } @@ -27,7 +32,16 @@ pub struct OUF32 { } #[cfg(feature = "f32")] -impl ProcessF32 for OUF32 { +impl OUF32 { + #[must_use] + #[inline(always)] + pub fn new(theta: f32, mu: f32, sigma: f32) -> Self { + Self { theta, mu, sigma } + } +} + +#[cfg(feature = "f32")] +impl Process for OUF32 { fn drift(&self, x: f32, _t: f32) -> f32 { self.theta * (self.mu - x) } diff --git a/src/stochastic/traits.rs b/src/stochastic/traits.rs index 6eda19f..a864fd6 100644 --- a/src/stochastic/traits.rs +++ b/src/stochastic/traits.rs @@ -1,13 +1,37 @@ -use ndarray::{Array1, Array2}; +use ndarray::{Array1, Array2, Axis}; use ndarray_rand::rand_distr::Normal; use ndarray_rand::RandomExt; use rayon::prelude::*; -pub trait Process: Send + Sync { - fn drift(&self, x: f64, t: f64) -> f64; +pub trait Process: Send + Sync { + fn drift(&self, x: T, t: T) -> T; + fn diffusion(&self, x: T, t: T) -> T; +} - fn diffusion(&self, x: f64, t: f64) -> f64; +pub trait Sampling { + fn sample(&self, n: usize, x_0: T, t_0: T, t: T, hurst: Option) -> Array1; + fn sample_as_vec(&self, n: usize, x_0: T, t_0: T, t: T, hurst: Option) -> Vec; + fn sample_parallel( + &self, + m: usize, + n: usize, + x_0: T, + t_0: T, + t: T, + hurst: Option, + ) -> Array2; + fn sample_parallel_as_vec( + &self, + m: usize, + n: usize, + x_0: T, + t_0: T, + t: T, + hurst: Option, + ) -> Vec>; +} +impl> Sampling for T { fn sample(&self, n: usize, x_0: f64, t_0: f64, t: f64, hurst: Option) -> Array1 { let dt = (t - t_0) / n as f64; let mut x = Array1::zeros(n); @@ -20,6 +44,7 @@ pub trait Process: Send + Sync { }; let times = Array1::linspace(t_0, t, n); + // TODO: test idx noise.into_iter().enumerate().for_each(|(idx, dw)| { x[idx + 1] = x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; @@ -40,11 +65,11 @@ pub trait Process: Send + Sync { t_0: f64, t: f64, hurst: Option, - ) -> Array2> { - let mut xs = Array2::>::default((m, n)); + ) -> Array2 { + let mut xs = Array2::::zeros((m, n)); - xs.par_iter_mut().for_each(|x| { - *x = self.sample(n, x_0, t_0, t, hurst); + xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { + x.assign(&self.sample(n, x_0, t_0, t, hurst)); }); xs @@ -61,6 +86,7 @@ pub trait Process: Send + Sync { ) -> Vec> { self .sample_parallel(m, n, x_0, t_0, t, hurst) + .axis_iter(Axis(0)) .into_par_iter() .map(|x| x.to_vec()) .collect() @@ -68,14 +94,7 @@ pub trait Process: Send + Sync { } #[cfg(feature = "f32")] -pub trait ProcessF32: Send + Sync { - #[cfg(feature = "f32")] - fn drift(&self, x: f32, t: f32) -> f32; - - #[cfg(feature = "f32")] - fn diffusion(&self, x: f32, t: f32) -> f32; - - #[cfg(feature = "f32")] +impl> Sampling for T { fn sample(&self, n: usize, x_0: f32, t_0: f32, t: f32, hurst: Option) -> Array1 { let dt = (t - t_0) / n as f32; let mut x = Array1::zeros(n); @@ -96,12 +115,10 @@ pub trait ProcessF32: Send + Sync { x } - #[cfg(feature = "f32")] fn sample_as_vec(&self, n: usize, x_0: f32, t_0: f32, t: f32, hurst: Option) -> Vec { self.sample(n, x_0, t_0, t, hurst).to_vec() } - #[cfg(feature = "f32")] fn sample_parallel( &self, m: usize, @@ -110,17 +127,16 @@ pub trait ProcessF32: Send + Sync { t_0: f32, t: f32, hurst: Option, - ) -> Array2> { - let mut xs = Array2::>::default((m, n)); + ) -> Array2 { + let mut xs = Array2::::zeros((m, n)); - xs.par_iter_mut().for_each(|x| { - *x = self.sample(n, x_0, t_0, t, hurst); + xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { + x.assign(&self.sample(n, x_0, t_0, t, hurst)); }); xs } - #[cfg(feature = "f32")] fn sample_parallel_as_vec( &self, m: usize, @@ -132,6 +148,7 @@ pub trait ProcessF32: Send + Sync { ) -> Vec> { self .sample_parallel(m, n, x_0, t_0, t, hurst) + .axis_iter(Axis(0)) .into_par_iter() .map(|x| x.to_vec()) .collect() From 64c821c8e4d44aaa29fe9c70819e45f8357d1ec9 Mon Sep 17 00:00:00 2001 From: Daniel Boros Date: Mon, 22 Jul 2024 22:21:35 +0200 Subject: [PATCH 04/11] feat: add default api structure --- src/lib.rs | 2 +- src/main.rs | 49 ++++--- src/{stochastic.rs => quant.rs} | 3 + src/quant/diffusions.rs | 4 + src/quant/diffusions/bm.rs | 50 +++++++ src/quant/diffusions/fbm.rs | 92 ++++++++++++ src/quant/diffusions/fou.rs | 113 +++++++++++++++ src/{stochastic => quant}/diffusions/ou.rs | 27 ++-- src/quant/models.rs | 0 src/quant/noises.rs | 1 + src/quant/noises/fgn.rs | 135 ++++++++++++++++++ src/quant/traits.rs | 112 +++++++++++++++ src/quant/traits_f.rs | 104 ++++++++++++++ src/{stochastic => quant}/types.rs | 0 src/stochastic/diffusions.rs | 1 - src/stochastic/traits.rs | 156 --------------------- 16 files changed, 655 insertions(+), 194 deletions(-) rename src/{stochastic.rs => quant.rs} (50%) create mode 100644 src/quant/diffusions.rs create mode 100644 src/quant/diffusions/bm.rs create mode 100644 src/quant/diffusions/fbm.rs create mode 100644 src/quant/diffusions/fou.rs rename src/{stochastic => quant}/diffusions/ou.rs (61%) create mode 100644 src/quant/models.rs create mode 100644 src/quant/noises.rs create mode 100644 src/quant/noises/fgn.rs create mode 100644 src/quant/traits.rs create mode 100644 src/quant/traits_f.rs rename src/{stochastic => quant}/types.rs (100%) delete mode 100644 src/stochastic/diffusions.rs delete mode 100644 src/stochastic/traits.rs diff --git a/src/lib.rs b/src/lib.rs index d69e71a..8814ef9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,7 @@ pub mod prelude; -pub mod stochastic; +pub mod quant; pub mod diffusions; pub mod jumps; diff --git a/src/main.rs b/src/main.rs index 6d10d64..0f02dfc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,36 +5,47 @@ use plotly::{ Plot, Scatter, }; use stochastic_rs::{ - diffusions::ou::fou, - jumps::jump_fou::jump_fou, - noises::fgn::FgnFft, processes::{ fbm::Fbm, poisson::{compound_poisson, poisson}, }, + quant::{diffusions::fbm, traits_f::SamplingF}, utils::Generator, }; fn main() { - let start = Instant::now(); - let mut plot = Plot::new(); + let fbm = fbm::FBM::new_f32(0.7, 100000, 0.0, 0.0, 1.0); + let fbm2 = Fbm::new(0.7, 100000, Some(1.0), Some(10000)); - for i in 0..1 { - let d = jump_fou(0.1, 2.0, 0.5, 2.0, 2.0, 100, Some(0.0), Some(1.0)); - //let d = compound_poisson(50, 1.0, None, None, None); - //let d = poisson(10.0, Some(50), None); - - let trace = Scatter::new((0..d.len()).collect::>(), d.clone()) - .mode(plotly::common::Mode::Lines) - .line( - Line::new().color("blue"), //.shape(plotly::common::LineShape::Hv), - ) - .name(format!("Sequence {}", i + 1).as_str()); - plot.add_trace(trace); - } + let start = Instant::now(); + fbm.sample_parallel(10000); + // for i in 0..10000 { + // let d = fbm.sample(); - plot.show(); + // // let trace = Scatter::new((0..d.len()).collect::>(), d.clone()) + // // .mode(plotly::common::Mode::Lines) + // // .line( + // // Line::new().color("blue"), //.shape(plotly::common::LineShape::Hv), + // // ) + // // .name(format!("Sequence {}", i + 1).as_str()); + // // plot.add_trace(trace); + // // let trace = Scatter::new((0..d2.len()).collect::>(), d2.clone()) + // // .mode(plotly::common::Mode::Lines) + // // .line( + // // Line::new().color("red"), //.shape(plotly::common::LineShape::Hv), + // // ) + // // .name(format!("Sequence {}", i + 1).as_str()); + // // plot.add_trace(trace); + // } + println!("{}", start.elapsed().as_secs_f64()); + let start = Instant::now(); + // for i in 0..10000 { + // let d2 = fbm2.sample(); + // } + fbm2.sample_par(); println!("{}", start.elapsed().as_secs_f64()); + + //plot.show(); } diff --git a/src/stochastic.rs b/src/quant.rs similarity index 50% rename from src/stochastic.rs rename to src/quant.rs index e4cbad9..fa4ba4f 100644 --- a/src/stochastic.rs +++ b/src/quant.rs @@ -1,4 +1,7 @@ pub mod diffusions; +pub mod models; +pub mod noises; pub mod traits; +pub mod traits_f; pub mod types; diff --git a/src/quant/diffusions.rs b/src/quant/diffusions.rs new file mode 100644 index 0000000..390ffb1 --- /dev/null +++ b/src/quant/diffusions.rs @@ -0,0 +1,4 @@ +pub mod bm; +pub mod fbm; +pub mod fou; +pub mod ou; diff --git a/src/quant/diffusions/bm.rs b/src/quant/diffusions/bm.rs new file mode 100644 index 0000000..73bd338 --- /dev/null +++ b/src/quant/diffusions/bm.rs @@ -0,0 +1,50 @@ +use crate::quant::traits::Process; + +pub struct BM { + mu: T, + sigma: T, +} + +impl BM { + #[must_use] + #[inline(always)] + pub fn new() -> Self { + Self { + mu: 0_f64, + sigma: 1_f64, + } + } +} + +impl Process for BM { + fn drift(&self, _x: f64, _t: f64) -> f64 { + self.mu + } + + fn diffusion(&self, _x: f64, _t: f64) -> f64 { + self.sigma + } +} + +#[cfg(feature = "f32")] +impl BM { + #[must_use] + #[inline(always)] + pub fn new_f32() -> Self { + Self { + mu: 0_f32, + sigma: 1_f32, + } + } +} + +#[cfg(feature = "f32")] +impl Process for BM { + fn drift(&self, _x: f32, _t: f32) -> f32 { + self.mu + } + + fn diffusion(&self, _x: f32, _t: f32) -> f32 { + self.sigma + } +} diff --git a/src/quant/diffusions/fbm.rs b/src/quant/diffusions/fbm.rs new file mode 100644 index 0000000..56982a7 --- /dev/null +++ b/src/quant/diffusions/fbm.rs @@ -0,0 +1,92 @@ +use crate::quant::{noises::fgn::FGN, traits_f::FractionalProcess}; + +pub struct FBM { + mu: T, + sigma: T, + pub hurst: T, + n: usize, + x_0: T, + t_0: T, + t: T, + fgn: FGN, +} + +impl FBM { + #[must_use] + #[inline(always)] + pub fn new(hurst: f64, n: usize, x_0: f64, t_0: f64, t: f64) -> Self { + Self { + mu: 0_f64, + sigma: 1_f64, + hurst, + n, + x_0, + t_0, + t, + fgn: FGN::new(hurst, n - 1, t), + } + } +} + +impl FractionalProcess for FBM { + fn drift(&self, _x: f64, _t: f64) -> f64 { + self.mu + } + + fn diffusion(&self, _x: f64, _t: f64) -> f64 { + self.sigma + } + + fn hurst(&self) -> f64 { + self.hurst + } + + fn fgn(&self) -> FGN { + self.fgn.clone() + } + + fn params(&self) -> (usize, f64, f64, f64) { + (self.n, self.x_0, self.t_0, self.t) + } +} + +#[cfg(feature = "f32")] +impl FBM { + #[must_use] + #[inline(always)] + pub fn new_f32(hurst: f32, n: usize, x_0: f32, t_0: f32, t: f32) -> Self { + Self { + mu: 0_f32, + sigma: 1_f32, + hurst, + n, + x_0, + t_0, + t, + fgn: FGN::new_f32(hurst, n - 1, t), + } + } +} + +#[cfg(feature = "f32")] +impl FractionalProcess for FBM { + fn drift(&self, _x: f32, _t: f32) -> f32 { + self.mu + } + + fn diffusion(&self, _x: f32, _t: f32) -> f32 { + self.sigma + } + + fn hurst(&self) -> f32 { + self.hurst + } + + fn fgn(&self) -> FGN { + self.fgn.clone() + } + + fn params(&self) -> (usize, f32, f32, f32) { + (self.n, self.x_0, self.t_0, self.t) + } +} diff --git a/src/quant/diffusions/fou.rs b/src/quant/diffusions/fou.rs new file mode 100644 index 0000000..068c3a5 --- /dev/null +++ b/src/quant/diffusions/fou.rs @@ -0,0 +1,113 @@ +use crate::quant::{noises::fgn::FGN, traits_f::FractionalProcess}; + +pub struct FOU { + pub theta: T, + pub mu: T, + pub sigma: T, + pub hurst: T, + n: usize, + x_0: T, + t_0: T, + t: T, + fgn: FGN, +} + +impl FOU { + #[must_use] + #[inline(always)] + pub fn new( + theta: f64, + mu: f64, + sigma: f64, + hurst: f64, + n: usize, + x_0: f64, + t_0: f64, + t: f64, + ) -> Self { + Self { + theta, + mu, + sigma, + hurst, + n, + x_0, + t_0, + t, + fgn: FGN::new(hurst, n, t), + } + } +} + +impl FractionalProcess for FOU { + fn drift(&self, x: f64, _t: f64) -> f64 { + self.theta * (self.mu - x) + } + + fn diffusion(&self, _x: f64, _t: f64) -> f64 { + self.sigma + } + + fn hurst(&self) -> f64 { + self.hurst + } + + fn fgn(&self) -> FGN { + self.fgn.clone() + } + + fn params(&self) -> (usize, f64, f64, f64) { + (self.n, self.x_0, self.t_0, self.t) + } +} + +#[cfg(feature = "f32")] +impl FOU { + #[must_use] + #[inline(always)] + pub fn new_f32( + theta: f32, + mu: f32, + sigma: f32, + hurst: f32, + n: usize, + x_0: f32, + t_0: f32, + t: f32, + ) -> Self { + Self { + theta, + mu, + sigma, + hurst, + n, + x_0, + t_0, + t, + fgn: FGN::new_f32(hurst, n, t), + } + } +} + +#[cfg(feature = "f32")] +impl FractionalProcess for FOU { + fn drift(&self, x: f32, _t: f32) -> f32 { + self.theta * (self.mu - x) + } + + fn diffusion(&self, _x: f32, _t: f32) -> f32 { + self.sigma + } + + fn hurst(&self) -> f32 { + self.hurst + } + + fn fgn(&self) -> FGN { + self.fgn.clone() + } + + fn params(&self) -> (usize, f32, f32, f32) { + (self.n, self.x_0, self.t_0, self.t) + } +} diff --git a/src/stochastic/diffusions/ou.rs b/src/quant/diffusions/ou.rs similarity index 61% rename from src/stochastic/diffusions/ou.rs rename to src/quant/diffusions/ou.rs index 80a2b1c..a34b7d7 100644 --- a/src/stochastic/diffusions/ou.rs +++ b/src/quant/diffusions/ou.rs @@ -1,12 +1,12 @@ -use crate::stochastic::traits::Process; +use crate::quant::traits::Process; -pub struct OU { - pub theta: f64, - pub mu: f64, - pub sigma: f64, +pub struct OU { + pub theta: T, + pub mu: T, + pub sigma: T, } -impl OU { +impl OU { #[must_use] #[inline(always)] pub fn new(theta: f64, mu: f64, sigma: f64) -> Self { @@ -14,7 +14,7 @@ impl OU { } } -impl Process for OU { +impl Process for OU { fn drift(&self, x: f64, _t: f64) -> f64 { self.theta * (self.mu - x) } @@ -25,23 +25,16 @@ impl Process for OU { } #[cfg(feature = "f32")] -pub struct OUF32 { - pub theta: f32, - pub mu: f32, - pub sigma: f32, -} - -#[cfg(feature = "f32")] -impl OUF32 { +impl OU { #[must_use] #[inline(always)] - pub fn new(theta: f32, mu: f32, sigma: f32) -> Self { + pub fn new_f32(theta: f32, mu: f32, sigma: f32) -> Self { Self { theta, mu, sigma } } } #[cfg(feature = "f32")] -impl Process for OUF32 { +impl Process for OU { fn drift(&self, x: f32, _t: f32) -> f32 { self.theta * (self.mu - x) } diff --git a/src/quant/models.rs b/src/quant/models.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/quant/noises.rs b/src/quant/noises.rs new file mode 100644 index 0000000..29a6c15 --- /dev/null +++ b/src/quant/noises.rs @@ -0,0 +1 @@ +pub mod fgn; diff --git a/src/quant/noises/fgn.rs b/src/quant/noises/fgn.rs new file mode 100644 index 0000000..a152f8a --- /dev/null +++ b/src/quant/noises/fgn.rs @@ -0,0 +1,135 @@ +use ndarray::{concatenate, prelude::*}; +use ndarray_rand::rand_distr::StandardNormal; +use ndarray_rand::RandomExt; +use ndrustfft::{ndfft_par, FftHandler}; +use num_complex::{Complex, ComplexDistribution}; + +#[derive(Clone)] +pub struct FGN { + hurst: T, + n: usize, + offset: usize, + t: T, + sqrt_eigenvalues: Array1>, + fft_handler: FftHandler, + fft_fgn: Array1>, +} + +impl FGN { + #[must_use] + #[inline(always)] + pub fn new(hurst: f64, n: usize, t: f64) -> Self { + if !(0_f64..=1_f64).contains(&hurst) { + panic!("Hurst parameter must be between 0 and 1"); + } + let n_ = n.next_power_of_two(); + let offset = n_ - n; + let n = n_; + let mut r = Array1::linspace(0_f64, n as f64, n + 1); + r.par_mapv_inplace(|x| { + if x == 0_f64 { + 1_f64 + } else { + 0.5 + * ((x + 1_f64).powf(2_f64 * hurst) - 2_f64 * x.powf(2_f64 * hurst) + + (x - 1_f64).powf(2_f64 * hurst)) + } + }); + let r = concatenate( + Axis(0), + #[allow(clippy::reversed_empty_ranges)] + &[r.view(), r.slice(s![..;-1]).slice(s![1..-1]).view()], + ) + .unwrap(); + let data = r.mapv(|v| Complex::new(v, 0_f64)); + let r_fft = FftHandler::new(r.len()); + let mut sqrt_eigenvalues = Array1::>::zeros(r.len()); + ndfft_par(&data, &mut sqrt_eigenvalues, &r_fft, 0); + sqrt_eigenvalues.par_mapv_inplace(|x| Complex::new((x.re / (2_f64 * n as f64)).sqrt(), x.im)); + + Self { + hurst, + n, + offset, + t, + sqrt_eigenvalues, + fft_handler: FftHandler::new(2 * n), + fft_fgn: Array1::>::zeros(2 * n), + } + } + + pub fn sample(&self) -> Array1 { + let rnd = Array1::>::random( + 2 * self.n, + ComplexDistribution::new(StandardNormal, StandardNormal), + ); + let fgn = &self.sqrt_eigenvalues * &rnd; + let fft_handler = self.fft_handler.clone(); + let mut fgn_fft = self.fft_fgn.clone(); + ndfft_par(&fgn, &mut fgn_fft, &fft_handler, 0); + let fgn = fgn_fft + .slice(s![1..self.n - self.offset + 1]) + .mapv(|x: Complex| (x.re * (self.n as f64).powf(-self.hurst)) * self.t.powf(self.hurst)); + fgn + } +} + +#[cfg(feature = "f32")] +impl FGN { + #[must_use] + #[inline(always)] + pub fn new_f32(hurst: f32, n: usize, t: f32) -> Self { + if !(0_f32..=1_f32).contains(&hurst) { + panic!("Hurst parameter must be between 0 and 1"); + } + let n_ = n.next_power_of_two(); + let offset = n_ - n; + let n = n_; + let mut r = Array1::linspace(0_f32, n as f32, n + 1); + r.par_mapv_inplace(|x| { + if x == 0_f32 { + 1_f32 + } else { + 0.5 + * ((x + 1_f32).powf(2_f32 * hurst) - 2_f32 * x.powf(2_f32 * hurst) + + (x - 1_f32).powf(2_f32 * hurst)) + } + }); + let r = concatenate( + Axis(0), + #[allow(clippy::reversed_empty_ranges)] + &[r.view(), r.slice(s![..;-1]).slice(s![1..-1]).view()], + ) + .unwrap(); + let data = r.mapv(|v| Complex::new(v, 0_f32)); + let r_fft = FftHandler::new(r.len()); + let mut sqrt_eigenvalues = Array1::>::zeros(r.len()); + ndfft_par(&data, &mut sqrt_eigenvalues, &r_fft, 0); + sqrt_eigenvalues.par_mapv_inplace(|x| Complex::new((x.re / (2_f32 * n as f32)).sqrt(), x.im)); + + Self { + hurst, + n, + offset, + t, + sqrt_eigenvalues, + fft_handler: FftHandler::new(2 * n), + fft_fgn: Array1::>::zeros(2 * n), + } + } + + pub fn sample(&self) -> Array1 { + let rnd = Array1::>::random( + 2 * self.n, + ComplexDistribution::new(StandardNormal, StandardNormal), + ); + let fgn = &self.sqrt_eigenvalues * &rnd; + let fft_handler = self.fft_handler.clone(); + let mut fgn_fft = self.fft_fgn.clone(); + ndfft_par(&fgn, &mut fgn_fft, &fft_handler, 0); + let fgn = fgn_fft + .slice(s![1..self.n - self.offset + 1]) + .mapv(|x: Complex| (x.re * (self.n as f32).powf(-self.hurst)) * self.t.powf(self.hurst)); + fgn + } +} diff --git a/src/quant/traits.rs b/src/quant/traits.rs new file mode 100644 index 0000000..dea7fd0 --- /dev/null +++ b/src/quant/traits.rs @@ -0,0 +1,112 @@ +use ndarray::{Array1, Array2, Axis}; +use ndarray_rand::rand_distr::Normal; +use ndarray_rand::RandomExt; +use rayon::prelude::*; + +pub trait Process: Send + Sync { + fn drift(&self, x: T, t: T) -> T; + fn diffusion(&self, x: T, t: T) -> T; +} + +pub trait Sampling { + fn sample(&self, n: usize, x_0: T, t_0: T, t: T) -> Array1; + fn sample_as_vec(&self, n: usize, x_0: T, t_0: T, t: T) -> Vec; + fn sample_parallel(&self, m: usize, n: usize, x_0: T, t_0: T, t: T) -> Array2; + fn sample_parallel_as_vec(&self, m: usize, n: usize, x_0: T, t_0: T, t: T) -> Vec>; +} + +impl> Sampling for T { + fn sample(&self, n: usize, x_0: f64, t_0: f64, t: f64) -> Array1 { + let dt = (t - t_0) / n as f64; + let mut x = Array1::zeros(n); + x[0] = x_0; + let noise = Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()); + let times = Array1::linspace(t_0, t, n); + + // TODO: test idx + noise.into_iter().enumerate().for_each(|(idx, dw)| { + x[idx + 1] = + x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; + }); + + x + } + + fn sample_as_vec(&self, n: usize, x_0: f64, t_0: f64, t: f64) -> Vec { + self.sample(n, x_0, t_0, t).to_vec() + } + + fn sample_parallel(&self, m: usize, n: usize, x_0: f64, t_0: f64, t: f64) -> Array2 { + let mut xs = Array2::::zeros((m, n)); + + xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { + x.assign(&self.sample(n, x_0, t_0, t)); + }); + + xs + } + + fn sample_parallel_as_vec( + &self, + m: usize, + n: usize, + x_0: f64, + t_0: f64, + t: f64, + ) -> Vec> { + self + .sample_parallel(m, n, x_0, t_0, t) + .axis_iter(Axis(0)) + .into_par_iter() + .map(|x| x.to_vec()) + .collect() + } +} + +#[cfg(feature = "f32")] +impl> Sampling for T { + fn sample(&self, n: usize, x_0: f32, t_0: f32, t: f32) -> Array1 { + let dt = (t - t_0) / n as f32; + let mut x = Array1::zeros(n); + x[0] = x_0; + let noise = Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()); + let times = Array1::linspace(t_0, t, n); + + noise.into_iter().enumerate().for_each(|(idx, dw)| { + x[idx + 1] = + x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; + }); + + x + } + + fn sample_as_vec(&self, n: usize, x_0: f32, t_0: f32, t: f32) -> Vec { + self.sample(n, x_0, t_0, t).to_vec() + } + + fn sample_parallel(&self, m: usize, n: usize, x_0: f32, t_0: f32, t: f32) -> Array2 { + let mut xs = Array2::::zeros((m, n)); + + xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { + x.assign(&self.sample(n, x_0, t_0, t)); + }); + + xs + } + + fn sample_parallel_as_vec( + &self, + m: usize, + n: usize, + x_0: f32, + t_0: f32, + t: f32, + ) -> Vec> { + self + .sample_parallel(m, n, x_0, t_0, t) + .axis_iter(Axis(0)) + .into_par_iter() + .map(|x| x.to_vec()) + .collect() + } +} diff --git a/src/quant/traits_f.rs b/src/quant/traits_f.rs new file mode 100644 index 0000000..d768502 --- /dev/null +++ b/src/quant/traits_f.rs @@ -0,0 +1,104 @@ +use crate::quant::noises::fgn::FGN; +use ndarray::{Array1, Array2, Axis}; +use rayon::prelude::*; + +pub trait FractionalProcess: Send + Sync { + fn drift(&self, x: T, t: T) -> T; + fn diffusion(&self, x: T, t: T) -> T; + fn hurst(&self) -> T; + fn fgn(&self) -> FGN; + fn params(&self) -> (usize, T, T, T); +} + +pub trait SamplingF { + fn sample(&self) -> Array1; + fn sample_as_vec(&self) -> Vec; + fn sample_parallel(&self, m: usize) -> Array2; + fn sample_parallel_as_vec(&self, m: usize) -> Vec>; +} + +impl> SamplingF for T { + fn sample(&self) -> Array1 { + let (n, x_0, t_0, t) = self.params(); + let dt = (t - t_0) / n as f64; + let mut x = Array1::zeros(n); + x[0] = x_0; + let noise = self.fgn().sample(); + let times = Array1::linspace(t_0, t, n); + + // TODO: test idx + noise.into_iter().enumerate().for_each(|(idx, dw)| { + x[idx + 1] = + x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; + }); + + x + } + + fn sample_as_vec(&self) -> Vec { + self.sample().to_vec() + } + + fn sample_parallel(&self, m: usize) -> Array2 { + let (n, ..) = self.params(); + let mut xs = Array2::::zeros((m, n)); + + xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { + x.assign(&self.sample()); + }); + + xs + } + + fn sample_parallel_as_vec(&self, m: usize) -> Vec> { + self + .sample_parallel(m) + .axis_iter(Axis(0)) + .into_par_iter() + .map(|x| x.to_vec()) + .collect() + } +} + +#[cfg(feature = "f32")] +impl> SamplingF for T { + fn sample(&self) -> Array1 { + let (n, x_0, t_0, t) = self.params(); + let dt = (t - t_0) / n as f32; + let mut x = Array1::zeros(n); + x[0] = x_0; + let noise = self.fgn().sample(); + let times = Array1::linspace(t_0, t, n); + + noise.into_iter().enumerate().for_each(|(idx, dw)| { + x[idx + 1] = + x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; + }); + + x + } + + fn sample_as_vec(&self) -> Vec { + self.sample().to_vec() + } + + fn sample_parallel(&self, m: usize) -> Array2 { + let (n, ..) = self.params(); + let mut xs = Array2::::zeros((m, n)); + + xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { + x.assign(&self.sample()); + }); + + xs + } + + fn sample_parallel_as_vec(&self, m: usize) -> Vec> { + self + .sample_parallel(m) + .axis_iter(Axis(0)) + .into_par_iter() + .map(|x| x.to_vec()) + .collect() + } +} diff --git a/src/stochastic/types.rs b/src/quant/types.rs similarity index 100% rename from src/stochastic/types.rs rename to src/quant/types.rs diff --git a/src/stochastic/diffusions.rs b/src/stochastic/diffusions.rs deleted file mode 100644 index fe0a0f7..0000000 --- a/src/stochastic/diffusions.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod ou; diff --git a/src/stochastic/traits.rs b/src/stochastic/traits.rs deleted file mode 100644 index a864fd6..0000000 --- a/src/stochastic/traits.rs +++ /dev/null @@ -1,156 +0,0 @@ -use ndarray::{Array1, Array2, Axis}; -use ndarray_rand::rand_distr::Normal; -use ndarray_rand::RandomExt; -use rayon::prelude::*; - -pub trait Process: Send + Sync { - fn drift(&self, x: T, t: T) -> T; - fn diffusion(&self, x: T, t: T) -> T; -} - -pub trait Sampling { - fn sample(&self, n: usize, x_0: T, t_0: T, t: T, hurst: Option) -> Array1; - fn sample_as_vec(&self, n: usize, x_0: T, t_0: T, t: T, hurst: Option) -> Vec; - fn sample_parallel( - &self, - m: usize, - n: usize, - x_0: T, - t_0: T, - t: T, - hurst: Option, - ) -> Array2; - fn sample_parallel_as_vec( - &self, - m: usize, - n: usize, - x_0: T, - t_0: T, - t: T, - hurst: Option, - ) -> Vec>; -} - -impl> Sampling for T { - fn sample(&self, n: usize, x_0: f64, t_0: f64, t: f64, hurst: Option) -> Array1 { - let dt = (t - t_0) / n as f64; - let mut x = Array1::zeros(n); - x[0] = x_0; - let noise = if hurst.is_some() && hurst.unwrap() != 0.5 { - // TODO: add fractional Gaussian noise - Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()) - } else { - Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()) - }; - let times = Array1::linspace(t_0, t, n); - - // TODO: test idx - noise.into_iter().enumerate().for_each(|(idx, dw)| { - x[idx + 1] = - x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; - }); - - x - } - - fn sample_as_vec(&self, n: usize, x_0: f64, t_0: f64, t: f64, hurst: Option) -> Vec { - self.sample(n, x_0, t_0, t, hurst).to_vec() - } - - fn sample_parallel( - &self, - m: usize, - n: usize, - x_0: f64, - t_0: f64, - t: f64, - hurst: Option, - ) -> Array2 { - let mut xs = Array2::::zeros((m, n)); - - xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { - x.assign(&self.sample(n, x_0, t_0, t, hurst)); - }); - - xs - } - - fn sample_parallel_as_vec( - &self, - m: usize, - n: usize, - x_0: f64, - t_0: f64, - t: f64, - hurst: Option, - ) -> Vec> { - self - .sample_parallel(m, n, x_0, t_0, t, hurst) - .axis_iter(Axis(0)) - .into_par_iter() - .map(|x| x.to_vec()) - .collect() - } -} - -#[cfg(feature = "f32")] -impl> Sampling for T { - fn sample(&self, n: usize, x_0: f32, t_0: f32, t: f32, hurst: Option) -> Array1 { - let dt = (t - t_0) / n as f32; - let mut x = Array1::zeros(n); - x[0] = x_0; - let noise = if hurst.is_some() && hurst.unwrap() != 0.5 { - // TODO: add fractional Gaussian noise - Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()) - } else { - Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()) - }; - let times = Array1::linspace(t_0, t, n); - - noise.into_iter().enumerate().for_each(|(idx, dw)| { - x[idx + 1] = - x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; - }); - - x - } - - fn sample_as_vec(&self, n: usize, x_0: f32, t_0: f32, t: f32, hurst: Option) -> Vec { - self.sample(n, x_0, t_0, t, hurst).to_vec() - } - - fn sample_parallel( - &self, - m: usize, - n: usize, - x_0: f32, - t_0: f32, - t: f32, - hurst: Option, - ) -> Array2 { - let mut xs = Array2::::zeros((m, n)); - - xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { - x.assign(&self.sample(n, x_0, t_0, t, hurst)); - }); - - xs - } - - fn sample_parallel_as_vec( - &self, - m: usize, - n: usize, - x_0: f32, - t_0: f32, - t: f32, - hurst: Option, - ) -> Vec> { - self - .sample_parallel(m, n, x_0, t_0, t, hurst) - .axis_iter(Axis(0)) - .into_par_iter() - .map(|x| x.to_vec()) - .collect() - } -} From cdfe52ae0fa7ec0c55d3342cbd612ff3d19cd78d Mon Sep 17 00:00:00 2001 From: Daniel Boros Date: Mon, 22 Jul 2024 23:26:52 +0200 Subject: [PATCH 05/11] chore: remove cargo nightly --- Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3f784a9..ee38d03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,3 @@ -cargo-features = ["edition2024"] - [package] name = "stochastic-rs" version = "0.5.3" From 49ea538f056f03cb74387664732cb129b10333f7 Mon Sep 17 00:00:00 2001 From: Daniel Boros Date: Fri, 26 Jul 2024 10:34:03 +0200 Subject: [PATCH 06/11] Merge branch 'main' into feat/rework-api-and-performance --- src/jumps/bates.rs | 22 +++---- src/main.rs | 151 +++++++++++++++++++++++++++++++++------------ 2 files changed, 123 insertions(+), 50 deletions(-) diff --git a/src/jumps/bates.rs b/src/jumps/bates.rs index b473fdf..84a8020 100644 --- a/src/jumps/bates.rs +++ b/src/jumps/bates.rs @@ -42,17 +42,17 @@ use crate::prelude::{ #[derive(Default)] pub struct Bates1996 { - mu: f64, - kappa: f64, - theta: f64, - eta: f64, - rho: f64, - lambda: f64, - n: usize, - s0: Option, - v0: Option, - t: Option, - use_sym: Option, + pub mu: f64, + pub kappa: f64, + pub theta: f64, + pub eta: f64, + pub rho: f64, + pub lambda: f64, + pub n: usize, + pub s0: Option, + pub v0: Option, + pub t: Option, + pub use_sym: Option, } pub fn bates_1996( diff --git a/src/main.rs b/src/main.rs index 87593b5..a47bacd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,49 +1,122 @@ -use std::time::Instant; - -use plotly::{common::Line, Plot, Scatter}; -use rand_distr::{Gamma, Normal}; -use stochastic_rs::{ - jumps::{bates::bates_1996, jump_fou::jump_fou, levy_diffusion::levy_diffusion, merton::merton}, - processes::{cpoisson::compound_poisson, fbm::Fbm, poisson::poisson}, - quant::{diffusions::fbm, traits_f::SamplingF}, - utils::Generator, +use plotly::{common::Line, Layout, Plot}; +use rand_distr::{Exp, Normal}; +use stochastic_rs::jumps::{ + bates::{bates_1996, Bates1996}, + jump_fou::{jump_fou, JumpFou}, + levy_diffusion::{levy_diffusion, LevyDiffusion}, + merton::{merton, Merton}, }; fn main() { let mut plot = Plot::new(); - let fbm = fbm::FBM::new_f32(0.7, 100000, 0.0, 0.0, 1.0); - let fbm2 = Fbm::new(0.7, 100000, Some(1.0), Some(10000)); + plot.set_layout(Layout::new().width(600).height(600)); - for i in 0..1 { - // let d = poisson(10.0, Some(50), None); - // ) - // .name("Poisson"); - //plot.add_trace(trace); - } + for _ in 0..1 { + // let jump_fou = jump_fou( + // &JumpFou { + // hurst: 0.75, + // mu: 10.0, + // sigma: 9.0, + // theta: 1.0, + // n: 100, + // t: Some(1.0), + // lambda: 0.25, + // ..Default::default() + // }, + // Exp::new(1.0).unwrap(), + // ); + // let trace = plotly::Scatter::new( + // (0..jump_fou.len()) + // .into_iter() + // .map(|idx| idx) + // .collect::>(), + // jump_fou.to_vec(), + // ) + // .line( + // Line::new() + // .color("blue") + // .shape(plotly::common::LineShape::Hv), + // ); + // plot.add_trace(trace); - plot.show(); + // let jump_fou = levy_diffusion( + // &LevyDiffusion { + // gamma: 1.0, + // sigma: 0.25, + // n: 100, + // t: Some(10.0), + // lambda: 40.0, + // ..Default::default() + // }, + // Normal::new(0.25, 2.0).unwrap(), + // ); + // let trace = plotly::Scatter::new( + // (0..jump_fou.len()) + // .into_iter() + // .map(|idx| idx) + // .collect::>(), + // jump_fou.to_vec(), + // ) + // .line( + // Line::new() + // .color("blue") + // .shape(plotly::common::LineShape::Hv), + // ); + // plot.add_trace(trace); - // // let trace = Scatter::new((0..d.len()).collect::>(), d.clone()) - // // .mode(plotly::common::Mode::Lines) - // // .line( - // // Line::new().color("blue"), //.shape(plotly::common::LineShape::Hv), - // // ) - // // .name(format!("Sequence {}", i + 1).as_str()); - // // let trace = Scatter::new((0..d2.len()).collect::>(), d2.clone()) - // // .mode(plotly::common::Mode::Lines) - // // .line( - // // Line::new().color("red"), //.shape(plotly::common::LineShape::Hv), - // // ) - // // .name(format!("Sequence {}", i + 1).as_str()); - // // plot.add_trace(trace); - // } + // let merton: ndarray::ArrayBase, ndarray::Dim<[usize; 1]>> = merton( + // &Merton { + // alpha: 0.08, + // sigma: 0.1, + // lambda: 25.0, + // theta: 0.08, + // n: 100, + // x0: Some(40.0), + // t: Some(1.0), + // }, + // Normal::new(0.0, 2.0).unwrap(), + // ); + // let trace = plotly::Scatter::new( + // (0..merton.len()) + // .into_iter() + // .map(|idx| idx) + // .collect::>(), + // merton.to_vec(), + // ) + // .line( + // Line::new() + // .color("blue") + // .shape(plotly::common::LineShape::Hv), + // ); + // plot.add_trace(trace); - let start = Instant::now(); - // for i in 0..10000 { - // let d2 = fbm2.sample(); - // } - fbm2.sample_par(); - println!("{}", start.elapsed().as_secs_f64()); + let [s, v] = bates_1996( + &Bates1996 { + mu: 1.0, + kappa: 1.0, + theta: 1.0, + eta: 1.0, + rho: 0.03, + lambda: 2.0, + n: 1000, + s0: Some(0.0), + v0: Some(0.0), + t: Some(1.0), + use_sym: Some(false), + }, + Normal::new(0.0, 1.0).unwrap(), + ); + let trace = plotly::Scatter::new( + (0..s.len()).into_iter().map(|idx| idx).collect::>(), + s.to_vec(), + ) + .line( + Line::new() + .color("blue") + .shape(plotly::common::LineShape::Hv), + ); + plot.add_trace(trace); + } - //plot.show(); + plot.show(); } From f607ad6689c2822a6b31c9fe07c99a1a178bdeee Mon Sep 17 00:00:00 2001 From: Daniel Boros Date: Tue, 30 Jul 2024 00:07:28 +0200 Subject: [PATCH 07/11] feat: add gn generation --- src/quant/diffusions/bm.rs | 8 +-- src/quant/diffusions/fbm.rs | 8 +-- src/quant/noises.rs | 1 + src/quant/noises/fgn.rs | 88 +++++++++++++++++++++------ src/quant/noises/gn.rs | 118 ++++++++++++++++++++++++++++++++++++ 5 files changed, 197 insertions(+), 26 deletions(-) create mode 100644 src/quant/noises/gn.rs diff --git a/src/quant/diffusions/bm.rs b/src/quant/diffusions/bm.rs index 73bd338..cf2fe08 100644 --- a/src/quant/diffusions/bm.rs +++ b/src/quant/diffusions/bm.rs @@ -10,8 +10,8 @@ impl BM { #[inline(always)] pub fn new() -> Self { Self { - mu: 0_f64, - sigma: 1_f64, + mu: 0.0, + sigma: 1.0, } } } @@ -32,8 +32,8 @@ impl BM { #[inline(always)] pub fn new_f32() -> Self { Self { - mu: 0_f32, - sigma: 1_f32, + mu: 0.0, + sigma: 1.0, } } } diff --git a/src/quant/diffusions/fbm.rs b/src/quant/diffusions/fbm.rs index 56982a7..209244a 100644 --- a/src/quant/diffusions/fbm.rs +++ b/src/quant/diffusions/fbm.rs @@ -16,8 +16,8 @@ impl FBM { #[inline(always)] pub fn new(hurst: f64, n: usize, x_0: f64, t_0: f64, t: f64) -> Self { Self { - mu: 0_f64, - sigma: 1_f64, + mu: 0.0, + sigma: 1.0, hurst, n, x_0, @@ -56,8 +56,8 @@ impl FBM { #[inline(always)] pub fn new_f32(hurst: f32, n: usize, x_0: f32, t_0: f32, t: f32) -> Self { Self { - mu: 0_f32, - sigma: 1_f32, + mu: 0.0, + sigma: 1.0, hurst, n, x_0, diff --git a/src/quant/noises.rs b/src/quant/noises.rs index 29a6c15..25a9976 100644 --- a/src/quant/noises.rs +++ b/src/quant/noises.rs @@ -1 +1,2 @@ pub mod fgn; +pub mod gn; diff --git a/src/quant/noises/fgn.rs b/src/quant/noises/fgn.rs index a152f8a..b73a299 100644 --- a/src/quant/noises/fgn.rs +++ b/src/quant/noises/fgn.rs @@ -3,6 +3,9 @@ use ndarray_rand::rand_distr::StandardNormal; use ndarray_rand::RandomExt; use ndrustfft::{ndfft_par, FftHandler}; use num_complex::{Complex, ComplexDistribution}; +use rayon::prelude::*; + +use crate::quant::traits_f::SamplingF; #[derive(Clone)] pub struct FGN { @@ -19,20 +22,19 @@ impl FGN { #[must_use] #[inline(always)] pub fn new(hurst: f64, n: usize, t: f64) -> Self { - if !(0_f64..=1_f64).contains(&hurst) { + if !(0.0..=1.0).contains(&hurst) { panic!("Hurst parameter must be between 0 and 1"); } let n_ = n.next_power_of_two(); let offset = n_ - n; let n = n_; - let mut r = Array1::linspace(0_f64, n as f64, n + 1); + let mut r = Array1::linspace(0.0, n as f64, n + 1); r.par_mapv_inplace(|x| { - if x == 0_f64 { - 1_f64 + if x == 0.0 { + 1.0 } else { 0.5 - * ((x + 1_f64).powf(2_f64 * hurst) - 2_f64 * x.powf(2_f64 * hurst) - + (x - 1_f64).powf(2_f64 * hurst)) + * ((x + 1.0).powf(2.0 * hurst) - 2.0 * x.powf(2.0 * hurst) + (x - 1.0).powf(2.0 * hurst)) } }); let r = concatenate( @@ -41,11 +43,11 @@ impl FGN { &[r.view(), r.slice(s![..;-1]).slice(s![1..-1]).view()], ) .unwrap(); - let data = r.mapv(|v| Complex::new(v, 0_f64)); + let data = r.mapv(|v| Complex::new(v, 0.0)); let r_fft = FftHandler::new(r.len()); let mut sqrt_eigenvalues = Array1::>::zeros(r.len()); ndfft_par(&data, &mut sqrt_eigenvalues, &r_fft, 0); - sqrt_eigenvalues.par_mapv_inplace(|x| Complex::new((x.re / (2_f64 * n as f64)).sqrt(), x.im)); + sqrt_eigenvalues.par_mapv_inplace(|x| Complex::new((x.re / (2.0 * n as f64)).sqrt(), x.im)); Self { hurst, @@ -57,8 +59,10 @@ impl FGN { fft_fgn: Array1::>::zeros(2 * n), } } +} - pub fn sample(&self) -> Array1 { +impl SamplingF for FGN { + fn sample(&self) -> Array1 { let rnd = Array1::>::random( 2 * self.n, ComplexDistribution::new(StandardNormal, StandardNormal), @@ -72,6 +76,29 @@ impl FGN { .mapv(|x: Complex| (x.re * (self.n as f64).powf(-self.hurst)) * self.t.powf(self.hurst)); fgn } + + fn sample_as_vec(&self) -> Vec { + self.sample().to_vec() + } + + fn sample_parallel(&self, m: usize) -> Array2 { + let mut xs = Array2::::zeros((m, self.n)); + + xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { + x.assign(&self.sample()); + }); + + xs + } + + fn sample_parallel_as_vec(&self, m: usize) -> Vec> { + self + .sample_parallel(m) + .axis_iter(Axis(0)) + .into_par_iter() + .map(|x| x.to_vec()) + .collect() + } } #[cfg(feature = "f32")] @@ -79,20 +106,19 @@ impl FGN { #[must_use] #[inline(always)] pub fn new_f32(hurst: f32, n: usize, t: f32) -> Self { - if !(0_f32..=1_f32).contains(&hurst) { + if !(0.0..=1.0).contains(&hurst) { panic!("Hurst parameter must be between 0 and 1"); } let n_ = n.next_power_of_two(); let offset = n_ - n; let n = n_; - let mut r = Array1::linspace(0_f32, n as f32, n + 1); + let mut r = Array1::linspace(0.0, n as f32, n + 1); r.par_mapv_inplace(|x| { - if x == 0_f32 { - 1_f32 + if x == 0.0 { + 1.0 } else { 0.5 - * ((x + 1_f32).powf(2_f32 * hurst) - 2_f32 * x.powf(2_f32 * hurst) - + (x - 1_f32).powf(2_f32 * hurst)) + * ((x + 1.0).powf(2.0 * hurst) - 2.0 * x.powf(2.0 * hurst) + (x - 1.0).powf(2.0 * hurst)) } }); let r = concatenate( @@ -101,11 +127,11 @@ impl FGN { &[r.view(), r.slice(s![..;-1]).slice(s![1..-1]).view()], ) .unwrap(); - let data = r.mapv(|v| Complex::new(v, 0_f32)); + let data = r.mapv(|v| Complex::new(v, 0.0)); let r_fft = FftHandler::new(r.len()); let mut sqrt_eigenvalues = Array1::>::zeros(r.len()); ndfft_par(&data, &mut sqrt_eigenvalues, &r_fft, 0); - sqrt_eigenvalues.par_mapv_inplace(|x| Complex::new((x.re / (2_f32 * n as f32)).sqrt(), x.im)); + sqrt_eigenvalues.par_mapv_inplace(|x| Complex::new((x.re / (2.0 * n as f32)).sqrt(), x.im)); Self { hurst, @@ -117,8 +143,11 @@ impl FGN { fft_fgn: Array1::>::zeros(2 * n), } } +} - pub fn sample(&self) -> Array1 { +#[cfg(feature = "f32")] +impl SamplingF for FGN { + fn sample(&self) -> Array1 { let rnd = Array1::>::random( 2 * self.n, ComplexDistribution::new(StandardNormal, StandardNormal), @@ -132,4 +161,27 @@ impl FGN { .mapv(|x: Complex| (x.re * (self.n as f32).powf(-self.hurst)) * self.t.powf(self.hurst)); fgn } + + fn sample_as_vec(&self) -> Vec { + self.sample().to_vec() + } + + fn sample_parallel(&self, m: usize) -> Array2 { + let mut xs = Array2::::zeros((m, self.n)); + + xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { + x.assign(&self.sample()); + }); + + xs + } + + fn sample_parallel_as_vec(&self, m: usize) -> Vec> { + self + .sample_parallel(m) + .axis_iter(Axis(0)) + .into_par_iter() + .map(|x| x.to_vec()) + .collect() + } } diff --git a/src/quant/noises/gn.rs b/src/quant/noises/gn.rs new file mode 100644 index 0000000..5545588 --- /dev/null +++ b/src/quant/noises/gn.rs @@ -0,0 +1,118 @@ +use ndarray::Array1; +use ndarray_rand::RandomExt; +use rand_distr::Normal; +use rayon::prelude::*; + +use crate::quant::traits::Sampling; + +pub struct GN; + +impl GN { + #[must_use] + #[inline(always)] + pub fn new() -> Self { + Self + } +} + +impl Sampling for GN { + fn sample(&self, n: usize, _x_0: f64, t_0: f64, t: f64) -> Array1 { + let sqrt_dt = ((t - t_0) / n as f64).sqrt(); + Array1::random(n, Normal::new(0.0, sqrt_dt).unwrap()) + } + + fn sample_as_vec(&self, n: usize, x_0: f64, t_0: f64, t: f64) -> Vec { + self.sample(n, x_0, t_0, t).to_vec() + } + + fn sample_parallel( + &self, + m: usize, + n: usize, + x_0: f64, + t_0: f64, + t: f64, + ) -> ndarray::Array2 { + let mut xs = ndarray::Array2::::zeros((m, n)); + + xs.axis_iter_mut(ndarray::Axis(0)) + .into_par_iter() + .for_each(|mut x| { + x.assign(&self.sample(n, x_0, t_0, t)); + }); + + xs + } + + fn sample_parallel_as_vec( + &self, + m: usize, + n: usize, + x_0: f64, + t_0: f64, + t: f64, + ) -> Vec> { + self + .sample_parallel(m, n, x_0, t_0, t) + .axis_iter(ndarray::Axis(0)) + .into_par_iter() + .map(|x| x.to_vec()) + .collect() + } +} + +#[cfg(feature = "f32")] +impl GN { + #[must_use] + #[inline(always)] + pub fn new_f32() -> Self { + Self + } +} + +#[cfg(feature = "f32")] +impl Sampling for GN { + fn sample(&self, n: usize, _x_0: f32, t_0: f32, t: f32) -> Array1 { + let sqrt_dt = ((t - t_0) / n as f32).sqrt(); + Array1::random(n, Normal::new(0.0, sqrt_dt).unwrap()) + } + + fn sample_as_vec(&self, n: usize, x_0: f32, t_0: f32, t: f32) -> Vec { + self.sample(n, x_0, t_0, t).to_vec() + } + + fn sample_parallel( + &self, + m: usize, + n: usize, + x_0: f32, + t_0: f32, + t: f32, + ) -> ndarray::Array2 { + let mut xs = ndarray::Array2::::zeros((m, n)); + + xs.axis_iter_mut(ndarray::Axis(0)) + .into_par_iter() + .for_each(|mut x| { + x.assign(&self.sample(n, x_0, t_0, t)); + }); + + xs + } + + fn sample_parallel_as_vec( + &self, + m: usize, + n: usize, + x_0: f32, + t_0: f32, + t: f32, + ) -> Vec> { + self + .sample_parallel(m, n, x_0, t_0, t) + .axis_iter(ndarray::Axis(0)) + .into_par_iter() + .map(|x| x.to_vec()) + .collect() + } +} From 8bb6e2e5e14cb291ea15e530d7fcf1a95d3b63f1 Mon Sep 17 00:00:00 2001 From: Daniel Boros Date: Wed, 11 Sep 2024 22:59:55 +0200 Subject: [PATCH 08/11] feat: rework core api --- .vscode/settings.json | 2 +- Cargo.toml | 51 +---- src/diffusions/cir.rs | 73 ------- src/diffusions/fcir.rs | 82 -------- src/diffusions/fgbm.rs | 70 ------- src/diffusions/fjacobi.rs | 87 -------- src/diffusions/fou.rs | 72 ------- src/diffusions/gbm.rs | 57 ------ src/diffusions/jacobi.rs | 79 -------- src/diffusions/ou.rs | 60 ------ src/noises/gn.rs | 26 --- src/noises/mod.rs | 14 -- src/processes/fbm.rs | 136 ------------- src/quant.rs | 7 - src/quant/models.rs | 0 src/quant/noises/fgn.rs | 187 ------------------ src/quant/noises/gn.rs | 118 ----------- src/quant/traits.rs | 112 ----------- src/quant/traits_f.rs | 104 ---------- src/quant/types.rs | 1 - src/utils.rs | 39 ---- stochastic-rs-core/Cargo.toml | 46 +++++ stochastic-rs-core/src/diffusions/cir.rs | 68 +++++++ stochastic-rs-core/src/diffusions/fcir.rs | 68 +++++++ stochastic-rs-core/src/diffusions/fgbm.rs | 60 ++++++ stochastic-rs-core/src/diffusions/fjacobi.rs | 74 +++++++ stochastic-rs-core/src/diffusions/fou.rs | 62 ++++++ stochastic-rs-core/src/diffusions/gbm.rs | 55 ++++++ stochastic-rs-core/src/diffusions/jacobi.rs | 70 +++++++ .../src}/diffusions/mod.rs | 0 stochastic-rs-core/src/diffusions/ou.rs | 57 ++++++ .../src}/interest/duffie_kan.rs | 0 .../src}/interest/fvasicek.rs | 0 .../src}/interest/ho_lee.rs | 0 .../src}/interest/mod.rs | 0 .../src}/interest/mvasicek.rs | 0 .../src}/interest/vasicek.rs | 0 .../src}/jumps/bates.rs | 0 {src => stochastic-rs-core/src}/jumps/ig.rs | 0 .../src}/jumps/jump_fou.rs | 1 - .../src}/jumps/levy_diffusion.rs | 0 .../src}/jumps/merton.rs | 0 {src => stochastic-rs-core/src}/jumps/mod.rs | 0 {src => stochastic-rs-core/src}/jumps/nig.rs | 0 {src => stochastic-rs-core/src}/jumps/vg.rs | 0 {src => stochastic-rs-core/src}/lib.rs | 38 +++- {src => stochastic-rs-core/src}/main.rs | 2 +- .../src}/noises/cfgns.rs | 2 +- .../src}/noises/cgns.rs | 0 {src => stochastic-rs-core/src}/noises/fgn.rs | 84 ++------ stochastic-rs-core/src/noises/mod.rs | 3 + .../src}/pricing/mod.rs | 0 .../src}/processes/bm.rs | 0 .../src}/processes/cbms.rs | 0 .../src}/processes/ccustom.rs | 0 .../src}/processes/cfbms.rs | 2 +- .../src}/processes/cpoisson.rs | 0 .../src}/processes/customjt.rs | 0 stochastic-rs-core/src/processes/fbm.rs | 76 +++++++ .../src}/processes/mod.rs | 0 .../src}/processes/poisson.rs | 0 .../src}/volatility/heston.rs | 0 .../src}/volatility/mod.rs | 0 .../src}/volatility/sabr.rs | 0 stochastic-rs-quant/Cargo.toml | 26 +++ .../src}/diffusions.rs | 0 .../src}/diffusions/bm.rs | 23 --- .../src}/diffusions/fbm.rs | 41 ---- .../src}/diffusions/fou.rs | 51 ----- .../src}/diffusions/ou.rs | 20 -- stochastic-rs-quant/src/lib.rs | 12 ++ .../src}/noises.rs | 0 stochastic-rs-quant/src/noises/fgn.rs | 89 +++++++++ stochastic-rs-quant/src/noises/gn.rs | 42 ++++ stochastic-rs-quant/src/traits.rs | 43 ++++ stochastic-rs-quant/src/traits_f.rs | 46 +++++ stochastic-rs-stats/Cargo.toml | 11 ++ .../src}/fractal_dim.rs | 0 .../mod.rs => stochastic-rs-stats/src/lib.rs | 0 79 files changed, 960 insertions(+), 1589 deletions(-) delete mode 100644 src/diffusions/cir.rs delete mode 100644 src/diffusions/fcir.rs delete mode 100644 src/diffusions/fgbm.rs delete mode 100644 src/diffusions/fjacobi.rs delete mode 100644 src/diffusions/fou.rs delete mode 100644 src/diffusions/gbm.rs delete mode 100644 src/diffusions/jacobi.rs delete mode 100644 src/diffusions/ou.rs delete mode 100644 src/noises/gn.rs delete mode 100644 src/noises/mod.rs delete mode 100644 src/processes/fbm.rs delete mode 100644 src/quant.rs delete mode 100644 src/quant/models.rs delete mode 100644 src/quant/noises/fgn.rs delete mode 100644 src/quant/noises/gn.rs delete mode 100644 src/quant/traits.rs delete mode 100644 src/quant/traits_f.rs delete mode 100644 src/quant/types.rs delete mode 100644 src/utils.rs create mode 100644 stochastic-rs-core/Cargo.toml create mode 100644 stochastic-rs-core/src/diffusions/cir.rs create mode 100644 stochastic-rs-core/src/diffusions/fcir.rs create mode 100644 stochastic-rs-core/src/diffusions/fgbm.rs create mode 100644 stochastic-rs-core/src/diffusions/fjacobi.rs create mode 100644 stochastic-rs-core/src/diffusions/fou.rs create mode 100644 stochastic-rs-core/src/diffusions/gbm.rs create mode 100644 stochastic-rs-core/src/diffusions/jacobi.rs rename {src => stochastic-rs-core/src}/diffusions/mod.rs (100%) create mode 100644 stochastic-rs-core/src/diffusions/ou.rs rename {src => stochastic-rs-core/src}/interest/duffie_kan.rs (100%) rename {src => stochastic-rs-core/src}/interest/fvasicek.rs (100%) rename {src => stochastic-rs-core/src}/interest/ho_lee.rs (100%) rename {src => stochastic-rs-core/src}/interest/mod.rs (100%) rename {src => stochastic-rs-core/src}/interest/mvasicek.rs (100%) rename {src => stochastic-rs-core/src}/interest/vasicek.rs (100%) rename {src => stochastic-rs-core/src}/jumps/bates.rs (100%) rename {src => stochastic-rs-core/src}/jumps/ig.rs (100%) rename {src => stochastic-rs-core/src}/jumps/jump_fou.rs (99%) rename {src => stochastic-rs-core/src}/jumps/levy_diffusion.rs (100%) rename {src => stochastic-rs-core/src}/jumps/merton.rs (100%) rename {src => stochastic-rs-core/src}/jumps/mod.rs (100%) rename {src => stochastic-rs-core/src}/jumps/nig.rs (100%) rename {src => stochastic-rs-core/src}/jumps/vg.rs (100%) rename {src => stochastic-rs-core/src}/lib.rs (73%) rename {src => stochastic-rs-core/src}/main.rs (89%) rename {src => stochastic-rs-core/src}/noises/cfgns.rs (96%) rename {src => stochastic-rs-core/src}/noises/cgns.rs (100%) rename {src => stochastic-rs-core/src}/noises/fgn.rs (56%) create mode 100644 stochastic-rs-core/src/noises/mod.rs rename {src => stochastic-rs-core/src}/pricing/mod.rs (100%) rename {src => stochastic-rs-core/src}/processes/bm.rs (100%) rename {src => stochastic-rs-core/src}/processes/cbms.rs (100%) rename {src => stochastic-rs-core/src}/processes/ccustom.rs (100%) rename {src => stochastic-rs-core/src}/processes/cfbms.rs (97%) rename {src => stochastic-rs-core/src}/processes/cpoisson.rs (100%) rename {src => stochastic-rs-core/src}/processes/customjt.rs (100%) create mode 100644 stochastic-rs-core/src/processes/fbm.rs rename {src => stochastic-rs-core/src}/processes/mod.rs (100%) rename {src => stochastic-rs-core/src}/processes/poisson.rs (100%) rename {src => stochastic-rs-core/src}/volatility/heston.rs (100%) rename {src => stochastic-rs-core/src}/volatility/mod.rs (100%) rename {src => stochastic-rs-core/src}/volatility/sabr.rs (100%) create mode 100644 stochastic-rs-quant/Cargo.toml rename {src/quant => stochastic-rs-quant/src}/diffusions.rs (100%) rename {src/quant => stochastic-rs-quant/src}/diffusions/bm.rs (51%) rename {src/quant => stochastic-rs-quant/src}/diffusions/fbm.rs (54%) rename {src/quant => stochastic-rs-quant/src}/diffusions/fou.rs (54%) rename {src/quant => stochastic-rs-quant/src}/diffusions/ou.rs (53%) create mode 100644 stochastic-rs-quant/src/lib.rs rename {src/quant => stochastic-rs-quant/src}/noises.rs (100%) create mode 100644 stochastic-rs-quant/src/noises/fgn.rs create mode 100644 stochastic-rs-quant/src/noises/gn.rs create mode 100644 stochastic-rs-quant/src/traits.rs create mode 100644 stochastic-rs-quant/src/traits_f.rs create mode 100644 stochastic-rs-stats/Cargo.toml rename {src/statistics => stochastic-rs-stats/src}/fractal_dim.rs (100%) rename src/statistics/mod.rs => stochastic-rs-stats/src/lib.rs (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json index ff116c2..8b13789 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1 +1 @@ -{ "rust-analyzer.cargo.features": ["f32"] } + diff --git a/Cargo.toml b/Cargo.toml index d0d5251..4f7e6e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,48 +1,3 @@ -[package] -name = "stochastic-rs" -version = "0.7.0" -edition = "2021" -license = "MIT" -description = "A Rust library for stochastic processes" -homepage = "https://github.com/dancixx/stochastic-rs" -documentation = "https://docs.rs/stochastic-rs/latest/stochastic_rs/" -repository = "https://github.com/dancixx/stochastic-rs" -readme = "README.md" -keywords = ["stochastic", "process", "random", "simulation", "monte-carlo"] - - -[dependencies] -linreg = "0.2.0" -ndarray = { version = "0.16.1", features = [ - "rayon", - "matrixmultiply-threading", -] } -num-complex = { version = "0.4.6", features = ["rand"] } -rand = "0.8.5" -rand_distr = "0.4.3" -rayon = "1.10.0" -plotly = "0.9.0" -ndarray-rand = "0.15.0" -ndrustfft = "0.5.0" -derive_builder = "0.20.1" -tikv-jemallocator = { version = "0.6.0", optional = true } -mimalloc = { version = "0.1.43", optional = true } - -[dev-dependencies] - -[features] -f32 = [] -mimalloc = ["dep:mimalloc"] -jemalloc = ["dep:tikv-jemallocator"] -default = ["jemalloc"] - -[lib] -name = "stochastic_rs" -crate-type = ["cdylib", "lib"] -path = "src/lib.rs" -doctest = false - -[profile.release] -debug = false -codegen-units = 1 -lto = true +[workspace] +members = ["stochastic-rs-core", "stochastic-rs-quant", "stochastic-rs-stats"] +resolver = "2" diff --git a/src/diffusions/cir.rs b/src/diffusions/cir.rs deleted file mode 100644 index 72e836f..0000000 --- a/src/diffusions/cir.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::noises::gn; -use derive_builder::Builder; -use ndarray::Array1; - -/// Generates a path of the Cox-Ingersoll-Ross (CIR) process. -/// -/// The CIR process is commonly used in financial mathematics to model interest rates. -/// -/// # Parameters -/// -/// - `theta`: Speed of mean reversion. -/// - `mu`: Long-term mean level. -/// - `sigma`: Volatility parameter. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// - `use_sym`: Whether to use symmetric noise (optional, defaults to false). -/// -/// # Returns -/// -/// A `Array1` representing the generated CIR process path. -/// -/// # Panics -/// -/// Panics if `2 * theta * mu < sigma^2`. -/// -/// # Example -/// -/// ``` -/// let cir_path = cir(0.5, 0.02, 0.1, 1000, Some(0.01), Some(1.0), Some(false)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct Cir { - pub theta: f64, - pub mu: f64, - pub sigma: f64, - pub n: usize, - pub x0: Option, - pub t: Option, - pub use_sym: Option, -} - -pub fn cir(params: &Cir) -> Array1 { - let Cir { - theta, - mu, - sigma, - n, - x0, - t, - use_sym, - } = *params; - - assert!(2.0 * theta * mu < sigma.powi(2), "2 * theta * mu < sigma^2"); - - let gn = gn::gn(n, Some(t.unwrap_or(1.0))); - let dt = t.unwrap_or(1.0) / n as f64; - - let mut cir = Array1::::zeros(n + 1); - cir[0] = x0.unwrap_or(0.0); - - for i in 1..(n + 1) { - let random = match use_sym.unwrap_or(false) { - true => sigma * (cir[i - 1]).abs().sqrt() * gn[i - 1], - false => sigma * (cir[i - 1]).max(0.0).sqrt() * gn[i - 1], - }; - cir[i] = cir[i - 1] + theta * (mu - cir[i - 1]) * dt + random - } - - cir -} diff --git a/src/diffusions/fcir.rs b/src/diffusions/fcir.rs deleted file mode 100644 index e28b8da..0000000 --- a/src/diffusions/fcir.rs +++ /dev/null @@ -1,82 +0,0 @@ -use derive_builder::Builder; -use ndarray::Array1; - -use crate::{noises::fgn::FgnFft, utils::Generator}; - -/// Generates a path of the fractional Cox-Ingersoll-Ross (fCIR) process. -/// -/// The fCIR process incorporates fractional Brownian motion, which introduces long-range dependence. -/// -/// # Parameters -/// -/// - `hurst`: Hurst parameter for fractional Brownian motion, must be in (0, 1). -/// - `theta`: Speed of mean reversion. -/// - `mu`: Long-term mean level. -/// - `sigma`: Volatility parameter. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// - `use_sym`: Whether to use symmetric noise (optional, defaults to false). -/// -/// # Returns -/// -/// A `Array1` representing the generated fCIR process path. -/// -/// # Panics -/// -/// Panics if `hurst` is not in (0, 1). -/// Panics if `2 * theta * mu < sigma^2`. -/// -/// # Example -/// -/// ``` -/// let fcir_path = fcir(0.75, 0.5, 0.02, 0.1, 1000, Some(0.01), Some(1.0), Some(false)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct Fcir { - pub hurst: f64, - pub theta: f64, - pub mu: f64, - pub sigma: f64, - pub n: usize, - pub x0: Option, - pub t: Option, - pub use_sym: Option, -} - -pub fn fcir(params: &Fcir) -> Array1 { - let Fcir { - hurst, - theta, - mu, - sigma, - n, - x0, - t, - use_sym, - } = *params; - - assert!( - hurst > 0.0 && hurst < 1.0, - "Hurst parameter must be in (0, 1)" - ); - assert!(2.0 * theta * mu < sigma.powi(2), "2 * theta * mu < sigma^2"); - - let fgn = FgnFft::new(hurst, n, t, None).sample(); - let dt = t.unwrap_or(1.0) / n as f64; - - let mut fcir = Array1::::zeros(n + 1); - fcir[0] = x0.unwrap_or(0.0); - - for i in 1..(n + 1) { - let random = match use_sym.unwrap_or(false) { - true => sigma * (fcir[i - 1]).abs().sqrt() * fgn[i - 1], - false => sigma * (fcir[i - 1]).max(0.0) * fgn[i - 1], - }; - fcir[i] = fcir[i - 1] + theta * (mu - fcir[i - 1]) * dt + random - } - - fcir -} diff --git a/src/diffusions/fgbm.rs b/src/diffusions/fgbm.rs deleted file mode 100644 index ed89382..0000000 --- a/src/diffusions/fgbm.rs +++ /dev/null @@ -1,70 +0,0 @@ -use derive_builder::Builder; -use ndarray::Array1; - -use crate::{noises::fgn::FgnFft, utils::Generator}; - -/// Generates a path of the fractional Geometric Brownian Motion (fGBM) process. -/// -/// The fGBM process incorporates fractional Brownian motion, which introduces long-range dependence. -/// -/// # Parameters -/// -/// - `hurst`: Hurst parameter for fractional Brownian motion, must be in (0, 1). -/// - `mu`: Drift parameter. -/// - `sigma`: Volatility parameter. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 100.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated fGBM process path. -/// -/// # Panics -/// -/// Panics if `hurst` is not in (0, 1). -/// -/// # Example -/// -/// ``` -/// let fgbm_path = fgbm(0.75, 0.05, 0.2, 1000, Some(100.0), Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct Fgbm { - pub hurst: f64, - pub mu: f64, - pub sigma: f64, - pub n: usize, - pub x0: Option, - pub t: Option, -} - -pub fn fgbm(params: &Fgbm) -> Array1 { - let Fgbm { - hurst, - mu, - sigma, - n, - x0, - t, - } = *params; - - assert!( - hurst > 0.0 && hurst < 1.0, - "Hurst parameter must be in (0, 1)" - ); - - let dt = t.unwrap_or(1.0) / n as f64; - let fgn = FgnFft::new(hurst, n, t, None).sample(); - - let mut fgbm = Array1::::zeros(n + 1); - fgbm[0] = x0.unwrap_or(100.0); - - for i in 1..(n + 1) { - fgbm[i] = fgbm[i - 1] + mu * fgbm[i - 1] * dt + sigma * fgbm[i - 1] * fgn[i - 1] - } - - fgbm -} diff --git a/src/diffusions/fjacobi.rs b/src/diffusions/fjacobi.rs deleted file mode 100644 index 400bf19..0000000 --- a/src/diffusions/fjacobi.rs +++ /dev/null @@ -1,87 +0,0 @@ -use derive_builder::Builder; -use ndarray::Array1; - -use crate::{noises::fgn::FgnFft, utils::Generator}; - -/// Generates a path of the fractional Jacobi (fJacobi) process. -/// -/// The fJacobi process incorporates fractional Brownian motion, which introduces long-range dependence. -/// -/// # Parameters -/// -/// - `hurst`: Hurst parameter for fractional Brownian motion, must be in (0, 1). -/// - `alpha`: Speed of mean reversion. -/// - `beta`: Long-term mean level. -/// - `sigma`: Volatility parameter. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated fJacobi process path. -/// -/// # Panics -/// -/// Panics if `hurst` is not in (0, 1). -/// Panics if `alpha`, `beta`, or `sigma` are not positive. -/// Panics if `alpha` is greater than `beta`. -/// -/// # Example -/// -/// ``` -/// let fjacobi_path = fjacobi(0.75, 0.5, 1.0, 0.2, 1000, Some(0.5), Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct Fjacobi { - pub hurst: f64, - pub alpha: f64, - pub beta: f64, - pub sigma: f64, - pub n: usize, - pub x0: Option, - pub t: Option, -} - -pub fn fjacobi(params: &Fjacobi) -> Array1 { - let Fjacobi { - hurst, - alpha, - beta, - sigma, - n, - x0, - t, - } = *params; - - assert!( - hurst > 0.0 && hurst < 1.0, - "Hurst parameter must be in (0, 1)" - ); - assert!(alpha > 0.0, "alpha must be positive"); - assert!(beta > 0.0, "beta must be positive"); - assert!(sigma > 0.0, "sigma must be positive"); - assert!(alpha < beta, "alpha must be less than beta"); - - let fgn = FgnFft::new(hurst, n, t, None).sample(); - let dt = t.unwrap_or(1.0) / n as f64; - - let mut fjacobi = Array1::::zeros(n + 1); - fjacobi[0] = x0.unwrap_or(0.0); - - for i in 1..(n + 1) { - fjacobi[i] = match fjacobi[i - 1] { - _ if fjacobi[i - 1] <= 0.0 && i > 0 => 0.0, - _ if fjacobi[i - 1] >= 1.0 && i > 0 => 1.0, - _ => { - fjacobi[i - 1] - + (alpha - beta * fjacobi[i - 1]) * dt - + sigma * (fjacobi[i - 1] * (1.0 - fjacobi[i - 1])).sqrt() * fgn[i - 1] - } - } - } - - fjacobi -} diff --git a/src/diffusions/fou.rs b/src/diffusions/fou.rs deleted file mode 100644 index 012f153..0000000 --- a/src/diffusions/fou.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::{noises::fgn::FgnFft, utils::Generator}; -use derive_builder::Builder; -use ndarray::Array1; - -/// Generates a path of the fractional Ornstein-Uhlenbeck (fOU) process. -/// -/// The fOU process incorporates fractional Brownian motion, which introduces long-range dependence. -/// -/// # Parameters -/// -/// - `hurst`: Hurst parameter for fractional Brownian motion, must be in (0, 1). -/// - `mu`: Long-term mean level. -/// - `sigma`: Volatility parameter. -/// - `theta`: Speed of mean reversion. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated fOU process path. -/// -/// # Panics -/// -/// Panics if `hurst` is not in (0, 1). -/// -/// # Example -/// -/// ``` -/// let fou_path = fou(0.75, 0.0, 0.1, 0.5, 1000, Some(0.0), Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct Fou { - pub hurst: f64, - pub mu: f64, - pub sigma: f64, - pub theta: f64, - pub n: usize, - pub x0: Option, - pub t: Option, -} - -pub fn fou(params: &Fou) -> Array1 { - let Fou { - hurst, - mu, - sigma, - theta, - n, - x0, - t, - } = *params; - - assert!( - hurst > 0.0 && hurst < 1.0, - "Hurst parameter must be in (0, 1)" - ); - - let fgn = FgnFft::new(hurst, n, t, None).sample(); - let dt = t.unwrap_or(1.0) / n as f64; - - let mut fou = Array1::::zeros(n + 1); - fou[0] = x0.unwrap_or(0.0); - - for i in 1..(n + 1) { - fou[i] = fou[i - 1] + theta * (mu - fou[i - 1]) * dt + sigma * fgn[i - 1] - } - - fou -} diff --git a/src/diffusions/gbm.rs b/src/diffusions/gbm.rs deleted file mode 100644 index 1ffdcc8..0000000 --- a/src/diffusions/gbm.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::noises::gn::gn; -use derive_builder::Builder; -use ndarray::Array1; - -/// Generates a path of the Geometric Brownian Motion (GBM) process. -/// -/// The GBM process is commonly used in financial mathematics to model stock prices. -/// -/// # Parameters -/// -/// - `mu`: Drift parameter. -/// - `sigma`: Volatility parameter. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 100.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated GBM process path. -/// -/// # Example -/// -/// ``` -/// let gbm_path = gbm(0.05, 0.2, 1000, Some(100.0), Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct Gbm { - pub mu: f64, - pub sigma: f64, - pub n: usize, - pub x0: Option, - pub t: Option, -} - -pub fn gbm(params: &Gbm) -> Array1 { - let Gbm { - mu, - sigma, - n, - x0, - t, - } = *params; - - let gn = gn(n, Some(t.unwrap_or(1.0))); - let dt = t.unwrap_or(1.0) / n as f64; - - let mut gbm = Array1::::zeros(n + 1); - gbm[0] = x0.unwrap_or(100.0); - - for i in 1..(n + 1) { - gbm[i] = gbm[i - 1] + mu * gbm[i - 1] * dt + sigma * gbm[i - 1] * gn[i - 1] - } - - gbm -} diff --git a/src/diffusions/jacobi.rs b/src/diffusions/jacobi.rs deleted file mode 100644 index 48b3648..0000000 --- a/src/diffusions/jacobi.rs +++ /dev/null @@ -1,79 +0,0 @@ -use derive_builder::Builder; -use ndarray::Array1; - -use crate::noises::gn; - -/// Generates a path of the Jacobi process. -/// -/// The Jacobi process is a mean-reverting process used in various fields such as finance and biology. -/// -/// # Parameters -/// -/// - `alpha`: Speed of mean reversion. -/// - `beta`: Long-term mean level. -/// - `sigma`: Volatility parameter. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated Jacobi process path. -/// -/// # Panics -/// -/// Panics if `alpha`, `beta`, or `sigma` are not positive. -/// Panics if `alpha` is greater than `beta`. -/// -/// # Example -/// -/// ``` -/// let jacobi_path = jacobi(0.5, 1.0, 0.2, 1000, Some(0.5), Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct Jacobi { - pub alpha: f64, - pub beta: f64, - pub sigma: f64, - pub n: usize, - pub x0: Option, - pub t: Option, -} - -pub fn jacobi(params: &Jacobi) -> Array1 { - let Jacobi { - alpha, - beta, - sigma, - n, - x0, - t, - } = *params; - - assert!(alpha < 0.0, "alpha must be positive"); - assert!(beta < 0.0, "beta must be positive"); - assert!(sigma < 0.0, "sigma must be positive"); - assert!(alpha < beta, "alpha must be less than beta"); - - let gn = gn::gn(n, Some(t.unwrap_or(1.0))); - let dt = t.unwrap_or(1.0) / n as f64; - - let mut jacobi = Array1::::zeros(n + 1); - jacobi[0] = x0.unwrap_or(0.0); - - for i in 1..(n + 1) { - jacobi[i] = match jacobi[i] { - _ if jacobi[i - 1] <= 0.0 && i > 0 => 0.0, - _ if jacobi[i - 1] >= 1.0 && i > 0 => 1.0, - _ => { - jacobi[i - 1] - + (alpha - beta * jacobi[i - 1]) * dt - + sigma * (jacobi[i - 1] * (1.0 - jacobi[i - 1])).sqrt() * gn[i - 1] - } - } - } - - jacobi -} diff --git a/src/diffusions/ou.rs b/src/diffusions/ou.rs deleted file mode 100644 index 3bc501f..0000000 --- a/src/diffusions/ou.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::noises::gn; -use derive_builder::Builder; -use ndarray::Array1; - -/// Generates a path of the Ornstein-Uhlenbeck (OU) process. -/// -/// The OU process is a mean-reverting stochastic process used in various fields such as finance and physics. -/// -/// # Parameters -/// -/// - `mu`: Long-term mean level. -/// - `sigma`: Volatility parameter. -/// - `theta`: Speed of mean reversion. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated OU process path. -/// -/// # Example -/// -/// ``` -/// let ou_path = ou(0.0, 0.1, 0.5, 1000, Some(0.0), Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct Ou { - pub mu: f64, - pub sigma: f64, - pub theta: f64, - pub n: usize, - pub x0: Option, - pub t: Option, -} - -pub fn ou(params: &Ou) -> Array1 { - let Ou { - mu, - sigma, - theta, - n, - x0, - t, - } = *params; - - let gn = gn::gn(n, Some(t.unwrap_or(1.0))); - let dt = t.unwrap_or(1.0) / n as f64; - - let mut ou = Array1::::zeros(n + 1); - ou[0] = x0.unwrap_or(0.0); - - for i in 1..(n + 1) { - ou[i] = ou[i - 1] + theta * (mu - ou[i - 1]) * dt + sigma * gn[i - 1] - } - - ou -} diff --git a/src/noises/gn.rs b/src/noises/gn.rs deleted file mode 100644 index 65c705e..0000000 --- a/src/noises/gn.rs +++ /dev/null @@ -1,26 +0,0 @@ -use ndarray::Array1; -use ndarray_rand::rand_distr::Normal; -use ndarray_rand::RandomExt; - -/// Generates a path of Gaussian noise (GN). -/// -/// The GN process is commonly used in simulations requiring white noise or random perturbations. -/// -/// # Parameters -/// -/// - `n`: Number of time steps. -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated Gaussian noise path. -/// -/// # Example -/// -/// ``` -/// let gn_path = gn(1000, Some(1.0)); -/// ``` -pub fn gn(n: usize, t: Option) -> Array1 { - let sqrt_dt = (t.unwrap_or(1.0) / n as f64).sqrt(); - Array1::random(n, Normal::new(0.0, sqrt_dt).unwrap()) -} diff --git a/src/noises/mod.rs b/src/noises/mod.rs deleted file mode 100644 index 5422ed3..0000000 --- a/src/noises/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! This module contains the implementations of various noise generation processes. -//! -//! The following noise generation processes are implemented: -//! -//! - **Fractional Gaussian Noise (FGN)** -//! - Generates fractional Gaussian noise using the Fast Fourier Transform (FFT) approach. -//! -//! - **Gaussian Noise (GN)** -//! - Generates Gaussian noise, commonly used in simulations requiring white noise or random perturbations. - -pub mod cfgns; -pub mod cgns; -pub mod fgn; -pub mod gn; diff --git a/src/processes/fbm.rs b/src/processes/fbm.rs deleted file mode 100644 index 690a23c..0000000 --- a/src/processes/fbm.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! Fractional Brownian Motion (fBM) generator. - -use crate::{noises::fgn::FgnFft, utils::Generator}; -use ndarray::{s, Array1, Array2, Axis}; -use rayon::prelude::*; - -/// Struct for generating Fractional Brownian Motion (fBM). -pub struct Fbm { - #[allow(dead_code)] - hurst: f64, - #[allow(dead_code)] - n: usize, - m: Option, - fgn: Option, -} - -impl Fbm { - /// Creates a new `Fbm` instance. - /// - /// # Parameters - /// - /// - `hurst`: Hurst parameter, must be between 0 and 1. - /// - `n`: Number of time steps. - /// - `t`: Total time (optional, defaults to 1.0). - /// - `m`: Number of parallel samples to generate (optional). - /// - /// # Returns - /// - /// A new `Fbm` instance. - /// - /// # Panics - /// - /// Panics if the `hurst` parameter is not between 0 and 1. - /// - /// # Example - /// - /// ``` - /// let fbm = Fbm::new(0.75, 1000, Some(1.0), Some(10)); - /// ``` - /// - pub fn new(hurst: f64, n: usize, t: Option, m: Option) -> Self { - if !(0.0..=1.0).contains(&hurst) { - panic!("Hurst parameter must be in (0, 1)") - } - - Self { - hurst, - n, - m, - fgn: Some(FgnFft::new(hurst, n - 1, t, None)), - } - } -} - -impl Generator for Fbm { - /// Generates a sample of fractional Brownian motion (fBM). - /// - /// # Returns - /// - /// A `Array1` representing the generated fBM sample. - /// - /// # Example - /// - /// ``` - /// let fbm = Fbm::new(0.75, 1000, Some(1.0), None); - /// let sample = fbm.sample(); - /// ``` - fn sample(&self) -> Array1 { - let fgn = self.fgn.as_ref().unwrap().sample(); - - let mut fbm = Array1::::zeros(self.n); - fbm.slice_mut(s![1..]).assign(&fgn); - - for i in 1..self.n { - fbm[i] += fbm[i - 1]; - } - - fbm - } - - /// Generates parallel samples of fractional Brownian motion (fBM). - /// - /// # Returns - /// - /// A `Array2>` representing the generated parallel fBM samples. - /// - /// # Panics - /// - /// Panics if `m` is not specified. - /// - /// # Example - /// - /// ``` - /// let fbm = Fbm::new(0.75, 1000, Some(1.0), Some(10)); - /// let samples = fbm.sample_par(); - /// ``` - fn sample_par(&self) -> Array2 { - if self.m.is_none() { - panic!("Number of paths must be specified") - } - - let mut xs = Array2::zeros((self.m.unwrap(), self.n)); - - xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { - x.assign(&self.sample()); - }); - - xs - } -} - -#[cfg(test)] -mod tests { - use plotly::{common::Line, Plot, Scatter}; - - use super::*; - - #[test] - fn plot() { - let fbm = Fbm::new(0.45, 1000, Some(1.0), Some(10)); - let mut plot = Plot::new(); - let d = fbm.sample_par(); - for data in d.axis_iter(Axis(0)) { - let trace = Scatter::new((0..data.len()).collect::>(), data.to_vec()) - .mode(plotly::common::Mode::Lines) - .line( - Line::new() - .color("orange") - .shape(plotly::common::LineShape::Linear), - ) - .name("Fbm"); - plot.add_trace(trace); - } - plot.show(); - } -} diff --git a/src/quant.rs b/src/quant.rs deleted file mode 100644 index fa4ba4f..0000000 --- a/src/quant.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod diffusions; -pub mod models; -pub mod noises; - -pub mod traits; -pub mod traits_f; -pub mod types; diff --git a/src/quant/models.rs b/src/quant/models.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/quant/noises/fgn.rs b/src/quant/noises/fgn.rs deleted file mode 100644 index b73a299..0000000 --- a/src/quant/noises/fgn.rs +++ /dev/null @@ -1,187 +0,0 @@ -use ndarray::{concatenate, prelude::*}; -use ndarray_rand::rand_distr::StandardNormal; -use ndarray_rand::RandomExt; -use ndrustfft::{ndfft_par, FftHandler}; -use num_complex::{Complex, ComplexDistribution}; -use rayon::prelude::*; - -use crate::quant::traits_f::SamplingF; - -#[derive(Clone)] -pub struct FGN { - hurst: T, - n: usize, - offset: usize, - t: T, - sqrt_eigenvalues: Array1>, - fft_handler: FftHandler, - fft_fgn: Array1>, -} - -impl FGN { - #[must_use] - #[inline(always)] - pub fn new(hurst: f64, n: usize, t: f64) -> Self { - if !(0.0..=1.0).contains(&hurst) { - panic!("Hurst parameter must be between 0 and 1"); - } - let n_ = n.next_power_of_two(); - let offset = n_ - n; - let n = n_; - let mut r = Array1::linspace(0.0, n as f64, n + 1); - r.par_mapv_inplace(|x| { - if x == 0.0 { - 1.0 - } else { - 0.5 - * ((x + 1.0).powf(2.0 * hurst) - 2.0 * x.powf(2.0 * hurst) + (x - 1.0).powf(2.0 * hurst)) - } - }); - let r = concatenate( - Axis(0), - #[allow(clippy::reversed_empty_ranges)] - &[r.view(), r.slice(s![..;-1]).slice(s![1..-1]).view()], - ) - .unwrap(); - let data = r.mapv(|v| Complex::new(v, 0.0)); - let r_fft = FftHandler::new(r.len()); - let mut sqrt_eigenvalues = Array1::>::zeros(r.len()); - ndfft_par(&data, &mut sqrt_eigenvalues, &r_fft, 0); - sqrt_eigenvalues.par_mapv_inplace(|x| Complex::new((x.re / (2.0 * n as f64)).sqrt(), x.im)); - - Self { - hurst, - n, - offset, - t, - sqrt_eigenvalues, - fft_handler: FftHandler::new(2 * n), - fft_fgn: Array1::>::zeros(2 * n), - } - } -} - -impl SamplingF for FGN { - fn sample(&self) -> Array1 { - let rnd = Array1::>::random( - 2 * self.n, - ComplexDistribution::new(StandardNormal, StandardNormal), - ); - let fgn = &self.sqrt_eigenvalues * &rnd; - let fft_handler = self.fft_handler.clone(); - let mut fgn_fft = self.fft_fgn.clone(); - ndfft_par(&fgn, &mut fgn_fft, &fft_handler, 0); - let fgn = fgn_fft - .slice(s![1..self.n - self.offset + 1]) - .mapv(|x: Complex| (x.re * (self.n as f64).powf(-self.hurst)) * self.t.powf(self.hurst)); - fgn - } - - fn sample_as_vec(&self) -> Vec { - self.sample().to_vec() - } - - fn sample_parallel(&self, m: usize) -> Array2 { - let mut xs = Array2::::zeros((m, self.n)); - - xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { - x.assign(&self.sample()); - }); - - xs - } - - fn sample_parallel_as_vec(&self, m: usize) -> Vec> { - self - .sample_parallel(m) - .axis_iter(Axis(0)) - .into_par_iter() - .map(|x| x.to_vec()) - .collect() - } -} - -#[cfg(feature = "f32")] -impl FGN { - #[must_use] - #[inline(always)] - pub fn new_f32(hurst: f32, n: usize, t: f32) -> Self { - if !(0.0..=1.0).contains(&hurst) { - panic!("Hurst parameter must be between 0 and 1"); - } - let n_ = n.next_power_of_two(); - let offset = n_ - n; - let n = n_; - let mut r = Array1::linspace(0.0, n as f32, n + 1); - r.par_mapv_inplace(|x| { - if x == 0.0 { - 1.0 - } else { - 0.5 - * ((x + 1.0).powf(2.0 * hurst) - 2.0 * x.powf(2.0 * hurst) + (x - 1.0).powf(2.0 * hurst)) - } - }); - let r = concatenate( - Axis(0), - #[allow(clippy::reversed_empty_ranges)] - &[r.view(), r.slice(s![..;-1]).slice(s![1..-1]).view()], - ) - .unwrap(); - let data = r.mapv(|v| Complex::new(v, 0.0)); - let r_fft = FftHandler::new(r.len()); - let mut sqrt_eigenvalues = Array1::>::zeros(r.len()); - ndfft_par(&data, &mut sqrt_eigenvalues, &r_fft, 0); - sqrt_eigenvalues.par_mapv_inplace(|x| Complex::new((x.re / (2.0 * n as f32)).sqrt(), x.im)); - - Self { - hurst, - n, - offset, - t, - sqrt_eigenvalues, - fft_handler: FftHandler::new(2 * n), - fft_fgn: Array1::>::zeros(2 * n), - } - } -} - -#[cfg(feature = "f32")] -impl SamplingF for FGN { - fn sample(&self) -> Array1 { - let rnd = Array1::>::random( - 2 * self.n, - ComplexDistribution::new(StandardNormal, StandardNormal), - ); - let fgn = &self.sqrt_eigenvalues * &rnd; - let fft_handler = self.fft_handler.clone(); - let mut fgn_fft = self.fft_fgn.clone(); - ndfft_par(&fgn, &mut fgn_fft, &fft_handler, 0); - let fgn = fgn_fft - .slice(s![1..self.n - self.offset + 1]) - .mapv(|x: Complex| (x.re * (self.n as f32).powf(-self.hurst)) * self.t.powf(self.hurst)); - fgn - } - - fn sample_as_vec(&self) -> Vec { - self.sample().to_vec() - } - - fn sample_parallel(&self, m: usize) -> Array2 { - let mut xs = Array2::::zeros((m, self.n)); - - xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { - x.assign(&self.sample()); - }); - - xs - } - - fn sample_parallel_as_vec(&self, m: usize) -> Vec> { - self - .sample_parallel(m) - .axis_iter(Axis(0)) - .into_par_iter() - .map(|x| x.to_vec()) - .collect() - } -} diff --git a/src/quant/noises/gn.rs b/src/quant/noises/gn.rs deleted file mode 100644 index 5545588..0000000 --- a/src/quant/noises/gn.rs +++ /dev/null @@ -1,118 +0,0 @@ -use ndarray::Array1; -use ndarray_rand::RandomExt; -use rand_distr::Normal; -use rayon::prelude::*; - -use crate::quant::traits::Sampling; - -pub struct GN; - -impl GN { - #[must_use] - #[inline(always)] - pub fn new() -> Self { - Self - } -} - -impl Sampling for GN { - fn sample(&self, n: usize, _x_0: f64, t_0: f64, t: f64) -> Array1 { - let sqrt_dt = ((t - t_0) / n as f64).sqrt(); - Array1::random(n, Normal::new(0.0, sqrt_dt).unwrap()) - } - - fn sample_as_vec(&self, n: usize, x_0: f64, t_0: f64, t: f64) -> Vec { - self.sample(n, x_0, t_0, t).to_vec() - } - - fn sample_parallel( - &self, - m: usize, - n: usize, - x_0: f64, - t_0: f64, - t: f64, - ) -> ndarray::Array2 { - let mut xs = ndarray::Array2::::zeros((m, n)); - - xs.axis_iter_mut(ndarray::Axis(0)) - .into_par_iter() - .for_each(|mut x| { - x.assign(&self.sample(n, x_0, t_0, t)); - }); - - xs - } - - fn sample_parallel_as_vec( - &self, - m: usize, - n: usize, - x_0: f64, - t_0: f64, - t: f64, - ) -> Vec> { - self - .sample_parallel(m, n, x_0, t_0, t) - .axis_iter(ndarray::Axis(0)) - .into_par_iter() - .map(|x| x.to_vec()) - .collect() - } -} - -#[cfg(feature = "f32")] -impl GN { - #[must_use] - #[inline(always)] - pub fn new_f32() -> Self { - Self - } -} - -#[cfg(feature = "f32")] -impl Sampling for GN { - fn sample(&self, n: usize, _x_0: f32, t_0: f32, t: f32) -> Array1 { - let sqrt_dt = ((t - t_0) / n as f32).sqrt(); - Array1::random(n, Normal::new(0.0, sqrt_dt).unwrap()) - } - - fn sample_as_vec(&self, n: usize, x_0: f32, t_0: f32, t: f32) -> Vec { - self.sample(n, x_0, t_0, t).to_vec() - } - - fn sample_parallel( - &self, - m: usize, - n: usize, - x_0: f32, - t_0: f32, - t: f32, - ) -> ndarray::Array2 { - let mut xs = ndarray::Array2::::zeros((m, n)); - - xs.axis_iter_mut(ndarray::Axis(0)) - .into_par_iter() - .for_each(|mut x| { - x.assign(&self.sample(n, x_0, t_0, t)); - }); - - xs - } - - fn sample_parallel_as_vec( - &self, - m: usize, - n: usize, - x_0: f32, - t_0: f32, - t: f32, - ) -> Vec> { - self - .sample_parallel(m, n, x_0, t_0, t) - .axis_iter(ndarray::Axis(0)) - .into_par_iter() - .map(|x| x.to_vec()) - .collect() - } -} diff --git a/src/quant/traits.rs b/src/quant/traits.rs deleted file mode 100644 index dea7fd0..0000000 --- a/src/quant/traits.rs +++ /dev/null @@ -1,112 +0,0 @@ -use ndarray::{Array1, Array2, Axis}; -use ndarray_rand::rand_distr::Normal; -use ndarray_rand::RandomExt; -use rayon::prelude::*; - -pub trait Process: Send + Sync { - fn drift(&self, x: T, t: T) -> T; - fn diffusion(&self, x: T, t: T) -> T; -} - -pub trait Sampling { - fn sample(&self, n: usize, x_0: T, t_0: T, t: T) -> Array1; - fn sample_as_vec(&self, n: usize, x_0: T, t_0: T, t: T) -> Vec; - fn sample_parallel(&self, m: usize, n: usize, x_0: T, t_0: T, t: T) -> Array2; - fn sample_parallel_as_vec(&self, m: usize, n: usize, x_0: T, t_0: T, t: T) -> Vec>; -} - -impl> Sampling for T { - fn sample(&self, n: usize, x_0: f64, t_0: f64, t: f64) -> Array1 { - let dt = (t - t_0) / n as f64; - let mut x = Array1::zeros(n); - x[0] = x_0; - let noise = Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()); - let times = Array1::linspace(t_0, t, n); - - // TODO: test idx - noise.into_iter().enumerate().for_each(|(idx, dw)| { - x[idx + 1] = - x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; - }); - - x - } - - fn sample_as_vec(&self, n: usize, x_0: f64, t_0: f64, t: f64) -> Vec { - self.sample(n, x_0, t_0, t).to_vec() - } - - fn sample_parallel(&self, m: usize, n: usize, x_0: f64, t_0: f64, t: f64) -> Array2 { - let mut xs = Array2::::zeros((m, n)); - - xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { - x.assign(&self.sample(n, x_0, t_0, t)); - }); - - xs - } - - fn sample_parallel_as_vec( - &self, - m: usize, - n: usize, - x_0: f64, - t_0: f64, - t: f64, - ) -> Vec> { - self - .sample_parallel(m, n, x_0, t_0, t) - .axis_iter(Axis(0)) - .into_par_iter() - .map(|x| x.to_vec()) - .collect() - } -} - -#[cfg(feature = "f32")] -impl> Sampling for T { - fn sample(&self, n: usize, x_0: f32, t_0: f32, t: f32) -> Array1 { - let dt = (t - t_0) / n as f32; - let mut x = Array1::zeros(n); - x[0] = x_0; - let noise = Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()); - let times = Array1::linspace(t_0, t, n); - - noise.into_iter().enumerate().for_each(|(idx, dw)| { - x[idx + 1] = - x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; - }); - - x - } - - fn sample_as_vec(&self, n: usize, x_0: f32, t_0: f32, t: f32) -> Vec { - self.sample(n, x_0, t_0, t).to_vec() - } - - fn sample_parallel(&self, m: usize, n: usize, x_0: f32, t_0: f32, t: f32) -> Array2 { - let mut xs = Array2::::zeros((m, n)); - - xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { - x.assign(&self.sample(n, x_0, t_0, t)); - }); - - xs - } - - fn sample_parallel_as_vec( - &self, - m: usize, - n: usize, - x_0: f32, - t_0: f32, - t: f32, - ) -> Vec> { - self - .sample_parallel(m, n, x_0, t_0, t) - .axis_iter(Axis(0)) - .into_par_iter() - .map(|x| x.to_vec()) - .collect() - } -} diff --git a/src/quant/traits_f.rs b/src/quant/traits_f.rs deleted file mode 100644 index d768502..0000000 --- a/src/quant/traits_f.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::quant::noises::fgn::FGN; -use ndarray::{Array1, Array2, Axis}; -use rayon::prelude::*; - -pub trait FractionalProcess: Send + Sync { - fn drift(&self, x: T, t: T) -> T; - fn diffusion(&self, x: T, t: T) -> T; - fn hurst(&self) -> T; - fn fgn(&self) -> FGN; - fn params(&self) -> (usize, T, T, T); -} - -pub trait SamplingF { - fn sample(&self) -> Array1; - fn sample_as_vec(&self) -> Vec; - fn sample_parallel(&self, m: usize) -> Array2; - fn sample_parallel_as_vec(&self, m: usize) -> Vec>; -} - -impl> SamplingF for T { - fn sample(&self) -> Array1 { - let (n, x_0, t_0, t) = self.params(); - let dt = (t - t_0) / n as f64; - let mut x = Array1::zeros(n); - x[0] = x_0; - let noise = self.fgn().sample(); - let times = Array1::linspace(t_0, t, n); - - // TODO: test idx - noise.into_iter().enumerate().for_each(|(idx, dw)| { - x[idx + 1] = - x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; - }); - - x - } - - fn sample_as_vec(&self) -> Vec { - self.sample().to_vec() - } - - fn sample_parallel(&self, m: usize) -> Array2 { - let (n, ..) = self.params(); - let mut xs = Array2::::zeros((m, n)); - - xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { - x.assign(&self.sample()); - }); - - xs - } - - fn sample_parallel_as_vec(&self, m: usize) -> Vec> { - self - .sample_parallel(m) - .axis_iter(Axis(0)) - .into_par_iter() - .map(|x| x.to_vec()) - .collect() - } -} - -#[cfg(feature = "f32")] -impl> SamplingF for T { - fn sample(&self) -> Array1 { - let (n, x_0, t_0, t) = self.params(); - let dt = (t - t_0) / n as f32; - let mut x = Array1::zeros(n); - x[0] = x_0; - let noise = self.fgn().sample(); - let times = Array1::linspace(t_0, t, n); - - noise.into_iter().enumerate().for_each(|(idx, dw)| { - x[idx + 1] = - x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; - }); - - x - } - - fn sample_as_vec(&self) -> Vec { - self.sample().to_vec() - } - - fn sample_parallel(&self, m: usize) -> Array2 { - let (n, ..) = self.params(); - let mut xs = Array2::::zeros((m, n)); - - xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { - x.assign(&self.sample()); - }); - - xs - } - - fn sample_parallel_as_vec(&self, m: usize) -> Vec> { - self - .sample_parallel(m) - .axis_iter(Axis(0)) - .into_par_iter() - .map(|x| x.to_vec()) - .collect() - } -} diff --git a/src/quant/types.rs b/src/quant/types.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/quant/types.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 3656d2e..0000000 --- a/src/utils.rs +++ /dev/null @@ -1,39 +0,0 @@ -use ndarray::{Array1, Array2}; - -/// A trait for generating stochastic process samples. -/// -/// This trait defines methods for generating single and parallel samples of stochastic processes. -/// -/// # Examples -/// -/// ``` -/// struct MyProcess; -/// -/// impl Generator for MyProcess { -/// fn sample(&self) -> Array1 { -/// vec![0.0, 1.0, 2.0] -/// } -/// -/// fn sample_par(&self) -> Array2> { -/// vec![self.sample(), self.sample()] -/// } -/// } -/// -/// let process = MyProcess; -/// let single_sample = process.sample(); -/// let parallel_samples = process.sample_par(); -/// ``` -pub trait Generator: Sync + Send { - /// Generates a single sample of the stochastic process. - /// - /// # Returns - /// - /// A `Array1` representing a single sample of the stochastic process. - fn sample(&self) -> Array1; - /// Generates parallel samples of the stochastic process. - /// - /// # Returns - /// - /// A `Array2>` where each inner vector represents a sample of the stochastic process. - fn sample_par(&self) -> Array2; -} diff --git a/stochastic-rs-core/Cargo.toml b/stochastic-rs-core/Cargo.toml new file mode 100644 index 0000000..2615728 --- /dev/null +++ b/stochastic-rs-core/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "stochastic-rs" +version = "0.7.1" +edition = "2021" +license = "MIT" +description = "A Rust library for stochastic processes" +homepage = "https://github.com/dancixx/stochastic-rs" +documentation = "https://docs.rs/stochastic-rs/latest/stochastic_rs/" +repository = "https://github.com/dancixx/stochastic-rs" +readme = "README.md" +keywords = ["stochastic", "process", "random", "simulation", "monte-carlo"] + + +[dependencies] +ndarray = { version = "0.16.1", features = [ + "rayon", + "matrixmultiply-threading", +] } +num-complex = { version = "0.4.6", features = ["rand"] } +rand = "0.8.5" +rand_distr = "0.4.3" +rayon = "1.10.0" +plotly = "0.9.0" +ndarray-rand = "0.15.0" +ndrustfft = "0.5.0" +derive_builder = "0.20.1" +tikv-jemallocator = { version = "0.6.0", optional = true } +mimalloc = { version = "0.1.43", optional = true } + +[dev-dependencies] + +[features] +mimalloc = ["dep:mimalloc"] +jemalloc = ["dep:tikv-jemallocator"] +default = ["jemalloc"] + +[lib] +name = "stochastic_rs" +crate-type = ["cdylib", "lib"] +path = "src/lib.rs" +doctest = false + +[profile.release] +debug = false +codegen-units = 1 +lto = true diff --git a/stochastic-rs-core/src/diffusions/cir.rs b/stochastic-rs-core/src/diffusions/cir.rs new file mode 100644 index 0000000..15026dc --- /dev/null +++ b/stochastic-rs-core/src/diffusions/cir.rs @@ -0,0 +1,68 @@ +use ndarray::Array1; +use ndarray_rand::RandomExt; +use rand_distr::Normal; + +use crate::Sampling; + +pub struct Cir { + pub theta: f64, + pub mu: f64, + pub sigma: f64, + pub n: usize, + pub x0: Option, + pub t: Option, + pub use_sym: Option, + pub m: Option, +} + +impl Cir { + pub fn new(params: &Self) -> Self { + Self { + theta: params.theta, + mu: params.mu, + sigma: params.sigma, + n: params.n, + x0: params.x0, + t: params.t, + use_sym: params.use_sym, + m: params.m, + } + } +} + +impl Sampling for Cir { + fn sample(&self) -> Array1 { + assert!( + 2.0 * self.theta * self.mu < self.sigma.powi(2), + "2 * theta * mu < sigma^2" + ); + + let gn = Array1::random( + self.n, + Normal::new(0.0, (self.t.unwrap_or(1.0) / self.n as f64).sqrt()).unwrap(), + ); + + let dt = self.t.unwrap_or(1.0) / self.n as f64; + + let mut cir = Array1::::zeros(self.n + 1); + cir[0] = self.x0.unwrap_or(0.0); + + for i in 1..(self.n + 1) { + let random = match self.use_sym.unwrap_or(false) { + true => self.sigma * (cir[i - 1]).abs().sqrt() * gn[i - 1], + false => self.sigma * (cir[i - 1]).max(0.0).sqrt() * gn[i - 1], + }; + cir[i] = cir[i - 1] + self.theta * (self.mu - cir[i - 1]) * dt + random + } + + cir + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } +} diff --git a/stochastic-rs-core/src/diffusions/fcir.rs b/stochastic-rs-core/src/diffusions/fcir.rs new file mode 100644 index 0000000..d430d27 --- /dev/null +++ b/stochastic-rs-core/src/diffusions/fcir.rs @@ -0,0 +1,68 @@ +use ndarray::Array1; + +use crate::{noises::fgn::Fgn, Sampling}; + +pub struct Fcir { + pub hurst: f64, + pub theta: f64, + pub mu: f64, + pub sigma: f64, + pub n: usize, + pub x0: Option, + pub t: Option, + pub use_sym: Option, + pub m: Option, + fgn: Fgn, +} + +impl Fcir { + pub fn new(params: &Self) -> Self { + let fgn = Fgn::new(params.hurst, params.n, params.t, None); + + Self { + hurst: params.hurst, + theta: params.theta, + mu: params.mu, + sigma: params.sigma, + n: params.n, + x0: params.x0, + t: params.t, + use_sym: params.use_sym, + m: params.m, + fgn, + } + } +} + +impl Sampling for Fcir { + fn sample(&self) -> Array1 { + assert!( + 2.0 * self.theta * self.mu < self.sigma.powi(2), + "2 * theta * mu < sigma^2" + ); + + let fgn = self.sample(); + let dt = self.t.unwrap_or(1.0) / self.n as f64; + + let mut fcir = Array1::::zeros(self.n + 1); + fcir[0] = self.x0.unwrap_or(0.0); + + for i in 1..(self.n + 1) { + let random = match self.use_sym.unwrap_or(false) { + true => self.sigma * (fcir[i - 1]).abs().sqrt() * fgn[i - 1], + false => self.sigma * (fcir[i - 1]).max(0.0) * fgn[i - 1], + }; + fcir[i] = fcir[i - 1] + self.theta * (self.mu - fcir[i - 1]) * dt + random + } + + fcir + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } +} diff --git a/stochastic-rs-core/src/diffusions/fgbm.rs b/stochastic-rs-core/src/diffusions/fgbm.rs new file mode 100644 index 0000000..1c165ed --- /dev/null +++ b/stochastic-rs-core/src/diffusions/fgbm.rs @@ -0,0 +1,60 @@ +use ndarray::Array1; + +use crate::{noises::fgn::Fgn, Sampling}; + +pub struct Fgbm { + pub hurst: f64, + pub mu: f64, + pub sigma: f64, + pub n: usize, + pub x0: Option, + pub t: Option, + pub m: Option, + fgn: Fgn, +} + +impl Fgbm { + pub fn new(params: &Self) -> Self { + let fgn = Fgn::new(params.hurst, params.n, params.t, None); + + Self { + hurst: params.hurst, + mu: params.mu, + sigma: params.sigma, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + fgn, + } + } +} + +impl Sampling for Fgbm { + fn sample(&self) -> Array1 { + assert!( + self.hurst > 0.0 && self.hurst < 1.0, + "Hurst parameter must be in (0, 1)" + ); + + let dt = self.t.unwrap_or(1.0) / self.n as f64; + let fgn = self.fgn.sample(); + + let mut fgbm = Array1::::zeros(self.n + 1); + fgbm[0] = self.x0.unwrap_or(100.0); + + for i in 1..(self.n + 1) { + fgbm[i] = fgbm[i - 1] + self.mu * fgbm[i - 1] * dt + self.sigma * fgbm[i - 1] * fgn[i - 1] + } + + fgbm + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } +} diff --git a/stochastic-rs-core/src/diffusions/fjacobi.rs b/stochastic-rs-core/src/diffusions/fjacobi.rs new file mode 100644 index 0000000..2aebd2b --- /dev/null +++ b/stochastic-rs-core/src/diffusions/fjacobi.rs @@ -0,0 +1,74 @@ +use ndarray::Array1; + +use crate::{noises::fgn::Fgn, Sampling}; + +pub struct Fjacobi { + pub hurst: f64, + pub alpha: f64, + pub beta: f64, + pub sigma: f64, + pub n: usize, + pub x0: Option, + pub t: Option, + pub m: Option, + fgn: Fgn, +} + +impl Fjacobi { + pub fn new(params: &Self) -> Self { + let fgn = Fgn::new(params.hurst, params.n, params.t, None); + + Self { + hurst: params.hurst, + alpha: params.alpha, + beta: params.beta, + sigma: params.sigma, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + fgn, + } + } +} + +impl Sampling for Fjacobi { + fn sample(&self) -> Array1 { + assert!( + self.hurst > 0.0 && self.hurst < 1.0, + "Hurst parameter must be in (0, 1)" + ); + assert!(self.alpha > 0.0, "alpha must be positive"); + assert!(self.beta > 0.0, "beta must be positive"); + assert!(self.sigma > 0.0, "sigma must be positive"); + assert!(self.alpha < self.beta, "alpha must be less than beta"); + + let dt = self.t.unwrap_or(1.0) / self.n as f64; + let fgn = self.fgn.sample(); + + let mut fjacobi = Array1::::zeros(self.n + 1); + fjacobi[0] = self.x0.unwrap_or(100.0); + + for i in 1..(self.n + 1) { + fjacobi[i] = match fjacobi[i - 1] { + _ if fjacobi[i - 1] <= 0.0 && i > 0 => 0.0, + _ if fjacobi[i - 1] >= 1.0 && i > 0 => 1.0, + _ => { + fjacobi[i - 1] + + (self.alpha - self.beta * fjacobi[i - 1]) * dt + + self.sigma * (fjacobi[i - 1] * (1.0 - fjacobi[i - 1])).sqrt() * fgn[i - 1] + } + } + } + + fjacobi + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } +} diff --git a/stochastic-rs-core/src/diffusions/fou.rs b/stochastic-rs-core/src/diffusions/fou.rs new file mode 100644 index 0000000..08ba2d9 --- /dev/null +++ b/stochastic-rs-core/src/diffusions/fou.rs @@ -0,0 +1,62 @@ +use ndarray::Array1; + +use crate::{noises::fgn::Fgn, Sampling}; + +pub struct Fou { + pub hurst: f64, + pub mu: f64, + pub sigma: f64, + pub theta: f64, + pub n: usize, + pub x0: Option, + pub t: Option, + pub m: Option, + fgn: Fgn, +} + +impl Fou { + pub fn new(params: &Self) -> Self { + let fgn = Fgn::new(params.hurst, params.n, params.t, None); + + Self { + hurst: params.hurst, + mu: params.mu, + sigma: params.sigma, + theta: params.theta, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + fgn, + } + } +} + +impl Sampling for Fou { + fn sample(&self) -> Array1 { + assert!( + self.hurst > 0.0 && self.hurst < 1.0, + "Hurst parameter must be in (0, 1)" + ); + + let dt = self.t.unwrap_or(1.0) / self.n as f64; + let fgn = self.fgn.sample(); + + let mut fou = Array1::::zeros(self.n + 1); + fou[0] = self.x0.unwrap_or(100.0); + + for i in 1..(self.n + 1) { + fou[i] = fou[i - 1] + self.theta * (self.mu - fou[i - 1]) * dt + self.sigma * fgn[i - 1] + } + + fou + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } +} diff --git a/stochastic-rs-core/src/diffusions/gbm.rs b/stochastic-rs-core/src/diffusions/gbm.rs new file mode 100644 index 0000000..2403fc6 --- /dev/null +++ b/stochastic-rs-core/src/diffusions/gbm.rs @@ -0,0 +1,55 @@ +use ndarray::Array1; +use ndarray_rand::RandomExt; +use rand_distr::Normal; + +use crate::Sampling; + +pub struct Gbm { + pub mu: f64, + pub sigma: f64, + pub n: usize, + pub x0: Option, + pub t: Option, + pub m: Option, +} + +impl Gbm { + pub fn new(params: &Self) -> Self { + Self { + mu: params.mu, + sigma: params.sigma, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + } + } +} + +impl Sampling for Gbm { + fn sample(&self) -> Array1 { + let gn = Array1::random( + self.n, + Normal::new(0.0, (self.t.unwrap_or(1.0) / self.n as f64).sqrt()).unwrap(), + ); + + let dt = self.t.unwrap_or(1.0) / self.n as f64; + + let mut gbm = Array1::::zeros(self.n + 1); + gbm[0] = self.x0.unwrap_or(100.0); + + for i in 1..(self.n + 1) { + gbm[i] = gbm[i - 1] + self.mu * gbm[i - 1] * dt + self.sigma * gbm[i - 1] * gn[i - 1] + } + + gbm + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } +} diff --git a/stochastic-rs-core/src/diffusions/jacobi.rs b/stochastic-rs-core/src/diffusions/jacobi.rs new file mode 100644 index 0000000..ffce457 --- /dev/null +++ b/stochastic-rs-core/src/diffusions/jacobi.rs @@ -0,0 +1,70 @@ +use ndarray::Array1; +use ndarray_rand::RandomExt; +use rand_distr::Normal; + +use crate::Sampling; + +pub struct Jacobi { + pub alpha: f64, + pub beta: f64, + pub sigma: f64, + pub n: usize, + pub x0: Option, + pub t: Option, + pub m: Option, +} + +impl Jacobi { + pub fn new(params: &Self) -> Self { + Self { + alpha: params.alpha, + beta: params.beta, + sigma: params.sigma, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + } + } +} + +impl Sampling for Jacobi { + fn sample(&self) -> Array1 { + assert!(self.alpha > 0.0, "alpha must be positive"); + assert!(self.beta > 0.0, "beta must be positive"); + assert!(self.sigma > 0.0, "sigma must be positive"); + assert!(self.alpha < self.beta, "alpha must be less than beta"); + + let gn = Array1::random( + self.n, + Normal::new(0.0, (self.t.unwrap_or(1.0) / self.n as f64).sqrt()).unwrap(), + ); + + let dt = self.t.unwrap_or(1.0) / self.n as f64; + + let mut jacobi = Array1::::zeros(self.n + 1); + jacobi[0] = self.x0.unwrap_or(0.0); + + for i in 1..(self.n + 1) { + jacobi[i] = match jacobi[i - 1] { + _ if jacobi[i - 1] <= 0.0 && i > 0 => 0.0, + _ if jacobi[i - 1] >= 1.0 && i > 0 => 1.0, + _ => { + jacobi[i - 1] + + (self.alpha - self.beta * jacobi[i - 1]) * dt + + self.sigma * (jacobi[i - 1] * (1.0 - jacobi[i - 1])).sqrt() * gn[i - 1] + } + } + } + + jacobi + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } +} diff --git a/src/diffusions/mod.rs b/stochastic-rs-core/src/diffusions/mod.rs similarity index 100% rename from src/diffusions/mod.rs rename to stochastic-rs-core/src/diffusions/mod.rs diff --git a/stochastic-rs-core/src/diffusions/ou.rs b/stochastic-rs-core/src/diffusions/ou.rs new file mode 100644 index 0000000..760acc8 --- /dev/null +++ b/stochastic-rs-core/src/diffusions/ou.rs @@ -0,0 +1,57 @@ +use ndarray::Array1; +use ndarray_rand::RandomExt; +use rand_distr::Normal; + +use crate::Sampling; + +pub struct Ou { + pub mu: f64, + pub sigma: f64, + pub theta: f64, + pub n: usize, + pub x0: Option, + pub t: Option, + pub m: Option, +} + +impl Ou { + pub fn new(params: &Self) -> Self { + Self { + mu: params.mu, + sigma: params.sigma, + theta: params.theta, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + } + } +} + +impl Sampling for Ou { + fn sample(&self) -> Array1 { + let gn = Array1::random( + self.n, + Normal::new(0.0, (self.t.unwrap_or(1.0) / self.n as f64).sqrt()).unwrap(), + ); + + let dt = self.t.unwrap_or(1.0) / self.n as f64; + + let mut ou = Array1::::zeros(self.n + 1); + ou[0] = self.x0.unwrap_or(0.0); + + for i in 1..(self.n + 1) { + ou[i] = ou[i - 1] + self.theta * (self.mu - ou[i - 1]) * dt + self.sigma * gn[i - 1] + } + + ou + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } +} diff --git a/src/interest/duffie_kan.rs b/stochastic-rs-core/src/interest/duffie_kan.rs similarity index 100% rename from src/interest/duffie_kan.rs rename to stochastic-rs-core/src/interest/duffie_kan.rs diff --git a/src/interest/fvasicek.rs b/stochastic-rs-core/src/interest/fvasicek.rs similarity index 100% rename from src/interest/fvasicek.rs rename to stochastic-rs-core/src/interest/fvasicek.rs diff --git a/src/interest/ho_lee.rs b/stochastic-rs-core/src/interest/ho_lee.rs similarity index 100% rename from src/interest/ho_lee.rs rename to stochastic-rs-core/src/interest/ho_lee.rs diff --git a/src/interest/mod.rs b/stochastic-rs-core/src/interest/mod.rs similarity index 100% rename from src/interest/mod.rs rename to stochastic-rs-core/src/interest/mod.rs diff --git a/src/interest/mvasicek.rs b/stochastic-rs-core/src/interest/mvasicek.rs similarity index 100% rename from src/interest/mvasicek.rs rename to stochastic-rs-core/src/interest/mvasicek.rs diff --git a/src/interest/vasicek.rs b/stochastic-rs-core/src/interest/vasicek.rs similarity index 100% rename from src/interest/vasicek.rs rename to stochastic-rs-core/src/interest/vasicek.rs diff --git a/src/jumps/bates.rs b/stochastic-rs-core/src/jumps/bates.rs similarity index 100% rename from src/jumps/bates.rs rename to stochastic-rs-core/src/jumps/bates.rs diff --git a/src/jumps/ig.rs b/stochastic-rs-core/src/jumps/ig.rs similarity index 100% rename from src/jumps/ig.rs rename to stochastic-rs-core/src/jumps/ig.rs diff --git a/src/jumps/jump_fou.rs b/stochastic-rs-core/src/jumps/jump_fou.rs similarity index 99% rename from src/jumps/jump_fou.rs rename to stochastic-rs-core/src/jumps/jump_fou.rs index 5e6e9dd..21cee95 100644 --- a/src/jumps/jump_fou.rs +++ b/stochastic-rs-core/src/jumps/jump_fou.rs @@ -5,7 +5,6 @@ use rand_distr::Distribution; use crate::{ noises::fgn::FgnFft, processes::cpoisson::{compound_poisson, CompoundPoissonBuilder}, - utils::Generator, }; /// Generates a path of the jump fractional Ornstein-Uhlenbeck (FOU) process. diff --git a/src/jumps/levy_diffusion.rs b/stochastic-rs-core/src/jumps/levy_diffusion.rs similarity index 100% rename from src/jumps/levy_diffusion.rs rename to stochastic-rs-core/src/jumps/levy_diffusion.rs diff --git a/src/jumps/merton.rs b/stochastic-rs-core/src/jumps/merton.rs similarity index 100% rename from src/jumps/merton.rs rename to stochastic-rs-core/src/jumps/merton.rs diff --git a/src/jumps/mod.rs b/stochastic-rs-core/src/jumps/mod.rs similarity index 100% rename from src/jumps/mod.rs rename to stochastic-rs-core/src/jumps/mod.rs diff --git a/src/jumps/nig.rs b/stochastic-rs-core/src/jumps/nig.rs similarity index 100% rename from src/jumps/nig.rs rename to stochastic-rs-core/src/jumps/nig.rs diff --git a/src/jumps/vg.rs b/stochastic-rs-core/src/jumps/vg.rs similarity index 100% rename from src/jumps/vg.rs rename to stochastic-rs-core/src/jumps/vg.rs diff --git a/src/lib.rs b/stochastic-rs-core/src/lib.rs similarity index 73% rename from src/lib.rs rename to stochastic-rs-core/src/lib.rs index 207d6fc..21a0fe2 100644 --- a/src/lib.rs +++ b/stochastic-rs-core/src/lib.rs @@ -61,14 +61,44 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -pub mod quant; - pub mod diffusions; pub mod interest; pub mod jumps; pub mod noises; pub mod pricing; pub mod processes; -pub mod statistics; -pub mod utils; pub mod volatility; + +use ndarray::parallel::prelude::*; +use ndarray::{Array1, Array2, Axis}; +use ndrustfft::Zero; + +pub trait Sampling: Send + Sync { + /// Generates a single sample of the stochastic process. + /// + /// # Returns + /// + /// A `Array1` representing a single sample of the stochastic process. + fn sample(&self) -> Array1; + /// Generates parallel samples of the stochastic process. + /// + /// # Returns + /// + /// A `Array2>` where each inner vector represents a sample of the stochastic process. + fn sample_par(&self) -> Array2 { + if self.m().is_none() { + panic!("m must be specified for parallel sampling"); + } + + let mut xs = Array2::zeros((self.m().unwrap(), self.n())); + + xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { + x.assign(&self.sample()); + }); + + xs + } + + fn n(&self) -> usize; + fn m(&self) -> Option; +} diff --git a/src/main.rs b/stochastic-rs-core/src/main.rs similarity index 89% rename from src/main.rs rename to stochastic-rs-core/src/main.rs index 16dbb49..f8fc86d 100644 --- a/src/main.rs +++ b/stochastic-rs-core/src/main.rs @@ -1,4 +1,4 @@ -use stochastic_rs::{processes::fbm::Fbm, utils::Generator}; +use stochastic_rs::processes::fbm::Fbm; fn main() { let fbm = Fbm::new(0.75, 10000, Some(1.0), None); diff --git a/src/noises/cfgns.rs b/stochastic-rs-core/src/noises/cfgns.rs similarity index 96% rename from src/noises/cfgns.rs rename to stochastic-rs-core/src/noises/cfgns.rs index 8d143aa..7b6eac8 100644 --- a/src/noises/cfgns.rs +++ b/stochastic-rs-core/src/noises/cfgns.rs @@ -1,7 +1,7 @@ use derive_builder::Builder; use ndarray::{Array1, Array2}; -use crate::{noises::fgn::FgnFft, utils::Generator}; +use crate::noises::fgn::FgnFft; /// Generates two correlated fractional Gaussian noise (fGn) paths. /// diff --git a/src/noises/cgns.rs b/stochastic-rs-core/src/noises/cgns.rs similarity index 100% rename from src/noises/cgns.rs rename to stochastic-rs-core/src/noises/cgns.rs diff --git a/src/noises/fgn.rs b/stochastic-rs-core/src/noises/fgn.rs similarity index 56% rename from src/noises/fgn.rs rename to stochastic-rs-core/src/noises/fgn.rs index 306cbd3..3d21e95 100644 --- a/src/noises/fgn.rs +++ b/stochastic-rs-core/src/noises/fgn.rs @@ -1,20 +1,14 @@ -//! Fractional Gaussian Noise (FGN) generator using FFT. -//! -//! The `FgnFft` struct provides methods to generate fractional Gaussian noise (FGN) -//! using the Fast Fourier Transform (FFT) approach. - use std::sync::Arc; -use crate::utils::Generator; -use ndarray::parallel::prelude::*; use ndarray::{concatenate, prelude::*}; use ndarray_rand::rand_distr::StandardNormal; use ndarray_rand::RandomExt; use ndrustfft::{ndfft, FftHandler}; use num_complex::{Complex, ComplexDistribution}; -/// Struct for generating Fractional Gaussian Noise (FGN) using FFT. -pub struct FgnFft { +use crate::Sampling; + +pub struct Fgn { hurst: f64, n: usize, offset: usize, @@ -24,29 +18,7 @@ pub struct FgnFft { fft_handler: Arc>, } -impl FgnFft { - /// Creates a new `FgnFft` instance. - /// - /// # Parameters - /// - /// - `hurst`: Hurst parameter, must be between 0 and 1. - /// - `n`: Number of time steps. - /// - `t`: Total time (optional, defaults to 1.0). - /// - `m`: Number of parallel samples to generate (optional). - /// - /// # Returns - /// - /// A new `FgnFft` instance. - /// - /// # Panics - /// - /// Panics if the `hurst` parameter is not between 0 and 1. - /// - /// # Example - /// - /// ``` - /// let fgn_fft = FgnFft::new(0.75, 1000, Some(1.0), Some(10)); - /// ``` +impl Fgn { pub fn new(hurst: f64, n: usize, t: Option, m: Option) -> Self { if !(0.0..=1.0).contains(&hurst) { panic!("Hurst parameter must be between 0 and 1"); @@ -87,19 +59,7 @@ impl FgnFft { } } -impl Generator for FgnFft { - /// Generates a sample of fractional Gaussian noise (FGN). - /// - /// # Returns - /// - /// A `Array1` representing the generated FGN sample. - /// - /// # Example - /// - /// ``` - /// let fgn_fft = FgnFft::new(0.75, 1000, Some(1.0), None); - /// let sample = fgn_fft.sample(); - /// ``` +impl Sampling for Fgn { fn sample(&self) -> Array1 { let rnd = Array1::>::random( 2 * self.n, @@ -115,34 +75,12 @@ impl Generator for FgnFft { fgn } - /// Generates parallel samples of fractional Gaussian noise (FGN). - /// - /// # Returns - /// - /// A `Array2>` representing the generated parallel FGN samples. - /// - /// # Panics - /// - /// Panics if `m` is not specified. - /// - /// # Example - /// - /// ``` - /// let fgn_fft = FgnFft::new(0.75, 1000, Some(1.0), Some(10)); - /// let samples = fgn_fft.sample_par(); - /// ``` - fn sample_par(&self) -> Array2 { - if self.m.is_none() { - panic!("m must be specified for parallel sampling"); - } - - let mut xs = Array2::zeros((self.m.unwrap(), self.n - self.offset)); - - xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { - x.assign(&self.sample().slice(s![..self.n - self.offset])); - }); + fn n(&self) -> usize { + self.n + } - xs + fn m(&self) -> Option { + self.m } } @@ -154,7 +92,7 @@ mod tests { #[test] fn plot() { - let fgn = FgnFft::new(0.9, 1000, Some(1.0), Some(1)); + let fgn = Fgn::new(0.9, 1000, Some(1.0), Some(1)); let mut plot = Plot::new(); let d = fgn.sample_par(); for data in d.axis_iter(Axis(0)) { diff --git a/stochastic-rs-core/src/noises/mod.rs b/stochastic-rs-core/src/noises/mod.rs new file mode 100644 index 0000000..cad6b72 --- /dev/null +++ b/stochastic-rs-core/src/noises/mod.rs @@ -0,0 +1,3 @@ +pub mod cfgns; +pub mod cgns; +pub mod fgn; diff --git a/src/pricing/mod.rs b/stochastic-rs-core/src/pricing/mod.rs similarity index 100% rename from src/pricing/mod.rs rename to stochastic-rs-core/src/pricing/mod.rs diff --git a/src/processes/bm.rs b/stochastic-rs-core/src/processes/bm.rs similarity index 100% rename from src/processes/bm.rs rename to stochastic-rs-core/src/processes/bm.rs diff --git a/src/processes/cbms.rs b/stochastic-rs-core/src/processes/cbms.rs similarity index 100% rename from src/processes/cbms.rs rename to stochastic-rs-core/src/processes/cbms.rs diff --git a/src/processes/ccustom.rs b/stochastic-rs-core/src/processes/ccustom.rs similarity index 100% rename from src/processes/ccustom.rs rename to stochastic-rs-core/src/processes/ccustom.rs diff --git a/src/processes/cfbms.rs b/stochastic-rs-core/src/processes/cfbms.rs similarity index 97% rename from src/processes/cfbms.rs rename to stochastic-rs-core/src/processes/cfbms.rs index 8dd4bfa..f9b8ffc 100644 --- a/src/processes/cfbms.rs +++ b/stochastic-rs-core/src/processes/cfbms.rs @@ -1,7 +1,7 @@ use derive_builder::Builder; use ndarray::{Array1, Array2}; -use crate::{noises::fgn::FgnFft, utils::Generator}; +use crate::noises::fgn::FgnFft; /// Generates two correlated fractional Brownian motion (fBM) paths. /// diff --git a/src/processes/cpoisson.rs b/stochastic-rs-core/src/processes/cpoisson.rs similarity index 100% rename from src/processes/cpoisson.rs rename to stochastic-rs-core/src/processes/cpoisson.rs diff --git a/src/processes/customjt.rs b/stochastic-rs-core/src/processes/customjt.rs similarity index 100% rename from src/processes/customjt.rs rename to stochastic-rs-core/src/processes/customjt.rs diff --git a/stochastic-rs-core/src/processes/fbm.rs b/stochastic-rs-core/src/processes/fbm.rs new file mode 100644 index 0000000..1f6ee6a --- /dev/null +++ b/stochastic-rs-core/src/processes/fbm.rs @@ -0,0 +1,76 @@ +use crate::{noises::fgn::Fgn, Sampling}; +use ndarray::{s, Array1}; + +pub struct Fbm { + #[allow(dead_code)] + hurst: f64, + #[allow(dead_code)] + n: usize, + m: Option, + fgn: Option, +} + +impl Fbm { + pub fn new(hurst: f64, n: usize, t: Option, m: Option) -> Self { + if !(0.0..=1.0).contains(&hurst) { + panic!("Hurst parameter must be in (0, 1)") + } + + Self { + hurst, + n, + m, + fgn: Some(Fgn::new(hurst, n - 1, t, None)), + } + } +} + +impl Sampling for Fbm { + fn sample(&self) -> Array1 { + let fgn = self.fgn.as_ref().unwrap().sample(); + + let mut fbm = Array1::::zeros(self.n); + fbm.slice_mut(s![1..]).assign(&fgn); + + for i in 1..self.n { + fbm[i] += fbm[i - 1]; + } + + fbm + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } +} + +#[cfg(test)] +mod tests { + use ndarray::Axis; + use plotly::{common::Line, Plot, Scatter}; + + use super::*; + + #[test] + fn plot() { + let fbm = Fbm::new(0.45, 1000, Some(1.0), Some(10)); + let mut plot = Plot::new(); + let d = fbm.sample_par(); + for data in d.axis_iter(Axis(0)) { + let trace = Scatter::new((0..data.len()).collect::>(), data.to_vec()) + .mode(plotly::common::Mode::Lines) + .line( + Line::new() + .color("orange") + .shape(plotly::common::LineShape::Linear), + ) + .name("Fbm"); + plot.add_trace(trace); + } + plot.show(); + } +} diff --git a/src/processes/mod.rs b/stochastic-rs-core/src/processes/mod.rs similarity index 100% rename from src/processes/mod.rs rename to stochastic-rs-core/src/processes/mod.rs diff --git a/src/processes/poisson.rs b/stochastic-rs-core/src/processes/poisson.rs similarity index 100% rename from src/processes/poisson.rs rename to stochastic-rs-core/src/processes/poisson.rs diff --git a/src/volatility/heston.rs b/stochastic-rs-core/src/volatility/heston.rs similarity index 100% rename from src/volatility/heston.rs rename to stochastic-rs-core/src/volatility/heston.rs diff --git a/src/volatility/mod.rs b/stochastic-rs-core/src/volatility/mod.rs similarity index 100% rename from src/volatility/mod.rs rename to stochastic-rs-core/src/volatility/mod.rs diff --git a/src/volatility/sabr.rs b/stochastic-rs-core/src/volatility/sabr.rs similarity index 100% rename from src/volatility/sabr.rs rename to stochastic-rs-core/src/volatility/sabr.rs diff --git a/stochastic-rs-quant/Cargo.toml b/stochastic-rs-quant/Cargo.toml new file mode 100644 index 0000000..beb2e65 --- /dev/null +++ b/stochastic-rs-quant/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "stochastic-rs-quant" +version = "0.1.0" +edition = "2021" + +[dependencies] +derive_builder = "0.20.1" +mimalloc = { version = "0.1.43", optional = true } +stochastic-rs = { path = "../stochastic-rs-core" } +tikv-jemallocator = { version = "0.6.0", optional = true } + +[features] +mimalloc = ["dep:mimalloc"] +jemalloc = ["dep:tikv-jemallocator"] +default = ["jemalloc"] + +[lib] +name = "stochastic_rs_quant" +crate-type = ["cdylib", "lib"] +path = "src/lib.rs" +doctest = false + +[profile.release] +debug = false +codegen-units = 1 +lto = true diff --git a/src/quant/diffusions.rs b/stochastic-rs-quant/src/diffusions.rs similarity index 100% rename from src/quant/diffusions.rs rename to stochastic-rs-quant/src/diffusions.rs diff --git a/src/quant/diffusions/bm.rs b/stochastic-rs-quant/src/diffusions/bm.rs similarity index 51% rename from src/quant/diffusions/bm.rs rename to stochastic-rs-quant/src/diffusions/bm.rs index cf2fe08..81f7752 100644 --- a/src/quant/diffusions/bm.rs +++ b/stochastic-rs-quant/src/diffusions/bm.rs @@ -25,26 +25,3 @@ impl Process for BM { self.sigma } } - -#[cfg(feature = "f32")] -impl BM { - #[must_use] - #[inline(always)] - pub fn new_f32() -> Self { - Self { - mu: 0.0, - sigma: 1.0, - } - } -} - -#[cfg(feature = "f32")] -impl Process for BM { - fn drift(&self, _x: f32, _t: f32) -> f32 { - self.mu - } - - fn diffusion(&self, _x: f32, _t: f32) -> f32 { - self.sigma - } -} diff --git a/src/quant/diffusions/fbm.rs b/stochastic-rs-quant/src/diffusions/fbm.rs similarity index 54% rename from src/quant/diffusions/fbm.rs rename to stochastic-rs-quant/src/diffusions/fbm.rs index 209244a..c8d84c6 100644 --- a/src/quant/diffusions/fbm.rs +++ b/stochastic-rs-quant/src/diffusions/fbm.rs @@ -49,44 +49,3 @@ impl FractionalProcess for FBM { (self.n, self.x_0, self.t_0, self.t) } } - -#[cfg(feature = "f32")] -impl FBM { - #[must_use] - #[inline(always)] - pub fn new_f32(hurst: f32, n: usize, x_0: f32, t_0: f32, t: f32) -> Self { - Self { - mu: 0.0, - sigma: 1.0, - hurst, - n, - x_0, - t_0, - t, - fgn: FGN::new_f32(hurst, n - 1, t), - } - } -} - -#[cfg(feature = "f32")] -impl FractionalProcess for FBM { - fn drift(&self, _x: f32, _t: f32) -> f32 { - self.mu - } - - fn diffusion(&self, _x: f32, _t: f32) -> f32 { - self.sigma - } - - fn hurst(&self) -> f32 { - self.hurst - } - - fn fgn(&self) -> FGN { - self.fgn.clone() - } - - fn params(&self) -> (usize, f32, f32, f32) { - (self.n, self.x_0, self.t_0, self.t) - } -} diff --git a/src/quant/diffusions/fou.rs b/stochastic-rs-quant/src/diffusions/fou.rs similarity index 54% rename from src/quant/diffusions/fou.rs rename to stochastic-rs-quant/src/diffusions/fou.rs index 068c3a5..2d9b434 100644 --- a/src/quant/diffusions/fou.rs +++ b/stochastic-rs-quant/src/diffusions/fou.rs @@ -60,54 +60,3 @@ impl FractionalProcess for FOU { (self.n, self.x_0, self.t_0, self.t) } } - -#[cfg(feature = "f32")] -impl FOU { - #[must_use] - #[inline(always)] - pub fn new_f32( - theta: f32, - mu: f32, - sigma: f32, - hurst: f32, - n: usize, - x_0: f32, - t_0: f32, - t: f32, - ) -> Self { - Self { - theta, - mu, - sigma, - hurst, - n, - x_0, - t_0, - t, - fgn: FGN::new_f32(hurst, n, t), - } - } -} - -#[cfg(feature = "f32")] -impl FractionalProcess for FOU { - fn drift(&self, x: f32, _t: f32) -> f32 { - self.theta * (self.mu - x) - } - - fn diffusion(&self, _x: f32, _t: f32) -> f32 { - self.sigma - } - - fn hurst(&self) -> f32 { - self.hurst - } - - fn fgn(&self) -> FGN { - self.fgn.clone() - } - - fn params(&self) -> (usize, f32, f32, f32) { - (self.n, self.x_0, self.t_0, self.t) - } -} diff --git a/src/quant/diffusions/ou.rs b/stochastic-rs-quant/src/diffusions/ou.rs similarity index 53% rename from src/quant/diffusions/ou.rs rename to stochastic-rs-quant/src/diffusions/ou.rs index a34b7d7..7555d80 100644 --- a/src/quant/diffusions/ou.rs +++ b/stochastic-rs-quant/src/diffusions/ou.rs @@ -23,23 +23,3 @@ impl Process for OU { self.sigma } } - -#[cfg(feature = "f32")] -impl OU { - #[must_use] - #[inline(always)] - pub fn new_f32(theta: f32, mu: f32, sigma: f32) -> Self { - Self { theta, mu, sigma } - } -} - -#[cfg(feature = "f32")] -impl Process for OU { - fn drift(&self, x: f32, _t: f32) -> f32 { - self.theta * (self.mu - x) - } - - fn diffusion(&self, _x: f32, _t: f32) -> f32 { - self.sigma - } -} diff --git a/stochastic-rs-quant/src/lib.rs b/stochastic-rs-quant/src/lib.rs new file mode 100644 index 0000000..39a0a66 --- /dev/null +++ b/stochastic-rs-quant/src/lib.rs @@ -0,0 +1,12 @@ +#[cfg(feature = "mimalloc")] +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +#[cfg(feature = "jemalloc")] +#[global_allocator] +static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; + +pub mod diffusions; +pub mod noises; +pub mod traits; +pub mod traits_f; diff --git a/src/quant/noises.rs b/stochastic-rs-quant/src/noises.rs similarity index 100% rename from src/quant/noises.rs rename to stochastic-rs-quant/src/noises.rs diff --git a/stochastic-rs-quant/src/noises/fgn.rs b/stochastic-rs-quant/src/noises/fgn.rs new file mode 100644 index 0000000..f3a06af --- /dev/null +++ b/stochastic-rs-quant/src/noises/fgn.rs @@ -0,0 +1,89 @@ +use ndarray::{concatenate, prelude::*}; +use ndarray_rand::rand_distr::StandardNormal; +use ndarray_rand::RandomExt; +use ndrustfft::{ndfft_par, FftHandler}; +use num_complex::{Complex, ComplexDistribution}; +use rayon::prelude::*; + +use crate::quant::traits_f::SamplingF; + +#[derive(Clone)] +pub struct FGN { + hurst: T, + n: usize, + offset: usize, + t: T, + sqrt_eigenvalues: Array1>, + fft_handler: FftHandler, + fft_fgn: Array1>, +} + +impl FGN { + #[must_use] + #[inline(always)] + pub fn new(hurst: f64, n: usize, t: f64) -> Self { + if !(0.0..=1.0).contains(&hurst) { + panic!("Hurst parameter must be between 0 and 1"); + } + let n_ = n.next_power_of_two(); + let offset = n_ - n; + let n = n_; + let mut r = Array1::linspace(0.0, n as f64, n + 1); + r.par_mapv_inplace(|x| { + if x == 0.0 { + 1.0 + } else { + 0.5 + * ((x + 1.0).powf(2.0 * hurst) - 2.0 * x.powf(2.0 * hurst) + (x - 1.0).powf(2.0 * hurst)) + } + }); + let r = concatenate( + Axis(0), + #[allow(clippy::reversed_empty_ranges)] + &[r.view(), r.slice(s![..;-1]).slice(s![1..-1]).view()], + ) + .unwrap(); + let data = r.mapv(|v| Complex::new(v, 0.0)); + let r_fft = FftHandler::new(r.len()); + let mut sqrt_eigenvalues = Array1::>::zeros(r.len()); + ndfft_par(&data, &mut sqrt_eigenvalues, &r_fft, 0); + sqrt_eigenvalues.par_mapv_inplace(|x| Complex::new((x.re / (2.0 * n as f64)).sqrt(), x.im)); + + Self { + hurst, + n, + offset, + t, + sqrt_eigenvalues, + fft_handler: FftHandler::new(2 * n), + fft_fgn: Array1::>::zeros(2 * n), + } + } +} + +impl SamplingF for FGN { + fn sample(&self) -> Array1 { + let rnd = Array1::>::random( + 2 * self.n, + ComplexDistribution::new(StandardNormal, StandardNormal), + ); + let fgn = &self.sqrt_eigenvalues * &rnd; + let fft_handler = self.fft_handler.clone(); + let mut fgn_fft = self.fft_fgn.clone(); + ndfft_par(&fgn, &mut fgn_fft, &fft_handler, 0); + let fgn = fgn_fft + .slice(s![1..self.n - self.offset + 1]) + .mapv(|x: Complex| (x.re * (self.n as f64).powf(-self.hurst)) * self.t.powf(self.hurst)); + fgn + } + + fn sample_parallel(&self, m: usize) -> Array2 { + let mut xs = Array2::::zeros((m, self.n)); + + xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { + x.assign(&self.sample()); + }); + + xs + } +} diff --git a/stochastic-rs-quant/src/noises/gn.rs b/stochastic-rs-quant/src/noises/gn.rs new file mode 100644 index 0000000..c379ced --- /dev/null +++ b/stochastic-rs-quant/src/noises/gn.rs @@ -0,0 +1,42 @@ +use ndarray::Array1; +use ndarray_rand::RandomExt; +use rand_distr::Normal; +use rayon::prelude::*; + +use crate::quant::traits::Sampling; + +pub struct GN; + +impl GN { + #[must_use] + #[inline(always)] + pub fn new() -> Self { + Self + } +} + +impl Sampling for GN { + fn sample(&self, n: usize, _x_0: f64, t_0: f64, t: f64) -> Array1 { + let sqrt_dt = ((t - t_0) / n as f64).sqrt(); + Array1::random(n, Normal::new(0.0, sqrt_dt).unwrap()) + } + + fn sample_parallel( + &self, + m: usize, + n: usize, + x_0: f64, + t_0: f64, + t: f64, + ) -> ndarray::Array2 { + let mut xs = ndarray::Array2::::zeros((m, n)); + + xs.axis_iter_mut(ndarray::Axis(0)) + .into_par_iter() + .for_each(|mut x| { + x.assign(&self.sample(n, x_0, t_0, t)); + }); + + xs + } +} diff --git a/stochastic-rs-quant/src/traits.rs b/stochastic-rs-quant/src/traits.rs new file mode 100644 index 0000000..ebcd22a --- /dev/null +++ b/stochastic-rs-quant/src/traits.rs @@ -0,0 +1,43 @@ +use ndarray::{Array1, Array2, Axis}; +use ndarray_rand::rand_distr::Normal; +use ndarray_rand::RandomExt; +use rayon::prelude::*; + +pub trait Process: Send + Sync { + fn drift(&self, x: T, t: T) -> T; + fn diffusion(&self, x: T, t: T) -> T; + fn jump() {} +} + +pub trait Sampling { + fn sample(&self, n: usize, x_0: T, t_0: T, t: T) -> Array1; + fn sample_parallel(&self, m: usize, n: usize, x_0: T, t_0: T, t: T) -> Array2; +} + +impl> Sampling for T { + fn sample(&self, n: usize, x_0: f64, t_0: f64, t: f64) -> Array1 { + let dt = (t - t_0) / n as f64; + let mut x = Array1::zeros(n); + x[0] = x_0; + let noise = Array1::random(n - 1, Normal::new(0.0, dt.sqrt()).unwrap()); + let times = Array1::linspace(t_0, t, n); + + // TODO: test idx + noise.into_iter().enumerate().for_each(|(idx, dw)| { + x[idx + 1] = + x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; + }); + + x + } + + fn sample_parallel(&self, m: usize, n: usize, x_0: f64, t_0: f64, t: f64) -> Array2 { + let mut xs = Array2::::zeros((m, n)); + + xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { + x.assign(&self.sample(n, x_0, t_0, t)); + }); + + xs + } +} diff --git a/stochastic-rs-quant/src/traits_f.rs b/stochastic-rs-quant/src/traits_f.rs new file mode 100644 index 0000000..2ae7388 --- /dev/null +++ b/stochastic-rs-quant/src/traits_f.rs @@ -0,0 +1,46 @@ +use crate::quant::noises::fgn::FGN; +use ndarray::{Array1, Array2, Axis}; +use rayon::prelude::*; + +pub trait FractionalProcess: Send + Sync { + fn drift(&self, x: T, t: T) -> T; + fn diffusion(&self, x: T, t: T) -> T; + fn hurst(&self) -> T; + fn fgn(&self) -> FGN; + fn params(&self) -> (usize, T, T, T); +} + +pub trait SamplingF { + fn sample(&self) -> Array1; + fn sample_parallel(&self, m: usize) -> Array2; +} + +impl> SamplingF for T { + fn sample(&self) -> Array1 { + let (n, x_0, t_0, t) = self.params(); + let dt = (t - t_0) / n as f64; + let mut x = Array1::zeros(n); + x[0] = x_0; + let noise = self.fgn().sample(); + let times = Array1::linspace(t_0, t, n); + + // TODO: test idx + noise.into_iter().enumerate().for_each(|(idx, dw)| { + x[idx + 1] = + x[idx] + self.drift(x[idx], times[idx]) * dt + self.diffusion(x[idx], times[idx]) * dw; + }); + + x + } + + fn sample_parallel(&self, m: usize) -> Array2 { + let (n, ..) = self.params(); + let mut xs = Array2::::zeros((m, n)); + + xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { + x.assign(&self.sample()); + }); + + xs + } +} diff --git a/stochastic-rs-stats/Cargo.toml b/stochastic-rs-stats/Cargo.toml new file mode 100644 index 0000000..2867872 --- /dev/null +++ b/stochastic-rs-stats/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "stochastic-rs-stats" +version = "0.1.0" +edition = "2021" + +[dependencies] +linreg = "0.2.0" +ndarray = { version = "0.16.1", features = [ + "rayon", + "matrixmultiply-threading", +] } \ No newline at end of file diff --git a/src/statistics/fractal_dim.rs b/stochastic-rs-stats/src/fractal_dim.rs similarity index 100% rename from src/statistics/fractal_dim.rs rename to stochastic-rs-stats/src/fractal_dim.rs diff --git a/src/statistics/mod.rs b/stochastic-rs-stats/src/lib.rs similarity index 100% rename from src/statistics/mod.rs rename to stochastic-rs-stats/src/lib.rs From 4fd66738eb00e40d264ae5c65e928a4b7e7d1983 Mon Sep 17 00:00:00 2001 From: Daniel Boros Date: Thu, 12 Sep 2024 00:23:47 +0200 Subject: [PATCH 09/11] refactor: volatility and processes --- stochastic-rs-core/Cargo.toml | 1 - stochastic-rs-core/src/diffusions/cir.rs | 2 + stochastic-rs-core/src/diffusions/fcir.rs | 2 + stochastic-rs-core/src/diffusions/fgbm.rs | 4 +- stochastic-rs-core/src/diffusions/fjacobi.rs | 4 +- stochastic-rs-core/src/diffusions/fou.rs | 4 +- stochastic-rs-core/src/diffusions/gbm.rs | 4 +- stochastic-rs-core/src/diffusions/jacobi.rs | 2 + stochastic-rs-core/src/diffusions/mod.rs | 30 ---- stochastic-rs-core/src/diffusions/ou.rs | 2 + stochastic-rs-core/src/interest/duffie_kan.rs | 131 ++++++++---------- stochastic-rs-core/src/interest/fvasicek.rs | 95 ++++++------- stochastic-rs-core/src/interest/ho_lee.rs | 81 +++++++---- stochastic-rs-core/src/interest/mod.rs | 4 - stochastic-rs-core/src/interest/vasicek.rs | 89 ++++++------ stochastic-rs-core/src/lib.rs | 28 ++-- stochastic-rs-core/src/main.rs | 34 ++--- stochastic-rs-core/src/noises/cfgns.rs | 100 +++++++------ stochastic-rs-core/src/noises/cgns.rs | 94 ++++++------- stochastic-rs-core/src/noises/fgn.rs | 6 + stochastic-rs-core/src/pricing/mod.rs | 0 stochastic-rs-core/src/processes/bm.rs | 74 +++++----- stochastic-rs-core/src/processes/cbms.rs | 97 ++++++------- stochastic-rs-core/src/processes/ccustom.rs | 78 ++++++++--- stochastic-rs-core/src/processes/cfbms.rs | 114 ++++++++------- stochastic-rs-core/src/processes/cpoisson.rs | 70 +++++++--- stochastic-rs-core/src/processes/customjt.rs | 73 ++++++---- stochastic-rs-core/src/processes/fbm.rs | 40 ++++-- stochastic-rs-core/src/processes/mod.rs | 23 --- stochastic-rs-core/src/processes/poisson.rs | 89 ++++++------ stochastic-rs-core/src/volatility/heston.rs | 114 ++++++++------- stochastic-rs-core/src/volatility/sabr.rs | 96 ++++++------- stochastic-rs-stats/src/fractal_dim.rs | 25 ---- 33 files changed, 824 insertions(+), 786 deletions(-) delete mode 100644 stochastic-rs-core/src/pricing/mod.rs diff --git a/stochastic-rs-core/Cargo.toml b/stochastic-rs-core/Cargo.toml index 2615728..d090368 100644 --- a/stochastic-rs-core/Cargo.toml +++ b/stochastic-rs-core/Cargo.toml @@ -23,7 +23,6 @@ rayon = "1.10.0" plotly = "0.9.0" ndarray-rand = "0.15.0" ndrustfft = "0.5.0" -derive_builder = "0.20.1" tikv-jemallocator = { version = "0.6.0", optional = true } mimalloc = { version = "0.1.43", optional = true } diff --git a/stochastic-rs-core/src/diffusions/cir.rs b/stochastic-rs-core/src/diffusions/cir.rs index 15026dc..a232590 100644 --- a/stochastic-rs-core/src/diffusions/cir.rs +++ b/stochastic-rs-core/src/diffusions/cir.rs @@ -4,6 +4,7 @@ use rand_distr::Normal; use crate::Sampling; +#[derive(Default)] pub struct Cir { pub theta: f64, pub mu: f64, @@ -16,6 +17,7 @@ pub struct Cir { } impl Cir { + #[must_use] pub fn new(params: &Self) -> Self { Self { theta: params.theta, diff --git a/stochastic-rs-core/src/diffusions/fcir.rs b/stochastic-rs-core/src/diffusions/fcir.rs index d430d27..dfd45dc 100644 --- a/stochastic-rs-core/src/diffusions/fcir.rs +++ b/stochastic-rs-core/src/diffusions/fcir.rs @@ -2,6 +2,7 @@ use ndarray::Array1; use crate::{noises::fgn::Fgn, Sampling}; +#[derive(Default)] pub struct Fcir { pub hurst: f64, pub theta: f64, @@ -16,6 +17,7 @@ pub struct Fcir { } impl Fcir { + #[must_use] pub fn new(params: &Self) -> Self { let fgn = Fgn::new(params.hurst, params.n, params.t, None); diff --git a/stochastic-rs-core/src/diffusions/fgbm.rs b/stochastic-rs-core/src/diffusions/fgbm.rs index 1c165ed..8cd51f3 100644 --- a/stochastic-rs-core/src/diffusions/fgbm.rs +++ b/stochastic-rs-core/src/diffusions/fgbm.rs @@ -2,6 +2,7 @@ use ndarray::Array1; use crate::{noises::fgn::Fgn, Sampling}; +#[derive(Default)] pub struct Fgbm { pub hurst: f64, pub mu: f64, @@ -14,6 +15,7 @@ pub struct Fgbm { } impl Fgbm { + #[must_use] pub fn new(params: &Self) -> Self { let fgn = Fgn::new(params.hurst, params.n, params.t, None); @@ -41,7 +43,7 @@ impl Sampling for Fgbm { let fgn = self.fgn.sample(); let mut fgbm = Array1::::zeros(self.n + 1); - fgbm[0] = self.x0.unwrap_or(100.0); + fgbm[0] = self.x0.unwrap_or(0.0); for i in 1..(self.n + 1) { fgbm[i] = fgbm[i - 1] + self.mu * fgbm[i - 1] * dt + self.sigma * fgbm[i - 1] * fgn[i - 1] diff --git a/stochastic-rs-core/src/diffusions/fjacobi.rs b/stochastic-rs-core/src/diffusions/fjacobi.rs index 2aebd2b..b89297e 100644 --- a/stochastic-rs-core/src/diffusions/fjacobi.rs +++ b/stochastic-rs-core/src/diffusions/fjacobi.rs @@ -2,6 +2,7 @@ use ndarray::Array1; use crate::{noises::fgn::Fgn, Sampling}; +#[derive(Default)] pub struct Fjacobi { pub hurst: f64, pub alpha: f64, @@ -15,6 +16,7 @@ pub struct Fjacobi { } impl Fjacobi { + #[must_use] pub fn new(params: &Self) -> Self { let fgn = Fgn::new(params.hurst, params.n, params.t, None); @@ -47,7 +49,7 @@ impl Sampling for Fjacobi { let fgn = self.fgn.sample(); let mut fjacobi = Array1::::zeros(self.n + 1); - fjacobi[0] = self.x0.unwrap_or(100.0); + fjacobi[0] = self.x0.unwrap_or(0.0); for i in 1..(self.n + 1) { fjacobi[i] = match fjacobi[i - 1] { diff --git a/stochastic-rs-core/src/diffusions/fou.rs b/stochastic-rs-core/src/diffusions/fou.rs index 08ba2d9..269a95b 100644 --- a/stochastic-rs-core/src/diffusions/fou.rs +++ b/stochastic-rs-core/src/diffusions/fou.rs @@ -2,6 +2,7 @@ use ndarray::Array1; use crate::{noises::fgn::Fgn, Sampling}; +#[derive(Default)] pub struct Fou { pub hurst: f64, pub mu: f64, @@ -15,6 +16,7 @@ pub struct Fou { } impl Fou { + #[must_use] pub fn new(params: &Self) -> Self { let fgn = Fgn::new(params.hurst, params.n, params.t, None); @@ -43,7 +45,7 @@ impl Sampling for Fou { let fgn = self.fgn.sample(); let mut fou = Array1::::zeros(self.n + 1); - fou[0] = self.x0.unwrap_or(100.0); + fou[0] = self.x0.unwrap_or(0.0); for i in 1..(self.n + 1) { fou[i] = fou[i - 1] + self.theta * (self.mu - fou[i - 1]) * dt + self.sigma * fgn[i - 1] diff --git a/stochastic-rs-core/src/diffusions/gbm.rs b/stochastic-rs-core/src/diffusions/gbm.rs index 2403fc6..9b4b757 100644 --- a/stochastic-rs-core/src/diffusions/gbm.rs +++ b/stochastic-rs-core/src/diffusions/gbm.rs @@ -4,6 +4,7 @@ use rand_distr::Normal; use crate::Sampling; +#[derive(Default)] pub struct Gbm { pub mu: f64, pub sigma: f64, @@ -14,6 +15,7 @@ pub struct Gbm { } impl Gbm { + #[must_use] pub fn new(params: &Self) -> Self { Self { mu: params.mu, @@ -36,7 +38,7 @@ impl Sampling for Gbm { let dt = self.t.unwrap_or(1.0) / self.n as f64; let mut gbm = Array1::::zeros(self.n + 1); - gbm[0] = self.x0.unwrap_or(100.0); + gbm[0] = self.x0.unwrap_or(0.0); for i in 1..(self.n + 1) { gbm[i] = gbm[i - 1] + self.mu * gbm[i - 1] * dt + self.sigma * gbm[i - 1] * gn[i - 1] diff --git a/stochastic-rs-core/src/diffusions/jacobi.rs b/stochastic-rs-core/src/diffusions/jacobi.rs index ffce457..1fd0962 100644 --- a/stochastic-rs-core/src/diffusions/jacobi.rs +++ b/stochastic-rs-core/src/diffusions/jacobi.rs @@ -4,6 +4,7 @@ use rand_distr::Normal; use crate::Sampling; +#[derive(Default)] pub struct Jacobi { pub alpha: f64, pub beta: f64, @@ -15,6 +16,7 @@ pub struct Jacobi { } impl Jacobi { + #[must_use] pub fn new(params: &Self) -> Self { Self { alpha: params.alpha, diff --git a/stochastic-rs-core/src/diffusions/mod.rs b/stochastic-rs-core/src/diffusions/mod.rs index 07d2d5b..ff334ca 100644 --- a/stochastic-rs-core/src/diffusions/mod.rs +++ b/stochastic-rs-core/src/diffusions/mod.rs @@ -1,33 +1,3 @@ -//! This module contains the implementations of various diffusion processes. -//! -//! The following diffusion processes are implemented: -//! -//! - **Cox-Ingersoll-Ross (CIR)** -//! - SDE: `dX(t) = theta(mu - X(t))dt + sigma sqrt(X(t))dW(t)` -//! -//! - **Fractional Cox-Ingersoll-Ross (fCIR)** -//! - SDE: `dX(t) = theta(mu - X(t))dt + sigma sqrt(X(t))dW^H(t)` -//! -//! - **Geometric Brownian Motion (GBM)** -//! - SDE: `dX(t) = mu X(t)dt + sigma X(t)dW(t)` -//! -//! - **Fractional Geometric Brownian Motion (fGBM)** -//! - SDE: `dX(t) = mu X(t)dt + sigma X(t)dW^H(t)` -//! -//! - **Jacobi Process** -//! - SDE: `dX(t) = (alpha - beta X(t))dt + sigma (X(t)(1 - X(t)))^(1/2)dW(t)` -//! -//! - **Fractional Jacobi Process** -//! - SDE: `dX(t) = (alpha - beta X(t))dt + sigma (X(t)(1 - X(t)))^(1/2)dW^H(t)` -//! -//! - **Ornstein-Uhlenbeck (OU)** -//! - SDE: `dX(t) = theta(mu - X(t))dt + sigma dW(t)` -//! -//! - **Fractional Ornstein-Uhlenbeck (fOU)** -//! - SDE: `dX(t) = theta(mu - X(t))dt + sigma dW^H(t)` -//! -//! Each process has its own module and functions to generate sample paths. - pub mod cir; pub mod fcir; pub mod fgbm; diff --git a/stochastic-rs-core/src/diffusions/ou.rs b/stochastic-rs-core/src/diffusions/ou.rs index 760acc8..ed059e5 100644 --- a/stochastic-rs-core/src/diffusions/ou.rs +++ b/stochastic-rs-core/src/diffusions/ou.rs @@ -4,6 +4,7 @@ use rand_distr::Normal; use crate::Sampling; +#[derive(Default)] pub struct Ou { pub mu: f64, pub sigma: f64, @@ -15,6 +16,7 @@ pub struct Ou { } impl Ou { + #[must_use] pub fn new(params: &Self) -> Self { Self { mu: params.mu, diff --git a/stochastic-rs-core/src/interest/duffie_kan.rs b/stochastic-rs-core/src/interest/duffie_kan.rs index acbd420..6b9232c 100644 --- a/stochastic-rs-core/src/interest/duffie_kan.rs +++ b/stochastic-rs-core/src/interest/duffie_kan.rs @@ -1,44 +1,9 @@ -use derive_builder::Builder; use ndarray::Array1; -use crate::noises::cgns::{cgns, Cgns}; +use crate::{noises::cgns::Cgns, Sampling2D}; -/// Generates paths for the Duffie-Kan multifactor interest rate model. -/// -/// The Duffie-Kan model is a multifactor interest rate model incorporating correlated Brownian motions, -/// used in financial mathematics for modeling interest rates. -/// -/// # Parameters -/// -/// - `alpha`: The drift term coefficient for the Brownian motion. -/// - `beta`: The drift term coefficient for the Brownian motion. -/// - `gamma`: The drift term coefficient for the Brownian motion. -/// - `rho`: The correlation between the two Brownian motions. -/// - `a1`: The coefficient for the `r` term in the drift of `r`. -/// - `b1`: The coefficient for the `x` term in the drift of `r`. -/// - `c1`: The constant term in the drift of `r`. -/// - `sigma1`: The diffusion coefficient for the `r` process. -/// - `a2`: The coefficient for the `r` term in the drift of `x`. -/// - `b2`: The coefficient for the `x` term in the drift of `x`. -/// - `c2`: The constant term in the drift of `x`. -/// - `sigma2`: The diffusion coefficient for the `x` process. -/// - `n`: Number of time steps. -/// - `r0`: Initial value of the `r` process (optional, defaults to 0.0). -/// - `x0`: Initial value of the `x` process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A tuple of two `Array1` representing the generated paths for the `r` and `x` processes. -/// -/// # Example -/// -/// ``` -/// let (r_path, x_path) = duffie_kan(0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1000, Some(0.05), Some(0.02), Some(1.0)); -/// ``` +#[derive(Default)] -#[derive(Default, Builder)] -#[builder(setter(into))] pub struct DuffieKan { pub alpha: f64, pub beta: f64, @@ -56,45 +21,71 @@ pub struct DuffieKan { pub r0: Option, pub x0: Option, pub t: Option, + pub m: Option, + cgns: Cgns, } -pub fn duffie_kan(params: &DuffieKan) -> [Array1; 2] { - let DuffieKan { - alpha, - beta, - gamma, - rho, - a1, - b1, - c1, - sigma1, - a2, - b2, - c2, - sigma2, - n, - r0, - x0, - t, - } = *params; +impl DuffieKan { + #[must_use] + pub fn new(params: &Self) -> Self { + let cgns = Cgns::new(&Cgns { + rho: params.rho, + n: params.n, + t: params.t, + m: params.m, + }); - let [cgn1, cgn2] = cgns(&Cgns { rho, n, t }); - let dt = t.unwrap_or(1.0) / n as f64; + Self { + alpha: params.alpha, + beta: params.beta, + gamma: params.gamma, + rho: params.rho, + a1: params.a1, + b1: params.b1, + c1: params.c1, + sigma1: params.sigma1, + a2: params.a2, + b2: params.b2, + c2: params.c2, + sigma2: params.sigma2, + n: params.n, + r0: params.r0, + x0: params.x0, + t: params.t, + m: params.m, + cgns, + } + } +} + +impl Sampling2D for DuffieKan { + fn sample(&self) -> [Array1; 2] { + let [cgn1, cgn2] = self.cgns.sample(); + let dt = self.t.unwrap_or(1.0) / self.n as f64; + + let mut r = Array1::::zeros(self.n + 1); + let mut x = Array1::::zeros(self.n + 1); - let mut r = Array1::::zeros(n + 1); - let mut x = Array1::::zeros(n + 1); + r[0] = self.r0.unwrap_or(0.0); + x[0] = self.x0.unwrap_or(0.0); - r[0] = r0.unwrap_or(0.0); - x[0] = x0.unwrap_or(0.0); + for i in 1..(self.n + 1) { + r[i] = r[i - 1] + + (self.a1 * r[i - 1] + self.b1 * x[i - 1] + self.c1) * dt + + self.sigma1 * (self.alpha * r[i - 1] + self.beta * x[i - 1] + self.gamma) * cgn1[i - 1]; + x[i] = x[i - 1] + + (self.a2 * r[i - 1] + self.b2 * x[i - 1] + self.c2) * dt + + self.sigma2 * (self.alpha * r[i - 1] + self.beta * x[i - 1] + self.gamma) * cgn2[i - 1]; + } - for i in 1..(n + 1) { - r[i] = r[i - 1] - + (a1 * r[i - 1] + b1 * x[i - 1] + c1) * dt - + sigma1 * (alpha * r[i - 1] + beta * x[i - 1] + gamma) * cgn1[i - 1]; - x[i] = x[i - 1] - + (a2 * r[i - 1] + b2 * x[i - 1] + c2) * dt - + sigma2 * (alpha * r[i - 1] + beta * x[i - 1] + gamma) * cgn2[i - 1]; + [r, x] } - [r, x] + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/interest/fvasicek.rs b/stochastic-rs-core/src/interest/fvasicek.rs index ba41028..b3a8e9a 100644 --- a/stochastic-rs-core/src/interest/fvasicek.rs +++ b/stochastic-rs-core/src/interest/fvasicek.rs @@ -1,38 +1,8 @@ -use derive_builder::Builder; use ndarray::Array1; -use crate::{diffusions::fou::fou, diffusions::fou::Fou}; +use crate::{diffusions::fou::Fou, Sampling}; -/// Generates a path of the fractional Vasicek (fVasicek) model. -/// -/// The fVasicek model incorporates fractional Brownian motion into the Vasicek model. -/// -/// # Parameters -/// -/// - `hurst`: Hurst parameter for fractional Brownian motion, must be in (0, 1). -/// - `mu`: Long-term mean level, must be non-zero. -/// - `sigma`: Volatility parameter. -/// - `theta`: Speed of mean reversion. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated fVasicek process path. -/// -/// # Panics -/// -/// Panics if `mu` is zero. -/// -/// # Example -/// -/// ``` -/// let fvasicek_path = fvasicek(0.75, 0.1, 0.02, 0.3, 1000, Some(0.0), Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] +#[derive(Default)] pub struct Fvasicek { pub hurst: f64, pub mu: f64, @@ -41,28 +11,49 @@ pub struct Fvasicek { pub n: usize, pub x0: Option, pub t: Option, + pub m: Option, + fou: Fou, +} + +impl Fvasicek { + #[must_use] + pub fn new(params: &Self) -> Self { + let fou = Fou::new(&Fou { + hurst: params.hurst, + mu: params.mu, + sigma: params.sigma, + theta: params.theta.unwrap_or(1.0), + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + ..Default::default() + }); + + Self { + hurst: params.hurst, + mu: params.mu, + sigma: params.sigma, + theta: params.theta, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + fou, + } + } } -pub fn fvasicek(params: &Fvasicek) -> Array1 { - let Fvasicek { - hurst, - mu, - sigma, - theta, - n, - x0, - t, - } = *params; +impl Sampling for Fvasicek { + fn sample(&self) -> Array1 { + self.fou.sample() + } - assert!(mu != 0.0, "mu must be non-zero"); + fn n(&self) -> usize { + self.n + } - fou(&Fou { - hurst, - mu, - sigma, - theta: theta.unwrap_or(1.0), - n, - x0, - t, - }) + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/interest/ho_lee.rs b/stochastic-rs-core/src/interest/ho_lee.rs index e449da3..b800f4d 100644 --- a/stochastic-rs-core/src/interest/ho_lee.rs +++ b/stochastic-rs-core/src/interest/ho_lee.rs @@ -1,6 +1,10 @@ +use std::sync::Arc; + use ndarray::Array1; +use ndarray_rand::RandomExt; +use rand_distr::Normal; -use crate::noises::gn; +use crate::Sampling; #[allow(non_snake_case)] #[derive(Default)] @@ -8,39 +12,60 @@ pub struct HoLee<'a> where 'a: 'static, { - pub f_T: Option f64 + Send + Sync + 'a>>, + pub f_T: Option f64 + Send + Sync + 'a>>, pub theta: Option, pub sigma: f64, pub n: usize, pub t: f64, + pub m: Option, } -pub fn ho_lee(params: &HoLee) -> Array1 { - let HoLee { - f_T, - theta, - sigma, - n, - t, - } = params; - assert!( - theta.is_none() && f_T.is_none(), - "theta or f_T must be provided" - ); - let dt = *t / *n as f64; - let gn = gn::gn(*n, Some(*t)); - - let mut r = Array1::::zeros(*n + 1); - - for i in 1..(*n + 1) { - let drift = if let Some(r#fn) = f_T { - (r#fn)(i as f64 * dt) + sigma.powf(2.0) - } else { - theta.unwrap() + sigma.powf(2.0) - }; - - r[i] = r[i - 1] + drift * dt + sigma * gn[i - 1]; +impl<'a> HoLee<'a> { + #[must_use] + pub fn new(params: &Self) -> Self { + Self { + f_T: params.f_T.clone(), + theta: params.theta, + sigma: params.sigma, + n: params.n, + t: params.t, + m: params.m, + } } +} + +impl<'a> Sampling for HoLee<'a> { + fn sample(&self) -> Array1 { + assert!( + self.theta.is_none() && self.f_T.is_none(), + "theta or f_T must be provided" + ); + let dt = self.t / self.n as f64; + let gn = Array1::random( + self.n, + Normal::new(0.0, (self.t / self.n as f64).sqrt()).unwrap(), + ); + + let mut r = Array1::::zeros(self.n + 1); + + for i in 1..(self.n + 1) { + let drift = if let Some(r#fn) = self.f_T.as_ref() { + (r#fn)(i as f64 * dt) + self.sigma.powf(2.0) + } else { + self.theta.unwrap() + self.sigma.powf(2.0) + }; + + r[i] = r[i - 1] + drift * dt + self.sigma * gn[i - 1]; + } - r + r + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/interest/mod.rs b/stochastic-rs-core/src/interest/mod.rs index e15fafb..5adfc7c 100644 --- a/stochastic-rs-core/src/interest/mod.rs +++ b/stochastic-rs-core/src/interest/mod.rs @@ -1,8 +1,4 @@ pub mod duffie_kan; pub mod fvasicek; pub mod ho_lee; -pub mod mvasicek; pub mod vasicek; - -pub use crate::diffusions::cir::cir; -pub use crate::diffusions::fcir::fcir; diff --git a/stochastic-rs-core/src/interest/vasicek.rs b/stochastic-rs-core/src/interest/vasicek.rs index 5cf4c55..199f3fe 100644 --- a/stochastic-rs-core/src/interest/vasicek.rs +++ b/stochastic-rs-core/src/interest/vasicek.rs @@ -1,37 +1,8 @@ -use derive_builder::Builder; use ndarray::Array1; -use crate::{diffusions::ou::ou, diffusions::ou::Ou}; +use crate::{diffusions::ou::Ou, Sampling}; -/// Generates a path of the Vasicek model. -/// -/// The Vasicek model is a type of Ornstein-Uhlenbeck process used to model interest rates. -/// -/// # Parameters -/// -/// - `mu`: Long-term mean level, must be non-zero. -/// - `sigma`: Volatility parameter. -/// - `theta`: Speed of mean reversion. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated Vasicek process path. -/// -/// # Panics -/// -/// Panics if `mu` is zero. -/// -/// # Example -/// -/// ``` -/// let vasicek_path = vasicek(0.1, 0.02, 0.3, 1000, Some(0.0), Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] +#[derive(Default)] pub struct Vasicek { pub mu: f64, pub sigma: f64, @@ -39,26 +10,46 @@ pub struct Vasicek { pub n: usize, pub x0: Option, pub t: Option, + pub m: Option, + ou: Ou, +} + +impl Vasicek { + #[must_use] + pub fn new(params: &Self) -> Self { + let ou = Ou::new(&Ou { + mu: params.mu, + sigma: params.sigma, + theta: params.theta.unwrap_or(1.0), + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + }); + + Self { + mu: params.mu, + sigma: params.sigma, + theta: params.theta, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + ou, + } + } } -pub fn vasicek(params: &Vasicek) -> Array1 { - let Vasicek { - mu, - sigma, - theta, - n, - x0, - t, - } = *params; +impl Sampling for Vasicek { + fn sample(&self) -> Array1 { + self.ou.sample() + } - assert!(mu != 0.0, "mu must be non-zero"); + fn n(&self) -> usize { + self.n + } - ou(&Ou { - mu, - sigma, - theta: theta.unwrap_or(1.0), - n, - x0, - t, - }) + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/lib.rs b/stochastic-rs-core/src/lib.rs index 21a0fe2..a9400e0 100644 --- a/stochastic-rs-core/src/lib.rs +++ b/stochastic-rs-core/src/lib.rs @@ -65,7 +65,6 @@ pub mod diffusions; pub mod interest; pub mod jumps; pub mod noises; -pub mod pricing; pub mod processes; pub mod volatility; @@ -74,17 +73,7 @@ use ndarray::{Array1, Array2, Axis}; use ndrustfft::Zero; pub trait Sampling: Send + Sync { - /// Generates a single sample of the stochastic process. - /// - /// # Returns - /// - /// A `Array1` representing a single sample of the stochastic process. fn sample(&self) -> Array1; - /// Generates parallel samples of the stochastic process. - /// - /// # Returns - /// - /// A `Array2>` where each inner vector represents a sample of the stochastic process. fn sample_par(&self) -> Array2 { if self.m().is_none() { panic!("m must be specified for parallel sampling"); @@ -98,7 +87,24 @@ pub trait Sampling: Send + Sync { xs } + fn n(&self) -> usize; + fn m(&self) -> Option; +} +pub trait Sampling2D: Send + Sync { + fn sample(&self) -> [Array1; 2]; + fn sample_par(&self) -> [Array2; 2] { + unimplemented!() + } + fn n(&self) -> usize; + fn m(&self) -> Option; +} + +pub trait Sampling3D: Send + Sync { + fn sample(&self) -> [Array1; 3]; + fn sample_par(&self) -> [Array2; 3] { + unimplemented!() + } fn n(&self) -> usize; fn m(&self) -> Option; } diff --git a/stochastic-rs-core/src/main.rs b/stochastic-rs-core/src/main.rs index f8fc86d..a4a48a4 100644 --- a/stochastic-rs-core/src/main.rs +++ b/stochastic-rs-core/src/main.rs @@ -1,24 +1,24 @@ use stochastic_rs::processes::fbm::Fbm; fn main() { - let fbm = Fbm::new(0.75, 10000, Some(1.0), None); - let mut runs = Vec::new(); + // let fbm = Fbm::new(0.75, 10000, Some(1.0), None); + // let mut runs = Vec::new(); - for _ in 0..20 { - let start = std::time::Instant::now(); - for _ in 0..1000 { - let _ = fbm.sample(); - } + // for _ in 0..20 { + // let start = std::time::Instant::now(); + // for _ in 0..1000 { + // let _ = fbm.sample(); + // } - let duration = start.elapsed(); - println!( - "Time elapsed in expensive_function() is: {:?}", - duration.as_secs_f32() - ); - runs.push(duration.as_secs_f32()); - } + // let duration = start.elapsed(); + // println!( + // "Time elapsed in expensive_function() is: {:?}", + // duration.as_secs_f32() + // ); + // runs.push(duration.as_secs_f32()); + // } - let sum: f32 = runs.iter().sum(); - let average = sum / runs.len() as f32; - println!("Average time: {}", average); + // let sum: f32 = runs.iter().sum(); + // let average = sum / runs.len() as f32; + // println!("Average time: {}", average); } diff --git a/stochastic-rs-core/src/noises/cfgns.rs b/stochastic-rs-core/src/noises/cfgns.rs index 7b6eac8..7e6ee45 100644 --- a/stochastic-rs-core/src/noises/cfgns.rs +++ b/stochastic-rs-core/src/noises/cfgns.rs @@ -1,69 +1,63 @@ -use derive_builder::Builder; use ndarray::{Array1, Array2}; -use crate::noises::fgn::FgnFft; +use crate::{Sampling, Sampling2D}; -/// Generates two correlated fractional Gaussian noise (fGn) paths. -/// -/// # Parameters -/// -/// - `hurst`: Hurst parameter for the fGn, must be in (0, 1). -/// - `rho`: Correlation coefficient between the two fGns, must be in [-1, 1]. -/// - `n`: Number of time steps. -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `[Array1; 2]` where each vector represents a generated fGn path. -/// -/// # Panics -/// -/// Panics if `hurst` is not in the range (0, 1). -/// Panics if `rho` is not in the range [-1, 1]. -/// -/// # Example -/// -/// ``` -/// let params = Cfgns { -/// hurst: 0.7, -/// rho: 0.5, -/// n: 1000, -/// t: Some(1.0), -/// }; -/// let correlated_fg_paths = cfgns(¶ms); -/// let fgn1 = correlated_fg_paths[0].clone(); -/// let fgn2 = correlated_fg_paths[1].clone(); -/// ``` +use super::fgn::Fgn; -#[derive(Default, Builder)] -#[builder(setter(into))] +#[derive(Default)] pub struct Cfgns { pub hurst: f64, pub rho: f64, pub n: usize, pub t: Option, + pub m: Option, + fgn: Fgn, } -pub fn cfgns(params: &Cfgns) -> [Array1; 2] { - let Cfgns { hurst, rho, n, t } = *params; - assert!( - !(0.0..=1.0).contains(&hurst), - "Hurst parameter must be in (0, 1)" - ); - assert!( - !(-1.0..=1.0).contains(&rho), - "Correlation coefficient must be in [-1, 1]" - ); +impl Cfgns { + #[must_use] + pub fn new(params: &Self) -> Self { + let fgn = Fgn::new(params.hurst, params.n, params.t, params.m); - let mut cfgns = Array2::::zeros((2, n + 1)); - let fgn = FgnFft::new(hurst, n, t, None); - let fgn1 = fgn.sample(); - let fgn2 = fgn.sample(); + Self { + hurst: params.hurst, + rho: params.rho, + n: params.n, + t: params.t, + m: params.m, + fgn, + } + } +} + +impl Sampling2D for Cfgns { + fn sample(&self) -> [Array1; 2] { + assert!( + !(0.0..=1.0).contains(&self.hurst), + "Hurst parameter must be in (0, 1)" + ); + assert!( + !(-1.0..=1.0).contains(&self.rho), + "Correlation coefficient must be in [-1, 1]" + ); + + let mut cfgns = Array2::::zeros((2, self.n + 1)); + let fgn1 = self.fgn.sample(); + let fgn2 = self.fgn.sample(); - for i in 1..(n + 1) { - cfgns[[0, i]] = fgn1[i - 1]; - cfgns[[1, i]] = rho * fgn1[i - 1] + (1.0 - rho.powi(2)).sqrt() * fgn2[i - 1]; + for i in 1..(self.n + 1) { + cfgns[[0, i]] = fgn1[i - 1]; + cfgns[[1, i]] = self.rho * fgn1[i - 1] + (1.0 - self.rho.powi(2)).sqrt() * fgn2[i - 1]; + } + + [cfgns.row(0).into_owned(), cfgns.row(1).into_owned()] + } + + fn n(&self) -> usize { + self.n } - [cfgns.row(0).into_owned(), cfgns.row(1).into_owned()] + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/noises/cgns.rs b/stochastic-rs-core/src/noises/cgns.rs index 1449875..d1e8553 100644 --- a/stochastic-rs-core/src/noises/cgns.rs +++ b/stochastic-rs-core/src/noises/cgns.rs @@ -1,60 +1,60 @@ -use derive_builder::Builder; use ndarray::{Array1, Array2}; +use ndarray_rand::RandomExt; +use rand_distr::Normal; -use crate::noises::gn; - -/// Generates two correlated Gaussian noise (GN) paths. -/// -/// # Parameters -/// -/// - `rho`: Correlation coefficient between the two GNs, must be in [-1, 1]. -/// - `n`: Number of time steps. -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `[Array1; 2]` where each vector represents a generated GN path. -/// -/// # Panics -/// -/// Panics if `rho` is not in the range [-1, 1]. -/// -/// # Example -/// -/// ``` -/// let params = Cgns { -/// rho: 0.5, -/// n: 1000, -/// t: Some(1.0), -/// }; -/// let correlated_gn_paths = cgns(¶ms); -/// let gn1 = correlated_gn_paths[0].clone(); -/// let gn2 = correlated_gn_paths[1].clone(); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] +use crate::Sampling2D; + +#[derive(Default)] pub struct Cgns { pub rho: f64, pub n: usize, pub t: Option, + pub m: Option, +} + +impl Cgns { + #[must_use] + pub fn new(params: &Self) -> Self { + Self { + rho: params.rho, + n: params.n, + t: params.t, + m: params.m, + } + } } -pub fn cgns(params: &Cgns) -> [Array1; 2] { - let Cgns { rho, n, t } = *params; - assert!( - (-1.0..=1.0).contains(&rho), - "Correlation coefficient must be in [-1, 1]" - ); +impl Sampling2D for Cgns { + fn sample(&self) -> [Array1; 2] { + assert!( + !(-1.0..=1.0).contains(&self.rho), + "Correlation coefficient must be in [-1, 1]" + ); - let mut cgns = Array2::::zeros((2, n + 1)); - let gn1 = gn::gn(n, Some(t.unwrap_or(1.0))); - let gn2 = gn::gn(n, Some(t.unwrap_or(1.0))); + let mut cgns = Array2::::zeros((2, self.n + 1)); + let gn1 = Array1::random( + self.n, + Normal::new(0.0, (self.t.unwrap_or(1.0) / self.n as f64).sqrt()).unwrap(), + ); + let gn2 = Array1::random( + self.n, + Normal::new(0.0, (self.t.unwrap_or(1.0) / self.n as f64).sqrt()).unwrap(), + ); - for i in 1..(n + 1) { - cgns[[0, i]] = gn1[i - 1]; - cgns[[1, i]] = rho * gn1[i - 1] + (1.0 - rho.powi(2)).sqrt() * gn2[i - 1]; + for i in 1..(self.n + 1) { + cgns[[0, i]] = cgns[[0, i - 1]] + gn1[i - 1]; + cgns[[1, i]] = + cgns[[1, i - 1]] + self.rho * gn1[i - 1] + (1.0 - self.rho.powi(2)).sqrt() * gn2[i - 1]; + } + + [cgns.row(0).into_owned(), cgns.row(1).into_owned()] + } + + fn n(&self) -> usize { + self.n } - [cgns.row(0).into_owned(), cgns.row(1).into_owned()] + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/noises/fgn.rs b/stochastic-rs-core/src/noises/fgn.rs index 3d21e95..1b8202b 100644 --- a/stochastic-rs-core/src/noises/fgn.rs +++ b/stochastic-rs-core/src/noises/fgn.rs @@ -18,6 +18,12 @@ pub struct Fgn { fft_handler: Arc>, } +impl Default for Fgn { + fn default() -> Self { + Self::new(0.7, 1000, None, None) + } +} + impl Fgn { pub fn new(hurst: f64, n: usize, t: Option, m: Option) -> Self { if !(0.0..=1.0).contains(&hurst) { diff --git a/stochastic-rs-core/src/pricing/mod.rs b/stochastic-rs-core/src/pricing/mod.rs deleted file mode 100644 index e69de29..0000000 diff --git a/stochastic-rs-core/src/processes/bm.rs b/stochastic-rs-core/src/processes/bm.rs index cdf6e20..cbe0000 100644 --- a/stochastic-rs-core/src/processes/bm.rs +++ b/stochastic-rs-core/src/processes/bm.rs @@ -1,38 +1,48 @@ -use derive_builder::Builder; -use ndarray::{Array1, Axis}; - -use crate::noises::gn; - -/// Generates a path of Brownian Motion (BM). -/// -/// The BM process is a fundamental continuous-time stochastic process used in various fields such as finance and physics. -/// -/// # Parameters -/// -/// - `n`: Number of time steps. -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated Brownian Motion path. -/// -/// # Example -/// -/// ``` -/// let bm_path = bm(1000, Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] +use ndarray::{s, Array1}; +use ndarray_rand::RandomExt; +use rand_distr::Normal; + +use crate::Sampling; + +#[derive(Default)] pub struct Bm { pub n: usize, pub t: Option, + pub m: Option, +} + +impl Bm { + #[must_use] + pub fn new(params: &Self) -> Self { + Self { + n: params.n, + t: params.t, + m: params.m, + } + } } -pub fn bm(params: &Bm) -> Array1 { - let Bm { n, t } = *params; - let gn = gn::gn(n, Some(t.unwrap_or(1.0))); - let mut bm = Array1::::from(gn); - bm.accumulate_axis_inplace(Axis(0), |&x, y| *y += x); - vec![0.0].into_iter().chain(bm).collect() +impl Sampling for Bm { + fn sample(&self) -> Array1 { + let gn = Array1::random( + self.n, + Normal::new(0.0, (self.t.unwrap_or(1.0) / self.n as f64).sqrt()).unwrap(), + ); + let mut bm = Array1::::zeros(self.n); + bm.slice_mut(s![1..]).assign(&gn); + + for i in 1..self.n { + bm[i] += bm[i - 1]; + } + + bm + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/processes/cbms.rs b/stochastic-rs-core/src/processes/cbms.rs index ede8f97..701437c 100644 --- a/stochastic-rs-core/src/processes/cbms.rs +++ b/stochastic-rs-core/src/processes/cbms.rs @@ -1,65 +1,60 @@ -use crate::noises::cgns::{cgns, Cgns}; -use derive_builder::Builder; use ndarray::{Array1, Array2}; -/// Generates two correlated Brownian motion (BM) paths. -/// -/// # Parameters -/// -/// - `rho`: Correlation coefficient between the two BMs, must be in [-1, 1]. -/// - `n`: Number of time steps. -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `[Array1; 2]` where each vector represents a generated BM path. -/// -/// # Panics -/// -/// Panics if `rho` is not in the range [-1, 1]. -/// -/// # Example -/// -/// ``` -/// let params = Cbms { -/// rho: 0.5, -/// n: 1000, -/// t: Some(1.0), -/// }; -/// let correlated_paths = cbms(¶ms); -/// let bm1 = correlated_paths[0].clone(); -/// let bm2 = correlated_paths[1].clone(); -/// ``` -/// -/// # Details -/// -/// This function generates two correlated Brownian motion paths using the provided correlation coefficient `rho` -/// and the number of time steps `n`. The total time `t` is optional and defaults to 1.0 if not provided. The function -/// ensures that `rho` is within the valid range of [-1, 1] and panics otherwise. The generated paths are stored in a -/// 2D array and returned as a tuple of two 1D arrays. +use crate::{noises::cgns::Cgns, Sampling2D}; -#[derive(Default, Builder)] -#[builder(setter(into))] +#[derive(Default)] pub struct Cbms { pub rho: f64, pub n: usize, pub t: Option, + pub m: Option, + cgns: Cgns, } -pub fn cbms(params: &Cbms) -> [Array1; 2] { - let Cbms { rho, n, t } = *params; - assert!( - !(-1.0..=1.0).contains(&rho), - "Correlation coefficient must be in [-1, 1]" - ); +impl Cbms { + #[must_use] + pub fn new(params: &Self) -> Self { + let cgns = Cgns::new(&Cgns { + rho: params.rho, + n: params.n, + t: params.t, + m: params.m, + }); - let mut bms = Array2::::zeros((2, n + 1)); - let [cgn1, cgn2] = cgns(&Cgns { rho, n, t }); + Self { + rho: params.rho, + n: params.n, + t: params.t, + m: params.m, + cgns, + } + } +} + +impl Sampling2D for Cbms { + fn sample(&self) -> [Array1; 2] { + assert!( + !(-1.0..=1.0).contains(&self.rho), + "Correlation coefficient must be in [-1, 1]" + ); + + let mut bms = Array2::::zeros((2, self.n + 1)); + let [cgn1, cgn2] = self.cgns.sample(); - for i in 1..(n + 1) { - bms[[0, i]] = bms[[0, i - 1]] + cgn1[i - 1]; - bms[[1, i]] = bms[[1, i - 1]] + rho * cgn1[i - 1] + (1.0 - rho.powi(2)).sqrt() * cgn2[i - 1]; + for i in 1..(self.n + 1) { + bms[[0, i]] = bms[[0, i - 1]] + cgn1[i - 1]; + bms[[1, i]] = + bms[[1, i - 1]] + self.rho * cgn1[i - 1] + (1.0 - self.rho.powi(2)).sqrt() * cgn2[i - 1]; + } + + [bms.row(0).into_owned(), bms.row(1).into_owned()] + } + + fn n(&self) -> usize { + self.n } - [bms.row(0).into_owned(), bms.row(1).into_owned()] + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/processes/ccustom.rs b/stochastic-rs-core/src/processes/ccustom.rs index 143ab7c..f53e462 100644 --- a/stochastic-rs-core/src/processes/ccustom.rs +++ b/stochastic-rs-core/src/processes/ccustom.rs @@ -1,35 +1,77 @@ -use derive_builder::Builder; use ndarray::{Array1, Axis}; use rand::thread_rng; use rand_distr::Distribution; -use super::customjt::{customjt, CustomJt}; +use crate::{Sampling, Sampling3D}; -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct CompoundCustom { +use super::customjt::CustomJt; + +#[derive(Default)] +pub struct CompoundCustom +where + D: Distribution + Copy + Send + Sync, + E: Distribution + Copy + Send + Sync, +{ pub n: Option, pub t_max: Option, + pub m: Option, + pub jumps_distribution: D, + pub jump_times_distribution: E, + customjt: CustomJt, } -pub fn compound_custom(params: &CompoundCustom, jdistr: D, jtdistr: E) -> [Array1; 3] +impl CompoundCustom where - D: Distribution + Copy, - E: Distribution + Copy, + D: Distribution + Copy + Send + Sync, + E: Distribution + Copy + Send + Sync, { - let CompoundCustom { n, t_max } = *params; - if n.is_none() && t_max.is_none() { - panic!("n or t_max must be provided"); + #[must_use] + pub fn new(params: &Self) -> Self { + let customjt = CustomJt::new(&CustomJt { + n: params.n, + t_max: params.t_max, + m: params.m, + distribution: params.jump_times_distribution, + }); + + Self { + n: params.n, + t_max: params.t_max, + m: params.m, + jumps_distribution: params.jumps_distribution, + jump_times_distribution: params.jump_times_distribution, + customjt, + } } +} + +impl Sampling3D for CompoundCustom +where + D: Distribution + Copy + Send + Sync, + E: Distribution + Copy + Send + Sync, +{ + fn sample(&self) -> [Array1; 3] { + if self.n.is_none() && self.t_max.is_none() { + panic!("n or t_max must be provided"); + } + + let p = self.customjt.sample(); + let mut jumps = Array1::::zeros(self.n.unwrap_or(p.len())); + for i in 1..p.len() { + jumps[i] = self.jumps_distribution.sample(&mut thread_rng()); + } - let p = customjt(&CustomJt { n, t_max }, jtdistr); - let mut jumps = Array1::::zeros(n.unwrap_or(p.len())); - for i in 1..p.len() { - jumps[i] = jdistr.sample(&mut thread_rng()); + let mut cum_jupms = jumps.clone(); + cum_jupms.accumulate_axis_inplace(Axis(0), |&prev, curr| *curr += prev); + + [p, cum_jupms, jumps] } - let mut cum_jupms = jumps.clone(); - cum_jupms.accumulate_axis_inplace(Axis(0), |&prev, curr| *curr += prev); + fn n(&self) -> usize { + self.n.unwrap_or(0) + } - [p, cum_jupms, jumps] + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/processes/cfbms.rs b/stochastic-rs-core/src/processes/cfbms.rs index f9b8ffc..566396c 100644 --- a/stochastic-rs-core/src/processes/cfbms.rs +++ b/stochastic-rs-core/src/processes/cfbms.rs @@ -1,79 +1,77 @@ -use derive_builder::Builder; use ndarray::{Array1, Array2}; -use crate::noises::fgn::FgnFft; +use crate::{noises::cfgns::Cfgns, Sampling2D}; -/// Generates two correlated fractional Brownian motion (fBM) paths. -/// -/// # Parameters -/// -/// - `hurst1`: Hurst parameter for the first fBM, must be in (0, 1). -/// - `hurst2`: Hurst parameter for the second fBM, must be in (0, 1). -/// - `rho`: Correlation coefficient between the two fBMs, must be in [-1, 1]. -/// - `n`: Number of time steps. -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `[Array1; 2]` where each vector represents a generated fBM path. -/// -/// # Panics -/// -/// Panics if `rho` is not in the range [-1, 1]. -/// Panics if either `hurst1` or `hurst2` is not in the range (0, 1). -/// -/// # Example -/// -/// ``` -/// let correlated_fbms = correlated_fbms(0.75, 0.75, 0.5, 1000, Some(1.0)); -/// let fbm1 = correlated_fbms[0]; -/// let fbm2 = correlated_fbms[1]; -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] +#[derive(Default)] pub struct Cfbms { pub hurst1: f64, pub hurst2: Option, pub rho: f64, pub n: usize, pub t: Option, + pub m: Option, + cfgns: Cfgns, } -pub fn correlated_fbms(params: &Cfbms) -> [Array1; 2] { - let Cfbms { - hurst1, - hurst2, - rho, - n, - t, - } = *params; +impl Cfbms { + #[must_use] + pub fn new(params: &Self) -> Self { + let cfgns = Cfgns::new(&Cfgns { + hurst: params.hurst1, + rho: params.rho, + n: params.n, + t: params.t, + m: params.m, + ..Default::default() + }); - assert!( - !(0.0..=1.0).contains(&hurst1), - "Hurst parameter for the first fBM must be in (0, 1)" - ); + Self { + hurst1: params.hurst1, + hurst2: params.hurst2, + rho: params.rho, + n: params.n, + t: params.t, + m: params.m, + cfgns, + } + } +} - if let Some(hurst2) = hurst2 { +impl Sampling2D for Cfbms { + fn sample(&self) -> [Array1; 2] { assert!( - !(0.0..=1.0).contains(&hurst2), - "Hurst parameter for the second fBM must be in (0, 1)" + !(0.0..=1.0).contains(&self.hurst1), + "Hurst parameter for the first fBM must be in (0, 1)" + ); + + if let Some(hurst2) = self.hurst2 { + assert!( + !(0.0..=1.0).contains(&hurst2), + "Hurst parameter for the second fBM must be in (0, 1)" + ); + } + assert!( + !(-1.0..=1.0).contains(&self.rho), + "Correlation coefficient must be in [-1, 1]" ); - } - assert!( - !(-1.0..=1.0).contains(&rho), - "Correlation coefficient must be in [-1, 1]" - ); - let mut fbms = Array2::::zeros((2, n + 1)); + let mut fbms = Array2::::zeros((2, self.n + 1)); + let [fgn1, fgn2] = self.cfgns.sample(); - let fgn1 = FgnFft::new(hurst1, n, t, None).sample(); - let fgn2 = FgnFft::new(hurst2.unwrap_or(hurst1), n, t, None).sample(); + for i in 1..(self.n + 1) { + fbms[[0, i]] = fbms[[0, i - 1]] + fgn1[i - 1]; + fbms[[1, i]] = + fbms[[1, i - 1]] + self.rho * fgn1[i - 1] + (1.0 - self.rho.powi(2)).sqrt() * fgn2[i - 1]; + } - for i in 1..(n + 1) { - fbms[[0, i]] = fbms[[0, i - 1]] + fgn1[i - 1]; - fbms[[1, i]] = fbms[[1, i - 1]] + rho * fgn2[i - 1] + (1.0 - rho.powi(2)).sqrt() * fgn2[i - 1]; + [fbms.row(0).to_owned(), fbms.row(1).to_owned()] } - [fbms.row(0).to_owned(), fbms.row(1).to_owned()] + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/processes/cpoisson.rs b/stochastic-rs-core/src/processes/cpoisson.rs index 21a7500..4a501f6 100644 --- a/stochastic-rs-core/src/processes/cpoisson.rs +++ b/stochastic-rs-core/src/processes/cpoisson.rs @@ -1,9 +1,10 @@ -use derive_builder::Builder; use ndarray::{Array1, Axis}; use rand::thread_rng; use rand_distr::Distribution; -use super::poisson::{poisson, Poisson}; +use crate::{Sampling, Sampling3D}; + +use super::poisson::Poisson; /// Generates a compound Poisson process. /// @@ -32,31 +33,62 @@ use super::poisson::{poisson, Poisson}; /// let (p, cum_cp, cp) = compound_poisson(1000, 2.0, None, Some(10.0), Some(0.0), Some(1.0)); /// ``` -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct CompoundPoisson { +pub struct CompoundPoisson +where + D: Distribution + Copy + Send + Sync, +{ pub n: Option, pub lambda: f64, pub t_max: Option, + pub m: Option, + pub distribution: D, + poisson: Poisson, } -pub fn compound_poisson(params: &CompoundPoisson, jdistr: D) -> [Array1; 3] -where - D: Distribution + Copy, -{ - let CompoundPoisson { n, lambda, t_max } = *params; - if n.is_none() && t_max.is_none() { - panic!("n or t_max must be provided"); +impl + Copy + Send + Sync> CompoundPoisson { + #[must_use] + pub fn new(params: &Self) -> Self { + let poisson = Poisson::new(&Poisson { + lambda: params.lambda, + n: params.n, + t_max: params.t_max, + m: params.m, + }); + + Self { + n: params.n, + lambda: params.lambda, + t_max: params.t_max, + m: params.m, + distribution: params.distribution, + poisson, + } } +} + +impl + Copy + Send + Sync> Sampling3D for CompoundPoisson { + fn sample(&self) -> [Array1; 3] { + if self.n.is_none() && self.t_max.is_none() { + panic!("n or t_max must be provided"); + } + + let poisson = self.poisson.sample(); + let mut jumps = Array1::::zeros(poisson.len()); + for i in 1..poisson.len() { + jumps[i] = self.distribution.sample(&mut thread_rng()); + } - let p = poisson(&Poisson { lambda, n, t_max }); - let mut jumps = Array1::::zeros(p.len()); - for i in 1..p.len() { - jumps[i] = jdistr.sample(&mut thread_rng()); + let mut cum_jupms = jumps.clone(); + cum_jupms.accumulate_axis_inplace(Axis(0), |&prev, curr| *curr += prev); + + [poisson, cum_jupms, jumps] } - let mut cum_jupms = jumps.clone(); - cum_jupms.accumulate_axis_inplace(Axis(0), |&prev, curr| *curr += prev); + fn n(&self) -> usize { + self.n.unwrap_or(0) + } - [p, cum_jupms, jumps] + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/processes/customjt.rs b/stochastic-rs-core/src/processes/customjt.rs index 65d5a53..39cfd87 100644 --- a/stochastic-rs-core/src/processes/customjt.rs +++ b/stochastic-rs-core/src/processes/customjt.rs @@ -1,41 +1,64 @@ -use derive_builder::Builder; use ndarray::{Array0, Array1, Axis, Dim}; use ndarray_rand::rand_distr::Distribution; use ndarray_rand::RandomExt; use rand::thread_rng; -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct CustomJt { +use crate::Sampling; + +#[derive(Default)] +pub struct CustomJt +where + D: Distribution + Copy + Send + Sync, +{ pub n: Option, pub t_max: Option, + pub m: Option, + pub distribution: D, } -pub fn customjt(params: &CustomJt, jtdistr: D) -> Array1 -where - D: Distribution + Copy, -{ - let CustomJt { n, t_max } = *params; - if let Some(n) = n { - let random = Array1::random(n, jtdistr); - let mut x = Array1::::zeros(n + 1); - for i in 1..n + 11 { - x[i] = x[i - 1] + random[i - 1]; +impl + Copy + Send + Sync> CustomJt { + #[must_use] + pub fn new(params: &Self) -> Self { + Self { + n: params.n, + t_max: params.t_max, + m: params.m, + distribution: params.distribution, } + } +} + +impl + Copy + Send + Sync> Sampling for CustomJt { + fn sample(&self) -> Array1 { + if let Some(n) = self.n { + let random = Array1::random(n, self.distribution); + let mut x = Array1::::zeros(n + 1); + for i in 1..n + 11 { + x[i] = x[i - 1] + random[i - 1]; + } + + x + } else if let Some(t_max) = self.t_max { + let mut x = Array1::from(vec![0.0]); + let mut t = 0.0; - x - } else if let Some(t_max) = t_max { - let mut x = Array1::from(vec![0.0]); - let mut t = 0.0; + while t < t_max { + t += self.distribution.sample(&mut thread_rng()); + x.push(Axis(0), Array0::from_elem(Dim(()), t).view()) + .unwrap(); + } - while t < t_max { - t += jtdistr.sample(&mut thread_rng()); - x.push(Axis(0), Array0::from_elem(Dim(()), t).view()) - .unwrap(); + x + } else { + panic!("n or t_max must be provided"); } + } + + fn n(&self) -> usize { + self.n.unwrap_or(0) + } - x - } else { - panic!("n or t_max must be provided"); + fn m(&self) -> Option { + self.m } } diff --git a/stochastic-rs-core/src/processes/fbm.rs b/stochastic-rs-core/src/processes/fbm.rs index 1f6ee6a..c71a8f3 100644 --- a/stochastic-rs-core/src/processes/fbm.rs +++ b/stochastic-rs-core/src/processes/fbm.rs @@ -1,33 +1,37 @@ -use crate::{noises::fgn::Fgn, Sampling}; use ndarray::{s, Array1}; +use crate::{noises::fgn::Fgn, Sampling}; + +#[derive(Default)] pub struct Fbm { - #[allow(dead_code)] - hurst: f64, - #[allow(dead_code)] - n: usize, - m: Option, - fgn: Option, + pub hurst: f64, + pub n: usize, + pub t: Option, + pub m: Option, + fgn: Fgn, } impl Fbm { - pub fn new(hurst: f64, n: usize, t: Option, m: Option) -> Self { - if !(0.0..=1.0).contains(&hurst) { + pub fn new(params: &Self) -> Self { + if !(0.0..=1.0).contains(¶ms.hurst) { panic!("Hurst parameter must be in (0, 1)") } + let fgn = Fgn::new(params.hurst, params.n, params.t, params.m); + Self { - hurst, - n, - m, - fgn: Some(Fgn::new(hurst, n - 1, t, None)), + hurst: params.hurst, + t: params.t, + n: params.n, + m: params.m, + fgn, } } } impl Sampling for Fbm { fn sample(&self) -> Array1 { - let fgn = self.fgn.as_ref().unwrap().sample(); + let fgn = self.fgn.sample(); let mut fbm = Array1::::zeros(self.n); fbm.slice_mut(s![1..]).assign(&fgn); @@ -57,7 +61,13 @@ mod tests { #[test] fn plot() { - let fbm = Fbm::new(0.45, 1000, Some(1.0), Some(10)); + let fbm = Fbm::new(&Fbm { + hurst: 0.5, + n: 1000, + t: Some(1.0), + m: None, + ..Default::default() + }); let mut plot = Plot::new(); let d = fbm.sample_par(); for data in d.axis_iter(Axis(0)) { diff --git a/stochastic-rs-core/src/processes/mod.rs b/stochastic-rs-core/src/processes/mod.rs index cd4e767..01f2943 100644 --- a/stochastic-rs-core/src/processes/mod.rs +++ b/stochastic-rs-core/src/processes/mod.rs @@ -1,26 +1,3 @@ -//! This module contains the implementations of various stochastic processes. -//! -//! The following stochastic processes are implemented: -//! -//! - **Brownian Motion (BM)** -//! - Standard Brownian motion, also known as Wiener process. -//! - SDE: `dX(t) = dW(t)` -//! -//! - **Correlated Brownian Motions (Correlated BMs)** -//! - Generates two correlated Brownian motion paths. -//! -//! - **Fractional Brownian Motion (fBM)** -//! - Generates fractional Brownian motion, which includes long-range dependence. -//! - SDE: `dX(t) = dW^H(t)` where `H` is the Hurst parameter. -//! -//! - **Poisson Process** -//! - Models the occurrence of events over time. -//! - SDE: `dN(t) ~ Poisson(\lambda t)` -//! -//! - **Compound Poisson Process** -//! - Models the occurrence of events over time, where each event has a random magnitude (jump). -//! - SDE: `dX(t) = \sum_{i=1}^{N(t)} Z_i` where `N(t)` is a Poisson process with rate `\lambda` and `Z_i` are i.i.d. jump sizes. - pub mod bm; pub mod cbms; pub mod ccustom; diff --git a/stochastic-rs-core/src/processes/poisson.rs b/stochastic-rs-core/src/processes/poisson.rs index 0a0c7ff..4a50804 100644 --- a/stochastic-rs-core/src/processes/poisson.rs +++ b/stochastic-rs-core/src/processes/poisson.rs @@ -1,65 +1,64 @@ -use derive_builder::Builder; use ndarray::{Array0, Array1, Axis, Dim}; use ndarray_rand::rand_distr::{Distribution, Exp}; use ndarray_rand::RandomExt; use rand::thread_rng; -/// Generates a Poisson process. -/// -/// The Poisson process models the occurrence of events over time. It is commonly used in fields such as queuing theory, telecommunications, and finance. -/// -/// # Parameters -/// -/// - `lambda`: Rate parameter (average number of events per unit time). -/// - `n`: Number of events (optional). -/// - `t_max`: Maximum time (optional). -/// -/// # Returns -/// -/// A `Array1` representing the generated Poisson process path. -/// -/// # Panics -/// -/// Panics if neither `n` nor `t_max` is provided. -/// -/// # Example -/// -/// ``` -/// let poisson_path = poisson(1.0, Some(1000), None); -/// let poisson_path = poisson(1.0, None, Some(100.0)); -/// ``` +use crate::Sampling; -#[derive(Default, Builder)] -#[builder(setter(into))] +#[derive(Default)] pub struct Poisson { pub lambda: f64, pub n: Option, pub t_max: Option, + pub m: Option, } -pub fn poisson(params: &Poisson) -> Array1 { - let Poisson { lambda, n, t_max } = *params; - if let Some(n) = n { - let exponentials = Array1::random(n, Exp::new(1.0 / lambda).unwrap()); - let mut poisson = Array1::::zeros(n + 1); - for i in 1..(n + 1) { - poisson[i] = poisson[i - 1] + exponentials[i - 1]; +impl Poisson { + #[must_use] + pub fn new(params: &Self) -> Self { + Self { + lambda: params.lambda, + n: params.n, + t_max: params.t_max, + m: params.m, } + } +} + +impl Sampling for Poisson { + fn sample(&self) -> Array1 { + if let Some(n) = self.n { + let exponentials = Array1::random(n, Exp::new(1.0 / self.lambda).unwrap()); + let mut poisson = Array1::::zeros(n + 1); + for i in 1..(n + 1) { + poisson[i] = poisson[i - 1] + exponentials[i - 1]; + } + + poisson + } else if let Some(t_max) = self.t_max { + let mut poisson = Array1::from(vec![0.0]); + let mut t = 0.0; - poisson - } else if let Some(t_max) = t_max { - let mut poisson = Array1::from(vec![0.0]); - let mut t = 0.0; + while t < t_max { + t += Exp::new(1.0 / self.lambda) + .unwrap() + .sample(&mut thread_rng()); + poisson + .push(Axis(0), Array0::from_elem(Dim(()), t).view()) + .unwrap(); + } - while t < t_max { - t += Exp::new(1.0 / lambda).unwrap().sample(&mut thread_rng()); poisson - .push(Axis(0), Array0::from_elem(Dim(()), t).view()) - .unwrap(); + } else { + panic!("n or t_max must be provided"); } + } + + fn n(&self) -> usize { + self.n.unwrap_or(0) + } - poisson - } else { - panic!("n or t_max must be provided"); + fn m(&self) -> Option { + self.m } } diff --git a/stochastic-rs-core/src/volatility/heston.rs b/stochastic-rs-core/src/volatility/heston.rs index f56325c..9ed7d14 100644 --- a/stochastic-rs-core/src/volatility/heston.rs +++ b/stochastic-rs-core/src/volatility/heston.rs @@ -1,39 +1,9 @@ -use derive_builder::Builder; use ndarray::Array1; -use crate::noises::cgns::{cgns, Cgns}; +use crate::{noises::cgns::Cgns, Sampling2D}; -/// Generates paths for the Heston model. -/// -/// The Heston model is a stochastic volatility model used to describe the evolution of the volatility of an underlying asset. -/// -/// # Parameters -/// -/// - `mu`: Drift parameter of the asset price. -/// - `kappa`: Rate of mean reversion of the volatility. -/// - `theta`: Long-term mean level of the volatility. -/// - `eta`: Volatility of the volatility (vol of vol). -/// - `rho`: Correlation between the asset price and its volatility. -/// - `n`: Number of time steps. -/// - `s0`: Initial value of the asset price (optional, defaults to 0.0). -/// - `v0`: Initial value of the volatility (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// - `use_sym`: Whether to use symmetric noise for the volatility (optional, defaults to false). -/// -/// # Returns -/// -/// A `[Array1; 2]` where the first vector represents the asset price path and the second vector represents the volatility path. -/// -/// # Example -/// -/// ``` -/// let paths = heston(0.05, 1.5, 0.04, 0.3, -0.7, 1000, Some(100.0), Some(0.04), Some(1.0), Some(false)); -/// let asset_prices = paths[0]; -/// let volatilities = paths[1]; -/// ``` +#[derive(Default)] -#[derive(Default, Builder)] -#[builder(setter(into))] pub struct Heston { pub mu: f64, pub kappa: f64, @@ -45,40 +15,66 @@ pub struct Heston { pub v0: Option, pub t: Option, pub use_sym: Option, + pub m: Option, + cgns: Cgns, } -pub fn heston(params: &Heston) -> [Array1; 2] { - let Heston { - mu, - kappa, - theta, - eta, - rho, - n, - s0, - v0, - t, - use_sym, - } = *params; +impl Heston { + #[must_use] + pub fn new(params: &Self) -> Self { + let cgns = Cgns::new(&Cgns { + rho: params.rho, + n: params.n, + t: params.t, + m: params.m, + }); - let [cgn1, cgn2] = cgns(&Cgns { rho, n, t }); - let dt = t.unwrap_or(1.0) / n as f64; + Self { + mu: params.mu, + kappa: params.kappa, + theta: params.theta, + eta: params.eta, + rho: params.rho, + n: params.n, + s0: params.s0, + v0: params.v0, + t: params.t, + use_sym: params.use_sym, + m: params.m, + cgns, + } + } +} + +impl Sampling2D for Heston { + fn sample(&self) -> [Array1; 2] { + let [cgn1, cgn2] = self.cgns.sample(); + let dt = self.t.unwrap_or(1.0) / self.n as f64; + + let mut s = Array1::::zeros(self.n + 1); + let mut v = Array1::::zeros(self.n + 1); - let mut s = Array1::::zeros(n + 1); - let mut v = Array1::::zeros(n + 1); + s[0] = self.s0.unwrap_or(0.0); + v[0] = self.v0.unwrap_or(0.0); - s[0] = s0.unwrap_or(0.0); - v[0] = v0.unwrap_or(0.0); + for i in 1..(self.n + 1) { + s[i] = s[i - 1] + self.mu * s[i - 1] * dt + s[i - 1] * v[i - 1].sqrt() * cgn1[i - 1]; - for i in 1..(n + 1) { - s[i] = s[i - 1] + mu * s[i - 1] * dt + s[i - 1] * v[i - 1].sqrt() * cgn1[i - 1]; + let random: f64 = match self.use_sym.unwrap_or(false) { + true => self.eta * (v[i - 1]).abs().sqrt() * cgn2[i - 1], + false => self.eta * (v[i - 1]).max(0.0).sqrt() * cgn2[i - 1], + }; + v[i] = v[i - 1] + self.kappa * (self.theta - v[i - 1]) * dt + random; + } - let random: f64 = match use_sym.unwrap_or(false) { - true => eta * (v[i - 1]).abs().sqrt() * cgn2[i - 1], - false => eta * (v[i - 1]).max(0.0).sqrt() * cgn2[i - 1], - }; - v[i] = v[i - 1] + kappa * (theta - v[i - 1]) * dt + random; + [s, v] } - [s, v] + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/volatility/sabr.rs b/stochastic-rs-core/src/volatility/sabr.rs index 23c896e..6996755 100644 --- a/stochastic-rs-core/src/volatility/sabr.rs +++ b/stochastic-rs-core/src/volatility/sabr.rs @@ -1,35 +1,9 @@ -use derive_builder::Builder; use ndarray::Array1; -use crate::noises::cgns::{cgns, Cgns}; +use crate::{noises::cgns::Cgns, Sampling2D}; -/// Generates a path of the SABR (Stochastic Alpha, Beta, Rho) model. -/// -/// The SABR model is widely used in financial mathematics for modeling stochastic volatility. -/// It incorporates correlated Brownian motions to simulate the underlying asset price and volatility. -/// -/// # Parameters -/// -/// - `alpha`: The volatility of volatility. -/// - `beta`: The elasticity parameter, must be in the range (0, 1). -/// - `rho`: The correlation between the asset price and volatility, must be in the range (-1, 1). -/// - `n`: Number of time steps. -/// - `f0`: Initial value of the forward rate (optional, defaults to 0.0). -/// - `v0`: Initial value of the volatility (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A tuple of two `Array1` representing the generated paths for the forward rate and volatility. -/// -/// # Example -/// -/// ``` -/// let (forward_rate_path, volatility_path) = sabr(0.2, 0.5, -0.3, 1000, Some(0.04), Some(0.2), Some(1.0)); -/// ``` +#[derive(Default)] -#[derive(Default, Builder)] -#[builder(setter(into))] pub struct Sabr { pub alpha: f64, pub beta: f64, @@ -38,35 +12,57 @@ pub struct Sabr { pub f0: Option, pub v0: Option, pub t: Option, + pub m: Option, + cgns: Cgns, } -pub fn sabr(params: &Sabr) -> [Array1; 2] { - let Sabr { - alpha, - beta, - rho, - n, - f0, - v0, - t, - } = *params; +impl Sabr { + #[must_use] + pub fn new(params: &Self) -> Self { + let cgns = Cgns::new(&Cgns { + rho: params.rho, + n: params.n, + t: params.t, + m: params.m, + }); - assert!(0.0 < beta && beta < 1.0, "Beta parameter must be in (0, 1)"); - assert!(-1.0 < rho && rho < 1.0, "Rho parameter must be in (-1, 1)"); - assert!(alpha > 0.0, "Alpha parameter must be positive"); + Self { + alpha: params.alpha, + beta: params.beta, + rho: params.rho, + n: params.n, + f0: params.f0, + v0: params.v0, + t: params.t, + m: params.m, + cgns, + } + } +} + +impl Sampling2D for Sabr { + fn sample(&self) -> [Array1; 2] { + let [cgn1, cgn2] = self.cgns.sample(); - let [cgn1, cgn2] = cgns(&Cgns { rho, n, t }); + let mut f = Array1::::zeros(self.n + 1); + let mut v = Array1::::zeros(self.n + 1); - let mut f = Array1::::zeros(n + 1); - let mut v = Array1::::zeros(n + 1); + f[0] = self.f0.unwrap_or(0.0); + v[0] = self.v0.unwrap_or(0.0); - f[0] = f0.unwrap_or(0.0); - v[0] = v0.unwrap_or(0.0); + for i in 1..(self.n + 1) { + f[i] = f[i - 1] + v[i - 1] * f[i - 1].powf(self.beta) * cgn1[i - 1]; + v[i] = v[i - 1] + self.alpha * v[i - 1] * cgn2[i - 1]; + } - for i in 1..(n + 1) { - f[i] = f[i - 1] + v[i - 1] * f[i - 1].powf(beta) * cgn1[i - 1]; - v[i] = v[i - 1] + alpha * v[i - 1] * cgn2[i - 1]; + [f, v] } - [f, v] + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-stats/src/fractal_dim.rs b/stochastic-rs-stats/src/fractal_dim.rs index 45cf3cd..57f48a9 100644 --- a/stochastic-rs-stats/src/fractal_dim.rs +++ b/stochastic-rs-stats/src/fractal_dim.rs @@ -1,30 +1,5 @@ use linreg::linear_regression; -/// Calculates the fractal dimension of a time series using the Higuchi method. -/// -/// The Higuchi method is a popular technique for estimating the fractal dimension of a time series, -/// which can be used to analyze the complexity and self-similarity of the data. -/// -/// # Parameters -/// -/// - `x`: A slice of `f64` representing the time series data. -/// - `kmax`: The maximum value of `k` to be used in the calculation. -/// -/// # Returns -/// -/// A `f64` value representing the estimated fractal dimension of the time series. -/// -/// # Example -/// -/// ``` -/// let data = vec![1.0, 2.0, 1.5, 3.0, 2.5, 4.0, 3.5, 5.0]; -/// let fd = higuchi_fd(&data, 5); -/// println!("Fractal Dimension: {}", fd); -/// ``` -/// -/// # Panics -/// -/// This function will panic if the input slice `x` is empty. pub fn higuchi_fd(x: &[f64], kmax: usize) -> f64 { let n_times = x.len(); From 6990fc112c56d8a68696dc73e8af1193afde2566 Mon Sep 17 00:00:00 2001 From: Daniel Boros Date: Thu, 12 Sep 2024 00:52:15 +0200 Subject: [PATCH 10/11] refactor: jumps --- stochastic-rs-core/src/diffusions/fcir.rs | 8 +- stochastic-rs-core/src/diffusions/fgbm.rs | 8 +- stochastic-rs-core/src/diffusions/fjacobi.rs | 8 +- stochastic-rs-core/src/diffusions/fou.rs | 8 +- stochastic-rs-core/src/jumps/bates.rs | 220 +++++++++---------- stochastic-rs-core/src/jumps/ig.rs | 76 ++++--- stochastic-rs-core/src/jumps/jump_fou.rs | 136 ++++++------ stochastic-rs-core/src/lib.rs | 3 + stochastic-rs-core/src/noises/cfgns.rs | 8 +- stochastic-rs-core/src/noises/fgn.rs | 46 ++-- stochastic-rs-core/src/processes/ccustom.rs | 14 +- stochastic-rs-core/src/processes/cpoisson.rs | 37 +--- stochastic-rs-core/src/processes/customjt.rs | 9 +- stochastic-rs-core/src/processes/fbm.rs | 8 +- 14 files changed, 304 insertions(+), 285 deletions(-) diff --git a/stochastic-rs-core/src/diffusions/fcir.rs b/stochastic-rs-core/src/diffusions/fcir.rs index dfd45dc..86f7ba4 100644 --- a/stochastic-rs-core/src/diffusions/fcir.rs +++ b/stochastic-rs-core/src/diffusions/fcir.rs @@ -19,7 +19,13 @@ pub struct Fcir { impl Fcir { #[must_use] pub fn new(params: &Self) -> Self { - let fgn = Fgn::new(params.hurst, params.n, params.t, None); + let fgn = Fgn::new(&Fgn { + hurst: params.hurst, + n: params.n, + t: params.t, + m: params.m, + ..Default::default() + }); Self { hurst: params.hurst, diff --git a/stochastic-rs-core/src/diffusions/fgbm.rs b/stochastic-rs-core/src/diffusions/fgbm.rs index 8cd51f3..f8f935a 100644 --- a/stochastic-rs-core/src/diffusions/fgbm.rs +++ b/stochastic-rs-core/src/diffusions/fgbm.rs @@ -17,7 +17,13 @@ pub struct Fgbm { impl Fgbm { #[must_use] pub fn new(params: &Self) -> Self { - let fgn = Fgn::new(params.hurst, params.n, params.t, None); + let fgn = Fgn::new(&Fgn { + hurst: params.hurst, + n: params.n, + t: params.t, + m: params.m, + ..Default::default() + }); Self { hurst: params.hurst, diff --git a/stochastic-rs-core/src/diffusions/fjacobi.rs b/stochastic-rs-core/src/diffusions/fjacobi.rs index b89297e..39643a5 100644 --- a/stochastic-rs-core/src/diffusions/fjacobi.rs +++ b/stochastic-rs-core/src/diffusions/fjacobi.rs @@ -18,7 +18,13 @@ pub struct Fjacobi { impl Fjacobi { #[must_use] pub fn new(params: &Self) -> Self { - let fgn = Fgn::new(params.hurst, params.n, params.t, None); + let fgn = Fgn::new(&Fgn { + hurst: params.hurst, + n: params.n, + t: params.t, + m: params.m, + ..Default::default() + }); Self { hurst: params.hurst, diff --git a/stochastic-rs-core/src/diffusions/fou.rs b/stochastic-rs-core/src/diffusions/fou.rs index 269a95b..df287c1 100644 --- a/stochastic-rs-core/src/diffusions/fou.rs +++ b/stochastic-rs-core/src/diffusions/fou.rs @@ -18,7 +18,13 @@ pub struct Fou { impl Fou { #[must_use] pub fn new(params: &Self) -> Self { - let fgn = Fgn::new(params.hurst, params.n, params.t, None); + let fgn = Fgn::new(&Fgn { + hurst: params.hurst, + n: params.n, + t: params.t, + m: params.m, + ..Default::default() + }); Self { hurst: params.hurst, diff --git a/stochastic-rs-core/src/jumps/bates.rs b/stochastic-rs-core/src/jumps/bates.rs index 61ad625..e4b22d6 100644 --- a/stochastic-rs-core/src/jumps/bates.rs +++ b/stochastic-rs-core/src/jumps/bates.rs @@ -1,70 +1,15 @@ -use derive_builder::Builder; use ndarray::Array1; -use rand_distr::Distribution; use crate::{ - noises::cgns::{cgns, Cgns}, - processes::cpoisson::{compound_poisson, CompoundPoisson}, + noises::cgns::Cgns, processes::cpoisson::CompoundPoisson, ProcessDistribution, Sampling2D, + Sampling3D, }; -/// Generates paths for the Bates (1996) model. -/// -/// The Bates model combines a stochastic volatility model with jump diffusion, -/// commonly used in financial mathematics to model asset prices. -/// -/// # Parameters -/// -/// - `mu`: Drift parameter of the asset price. -/// - `b`: The continuously compounded domestic/foreign interest rate differential. -/// - `r`: The continuously compounded risk-free interest rate. -/// - `r_f`: The continuously compounded foreign interest rate. -/// - `lambda`: Jump intensity. -/// - `k`: Mean jump size. -/// - `alpha`: Rate of mean reversion of the volatility. -/// - `beta`: Long-term mean level of the volatility. -/// - `sigma`: Volatility of the volatility (vol of vol). -/// - `rho`: Correlation between the asset price and its volatility. -/// - `n`: Number of time steps. -/// - `s0`: Initial value of the asset price (optional, defaults to 0.0). -/// - `v0`: Initial value of the volatility (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// - `use_sym`: Whether to use symmetric noise for the volatility (optional, defaults to false). -/// -/// # Returns -/// -/// A `[Array1; 2]` where the first vector represents the asset price path and the second vector represents the volatility path. -/// -/// # Example -/// -/// ``` -/// let params = Bates1996 { -/// mu: 0.05, -/// lambda: 0.1, -/// k: 0.2, -/// alpha: 1.5, -/// beta: 0.04, -/// sigma: 0.3, -/// rho: -0.7, -/// n: 1000, -/// s0: Some(100.0), -/// v0: Some(0.04), -/// t: Some(1.0), -/// use_sym: Some(false), -/// }; -/// -/// let jump_distr = Normal::new(0.0, 1.0); // Example jump distribution -/// let paths = bates_1996(¶ms, jump_distr); -/// let asset_prices = paths[0]; -/// let volatilities = paths[1]; -/// ``` -/// -/// # Panics -/// -/// This function will panic if the `correlated_bms` or `compound_poisson` functions return invalid lengths or if there are issues with array indexing. - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct Bates1996 { +#[derive(Default)] +pub struct Bates1996 +where + D: ProcessDistribution, +{ pub mu: Option, pub b: Option, pub r: Option, @@ -80,68 +25,99 @@ pub struct Bates1996 { pub v0: Option, pub t: Option, pub use_sym: Option, + pub m: Option, + pub jumps_distribution: D, + cgns: Cgns, + cpoisson: CompoundPoisson, } -pub fn bates_1996(params: &Bates1996, jdistr: D) -> [Array1; 2] -where - D: Distribution + Copy, -{ - let Bates1996 { - mu, - b, - r, - r_f, - lambda, - k, - alpha, - beta, - sigma, - rho, - n, - s0, - v0, - t, - use_sym, - } = *params; - - let [cgn1, cgn2] = cgns(&Cgns { rho, n, t }); - let dt = t.unwrap_or(1.0) / n as f64; - - let mut s = Array1::::zeros(n + 1); - let mut v = Array1::::zeros(n + 1); - - s[0] = s0.unwrap_or(0.0); - v[0] = v0.unwrap_or(0.0); - - let drift = match (mu, b, r, r_f) { - (Some(r), Some(r_f), ..) => r - r_f, - (Some(b), ..) => b, - _ => mu.unwrap(), - }; - - for i in 1..(n + 1) { - let [.., jumps] = compound_poisson( - &CompoundPoisson { - n: None, - lambda, - t_max: Some(dt), - }, - jdistr, - ); - - let sqrt_v = use_sym - .unwrap_or(false) - .then(|| v[i - 1].abs()) - .unwrap_or(v[i - 1].max(0.0)) - .sqrt(); - - s[i] = s[i - 1] - + (drift - lambda * k) * s[i - 1] * dt - + s[i - 1] * sqrt_v * cgn1[i - 1] - + jumps.sum(); - - v[i] = v[i - 1] + (alpha - beta * v[i - 1]) * dt + sigma * v[i - 1] * cgn2[i - 1]; +impl Bates1996 { + #[must_use] + pub fn new(params: &Bates1996) -> Self { + let cgns = Cgns::new(&Cgns { + rho: params.rho, + n: params.n, + t: params.t, + m: params.m, + }); + + let cpoisson = CompoundPoisson::new(&CompoundPoisson { + n: None, + lambda: params.lambda, + t_max: Some(params.t.unwrap_or(1.0) / params.n as f64), + distribution: params.jumps_distribution, + m: params.m, + ..Default::default() + }); + + Self { + mu: params.mu, + b: params.b, + r: params.r, + r_f: params.r_f, + lambda: params.lambda, + k: params.k, + alpha: params.alpha, + beta: params.beta, + sigma: params.sigma, + rho: params.rho, + n: params.n, + s0: params.s0, + v0: params.v0, + t: params.t, + use_sym: params.use_sym, + m: params.m, + jumps_distribution: params.jumps_distribution, + cgns, + cpoisson, + } + } +} + +impl Sampling2D for Bates1996 { + fn sample(&self) -> [Array1; 2] { + let [cgn1, cgn2] = self.cgns.sample(); + let dt = self.t.unwrap_or(1.0) / self.n as f64; + + let mut s = Array1::::zeros(self.n + 1); + let mut v = Array1::::zeros(self.n + 1); + + s[0] = self.s0.unwrap_or(0.0); + v[0] = self.v0.unwrap_or(0.0); + + let drift = match (self.mu, self.b, self.r, self.r_f) { + (Some(r), Some(r_f), ..) => r - r_f, + (Some(b), ..) => b, + _ => self.mu.unwrap(), + }; + + for i in 1..(self.n + 1) { + let [.., jumps] = self.cpoisson.sample(); + + let sqrt_v = self + .use_sym + .unwrap_or(false) + .then(|| v[i - 1].abs()) + .unwrap_or(v[i - 1].max(0.0)) + .sqrt(); + + s[i] = s[i - 1] + + (drift - self.lambda * self.k) * s[i - 1] * dt + + s[i - 1] * sqrt_v * cgn1[i - 1] + + jumps.sum(); + + v[i] = + v[i - 1] + (self.alpha - self.beta * v[i - 1]) * dt + self.sigma * v[i - 1] * cgn2[i - 1]; + } + + [s, v] } - [s, v] + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/jumps/ig.rs b/stochastic-rs-core/src/jumps/ig.rs index 20ae7cb..d5f1341 100644 --- a/stochastic-rs-core/src/jumps/ig.rs +++ b/stochastic-rs-core/src/jumps/ig.rs @@ -1,48 +1,54 @@ -use crate::noises::gn::gn; - -use derive_builder::Builder; use ndarray::Array1; +use ndarray_rand::RandomExt; +use rand_distr::Normal; + +use crate::Sampling; + +#[derive(Default)] -/// Generates a path of the Inverse Gaussian (IG) process. -/// -/// The IG process is used in various fields such as finance and engineering. -/// -/// # Parameters -/// -/// - `gamma`: Drift parameter. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated IG process path. -/// -/// # Example -/// -/// ``` -/// let ig_path = ig(0.1, 1000, Some(0.0), Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] pub struct Ig { pub gamma: f64, pub n: usize, pub x0: Option, pub t: Option, + pub m: Option, +} + +impl Ig { + #[must_use] + pub fn new(params: &Ig) -> Self { + Self { + gamma: params.gamma, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + } + } } -pub fn ig(params: &Ig) -> Array1 { - let Ig { gamma, n, x0, t } = *params; - let dt = t.unwrap_or(1.0) / n as f64; - let gn = gn(n, t); - let mut ig = Array1::zeros(n + 1); - ig[0] = x0.unwrap_or(0.0); +impl Sampling for Ig { + fn sample(&self) -> Array1 { + let dt = self.t.unwrap_or(1.0) / self.n as f64; + let gn = Array1::random( + self.n, + Normal::new(0.0, (self.t.unwrap_or(1.0) / self.n as f64).sqrt()).unwrap(), + ); + let mut ig = Array1::zeros(self.n + 1); + ig[0] = self.x0.unwrap_or(0.0); + + for i in 1..(self.n + 1) { + ig[i] = ig[i - 1] + self.gamma * dt + gn[i - 1] + } - for i in 1..(n + 1) { - ig[i] = ig[i - 1] + gamma * dt + gn[i - 1] + ig } - ig + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/jumps/jump_fou.rs b/stochastic-rs-core/src/jumps/jump_fou.rs index 21cee95..8a5301f 100644 --- a/stochastic-rs-core/src/jumps/jump_fou.rs +++ b/stochastic-rs-core/src/jumps/jump_fou.rs @@ -1,41 +1,14 @@ -use derive_builder::Builder; use ndarray::Array1; -use rand_distr::Distribution; use crate::{ - noises::fgn::FgnFft, - processes::cpoisson::{compound_poisson, CompoundPoissonBuilder}, + noises::fgn::Fgn, processes::cpoisson::CompoundPoisson, ProcessDistribution, Sampling, Sampling3D, }; -/// Generates a path of the jump fractional Ornstein-Uhlenbeck (FOU) process. -/// -/// The jump FOU process incorporates both the fractional Ornstein-Uhlenbeck dynamics and compound Poisson jumps, -/// which can be useful in various financial and physical modeling contexts. -/// -/// # Parameters -/// -/// - `hurst`: The Hurst parameter for the fractional Ornstein-Uhlenbeck process. -/// - `mu`: The mean reversion level. -/// - `sigma`: The volatility parameter. -/// - `theta`: The mean reversion speed. -/// - `lambda`: The jump intensity of the compound Poisson process. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated jump FOU process path. -/// -/// # Example -/// -/// ``` -/// let jump_fou_path = jump_fou(0.1, 0.2, 0.5, 0.3, 0.5, 1000, None, Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct JumpFou { +#[derive(Default)] +pub struct JumpFou +where + D: ProcessDistribution, +{ pub hurst: f64, pub mu: f64, pub sigma: f64, @@ -44,41 +17,78 @@ pub struct JumpFou { pub n: usize, pub x0: Option, pub t: Option, + pub m: Option, + pub jump_distribution: D, + fgn: Fgn, + cpoisson: CompoundPoisson, } -pub fn jump_fou(params: &JumpFou, jdistr: D) -> Array1 -where - D: Distribution + Copy, -{ - let JumpFou { - hurst, - mu, - sigma, - theta, - lambda, - n, - x0, - t, - } = params; - let dt = t.unwrap_or(1.0) / *n as f64; - let fgn = FgnFft::new(*hurst, *n, *t, None).sample(); - let mut jump_fou = Array1::::zeros(*n + 1); - jump_fou[0] = x0.unwrap_or(0.0); +impl JumpFou { + #[must_use] + pub fn new(params: &JumpFou) -> Self { + let fgn = Fgn::new(&Fgn { + hurst: params.hurst, + n: params.n, + t: params.t, + m: params.m, + ..Default::default() + }); + + let cpoisson = CompoundPoisson::new(&CompoundPoisson { + n: None, + lambda: params.lambda.unwrap(), + t_max: Some(params.t.unwrap_or(1.0) / params.n as f64), + distribution: params.jump_distribution, + m: params.m, + ..Default::default() + }); - for i in 1..(*n + 1) { - let [.., jumps] = compound_poisson( - &CompoundPoissonBuilder::default() - .lambda(lambda.unwrap()) - .t_max(dt) - .n(*n) - .build() - .unwrap(), - jdistr, + Self { + hurst: params.hurst, + mu: params.mu, + sigma: params.sigma, + theta: params.theta, + lambda: params.lambda, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + jump_distribution: params.jump_distribution, + fgn, + cpoisson, + } + } +} + +impl Sampling for JumpFou { + fn sample(&self) -> Array1 { + assert!( + self.hurst > 0.0 && self.hurst < 1.0, + "Hurst parameter must be in (0, 1)" ); - jump_fou[i] = - jump_fou[i - 1] + theta * (mu - jump_fou[i - 1]) * dt + sigma * fgn[i - 1] + jumps.sum(); + let dt = self.t.unwrap_or(1.0) / self.n as f64; + let fgn = self.fgn.sample(); + let mut jump_fou = Array1::::zeros(self.n + 1); + jump_fou[0] = self.x0.unwrap_or(0.0); + + for i in 1..(self.n + 1) { + let [.., jumps] = self.cpoisson.sample(); + + jump_fou[i] = jump_fou[i - 1] + + self.theta * (self.mu - jump_fou[i - 1]) * dt + + self.sigma * fgn[i - 1] + + jumps.sum(); + } + + jump_fou } - jump_fou + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } } diff --git a/stochastic-rs-core/src/lib.rs b/stochastic-rs-core/src/lib.rs index a9400e0..8296fd5 100644 --- a/stochastic-rs-core/src/lib.rs +++ b/stochastic-rs-core/src/lib.rs @@ -71,6 +71,9 @@ pub mod volatility; use ndarray::parallel::prelude::*; use ndarray::{Array1, Array2, Axis}; use ndrustfft::Zero; +use rand_distr::Distribution; + +pub trait ProcessDistribution: Distribution + Copy + Send + Sync + Default {} pub trait Sampling: Send + Sync { fn sample(&self) -> Array1; diff --git a/stochastic-rs-core/src/noises/cfgns.rs b/stochastic-rs-core/src/noises/cfgns.rs index 7e6ee45..60b2af1 100644 --- a/stochastic-rs-core/src/noises/cfgns.rs +++ b/stochastic-rs-core/src/noises/cfgns.rs @@ -17,7 +17,13 @@ pub struct Cfgns { impl Cfgns { #[must_use] pub fn new(params: &Self) -> Self { - let fgn = Fgn::new(params.hurst, params.n, params.t, params.m); + let fgn = Fgn::new(&Fgn { + hurst: params.hurst, + n: params.n, + t: params.t, + m: params.m, + ..Default::default() + }); Self { hurst: params.hurst, diff --git a/stochastic-rs-core/src/noises/fgn.rs b/stochastic-rs-core/src/noises/fgn.rs index 1b8202b..3dfb30e 100644 --- a/stochastic-rs-core/src/noises/fgn.rs +++ b/stochastic-rs-core/src/noises/fgn.rs @@ -9,28 +9,37 @@ use num_complex::{Complex, ComplexDistribution}; use crate::Sampling; pub struct Fgn { - hurst: f64, - n: usize, + pub hurst: f64, + pub n: usize, + pub t: Option, + pub m: Option, offset: usize, - t: f64, sqrt_eigenvalues: Arc>>, - m: Option, fft_handler: Arc>, } impl Default for Fgn { fn default() -> Self { - Self::new(0.7, 1000, None, None) + Self { + hurst: 0.5, + n: 1024, + t: None, + m: None, + offset: 0, + sqrt_eigenvalues: Arc::new(Array1::zeros(0)), + fft_handler: Arc::new(FftHandler::new(0)), + } } } impl Fgn { - pub fn new(hurst: f64, n: usize, t: Option, m: Option) -> Self { - if !(0.0..=1.0).contains(&hurst) { + #[must_use] + pub fn new(params: &Self) -> Self { + if !(0.0..=1.0).contains(¶ms.hurst) { panic!("Hurst parameter must be between 0 and 1"); } - let n_ = n.next_power_of_two(); - let offset = n_ - n; + let n_ = params.n.next_power_of_two(); + let offset = n_ - params.n; let n = n_; let mut r = Array1::linspace(0.0, n as f64, n + 1); r.mapv_inplace(|x| { @@ -38,7 +47,8 @@ impl Fgn { 1.0 } else { 0.5 - * ((x + 1.0).powf(2.0 * hurst) - 2.0 * x.powf(2.0 * hurst) + (x - 1.0).powf(2.0 * hurst)) + * ((x + 1.0).powf(2.0 * params.hurst) - 2.0 * x.powf(2.0 * params.hurst) + + (x - 1.0).powf(2.0 * params.hurst)) } }); let r = concatenate( @@ -54,12 +64,12 @@ impl Fgn { sqrt_eigenvalues.mapv_inplace(|x| Complex::new((x.re / (2.0 * n as f64)).sqrt(), x.im)); Self { - hurst, + hurst: params.hurst, n, offset, - t: t.unwrap_or(1.0), + t: params.t, sqrt_eigenvalues: Arc::new(sqrt_eigenvalues), - m, + m: params.m, fft_handler: Arc::new(FftHandler::new(2 * n)), } } @@ -74,7 +84,7 @@ impl Sampling for Fgn { let fgn = &*self.sqrt_eigenvalues * &rnd; let mut fgn_fft = Array1::>::zeros(2 * self.n); ndfft(&fgn, &mut fgn_fft, &*self.fft_handler, 0); - let scale = (self.n as f64).powf(-self.hurst) * self.t.powf(self.hurst); + let scale = (self.n as f64).powf(-self.hurst) * self.t.unwrap_or(1.0).powf(self.hurst); let fgn = fgn_fft .slice(s![1..self.n - self.offset + 1]) .mapv(|x: Complex| x.re * scale); @@ -98,7 +108,13 @@ mod tests { #[test] fn plot() { - let fgn = Fgn::new(0.9, 1000, Some(1.0), Some(1)); + let fgn = Fgn::new(&Fgn { + hurst: 0.7, + n: 1000, + t: Some(1.0), + m: None, + ..Default::default() + }); let mut plot = Plot::new(); let d = fgn.sample_par(); for data in d.axis_iter(Axis(0)) { diff --git a/stochastic-rs-core/src/processes/ccustom.rs b/stochastic-rs-core/src/processes/ccustom.rs index f53e462..df53703 100644 --- a/stochastic-rs-core/src/processes/ccustom.rs +++ b/stochastic-rs-core/src/processes/ccustom.rs @@ -2,15 +2,15 @@ use ndarray::{Array1, Axis}; use rand::thread_rng; use rand_distr::Distribution; -use crate::{Sampling, Sampling3D}; +use crate::{ProcessDistribution, Sampling, Sampling3D}; use super::customjt::CustomJt; #[derive(Default)] pub struct CompoundCustom where - D: Distribution + Copy + Send + Sync, - E: Distribution + Copy + Send + Sync, + D: ProcessDistribution, + E: ProcessDistribution, { pub n: Option, pub t_max: Option, @@ -22,8 +22,8 @@ where impl CompoundCustom where - D: Distribution + Copy + Send + Sync, - E: Distribution + Copy + Send + Sync, + D: ProcessDistribution, + E: ProcessDistribution, { #[must_use] pub fn new(params: &Self) -> Self { @@ -47,8 +47,8 @@ where impl Sampling3D for CompoundCustom where - D: Distribution + Copy + Send + Sync, - E: Distribution + Copy + Send + Sync, + D: ProcessDistribution, + E: ProcessDistribution, { fn sample(&self) -> [Array1; 3] { if self.n.is_none() && self.t_max.is_none() { diff --git a/stochastic-rs-core/src/processes/cpoisson.rs b/stochastic-rs-core/src/processes/cpoisson.rs index 4a501f6..5f5b2c8 100644 --- a/stochastic-rs-core/src/processes/cpoisson.rs +++ b/stochastic-rs-core/src/processes/cpoisson.rs @@ -1,41 +1,14 @@ use ndarray::{Array1, Axis}; use rand::thread_rng; -use rand_distr::Distribution; -use crate::{Sampling, Sampling3D}; +use crate::{ProcessDistribution, Sampling, Sampling3D}; use super::poisson::Poisson; -/// Generates a compound Poisson process. -/// -/// The compound Poisson process models the occurrence of events over time, where each event has a random magnitude (jump). It is commonly used in insurance and finance. -/// -/// # Parameters -/// -/// - `n`: Number of time steps. -/// - `lambda`: Rate parameter (average number of events per unit time). -/// - `jumps`: Vector of jump sizes (optional). -/// - `t_max`: Maximum time (optional, defaults to 1.0). -/// - `jump_mean`: Mean of the jump sizes (optional, defaults to 0.0). -/// - `jump_std`: Standard deviation of the jump sizes (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `(Array1, Array1, Array1)` representing the exponetial times from Poisson, generated compound Poisson cumulative process path and the jumps. -/// -/// # Panics -/// -/// Panics if `n` is zero. -/// -/// # Example -/// -/// ``` -/// let (p, cum_cp, cp) = compound_poisson(1000, 2.0, None, Some(10.0), Some(0.0), Some(1.0)); -/// ``` - +#[derive(Default)] pub struct CompoundPoisson where - D: Distribution + Copy + Send + Sync, + D: ProcessDistribution, { pub n: Option, pub lambda: f64, @@ -45,7 +18,7 @@ where poisson: Poisson, } -impl + Copy + Send + Sync> CompoundPoisson { +impl CompoundPoisson { #[must_use] pub fn new(params: &Self) -> Self { let poisson = Poisson::new(&Poisson { @@ -66,7 +39,7 @@ impl + Copy + Send + Sync> CompoundPoisson { } } -impl + Copy + Send + Sync> Sampling3D for CompoundPoisson { +impl Sampling3D for CompoundPoisson { fn sample(&self) -> [Array1; 3] { if self.n.is_none() && self.t_max.is_none() { panic!("n or t_max must be provided"); diff --git a/stochastic-rs-core/src/processes/customjt.rs b/stochastic-rs-core/src/processes/customjt.rs index 39cfd87..a8d8337 100644 --- a/stochastic-rs-core/src/processes/customjt.rs +++ b/stochastic-rs-core/src/processes/customjt.rs @@ -1,14 +1,13 @@ use ndarray::{Array0, Array1, Axis, Dim}; -use ndarray_rand::rand_distr::Distribution; use ndarray_rand::RandomExt; use rand::thread_rng; -use crate::Sampling; +use crate::{ProcessDistribution, Sampling}; #[derive(Default)] pub struct CustomJt where - D: Distribution + Copy + Send + Sync, + D: ProcessDistribution, { pub n: Option, pub t_max: Option, @@ -16,7 +15,7 @@ where pub distribution: D, } -impl + Copy + Send + Sync> CustomJt { +impl CustomJt { #[must_use] pub fn new(params: &Self) -> Self { Self { @@ -28,7 +27,7 @@ impl + Copy + Send + Sync> CustomJt { } } -impl + Copy + Send + Sync> Sampling for CustomJt { +impl Sampling for CustomJt { fn sample(&self) -> Array1 { if let Some(n) = self.n { let random = Array1::random(n, self.distribution); diff --git a/stochastic-rs-core/src/processes/fbm.rs b/stochastic-rs-core/src/processes/fbm.rs index c71a8f3..ac9f0c9 100644 --- a/stochastic-rs-core/src/processes/fbm.rs +++ b/stochastic-rs-core/src/processes/fbm.rs @@ -17,7 +17,13 @@ impl Fbm { panic!("Hurst parameter must be in (0, 1)") } - let fgn = Fgn::new(params.hurst, params.n, params.t, params.m); + let fgn = Fgn::new(&Fgn { + hurst: params.hurst, + n: params.n, + t: params.t, + m: params.m, + ..Default::default() + }); Self { hurst: params.hurst, From 12087a478bdd4c361a6a68544251915ba65972d8 Mon Sep 17 00:00:00 2001 From: Daniel Boros Date: Thu, 12 Sep 2024 01:27:27 +0200 Subject: [PATCH 11/11] reafactor: reorganize modules --- .../src/{diffusions/mod.rs => diffusion.rs} | 0 .../src/{diffusions => diffusion}/cir.rs | 0 .../src/{diffusions => diffusion}/fcir.rs | 14 +--- .../src/{diffusions => diffusion}/fgbm.rs | 10 +-- .../src/{diffusions => diffusion}/fjacobi.rs | 12 +-- .../src/{diffusions => diffusion}/fou.rs | 12 +-- .../src/{diffusions => diffusion}/gbm.rs | 0 .../src/{diffusions => diffusion}/jacobi.rs | 0 .../src/{diffusions => diffusion}/ou.rs | 0 .../src/{interest/mod.rs => interest.rs} | 0 stochastic-rs-core/src/interest/duffie_kan.rs | 4 +- stochastic-rs-core/src/interest/fvasicek.rs | 4 +- stochastic-rs-core/src/interest/mvasicek.rs | 48 ----------- stochastic-rs-core/src/interest/vasicek.rs | 4 +- stochastic-rs-core/src/jump.rs | 7 ++ .../src/{jumps => jump}/bates.rs | 6 +- stochastic-rs-core/src/{jumps => jump}/ig.rs | 0 .../src/{jumps => jump}/jump_fou.rs | 14 +--- stochastic-rs-core/src/jump/levy_diffusion.rs | 74 +++++++++++++++++ stochastic-rs-core/src/jump/merton.rs | 79 ++++++++++++++++++ stochastic-rs-core/src/jump/nig.rs | 61 ++++++++++++++ stochastic-rs-core/src/jump/vg.rs | 64 +++++++++++++++ .../src/jumps/levy_diffusion.rs | 75 ----------------- stochastic-rs-core/src/jumps/merton.rs | 81 ------------------- stochastic-rs-core/src/jumps/mod.rs | 41 ---------- stochastic-rs-core/src/jumps/nig.rs | 64 --------------- stochastic-rs-core/src/jumps/vg.rs | 67 --------------- stochastic-rs-core/src/lib.rs | 41 +--------- stochastic-rs-core/src/main.rs | 2 +- .../src/{noises/mod.rs => noise.rs} | 0 .../src/{noises => noise}/cfgns.rs | 10 +-- .../src/{noises => noise}/cgns.rs | 0 .../src/{noises => noise}/fgn.rs | 41 +++------- .../src/{processes/mod.rs => process.rs} | 0 .../src/{processes => process}/bm.rs | 0 .../src/{processes => process}/cbms.rs | 4 +- .../src/{processes => process}/ccustom.rs | 3 +- .../src/{processes => process}/cfbms.rs | 4 +- .../src/{processes => process}/cpoisson.rs | 2 +- .../src/{processes => process}/customjt.rs | 0 .../src/{processes => process}/fbm.rs | 12 +-- .../src/{processes => process}/poisson.rs | 0 .../src/{volatility/mod.rs => volatility.rs} | 0 stochastic-rs-core/src/volatility/heston.rs | 4 +- stochastic-rs-core/src/volatility/sabr.rs | 4 +- 45 files changed, 343 insertions(+), 525 deletions(-) rename stochastic-rs-core/src/{diffusions/mod.rs => diffusion.rs} (100%) rename stochastic-rs-core/src/{diffusions => diffusion}/cir.rs (100%) rename stochastic-rs-core/src/{diffusions => diffusion}/fcir.rs (84%) rename stochastic-rs-core/src/{diffusions => diffusion}/fgbm.rs (84%) rename stochastic-rs-core/src/{diffusions => diffusion}/fjacobi.rs (88%) rename stochastic-rs-core/src/{diffusions => diffusion}/fou.rs (84%) rename stochastic-rs-core/src/{diffusions => diffusion}/gbm.rs (100%) rename stochastic-rs-core/src/{diffusions => diffusion}/jacobi.rs (100%) rename stochastic-rs-core/src/{diffusions => diffusion}/ou.rs (100%) rename stochastic-rs-core/src/{interest/mod.rs => interest.rs} (100%) delete mode 100644 stochastic-rs-core/src/interest/mvasicek.rs create mode 100644 stochastic-rs-core/src/jump.rs rename stochastic-rs-core/src/{jumps => jump}/bates.rs (94%) rename stochastic-rs-core/src/{jumps => jump}/ig.rs (100%) rename stochastic-rs-core/src/{jumps => jump}/jump_fou.rs (85%) create mode 100644 stochastic-rs-core/src/jump/levy_diffusion.rs create mode 100644 stochastic-rs-core/src/jump/merton.rs create mode 100644 stochastic-rs-core/src/jump/nig.rs create mode 100644 stochastic-rs-core/src/jump/vg.rs delete mode 100644 stochastic-rs-core/src/jumps/levy_diffusion.rs delete mode 100644 stochastic-rs-core/src/jumps/merton.rs delete mode 100644 stochastic-rs-core/src/jumps/mod.rs delete mode 100644 stochastic-rs-core/src/jumps/nig.rs delete mode 100644 stochastic-rs-core/src/jumps/vg.rs rename stochastic-rs-core/src/{noises/mod.rs => noise.rs} (100%) rename stochastic-rs-core/src/{noises => noise}/cfgns.rs (87%) rename stochastic-rs-core/src/{noises => noise}/cgns.rs (100%) rename stochastic-rs-core/src/{noises => noise}/fgn.rs (76%) rename stochastic-rs-core/src/{processes/mod.rs => process.rs} (100%) rename stochastic-rs-core/src/{processes => process}/bm.rs (100%) rename stochastic-rs-core/src/{processes => process}/cbms.rs (94%) rename stochastic-rs-core/src/{processes => process}/ccustom.rs (96%) rename stochastic-rs-core/src/{processes => process}/cfbms.rs (95%) rename stochastic-rs-core/src/{processes => process}/cpoisson.rs (98%) rename stochastic-rs-core/src/{processes => process}/customjt.rs (100%) rename stochastic-rs-core/src/{processes => process}/fbm.rs (87%) rename stochastic-rs-core/src/{processes => process}/poisson.rs (100%) rename stochastic-rs-core/src/{volatility/mod.rs => volatility.rs} (100%) diff --git a/stochastic-rs-core/src/diffusions/mod.rs b/stochastic-rs-core/src/diffusion.rs similarity index 100% rename from stochastic-rs-core/src/diffusions/mod.rs rename to stochastic-rs-core/src/diffusion.rs diff --git a/stochastic-rs-core/src/diffusions/cir.rs b/stochastic-rs-core/src/diffusion/cir.rs similarity index 100% rename from stochastic-rs-core/src/diffusions/cir.rs rename to stochastic-rs-core/src/diffusion/cir.rs diff --git a/stochastic-rs-core/src/diffusions/fcir.rs b/stochastic-rs-core/src/diffusion/fcir.rs similarity index 84% rename from stochastic-rs-core/src/diffusions/fcir.rs rename to stochastic-rs-core/src/diffusion/fcir.rs index 86f7ba4..d9e72f8 100644 --- a/stochastic-rs-core/src/diffusions/fcir.rs +++ b/stochastic-rs-core/src/diffusion/fcir.rs @@ -1,6 +1,6 @@ use ndarray::Array1; -use crate::{noises::fgn::Fgn, Sampling}; +use crate::{noise::fgn::Fgn, Sampling}; #[derive(Default)] pub struct Fcir { @@ -13,19 +13,13 @@ pub struct Fcir { pub t: Option, pub use_sym: Option, pub m: Option, - fgn: Fgn, + pub fgn: Fgn, } impl Fcir { #[must_use] pub fn new(params: &Self) -> Self { - let fgn = Fgn::new(&Fgn { - hurst: params.hurst, - n: params.n, - t: params.t, - m: params.m, - ..Default::default() - }); + let fgn = Fgn::new(params.hurst, params.n, params.t, params.m); Self { hurst: params.hurst, @@ -49,7 +43,7 @@ impl Sampling for Fcir { "2 * theta * mu < sigma^2" ); - let fgn = self.sample(); + let fgn = self.fgn.sample(); let dt = self.t.unwrap_or(1.0) / self.n as f64; let mut fcir = Array1::::zeros(self.n + 1); diff --git a/stochastic-rs-core/src/diffusions/fgbm.rs b/stochastic-rs-core/src/diffusion/fgbm.rs similarity index 84% rename from stochastic-rs-core/src/diffusions/fgbm.rs rename to stochastic-rs-core/src/diffusion/fgbm.rs index f8f935a..368e494 100644 --- a/stochastic-rs-core/src/diffusions/fgbm.rs +++ b/stochastic-rs-core/src/diffusion/fgbm.rs @@ -1,6 +1,6 @@ use ndarray::Array1; -use crate::{noises::fgn::Fgn, Sampling}; +use crate::{noise::fgn::Fgn, Sampling}; #[derive(Default)] pub struct Fgbm { @@ -17,13 +17,7 @@ pub struct Fgbm { impl Fgbm { #[must_use] pub fn new(params: &Self) -> Self { - let fgn = Fgn::new(&Fgn { - hurst: params.hurst, - n: params.n, - t: params.t, - m: params.m, - ..Default::default() - }); + let fgn = Fgn::new(params.hurst, params.n, params.t, params.m); Self { hurst: params.hurst, diff --git a/stochastic-rs-core/src/diffusions/fjacobi.rs b/stochastic-rs-core/src/diffusion/fjacobi.rs similarity index 88% rename from stochastic-rs-core/src/diffusions/fjacobi.rs rename to stochastic-rs-core/src/diffusion/fjacobi.rs index 39643a5..f26e558 100644 --- a/stochastic-rs-core/src/diffusions/fjacobi.rs +++ b/stochastic-rs-core/src/diffusion/fjacobi.rs @@ -1,6 +1,6 @@ use ndarray::Array1; -use crate::{noises::fgn::Fgn, Sampling}; +use crate::{noise::fgn::Fgn, Sampling}; #[derive(Default)] pub struct Fjacobi { @@ -12,19 +12,13 @@ pub struct Fjacobi { pub x0: Option, pub t: Option, pub m: Option, - fgn: Fgn, + pub fgn: Fgn, } impl Fjacobi { #[must_use] pub fn new(params: &Self) -> Self { - let fgn = Fgn::new(&Fgn { - hurst: params.hurst, - n: params.n, - t: params.t, - m: params.m, - ..Default::default() - }); + let fgn = Fgn::new(params.hurst, params.n, params.t, params.m); Self { hurst: params.hurst, diff --git a/stochastic-rs-core/src/diffusions/fou.rs b/stochastic-rs-core/src/diffusion/fou.rs similarity index 84% rename from stochastic-rs-core/src/diffusions/fou.rs rename to stochastic-rs-core/src/diffusion/fou.rs index df287c1..e31b9dc 100644 --- a/stochastic-rs-core/src/diffusions/fou.rs +++ b/stochastic-rs-core/src/diffusion/fou.rs @@ -1,6 +1,6 @@ use ndarray::Array1; -use crate::{noises::fgn::Fgn, Sampling}; +use crate::{noise::fgn::Fgn, Sampling}; #[derive(Default)] pub struct Fou { @@ -12,19 +12,13 @@ pub struct Fou { pub x0: Option, pub t: Option, pub m: Option, - fgn: Fgn, + pub fgn: Fgn, } impl Fou { #[must_use] pub fn new(params: &Self) -> Self { - let fgn = Fgn::new(&Fgn { - hurst: params.hurst, - n: params.n, - t: params.t, - m: params.m, - ..Default::default() - }); + let fgn = Fgn::new(params.hurst, params.n, params.t, params.m); Self { hurst: params.hurst, diff --git a/stochastic-rs-core/src/diffusions/gbm.rs b/stochastic-rs-core/src/diffusion/gbm.rs similarity index 100% rename from stochastic-rs-core/src/diffusions/gbm.rs rename to stochastic-rs-core/src/diffusion/gbm.rs diff --git a/stochastic-rs-core/src/diffusions/jacobi.rs b/stochastic-rs-core/src/diffusion/jacobi.rs similarity index 100% rename from stochastic-rs-core/src/diffusions/jacobi.rs rename to stochastic-rs-core/src/diffusion/jacobi.rs diff --git a/stochastic-rs-core/src/diffusions/ou.rs b/stochastic-rs-core/src/diffusion/ou.rs similarity index 100% rename from stochastic-rs-core/src/diffusions/ou.rs rename to stochastic-rs-core/src/diffusion/ou.rs diff --git a/stochastic-rs-core/src/interest/mod.rs b/stochastic-rs-core/src/interest.rs similarity index 100% rename from stochastic-rs-core/src/interest/mod.rs rename to stochastic-rs-core/src/interest.rs diff --git a/stochastic-rs-core/src/interest/duffie_kan.rs b/stochastic-rs-core/src/interest/duffie_kan.rs index 6b9232c..225e812 100644 --- a/stochastic-rs-core/src/interest/duffie_kan.rs +++ b/stochastic-rs-core/src/interest/duffie_kan.rs @@ -1,6 +1,6 @@ use ndarray::Array1; -use crate::{noises::cgns::Cgns, Sampling2D}; +use crate::{noise::cgns::Cgns, Sampling2D}; #[derive(Default)] @@ -22,7 +22,7 @@ pub struct DuffieKan { pub x0: Option, pub t: Option, pub m: Option, - cgns: Cgns, + pub cgns: Cgns, } impl DuffieKan { diff --git a/stochastic-rs-core/src/interest/fvasicek.rs b/stochastic-rs-core/src/interest/fvasicek.rs index b3a8e9a..a078eb8 100644 --- a/stochastic-rs-core/src/interest/fvasicek.rs +++ b/stochastic-rs-core/src/interest/fvasicek.rs @@ -1,6 +1,6 @@ use ndarray::Array1; -use crate::{diffusions::fou::Fou, Sampling}; +use crate::{diffusion::fou::Fou, Sampling}; #[derive(Default)] pub struct Fvasicek { @@ -12,7 +12,7 @@ pub struct Fvasicek { pub x0: Option, pub t: Option, pub m: Option, - fou: Fou, + pub fou: Fou, } impl Fvasicek { diff --git a/stochastic-rs-core/src/interest/mvasicek.rs b/stochastic-rs-core/src/interest/mvasicek.rs deleted file mode 100644 index e64a8c9..0000000 --- a/stochastic-rs-core/src/interest/mvasicek.rs +++ /dev/null @@ -1,48 +0,0 @@ -use derive_builder::Builder; -use ndarray::{Array1, Array2, Axis}; -use rayon::prelude::*; - -use super::vasicek::{self, Vasicek}; - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct Mvasicek { - pub mu: Array1, - pub sigma: Array1, - pub theta: Option, - pub n: usize, - pub m: usize, - pub x0: Option, - pub t: Option, -} - -pub fn mvasicek(params: &Mvasicek) -> Array2 { - let Mvasicek { - mu, - sigma, - theta, - n, - m, - x0, - t, - } = params; - - let mut xs = Array2::::zeros((*m, *n)); - - for i in 0..*m { - let vasicek = Vasicek { - mu: mu[i], - sigma: sigma[i], - theta: *theta, - n: *n, - x0: *x0, - t: *t, - }; - - xs.axis_iter_mut(Axis(0)).into_par_iter().for_each(|mut x| { - x.assign(&vasicek::vasicek(&vasicek)); - }) - } - - xs -} diff --git a/stochastic-rs-core/src/interest/vasicek.rs b/stochastic-rs-core/src/interest/vasicek.rs index 199f3fe..9322816 100644 --- a/stochastic-rs-core/src/interest/vasicek.rs +++ b/stochastic-rs-core/src/interest/vasicek.rs @@ -1,6 +1,6 @@ use ndarray::Array1; -use crate::{diffusions::ou::Ou, Sampling}; +use crate::{diffusion::ou::Ou, Sampling}; #[derive(Default)] pub struct Vasicek { @@ -11,7 +11,7 @@ pub struct Vasicek { pub x0: Option, pub t: Option, pub m: Option, - ou: Ou, + pub ou: Ou, } impl Vasicek { diff --git a/stochastic-rs-core/src/jump.rs b/stochastic-rs-core/src/jump.rs new file mode 100644 index 0000000..ec5fa81 --- /dev/null +++ b/stochastic-rs-core/src/jump.rs @@ -0,0 +1,7 @@ +pub mod bates; +pub mod ig; +pub mod jump_fou; +pub mod levy_diffusion; +pub mod merton; +pub mod nig; +pub mod vg; diff --git a/stochastic-rs-core/src/jumps/bates.rs b/stochastic-rs-core/src/jump/bates.rs similarity index 94% rename from stochastic-rs-core/src/jumps/bates.rs rename to stochastic-rs-core/src/jump/bates.rs index e4b22d6..cb14f3f 100644 --- a/stochastic-rs-core/src/jumps/bates.rs +++ b/stochastic-rs-core/src/jump/bates.rs @@ -1,7 +1,7 @@ use ndarray::Array1; use crate::{ - noises::cgns::Cgns, processes::cpoisson::CompoundPoisson, ProcessDistribution, Sampling2D, + noise::cgns::Cgns, process::cpoisson::CompoundPoisson, ProcessDistribution, Sampling2D, Sampling3D, }; @@ -27,8 +27,8 @@ where pub use_sym: Option, pub m: Option, pub jumps_distribution: D, - cgns: Cgns, - cpoisson: CompoundPoisson, + pub cgns: Cgns, + pub cpoisson: CompoundPoisson, } impl Bates1996 { diff --git a/stochastic-rs-core/src/jumps/ig.rs b/stochastic-rs-core/src/jump/ig.rs similarity index 100% rename from stochastic-rs-core/src/jumps/ig.rs rename to stochastic-rs-core/src/jump/ig.rs diff --git a/stochastic-rs-core/src/jumps/jump_fou.rs b/stochastic-rs-core/src/jump/jump_fou.rs similarity index 85% rename from stochastic-rs-core/src/jumps/jump_fou.rs rename to stochastic-rs-core/src/jump/jump_fou.rs index 8a5301f..913a283 100644 --- a/stochastic-rs-core/src/jumps/jump_fou.rs +++ b/stochastic-rs-core/src/jump/jump_fou.rs @@ -1,7 +1,7 @@ use ndarray::Array1; use crate::{ - noises::fgn::Fgn, processes::cpoisson::CompoundPoisson, ProcessDistribution, Sampling, Sampling3D, + noise::fgn::Fgn, process::cpoisson::CompoundPoisson, ProcessDistribution, Sampling, Sampling3D, }; #[derive(Default)] @@ -19,20 +19,14 @@ where pub t: Option, pub m: Option, pub jump_distribution: D, - fgn: Fgn, - cpoisson: CompoundPoisson, + pub fgn: Fgn, + pub cpoisson: CompoundPoisson, } impl JumpFou { #[must_use] pub fn new(params: &JumpFou) -> Self { - let fgn = Fgn::new(&Fgn { - hurst: params.hurst, - n: params.n, - t: params.t, - m: params.m, - ..Default::default() - }); + let fgn = Fgn::new(params.hurst, params.n, params.t, params.m); let cpoisson = CompoundPoisson::new(&CompoundPoisson { n: None, diff --git a/stochastic-rs-core/src/jump/levy_diffusion.rs b/stochastic-rs-core/src/jump/levy_diffusion.rs new file mode 100644 index 0000000..da268e4 --- /dev/null +++ b/stochastic-rs-core/src/jump/levy_diffusion.rs @@ -0,0 +1,74 @@ +use ndarray::Array1; +use ndarray_rand::RandomExt; +use rand_distr::Normal; + +use crate::{process::cpoisson::CompoundPoisson, ProcessDistribution, Sampling, Sampling3D}; + +#[derive(Default)] +pub struct LevyDiffusion +where + D: ProcessDistribution, +{ + pub gamma: f64, + pub sigma: f64, + pub lambda: f64, + pub n: usize, + pub x0: Option, + pub t: Option, + pub m: Option, + pub jump_distribution: D, + pub cpoisson: CompoundPoisson, +} + +impl LevyDiffusion { + #[must_use] + pub fn new(params: &LevyDiffusion) -> Self { + let cpoisson = CompoundPoisson::new(&CompoundPoisson { + n: None, + lambda: params.lambda, + t_max: Some(params.t.unwrap_or(1.0) / params.n as f64), + distribution: params.jump_distribution, + m: params.m, + ..Default::default() + }); + + Self { + gamma: params.gamma, + sigma: params.sigma, + lambda: params.lambda, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + jump_distribution: params.jump_distribution, + cpoisson, + } + } +} + +impl Sampling for LevyDiffusion { + fn sample(&self) -> Array1 { + let dt = self.t.unwrap_or(1.0) / self.n as f64; + let mut levy = Array1::::zeros(self.n + 1); + levy[0] = self.x0.unwrap_or(0.0); + let gn = Array1::random( + self.n, + Normal::new(0.0, (self.t.unwrap_or(1.0) / self.n as f64).sqrt()).unwrap(), + ); + + for i in 1..(self.n + 1) { + let [.., jumps] = self.cpoisson.sample(); + levy[i] = levy[i - 1] + self.gamma * dt + self.sigma * gn[i - 1] + jumps.sum(); + } + + levy + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } +} diff --git a/stochastic-rs-core/src/jump/merton.rs b/stochastic-rs-core/src/jump/merton.rs new file mode 100644 index 0000000..481f8ea --- /dev/null +++ b/stochastic-rs-core/src/jump/merton.rs @@ -0,0 +1,79 @@ +use ndarray::Array1; +use ndarray_rand::RandomExt; +use rand_distr::Normal; + +use crate::{process::cpoisson::CompoundPoisson, ProcessDistribution, Sampling, Sampling3D}; + +#[derive(Default)] +pub struct Merton +where + D: ProcessDistribution, +{ + pub alpha: f64, + pub sigma: f64, + pub lambda: f64, + pub theta: f64, + pub n: usize, + pub x0: Option, + pub t: Option, + pub m: Option, + pub jump_distribution: D, + pub cpoisson: CompoundPoisson, +} + +impl Merton { + #[must_use] + pub fn new(params: &Merton) -> Self { + let cpoisson = CompoundPoisson::new(&CompoundPoisson { + n: None, + lambda: params.lambda, + t_max: Some(params.t.unwrap_or(1.0) / params.n as f64), + distribution: params.jump_distribution, + m: params.m, + ..Default::default() + }); + + Self { + alpha: params.alpha, + sigma: params.sigma, + lambda: params.lambda, + theta: params.theta, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + jump_distribution: params.jump_distribution, + cpoisson, + } + } +} + +impl Sampling for Merton { + fn sample(&self) -> Array1 { + let dt = self.t.unwrap_or(1.0) / self.n as f64; + let mut merton = Array1::::zeros(self.n + 1); + merton[0] = self.x0.unwrap_or(0.0); + let gn = Array1::random( + self.n, + Normal::new(0.0, (self.t.unwrap_or(1.0) / self.n as f64).sqrt()).unwrap(), + ); + + for i in 1..(self.n + 1) { + let [.., jumps] = self.cpoisson.sample(); + merton[i] = merton[i - 1] + + (self.alpha * self.sigma.powf(2.0) / 2.0 - self.lambda * self.theta) * dt + + self.sigma * gn[i - 1] + + jumps.sum(); + } + + merton + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } +} diff --git a/stochastic-rs-core/src/jump/nig.rs b/stochastic-rs-core/src/jump/nig.rs new file mode 100644 index 0000000..a0eebbf --- /dev/null +++ b/stochastic-rs-core/src/jump/nig.rs @@ -0,0 +1,61 @@ +use ndarray::Array1; +use ndarray_rand::{rand_distr::InverseGaussian, RandomExt}; +use rand_distr::Normal; + +use crate::Sampling; + +#[derive(Default)] + +pub struct Nig { + pub theta: f64, + pub sigma: f64, + pub kappa: f64, + pub n: usize, + pub x0: Option, + pub t: Option, + pub m: Option, +} + +impl Nig { + #[must_use] + pub fn new(params: &Self) -> Self { + Self { + theta: params.theta, + sigma: params.sigma, + kappa: params.kappa, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + } + } +} + +impl Sampling for Nig { + fn sample(&self) -> Array1 { + let dt = self.t.unwrap_or(1.0) / self.n as f64; + let scale = dt.powf(2.0) / self.kappa; + let mean = dt / scale; + let ig = Array1::random(self.n, InverseGaussian::new(mean, scale).unwrap()); + let gn = Array1::random( + self.n, + Normal::new(0.0, (self.t.unwrap_or(1.0) / self.n as f64).sqrt()).unwrap(), + ); + let mut nig = Array1::zeros(self.n + 1); + nig[0] = self.x0.unwrap_or(0.0); + + for i in 1..(self.n + 1) { + nig[i] = nig[i - 1] + self.theta * ig[i - 1] + self.sigma * ig[i - 1].sqrt() * gn[i - 1] + } + + nig + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } +} diff --git a/stochastic-rs-core/src/jump/vg.rs b/stochastic-rs-core/src/jump/vg.rs new file mode 100644 index 0000000..fef7947 --- /dev/null +++ b/stochastic-rs-core/src/jump/vg.rs @@ -0,0 +1,64 @@ +use ndarray::Array1; +use ndarray_rand::rand_distr::Gamma; +use ndarray_rand::RandomExt; +use rand_distr::Normal; + +use crate::Sampling; + +#[derive(Default)] +pub struct Vg { + pub mu: f64, + pub sigma: f64, + pub nu: f64, + pub n: usize, + pub x0: Option, + pub t: Option, + pub m: Option, +} + +impl Vg { + #[must_use] + pub fn new(params: &Vg) -> Self { + Self { + mu: params.mu, + sigma: params.sigma, + nu: params.nu, + n: params.n, + x0: params.x0, + t: params.t, + m: params.m, + } + } +} + +impl Sampling for Vg { + fn sample(&self) -> Array1 { + let dt = self.t.unwrap_or(1.0) / self.n as f64; + + let shape = dt / self.nu; + let scale = self.nu; + + let mut vg = Array1::::zeros(self.n + 1); + vg[0] = self.x0.unwrap_or(0.0); + + let gn = Array1::random( + self.n, + Normal::new(0.0, (self.t.unwrap_or(1.0) / self.n as f64).sqrt()).unwrap(), + ); + let gammas = Array1::random(self.n, Gamma::new(shape, scale).unwrap()); + + for i in 1..(self.n + 1) { + vg[i] = vg[i - 1] + self.mu * gammas[i - 1] + self.sigma * gammas[i - 1].sqrt() * gn[i - 1]; + } + + vg + } + + fn n(&self) -> usize { + self.n + } + + fn m(&self) -> Option { + self.m + } +} diff --git a/stochastic-rs-core/src/jumps/levy_diffusion.rs b/stochastic-rs-core/src/jumps/levy_diffusion.rs deleted file mode 100644 index 80d2396..0000000 --- a/stochastic-rs-core/src/jumps/levy_diffusion.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{ - noises::gn::gn, - processes::cpoisson::{compound_poisson, CompoundPoisson}, -}; -use derive_builder::Builder; -use ndarray::Array1; -use rand_distr::Distribution; - -/// Generates a path of the Lévy diffusion process. -/// -/// The Lévy diffusion process incorporates both Gaussian and jump components, often used in financial modeling. -/// -/// # Parameters -/// -/// - `gamma`: Drift parameter. -/// - `sigma`: Volatility parameter. -/// - `lambda`: Jump intensity. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated Lévy diffusion process path. -/// -/// # Example -/// -/// ``` -/// let levy_path = levy_diffusion(0.1, 0.2, 0.5, 1000, Some(0.0), Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct LevyDiffusion { - pub gamma: f64, - pub sigma: f64, - pub lambda: f64, - pub n: usize, - pub x0: Option, - pub t: Option, -} - -pub fn levy_diffusion(params: &LevyDiffusion, jdistr: D) -> Array1 -where - D: Distribution + Copy, -{ - let LevyDiffusion { - gamma, - sigma, - lambda, - n, - x0, - t, - } = *params; - - let dt = t.unwrap_or(1.0) / n as f64; - let mut levy = Array1::::zeros(n + 1); - levy[0] = x0.unwrap_or(0.0); - let gn = gn(n, t); - - for i in 1..(n + 1) { - let [.., jumps] = compound_poisson( - &CompoundPoisson { - lambda, - t_max: Some(dt), - n: None, - }, - jdistr, - ); - - levy[i] = levy[i - 1] + gamma * dt + sigma * gn[i - 1] + jumps.sum(); - } - - levy -} diff --git a/stochastic-rs-core/src/jumps/merton.rs b/stochastic-rs-core/src/jumps/merton.rs deleted file mode 100644 index e3af883..0000000 --- a/stochastic-rs-core/src/jumps/merton.rs +++ /dev/null @@ -1,81 +0,0 @@ -use derive_builder::Builder; -use ndarray::Array1; -use rand_distr::Distribution; - -use crate::{ - noises::gn::gn, - processes::cpoisson::{compound_poisson, CompoundPoisson}, -}; - -/// Generates a path of the Merton jump diffusion process. -/// -/// The Merton jump diffusion process combines a continuous diffusion process with jumps, commonly used in financial modeling. -/// -/// # Parameters -/// -/// - `alpha`: Drift parameter. -/// - `sigma`: Volatility parameter. -/// - `lambda`: Jump intensity. -/// - `theta`: Jump size. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated Merton jump diffusion process path. -/// -/// # Example -/// -/// ``` -/// let merton_path = merton(0.1, 0.2, 0.5, 0.05, 1000, Some(0.0), Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct Merton { - pub alpha: f64, - pub sigma: f64, - pub lambda: f64, - pub theta: f64, - pub n: usize, - pub x0: Option, - pub t: Option, -} - -pub fn merton(params: &Merton, jdistr: D) -> Array1 -where - D: Distribution + Copy, -{ - let Merton { - alpha, - sigma, - lambda, - theta, - n, - x0, - t, - } = *params; - let dt = t.unwrap_or(1.0) / n as f64; - let mut merton = Array1::::zeros(n + 1); - merton[0] = x0.unwrap_or(0.0); - let gn = gn(n, t); - - for i in 1..(n + 1) { - let [.., jumps] = compound_poisson( - &CompoundPoisson { - lambda, - t_max: Some(dt), - n: None, - }, - jdistr, - ); - - merton[i] = merton[i - 1] - + (alpha * sigma.powf(2.0) / 2.0 - lambda * theta) * dt - + sigma * gn[i - 1] - + jumps.sum(); - } - - merton -} diff --git a/stochastic-rs-core/src/jumps/mod.rs b/stochastic-rs-core/src/jumps/mod.rs deleted file mode 100644 index d4ad105..0000000 --- a/stochastic-rs-core/src/jumps/mod.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! This module contains the implementations of various diffusion processes. -//! -//! The following diffusion processes are implemented: -//! -//! - **Bates (1996) Model** -//! - Combines stochastic volatility with jump diffusion. -//! - SDE: `dX(t) = mu * S(t) * dt + S(t) * sqrt(V(t)) * dW(t) + Jumps` -//! -//! - **Inverse Gaussian (IG) Process** -//! - Models heavy-tailed distributions. -//! - SDE: `dX(t) = gamma * dt + dW(t)` -//! -//! - **Jump-Fractional Ornstein-Uhlenbeck (JFOU) Process** -//! - Combines jumps with fractional Ornstein-Uhlenbeck process. -//! - SDE: `dX(t) = theta(mu - X(t))dt + sigma dW^H(t) + Jumps` -//! -//! - **Normal Inverse Gaussian (NIG) Process** -//! - Models stock returns with normal and inverse Gaussian components. -//! - SDE: `dX(t) = theta * IG(t) + sigma * sqrt(IG(t)) * dW(t)` -//! -//! - **Lévy Diffusion Process** -//! - Incorporates Gaussian and jump components. -//! - SDE: `dX(t) = gamma * dt + sigma * dW(t) * Jump` -//! -//! - **Merton Jump Diffusion Process** -//! - Combines continuous diffusion with jumps. -//! - SDE: `dX(t) = (alpha * sigma^2 / 2 - lambda * theta) * dt + sigma * dW(t) + Jumps` -//! -//! - **Variance Gamma (VG) Process** -//! - Captures excess kurtosis and skewness in asset returns. -//! - SDE: `dX(t) = mu * Gamma(t) + sigma * sqrt(Gamma(t)) * dW(t)` -//! -//! Each process has its own module and functions to generate sample paths. - -pub mod bates; -pub mod ig; -pub mod jump_fou; -pub mod levy_diffusion; -pub mod merton; -pub mod nig; -pub mod vg; diff --git a/stochastic-rs-core/src/jumps/nig.rs b/stochastic-rs-core/src/jumps/nig.rs deleted file mode 100644 index c88b5bd..0000000 --- a/stochastic-rs-core/src/jumps/nig.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::noises::gn::gn; - -use derive_builder::Builder; -use ndarray::Array1; -use ndarray_rand::{rand_distr::InverseGaussian, RandomExt}; - -/// Generates a path of the Normal Inverse Gaussian (NIG) process. -/// -/// The NIG process is used in financial mathematics to model stock returns. -/// -/// # Parameters -/// -/// - `theta`: Drift parameter. -/// - `sigma`: Volatility parameter. -/// - `kappa`: Shape parameter of the Inverse Gaussian distribution. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated NIG process path. -/// -/// # Example -/// -/// ``` -/// let nig_path = nig(0.1, 0.2, 0.5, 1000, Some(0.0), Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct Nig { - theta: f64, - sigma: f64, - kappa: f64, - n: usize, - x0: Option, - t: Option, -} - -pub fn nig(params: &Nig) -> Array1 { - let Nig { - theta, - sigma, - kappa, - n, - x0, - t, - } = *params; - - let dt = t.unwrap_or(1.0) / n as f64; - let scale = dt.powf(2.0) / kappa; - let mean = dt / scale; - let ig = Array1::random(n, InverseGaussian::new(mean, scale).unwrap()); - let gn = gn(n, t); - let mut nig = Array1::zeros(n + 1); - nig[0] = x0.unwrap_or(0.0); - - for i in 1..(n + 1) { - nig[i] = nig[i - 1] + theta * ig[i - 1] + sigma * ig[i - 1].sqrt() * gn[i - 1] - } - - nig -} diff --git a/stochastic-rs-core/src/jumps/vg.rs b/stochastic-rs-core/src/jumps/vg.rs deleted file mode 100644 index 9ffb690..0000000 --- a/stochastic-rs-core/src/jumps/vg.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::noises::gn; -use derive_builder::Builder; -use ndarray::Array1; -use ndarray_rand::rand_distr::Gamma; -use ndarray_rand::RandomExt; - -/// Generates a path of the Variance Gamma (VG) process. -/// -/// The VG process is used in financial modeling to capture excess kurtosis and skewness in asset returns. -/// -/// # Parameters -/// -/// - `mu`: Drift parameter. -/// - `sigma`: Volatility parameter. -/// - `nu`: Variance rate parameter. -/// - `n`: Number of time steps. -/// - `x0`: Initial value of the process (optional, defaults to 0.0). -/// - `t`: Total time (optional, defaults to 1.0). -/// -/// # Returns -/// -/// A `Array1` representing the generated VG process path. -/// -/// # Example -/// -/// ``` -/// let vg_path = vg(0.1, 0.2, 0.5, 1000, Some(0.0), Some(1.0)); -/// ``` - -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct Vg { - mu: f64, - sigma: f64, - nu: f64, - n: usize, - x0: Option, - t: Option, -} - -pub fn vg(params: &Vg) -> Array1 { - let Vg { - mu, - sigma, - nu, - n, - x0, - t, - } = *params; - - let dt = t.unwrap_or(1.0) / n as f64; - - let shape = dt / nu; - let scale = nu; - - let mut vg = Array1::::zeros(n + 1); - vg[0] = x0.unwrap_or(0.0); - - let gn = gn::gn(n, t); - let gammas = Array1::random(n, Gamma::new(shape, scale).unwrap()); - - for i in 1..(n + 1) { - vg[i] = vg[i - 1] + mu * gammas[i - 1] + sigma * gammas[i - 1].sqrt() * gn[i - 1]; - } - - vg -} diff --git a/stochastic-rs-core/src/lib.rs b/stochastic-rs-core/src/lib.rs index 8296fd5..2920945 100644 --- a/stochastic-rs-core/src/lib.rs +++ b/stochastic-rs-core/src/lib.rs @@ -11,39 +11,6 @@ //! - Calculation of fractal dimensions and other statistical properties of time series data. //! - Support for jump processes and compound Poisson processes. //! -//! ## Modules -//! -//! - [`prelude`]: Re-exports of common types and traits for easier usage. -//! - [`diffusions`]: Contains implementations of various diffusion processes. -//! - [`jumps`]: Contains implementations of jump processes. -//! - [`models`]: Contains implementations of advanced stochastic models. -//! - [`noises`]: Contains implementations of noise generation processes. -//! - [`processes`]: Contains implementations of various stochastic processes. -//! - [`statistics`]: Contains tools for statistical analysis of time series data. -//! - [`utils`]: Contains utility functions and helpers. -//! -//! ## Examples -//! -//! ```rust -//! use stochastic_rs::prelude::*; -//! use stochastic_rs::diffusions::bm; -//! -//! // Simulate a Brownian motion process -//! let n = 1000; -//! let t = 1.0; -//! let bm_path = bm(n, Some(t)); -//! println!("Brownian Motion Path: {:?}", bm_path); -//! ``` -//! -//! ```rust -//! use stochastic_rs::statistics::higuchi_fd; -//! -//! // Calculate the Higuchi Fractal Dimension of a time series -//! let time_series = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]; -//! let fd = higuchi_fd(&time_series, 5); -//! println!("Higuchi Fractal Dimension: {}", fd); -//! ``` -//! //! ## License //! //! This project is licensed under the MIT License. @@ -61,11 +28,11 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -pub mod diffusions; +pub mod diffusion; pub mod interest; -pub mod jumps; -pub mod noises; -pub mod processes; +pub mod jump; +pub mod noise; +pub mod process; pub mod volatility; use ndarray::parallel::prelude::*; diff --git a/stochastic-rs-core/src/main.rs b/stochastic-rs-core/src/main.rs index a4a48a4..64b364f 100644 --- a/stochastic-rs-core/src/main.rs +++ b/stochastic-rs-core/src/main.rs @@ -1,4 +1,4 @@ -use stochastic_rs::processes::fbm::Fbm; +use stochastic_rs::process::fbm::Fbm; fn main() { // let fbm = Fbm::new(0.75, 10000, Some(1.0), None); diff --git a/stochastic-rs-core/src/noises/mod.rs b/stochastic-rs-core/src/noise.rs similarity index 100% rename from stochastic-rs-core/src/noises/mod.rs rename to stochastic-rs-core/src/noise.rs diff --git a/stochastic-rs-core/src/noises/cfgns.rs b/stochastic-rs-core/src/noise/cfgns.rs similarity index 87% rename from stochastic-rs-core/src/noises/cfgns.rs rename to stochastic-rs-core/src/noise/cfgns.rs index 60b2af1..8f106ef 100644 --- a/stochastic-rs-core/src/noises/cfgns.rs +++ b/stochastic-rs-core/src/noise/cfgns.rs @@ -11,19 +11,13 @@ pub struct Cfgns { pub n: usize, pub t: Option, pub m: Option, - fgn: Fgn, + pub fgn: Fgn, } impl Cfgns { #[must_use] pub fn new(params: &Self) -> Self { - let fgn = Fgn::new(&Fgn { - hurst: params.hurst, - n: params.n, - t: params.t, - m: params.m, - ..Default::default() - }); + let fgn = Fgn::new(params.hurst, params.n, params.t, params.m); Self { hurst: params.hurst, diff --git a/stochastic-rs-core/src/noises/cgns.rs b/stochastic-rs-core/src/noise/cgns.rs similarity index 100% rename from stochastic-rs-core/src/noises/cgns.rs rename to stochastic-rs-core/src/noise/cgns.rs diff --git a/stochastic-rs-core/src/noises/fgn.rs b/stochastic-rs-core/src/noise/fgn.rs similarity index 76% rename from stochastic-rs-core/src/noises/fgn.rs rename to stochastic-rs-core/src/noise/fgn.rs index 3dfb30e..3f67fcd 100644 --- a/stochastic-rs-core/src/noises/fgn.rs +++ b/stochastic-rs-core/src/noise/fgn.rs @@ -13,33 +13,25 @@ pub struct Fgn { pub n: usize, pub t: Option, pub m: Option, - offset: usize, - sqrt_eigenvalues: Arc>>, - fft_handler: Arc>, + pub offset: usize, + pub sqrt_eigenvalues: Arc>>, + pub fft_handler: Arc>, } impl Default for Fgn { fn default() -> Self { - Self { - hurst: 0.5, - n: 1024, - t: None, - m: None, - offset: 0, - sqrt_eigenvalues: Arc::new(Array1::zeros(0)), - fft_handler: Arc::new(FftHandler::new(0)), - } + Self::new(0.5, 1024, None, None) } } impl Fgn { #[must_use] - pub fn new(params: &Self) -> Self { - if !(0.0..=1.0).contains(¶ms.hurst) { + pub fn new(hurst: f64, n: usize, t: Option, m: Option) -> Self { + if !(0.0..=1.0).contains(&hurst) { panic!("Hurst parameter must be between 0 and 1"); } - let n_ = params.n.next_power_of_two(); - let offset = n_ - params.n; + let n_ = n.next_power_of_two(); + let offset = n_ - n; let n = n_; let mut r = Array1::linspace(0.0, n as f64, n + 1); r.mapv_inplace(|x| { @@ -47,8 +39,7 @@ impl Fgn { 1.0 } else { 0.5 - * ((x + 1.0).powf(2.0 * params.hurst) - 2.0 * x.powf(2.0 * params.hurst) - + (x - 1.0).powf(2.0 * params.hurst)) + * ((x + 1.0).powf(2.0 * hurst) - 2.0 * x.powf(2.0 * hurst) + (x - 1.0).powf(2.0 * hurst)) } }); let r = concatenate( @@ -64,12 +55,12 @@ impl Fgn { sqrt_eigenvalues.mapv_inplace(|x| Complex::new((x.re / (2.0 * n as f64)).sqrt(), x.im)); Self { - hurst: params.hurst, + hurst, n, offset, - t: params.t, + t, sqrt_eigenvalues: Arc::new(sqrt_eigenvalues), - m: params.m, + m, fft_handler: Arc::new(FftHandler::new(2 * n)), } } @@ -108,13 +99,7 @@ mod tests { #[test] fn plot() { - let fgn = Fgn::new(&Fgn { - hurst: 0.7, - n: 1000, - t: Some(1.0), - m: None, - ..Default::default() - }); + let fgn = Fgn::new(0.7, 1024, Some(1.0), None); let mut plot = Plot::new(); let d = fgn.sample_par(); for data in d.axis_iter(Axis(0)) { diff --git a/stochastic-rs-core/src/processes/mod.rs b/stochastic-rs-core/src/process.rs similarity index 100% rename from stochastic-rs-core/src/processes/mod.rs rename to stochastic-rs-core/src/process.rs diff --git a/stochastic-rs-core/src/processes/bm.rs b/stochastic-rs-core/src/process/bm.rs similarity index 100% rename from stochastic-rs-core/src/processes/bm.rs rename to stochastic-rs-core/src/process/bm.rs diff --git a/stochastic-rs-core/src/processes/cbms.rs b/stochastic-rs-core/src/process/cbms.rs similarity index 94% rename from stochastic-rs-core/src/processes/cbms.rs rename to stochastic-rs-core/src/process/cbms.rs index 701437c..a43de9f 100644 --- a/stochastic-rs-core/src/processes/cbms.rs +++ b/stochastic-rs-core/src/process/cbms.rs @@ -1,6 +1,6 @@ use ndarray::{Array1, Array2}; -use crate::{noises::cgns::Cgns, Sampling2D}; +use crate::{noise::cgns::Cgns, Sampling2D}; #[derive(Default)] pub struct Cbms { @@ -8,7 +8,7 @@ pub struct Cbms { pub n: usize, pub t: Option, pub m: Option, - cgns: Cgns, + pub cgns: Cgns, } impl Cbms { diff --git a/stochastic-rs-core/src/processes/ccustom.rs b/stochastic-rs-core/src/process/ccustom.rs similarity index 96% rename from stochastic-rs-core/src/processes/ccustom.rs rename to stochastic-rs-core/src/process/ccustom.rs index df53703..d0a0831 100644 --- a/stochastic-rs-core/src/processes/ccustom.rs +++ b/stochastic-rs-core/src/process/ccustom.rs @@ -1,6 +1,5 @@ use ndarray::{Array1, Axis}; use rand::thread_rng; -use rand_distr::Distribution; use crate::{ProcessDistribution, Sampling, Sampling3D}; @@ -17,7 +16,7 @@ where pub m: Option, pub jumps_distribution: D, pub jump_times_distribution: E, - customjt: CustomJt, + pub customjt: CustomJt, } impl CompoundCustom diff --git a/stochastic-rs-core/src/processes/cfbms.rs b/stochastic-rs-core/src/process/cfbms.rs similarity index 95% rename from stochastic-rs-core/src/processes/cfbms.rs rename to stochastic-rs-core/src/process/cfbms.rs index 566396c..b15c50f 100644 --- a/stochastic-rs-core/src/processes/cfbms.rs +++ b/stochastic-rs-core/src/process/cfbms.rs @@ -1,6 +1,6 @@ use ndarray::{Array1, Array2}; -use crate::{noises::cfgns::Cfgns, Sampling2D}; +use crate::{noise::cfgns::Cfgns, Sampling2D}; #[derive(Default)] pub struct Cfbms { @@ -10,7 +10,7 @@ pub struct Cfbms { pub n: usize, pub t: Option, pub m: Option, - cfgns: Cfgns, + pub cfgns: Cfgns, } impl Cfbms { diff --git a/stochastic-rs-core/src/processes/cpoisson.rs b/stochastic-rs-core/src/process/cpoisson.rs similarity index 98% rename from stochastic-rs-core/src/processes/cpoisson.rs rename to stochastic-rs-core/src/process/cpoisson.rs index 5f5b2c8..adf6808 100644 --- a/stochastic-rs-core/src/processes/cpoisson.rs +++ b/stochastic-rs-core/src/process/cpoisson.rs @@ -15,7 +15,7 @@ where pub t_max: Option, pub m: Option, pub distribution: D, - poisson: Poisson, + pub poisson: Poisson, } impl CompoundPoisson { diff --git a/stochastic-rs-core/src/processes/customjt.rs b/stochastic-rs-core/src/process/customjt.rs similarity index 100% rename from stochastic-rs-core/src/processes/customjt.rs rename to stochastic-rs-core/src/process/customjt.rs diff --git a/stochastic-rs-core/src/processes/fbm.rs b/stochastic-rs-core/src/process/fbm.rs similarity index 87% rename from stochastic-rs-core/src/processes/fbm.rs rename to stochastic-rs-core/src/process/fbm.rs index ac9f0c9..e2423b9 100644 --- a/stochastic-rs-core/src/processes/fbm.rs +++ b/stochastic-rs-core/src/process/fbm.rs @@ -1,6 +1,6 @@ use ndarray::{s, Array1}; -use crate::{noises::fgn::Fgn, Sampling}; +use crate::{noise::fgn::Fgn, Sampling}; #[derive(Default)] pub struct Fbm { @@ -8,7 +8,7 @@ pub struct Fbm { pub n: usize, pub t: Option, pub m: Option, - fgn: Fgn, + pub fgn: Fgn, } impl Fbm { @@ -17,13 +17,7 @@ impl Fbm { panic!("Hurst parameter must be in (0, 1)") } - let fgn = Fgn::new(&Fgn { - hurst: params.hurst, - n: params.n, - t: params.t, - m: params.m, - ..Default::default() - }); + let fgn = Fgn::new(params.hurst, params.n, params.t, params.m); Self { hurst: params.hurst, diff --git a/stochastic-rs-core/src/processes/poisson.rs b/stochastic-rs-core/src/process/poisson.rs similarity index 100% rename from stochastic-rs-core/src/processes/poisson.rs rename to stochastic-rs-core/src/process/poisson.rs diff --git a/stochastic-rs-core/src/volatility/mod.rs b/stochastic-rs-core/src/volatility.rs similarity index 100% rename from stochastic-rs-core/src/volatility/mod.rs rename to stochastic-rs-core/src/volatility.rs diff --git a/stochastic-rs-core/src/volatility/heston.rs b/stochastic-rs-core/src/volatility/heston.rs index 9ed7d14..a4edea1 100644 --- a/stochastic-rs-core/src/volatility/heston.rs +++ b/stochastic-rs-core/src/volatility/heston.rs @@ -1,6 +1,6 @@ use ndarray::Array1; -use crate::{noises::cgns::Cgns, Sampling2D}; +use crate::{noise::cgns::Cgns, Sampling2D}; #[derive(Default)] @@ -16,7 +16,7 @@ pub struct Heston { pub t: Option, pub use_sym: Option, pub m: Option, - cgns: Cgns, + pub cgns: Cgns, } impl Heston { diff --git a/stochastic-rs-core/src/volatility/sabr.rs b/stochastic-rs-core/src/volatility/sabr.rs index 6996755..b30a24f 100644 --- a/stochastic-rs-core/src/volatility/sabr.rs +++ b/stochastic-rs-core/src/volatility/sabr.rs @@ -1,6 +1,6 @@ use ndarray::Array1; -use crate::{noises::cgns::Cgns, Sampling2D}; +use crate::{noise::cgns::Cgns, Sampling2D}; #[derive(Default)] @@ -13,7 +13,7 @@ pub struct Sabr { pub v0: Option, pub t: Option, pub m: Option, - cgns: Cgns, + pub cgns: Cgns, } impl Sabr {