use ndarray::Array1;
use ndarray::Array2;
use stochastic_rs_core::simd_rng::Deterministic;
use stochastic_rs_core::simd_rng::SeedExt;
use stochastic_rs_core::simd_rng::Unseeded;
use super::kernel::RlKernel;
use super::markov_lift::MarkovLift;
use super::markov_lift::RoughSimd;
use crate::noise::cgns::Cgns;
use crate::traits::FloatExt;
use crate::traits::ProcessExt;
#[derive(Clone)]
pub struct RlHeston<T: FloatExt, S: SeedExt = Unseeded> {
pub hurst: T,
pub s0: Option<T>,
pub v0: Option<T>,
pub kappa: T,
pub theta: T,
pub sigma: T,
pub rho: T,
pub mu: T,
pub n: usize,
pub t: Option<T>,
pub degree: Option<usize>,
pub seed: S,
cgns: Cgns<T>,
markov: MarkovLift<T>,
}
fn build_markov<T: FloatExt>(
hurst: T,
n: usize,
t: Option<T>,
degree: Option<usize>,
) -> MarkovLift<T> {
let dt = t.unwrap_or(T::one()) / T::from_usize_(n - 1);
let deg = degree.unwrap_or_else(|| RlKernel::<T>::default_degree(n));
let kernel = RlKernel::<T>::new(hurst, deg);
MarkovLift::new(kernel, dt)
}
impl<T: FloatExt> RlHeston<T> {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
hurst: T,
s0: Option<T>,
v0: Option<T>,
kappa: T,
theta: T,
sigma: T,
rho: T,
mu: T,
n: usize,
t: Option<T>,
degree: Option<usize>,
) -> Self {
assert!(n >= 2, "n must be at least 2");
assert!(kappa >= T::zero(), "kappa must be non-negative");
assert!(theta >= T::zero(), "theta must be non-negative");
assert!(sigma >= T::zero(), "sigma must be non-negative");
if let Some(v0) = v0 {
assert!(v0 >= T::zero(), "v0 must be non-negative");
}
Self {
hurst,
s0,
v0,
kappa,
theta,
sigma,
rho,
mu,
n,
t,
degree,
seed: Unseeded,
cgns: Cgns::new(rho, n - 1, t),
markov: build_markov(hurst, n, t, degree),
}
}
}
impl<T: FloatExt> RlHeston<T, Deterministic> {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn seeded(
hurst: T,
s0: Option<T>,
v0: Option<T>,
kappa: T,
theta: T,
sigma: T,
rho: T,
mu: T,
n: usize,
t: Option<T>,
degree: Option<usize>,
seed: u64,
) -> Self {
assert!(n >= 2, "n must be at least 2");
Self {
hurst,
s0,
v0,
kappa,
theta,
sigma,
rho,
mu,
n,
t,
degree,
seed: Deterministic::new(seed),
cgns: Cgns::new(rho, n - 1, t),
markov: build_markov(hurst, n, t, degree),
}
}
}
impl<T: FloatExt + RoughSimd, S: SeedExt> RlHeston<T, S> {
pub fn sample_batch(&self, m: usize) -> [Array2<T>; 2] {
let n_minus_1 = self.n - 1;
let mut dw_s = Array2::<T>::zeros((m, n_minus_1));
let mut dw_v = Array2::<T>::zeros((m, n_minus_1));
for p in 0..m {
let [s_row, v_row] = self.cgns.sample_impl(&self.seed.derive());
dw_s.row_mut(p).assign(&s_row);
dw_v.row_mut(p).assign(&v_row);
}
let kappa = self.kappa;
let theta = self.theta;
let sigma = self.sigma;
let v0 = self.v0.unwrap_or(T::zero()).max(T::zero());
let variance = self.markov.simulate_batch(
v0,
|vv| kappa * (theta - vv.max(T::zero())),
|vv| sigma * vv.max(T::zero()).sqrt(),
dw_v.view(),
);
let dt = self.t.unwrap_or(T::one()) / T::from_usize_(self.n - 1);
let s0 = self.s0.unwrap_or(T::zero());
let mut spots = Array2::<T>::zeros((m, self.n));
for p in 0..m {
spots[[p, 0]] = s0;
for i in 1..self.n {
let v_prev = variance[[p, i - 1]].max(T::zero());
spots[[p, i]] = spots[[p, i - 1]]
+ self.mu * spots[[p, i - 1]] * dt
+ spots[[p, i - 1]] * v_prev.sqrt() * dw_s[[p, i - 1]];
}
}
let variance_clipped = variance.map(|v| (*v).max(T::zero()));
[spots, variance_clipped]
}
}
impl<T: FloatExt + RoughSimd, S: SeedExt> ProcessExt<T> for RlHeston<T, S> {
type Output = [Array1<T>; 2];
fn sample(&self) -> Self::Output {
let dt = self.t.unwrap_or(T::one()) / T::from_usize_(self.n - 1);
let [dw_s, dw_v] = self.cgns.sample_impl(&self.seed.derive());
let kappa = self.kappa;
let theta = self.theta;
let sigma = self.sigma;
let v0 = self.v0.unwrap_or(T::zero()).max(T::zero());
let v = self.markov.simulate(
v0,
|vv| kappa * (theta - vv.max(T::zero())),
|vv| sigma * vv.max(T::zero()).sqrt(),
dw_v.as_slice().expect("dw_v must be contiguous"),
);
let mut s = Array1::<T>::zeros(self.n);
s[0] = self.s0.unwrap_or(T::zero());
for i in 1..self.n {
let v_prev = v[i - 1].max(T::zero());
s[i] = s[i - 1] + self.mu * s[i - 1] * dt + s[i - 1] * v_prev.sqrt() * dw_s[i - 1];
}
let v_clipped = v.map(|x| (*x).max(T::zero()));
[s, v_clipped]
}
}
#[cfg(test)]
mod tests {
use super::RlHeston;
use crate::traits::ProcessExt;
#[test]
#[should_panic(expected = "n must be at least 2")]
fn rejects_too_short_grid() {
let _ = RlHeston::<f64>::new(
0.3,
Some(100.0),
Some(0.04),
1.0,
0.04,
0.5,
-0.5,
0.0,
1,
Some(1.0),
None,
);
}
#[test]
fn variance_stays_non_negative() {
let p = RlHeston::seeded(
0.12_f64,
Some(100.0),
Some(0.04),
0.1,
0.3156,
0.0331,
-0.681,
0.0,
256,
Some(1.0),
None,
11,
);
let [s, v] = p.sample();
assert!(v.iter().all(|x| *x >= 0.0));
assert!(s.iter().all(|x| x.is_finite()));
}
#[test]
fn batch_shape_and_nonneg_variance() {
let p = RlHeston::seeded(
0.12_f64,
Some(100.0),
Some(0.04),
0.1,
0.3156,
0.0331,
-0.681,
0.0,
64,
Some(1.0),
Some(25),
17,
);
let [spots, variances] = p.sample_batch(13);
assert_eq!(spots.dim(), (13, 64));
assert_eq!(variances.dim(), (13, 64));
assert!(variances.iter().all(|x| *x >= 0.0));
assert!(spots.iter().all(|x| x.is_finite()));
}
#[test]
fn heston_limit_h_half_asymptotic_variance_is_finite() {
let p = RlHeston::seeded(
0.49_f64,
Some(100.0),
Some(0.04),
1.0,
0.04,
0.3,
-0.5,
0.0,
512,
Some(1.0),
None,
3,
);
let [_s, v] = p.sample();
let mean: f64 = v.iter().sum::<f64>() / v.len() as f64;
assert!(
mean > 0.0 && mean < 1.0,
"mean variance out of sanity range: {mean}"
);
}
}