use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiscretizationScheme {
Exact,
Euler,
Milstein,
}
impl FromStr for DiscretizationScheme {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_lowercase().as_str() {
"exact" => Ok(DiscretizationScheme::Exact),
"euler" => Ok(DiscretizationScheme::Euler),
"milstein" => Ok(DiscretizationScheme::Milstein),
other => Err(format!("Invalid discretization scheme '{other}'")),
}
}
}
pub fn numeric_diffusion_dx<P: StochasticProcess1D + ?Sized>(p: &P, t: f64, x: f64) -> f64 {
let h = 1e-5 * x.abs().max(1.0);
(p.diffusion(t, x + h) - p.diffusion(t, x - h)) / (2.0 * h)
}
pub trait StochasticProcess1D: Sync {
fn drift(&self, t: f64, x: f64) -> f64;
fn diffusion(&self, t: f64, x: f64) -> f64;
fn diffusion_dx(&self, t: f64, x: f64) -> f64 {
numeric_diffusion_dx(self, t, x)
}
fn exact_step(&self, _t: f64, _x: f64, _dt: f64, _dw: f64) -> Option<f64> {
None
}
fn constrain(&self, x: f64) -> f64 {
x
}
fn evolve(&self, scheme: DiscretizationScheme, t: f64, x: f64, dt: f64, dw: f64) -> f64 {
let next = match scheme {
DiscretizationScheme::Exact => self
.exact_step(t, x, dt, dw)
.unwrap_or_else(|| x + self.drift(t, x) * dt + self.diffusion(t, x) * dw),
DiscretizationScheme::Euler => x + self.drift(t, x) * dt + self.diffusion(t, x) * dw,
DiscretizationScheme::Milstein => {
let b = self.diffusion(t, x);
x + self.drift(t, x) * dt
+ b * dw
+ 0.5 * b * self.diffusion_dx(t, x) * (dw * dw - dt)
}
};
self.constrain(next)
}
}
pub trait StochasticProcess: Sync {
fn dim(&self) -> usize;
fn factors(&self) -> usize;
fn drift(&self, t: f64, x: &[f64], out: &mut [f64]);
fn diffusion(&self, t: f64, x: &[f64], out: &mut [f64]);
fn constrain(&self, _x: &mut [f64]) {}
fn evolve(&self, t: f64, x: &[f64], dt: f64, dw: &[f64], out: &mut [f64]) {
let (dim, factors) = (self.dim(), self.factors());
debug_assert_eq!(dw.len(), factors);
let mut a = vec![0.0; dim];
let mut b = vec![0.0; dim * factors];
self.drift(t, x, &mut a);
self.diffusion(t, x, &mut b);
for i in 0..dim {
let shock: f64 =
(0..factors).map(|j| b[i * factors + j] * dw[j]).sum();
out[i] = x[i] + a[i] * dt + shock;
}
self.constrain(out);
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Vasicek {
kappa: f64,
theta: f64,
sigma: f64,
}
impl StochasticProcess1D for Vasicek {
fn drift(&self, _t: f64, x: f64) -> f64 {
self.kappa * (self.theta - x)
}
fn diffusion(&self, _t: f64, _x: f64) -> f64 {
self.sigma
}
fn exact_step(&self, _t: f64, x: f64, dt: f64, dw: f64) -> Option<f64> {
let decay = (-self.kappa * dt).exp();
let mean = self.theta + (x - self.theta) * decay;
let var = self.sigma * self.sigma * (1.0 - decay * decay) / (2.0 * self.kappa);
let z = dw / dt.sqrt();
Some(mean + var.sqrt() * z)
}
}
#[test]
fn euler_matches_hand_computed_step() {
let p = Vasicek { kappa: 2.0, theta: 0.03, sigma: 0.01 };
let (t, x, dt, dw) = (0.5, 0.05, 0.01, -0.02);
let expected = x + 2.0 * (0.03 - 0.05) * dt + 0.01 * dw;
let got = p.evolve(DiscretizationScheme::Euler, t, x, dt, dw);
assert!((got - expected).abs() < 1e-15);
}
#[test]
fn milstein_reduces_to_euler_for_additive_noise() {
let p = Vasicek { kappa: 2.0, theta: 0.03, sigma: 0.01 };
let (t, x, dt, dw) = (0.5, 0.05, 0.01, 0.03);
let euler = p.evolve(DiscretizationScheme::Euler, t, x, dt, dw);
let milstein = p.evolve(DiscretizationScheme::Milstein, t, x, dt, dw);
assert!((euler - milstein).abs() < 1e-12);
}
#[test]
fn state_can_go_negative_without_a_gbm_floor() {
let p = Vasicek { kappa: 0.5, theta: 0.01, sigma: 0.02 };
let next = p.evolve(DiscretizationScheme::Euler, 0.0, 0.001, 0.01, -0.5);
assert!(next < 0.0);
}
#[test]
fn exact_step_hits_the_ou_transition_mean() {
let p = Vasicek { kappa: 2.0, theta: 0.03, sigma: 0.01 };
let next = p.evolve(DiscretizationScheme::Exact, 0.0, 0.05, 0.25, 0.0);
let mean = 0.03 + (0.05 - 0.03) * (-2.0_f64 * 0.25).exp();
assert!((next - mean).abs() < 1e-15);
}
#[test]
fn numeric_diffusion_dx_recovers_multiplicative_slope() {
struct Gbmish;
impl StochasticProcess1D for Gbmish {
fn drift(&self, _t: f64, x: f64) -> f64 {
0.05 * x
}
fn diffusion(&self, _t: f64, x: f64) -> f64 {
0.2 * x
}
}
let d = numeric_diffusion_dx(&Gbmish, 0.0, 100.0);
assert!((d - 0.2).abs() < 1e-8, "{d}");
}
#[test]
fn multi_dim_default_euler() {
struct TwoDim;
impl StochasticProcess for TwoDim {
fn dim(&self) -> usize {
2
}
fn factors(&self) -> usize {
2
}
fn drift(&self, _t: f64, x: &[f64], out: &mut [f64]) {
out[0] = 0.1 * x[0];
out[1] = -0.2 * x[1];
}
fn diffusion(&self, _t: f64, _x: &[f64], out: &mut [f64]) {
out.copy_from_slice(&[0.3, 0.0, 0.1, 0.2]);
}
}
let (x, dt, dw) = ([1.0, 2.0], 0.01, [0.05, -0.03]);
let mut out = [0.0; 2];
TwoDim.evolve(0.0, &x, dt, &dw, &mut out);
assert!((out[0] - (1.0 + 0.1 * 1.0 * dt + 0.3 * 0.05)).abs() < 1e-15);
assert!((out[1] - (2.0 - 0.2 * 2.0 * dt + 0.1 * 0.05 - 0.2 * 0.03)).abs() < 1e-15);
}
}