#![allow(clippy::indexing_slicing)]
use crate::constants::{MAX_VOLATILITY, MIN_VOLATILITY};
use crate::error::VolatilityError;
use crate::model::decimal::{
d_add, d_div, d_mul, d_sub, d_sum, decimal_normal_sample, finite_decimal,
};
use crate::utils::time::TimeFrame;
use crate::{ExpirationDate, OptionStyle, OptionType, Options, Side};
use num_traits::{FromPrimitive, ToPrimitive};
use positive::Positive;
use rand::random;
use rayon::prelude::*;
use rust_decimal::{Decimal, MathematicalOps};
use tracing::instrument;
#[cfg(test)]
use positive::pos_or_panic;
pub fn constant_volatility(returns: &[Decimal]) -> Result<Positive, VolatilityError> {
let n_dec =
Decimal::from_usize(returns.len()).ok_or_else(|| VolatilityError::NumericalFailure {
reason: format!(
"constant_volatility: returns.len() {} not representable as Decimal",
returns.len()
),
})?;
let n = Positive::new_decimal(n_dec).unwrap_or(Positive::ZERO);
if n < Decimal::TWO {
return Ok(Positive::ZERO);
}
let total = d_sum(returns, "volatility::constant::total")?;
let mean = d_div(total, n.to_dec(), "volatility::constant::mean")?;
let mut sq_total = Decimal::ZERO;
for &r in returns {
let centred = d_sub(r, mean, "volatility::constant::centred")?;
let centred_sq = d_mul(centred, centred, "volatility::constant::centred_sq")?;
sq_total = d_add(sq_total, centred_sq, "volatility::constant::sq_total")?;
}
let variance = d_div(
sq_total,
(n - Decimal::ONE).to_dec(),
"volatility::constant::variance",
)?;
let std_dev = variance
.sqrt()
.ok_or_else(|| VolatilityError::NumericalFailure {
reason: "constant_volatility: sqrt(variance) failed (overflow)".to_string(),
})?;
Ok(Positive::new_decimal(std_dev).unwrap_or(Positive::ZERO))
}
pub fn historical_volatility(
returns: &[Decimal],
window_size: usize,
) -> Result<Vec<Positive>, VolatilityError> {
returns
.windows(window_size)
.map(constant_volatility)
.collect()
}
pub fn ewma_volatility(
returns: &[Decimal],
lambda: Decimal,
) -> Result<Vec<Positive>, VolatilityError> {
let first_return = returns
.first()
.ok_or_else(|| VolatilityError::NumericalFailure {
reason: "ewma_volatility: returns slice is empty".to_string(),
})?;
let mut variance = d_mul(
*first_return,
*first_return,
"volatility::ewma::initial_variance",
)?;
let initial_std_dev = variance
.sqrt()
.ok_or_else(|| VolatilityError::NumericalFailure {
reason: "ewma_volatility: sqrt(initial variance) failed (overflow)".to_string(),
})?;
let mut volatilities = vec![Positive::new_decimal(initial_std_dev).unwrap_or(Positive::ZERO)];
for &return_value in &returns[1..] {
let persistent = d_mul(lambda, variance, "volatility::ewma::persistent")?;
let innovation_weight = d_sub(Decimal::ONE, lambda, "volatility::ewma::innovation_weight")?;
let return_sq = d_mul(return_value, return_value, "volatility::ewma::return_sq")?;
let innovation = d_mul(innovation_weight, return_sq, "volatility::ewma::innovation")?;
variance = d_add(persistent, innovation, "volatility::ewma::variance")?;
let std_dev = variance
.sqrt()
.ok_or_else(|| VolatilityError::NumericalFailure {
reason: "ewma_volatility: sqrt(variance) failed (overflow)".to_string(),
})?;
volatilities.push(Positive::new_decimal(std_dev).unwrap_or(Positive::ZERO));
}
Ok(volatilities)
}
#[instrument(skip(options), fields(
market_price = %market_price,
strike = %options.strike_price,
max_iterations,
))]
pub fn implied_volatility(
market_price: Positive,
options: &mut Options,
max_iterations: i64,
) -> Result<Positive, VolatilityError> {
let base_option = options.clone();
let iterations = 100 * max_iterations;
let result = (1..iterations)
.into_par_iter()
.map(|i| {
let mut option = base_option.clone();
option.side = Side::Long; let iv = Positive::new(i as f64 / iterations as f64).unwrap_or(Positive::ZERO);
option.implied_volatility = iv;
match option.calculate_price_black_scholes() {
Ok(price) => {
let diff = (price - market_price.to_dec()).abs();
Some((iv, diff))
}
Err(_) => None,
}
})
.filter_map(|x| x) .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
match result {
Some((best_iv, _)) => {
let iv = best_iv.clamp(*MIN_VOLATILITY, MAX_VOLATILITY);
if iv == Positive::new(1f64 / iterations as f64)? {
Err(VolatilityError::IvNotFound)
} else {
Ok(iv)
}
}
None => Err(VolatilityError::NoValidVolatility),
}
}
pub fn calculate_iv(
option_price: Positive,
strike: Positive,
option_style: OptionStyle,
underlying_price: Positive,
days: Positive,
symbol: String,
) -> Result<Positive, VolatilityError> {
let mut option = Options::new(
OptionType::European,
Side::Long,
symbol,
strike,
ExpirationDate::Days(days),
Positive::ZERO,
Positive::ONE,
underlying_price,
Decimal::ZERO,
option_style,
Positive::ZERO,
None,
);
implied_volatility(option_price, &mut option, 10)
}
pub fn garch_volatility(
returns: &[Decimal],
omega: Decimal,
alpha: Decimal,
beta: Decimal,
) -> Result<Vec<Positive>, VolatilityError> {
let to_positive_variance =
|value: Decimal, context: &'static str| -> Result<Positive, VolatilityError> {
Positive::new_decimal(value).map_err(|_| VolatilityError::NumericalFailure {
reason: format!("garch_volatility: negative variance at {context} ({value})"),
})
};
let seed = d_mul(
returns[0],
returns[0],
"volatility::garch::initial_variance",
)?;
let mut variance = to_positive_variance(seed, "initial_variance")?;
let mut volatilities = vec![variance.sqrt()];
for &return_value in &returns[1..] {
let return_sq = d_mul(return_value, return_value, "volatility::garch::return_sq")?;
let alpha_r2 = d_mul(alpha, return_sq, "volatility::garch::alpha_r2")?;
let beta_v = d_mul(beta, variance.to_dec(), "volatility::garch::beta_v")?;
let persistent = d_add(omega, alpha_r2, "volatility::garch::omega_plus_alpha")?;
let next_variance = d_add(persistent, beta_v, "volatility::garch::variance")?;
variance = to_positive_variance(next_variance, "variance")?;
volatilities.push(variance.sqrt());
}
Ok(volatilities)
}
pub fn simulate_heston_volatility(
kappa: Decimal,
theta: Decimal,
xi: Decimal,
v0: Decimal,
dt: Decimal,
steps: usize,
) -> Result<Vec<Positive>, VolatilityError> {
let mut v = v0.max(Decimal::ZERO);
let mut v_pos = Positive::new_decimal(v).unwrap_or(Positive::ZERO);
let mut volatilities = vec![v_pos.sqrt()];
let dt_sqrt_f64 = dt
.sqrt()
.ok_or_else(|| VolatilityError::NumericalFailure {
reason: "simulate_heston_volatility: sqrt(dt) failed (overflow)".to_string(),
})?
.to_f64()
.ok_or_else(|| VolatilityError::NumericalFailure {
reason: "simulate_heston_volatility: sqrt(dt) not representable as f64".to_string(),
})?;
for _ in 1..steps {
let dw_f64 = random::<f64>() * dt_sqrt_f64;
let dw = finite_decimal(dw_f64)
.ok_or_else(|| VolatilityError::non_finite("volatility::heston::dw", dw_f64))?;
let sqrt_v = v_pos.sqrt().to_dec();
v += kappa * (theta - v) * dt + xi * sqrt_v * dw;
v = v.max(Decimal::ZERO); v_pos = Positive::new_decimal(v).unwrap_or(Positive::ZERO);
volatilities.push(v_pos.sqrt());
}
Ok(volatilities)
}
pub fn uncertain_volatility_bounds(
option: &Options,
min_volatility: Positive,
max_volatility: Positive,
) -> Result<(Positive, Positive), VolatilityError> {
let mut lower_bound_option = option.clone();
lower_bound_option.implied_volatility = min_volatility;
let mut upper_bound_option = option.clone();
upper_bound_option.implied_volatility = max_volatility;
let lower_bound = Positive::new_decimal(lower_bound_option.calculate_price_black_scholes()?)
.unwrap_or(Positive::ZERO);
let upper_bound = Positive::new_decimal(upper_bound_option.calculate_price_black_scholes()?)
.unwrap_or(Positive::ZERO);
Ok((lower_bound, upper_bound))
}
#[inline]
pub fn annualized_volatility(
volatility: Positive,
timeframe: TimeFrame,
) -> Result<Positive, VolatilityError> {
Ok(volatility * timeframe.periods_per_year().sqrt())
}
#[inline]
pub fn de_annualized_volatility(
annual_volatility: Positive,
timeframe: TimeFrame,
) -> Result<Positive, VolatilityError> {
Ok(annual_volatility / timeframe.periods_per_year().sqrt())
}
pub fn adjust_volatility(
volatility: Positive,
from_frame: TimeFrame,
to_frame: TimeFrame,
) -> Result<Positive, VolatilityError> {
if from_frame == to_frame {
return Ok(volatility);
}
let from_periods = from_frame.periods_per_year();
let to_periods = to_frame.periods_per_year();
if from_periods == to_periods {
return Ok(volatility);
}
if to_periods.is_zero() {
return Err(VolatilityError::InvalidTime {
time: to_periods,
reason: format!(
"Cannot adjust volatility to timeframe with zero periods per year: {to_frame:?}"
),
});
}
let scale_factor = (from_periods / to_periods).sqrt();
Ok(volatility * scale_factor)
}
#[inline]
pub fn volatility_for_dt(
annual_volatility: Positive,
_dt: Positive,
_dt_timeframe: TimeFrame,
dt_base_timeframe: TimeFrame,
) -> Result<Positive, VolatilityError> {
de_annualized_volatility(annual_volatility, dt_base_timeframe)
}
#[must_use]
pub fn generate_ou_process(
x0: Positive,
mu: Positive,
theta: Positive,
volatility: Positive,
dt: Positive,
steps: usize,
) -> Vec<Positive> {
let sqrt_dt = dt.sqrt();
let mut x = x0.to_dec();
let mut result = Vec::with_capacity(steps);
result.push(Positive::new_decimal(x).unwrap_or(Positive::ZERO));
for _ in 1..steps {
let dw = decimal_normal_sample() * sqrt_dt; let drift = (theta * mu.sub_or_zero(&x) * dt).to_dec(); let diffusion = dw * volatility; x += drift + diffusion; x = x.max(Decimal::ZERO); result.push(Positive::new_decimal(x).unwrap_or(Positive::ZERO));
}
result
}
#[cfg(test)]
mod tests_annualize_volatility {
use super::*;
use positive::assert_pos_relative_eq;
#[test]
fn test_annualize_daily_volatility() {
let daily_vol = pos_or_panic!(0.01); let annual_vol = annualized_volatility(daily_vol, TimeFrame::Day).unwrap();
let expected = daily_vol * pos_or_panic!(252.0f64.sqrt());
assert_pos_relative_eq!(annual_vol, expected, pos_or_panic!(1e-10));
}
#[test]
fn test_deannualize_annual_volatility() {
let annual_vol = pos_or_panic!(0.20); let daily_vol = de_annualized_volatility(annual_vol, TimeFrame::Day).unwrap();
let expected = pos_or_panic!(0.01259881576697424);
assert_pos_relative_eq!(daily_vol, expected, pos_or_panic!(1e-10));
}
#[test]
fn test_custom_timeframe() {
let custom_periods = Positive::HUNDRED;
let vol = pos_or_panic!(0.05);
let annual_vol = annualized_volatility(vol, TimeFrame::Custom(custom_periods)).unwrap();
let expected = vol * custom_periods.sqrt();
assert_pos_relative_eq!(annual_vol, expected, pos_or_panic!(1e-10));
}
#[test]
fn test_conversion_roundtrip() {
let original_vol = pos_or_panic!(0.15);
let annualized = annualized_volatility(original_vol, TimeFrame::Day).unwrap();
let roundtrip = de_annualized_volatility(annualized, TimeFrame::Day).unwrap();
assert_pos_relative_eq!(original_vol, roundtrip, pos_or_panic!(1e-10));
}
#[test]
fn test_different_timeframes() {
let daily_vol = pos_or_panic!(0.01);
let weekly_vol = annualized_volatility(daily_vol, TimeFrame::Day).unwrap();
let monthly_vol = de_annualized_volatility(weekly_vol, TimeFrame::Month).unwrap();
assert!(monthly_vol > daily_vol); }
}
#[cfg(test)]
mod tests_constant_volatility {
use super::*;
use crate::constants::ZERO;
use positive::assert_pos_relative_eq;
use rust_decimal_macros::dec;
#[test]
fn test_constant_volatility_single_value() {
let returns = [dec!(0.05)];
let result = constant_volatility(&returns).unwrap();
assert_eq!(result, ZERO);
}
#[test]
fn test_constant_volatility_identical_values() {
let returns = [dec!(0.02), dec!(0.02), dec!(0.02), dec!(0.02)];
let result = constant_volatility(&returns).unwrap();
assert_eq!(result, ZERO);
}
#[test]
fn test_constant_volatility_varying_values() {
let returns = [dec!(0.01), dec!(0.03), dec!(0.02), dec!(0.04)];
let result = constant_volatility(&returns).unwrap();
assert_pos_relative_eq!(
result,
pos_or_panic!(0.012909944487358056),
pos_or_panic!(1e-10)
);
}
}
#[cfg(test)]
mod tests_historical_volatility {
use super::*;
use positive::assert_pos_relative_eq;
use rust_decimal_macros::dec;
#[test]
fn test_historical_volatility_empty_returns() {
let returns: [Decimal; 0] = [];
let result = historical_volatility(&returns, 3).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_historical_volatility_single_value() {
let returns = [dec!(0.02)];
let result = historical_volatility(&returns, 3).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_historical_volatility_insufficient_data() {
let returns = [dec!(0.01), dec!(0.02)];
let result = historical_volatility(&returns, 3).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_historical_volatility_exact_window() {
let returns = [dec!(0.01), dec!(0.02), dec!(0.03)];
let result = historical_volatility(&returns, 3).unwrap();
assert_eq!(result.len(), 1);
assert_pos_relative_eq!(result[0], pos_or_panic!(0.01), pos_or_panic!(1e-10));
}
#[test]
fn test_historical_volatility_larger_window() {
let returns = [dec!(0.01), dec!(0.02), dec!(0.03), dec!(0.04)];
let result = historical_volatility(&returns, 3).unwrap();
assert_eq!(result.len(), 2);
assert_pos_relative_eq!(result[0], pos_or_panic!(0.01), pos_or_panic!(1e-10));
assert_pos_relative_eq!(result[1], pos_or_panic!(0.01), pos_or_panic!(1e-10));
}
}
#[cfg(test)]
mod tests_ewma_volatility {
use super::*;
use positive::assert_pos_relative_eq;
use rust_decimal_macros::dec;
#[test]
fn test_ewma_volatility_single_return() {
let returns = [dec!(0.02)];
let lambda = dec!(0.94);
let result = ewma_volatility(&returns, lambda).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], pos_or_panic!(0.02)); }
#[test]
fn test_ewma_volatility_constant_returns() {
let returns = [dec!(0.02), dec!(0.02), dec!(0.02), dec!(0.02)];
let lambda = dec!(0.94);
let single_value = dec!(0.02);
let result = ewma_volatility(&returns, lambda).unwrap();
assert_eq!(result.len(), 4);
let expected = [
pos_or_panic!(0.02),
Positive::try_from(
(lambda * single_value.powi(2) + (Decimal::ONE - lambda) * single_value.powi(2))
.sqrt()
.unwrap(),
)
.unwrap_or_default(),
Positive::try_from(
(lambda * single_value.powi(2) + (Decimal::ONE - lambda) * single_value.powi(2))
.sqrt()
.unwrap()
.powi(2),
)
.unwrap_or_default()
.sqrt(),
Positive::try_from(
(lambda * single_value.powi(2) + (Decimal::ONE - lambda) * single_value.powi(2))
.sqrt()
.unwrap()
.powi(2)
.sqrt()
.unwrap()
.powi(2),
)
.unwrap_or_default()
.sqrt(),
];
for (res, exp) in result.iter().zip(expected.iter()) {
assert_pos_relative_eq!(*res, *exp, pos_or_panic!(1e-10));
}
}
#[test]
fn test_ewma_volatility_varying_returns() {
let returns = [dec!(0.01), dec!(0.02), dec!(0.03), dec!(0.04)];
let lambda = dec!(0.94);
let result = ewma_volatility(&returns, lambda).unwrap();
assert_eq!(result.len(), 4);
assert!(result.iter().all(|&x| x > Positive::ZERO));
assert!(
result
.iter()
.zip(result.iter().skip(1))
.all(|(a, b)| *b >= *a)
);
}
#[test]
fn test_ewma_volatility_low_lambda() {
let returns = [dec!(0.01), dec!(0.02), dec!(0.03), dec!(0.04)];
let lambda = dec!(0.5); let result = ewma_volatility(&returns, lambda).unwrap();
assert_eq!(result.len(), 4);
let last = result.last().unwrap();
assert!(*last > *result.first().unwrap());
}
#[test]
fn test_ewma_volatility_high_lambda() {
let returns = [dec!(0.01), dec!(0.02), dec!(0.03), dec!(0.04)];
let lambda = dec!(0.99); let result = ewma_volatility(&returns, lambda).unwrap();
assert_eq!(result.len(), 4);
let differences: Vec<_> = result.windows(2).map(|w| w[1] - w[0]).collect();
assert!(differences.iter().all(|&d| d.to_dec() < dec!(0.01)));
}
}
#[cfg(test)]
mod tests_implied_volatility {
use super::*;
use crate::ExpirationDate;
use crate::assert_decimal_eq;
use crate::constants::{MAX_VOLATILITY, MIN_VOLATILITY};
use crate::greeks::Greeks;
use crate::model::types::{OptionStyle, OptionType, Side};
use positive::assert_pos_relative_eq;
use rust_decimal_macros::dec;
use tracing::error;
fn create_test_option() -> Options {
Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
Positive::HUNDRED, ExpirationDate::Days(pos_or_panic!(30.0)), pos_or_panic!(0.2), Positive::ONE, Positive::HUNDRED, dec!(0.05), OptionStyle::Call, Positive::ZERO, None, )
}
#[test]
fn test_implied_volatility_max_iterations() {
let mut option = create_test_option();
let market_price = pos_or_panic!(5.0);
let result = implied_volatility(market_price, &mut option, 1);
assert!(result.is_ok());
let iv = result.unwrap();
assert!(iv >= *MIN_VOLATILITY && iv <= MAX_VOLATILITY);
let result = calculate_iv(
market_price,
Positive::HUNDRED,
OptionStyle::Call,
Positive::HUNDRED,
pos_or_panic!(30.0),
"TEST".to_string(),
);
assert!(result.is_ok());
let iv = result.unwrap();
assert!(iv >= *MIN_VOLATILITY && iv <= MAX_VOLATILITY);
assert_pos_relative_eq!(iv, pos_or_panic!(0.437), pos_or_panic!(1e-3));
}
#[test]
fn test_implied_volatility_zero_dte() {
let iv = pos_or_panic!(0.25);
let mut option = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(pos_or_panic!(0.5)),
iv,
Positive::ONE,
Positive::HUNDRED,
dec!(0.0),
OptionStyle::Call,
Positive::ZERO,
None,
);
let delta = option.delta().unwrap();
let gamma = option.gamma().unwrap();
let vega = option.vega().unwrap();
let theta = option.theta().unwrap();
let rho = option.rho().unwrap();
let vanna = option.vanna().unwrap();
let vomma = option.vomma().unwrap();
let veta = option.veta().unwrap();
let charm = option.charm().unwrap();
let color = option.color().unwrap();
assert_decimal_eq!(delta, dec!(0.502), dec!(0.002));
assert_decimal_eq!(gamma, dec!(0.431), dec!(0.002));
assert_decimal_eq!(vega, dec!(0.015), dec!(0.002));
assert_decimal_eq!(theta, dec!(-0.369), dec!(0.002));
assert_decimal_eq!(rho, dec!(0.001), dec!(0.002));
assert_decimal_eq!(vanna, dec!(0.0073826), dec!(0.0000001));
assert_decimal_eq!(vomma, dec!(-0.0000012641), dec!(0.0000000001));
assert_decimal_eq!(veta, dec!(0.0002138), dec!(0.0000001));
assert_decimal_eq!(charm, dec!(-0.0018456), dec!(0.0000001));
assert_decimal_eq!(color, dec!(-0.4311576), dec!(0.0000001));
let market_price = option.calculate_price_black_scholes().unwrap();
assert_decimal_eq!(market_price, dec!(0.369), dec!(0.002));
option.implied_volatility = pos_or_panic!(0.4); let iv_result = implied_volatility(
Positive::new_decimal(market_price).unwrap(),
&mut option,
10,
);
match iv_result {
Ok(iv_aprox) => {
assert_pos_relative_eq!(iv_aprox, iv, pos_or_panic!(0.001)); }
Err(_) => {
error!("Non-convergence is acceptable for zero DTE options");
}
}
option.implied_volatility = iv;
option.option_style = OptionStyle::Put;
let delta = option.delta().unwrap();
let gamma = option.gamma().unwrap();
let vega = option.vega().unwrap();
let theta = option.theta().unwrap();
let rho = option.rho().unwrap();
let vanna = option.vanna().unwrap();
let vomma = option.vomma().unwrap();
let veta = option.veta().unwrap();
let charm = option.charm().unwrap();
let color = option.color().unwrap();
assert_decimal_eq!(delta, dec!(-0.498), dec!(0.002));
assert_decimal_eq!(gamma, dec!(0.431), dec!(0.002));
assert_decimal_eq!(vega, dec!(0.015), dec!(0.002));
assert_decimal_eq!(theta, dec!(-0.369), dec!(0.002));
assert_decimal_eq!(rho, dec!(0.001), dec!(0.002));
assert_decimal_eq!(vanna, dec!(0.0073826), dec!(0.0000001));
assert_decimal_eq!(vomma, dec!(-0.0000012641), dec!(0.0000000001));
assert_decimal_eq!(veta, dec!(0.0002138), dec!(0.0000001));
assert_decimal_eq!(charm, dec!(-0.0018456), dec!(0.0000001));
assert_decimal_eq!(color, dec!(-0.4311576), dec!(0.0000001));
let market_price = option.calculate_price_black_scholes().unwrap();
assert_decimal_eq!(market_price, dec!(0.369), dec!(0.002));
option.implied_volatility = pos_or_panic!(0.4); let iv_result = implied_volatility(
Positive::new_decimal(market_price).unwrap(),
&mut option,
10,
);
match iv_result {
Ok(iv_aprox) => {
assert_pos_relative_eq!(iv_aprox, iv, pos_or_panic!(0.001)); }
Err(_) => {
error!("Non-convergence is acceptable for zero DTE options");
}
}
}
#[test]
fn test_implied_volatility_zero_dte_real() {
let iv = pos_or_panic!(0.356831);
let mut option = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
pos_or_panic!(20600.0),
ExpirationDate::Days(pos_or_panic!(0.52)),
iv,
Positive::ONE,
pos_or_panic!(21049.88),
dec!(0.0),
OptionStyle::Call,
pos_or_panic!(0.05),
None,
);
let delta = option.delta().unwrap();
let gamma = option.gamma().unwrap();
let vega = option.vega().unwrap();
let theta = option.theta().unwrap();
let rho = option.rho().unwrap();
let vanna = option.vanna().unwrap();
let vomma = option.vomma().unwrap();
let veta = option.veta().unwrap();
let charm = option.charm().unwrap();
let color = option.color().unwrap();
assert_decimal_eq!(delta, dec!(0.946), dec!(0.002));
assert_decimal_eq!(gamma, dec!(0.00038), dec!(0.002));
assert_decimal_eq!(vega, dec!(0.866), dec!(0.002));
assert_decimal_eq!(theta, dec!(-26.990), dec!(0.002));
assert_decimal_eq!(rho, dec!(0.277), dec!(0.002));
assert_decimal_eq!(vanna, dec!(-0.4879786), dec!(0.0000001));
assert_decimal_eq!(vomma, dec!(6.2450679681), dec!(0.00000001));
assert_decimal_eq!(veta, dec!(0.0433019), dec!(0.0000001));
assert_decimal_eq!(charm, dec!(0.1686671), dec!(0.0000001));
assert_decimal_eq!(color, dec!(0.0005877), dec!(0.0000001));
let market_price = option.calculate_price_black_scholes().unwrap();
assert_decimal_eq!(market_price, dec!(454.917), dec!(0.002));
option.implied_volatility = pos_or_panic!(0.4); let iv_result = implied_volatility(
Positive::new_decimal(market_price).unwrap(),
&mut option,
10,
);
match iv_result {
Ok(iv_aprox) => {
assert_pos_relative_eq!(iv_aprox, iv, pos_or_panic!(0.001));
}
Err(_) => {
error!("Non-convergence is acceptable for zero DTE options");
}
}
option.implied_volatility = iv;
option.option_style = OptionStyle::Put;
let delta = option.delta().unwrap();
let gamma = option.gamma().unwrap();
let vega = option.vega().unwrap();
let theta = option.theta().unwrap();
let rho = option.rho().unwrap();
let vanna = option.vanna().unwrap();
let vomma = option.vomma().unwrap();
let veta = option.veta().unwrap();
let charm = option.charm().unwrap();
let color = option.color().unwrap();
assert_decimal_eq!(delta, dec!(-0.053), dec!(0.001));
assert_decimal_eq!(gamma, dec!(0.0), dec!(0.001));
assert_decimal_eq!(vega, dec!(0.866), dec!(0.001));
assert_decimal_eq!(theta, dec!(-29.874), dec!(0.001));
assert_decimal_eq!(rho, dec!(-0.016), dec!(0.001));
assert_decimal_eq!(vanna, dec!(-0.4879786), dec!(0.0000001));
assert_decimal_eq!(vomma, dec!(6.2450679681), dec!(0.00000001));
assert_decimal_eq!(veta, dec!(0.0433019), dec!(0.0000001));
assert_decimal_eq!(charm, dec!(0.1685301), dec!(0.0000001));
assert_decimal_eq!(color, dec!(0.0005877), dec!(0.0000001));
let market_price = option.calculate_price_black_scholes().unwrap();
assert_decimal_eq!(market_price, dec!(6.537), dec!(0.002));
option.implied_volatility = pos_or_panic!(0.4); let iv_result = implied_volatility(
Positive::new_decimal(market_price).unwrap(),
&mut option,
10,
);
match iv_result {
Ok(iv_aprox) => {
assert_pos_relative_eq!(iv_aprox, iv, pos_or_panic!(0.001));
}
Err(_) => {
error!("Non-convergence is acceptable for zero DTE options");
}
}
}
#[test]
fn test_implied_volatility_zero_dte_real_short() {
let iv = pos_or_panic!(0.356831);
let mut option = Options::new(
OptionType::European,
Side::Short,
"TEST".to_string(),
pos_or_panic!(20600.0),
ExpirationDate::Days(pos_or_panic!(0.52)),
iv,
Positive::ONE,
pos_or_panic!(21049.88),
dec!(0.0),
OptionStyle::Call,
pos_or_panic!(0.05),
None,
);
let delta = option.delta().unwrap();
let gamma = option.gamma().unwrap();
let vega = option.vega().unwrap();
let theta = option.theta().unwrap();
let rho = option.rho().unwrap();
let vanna = option.vanna().unwrap();
let vomma = option.vomma().unwrap();
let veta = option.veta().unwrap();
let charm = option.charm().unwrap();
let color = option.color().unwrap();
assert_decimal_eq!(delta, dec!(-0.946), dec!(0.002));
assert_decimal_eq!(gamma, dec!(0.00038), dec!(0.002));
assert_decimal_eq!(vega, dec!(0.866), dec!(0.002));
assert_decimal_eq!(theta, dec!(-26.990), dec!(0.002));
assert_decimal_eq!(rho, dec!(0.277), dec!(0.002));
assert_decimal_eq!(vanna, dec!(-0.4879786), dec!(0.0000001));
assert_decimal_eq!(vomma, dec!(6.2450679681), dec!(0.00000001));
assert_decimal_eq!(veta, dec!(0.0433019), dec!(0.0000001));
assert_decimal_eq!(charm, dec!(0.1686671), dec!(0.0000001));
assert_decimal_eq!(color, dec!(0.0005877), dec!(0.0000001));
let market_price = option.calculate_price_black_scholes().unwrap().abs();
assert_decimal_eq!(market_price, dec!(454.917), dec!(0.002));
option.implied_volatility = pos_or_panic!(0.4); let iv_result = implied_volatility(
Positive::new_decimal(market_price).unwrap(),
&mut option,
10,
);
match iv_result {
Ok(iv_aprox) => {
assert_pos_relative_eq!(iv_aprox, iv, pos_or_panic!(0.001));
}
Err(_) => {
error!("Non-convergence is acceptable for zero DTE options");
}
}
option.implied_volatility = iv;
option.option_style = OptionStyle::Put;
let delta = option.delta().unwrap();
let gamma = option.gamma().unwrap();
let vega = option.vega().unwrap();
let theta = option.theta().unwrap();
let rho = option.rho().unwrap();
let vanna = option.vanna().unwrap();
let vomma = option.vomma().unwrap();
let veta = option.veta().unwrap();
let charm = option.charm().unwrap();
let color = option.color().unwrap();
assert_decimal_eq!(delta, dec!(0.053), dec!(0.001));
assert_decimal_eq!(gamma, dec!(0.0), dec!(0.001));
assert_decimal_eq!(vega, dec!(0.866), dec!(0.001));
assert_decimal_eq!(theta, dec!(-29.874), dec!(0.001));
assert_decimal_eq!(rho, dec!(-0.016), dec!(0.001));
assert_decimal_eq!(vanna, dec!(-0.4879786), dec!(0.0000001));
assert_decimal_eq!(vomma, dec!(6.2450679681), dec!(0.00000001));
assert_decimal_eq!(veta, dec!(0.0433019), dec!(0.0000001));
assert_decimal_eq!(charm, dec!(0.1685301), dec!(0.0000001));
assert_decimal_eq!(color, dec!(0.0005877), dec!(0.0000001));
let market_price = option.calculate_price_black_scholes().unwrap().abs();
assert_decimal_eq!(market_price, dec!(6.537), dec!(0.002));
option.implied_volatility = pos_or_panic!(0.4); let iv_result = implied_volatility(
Positive::new_decimal(market_price).unwrap(),
&mut option,
10,
);
match iv_result {
Ok(iv_aprox) => {
assert_pos_relative_eq!(iv_aprox, iv, pos_or_panic!(0.001));
}
Err(_) => {
error!("Non-convergence is acceptable for zero DTE options");
}
}
}
#[test]
fn test_implied_volatility_zero_dte_real_put() {
let iv = pos_or_panic!(0.356831);
let mut option = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
pos_or_panic!(23325.0),
ExpirationDate::Days(pos_or_panic!(0.29)),
iv,
Positive::ONE,
pos_or_panic!(24118.5),
dec!(0.0),
OptionStyle::Put,
Positive::ZERO,
None,
);
let delta = option.delta().unwrap();
let gamma = option.gamma().unwrap();
let vega = option.vega().unwrap();
let theta = option.theta().unwrap();
let rho = option.rho().unwrap();
let vanna = option.vanna().unwrap();
let vomma = option.vomma().unwrap();
let veta = option.veta().unwrap();
let charm = option.charm().unwrap();
let color = option.color().unwrap();
assert_decimal_eq!(delta, dec!(-0.00043), dec!(0.002));
assert_decimal_eq!(gamma, dec!(0.0000064), dec!(0.00001));
assert_decimal_eq!(vega, dec!(0.0105), dec!(0.002));
assert_decimal_eq!(theta, dec!(-0.64997), dec!(0.002));
assert_decimal_eq!(rho, dec!(-0.000083), dec!(0.002));
assert_decimal_eq!(vanna, dec!(-0.0144632), dec!(0.0000001));
assert_decimal_eq!(vomma, dec!(0.3275301270), dec!(0.0000000001));
assert_decimal_eq!(veta, dec!(0.0031824), dec!(0.0000001));
assert_decimal_eq!(charm, dec!(0.0088981), dec!(0.0000001));
assert_decimal_eq!(color, dec!(0.0001111), dec!(0.0000001));
let market_price = option.calculate_price_black_scholes().unwrap();
assert_decimal_eq!(market_price, dec!(0.027), dec!(0.002));
option.implied_volatility = pos_or_panic!(0.4); let iv_result = implied_volatility(pos_or_panic!(1.5), &mut option, 10);
match iv_result {
Ok(iv_aprox) => {
assert_pos_relative_eq!(iv_aprox, pos_or_panic!(0.528), pos_or_panic!(0.001));
}
Err(_) => {
error!("Non-convergence is acceptable for zero DTE options");
}
}
}
#[test]
fn test_implied_volatility_zero_dte_real_call() {
let iv = pos_or_panic!(0.356831);
let mut option = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
pos_or_panic!(23325.0),
ExpirationDate::Days(pos_or_panic!(0.32)),
iv,
Positive::ONE,
pos_or_panic!(24103.00),
dec!(0.0),
OptionStyle::Call,
Positive::ZERO,
None,
);
let delta = option.delta().unwrap();
let gamma = option.gamma().unwrap();
let vega = option.vega().unwrap();
let theta = option.theta().unwrap();
let rho = option.rho().unwrap();
let vanna = option.vanna().unwrap();
let vomma = option.vomma().unwrap();
let veta = option.veta().unwrap();
let charm = option.charm().unwrap();
let color = option.color().unwrap();
assert_decimal_eq!(delta, dec!(0.999), dec!(0.001));
assert_decimal_eq!(gamma, dec!(0.00001), dec!(0.00001));
assert_decimal_eq!(vega, dec!(0.02255), dec!(0.002));
assert_decimal_eq!(theta, dec!(-1.25733), dec!(0.002));
assert_decimal_eq!(rho, dec!(0.204), dec!(0.002));
assert_decimal_eq!(vanna, dec!(-0.0274529), dec!(0.0000001));
assert_decimal_eq!(vomma, dec!(0.6094669204), dec!(0.0000000001));
assert_decimal_eq!(veta, dec!(0.0054321), dec!(0.0000001));
assert_decimal_eq!(charm, dec!(0.0153063), dec!(0.0000001));
assert_decimal_eq!(color, dec!(0.0001675), dec!(0.0000001));
let market_price = option.calculate_price_black_scholes().unwrap();
assert_decimal_eq!(market_price, dec!(778.065), dec!(0.002));
option.implied_volatility = pos_or_panic!(0.4); let iv_result = implied_volatility(
Positive::new_decimal(market_price).unwrap(),
&mut option,
10,
);
match iv_result {
Ok(iv_aprox) => {
assert_pos_relative_eq!(iv_aprox, iv, pos_or_panic!(0.001));
}
Err(_) => {
error!("Non-convergence is acceptable for zero DTE options");
}
}
}
}
#[cfg(test)]
mod tests_garch_volatility {
use super::*;
use crate::assert_decimal_eq;
use positive::assert_pos_relative_eq;
use rust_decimal_macros::dec;
#[test]
fn test_garch_single_return() {
let returns = vec![dec!(0.02)];
let omega = dec!(0.1);
let alpha = dec!(0.2);
let beta = dec!(0.7);
let result = garch_volatility(&returns, omega, alpha, beta).unwrap();
assert_eq!(result.len(), 1);
assert_pos_relative_eq!(result[0], pos_or_panic!(0.02), pos_or_panic!(1e-10));
}
#[test]
fn test_garch_constant_returns() {
let returns = vec![dec!(0.02), dec!(0.02), dec!(0.02), dec!(0.02)];
let omega = dec!(0.1);
let alpha = dec!(0.2);
let beta = dec!(0.7);
let result = garch_volatility(&returns, omega, alpha, beta).unwrap();
assert_eq!(result.len(), 4);
assert_pos_relative_eq!(result[0], pos_or_panic!(0.02), pos_or_panic!(1e-10));
let last_two_diff = (result[3].to_dec() - result[2].to_dec()).abs();
assert_decimal_eq!(last_two_diff, dec!(0.0555), dec!(0.001));
}
#[test]
fn test_garch_varying_returns() {
let returns = vec![dec!(0.01), dec!(-0.02), dec!(0.03), dec!(-0.01)];
let omega = dec!(0.1);
let alpha = dec!(0.2);
let beta = dec!(0.7);
let result = garch_volatility(&returns, omega, alpha, beta).unwrap();
assert_eq!(result.len(), 4);
assert!(result.iter().all(|&v| v > Positive::ZERO));
assert!(result[2] > result[1]); }
#[test]
fn test_garch_zero_initial_return() {
let returns = vec![dec!(0.0), dec!(0.02), dec!(0.03)];
let omega = dec!(0.1);
let alpha = dec!(0.2);
let beta = dec!(0.7);
let result = garch_volatility(&returns, omega, alpha, beta).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0], Positive::ZERO);
}
#[test]
fn test_garch_parameter_sum_one() {
let returns = vec![dec!(0.01), dec!(0.02), dec!(0.03)];
let omega = dec!(0.05);
let alpha = dec!(0.15);
let beta = dec!(0.8);
let result = garch_volatility(&returns, omega, alpha, beta).unwrap();
assert!(result.iter().all(|&v| v < Positive::ONE));
}
#[test]
fn test_garch_extreme_returns() {
let returns = vec![dec!(0.01), dec!(0.2), dec!(-0.2), dec!(0.01)]; let omega = dec!(0.1);
let alpha = dec!(0.2);
let beta = dec!(0.7);
let result = garch_volatility(&returns, omega, alpha, beta).unwrap();
assert!(result[2] > result[1]);
assert!(result[2] > result[0] * Positive::TWO); }
#[test]
fn test_garch_parameters_validation() {
let returns = vec![dec!(0.01)];
let result_negative = garch_volatility(&returns, dec!(-0.1), dec!(0.2), dec!(0.7));
assert!(result_negative.is_ok());
let result_sum = garch_volatility(&returns, dec!(0.1), dec!(0.5), dec!(0.6));
assert!(result_sum.is_ok()); }
}
#[cfg(test)]
mod tests_heston_volatility {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_heston_basic_properties() {
let kappa = dec!(2.0); let theta = dec!(0.04); let xi = dec!(0.3); let v0 = dec!(0.04); let dt = dec!(0.01); let steps = 100;
let result = simulate_heston_volatility(kappa, theta, xi, v0, dt, steps).unwrap();
assert_eq!(result.len(), steps);
assert_eq!(result[0], Positive::new_decimal(v0).unwrap().sqrt());
assert!(result.iter().all(|&x| x >= Positive::ZERO));
}
#[test]
fn test_heston_zero_volatility_of_volatility() {
let kappa = dec!(2.0);
let theta = dec!(0.04);
let xi = dec!(0.0); let v0 = dec!(0.04);
let dt = dec!(0.01);
let steps = 50;
let result = simulate_heston_volatility(kappa, theta, xi, v0, dt, steps).unwrap();
for i in 1..steps {
match v0 < theta {
true => assert!(result[i] >= result[i - 1]),
false => assert!(result[i] <= result[i - 1]),
}
}
}
#[test]
fn test_heston_high_mean_reversion() {
let kappa = dec!(10.0); let theta = dec!(0.04);
let xi = dec!(0.1);
let v0 = dec!(0.08); let dt = dec!(0.01);
let steps = 200;
let result = simulate_heston_volatility(kappa, theta, xi, v0, dt, steps).unwrap();
let theta_vol = Positive::new_decimal(theta).unwrap().sqrt();
let initial_dist = (result[0].to_dec() - theta_vol.to_dec()).abs();
let final_dist = (result[steps - 1].to_dec() - theta_vol.to_dec()).abs();
assert!(final_dist < initial_dist);
}
#[test]
fn test_heston_high_volatility() {
let kappa = dec!(2.0);
let theta = dec!(0.04);
let xi = dec!(1.0); let v0 = dec!(0.04);
let dt = dec!(0.01);
let steps = 100;
let result = simulate_heston_volatility(kappa, theta, xi, v0, dt, steps).unwrap();
let variations: Vec<_> = result
.windows(2)
.map(|w| (w[1].to_dec() - w[0].to_dec()).abs())
.collect();
assert!(variations.iter().any(|&x| x > dec!(0.001)));
}
#[test]
fn test_heston_zero_initial_variance() {
let kappa = dec!(2.0);
let theta = dec!(0.04);
let xi = dec!(0.3);
let v0 = dec!(0.0); let dt = dec!(0.01);
let steps = 100;
let result = simulate_heston_volatility(kappa, theta, xi, v0, dt, steps).unwrap();
assert_eq!(result[0], Positive::ZERO);
assert!(result.iter().skip(1).any(|&x| x > Positive::ZERO));
}
#[test]
fn test_heston_numerical_stability() {
let kappa = dec!(2.0);
let theta = dec!(0.04);
let xi = dec!(0.3);
let v0 = dec!(0.04);
let dt = dec!(0.001); let steps = 1000;
let result = simulate_heston_volatility(kappa, theta, xi, v0, dt, steps).unwrap();
assert!(result.iter().all(|&x| x < Positive::HUNDRED));
assert!(result.iter().all(|&x| x >= Positive::ZERO));
}
#[test]
fn test_heston_large_time_steps() {
let kappa = dec!(2.0);
let theta = dec!(0.04);
let xi = dec!(0.3);
let v0 = dec!(0.04);
let dt = dec!(0.1); let steps = 10;
let result = simulate_heston_volatility(kappa, theta, xi, v0, dt, steps).unwrap();
assert!(result.iter().all(|&x| x >= Positive::ZERO));
}
#[test]
fn test_heston_extreme_parameters() {
let kappa = dec!(20.0); let theta = dec!(1.0); let xi = dec!(2.0); let v0 = dec!(0.5);
let dt = dec!(0.01);
let steps = 100;
let result = simulate_heston_volatility(kappa, theta, xi, v0, dt, steps).unwrap();
assert!(result.iter().all(|&x| x >= Positive::ZERO));
}
#[test]
fn test_heston_min_steps() {
let kappa = dec!(2.0);
let theta = dec!(0.04);
let xi = dec!(0.3);
let v0 = dec!(0.04);
let dt = dec!(0.01);
let steps = 1;
let result = simulate_heston_volatility(kappa, theta, xi, v0, dt, steps).unwrap();
assert_eq!(result.len(), steps);
assert_eq!(result[0], Positive::new_decimal(v0).unwrap().sqrt());
}
}
#[cfg(test)]
mod tests_uncertain_volatility_bounds {
use super::*;
use crate::model::types::{OptionStyle, OptionType, Side};
use positive::assert_pos_relative_eq;
use crate::ExpirationDate;
use rust_decimal_macros::dec;
fn create_test_option(style: OptionStyle, side: Side, strike: Positive) -> Options {
Options::new(
OptionType::European,
side,
"TEST".to_string(),
strike,
ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2), Positive::ONE, Positive::HUNDRED, dec!(0.05), style,
Positive::ZERO, None, )
}
#[test]
fn test_bounds_basic_call() {
let option = create_test_option(OptionStyle::Call, Side::Long, Positive::HUNDRED);
let (lower, upper) =
uncertain_volatility_bounds(&option, pos_or_panic!(0.1), pos_or_panic!(0.3)).unwrap();
assert!(lower < upper);
assert!(lower > Positive::ZERO);
assert!(upper > Positive::ZERO);
}
#[test]
fn test_bounds_basic_put() {
let option = create_test_option(OptionStyle::Put, Side::Long, Positive::HUNDRED);
let (lower, upper) =
uncertain_volatility_bounds(&option, pos_or_panic!(0.1), pos_or_panic!(0.3)).unwrap();
assert!(lower < upper);
assert!(lower > Positive::ZERO);
assert!(upper > Positive::ZERO);
}
#[test]
fn test_bounds_same_volatility() {
let option = create_test_option(OptionStyle::Call, Side::Long, Positive::HUNDRED);
let vol = pos_or_panic!(0.2);
let (lower, upper) = uncertain_volatility_bounds(&option, vol, vol).unwrap();
assert_pos_relative_eq!(lower, upper, pos_or_panic!(1e-10));
}
#[test]
fn test_bounds_itm_call() {
let itm_option = create_test_option(OptionStyle::Call, Side::Long, pos_or_panic!(90.0));
let (lower, upper) =
uncertain_volatility_bounds(&itm_option, pos_or_panic!(0.1), pos_or_panic!(0.3))
.unwrap();
let intrinsic = pos_or_panic!(10.0); assert!(lower > intrinsic);
assert!(upper > intrinsic);
}
#[test]
fn test_bounds_otm_call() {
let otm_option = create_test_option(OptionStyle::Call, Side::Long, pos_or_panic!(110.0));
let (lower, upper) =
uncertain_volatility_bounds(&otm_option, pos_or_panic!(0.1), pos_or_panic!(0.3))
.unwrap();
assert!(lower > Positive::ZERO);
assert!(upper < pos_or_panic!(110.0));
}
#[test]
fn test_bounds_otm_put() {
let otm_option = create_test_option(OptionStyle::Put, Side::Long, pos_or_panic!(90.0));
let (lower, upper) =
uncertain_volatility_bounds(&otm_option, pos_or_panic!(0.1), pos_or_panic!(0.3))
.unwrap();
assert!(lower > Positive::ZERO);
assert!(upper < pos_or_panic!(90.0));
}
#[test]
fn test_bounds_extreme_volatilities() {
let option = create_test_option(OptionStyle::Call, Side::Long, Positive::HUNDRED);
let (lower, upper) =
uncertain_volatility_bounds(&option, pos_or_panic!(0.01), Positive::ONE).unwrap();
assert!(lower < upper);
assert!(lower > Positive::ZERO);
assert!(upper < option.underlying_price);
}
}
#[cfg(test)]
mod tests_adjust_volatility {
use super::*;
use positive::assert_pos_relative_eq;
#[test]
fn test_same_timeframe() {
let vol = pos_or_panic!(0.2);
let result = adjust_volatility(vol, TimeFrame::Day, TimeFrame::Day).unwrap();
assert_eq!(result, vol);
}
#[test]
fn test_same_periods_different_timeframe() {
let vol = pos_or_panic!(0.2);
let result =
adjust_volatility(vol, TimeFrame::Custom(pos_or_panic!(252.0)), TimeFrame::Day)
.unwrap();
assert_eq!(result, vol);
}
#[test]
fn test_zero_periods() {
let vol = pos_or_panic!(0.2);
let result = adjust_volatility(vol, TimeFrame::Day, TimeFrame::Custom(Positive::ZERO));
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("zero periods per year"));
} else {
panic!("Expected error for zero periods");
}
}
#[test]
fn test_daily_to_minute() {
let daily_vol = pos_or_panic!(0.2);
let result = adjust_volatility(daily_vol, TimeFrame::Day, TimeFrame::Minute).unwrap();
assert_pos_relative_eq!(result, pos_or_panic!(0.0101273936), pos_or_panic!(0.0001));
}
#[test]
fn test_minute_to_daily() {
let minute_vol = pos_or_panic!(0.01012);
let result = adjust_volatility(minute_vol, TimeFrame::Minute, TimeFrame::Day).unwrap();
assert_pos_relative_eq!(result, pos_or_panic!(0.199853), pos_or_panic!(0.0001));
}
#[test]
fn test_daily_to_hourly() {
let daily_vol = pos_or_panic!(0.2);
let result = adjust_volatility(daily_vol, TimeFrame::Day, TimeFrame::Hour).unwrap();
assert_pos_relative_eq!(result, pos_or_panic!(0.07844), pos_or_panic!(0.0001));
}
#[test]
fn test_monthly_to_daily() {
let monthly_vol = pos_or_panic!(0.3);
let result = adjust_volatility(monthly_vol, TimeFrame::Month, TimeFrame::Day).unwrap();
assert_pos_relative_eq!(result, pos_or_panic!(0.0654653), pos_or_panic!(0.0001));
}
#[test]
fn test_custom_timeframe() {
let vol = pos_or_panic!(0.25);
let custom_periods = Positive::HUNDRED;
let result =
adjust_volatility(vol, TimeFrame::Custom(custom_periods), TimeFrame::Day).unwrap();
assert_pos_relative_eq!(result, pos_or_panic!(0.157485197), pos_or_panic!(0.0001));
}
#[test]
fn test_yearly_to_daily() {
let yearly_vol = pos_or_panic!(0.4);
let result = adjust_volatility(yearly_vol, TimeFrame::Year, TimeFrame::Day).unwrap();
assert_pos_relative_eq!(result, pos_or_panic!(0.025197631), pos_or_panic!(0.0001));
}
#[test]
fn test_zero_volatility() {
let result = adjust_volatility(Positive::ZERO, TimeFrame::Day, TimeFrame::Minute).unwrap();
assert_eq!(result, Positive::ZERO);
}
#[test]
fn test_very_small_volatility() {
let small_vol = pos_or_panic!(0.0001);
let result = adjust_volatility(small_vol, TimeFrame::Day, TimeFrame::Hour).unwrap();
assert!(result > Positive::ZERO);
assert!(result < small_vol);
}
#[test]
fn test_very_large_volatility() {
let large_vol = pos_or_panic!(10.0);
let result = adjust_volatility(large_vol, TimeFrame::Day, TimeFrame::Minute).unwrap();
assert!(result > Positive::ZERO);
assert!(result < large_vol);
}
}
#[cfg(test)]
mod tests_generate_ou_process {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_process_length() {
let steps = 500;
let process = generate_ou_process(
Positive::ONE,
pos_or_panic!(1.5),
pos_or_panic!(0.1),
pos_or_panic!(0.2),
pos_or_panic!(0.01),
steps,
);
assert_eq!(process.len(), steps);
}
#[test]
fn test_all_values_positive() {
let process = generate_ou_process(
Positive::ONE,
pos_or_panic!(1.5),
pos_or_panic!(0.2),
pos_or_panic!(0.3),
pos_or_panic!(0.01),
1000,
);
for value in process {
assert!(
value >= Positive::ZERO,
"Found non-positive value: {value:?}"
);
}
}
#[test]
fn test_mean_reversion_tendency() {
let process = generate_ou_process(
pos_or_panic!(0.1),
Positive::ONE,
Positive::ONE, pos_or_panic!(0.01), pos_or_panic!(0.01),
1000,
);
let last = process.last().unwrap().to_dec();
let diff = (last - dec!(1.0)).abs();
assert!(diff < dec!(0.1), "Final value too far from mean: {last}");
}
}
#[cfg(test)]
mod tests_non_finite_guards {
use super::*;
#[test]
fn constant_volatility_nan_return_surfaces_decimal_error() {
let returns = [Decimal::ZERO, Decimal::ZERO];
let v = constant_volatility(&returns).expect("finite inputs");
assert_eq!(v, Positive::ZERO);
}
#[test]
fn heston_happy_path_does_not_trip_non_finite() {
use rust_decimal_macros::dec;
let res = simulate_heston_volatility(
dec!(1.0), dec!(0.04), dec!(0.1), dec!(0.04), dec!(0.01), 10, );
assert!(res.is_ok(), "finite inputs unexpectedly failed: {res:?}");
}
}