use crate::errors::{QlError, QlResult};
use crate::math::distributions::normal::InverseCumulativeNormal;
use crate::math::distributions::poisson::InverseCumulativePoisson;
use crate::math::distributions::{Probability, Quantile};
use crate::math::randomnumbers::{MersenneTwisterUniformRng, UniformRng};
use crate::require;
use crate::types::{Natural, Real, Size, Time};
#[derive(Clone, Copy)]
pub struct MertonRequest {
pub spot: Real,
pub drift: Real,
pub volatility: Real,
pub jump_intensity: Real,
pub log_mean_jump: Real,
pub log_jump_volatility: Real,
pub horizon: Time,
pub steps: Size,
pub paths: Size,
pub seed: Natural,
pub terminal_only: bool,
}
fn error(message: &str) -> QlError {
QlError::new(message, file!(), line!())
}
pub fn output_len(request: &MertonRequest) -> QlResult<Size> {
let times = if request.terminal_only {
1
} else {
request
.steps
.checked_add(1)
.ok_or_else(|| error("step count overflows"))?
};
request
.paths
.checked_mul(times)
.ok_or_else(|| error("simulation dimensions overflow"))
}
fn shifted_seed(seed: Natural, offset: Natural) -> Natural {
(1 + ((u64::from(seed) - 1 + u64::from(offset)) % u64::from(Natural::MAX))) as Natural
}
struct Draws {
diffusion: MersenneTwisterUniformRng,
counts: MersenneTwisterUniformRng,
jumps: MersenneTwisterUniformRng,
normal: InverseCumulativeNormal,
poisson: Option<InverseCumulativePoisson>,
}
impl Draws {
fn new(seed: Natural, poisson_mean: Real) -> QlResult<Self> {
Ok(Self {
diffusion: MersenneTwisterUniformRng::new(seed),
counts: MersenneTwisterUniformRng::new(shifted_seed(seed, 0x9e37_79b9)),
jumps: MersenneTwisterUniformRng::new(shifted_seed(seed, 0xbb67_ae85)),
normal: InverseCumulativeNormal::standard(),
poisson: if poisson_mean == 0.0 {
None
} else {
Some(InverseCumulativePoisson::new(poisson_mean)?)
},
})
}
fn next(&mut self) -> QlResult<(Real, Real, Real)> {
let diffusion = self.normal.value(self.diffusion.next_real())?;
let uniform = self.counts.next_real();
let jump = self.normal.value(self.jumps.next_real())?;
let count = match &self.poisson {
Some(poisson) => poisson.quantile(Probability::try_from(uniform)?)?,
None => 0.0,
};
require!(count.is_finite() && count >= 0.0, "invalid jump count");
Ok((diffusion, count, jump))
}
}
struct Transition {
dt: Real,
log_growth: Real,
diffusion_scale: Real,
poisson_mean: Real,
}
fn validate(request: &MertonRequest) -> QlResult<Transition> {
require!(
request.spot.is_finite() && request.spot > 0.0,
"spot must be finite and positive"
);
require!(request.drift.is_finite(), "drift must be finite");
require!(
request.log_mean_jump.is_finite(),
"log-jump mean must be finite"
);
for (value, name) in [
(request.volatility, "volatility"),
(request.jump_intensity, "jump intensity"),
(request.log_jump_volatility, "log-jump volatility"),
(request.horizon, "horizon"),
] {
require!(
value.is_finite() && value >= 0.0,
"{name} must be finite and non-negative"
);
}
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)
.and_then(|n| n.checked_mul(3))
.ok_or_else(|| error("simulation work dimensions overflow"))?;
let diffusion_variance = request.volatility * request.volatility;
let jump_variance = request.log_jump_volatility * request.log_jump_volatility;
let jump_exponent = request.log_mean_jump + 0.5 * jump_variance;
let multiplier = jump_exponent.exp();
let compensation = request.jump_intensity * jump_exponent.exp_m1();
require!(
diffusion_variance.is_finite()
&& jump_variance.is_finite()
&& jump_exponent.is_finite()
&& multiplier.is_finite()
&& multiplier > 0.0
&& compensation.is_finite(),
"jump or diffusion parameters are not representable"
);
let dt = request.horizon / request.steps as Real;
require!(request.horizon == 0.0 || dt > 0.0, "time step underflows");
let poisson_mean = request.jump_intensity * dt;
require!(poisson_mean.is_finite(), "Poisson mean overflows");
require!(
request.jump_intensity == 0.0 || request.horizon == 0.0 || poisson_mean > 0.0,
"Poisson mean underflows"
);
let (log_growth, diffusion_scale) = if request.horizon == 0.0 {
(0.0, 0.0)
} else {
let log_drift = if request.jump_intensity == 0.0 {
request.drift - 0.5 * request.volatility * request.volatility
} else {
request.drift - compensation - 0.5 * request.volatility * request.volatility
};
(log_drift * dt, request.volatility * dt.sqrt())
};
require!(
log_growth.is_finite() && diffusion_scale.is_finite(),
"transition parameters are not representable"
);
Ok(Transition {
dt,
log_growth,
diffusion_scale,
poisson_mean,
})
}
pub fn merton_paths(request: &MertonRequest) -> QlResult<Vec<Real>> {
let transition = validate(request)?;
let mut draws = Draws::new(request.seed, transition.poisson_mean)?;
let count = output_len(request)?;
let mut output = Vec::new();
output
.try_reserve_exact(count)
.map_err(|_| error("allocation failed"))?;
for _ in 0..request.paths {
let mut spot = request.spot;
if !request.terminal_only {
output.push(spot);
}
for step in 0..request.steps {
let (diffusion, count, jump) = draws.next()?;
if request.horizon == 0.0 {
spot = request.spot;
} else if request.jump_intensity == 0.0 && request.volatility == 0.0 {
let time = if step + 1 == request.steps {
request.horizon
} else {
(step + 1) as Real * transition.dt
};
spot = request.spot * (request.drift * time).exp();
} else {
let exponent = transition.log_growth + transition.diffusion_scale * diffusion;
let exponent = if request.jump_intensity == 0.0 {
exponent
} else {
exponent
+ count * request.log_mean_jump
+ count.sqrt() * request.log_jump_volatility * jump
};
spot *= exponent.exp();
}
require!(
spot.is_finite() && spot > 0.0,
"simulated price overflow or underflow"
);
if !request.terminal_only {
output.push(spot);
}
}
if request.terminal_only {
output.push(spot);
}
}
Ok(output)
}
#[cfg(test)]
mod tests;