use crate::errors::{QlError, QlResult};
use crate::math::randomnumbers::rngtraits::{McRngTraits, PseudoRandom, SequenceGenerator};
use crate::processes::OrnsteinUhlenbeckProcess;
use crate::require;
use crate::stochasticprocess::StochasticProcess1D;
use crate::types::{Natural, Real, Size, Time};
pub struct OuRequest {
pub initial: Real,
pub level: Real,
pub speed: Real,
pub volatility: Real,
pub horizon: Time,
pub steps: Size,
pub paths: Size,
pub seed: Natural,
pub terminal_only: bool,
}
pub fn output_len(request: &OuRequest) -> QlResult<Size> {
let times = if request.terminal_only {
1
} else {
request
.steps
.checked_add(1)
.ok_or_else(|| QlError::new("step count overflows", file!(), line!()))?
};
request
.paths
.checked_mul(times)
.ok_or_else(|| QlError::new("simulation dimensions overflow", file!(), line!()))
}
pub fn ou_paths(request: &OuRequest) -> QlResult<Vec<Real>> {
require!(request.initial.is_finite(), "invalid initial value");
require!(request.level.is_finite(), "invalid level");
require!(
request.speed.is_finite() && request.speed >= 0.0,
"invalid speed"
);
require!(
request.volatility.is_finite() && request.volatility >= 0.0,
"invalid volatility"
);
require!(
request.horizon.is_finite() && request.horizon >= 0.0,
"invalid horizon"
);
require!(
request.steps > 0 && request.paths > 0,
"steps and paths must be positive"
);
require!(request.seed != 0, "seed must be nonzero");
request
.steps
.checked_mul(request.paths)
.ok_or_else(|| QlError::new("simulation dimensions overflow", file!(), line!()))?;
let dt = request.horizon / request.steps as Real;
require!(request.horizon == 0.0 || dt > 0.0, "time step underflows");
let count = output_len(request)?;
let mut output = Vec::new();
output
.try_reserve_exact(count)
.map_err(|_| QlError::new("allocation failed", file!(), line!()))?;
let process = OrnsteinUhlenbeckProcess::new(
request.speed,
request.volatility,
request.initial,
request.level,
)?;
let mut rng = PseudoRandom::make_sequence_generator(1, request.seed)?;
for _ in 0..request.paths {
let mut value = request.initial;
if !request.terminal_only {
output.push(value);
}
for step in 0..request.steps {
let draw = rng.next_sequence().value[0];
if dt != 0.0 {
value = process.evolve(step as Real * dt, value, dt, draw)?;
require!(value.is_finite(), "simulated value is nonfinite");
}
if !request.terminal_only {
output.push(value);
}
}
if request.terminal_only {
output.push(value);
}
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::methods::montecarlo::simulation_kernel::gaussian_draws;
fn request() -> OuRequest {
OuRequest {
initial: 1.0,
level: 3.0,
speed: 0.5,
volatility: 0.2,
horizon: 1.0,
steps: 4,
paths: 3,
seed: 42,
terminal_only: false,
}
}
#[test]
fn seeded_paths_match_pinned_multistep_fixture() {
let expected = [
1.0,
1.2049197140571397,
1.4938576175387845,
1.8262101587088548,
1.879255526298315,
1.0,
1.2932179159179402,
1.5663072780654845,
1.758274886469132,
1.9272460691202662,
1.0,
1.139911950142801,
1.3456668679224093,
1.4449524117278607,
1.50711446963386,
];
let r = request();
let full = ou_paths(&r).unwrap();
assert_eq!(full.len(), expected.len());
for (index, (&actual, &expected)) in full.iter().zip(&expected).enumerate() {
assert!((actual - expected).abs() < 2e-15, "value {index}");
}
let terminal = ou_paths(&OuRequest {
terminal_only: true,
..r
})
.unwrap();
assert_eq!(terminal.len(), 3);
for (path, &actual) in terminal.iter().enumerate() {
assert!((actual - expected[path * 5 + 4]).abs() < 2e-15);
assert_eq!(actual, full[path * 5 + 4]);
}
}
#[test]
fn first_transition_and_terminal_mode_share_exact_values() {
let r = request();
let values = ou_paths(&r).unwrap();
let p = OrnsteinUhlenbeckProcess::new(r.speed, r.volatility, r.initial, r.level).unwrap();
assert_eq!(
values[1],
p.evolve(0.0, r.initial, 0.25, gaussian_draws(1, 42).unwrap()[0])
.unwrap()
);
let terminals = ou_paths(&OuRequest {
terminal_only: true,
..r
})
.unwrap();
for (i, terminal) in terminals.iter().enumerate() {
assert_eq!(*terminal, values[i * 5 + 4]);
}
}
#[test]
fn zero_horizon_and_zero_volatility_are_deterministic() {
let r = OuRequest {
horizon: 0.0,
..request()
};
assert!(ou_paths(&r).unwrap().iter().all(|&v| v == r.initial));
let r = OuRequest {
volatility: 0.0,
..request()
};
assert_eq!(
ou_paths(&r).unwrap(),
ou_paths(&OuRequest { seed: 91, ..r }).unwrap()
);
let brownian = OuRequest {
speed: 0.0,
steps: 1,
paths: 1,
..request()
};
let normal = gaussian_draws(1, brownian.seed).unwrap()[0];
assert!(
(ou_paths(&brownian).unwrap()[1]
- (brownian.initial + brownian.volatility * brownian.horizon.sqrt() * normal))
.abs()
< 1e-15
);
}
#[test]
fn terminal_moments_match_analytic_transition() {
let r = OuRequest {
steps: 2,
paths: 50_000,
terminal_only: true,
..request()
};
let values = ou_paths(&r).unwrap();
let process =
OrnsteinUhlenbeckProcess::new(r.speed, r.volatility, r.initial, r.level).unwrap();
let expected_mean = process.expectation(0.0, r.initial, r.horizon).unwrap();
let expected_var = process.variance(0.0, r.initial, r.horizon).unwrap();
let n = values.len() as Real;
let mean = values.iter().sum::<Real>() / n;
let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<Real>() / n;
assert!((mean - expected_mean).abs() < 6.0 * (expected_var / n).sqrt());
assert!((variance - expected_var).abs() < 6.0 * expected_var * (2.0 / n).sqrt());
}
#[test]
fn invalid_requests_fail() {
let cases = [
OuRequest {
initial: Real::NAN,
..request()
},
OuRequest {
level: Real::INFINITY,
..request()
},
OuRequest {
speed: -1.0,
..request()
},
OuRequest {
volatility: -1.0,
..request()
},
OuRequest {
horizon: -1.0,
..request()
},
OuRequest {
horizon: Real::from_bits(1),
..request()
},
OuRequest {
steps: 0,
..request()
},
OuRequest {
paths: 0,
..request()
},
OuRequest {
seed: 0,
..request()
},
OuRequest {
steps: usize::MAX,
..request()
},
OuRequest {
paths: usize::MAX,
..request()
},
];
for case in cases {
assert!(ou_paths(&case).is_err());
}
}
}