use crate::error::SimulationError;
use crate::simulation::model::WalkPath;
use crate::simulation::ou::ou_path;
use crate::simulation::{WalkParams, WalkType, path_buffer};
use optionstratlib_core::model::Positive;
use optionstratlib_core::model::decimal::{
d_add, d_div, d_exp, d_exp_f64, d_mul, d_sqrt, d_sqrt_f64, d_sub, decimal_normal_sample_with,
decimal_to_f64, decimal_uniform_sample_with, finite_decimal, p_sqrt,
};
use optionstratlib_core::utils::deterministic_rng;
use rand::Rng;
use rust_decimal::Decimal;
use std::convert::TryInto;
use std::fmt::{Debug, Display};
use std::ops::AddAssign;
macro_rules! with_walk_rng {
($params:expr, $kernel:ident) => {
match $params.seed {
Some(seed) => $kernel($params, &mut deterministic_rng(seed)),
None => $kernel($params, &mut rand::rng()),
}
};
}
pub fn garch_walk<X, Y>(params: &WalkParams<X, Y>) -> Result<WalkPath, SimulationError>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
{
with_walk_rng!(params, garch_path)
}
fn garch_path<X, Y, R>(params: &WalkParams<X, Y>, rng: &mut R) -> Result<WalkPath, SimulationError>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
R: Rng + ?Sized,
{
match params.walk_type {
WalkType::Garch {
dt,
drift,
volatility,
alpha,
beta,
} => {
let persistence = alpha.checked_add(&beta)?;
if persistence >= Decimal::ONE {
return Err(SimulationError::GarchStationarity { alpha, beta });
}
let mut path = path_buffer(params.size, 1)?;
let mut vols = path_buffer(params.size, 1)?;
let mut price = params.ystep_as_positive()?.to_dec();
path.push(Positive::new_decimal(price).unwrap_or(Positive::ZERO));
vols.push(volatility);
let mut var = volatility.checked_mul(&volatility)?; let mut prev_eps2 = Decimal::ZERO;
let stationary_weight = d_sub(
d_sub(Decimal::ONE, alpha.to_dec(), "simulation::garch::omega")?,
beta.to_dec(),
"simulation::garch::omega",
)?;
let omega = volatility
.checked_powu(2)?
.checked_mul_dec(stationary_weight)?;
let sqrt_dt = decimal_to_f64(dt.to_dec())?.sqrt(); let sqrt_dt_dec = finite_decimal(sqrt_dt).ok_or_else(|| {
SimulationError::non_finite("simulation::garch::sqrt_dt", sqrt_dt)
})?;
for _ in 1..params.size {
var = omega
.checked_add(&alpha.checked_mul_dec(prev_eps2)?)?
.checked_add(&beta.checked_mul(&var)?)?;
let z = decimal_normal_sample_with(rng);
let var_sqrt = Positive::new_decimal(d_sqrt_f64(
var.to_dec(),
"simulation::traits::garch_walk",
)?)?;
let eps = d_mul(
d_mul(z, var_sqrt.to_dec(), "simulation::garch::eps")?,
sqrt_dt_dec,
"simulation::garch::eps",
)?;
let ret = d_add(
d_mul(drift, dt.to_dec(), "simulation::garch::ret")?,
eps,
"simulation::garch::ret",
)?;
price = d_mul(
price,
d_exp_f64(ret, "simulation::garch::price")?,
"simulation::garch::price",
)?;
path.push(Positive::new_decimal(price).unwrap_or(Positive::ZERO));
vols.push(var_sqrt);
prev_eps2 = d_mul(eps, eps, "simulation::garch::eps_squared")?; }
Ok(WalkPath {
prices: path,
vols: Some(vols),
})
}
_ => Err(SimulationError::InvalidWalkType { expected: "GARCH" }),
}
}
pub fn heston_walk<X, Y>(params: &WalkParams<X, Y>) -> Result<WalkPath, SimulationError>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
{
with_walk_rng!(params, heston_path)
}
fn heston_path<X, Y, R>(params: &WalkParams<X, Y>, rng: &mut R) -> Result<WalkPath, SimulationError>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
R: Rng + ?Sized,
{
match params.walk_type {
WalkType::Heston {
dt,
drift,
volatility,
kappa,
theta,
xi,
rho,
} => {
if rho < -Decimal::ONE || rho > Decimal::ONE {
return Err(SimulationError::InvalidCorrelation { rho });
}
let mut values = path_buffer(params.size, 0)?;
let mut vols = path_buffer(params.size, 0)?;
let mut price: Positive = params.ystep_as_positive()?;
let mut variance = d_mul(
volatility.to_dec(),
volatility.to_dec(),
"simulation::heston::variance0",
)?;
values.push(price); vols.push(volatility);
let dt_sqrt = d_sqrt(dt.to_dec(), "simulation::heston::sqrt_dt")
.map_err(|_| SimulationError::walk_error("Heston: sqrt(dt) failed (overflow)"))?;
let one_minus_rho_sq = d_sub(
Decimal::ONE,
d_mul(rho, rho, "simulation::heston::one_minus_rho_sq")?,
"simulation::heston::one_minus_rho_sq",
)?;
let one_minus_rho_sq_sqrt = d_sqrt(
one_minus_rho_sq,
"simulation::heston::one_minus_rho_sq_sqrt",
)
.map_err(|_| {
SimulationError::walk_error(
"Heston: sqrt(1 - rho^2) failed (rho out of range or overflow)",
)
})?;
for _ in 1..params.size {
let z1 = decimal_normal_sample_with(rng);
let z2 = d_add(
d_mul(rho, z1, "simulation::heston::z2")?,
d_mul(
one_minus_rho_sq_sqrt,
decimal_normal_sample_with(rng),
"simulation::heston::z2",
)?,
"simulation::heston::z2",
)?;
let variance_sqrt = d_sqrt_f64(variance, "simulation::heston::variance_sqrt")
.map_err(|_| {
SimulationError::walk_error("Heston: sqrt(variance) failed (overflow)")
})?;
let variance_drift = d_mul(
d_mul(
kappa.to_dec(),
d_sub(theta.to_dec(), variance, "simulation::heston::variance_gap")?,
"simulation::heston::variance_drift",
)?,
dt.to_dec(),
"simulation::heston::variance_drift",
)?;
let variance_diffusion = d_mul(
d_mul(
d_mul(
xi.to_dec(),
variance_sqrt,
"simulation::heston::variance_diffusion",
)?,
z2,
"simulation::heston::variance_diffusion",
)?,
dt_sqrt,
"simulation::heston::variance_diffusion",
)?;
let variance_new = d_add(
d_add(variance, variance_drift, "simulation::heston::variance_new")?,
variance_diffusion,
"simulation::heston::variance_new",
)?
.max(Decimal::ZERO);
let avg_variance = d_div(
d_add(variance, variance_new, "simulation::heston::avg_variance")?,
Decimal::TWO,
"simulation::heston::avg_variance",
)?;
let avg_variance_sqrt = d_sqrt_f64(
avg_variance,
"simulation::heston::avg_variance_sqrt",
)
.map_err(|_| {
SimulationError::walk_error("Heston: sqrt(avg_variance) failed (overflow)")
})?;
let price_change = d_add(
d_mul(drift, dt.to_dec(), "simulation::heston::price_change")?,
d_mul(
d_mul(avg_variance_sqrt, z1, "simulation::heston::price_change")?,
dt_sqrt,
"simulation::heston::price_change",
)?,
"simulation::heston::price_change",
)?;
price =
price.checked_mul_dec(d_exp_f64(price_change, "simulation::heston::price")?)?;
variance = variance_new;
values.push(price);
let vol_step =
d_sqrt_f64(variance, "simulation::heston::vol_step").map_err(|_| {
SimulationError::walk_error("Heston: sqrt(variance) failed (overflow)")
})?;
vols.push(Positive::new_decimal(vol_step).unwrap_or(Positive::ZERO));
}
Ok(WalkPath {
prices: values,
vols: Some(vols),
})
}
_ => Err(SimulationError::InvalidWalkType { expected: "Heston" }),
}
}
pub fn custom_walk<X, Y>(params: &WalkParams<X, Y>) -> Result<WalkPath, SimulationError>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
{
with_walk_rng!(params, custom_path)
}
fn custom_path<X, Y, R>(params: &WalkParams<X, Y>, rng: &mut R) -> Result<WalkPath, SimulationError>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
R: Rng + ?Sized,
{
match params.walk_type {
WalkType::Custom {
dt,
drift,
volatility,
vov,
vol_speed,
vol_mean,
} => {
let vols = ou_path(volatility, vol_mean, vol_speed, vov, dt, params.size, rng)?;
let sqrt_dt = p_sqrt(&dt, "simulation::traits::custom_walk")?;
let mut price = params.ystep_as_positive()?.to_dec();
let mut path = path_buffer(params.size, 1)?;
let mut vols_out = path_buffer(params.size, 1)?;
path.push(Positive::new_decimal(price).unwrap_or(Positive::ZERO));
vols_out.push(volatility);
for (&vol, _) in vols.iter().zip(1..params.size) {
let z = decimal_normal_sample_with(rng);
let sigma_abs = d_mul(vol.to_dec(), price, "simulation::custom::sigma_abs")?;
let random_step = d_mul(
d_mul(z, sigma_abs, "simulation::custom::random_step")?,
sqrt_dt.to_dec(),
"simulation::custom::random_step",
)?;
price = d_add(
price,
d_add(
d_mul(drift, dt.to_dec(), "simulation::custom::price")?,
random_step,
"simulation::custom::price",
)?,
"simulation::custom::price",
)?;
path.push(
Positive::new_decimal(price.max(Decimal::ZERO)).unwrap_or(Positive::ZERO),
);
vols_out.push(vol);
}
Ok(WalkPath {
prices: path,
vols: Some(vols_out),
})
}
_ => Err(SimulationError::InvalidWalkType { expected: "Custom" }),
}
}
pub fn telegraph_walk<X, Y>(params: &WalkParams<X, Y>) -> Result<WalkPath, SimulationError>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
{
with_walk_rng!(params, telegraph_path)
}
fn telegraph_path<X, Y, R>(
params: &WalkParams<X, Y>,
rng: &mut R,
) -> Result<WalkPath, SimulationError>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
R: Rng + ?Sized,
{
match params.walk_type {
WalkType::Telegraph {
dt,
drift,
volatility,
lambda_up,
lambda_down,
vol_multiplier_up,
vol_multiplier_down,
} => {
let mut values = path_buffer(params.size, 0)?;
let mut vols = path_buffer(params.size, 0)?;
let mut price = params.ystep_as_positive()?.to_dec();
values.push(Positive::new_decimal(price).unwrap_or(Positive::ZERO));
vols.push(volatility);
let mut state: i8 = if decimal_normal_sample_with(rng) < Decimal::ZERO {
1
} else {
-1
};
let sqrt_dt = p_sqrt(&dt, "simulation::traits::telegraph_walk")?;
let vol_mult_up = vol_multiplier_up.unwrap_or(Positive::ONE);
let vol_mult_down = vol_multiplier_down.unwrap_or(Positive::ONE);
let transition_probability = |lambda: Decimal| {
d_sub(
Decimal::ONE,
d_exp(
d_mul(-lambda, dt.to_dec(), "simulation::telegraph::transition")?,
"simulation::telegraph::transition",
)?,
"simulation::telegraph::transition",
)
};
let leave_up = transition_probability(lambda_down.to_dec())?;
let leave_down = transition_probability(lambda_up.to_dec())?;
for _ in 1..params.size {
let transition_prob = if state == 1 { leave_up } else { leave_down };
if decimal_uniform_sample_with(rng) < transition_prob {
state *= -1;
}
let current_vol = if state == 1 {
volatility.checked_mul(&vol_mult_up)?
} else {
volatility.checked_mul(&vol_mult_down)?
};
let z = decimal_normal_sample_with(rng);
let diffusion = d_mul(
d_mul(
current_vol.to_dec(),
sqrt_dt.to_dec(),
"simulation::telegraph::diffusion",
)?,
z,
"simulation::telegraph::diffusion",
)?;
let drift_term = d_mul(drift, dt.to_dec(), "simulation::telegraph::drift")?;
let price_change = d_add(drift_term, diffusion, "simulation::telegraph::price")?;
price = d_mul(
price,
d_exp_f64(price_change, "simulation::telegraph::price")?,
"simulation::telegraph::price",
)?;
values.push(Positive::new_decimal(price).unwrap_or(Positive::ZERO));
vols.push(current_vol);
}
Ok(WalkPath {
prices: values,
vols: Some(vols),
})
}
_ => Err(SimulationError::InvalidWalkType {
expected: "Telegraph",
}),
}
}
fn brownian_path<X, Y, R>(
params: &WalkParams<X, Y>,
rng: &mut R,
) -> Result<Vec<Positive>, SimulationError>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
R: Rng + ?Sized,
{
match params.walk_type {
WalkType::Brownian {
dt,
drift,
volatility,
} => {
let mut values = path_buffer(params.size, 1)?;
let start: Positive = params.ystep_as_positive()?;
values.push(start);
let mut x: Decimal = start.to_dec();
let sigma_abs = volatility.checked_mul(&start)?.to_dec();
let sqrt_dt = decimal_to_f64(dt.to_dec())?.sqrt(); let sqrt_dt_dec = finite_decimal(sqrt_dt).ok_or_else(|| {
SimulationError::non_finite("simulation::brownian::sqrt_dt", sqrt_dt)
})?;
for _ in 1..params.size {
let z = decimal_normal_sample_with(rng);
let diffusion = d_mul(
d_mul(sigma_abs, sqrt_dt_dec, "simulation::brownian::diffusion")?,
z,
"simulation::brownian::diffusion",
)?;
let drift_term = d_mul(drift, dt.to_dec(), "simulation::brownian::drift")?;
x = d_add(
x,
d_add(drift_term, diffusion, "simulation::brownian::step")?,
"simulation::brownian::step",
)?;
values.push(Positive::new_decimal(x.max(Decimal::ZERO)).unwrap_or(Positive::ZERO));
}
Ok(values)
}
_ => Err(SimulationError::InvalidWalkType {
expected: "Brownian",
}),
}
}
fn geometric_brownian_path<X, Y, R>(
params: &WalkParams<X, Y>,
rng: &mut R,
) -> Result<Vec<Positive>, SimulationError>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
R: Rng + ?Sized,
{
match params.walk_type {
WalkType::GeometricBrownian {
dt,
drift,
volatility,
} => {
let mut values = path_buffer(params.size, 0)?;
let mut current_value: Positive = params.ystep_as_positive()?;
values.push(current_value);
let sqrt_dt = p_sqrt(&dt, "simulation::traits::geometric_brownian")?;
for _ in 1..params.size {
let diffusion = d_mul(
d_mul(
decimal_normal_sample_with(rng),
volatility.to_dec(),
"simulation::gbm::diffusion",
)?,
sqrt_dt.to_dec(),
"simulation::gbm::diffusion",
)?;
let drift_term = d_add(
d_mul(drift, dt.to_dec(), "simulation::gbm::drift")?,
diffusion,
"simulation::gbm::drift",
)?;
current_value = current_value
.checked_mul_dec(d_exp_f64(drift_term, "simulation::gbm::price")?)?;
values.push(current_value);
}
Ok(values)
}
_ => Err(SimulationError::InvalidWalkType {
expected: "GeometricBrownian",
}),
}
}
fn log_returns_path<X, Y, R>(
params: &WalkParams<X, Y>,
rng: &mut R,
) -> Result<Vec<Positive>, SimulationError>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
R: Rng + ?Sized,
{
match params.walk_type {
WalkType::LogReturns {
dt,
expected_return,
volatility,
autocorrelation,
} => {
let mut values = path_buffer(params.size, 1)?;
let mut price: Positive = params.ystep_as_positive()?;
values.push(price);
let sqrt_dt = decimal_to_f64(dt.to_dec())?.sqrt(); let sqrt_dt_dec = finite_decimal(sqrt_dt).ok_or_else(|| {
SimulationError::non_finite("simulation::log_returns::sqrt_dt", sqrt_dt)
})?;
let mut prev_log_ret = Decimal::ZERO;
for _ in 1..params.size {
let z = decimal_normal_sample_with(rng);
let diffusion = d_mul(
d_mul(z, volatility.to_dec(), "simulation::log_returns::diffusion")?,
sqrt_dt_dec,
"simulation::log_returns::diffusion",
)?;
let mut log_ret = d_add(
d_mul(
expected_return,
dt.to_dec(),
"simulation::log_returns::log_ret",
)?,
diffusion,
"simulation::log_returns::log_ret",
)?;
if let Some(ac) = autocorrelation {
if !(-Decimal::ONE..=Decimal::ONE).contains(&ac) {
return Err(SimulationError::InvalidAutocorrelation { value: ac });
}
log_ret = d_add(
log_ret,
d_mul(ac, prev_log_ret, "simulation::log_returns::autocorrelation")?,
"simulation::log_returns::autocorrelation",
)?;
}
price =
price.checked_mul_dec(d_exp_f64(log_ret, "simulation::log_returns::price")?)?;
values.push(price);
prev_log_ret = log_ret;
}
Ok(values)
}
_ => Err(SimulationError::InvalidWalkType {
expected: "LogReturns",
}),
}
}
fn mean_reverting_path<X, Y, R>(
params: &WalkParams<X, Y>,
rng: &mut R,
) -> Result<Vec<Positive>, SimulationError>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
R: Rng + ?Sized,
{
match params.walk_type {
WalkType::MeanReverting {
dt,
volatility,
speed,
mean, } => {
let sigma_abs = volatility.checked_mul(&mean)?;
ou_path(
params.ystep_as_positive()?,
mean,
speed,
sigma_abs,
dt,
params.size,
rng,
)
}
_ => Err(SimulationError::InvalidWalkType {
expected: "MeanReverting",
}),
}
}
fn jump_diffusion_path<X, Y, R>(
params: &WalkParams<X, Y>,
rng: &mut R,
) -> Result<Vec<Positive>, SimulationError>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
R: Rng + ?Sized,
{
match params.walk_type {
WalkType::JumpDiffusion {
dt,
drift,
volatility,
intensity,
jump_mean,
jump_volatility,
} => {
let mut values = path_buffer(params.size, 1)?;
let mut x: Decimal = params.ystep_as_positive()?.to_dec();
values.push(Positive::new_decimal(x).unwrap_or(Positive::ZERO));
let sqrt_dt = p_sqrt(&dt, "simulation::traits::jump_diffusion")?;
let lambda_dt = intensity.checked_mul(&dt)?;
for _ in 1..params.size {
let z = decimal_normal_sample_with(rng);
let sigma_abs = d_mul(volatility.to_dec(), x, "simulation::jump::sigma_abs")?;
let diffusion = d_mul(
d_mul(sigma_abs, sqrt_dt.to_dec(), "simulation::jump::diffusion")?,
z,
"simulation::jump::diffusion",
)?;
let drift_term = d_mul(drift, dt.to_dec(), "simulation::jump::drift")?;
let jump = if decimal_uniform_sample_with(rng) < lambda_dt.to_dec() {
d_add(
jump_mean,
d_mul(
decimal_normal_sample_with(rng),
jump_volatility.to_dec(),
"simulation::jump::size",
)?,
"simulation::jump::size",
)?
} else {
Decimal::ZERO
};
x = d_add(
x,
d_add(
d_add(drift_term, diffusion, "simulation::jump::step")?,
jump,
"simulation::jump::step",
)?,
"simulation::jump::step",
)?;
x = x.max(Decimal::ZERO);
values.push(Positive::new_decimal(x).unwrap_or(Positive::ZERO));
}
Ok(values)
}
_ => Err(SimulationError::InvalidWalkType {
expected: "JumpDiffusion",
}),
}
}
pub trait WalkTypeAbleClone<X, Y>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
{
#[must_use]
fn clone_box(&self) -> Box<dyn WalkTypeAble<X, Y>>;
}
impl<T, X, Y> WalkTypeAbleClone<X, Y> for T
where
T: 'static + WalkTypeAble<X, Y> + Clone,
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
{
#[inline]
fn clone_box(&self) -> Box<dyn WalkTypeAble<X, Y>> {
Box::new(self.clone())
}
}
pub trait WalkTypeAble<X, Y>: WalkTypeAbleClone<X, Y> + Send + Sync
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
{
fn generate(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
match ¶ms.walk_type {
WalkType::Brownian { .. } => self.brownian(params),
WalkType::GeometricBrownian { .. } => self.geometric_brownian(params),
WalkType::LogReturns { .. } => self.log_returns(params),
WalkType::MeanReverting { .. } => self.mean_reverting(params),
WalkType::JumpDiffusion { .. } => self.jump_diffusion(params),
WalkType::Garch { .. } => self.garch(params),
WalkType::Heston { .. } => self.heston(params),
WalkType::Custom { .. } => self.custom(params),
WalkType::Telegraph { .. } => self.telegraph(params),
WalkType::Historical { .. } => self.historical(params),
}
}
fn generate_with_vol(&self, params: &WalkParams<X, Y>) -> Result<WalkPath, SimulationError> {
match ¶ms.walk_type {
WalkType::Garch { .. } => self.garch_with_vol(params),
WalkType::Heston { .. } => self.heston_with_vol(params),
WalkType::Custom { .. } => self.custom_with_vol(params),
WalkType::Telegraph { .. } => self.telegraph_with_vol(params),
_ => Ok(WalkPath {
prices: self.generate(params)?,
vols: None,
}),
}
}
fn brownian(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
with_walk_rng!(params, brownian_path)
}
fn geometric_brownian(
&self,
params: &WalkParams<X, Y>,
) -> Result<Vec<Positive>, SimulationError> {
with_walk_rng!(params, geometric_brownian_path)
}
fn log_returns(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
with_walk_rng!(params, log_returns_path)
}
fn mean_reverting(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
with_walk_rng!(params, mean_reverting_path)
}
fn jump_diffusion(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
with_walk_rng!(params, jump_diffusion_path)
}
fn garch(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
Ok(garch_walk(params)?.prices)
}
fn garch_with_vol(&self, params: &WalkParams<X, Y>) -> Result<WalkPath, SimulationError> {
garch_walk(params)
}
fn heston(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
Ok(heston_walk(params)?.prices)
}
fn heston_with_vol(&self, params: &WalkParams<X, Y>) -> Result<WalkPath, SimulationError> {
heston_walk(params)
}
fn custom(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
Ok(custom_walk(params)?.prices)
}
fn custom_with_vol(&self, params: &WalkParams<X, Y>) -> Result<WalkPath, SimulationError> {
custom_walk(params)
}
fn telegraph(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
Ok(telegraph_walk(params)?.prices)
}
fn telegraph_with_vol(&self, params: &WalkParams<X, Y>) -> Result<WalkPath, SimulationError> {
telegraph_walk(params)
}
fn historical(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
match ¶ms.walk_type {
WalkType::Historical {
timeframe: _timeframe,
prices,
symbol: _symbol,
} => match prices.get(0..params.size) {
Some(window) => Ok(window.to_vec()),
None => Err(SimulationError::InsufficientHistoricalData {
required: params.size,
found: prices.len(),
}),
},
_ => Err(SimulationError::InvalidWalkType {
expected: "Historical",
}),
}
}
}
impl<X, Y> Debug for Box<dyn WalkTypeAble<X, Y>> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "WalkTypeAble")
}
}
impl<X, Y> Clone for Box<dyn WalkTypeAble<X, Y>>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
{
fn clone(&self) -> Self {
self.clone_box()
}
}
#[cfg(test)]
mod tests_walk_type_able {
use super::*;
use optionstratlib_core::model::ExpirationDate;
use crate::simulation::model::WalkType;
use crate::simulation::params::WalkParams;
use crate::simulation::steps::Step;
use crate::simulation::traits::WalkTypeAble;
use optionstratlib_core::pos_or_panic;
use optionstratlib_core::utils::TimeFrame;
use rust_decimal::Decimal;
use std::fmt::Display;
use std::ops::AddAssign;
const SEED: u64 = 685;
#[derive(Debug, Clone)]
struct TestWalker {}
impl<X, Y> WalkTypeAble<X, Y> for TestWalker
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: Copy + TryInto<Positive> + Display,
{
}
fn create_test_params<X, Y>(
size: usize,
x_value: X,
y_value: Y,
walk_type: WalkType,
) -> WalkParams<X, Y>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: Copy + TryInto<Positive> + Display,
{
let init_step = Step::new(
x_value,
TimeFrame::Day,
ExpirationDate::Days(pos_or_panic!(30.0)),
y_value,
);
WalkParams {
size,
init_step,
walk_type,
walker: Box::new(TestWalker {}),
seed: Some(SEED),
}
}
#[test]
fn test_brownian_walk() -> Result<(), SimulationError> {
let params = create_test_params(
5,
10.0,
100.0,
WalkType::Brownian {
dt: Positive::ONE,
drift: Decimal::ZERO,
volatility: pos_or_panic!(0.2),
},
);
let walker = TestWalker {};
let result = walker.brownian(¶ms)?;
assert_eq!(result.len(), 5);
Ok(())
}
#[test]
fn test_geometric_brownian_walk() -> Result<(), SimulationError> {
let params = create_test_params(
5,
10.0,
100.0,
WalkType::GeometricBrownian {
dt: Positive::ONE,
drift: Decimal::new(5, 2), volatility: pos_or_panic!(0.2),
},
);
let walker = TestWalker {};
let result = walker.geometric_brownian(¶ms)?;
assert_eq!(result.len(), 5);
Ok(())
}
#[test]
fn test_log_returns_walk() -> Result<(), SimulationError> {
let params = create_test_params(
5,
10.0,
100.0,
WalkType::LogReturns {
dt: Positive::ONE,
expected_return: Decimal::new(5, 2), volatility: pos_or_panic!(0.2),
autocorrelation: None,
},
);
let walker = TestWalker {};
let result = walker.log_returns(¶ms)?;
assert_eq!(result.len(), 5);
Ok(())
}
#[test]
fn test_mean_reverting_walk() -> Result<(), SimulationError> {
let mean_value = pos_or_panic!(150.0);
let params = create_test_params(
5,
10.0,
100.0,
WalkType::MeanReverting {
dt: Positive::ONE,
volatility: pos_or_panic!(0.2),
speed: pos_or_panic!(0.1),
mean: mean_value,
},
);
let walker = TestWalker {};
let result = walker.mean_reverting(¶ms)?;
assert_eq!(result.len(), 5);
assert_eq!(result[0], Positive::HUNDRED);
Ok(())
}
#[test]
fn test_jump_diffusion_walk() -> Result<(), SimulationError> {
let params = create_test_params(
6,
10.0,
100.0,
WalkType::JumpDiffusion {
dt: Positive::ONE,
drift: Decimal::ZERO,
volatility: pos_or_panic!(0.2),
intensity: pos_or_panic!(0.1),
jump_mean: Decimal::ZERO,
jump_volatility: Positive::ONE,
},
);
let walker = TestWalker {};
let result = walker.jump_diffusion(¶ms)?;
assert_eq!(result.len(), 6);
Ok(())
}
fn count_jumps(
steps: usize,
intensity: Positive,
dt: Positive,
) -> Result<u64, SimulationError> {
let params = create_test_params(
steps + 1,
1.0,
100.0,
WalkType::JumpDiffusion {
dt,
drift: Decimal::ZERO,
volatility: Positive::ZERO,
intensity,
jump_mean: Decimal::ONE,
jump_volatility: Positive::ZERO,
},
);
let path = TestWalker {}.jump_diffusion(¶ms)?;
assert_eq!(path.len(), steps + 1);
let mut jumps = 0_u64;
for pair in path.windows(2) {
let increment = pair[1].to_dec() - pair[0].to_dec();
if increment == Decimal::ONE {
jumps += 1;
} else {
assert_eq!(increment, Decimal::ZERO, "step moved without a jump");
}
}
Ok(jumps)
}
fn assert_bernoulli_frequency(jumps: u64, steps: usize, p: f64) {
let n = steps as f64;
let mean = n * p;
let bound = 5.0 * (n * p * (1.0 - p)).sqrt();
assert!(
(jumps as f64 - mean).abs() < bound,
"{jumps} jumps in {steps} steps, expected {mean} +- {bound}"
);
}
#[test]
fn test_jump_diffusion_daily_jump_frequency_matches_lambda_dt() -> Result<(), SimulationError> {
const STEPS: usize = 500_000;
let jumps = count_jumps(STEPS, Positive::ONE, pos_or_panic!(0.004))?;
assert_bernoulli_frequency(jumps, STEPS, 0.004);
Ok(())
}
#[test]
fn test_jump_diffusion_frequent_jump_frequency_matches_lambda_dt() -> Result<(), SimulationError>
{
const STEPS: usize = 100_000;
let jumps = count_jumps(STEPS, pos_or_panic!(25.0), pos_or_panic!(0.004))?;
assert_bernoulli_frequency(jumps, STEPS, 0.1);
Ok(())
}
#[test]
fn test_jump_diffusion_lambda_dt_at_least_one_jumps_every_step() -> Result<(), SimulationError>
{
const STEPS: usize = 1_000;
let jumps = count_jumps(STEPS, pos_or_panic!(250.0), pos_or_panic!(0.004))?;
assert_eq!(jumps, STEPS as u64);
Ok(())
}
#[test]
fn test_jump_diffusion_zero_intensity_never_jumps() -> Result<(), SimulationError> {
const STEPS: usize = 1_000;
let jumps = count_jumps(STEPS, Positive::ZERO, pos_or_panic!(0.004))?;
assert_eq!(jumps, 0);
Ok(())
}
#[test]
fn test_garch_walk() -> Result<(), SimulationError> {
let params = create_test_params(
5,
10.0,
100.0,
WalkType::Garch {
dt: Positive::ONE,
drift: Decimal::ZERO,
volatility: pos_or_panic!(0.2),
alpha: pos_or_panic!(0.1),
beta: pos_or_panic!(0.8),
},
);
let walker = TestWalker {};
let result = walker.garch(¶ms)?;
assert_eq!(result.len(), 5);
Ok(())
}
#[test]
fn test_heston_walk() -> Result<(), SimulationError> {
let params = create_test_params(
5,
10.0,
100.0,
WalkType::Heston {
dt: Positive::ONE,
drift: Decimal::ZERO,
volatility: pos_or_panic!(0.2),
kappa: Positive::TWO,
theta: pos_or_panic!(0.04),
xi: pos_or_panic!(0.4),
rho: Decimal::new(-5, 1), },
);
let walker = TestWalker {};
let result = walker.heston(¶ms)?;
assert_eq!(result.len(), 5);
Ok(())
}
#[test]
fn test_custom_walk() -> Result<(), SimulationError> {
let params = create_test_params(
5,
10.0,
100.0,
WalkType::Custom {
dt: Positive::ONE,
drift: Decimal::ZERO,
volatility: pos_or_panic!(0.2),
vov: pos_or_panic!(0.4),
vol_speed: Positive::ONE,
vol_mean: pos_or_panic!(0.2),
},
);
let walker = TestWalker {};
let result = walker.custom(¶ms)?;
assert_eq!(result.len(), 5);
Ok(())
}
#[test]
fn test_telegraph_walk() -> Result<(), SimulationError> {
let params = create_test_params(
5,
10.0,
100.0,
WalkType::Telegraph {
dt: Positive::ONE,
drift: Decimal::new(5, 2), volatility: pos_or_panic!(0.2),
lambda_up: pos_or_panic!(0.5),
lambda_down: pos_or_panic!(0.3),
vol_multiplier_up: Some(pos_or_panic!(1.5)),
vol_multiplier_down: Some(pos_or_panic!(0.8)),
},
);
let walker = TestWalker {};
let result = walker.telegraph(¶ms)?;
assert_eq!(result.len(), 5);
assert_eq!(result[0], Positive::HUNDRED); Ok(())
}
#[derive(Debug, Default)]
struct SwitchTally {
up_trials: u64,
up_switches: u64,
down_trials: u64,
down_switches: u64,
}
fn tally_switches(
steps: usize,
lambda_up: Positive,
lambda_down: Positive,
) -> Result<SwitchTally, SimulationError> {
let params = create_test_params(
steps + 1,
1.0,
100.0,
WalkType::Telegraph {
dt: pos_or_panic!(0.004),
drift: Decimal::ZERO,
volatility: pos_or_panic!(0.2),
lambda_up,
lambda_down,
vol_multiplier_up: Some(pos_or_panic!(1.5)),
vol_multiplier_down: Some(pos_or_panic!(0.5)),
},
);
let vols = telegraph_walk(¶ms)?
.vols
.ok_or_else(|| SimulationError::walk_error("telegraph returned no vols"))?;
assert_eq!(vols.len(), steps + 1);
let up = pos_or_panic!(0.3);
let down = pos_or_panic!(0.1);
let mut tally = SwitchTally::default();
for pair in vols.windows(2).skip(1) {
let (from, to) = (pair[0], pair[1]);
assert!(to == up || to == down, "{to} is not a regime volatility");
if from == up {
tally.up_trials += 1;
tally.up_switches += u64::from(to == down);
} else {
tally.down_trials += 1;
tally.down_switches += u64::from(to == up);
}
}
Ok(tally)
}
fn assert_switch_frequency(switches: u64, trials: u64, lambda_dt: f64) {
assert!(trials > 0, "no trial started from this regime");
let n = trials as f64;
let p = 1.0 - (-lambda_dt).exp();
let mean = n * p;
let bound = 5.0 * (n * p * (1.0 - p)).sqrt();
assert!(
(switches as f64 - mean).abs() < bound,
"{switches} switches in {trials} trials, expected {mean} +- {bound}"
);
}
#[test]
fn test_telegraph_switch_frequency_matches_one_minus_exp_lambda_dt()
-> Result<(), SimulationError> {
const STEPS: usize = 200_000;
let tally = tally_switches(STEPS, pos_or_panic!(4.0), pos_or_panic!(12.0))?;
assert_eq!(tally.up_trials + tally.down_trials, STEPS as u64 - 1);
assert_switch_frequency(tally.up_switches, tally.up_trials, 12.0 * 0.004);
assert_switch_frequency(tally.down_switches, tally.down_trials, 4.0 * 0.004);
Ok(())
}
#[test]
fn test_telegraph_zero_rates_never_switch() -> Result<(), SimulationError> {
let tally = tally_switches(1_000, Positive::ZERO, Positive::ZERO)?;
assert_eq!(tally.up_switches + tally.down_switches, 0);
Ok(())
}
#[test]
fn test_telegraph_flushed_rate_switches_every_step() -> Result<(), SimulationError> {
let tally = tally_switches(1_000, pos_or_panic!(25_000.0), pos_or_panic!(25_000.0))?;
assert_eq!(tally.up_switches, tally.up_trials);
assert_eq!(tally.down_switches, tally.down_trials);
Ok(())
}
#[test]
fn test_with_different_types() -> Result<(), SimulationError> {
#[derive(Debug, Copy, Clone, PartialEq)]
struct XType(f64);
impl From<XType> for Positive {
fn from(val: XType) -> Self {
pos_or_panic!(val.0)
}
}
impl AddAssign for XType {
fn add_assign(&mut self, other: Self) {
self.0 += other.0;
}
}
impl Display for XType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
struct YType(f64);
impl From<YType> for Positive {
fn from(val: YType) -> Self {
pos_or_panic!(val.0)
}
}
impl Display for YType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
let params = create_test_params(
5,
XType(10.0),
YType(100.0),
WalkType::Brownian {
dt: Positive::ONE,
drift: Decimal::ZERO,
volatility: pos_or_panic!(0.2),
},
);
let walker = TestWalker {};
let result = walker.brownian(¶ms)?;
assert_eq!(result.len(), 5);
Ok(())
}
#[test]
fn test_error_handling() {
#[derive(Clone)]
struct ErrorWalker {}
impl<X, Y> WalkTypeAble<X, Y> for ErrorWalker
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: Copy + TryInto<Positive> + Display,
{
fn brownian(
&self,
_params: &WalkParams<X, Y>,
) -> Result<Vec<Positive>, SimulationError> {
Err(SimulationError::walk_error("Error simulado para prueba"))
}
fn geometric_brownian(
&self,
params: &WalkParams<X, Y>,
) -> Result<Vec<Positive>, SimulationError> {
self.brownian(params)
}
fn log_returns(
&self,
params: &WalkParams<X, Y>,
) -> Result<Vec<Positive>, SimulationError> {
self.brownian(params)
}
fn mean_reverting(
&self,
params: &WalkParams<X, Y>,
) -> Result<Vec<Positive>, SimulationError> {
self.brownian(params)
}
fn jump_diffusion(
&self,
params: &WalkParams<X, Y>,
) -> Result<Vec<Positive>, SimulationError> {
self.brownian(params)
}
fn garch(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
self.brownian(params)
}
fn heston(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
self.brownian(params)
}
fn custom(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
self.brownian(params)
}
}
let params = create_test_params(
5,
10.0,
100.0,
WalkType::Brownian {
dt: Positive::ONE,
drift: Decimal::ZERO,
volatility: pos_or_panic!(0.2),
},
);
let error_walker = ErrorWalker {};
assert!(error_walker.brownian(¶ms).is_err());
}
#[test]
fn test_price_path_and_with_vol_overrides_are_independent() {
use crate::simulation::model::WalkPath;
#[derive(Clone)]
struct RampHestonWalker {}
impl WalkTypeAble<Positive, Positive> for RampHestonWalker {
fn heston(
&self,
params: &WalkParams<Positive, Positive>,
) -> Result<Vec<Positive>, SimulationError> {
let start = params.ystep_as_positive()?;
Ok((0..params.size).map(|i| start + i as f64).collect())
}
}
let params = create_test_params(
4,
Positive::ONE,
Positive::HUNDRED,
WalkType::Heston {
dt: pos_or_panic!(0.01),
drift: Decimal::ZERO,
volatility: pos_or_panic!(0.2),
kappa: Positive::TWO,
theta: pos_or_panic!(0.04),
xi: pos_or_panic!(0.1),
rho: Decimal::ZERO,
},
);
let params = WalkParams {
walker: Box::new(RampHestonWalker {}),
..params
};
let walker = RampHestonWalker {};
let prices = match walker.generate(¶ms) {
Ok(prices) => prices,
Err(e) => panic!("generate failed: {e}"),
};
assert_eq!(
prices,
vec![
Positive::HUNDRED,
pos_or_panic!(101.0),
pos_or_panic!(102.0),
pos_or_panic!(103.0)
],
"generate must honor the heston override"
);
#[derive(Clone)]
struct RampHestonVolWalker {}
impl WalkTypeAble<Positive, Positive> for RampHestonVolWalker {
fn heston_with_vol(
&self,
params: &WalkParams<Positive, Positive>,
) -> Result<WalkPath, SimulationError> {
let start = params.ystep_as_positive()?;
let prices: Vec<Positive> = (0..params.size).map(|i| start + i as f64).collect();
let vols = vec![pos_or_panic!(0.5); params.size];
Ok(WalkPath {
prices,
vols: Some(vols),
})
}
}
let params = WalkParams {
walker: Box::new(RampHestonVolWalker {}),
..params
};
let walker = RampHestonVolWalker {};
let path = match walker.generate_with_vol(¶ms) {
Ok(path) => path,
Err(e) => panic!("generate_with_vol failed: {e}"),
};
assert_eq!(path.vols, Some(vec![pos_or_panic!(0.5); 4]));
assert_eq!(path.prices.first(), Some(&Positive::HUNDRED));
}
#[test]
fn test_box_dyn_walktypeable_clone_roundtrip() {
#[derive(Clone, Debug, PartialEq)]
struct CountingWalker {
seed: u32,
}
impl<X, Y> WalkTypeAble<X, Y> for CountingWalker
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: Copy + TryInto<Positive> + Display,
{
fn brownian(
&self,
params: &WalkParams<X, Y>,
) -> Result<Vec<Positive>, SimulationError> {
let start = params.ystep_as_positive()?;
let mut out = path_buffer(params.size, 0)?;
out.push(start);
for i in 1..params.size {
let offset = Positive::new(f64::from(self.seed) + i as f64)?;
out.push(start + offset);
}
Ok(out)
}
}
let original: Box<dyn WalkTypeAble<Positive, Positive>> =
Box::new(CountingWalker { seed: 42 });
let cloned = original.clone();
let params = create_test_params(
4,
pos_or_panic!(1.0),
Positive::HUNDRED,
WalkType::Brownian {
dt: pos_or_panic!(0.01),
drift: Decimal::ZERO,
volatility: pos_or_panic!(0.2),
},
);
let original_out = original
.brownian(¶ms)
.expect("deterministic walker succeeds");
let cloned_out = cloned.brownian(¶ms).expect("cloned walker succeeds");
assert_eq!(original_out, cloned_out);
}
}
#[cfg(test)]
mod tests_walk_size_panic_freedom {
use super::*;
use crate::simulation::steps::Step;
use optionstratlib_core::model::ExpirationDate;
use optionstratlib_core::utils::TimeFrame;
use rust_decimal_macros::dec;
#[derive(Debug, Clone)]
struct BareWalker;
impl WalkTypeAble<Positive, Positive> for BareWalker {}
fn walk_types() -> Vec<WalkType> {
let dt = Positive::new_decimal(dec!(0.004)).unwrap_or(Positive::ONE);
let volatility = Positive::new_decimal(dec!(0.2)).unwrap_or(Positive::ONE);
vec![
WalkType::Brownian {
dt,
drift: Decimal::ZERO,
volatility,
},
WalkType::GeometricBrownian {
dt,
drift: Decimal::ZERO,
volatility,
},
WalkType::LogReturns {
dt,
expected_return: Decimal::ZERO,
volatility,
autocorrelation: None,
},
WalkType::MeanReverting {
dt,
volatility,
speed: Positive::ONE,
mean: Positive::HUNDRED,
},
WalkType::JumpDiffusion {
dt,
drift: Decimal::ZERO,
volatility,
intensity: Positive::ONE,
jump_mean: Decimal::ZERO,
jump_volatility: volatility,
},
WalkType::Garch {
dt,
drift: Decimal::ZERO,
volatility,
alpha: Positive::new_decimal(dec!(0.1)).unwrap_or(Positive::ZERO),
beta: Positive::new_decimal(dec!(0.8)).unwrap_or(Positive::ZERO),
},
WalkType::Heston {
dt,
drift: Decimal::ZERO,
volatility,
kappa: Positive::ONE,
theta: volatility,
xi: volatility,
rho: Decimal::ZERO,
},
WalkType::Custom {
dt,
drift: Decimal::ZERO,
volatility,
vov: volatility,
vol_speed: Positive::ONE,
vol_mean: volatility,
},
WalkType::Telegraph {
dt,
drift: Decimal::ZERO,
volatility,
lambda_up: Positive::ONE,
lambda_down: Positive::ONE,
vol_multiplier_up: None,
vol_multiplier_down: None,
},
]
}
#[test]
fn test_walk_kernels_unrepresentable_size_return_error() {
for size in [usize::MAX, usize::MAX / 2] {
for walk_type in walk_types() {
let params = WalkParams {
size,
init_step: Step::new(
Positive::ONE,
TimeFrame::Day,
ExpirationDate::Days(Positive::HUNDRED),
Positive::HUNDRED,
),
walk_type: walk_type.clone(),
walker: Box::new(BareWalker),
seed: Some(788),
};
let result = BareWalker.generate(¶ms);
assert!(
matches!(result, Err(SimulationError::InvalidParameters { .. })),
"size {size}, {walk_type:?}: {result:?}"
);
}
}
}
}