use crate::backtesting::results::SimulationStatsResult;
use crate::error::SimulationError;
use crate::model::decimal::decimal_normal_sample;
use crate::simulation::simulator::Simulator;
use crate::simulation::{ExitPolicy, WalkParams, WalkType};
use crate::volatility::generate_ou_process;
use num_traits::{FromPrimitive, ToPrimitive};
use positive::Positive;
use rust_decimal::{Decimal, MathematicalOps};
use std::convert::TryInto;
use std::fmt::{Debug, Display};
use std::ops::AddAssign;
pub trait WalkTypeAble<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 = Decimal::from_f64(sqrt_dt).unwrap_or(Decimal::ZERO);
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("Invalid walk type for Brownian motion".into()),
}
}
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("Invalid walk type for Geometric Brownian motion".into()),
}
}
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 = Decimal::from_f64(sqrt_dt).unwrap_or(Decimal::ZERO);
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 {
assert!((-Decimal::ONE..=Decimal::ONE).contains(&ac));
log_ret += ac * prev_log_ret;
}
price *= log_ret.exp();
values.push(price);
prev_log_ret = log_ret;
}
Ok(values)
}
_ => Err("Invalid walk type for Log Returns motion".into()),
}
}
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("Invalid walk type for Mean Reverting motion".into()),
}
}
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("Invalid walk type for Jump Diffusion motion".into()),
}
}
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("alpha + beta must be < 1 for stationarity".into());
}
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 = Decimal::from_f64(sqrt_dt).unwrap_or(Decimal::ZERO);
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("Invalid walk type for GARCH model".into()),
}
}
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("Correlation rho must be between -1 and 1".into());
}
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);
for _ in 0..params.size - 1 {
let z1 = decimal_normal_sample();
let z2 = rho * z1
+ (Decimal::ONE - rho * rho).sqrt().unwrap() * decimal_normal_sample();
let variance_new = (variance
+ kappa.to_dec() * (theta.to_dec() - variance) * dt.to_dec()
+ xi.to_dec()
* variance.sqrt().unwrap()
* z2
* dt.to_dec().sqrt().unwrap())
.max(Decimal::ZERO);
let avg_variance = (variance + variance_new) / Decimal::TWO;
let price_change = drift * dt.to_dec()
+ avg_variance.sqrt().unwrap() * z1 * dt.to_dec().sqrt().unwrap();
price *= (price_change).exp();
variance = variance_new;
values.push(price);
}
Ok(values)
}
_ => Err("Invalid walk type for Heston model".into()),
}
}
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("Invalid walk type for Custom motion".into()),
}
}
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("Invalid walk type for Telegraph process".into()),
}
}
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("Historical prices are not enough to generate the walk".into())
}
}
_ => Err("Invalid walk type for Historical motion".into()),
}
}
}
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>> {
fn clone(&self) -> Self {
panic!("Box<dyn WalkTypeAble<X, Y>> cannot be cloned. Use clone_box() instead.")
}
}
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::error::Error;
use std::fmt::Display;
use std::ops::AddAssign;
#[derive(Debug)]
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<(), Box<dyn Error>> {
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<(), Box<dyn Error>> {
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<(), Box<dyn Error>> {
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<(), Box<dyn Error>> {
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<(), Box<dyn Error>> {
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<(), Box<dyn Error>> {
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<(), Box<dyn Error>> {
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<(), Box<dyn Error>> {
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<(), Box<dyn Error>> {
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<(), Box<dyn Error>> {
#[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() {
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("Error simulado para prueba".into())
}
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());
}
}