#![allow(clippy::indexing_slicing)]
use crate::backtesting::results::SimulationStatsResult;
use crate::error::SimulationError;
use crate::model::decimal::{decimal_normal_sample, finite_decimal};
use crate::simulation::simulator::Simulator;
use crate::simulation::{ExitPolicy, WalkParams, WalkType};
use crate::volatility::generate_ou_process;
use num_traits::ToPrimitive;
use positive::Positive;
use rust_decimal::{Decimal, MathematicalOps};
use std::convert::TryInto;
use std::fmt::{Debug, Display};
use std::ops::AddAssign;
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>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
{
fn brownian(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
match params.walk_type {
WalkType::Brownian {
dt,
drift,
volatility,
} => {
let mut values = Vec::with_capacity(params.size + 1);
let start: Positive = params.ystep_as_positive()?;
values.push(start);
let mut x: Decimal = start.to_dec();
let sigma_abs = (volatility * start).to_dec();
let sqrt_dt = dt.to_f64().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();
let diffusion = sigma_abs * sqrt_dt_dec * z;
let drift_term = drift * dt;
x += drift_term + diffusion;
values.push(
Positive::new_decimal(x.max(Decimal::ZERO)).unwrap_or(Positive::ZERO),
);
}
Ok(values)
}
_ => Err(SimulationError::InvalidWalkType {
expected: "Brownian",
}),
}
}
fn geometric_brownian(
&self,
params: &WalkParams<X, Y>,
) -> Result<Vec<Positive>, SimulationError> {
match params.walk_type {
WalkType::GeometricBrownian {
dt,
drift,
volatility,
} => {
let mut values = Vec::with_capacity(params.size);
let mut current_value: Positive = params.ystep_as_positive()?;
values.push(current_value);
let sqrt_dt = dt.sqrt();
for _ in 1..params.size {
let diffusion = decimal_normal_sample() * volatility * sqrt_dt;
let drift_term = (drift * dt) + diffusion;
current_value *= Decimal::exp(&drift_term);
values.push(current_value);
}
Ok(values)
}
_ => Err(SimulationError::InvalidWalkType {
expected: "GeometricBrownian",
}),
}
}
fn log_returns(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
match params.walk_type {
WalkType::LogReturns {
dt,
expected_return,
volatility,
autocorrelation,
} => {
let mut values = Vec::with_capacity(params.size + 1);
let mut price: Positive = params.ystep_as_positive()?;
values.push(price);
let sqrt_dt = dt.to_f64().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();
let diffusion = z * volatility * sqrt_dt_dec;
let mut log_ret = (expected_return * dt) + diffusion;
if let Some(ac) = autocorrelation {
if !(-Decimal::ONE..=Decimal::ONE).contains(&ac) {
return Err(SimulationError::InvalidAutocorrelation { value: ac });
}
log_ret += ac * prev_log_ret;
}
price *= log_ret.exp();
values.push(price);
prev_log_ret = log_ret;
}
Ok(values)
}
_ => Err(SimulationError::InvalidWalkType {
expected: "LogReturns",
}),
}
}
fn mean_reverting(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
match params.walk_type {
WalkType::MeanReverting {
dt,
volatility,
speed,
mean, } => {
let sigma_abs = volatility * mean;
Ok(generate_ou_process(
params.ystep_as_positive()?,
mean,
speed,
sigma_abs,
dt,
params.size,
))
}
_ => Err(SimulationError::InvalidWalkType {
expected: "MeanReverting",
}),
}
}
fn jump_diffusion(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
match params.walk_type {
WalkType::JumpDiffusion {
dt,
drift,
volatility,
intensity,
jump_mean,
jump_volatility,
} => {
let mut values = Vec::with_capacity(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 = dt.sqrt();
let lambda_dt = intensity * dt;
for _ in 1..params.size {
let z = decimal_normal_sample();
let sigma_abs = volatility.to_dec() * x;
let diffusion = sigma_abs * sqrt_dt.to_dec() * z;
let drift_term = drift * dt;
let jump = if decimal_normal_sample() < lambda_dt.to_dec() {
jump_mean + decimal_normal_sample() * jump_volatility
} else {
Decimal::ZERO
};
x += drift_term + diffusion + jump;
x = x.max(Decimal::ZERO);
values.push(Positive::new_decimal(x).unwrap_or(Positive::ZERO));
}
Ok(values)
}
_ => Err(SimulationError::InvalidWalkType {
expected: "JumpDiffusion",
}),
}
}
fn garch(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
match params.walk_type {
WalkType::Garch {
dt,
drift,
volatility,
alpha,
beta,
} => {
if alpha + beta >= Decimal::ONE {
return Err(SimulationError::GarchStationarity { alpha, beta });
}
let mut path = Vec::with_capacity(params.size + 1);
let mut price = params.ystep_as_positive()?.to_dec();
path.push(Positive::new_decimal(price).unwrap_or(Positive::ZERO));
let mut var = volatility * volatility; let mut prev_eps2 = Decimal::ZERO;
let omega = volatility.powu(2) * (Decimal::ONE - alpha - beta);
let sqrt_dt = dt.to_f64().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 + alpha * prev_eps2 + beta * var;
let z = decimal_normal_sample();
let eps = z * var.sqrt() * sqrt_dt_dec;
let ret = drift * dt + eps;
price *= (ret).exp();
path.push(Positive::new_decimal(price).unwrap_or(Positive::ZERO));
prev_eps2 = eps.powu(2); }
Ok(path)
}
_ => Err(SimulationError::InvalidWalkType { expected: "GARCH" }),
}
}
fn heston(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
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 = Vec::with_capacity(params.size);
let mut price: Positive = params.ystep_as_positive()?;
let mut variance = volatility.to_dec() * volatility.to_dec();
values.push(price);
let dt_sqrt = dt.to_dec().sqrt().ok_or_else(|| {
SimulationError::walk_error("Heston: sqrt(dt) failed (overflow)")
})?;
let one_minus_rho_sq_sqrt = (Decimal::ONE - rho * rho).sqrt().ok_or_else(|| {
SimulationError::walk_error(
"Heston: sqrt(1 - rho^2) failed (rho out of range or overflow)",
)
})?;
for _ in 0..params.size - 1 {
let z1 = decimal_normal_sample();
let z2 = rho * z1 + one_minus_rho_sq_sqrt * decimal_normal_sample();
let variance_sqrt = variance.sqrt().ok_or_else(|| {
SimulationError::walk_error("Heston: sqrt(variance) failed (overflow)")
})?;
let variance_new = (variance
+ kappa.to_dec() * (theta.to_dec() - variance) * dt.to_dec()
+ xi.to_dec() * variance_sqrt * z2 * dt_sqrt)
.max(Decimal::ZERO);
let avg_variance = (variance + variance_new) / Decimal::TWO;
let avg_variance_sqrt = avg_variance.sqrt().ok_or_else(|| {
SimulationError::walk_error("Heston: sqrt(avg_variance) failed (overflow)")
})?;
let price_change = drift * dt.to_dec() + avg_variance_sqrt * z1 * dt_sqrt;
price *= (price_change).exp();
variance = variance_new;
values.push(price);
}
Ok(values)
}
_ => Err(SimulationError::InvalidWalkType { expected: "Heston" }),
}
}
fn custom(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
match params.walk_type {
WalkType::Custom {
dt,
drift,
volatility,
vov,
vol_speed,
vol_mean,
} => {
let vols =
generate_ou_process(volatility, vol_mean, vol_speed, vov, dt, params.size);
let sqrt_dt = dt.sqrt();
let mut price = params.ystep_as_positive()?.to_dec();
let mut path = Vec::with_capacity(params.size + 1);
path.push(Positive::new_decimal(price).unwrap_or(Positive::ZERO));
for &vol in vols.iter().take(params.size - 1) {
let z = decimal_normal_sample();
let sigma_abs = vol.to_dec() * price;
let random_step = z * sigma_abs * sqrt_dt.to_dec();
price += drift * dt + random_step;
path.push(
Positive::new_decimal(price.max(Decimal::ZERO)).unwrap_or(Positive::ZERO),
);
}
Ok(path)
}
_ => Err(SimulationError::InvalidWalkType { expected: "Custom" }),
}
}
fn telegraph(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
match params.walk_type {
WalkType::Telegraph {
dt,
drift,
volatility,
lambda_up,
lambda_down,
vol_multiplier_up,
vol_multiplier_down,
} => {
let mut values = Vec::with_capacity(params.size);
let mut price = params.ystep_as_positive()?.to_dec();
values.push(Positive::new_decimal(price).unwrap_or(Positive::ZERO));
let mut state: i8 = if decimal_normal_sample().to_f64().unwrap_or(0.0) < 0.0 {
1
} else {
-1
};
let sqrt_dt = dt.sqrt();
let vol_mult_up = vol_multiplier_up.unwrap_or(Positive::ONE);
let vol_mult_down = vol_multiplier_down.unwrap_or(Positive::ONE);
for _ in 1..params.size {
let lambda = if state == 1 {
lambda_down.to_dec()
} else {
lambda_up.to_dec()
};
let transition_prob = Decimal::ONE - (-lambda * dt.to_dec()).exp();
let uniform_sample =
(decimal_normal_sample().abs() + Decimal::ONE) / Decimal::TWO; if uniform_sample < transition_prob {
state *= -1;
}
let current_vol = if state == 1 {
volatility * vol_mult_up
} else {
volatility * vol_mult_down
};
let z = decimal_normal_sample();
let diffusion = current_vol.to_dec() * sqrt_dt.to_dec() * z;
let drift_term = drift * dt.to_dec();
let price_change = drift_term + diffusion;
price *= price_change.exp();
values.push(Positive::new_decimal(price).unwrap_or(Positive::ZERO));
}
Ok(values)
}
_ => Err(SimulationError::InvalidWalkType {
expected: "Telegraph",
}),
}
}
fn historical(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
match ¶ms.walk_type {
WalkType::Historical {
timeframe: _timeframe,
prices,
symbol: _symbol,
} => {
if prices.len() >= params.size {
Ok(prices[0..params.size].to_vec())
} else {
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()
}
}
pub trait Simulate<X, Y>
where
X: Copy + TryInto<Positive> + AddAssign + Display,
Y: TryInto<Positive> + Display + Clone,
{
fn simulate(
&self,
sim: &Simulator<X, Y>,
exit: ExitPolicy,
) -> Result<SimulationStatsResult, SimulationError>;
}
#[cfg(test)]
mod tests_walk_type_able {
use super::*;
use crate::ExpirationDate;
use crate::simulation::model::WalkType;
use crate::simulation::params::WalkParams;
use crate::simulation::steps::Step;
use crate::simulation::traits::WalkTypeAble;
use crate::utils::TimeFrame;
use positive::pos_or_panic;
use rust_decimal::Decimal;
use std::fmt::Display;
use std::ops::AddAssign;
#[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 {}),
}
}
#[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(())
}
#[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(())
}
#[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_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 = Vec::with_capacity(params.size);
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);
}
}