use crate::error::PricingError;
use crate::kernels::{big_n, d2, discount_factor};
use crate::pricing::binomial_model::BinomialPricingParams;
use crate::pricing::constants::{CLAMP_MAX, CLAMP_MIN};
use optionstratlib_core::error::DecimalError;
use optionstratlib_core::model::Options;
use optionstratlib_core::model::Positive;
use optionstratlib_core::model::decimal::{
d_add, d_div, d_exp, d_ln, d_mul, d_powd, d_sqrt, d_sub, decimal_to_f64,
};
use optionstratlib_core::model::payoff::{Payoff, PayoffInfo};
use optionstratlib_core::model::types::Side;
use optionstratlib_core::utils::random_decimal;
use rand::Rng;
#[cfg(test)]
use optionstratlib_core::model::decimal::finite_decimal;
#[cfg(test)]
use rand_distr::{Distribution, Normal};
use rust_decimal::{Decimal, MathematicalOps};
use rust_decimal_macros::dec;
pub fn simulate_returns<R: Rng + ?Sized>(
mean: Decimal,
std_dev: Positive,
length: usize,
time_step: Decimal,
rng: &mut R,
) -> Result<Vec<Decimal>, DecimalError> {
fn generate_normal_pair<R: Rng + ?Sized>(
rng: &mut R,
) -> Result<(Decimal, Decimal), DecimalError> {
let u1 = random_decimal(rng)?;
let u2 = random_decimal(rng)?;
let r = d_sqrt(
d_mul(
-Decimal::TWO,
d_ln(u1, "pricing::utils::box_muller::log_u1")?,
"pricing::utils::box_muller::radius_squared",
)?,
"pricing::utils::box_muller::radius",
)
.map_err(|_| {
DecimalError::arithmetic_error("sqrt", "non-finite operand in Box-Muller r")
})?;
let theta = d_mul(
d_mul(
Decimal::TWO,
Decimal::PI,
"pricing::utils::box_muller::two_pi",
)?,
u2,
"pricing::utils::box_muller::theta",
)?;
let trig = || {
DecimalError::arithmetic_error(
"pricing::utils::box_muller",
"trigonometric series overflowed",
)
};
let x1 = d_mul(
r,
theta.checked_cos().ok_or_else(trig)?,
"pricing::utils::box_muller::x1",
)?;
let x2 = d_mul(
r,
theta.checked_sin().ok_or_else(trig)?,
"pricing::utils::box_muller::x2",
)?;
Ok((x1, x2))
}
if std_dev < Decimal::ZERO {
return Err(DecimalError::InvalidValue {
value: decimal_to_f64(std_dev.to_dec())?,
reason: "Standard deviation cannot be negative".to_string(),
});
}
let adjusted_mean = d_mul(mean, time_step, "pricing::utils::simulate::adjusted_mean")?;
let adjusted_std = d_mul(
std_dev.to_dec(),
d_sqrt(time_step, "pricing::utils::simulate::sqrt_time_step").map_err(|_| {
DecimalError::arithmetic_error("sqrt", "invalid (negative or non-finite) time_step")
})?,
"pricing::utils::simulate::adjusted_std",
)?;
let mut returns: Vec<Decimal> = Vec::new();
returns.try_reserve_exact(length).map_err(|_| {
DecimalError::arithmetic_error(
"pricing::utils::simulate_returns",
&format!("cannot allocate {length} returns"),
)
})?;
if adjusted_std == Decimal::ZERO {
returns.resize(length, adjusted_mean);
return Ok(returns);
}
for _ in 0..length.div_ceil(2) {
let (n1, n2) = generate_normal_pair(rng)?;
let r1 = d_add(
d_mul(n1, adjusted_std, "pricing::utils::simulate::scale_1")?,
adjusted_mean,
"pricing::utils::simulate::shift_1",
)?;
returns.push(r1);
if returns.len() < length {
let r2 = d_add(
d_mul(n2, adjusted_std, "pricing::utils::simulate::scale_2")?,
adjusted_mean,
"pricing::utils::simulate::shift_2",
)?;
returns.push(r2);
}
}
Ok(returns)
}
#[inline]
pub(crate) fn calculate_up_factor(
volatility: Positive,
dt: Decimal,
) -> Result<Decimal, DecimalError> {
let sqrt_dt = d_sqrt(dt, "pricing::utils::up_factor::sqrt_dt")
.map_err(|_| DecimalError::arithmetic_error("sqrt", "non-finite dt in up factor"))?;
d_exp(
d_mul(
sqrt_dt,
volatility.to_dec(),
"pricing::binomial::up_factor::exponent",
)?,
"pricing::binomial::up_factor",
)
}
#[inline]
pub(crate) fn calculate_down_factor(
volatility: Positive,
dt: Decimal,
) -> Result<Decimal, DecimalError> {
let sqrt_dt = d_sqrt(dt, "pricing::utils::down_factor::sqrt_dt")
.map_err(|_| DecimalError::arithmetic_error("sqrt", "non-finite dt in down factor"))?;
d_exp(
d_mul(
d_mul(
dec!(-1.0),
sqrt_dt,
"pricing::binomial::down_factor::sqrt_dt",
)?,
volatility.to_dec(),
"pricing::binomial::down_factor::exponent",
)?,
"pricing::binomial::down_factor",
)
}
#[inline]
pub(crate) fn calculate_probability(
int_rate: Decimal,
dt: Decimal,
down_factor: Decimal,
up_factor: Decimal,
) -> Result<Decimal, DecimalError> {
let spread = d_sub(
up_factor,
down_factor,
"pricing::binomial::probability::spread",
)?;
if spread.is_zero() {
return Err(DecimalError::arithmetic_error(
"pricing::binomial::probability",
"up and down factors coincide, risk-neutral probability is undefined",
));
}
let growth = d_exp(
d_mul(int_rate, dt, "pricing::binomial::probability::rate_dt")?,
"pricing::binomial::probability::growth",
)?;
Ok(d_div(
d_sub(
growth,
down_factor,
"pricing::binomial::probability::numerator",
)?,
spread,
"pricing::binomial::probability",
)?
.clamp(CLAMP_MIN, CLAMP_MAX))
}
#[inline]
pub(crate) fn calculate_discount_factor(
int_rate: Decimal,
dt: Decimal,
) -> Result<Decimal, DecimalError> {
discount_factor(
int_rate,
dt,
"pricing::binomial::discount_factor::exponent",
"pricing::binomial::discount_factor",
)
}
#[inline]
pub(crate) fn option_node_value_wrapper(
probability: Decimal,
next: &mut [Vec<Decimal>],
node: usize,
discount_factor: Decimal,
) -> Result<Decimal, DecimalError> {
let next_step = next
.first()
.ok_or_else(|| DecimalError::arithmetic_error("pricing::binomial::node", "missing step"))?;
let price_up = *next_step.get(node).ok_or_else(|| {
DecimalError::arithmetic_error("pricing::binomial::node", "missing up node")
})?;
let price_down = *node
.checked_add(1)
.and_then(|down| next_step.get(down))
.ok_or_else(|| {
DecimalError::arithmetic_error("pricing::binomial::node", "missing down node")
})?;
option_node_value(probability, price_up, price_down, discount_factor)
}
#[inline]
pub(crate) fn option_node_value(
probability: Decimal,
price_up: Decimal,
price_down: Decimal,
discount_factor: Decimal,
) -> Result<Decimal, DecimalError> {
let up_branch = d_mul(probability, price_up, "pricing::binomial::node::up_branch")?;
let down_branch = d_mul(
d_sub(
Decimal::ONE,
probability,
"pricing::binomial::node::down_weight",
)?,
price_down,
"pricing::binomial::node::down_branch",
)?;
let expected = d_add(up_branch, down_branch, "pricing::binomial::node::expected")?;
d_mul(
expected,
discount_factor,
"pricing::binomial::node::discounted",
)
}
pub(crate) struct LatticePowers {
up: Vec<Option<Decimal>>,
down: Vec<Option<Decimal>>,
}
impl LatticePowers {
pub(crate) fn new(u: Decimal, d: Decimal, no_steps: usize) -> Result<Self, PricingError> {
let too_large = || {
PricingError::invalid_parameter(
"no_steps",
Decimal::from(no_steps),
"a binomial lattice of this many steps cannot be allocated",
)
};
let width = no_steps.checked_add(1).ok_or_else(too_large)?;
let mut up = Vec::new();
up.try_reserve_exact(width).map_err(|_| too_large())?;
let mut down = Vec::new();
down.try_reserve_exact(width).map_err(|_| too_large())?;
for k in 0..width {
let exponent = Decimal::from(k as u64);
up.push(d_powd(u, exponent, "pricing::binomial::powers::up").ok());
down.push(d_powd(d, exponent, "pricing::binomial::powers::down").ok());
}
Ok(Self { up, down })
}
pub(crate) fn up(
&self,
u: Decimal,
k: usize,
op: &'static str,
) -> Result<Decimal, PricingError> {
match self.up.get(k).copied().flatten() {
Some(power) => Ok(power),
None => Ok(d_powd(u, Decimal::from(k as u64), op)?),
}
}
pub(crate) fn down(
&self,
d: Decimal,
k: usize,
op: &'static str,
) -> Result<Decimal, PricingError> {
match self.down.get(k).copied().flatten() {
Some(power) => Ok(power),
None => Ok(d_powd(d, Decimal::from(k as u64), op)?),
}
}
}
pub(crate) fn calculate_option_price(
params: BinomialPricingParams,
u: Decimal,
d: Decimal,
i: usize,
powers: &LatticePowers,
) -> Result<Decimal, PricingError> {
let down_steps = params.no_steps.get().checked_sub(i).ok_or_else(|| {
PricingError::method_error(
"calculate_option_price",
"step index exceeds the number of lattice steps",
)
})?;
let up_power = powers.up(u, i, "pricing::binomial::option_price::up_power")?;
let down_power = powers.down(d, down_steps, "pricing::binomial::option_price::down_power")?;
let spot = d_mul(
d_mul(
params.asset.to_dec(),
up_power,
"pricing::binomial::option_price::spot_up",
)?,
down_power,
"pricing::binomial::option_price::spot",
)?;
let info = PayoffInfo {
spot: Positive::new_decimal(spot)?,
strike: params.strike,
style: *params.option_style,
side: *params.side,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: None,
};
let payoff = params.option_type.payoff(&info)?;
Ok(payoff)
}
pub(crate) fn calculate_discounted_payoff(
params: BinomialPricingParams,
) -> Result<Decimal, PricingError> {
let growth = d_exp(
d_mul(
params.int_rate,
params.expiry.to_dec(),
"pricing::binomial::discounted_payoff::growth_exponent",
)?,
"pricing::binomial::discounted_payoff::growth",
)?;
let forward = d_mul(
params.asset.to_dec(),
growth,
"pricing::binomial::discounted_payoff::forward",
)?;
let info = PayoffInfo {
spot: Positive::new_decimal(forward)?,
strike: params.strike,
style: *params.option_style,
side: *params.side,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: None,
};
let payoff = params.option_type.payoff(&info)?;
let discount = discount_factor(
params.int_rate,
params.expiry.to_dec(),
"pricing::binomial::discounted_payoff::discount_exponent",
"pricing::binomial::discounted_payoff::discount",
)?;
let discounted_payoff = d_mul(
discount,
payoff,
"pricing::binomial::discounted_payoff::discounted",
)?;
match params.side {
Side::Long => Ok(discounted_payoff),
Side::Short => Ok(-discounted_payoff),
}
}
#[cfg(test)]
pub(crate) fn wiener_increment<R: Rng + ?Sized>(
sqrt_dt: Decimal,
rng: &mut R,
) -> Result<Decimal, PricingError> {
let normal = Normal::new(0.0, 1.0)
.map_err(|e| DecimalError::arithmetic_error("Normal::new(0.0, 1.0)", &e.to_string()))?;
let sample_f64 = normal.sample(rng);
let sample = finite_decimal(sample_f64).ok_or_else(|| {
PricingError::non_finite("pricing::monte_carlo::wiener_increment::sample", sample_f64)
})?;
Ok(d_mul(
sample,
sqrt_dt,
"pricing::monte_carlo::wiener_increment::scaled",
)?)
}
pub(crate) fn wiener_sqrt_dt(dt: Decimal) -> Result<Decimal, DecimalError> {
d_sqrt(dt, "pricing::monte_carlo::wiener_increment::sqrt_dt")
.map_err(|_| DecimalError::arithmetic_error("sqrt", "non-finite dt in wiener_increment"))
}
pub fn probability_keep_under_strike(
option: Options,
strike: Option<Positive>,
) -> Result<Decimal, DecimalError> {
let strike_price = match strike {
Some(strike) => strike,
None => option.strike_price,
};
let years = option.expiration_date.get_years()?;
let d2_val = d2(
option.underlying_price,
strike_price,
d_sub(
option.risk_free_rate,
option.dividend_yield.to_dec(),
"pricing::utils::probability_keep_under_strike::carry",
)?, years,
option.implied_volatility,
)
.map_err(|e| DecimalError::arithmetic_error("d2", &e.to_string()))?;
big_n(-d2_val)
}
#[cfg(test)]
mod tests_simulate_returns {
use super::*;
use num_traits::FromPrimitive;
use optionstratlib_core::pos_or_panic;
use optionstratlib_core::utils::{DETERMINISTIC_RNG_DEFAULT_SEED, deterministic_rng};
use optionstratlib_core::assert_decimal_eq;
use optionstratlib_core::model::decimal::DecimalStats;
use rust_decimal_macros::dec;
#[test]
fn test_simulate_returns() {
let mean = dec!(0.05); let std_dev = pos_or_panic!(0.2); let length = 252; let time_step = Decimal::from_f64(1.0 / 252.0).unwrap();
let returns = simulate_returns(
mean,
std_dev,
length,
time_step,
&mut deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED),
)
.unwrap();
assert_eq!(returns.len(), length);
let simulated_mean = returns.clone().mean().unwrap();
let simulated_std_dev = returns.std_dev().unwrap();
assert_decimal_eq!(simulated_mean, mean * time_step, dec!(0.01));
assert_decimal_eq!(
simulated_std_dev,
std_dev * time_step.sqrt().unwrap(),
dec!(0.01)
);
}
}
#[cfg(test)]
mod tests_simulate_returns_bis {
use super::*;
const PINNED_RETURNS: [Decimal; 3] = [
dec!(-0.0069930797843514744440741021),
dec!(0.0115367336920983749405395342),
dec!(-0.0018475187016137067853641022),
];
use optionstratlib_core::pos_or_panic;
use optionstratlib_core::utils::{DETERMINISTIC_RNG_DEFAULT_SEED, deterministic_rng};
use num_traits::FromPrimitive;
use optionstratlib_core::assert_decimal_eq;
use optionstratlib_core::model::decimal::DecimalStats;
use rust_decimal_macros::dec;
#[test]
fn test_simulate_returns_length() {
let length = 1000;
let returns = simulate_returns(
dec!(0.05),
pos_or_panic!(0.2),
length,
Decimal::from_f64(1.0 / 252.0).unwrap(),
&mut deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED),
)
.unwrap();
assert_eq!(returns.len(), length);
}
#[test]
fn test_simulate_returns_zero_mean() {
let returns = simulate_returns(
dec!(0.0),
pos_or_panic!(0.2),
1000,
Decimal::from_f64(1.0 / 252.0).unwrap(),
&mut deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED),
)
.unwrap();
let mean = returns.mean().unwrap();
assert!(mean.abs() < dec!(0.01));
}
#[test]
fn test_simulate_returns_zero_volatility() {
let mean = dec!(0.05);
let time_step = Decimal::from_f64(1.0 / 252.0).unwrap();
let returns = simulate_returns(
mean,
Positive::ZERO,
100,
time_step,
&mut deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED),
)
.unwrap();
let expected = mean * time_step;
for r in returns {
assert_decimal_eq!(r, expected, dec!(1e-10));
}
}
#[test]
fn test_simulate_returns_single_value() {
let returns = simulate_returns(
dec!(0.05),
pos_or_panic!(0.2),
1,
Decimal::from_f64(1.0 / 252.0).unwrap(),
&mut deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED),
)
.unwrap();
assert_eq!(returns.len(), 1);
}
#[test]
fn test_simulate_returns_yearly_step() {
let returns = simulate_returns(
dec!(0.05),
pos_or_panic!(0.2),
100,
dec!(1.0),
&mut deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED),
)
.unwrap();
assert_eq!(returns.len(), 100);
for r in returns {
assert!(r > dec!(-1.0));
}
}
#[test]
#[should_panic]
fn test_simulate_returns_invalid_std_dev() {
assert!(
simulate_returns(
dec!(0.05),
pos_or_panic!(-0.2),
100,
Decimal::from_f64(1.0 / 252.0).unwrap(),
&mut deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED),
)
.is_err()
);
}
#[test]
fn test_simulate_returns_same_seed_identical_returns() {
let draw = |seed: u64| {
simulate_returns(
dec!(0.05),
pos_or_panic!(0.2),
9,
dec!(0.004),
&mut deterministic_rng(seed),
)
.unwrap()
};
assert_eq!(draw(5), draw(5));
assert_ne!(draw(5), draw(6));
}
#[test]
fn test_simulate_returns_seeded_regression_pinned() {
let returns = simulate_returns(
Decimal::ZERO,
pos_or_panic!(0.2),
3,
dec!(0.004),
&mut deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED),
)
.unwrap();
assert_eq!(returns, PINNED_RETURNS.to_vec());
}
#[test]
fn test_simulate_returns_seeded_moments_match_parameters() {
let mean = dec!(0.05);
let time_step = dec!(0.004);
let returns = simulate_returns(
mean,
pos_or_panic!(0.2),
20_000,
time_step,
&mut deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED),
)
.unwrap();
let sample_mean = returns.clone().mean().unwrap();
let sample_std = returns.std_dev().unwrap();
assert_decimal_eq!(sample_mean, mean * time_step, dec!(0.000447));
assert_decimal_eq!(
sample_std,
dec!(0.2) * time_step.sqrt().unwrap(),
dec!(0.000317)
);
}
}
#[cfg(test)]
mod tests_utils {
use super::*;
use optionstratlib_core::pos_or_panic;
use optionstratlib_core::assert_decimal_eq;
use rust_decimal_macros::dec;
const EPSILON: Decimal = dec!(1e-6);
#[test]
fn test_calculate_up_factor() {
let volatility = pos_or_panic!(0.09531018);
let dt = dec!(1.0);
let up_factor = calculate_up_factor(volatility, dt).unwrap();
let expected_up_factor = (volatility * dt.sqrt().unwrap()).exp();
assert!(
(up_factor - expected_up_factor).abs() < EPSILON,
"Expected {expected_up_factor}, got {up_factor}"
);
}
#[test]
fn test_calculate_up_factor_2() {
let volatility = pos_or_panic!(0.17);
let dt = dec!(1.0);
let up_factor = calculate_up_factor(volatility, dt).unwrap();
let expected_up_factor = dec!(1.1853048504885680);
assert_decimal_eq!(up_factor, expected_up_factor, EPSILON);
}
#[test]
fn test_calculate_down_factor() {
let volatility = pos_or_panic!(0.09531018);
let dt = dec!(1.0);
let down_factor = calculate_down_factor(volatility, dt).unwrap();
let expected_down_factor = (-dt.sqrt().unwrap() * volatility).exp();
assert!(
(down_factor - expected_down_factor).abs() < EPSILON,
"Expected {expected_down_factor}, got {down_factor}"
);
}
#[test]
fn test_calculate_down_factor_2() {
let volatility = pos_or_panic!(0.17);
let dt = dec!(1.0);
let up_factor = calculate_down_factor(volatility, dt).unwrap();
let expected_up_factor = dec!(0.843664817188432427);
assert_decimal_eq!(up_factor, expected_up_factor, EPSILON);
}
#[test]
fn test_calculate_probability() {
let int_rate = dec!(0.05);
let dt = Decimal::ONE;
let down_factor = dec!(0.909090909);
let up_factor = dec!(1.1);
let probability = calculate_probability(int_rate, dt, down_factor, up_factor).unwrap();
let expected_probability = (((int_rate * dt).exp() - down_factor)
/ (up_factor - down_factor))
.clamp(CLAMP_MIN, CLAMP_MAX);
assert!(
(probability - expected_probability).abs() < EPSILON,
"Expected {expected_probability}, got {probability}"
);
}
#[test]
fn test_calculate_probability_ii() {
let int_rate = dec!(0.05);
let dt = Decimal::ONE;
let down_factor = dec!(0.8);
let up_factor = dec!(1.2);
let probability = calculate_probability(int_rate, dt, down_factor, up_factor).unwrap();
assert_decimal_eq!(probability, dec!(0.62817774088541), EPSILON);
}
#[test]
fn test_calculate_discount_factor() {
let int_rate = dec!(0.05);
let dt = Decimal::ONE;
let discount_factor = calculate_discount_factor(int_rate, dt).unwrap();
let expected_discount_factor = (-int_rate * dt).exp();
assert!(
(discount_factor - expected_discount_factor).abs() < EPSILON,
"Expected {expected_discount_factor}, got {discount_factor}"
);
}
}
#[cfg(test)]
mod tests_wiener_increment_hoisted_sqrt {
use super::*;
use optionstratlib_core::utils::deterministic_rng;
use rust_decimal_macros::dec;
fn per_step_increment<R: Rng + ?Sized>(
dt: Decimal,
rng: &mut R,
) -> Result<Decimal, PricingError> {
let normal = Normal::new(0.0, 1.0)
.map_err(|e| DecimalError::arithmetic_error("Normal::new(0.0, 1.0)", &e.to_string()))?;
let sample_f64 = normal.sample(rng);
let sample = finite_decimal(sample_f64).ok_or_else(|| {
PricingError::non_finite("pricing::monte_carlo::wiener_increment::sample", sample_f64)
})?;
let sqrt_dt =
d_sqrt(dt, "pricing::monte_carlo::wiener_increment::sqrt_dt").map_err(|_| {
DecimalError::arithmetic_error("sqrt", "non-finite dt in wiener_increment")
})?;
Ok(d_mul(
sample,
sqrt_dt,
"pricing::monte_carlo::wiener_increment::scaled",
)?)
}
#[test]
fn test_wiener_increment_hoisted_sqrt_is_bit_identical() {
for dt in [
dec!(0.00396825396825),
dec!(0.0833333333333),
Decimal::ONE,
dec!(2.5),
] {
let mut hoisted_rng = deterministic_rng(17);
let mut per_step_rng = deterministic_rng(17);
let sqrt_dt = wiener_sqrt_dt(dt).unwrap();
for step in 0..5_000 {
let hoisted = wiener_increment(sqrt_dt, &mut hoisted_rng).unwrap();
let per_step = per_step_increment(dt, &mut per_step_rng).unwrap();
assert_eq!(hoisted, per_step, "dt {dt}, step {step}");
}
}
}
#[test]
fn test_wiener_sqrt_dt_rejects_a_negative_dt() {
assert!(wiener_sqrt_dt(dec!(-0.01)).is_err());
assert_eq!(wiener_sqrt_dt(dec!(0.25)).unwrap(), dec!(0.5));
}
}
#[cfg(test)]
mod tests_probability_keep_under_strike {
use super::*;
use optionstratlib_core::{model::Positive, pos_or_panic, spos};
use optionstratlib_core::assert_decimal_eq;
use optionstratlib_core::constants::DAYS_IN_A_YEAR;
use optionstratlib_core::model::ExpirationDate;
use optionstratlib_core::model::types::{OptionStyle, OptionType};
use rust_decimal_macros::dec;
use tracing::info;
#[test]
fn test_probability_keep_under_strike_with_given_strike() {
let option = Options {
option_type: OptionType::European,
side: Side::Long,
underlying_price: Positive::HUNDRED,
strike_price: Positive::HUNDRED,
risk_free_rate: Decimal::ZERO,
option_style: OptionStyle::Call,
dividend_yield: Positive::ZERO,
expiration_date: ExpirationDate::Days(DAYS_IN_A_YEAR),
implied_volatility: pos_or_panic!(0.001),
underlying_symbol: "".to_string(),
quantity: Positive::ONE,
contract_size: Positive::ONE,
exotic_params: None,
};
let strike = spos!(100.0);
let probability = probability_keep_under_strike(option, strike).unwrap();
info!("{:?} {}", strike, probability);
assert_decimal_eq!(probability, dec!(0.5), dec!(0.001));
}
#[test]
fn test_probability_keep_under_strike_with_default_strike() {
let option = Options {
option_type: OptionType::European,
side: Side::Long,
underlying_price: Positive::HUNDRED,
strike_price: pos_or_panic!(110.0),
risk_free_rate: dec!(0.05),
option_style: OptionStyle::Call,
dividend_yield: Positive::ZERO,
expiration_date: ExpirationDate::Days(DAYS_IN_A_YEAR),
implied_volatility: pos_or_panic!(0.2),
underlying_symbol: "".to_string(),
quantity: Positive::ZERO,
contract_size: Positive::ONE,
exotic_params: None,
};
let strike = None;
let probability = probability_keep_under_strike(option, strike).unwrap();
assert!(
probability > Decimal::ZERO && probability < Decimal::ONE,
"Probability should be between 0 and 1"
);
}
#[test]
fn test_probability_keep_under_strike_zero_volatility() {
let option = Options {
option_type: OptionType::European,
side: Side::Long,
underlying_price: Positive::HUNDRED,
strike_price: Positive::HUNDRED,
risk_free_rate: dec!(0.05),
option_style: OptionStyle::Call,
dividend_yield: Positive::ZERO,
expiration_date: ExpirationDate::Days(DAYS_IN_A_YEAR),
implied_volatility: Positive::ZERO,
underlying_symbol: "".to_string(),
quantity: Positive::ZERO,
contract_size: Positive::ONE,
exotic_params: None,
};
let strike = None;
assert!(
probability_keep_under_strike(option, strike).is_err(),
"zero volatility should produce a DecimalError, not a panic"
);
}
#[test]
fn test_probability_keep_under_strike_high_volatility() {
let option = Options {
option_type: OptionType::European,
side: Side::Long,
underlying_price: Positive::HUNDRED,
strike_price: pos_or_panic!(110.0),
risk_free_rate: dec!(0.05),
option_style: OptionStyle::Call,
dividend_yield: Positive::ZERO,
expiration_date: ExpirationDate::Days(DAYS_IN_A_YEAR),
implied_volatility: pos_or_panic!(5.0), underlying_symbol: "".to_string(),
quantity: Positive::ZERO,
contract_size: Positive::ONE,
exotic_params: None,
};
let strike = None;
let probability = probability_keep_under_strike(option, strike).unwrap();
assert!(
probability > Decimal::ZERO && probability < Decimal::ONE,
"Probability should still be valid even with high volatility"
);
}
#[test]
fn test_probability_keep_under_strike_expired_option() {
let option = Options {
option_type: OptionType::European,
side: Side::Long,
underlying_price: Positive::HUNDRED,
strike_price: pos_or_panic!(110.0),
risk_free_rate: dec!(0.05),
option_style: OptionStyle::Call,
dividend_yield: Positive::ZERO,
expiration_date: ExpirationDate::Days(Positive::ONE),
implied_volatility: pos_or_panic!(0.2),
underlying_symbol: "".to_string(),
quantity: Positive::ZERO,
contract_size: Positive::ONE,
exotic_params: None,
};
let strike = None;
let probability = probability_keep_under_strike(option, strike).unwrap();
assert_eq!(
probability,
Decimal::ONE,
"Expired option should have zero probability of being ITM"
);
}
}
#[cfg(test)]
mod tests_calculate_up_down_factor {
use super::*;
use optionstratlib_core::pos_or_panic;
use optionstratlib_core::assert_decimal_eq;
use optionstratlib_core::model::decimal::ONE_DAY;
use rust_decimal_macros::dec;
const EPSILON: Decimal = dec!(1e-6);
#[test]
fn test_factors_standard_case() {
let volatility = pos_or_panic!(0.2); let dt = ONE_DAY;
let up = calculate_up_factor(volatility, dt).unwrap();
let down = calculate_down_factor(volatility, dt).unwrap();
assert_decimal_eq!(up * down, dec!(1.0), EPSILON);
assert!(up > Decimal::ONE);
assert!(down < Decimal::ONE);
}
#[test]
fn test_factors_zero_volatility() {
let volatility = Positive::ZERO;
let dt = ONE_DAY;
let up = calculate_up_factor(volatility, dt).unwrap();
let down = calculate_down_factor(volatility, dt).unwrap();
assert_decimal_eq!(up, Decimal::ONE, dec!(1e-10));
assert_decimal_eq!(down, Decimal::ONE, dec!(1e-10));
}
#[test]
fn test_factors_zero_dt() {
let volatility = pos_or_panic!(0.2);
let dt = Decimal::ZERO;
let up = calculate_up_factor(volatility, dt).unwrap();
let down = calculate_down_factor(volatility, dt).unwrap();
assert_decimal_eq!(up, Decimal::ONE, EPSILON);
assert_decimal_eq!(down, Decimal::ONE, EPSILON);
}
#[test]
fn test_factors_high_volatility() {
let volatility = Positive::ONE; let dt = Decimal::ONE;
let up = calculate_up_factor(volatility, dt).unwrap();
let down = calculate_down_factor(volatility, dt).unwrap();
assert!(up > dec!(1.0));
assert!(down < dec!(1.0));
assert_decimal_eq!(up * down, Decimal::ONE, dec!(1e-10));
}
#[test]
fn test_factors_small_dt() {
let volatility = pos_or_panic!(0.2);
let dt = ONE_DAY / dec!(24.0);
let up = calculate_up_factor(volatility, dt).unwrap();
let down = calculate_down_factor(volatility, dt).unwrap();
assert!(up > Decimal::ONE);
assert!(down < Decimal::ONE);
assert_decimal_eq!(up * down, Decimal::ONE, dec!(1e-10));
}
#[test]
fn test_factors_different_time_periods() {
let volatility = pos_or_panic!(0.2);
let daily_dt = ONE_DAY;
let weekly_dt = dec!(5.0) / dec!(252.0);
let monthly_dt = dec!(21.0) / dec!(252.0);
let daily_up = calculate_up_factor(volatility, daily_dt).unwrap();
let weekly_up = calculate_up_factor(volatility, weekly_dt).unwrap();
let monthly_up = calculate_up_factor(volatility, monthly_dt).unwrap();
assert!(daily_up < weekly_up);
assert!(weekly_up < monthly_up);
}
#[test]
fn test_factors_extreme_volatility() {
let volatility = pos_or_panic!(5.0); let dt = Decimal::ONE;
let up = calculate_up_factor(volatility, dt).unwrap();
let down = calculate_down_factor(volatility, dt).unwrap();
assert!(up > Decimal::ONE);
assert!(down < Decimal::ONE);
assert_decimal_eq!(up * down, Decimal::ONE, EPSILON);
}
#[test]
fn test_factors_symmetry() {
let volatility = pos_or_panic!(0.3);
let dt = dec!(1.0) / dec!(12.0);
let up = calculate_up_factor(volatility, dt).unwrap();
let down = calculate_down_factor(volatility, dt).unwrap();
assert_decimal_eq!(up, Decimal::ONE / down, dec!(1e-10));
}
#[test]
fn test_factors_consistency() {
let volatility = pos_or_panic!(0.2);
let dt1 = ONE_DAY;
let dt2 = dt1 / dec!(2.0);
let up1 = calculate_up_factor(volatility, dt1).unwrap();
let up2 = calculate_up_factor(volatility, dt2).unwrap();
assert!(up1 > up2);
}
}
#[cfg(test)]
mod tests_lattice_powers {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_lattice_powers_match_per_node_powd() {
let (u, d) = (dec!(1.0316), dec!(0.9693));
let steps = 200;
let powers = match LatticePowers::new(u, d, steps) {
Ok(powers) => powers,
Err(e) => panic!("table must build: {e:?}"),
};
for k in 0..=steps {
let exponent = Decimal::from(k as u64);
assert!(matches!(
(powers.up(u, k, "up"), d_powd(u, exponent, "up")),
(Ok(a), Ok(b)) if a == b
));
assert!(matches!(
(powers.down(d, k, "down"), d_powd(d, exponent, "down")),
(Ok(a), Ok(b)) if a == b
));
}
}
#[test]
fn test_lattice_powers_overflow_reports_the_per_node_error() {
let u = dec!(1000);
let powers = match LatticePowers::new(u, Decimal::ONE, 20) {
Ok(powers) => powers,
Err(e) => panic!("table must build: {e:?}"),
};
let from_table = powers.up(u, 20, "pricing::binomial::lattice_spot::up");
let per_node: Result<Decimal, PricingError> = d_powd(
u,
Decimal::from(20u64),
"pricing::binomial::lattice_spot::up",
)
.map_err(PricingError::from);
assert!(from_table.is_err());
assert_eq!(format!("{from_table:?}"), format!("{per_node:?}"));
}
#[test]
fn test_lattice_powers_unallocatable_is_invalid_parameter() {
assert!(matches!(
LatticePowers::new(Decimal::TWO, Decimal::ONE, usize::MAX),
Err(PricingError::InvalidParameter { .. })
));
}
}