use crate::error::ChainError;
use crate::series::OptionSeries;
use crate::simulation::steps::{Step, Ystep};
use crate::simulation::{WalkParams, WalkType};
use crate::utils::TimeFrame;
use crate::utils::others::calculate_log_returns;
use crate::volatility::{adjust_volatility, constant_volatility};
use core::option::Option;
use positive::Positive;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use tracing::debug;
#[cfg(test)]
use positive::pos_or_panic;
fn create_series_from_step(
previous_y_step: &Ystep<OptionSeries>,
new_price: &Positive,
volatility: Option<Positive>,
) -> Result<OptionSeries, ChainError> {
let series = previous_y_step.value();
let mut series_params = series.to_build_params()?;
series_params.set_underlying_price(new_price);
if let Some(volatility) = volatility {
series_params.set_implied_volatility(volatility);
}
let new_chain = OptionSeries::build_series(&series_params)?;
Ok(new_chain)
}
pub fn generator_optionseries(
walk_params: &WalkParams<Positive, OptionSeries>,
) -> Result<Vec<Step<Positive, OptionSeries>>, ChainError> {
debug!("{}", walk_params);
let (mut y_steps, volatility) = match &walk_params.walk_type {
WalkType::Brownian { volatility, .. } => {
(walk_params.walker.brownian(walk_params)?, Some(*volatility))
}
WalkType::GeometricBrownian { volatility, .. } => (
walk_params.walker.geometric_brownian(walk_params)?,
Some(*volatility),
),
WalkType::LogReturns { volatility, .. } => (
walk_params.walker.log_returns(walk_params)?,
Some(*volatility),
),
WalkType::MeanReverting { volatility, .. } => (
walk_params.walker.mean_reverting(walk_params)?,
Some(*volatility),
),
WalkType::JumpDiffusion { volatility, .. } => (
walk_params.walker.jump_diffusion(walk_params)?,
Some(*volatility),
),
WalkType::Garch { volatility, .. } => {
(walk_params.walker.garch(walk_params)?, Some(*volatility))
}
WalkType::Heston { volatility, .. } => {
(walk_params.walker.heston(walk_params)?, Some(*volatility))
}
WalkType::Custom { volatility, .. } => {
(walk_params.walker.custom(walk_params)?, Some(*volatility))
}
WalkType::Telegraph { volatility, .. } => (
walk_params.walker.telegraph(walk_params)?,
Some(*volatility),
),
WalkType::Historical {
timeframe, prices, ..
} => {
if prices.is_empty() || prices.len() < walk_params.size {
(Vec::new(), None)
} else {
let log_returns: Vec<Decimal> = calculate_log_returns(prices)?
.iter()
.map(|p: &Positive| p.to_dec())
.collect();
let constant_volatility = constant_volatility(&log_returns)?;
let implied_volatility =
adjust_volatility(constant_volatility, *timeframe, TimeFrame::Year)?;
(
walk_params.walker.historical(walk_params)?,
Some(implied_volatility),
)
}
}
};
if y_steps.is_empty() {
return Ok(vec![walk_params.init_step.clone()]);
}
let _ = y_steps.remove(0); let mut steps: Vec<Step<Positive, OptionSeries>> = vec![walk_params.init_step.clone()];
let mut previous_x_step = walk_params.init_step.x;
let mut previous_y_step = walk_params.ystep();
let volatility =
volatility.unwrap_or_else(|| Positive::new_decimal(dec!(0.20)).unwrap_or(Positive::ZERO));
for y_step in y_steps.iter() {
previous_x_step = match previous_x_step.next() {
Ok(x_step) => x_step,
Err(_) => break,
};
let y_step_series: OptionSeries =
create_series_from_step(&previous_y_step, y_step, Some(volatility))?;
previous_y_step = previous_y_step.next(y_step_series).clone();
let step = Step {
x: previous_x_step,
y: previous_y_step.clone(),
};
steps.push(step)
}
if steps.len() > walk_params.size {
debug!(
"generated {} steps, truncating to configured size {}",
steps.len(),
walk_params.size
);
steps.truncate(walk_params.size);
}
Ok(steps)
}
#[cfg(test)]
mod tests_generator_optionseries {
use super::*;
use positive::{assert_pos_relative_eq, spos};
use crate::ExpirationDate;
use crate::chains::utils::OptionChainBuildParams;
use crate::chains::utils::OptionDataPriceParams;
use crate::series::{OptionSeries, OptionSeriesBuildParams};
use crate::simulation::steps::{Step, Xstep, Ystep};
use crate::simulation::{WalkParams, WalkType, WalkTypeAble};
use crate::utils::TimeFrame;
use crate::utils::time::convert_time_frame;
use rust_decimal_macros::dec;
#[derive(Clone)]
struct TestWalker {}
impl TestWalker {
fn new() -> Self {
TestWalker {}
}
}
impl WalkTypeAble<Positive, OptionSeries> for TestWalker {}
fn create_test_option_series() -> OptionSeries {
let price_params = OptionDataPriceParams::new(
Some(Box::new(Positive::HUNDRED)),
Some(ExpirationDate::Days(pos_or_panic!(30.0))),
Some(dec!(0.05)),
spos!(0.02),
Some("TEST".to_string()),
);
let chain_params = OptionChainBuildParams::new(
"TEST".to_string(),
None,
5,
spos!(5.0),
dec!(-0.2),
dec!(0.1),
pos_or_panic!(0.02),
2,
price_params,
pos_or_panic!(0.2),
);
let series = vec![
pos_or_panic!(30.0),
pos_or_panic!(60.0),
pos_or_panic!(90.0),
];
let series_params = OptionSeriesBuildParams::new(chain_params, series);
OptionSeries::build_series(&series_params).unwrap()
}
#[test]
fn test_generator_optionseries_basic() {
let n_steps = 5;
let initial_series = create_test_option_series();
let std_dev = pos_or_panic!(0.2);
let days = pos_or_panic!(30.0);
let walker = Box::new(TestWalker::new());
let walk_params = WalkParams {
size: n_steps,
init_step: Step {
x: Xstep::new(Positive::ONE, TimeFrame::Day, ExpirationDate::Days(days)),
y: Ystep::new(0, initial_series),
},
walk_type: WalkType::GeometricBrownian {
dt: convert_time_frame(Positive::ONE, &TimeFrame::Day, &TimeFrame::Day),
drift: dec!(0.0),
volatility: std_dev,
},
walker,
};
let Ok(steps) = generator_optionseries(&walk_params) else {
panic!("test fixture failed")
};
assert!(!steps.is_empty(), "Steps should not be empty");
assert_eq!(
steps.len(),
5,
"Should start with just the initial step since we're mocking"
);
let first_step = &steps[0];
assert_eq!(
first_step.x.datetime().get_days().unwrap(),
pos_or_panic!(30.0)
);
assert_eq!(*first_step.y.index(), 0);
}
#[test]
fn test_generator_optionseries_empty_result() {
#[derive(Clone)]
struct TestWalker {}
let initial_series = create_test_option_series();
let walker = Box::new(TestWalker {});
impl WalkTypeAble<Positive, OptionSeries> for TestWalker {}
let walk_params = WalkParams {
size: 5,
init_step: Step {
x: Xstep::new(
Positive::ONE,
TimeFrame::Day,
ExpirationDate::Days(pos_or_panic!(30.0)),
),
y: Ystep::new(0, initial_series),
},
walk_type: WalkType::Brownian {
dt: pos_or_panic!(0.01),
drift: dec!(0.0),
volatility: pos_or_panic!(0.2),
},
walker,
};
let Ok(steps) = generator_optionseries(&walk_params) else {
panic!("test fixture failed")
};
assert!(!steps.is_empty(), "Steps shouldn't be empty");
}
#[test]
fn test_generator_optionseries_historical_empty_prices() {
let initial_series = create_test_option_series();
let walker = Box::new(TestWalker::new());
let walk_params = WalkParams {
size: 5,
init_step: Step {
x: Xstep::new(
Positive::ONE,
TimeFrame::Day,
ExpirationDate::Days(pos_or_panic!(30.0)),
),
y: Ystep::new(0, initial_series),
},
walk_type: WalkType::Historical {
timeframe: TimeFrame::Day,
prices: Vec::new(), symbol: None,
},
walker,
};
let Ok(steps) = generator_optionseries(&walk_params) else {
panic!("test fixture failed")
};
assert_eq!(
steps.len(),
1,
"Steps should contain only the init step when historical prices are empty"
);
}
#[test]
fn test_generator_optionseries_historical_insufficient_prices() {
let initial_series = create_test_option_series();
let walker = Box::new(TestWalker::new());
let walk_params = WalkParams {
size: 5,
init_step: Step {
x: Xstep::new(
Positive::ONE,
TimeFrame::Day,
ExpirationDate::Days(pos_or_panic!(30.0)),
),
y: Ystep::new(0, initial_series),
},
walk_type: WalkType::Historical {
timeframe: TimeFrame::Day,
prices: vec![Positive::HUNDRED, pos_or_panic!(101.0)], symbol: None,
},
walker,
};
let Ok(steps) = generator_optionseries(&walk_params) else {
panic!("test fixture failed")
};
assert_eq!(
steps.len(),
1,
"Steps should contain only the init step when historical prices are insufficient"
);
}
#[test]
fn test_generator_optionseries_all_walk_types() {
let initial_series = create_test_option_series();
let walker = Box::new(TestWalker::new());
let volatility = pos_or_panic!(0.2);
let walk_types = vec![
WalkType::Brownian {
dt: pos_or_panic!(0.01),
drift: dec!(0.0),
volatility,
},
WalkType::GeometricBrownian {
dt: pos_or_panic!(0.01),
drift: dec!(0.0),
volatility,
},
WalkType::LogReturns {
dt: pos_or_panic!(0.01),
expected_return: dec!(0.0),
volatility,
autocorrelation: Some(dec!(0.0)),
},
WalkType::MeanReverting {
dt: pos_or_panic!(0.01),
volatility,
speed: pos_or_panic!(0.1),
mean: Positive::HUNDRED,
},
WalkType::JumpDiffusion {
dt: pos_or_panic!(0.01),
drift: dec!(0.0),
volatility,
intensity: pos_or_panic!(0.1),
jump_mean: dec!(0.0),
jump_volatility: pos_or_panic!(0.1),
},
WalkType::Garch {
dt: pos_or_panic!(0.01),
drift: dec!(0.0),
volatility,
alpha: pos_or_panic!(0.1),
beta: pos_or_panic!(0.8),
},
WalkType::Heston {
dt: pos_or_panic!(0.01),
drift: dec!(0.0),
volatility,
kappa: Positive::TWO,
theta: pos_or_panic!(0.04),
xi: pos_or_panic!(0.1),
rho: dec!(-0.7),
},
WalkType::Custom {
dt: pos_or_panic!(0.01),
drift: dec!(0.0),
volatility,
vov: pos_or_panic!(0.1),
vol_speed: pos_or_panic!(0.1),
vol_mean: pos_or_panic!(0.2),
},
];
for walk_type in walk_types {
let walk_params = WalkParams {
size: 5,
init_step: Step {
x: Xstep::new(
Positive::ONE,
TimeFrame::Day,
ExpirationDate::Days(pos_or_panic!(30.0)),
),
y: Ystep::new(0, initial_series.clone()),
},
walk_type,
walker: walker.clone(),
};
let _ = generator_optionseries(&walk_params);
}
}
#[test]
fn test_generator_optionseries_historical() {
let initial_series = create_test_option_series();
let walker = Box::new(TestWalker {});
let historical_prices = vec![
Positive::HUNDRED,
pos_or_panic!(102.0),
pos_or_panic!(98.0),
pos_or_panic!(105.0),
pos_or_panic!(110.0),
pos_or_panic!(115.0),
pos_or_panic!(112.0),
pos_or_panic!(118.0),
pos_or_panic!(120.0),
pos_or_panic!(125.0),
];
let walk_params = WalkParams {
size: 5,
init_step: Step {
x: Xstep::new(
Positive::ONE,
TimeFrame::Day,
ExpirationDate::Days(pos_or_panic!(30.0)),
),
y: Ystep::new(0, initial_series),
},
walk_type: WalkType::Historical {
timeframe: TimeFrame::Day,
prices: historical_prices,
symbol: None,
},
walker,
};
let Ok(steps) = generator_optionseries(&walk_params) else {
panic!("test fixture failed")
};
assert!(!steps.is_empty(), "Should have at least the initial step");
assert_eq!(
steps.len(),
5,
"Should have just the initial step with our mock"
);
}
#[test]
fn test_create_series_from_step() {
let initial_series = create_test_option_series();
let y_step = Ystep::new(0, initial_series);
let new_price = pos_or_panic!(105.0);
let volatility = spos!(0.22);
let result = create_series_from_step(&y_step, &new_price, volatility);
assert!(result.is_ok(), "create_series_from_step should succeed");
let new_series = result.unwrap();
assert_eq!(
new_series.underlying_price, new_price,
"New series should have updated price"
);
if let Ok(params) = new_series.to_build_params() {
let iv = params.chain_params.get_implied_volatility();
assert_pos_relative_eq!(iv, volatility.unwrap(), pos_or_panic!(0.01));
}
}
#[test]
fn test_assert_steps_length() {
let n_steps = 3;
let initial_series = create_test_option_series();
let walker = Box::new(TestWalker {});
let walk_params = WalkParams {
size: n_steps,
init_step: Step {
x: Xstep::new(
Positive::ONE,
TimeFrame::Day,
ExpirationDate::Days(pos_or_panic!(30.0)),
),
y: Ystep::new(0, initial_series),
},
walk_type: WalkType::GeometricBrownian {
dt: pos_or_panic!(0.01),
drift: dec!(0.0),
volatility: pos_or_panic!(0.2),
},
walker,
};
let Ok(steps) = generator_optionseries(&walk_params) else {
panic!("test fixture failed")
};
assert!(
steps.len() <= n_steps,
"Steps length should not exceed the specified size"
);
}
}