use crate::error::{PricingError, VolatilityError};
use crate::pricing::black_scholes_model::european_price_band;
use crate::pricing::constants::{IV_TOLERANCE, MAX_ITERATIONS_IV};
use crate::pricing::iv_solver::{
IV_BRACKET_MAX, IvOutcome, iv_initial_guess, long_residual, solve_implied_volatility,
};
use crate::pricing::monte_carlo::price_option_monte_carlo;
use crate::pricing::{
BinomialPricingParams, RegimeVolatility, TELEGRAPH_PATHS, black_scholes,
generate_binomial_tree, price_binomial, telegraph,
};
use optionstratlib_core::constants::MIN_VOLATILITY;
use optionstratlib_core::error::{OptionsError, OptionsResult};
use optionstratlib_core::model::Options;
use optionstratlib_core::model::Positive;
use optionstratlib_core::model::decimal::{d_add, d_sub};
use optionstratlib_core::model::types::OptionType;
use rand::Rng;
use rust_decimal::Decimal;
use std::num::NonZeroUsize;
pub type PriceBinomialTree = OptionsResult<(Decimal, Vec<Vec<Decimal>>, Vec<Vec<Decimal>>)>;
pub trait OptionPricing {
fn calculate_price_binomial(&self, no_steps: NonZeroUsize) -> OptionsResult<Decimal>;
fn calculate_price_binomial_tree(&self, no_steps: NonZeroUsize) -> PriceBinomialTree;
fn calculate_price_black_scholes(&self) -> OptionsResult<Decimal>;
fn calculate_price_montecarlo(&self, prices: &[Positive]) -> OptionsResult<Positive>;
fn calculate_price_telegraph(
&self,
no_steps: NonZeroUsize,
volatility: RegimeVolatility,
rng: &mut dyn Rng,
) -> OptionsResult<Decimal>;
fn time_value(&self) -> OptionsResult<Decimal>;
fn calculate_implied_volatility(
&self,
market_price: Decimal,
) -> Result<Positive, VolatilityError>;
}
impl OptionPricing for Options {
fn calculate_price_binomial(&self, no_steps: NonZeroUsize) -> OptionsResult<Decimal> {
let expiry = self.time_to_expiration()?;
let cpb = price_binomial(BinomialPricingParams {
asset: self.underlying_price,
volatility: self.implied_volatility,
int_rate: self.risk_free_rate,
strike: self.strike_price,
expiry,
no_steps,
option_type: &self.option_type,
option_style: &self.option_style,
side: &self.side,
})?;
Ok(cpb)
}
fn calculate_price_binomial_tree(&self, no_steps: NonZeroUsize) -> PriceBinomialTree {
let expiry = self.time_to_expiration()?;
let params = BinomialPricingParams {
asset: self.underlying_price,
volatility: self.implied_volatility,
int_rate: self.risk_free_rate,
strike: self.strike_price,
expiry,
no_steps,
option_type: &self.option_type,
option_style: &self.option_style,
side: &self.side,
};
let (asset_tree, option_tree) = generate_binomial_tree(¶ms)?;
let price = option_tree
.first()
.and_then(|row| row.first())
.copied()
.ok_or(PricingError::BinomialNodeMissing {
node: "option[0][0]",
})?;
Ok((price, asset_tree, option_tree))
}
fn calculate_price_black_scholes(&self) -> OptionsResult<Decimal> {
Ok(black_scholes(self)?)
}
fn calculate_price_montecarlo(&self, prices: &[Positive]) -> OptionsResult<Positive> {
Ok(price_option_monte_carlo(self, prices)?)
}
fn calculate_price_telegraph(
&self,
no_steps: NonZeroUsize,
volatility: RegimeVolatility,
rng: &mut dyn Rng,
) -> OptionsResult<Decimal> {
Ok(telegraph(
self,
no_steps,
TELEGRAPH_PATHS,
None,
None,
volatility,
rng,
)?)
}
fn time_value(&self) -> OptionsResult<Decimal> {
let option_price = OptionPricing::calculate_price_black_scholes(self)?.abs();
let intrinsic_value = self.intrinsic_value(self.underlying_price)?;
let time_value = d_sub(option_price, intrinsic_value, "model::option::time_value")?;
Ok(time_value.max(Decimal::ZERO))
}
fn calculate_implied_volatility(
&self,
market_price: Decimal,
) -> Result<Positive, VolatilityError> {
if self.time_to_expiration()?.is_zero() {
return Err(OptionsError::TimeError {
operation: "calculate_implied_volatility".to_string(),
reason:
"the option has expired: at T = 0 the price does not depend on the volatility"
.to_string(),
}
.into());
}
let is_short = self.is_short();
let target_price = if is_short {
-market_price
} else {
market_price
};
if matches!(self.option_type, OptionType::European) {
let (lower, upper) = european_price_band(self).map_err(OptionsError::from)?;
let below = target_price < d_sub(lower, IV_TOLERANCE, "pricing::iv::band::lower")?;
let above = target_price > d_add(upper, IV_TOLERANCE, "pricing::iv::band::upper")?;
if below || above {
return Err(outside_band_error(market_price, target_price, lower, upper));
}
}
let guess = iv_initial_guess(self, target_price).unwrap_or(IV_BRACKET_MAX);
let outcome = solve_implied_volatility(
*MIN_VOLATILITY,
IV_BRACKET_MAX,
guess,
MAX_ITERATIONS_IV,
|sigma, with_slope| long_residual(self, target_price, sigma, with_slope),
)?;
match outcome {
IvOutcome::Found(volatility) => Ok(volatility),
IvOutcome::BelowBracket => Ok(*MIN_VOLATILITY),
IvOutcome::AboveBracket => Err(VolatilityError::NoConvergence {
iterations: 0,
last_volatility: IV_BRACKET_MAX,
}),
IvOutcome::NoConvergence(last_volatility) => Err(VolatilityError::NoConvergence {
iterations: MAX_ITERATIONS_IV,
last_volatility,
}),
}
}
}
#[cold]
#[inline(never)]
fn outside_band_error(
market_price: Decimal,
target_price: Decimal,
lower: Decimal,
upper: Decimal,
) -> VolatilityError {
match Positive::new_decimal(market_price.abs()) {
Ok(price) => VolatilityError::InvalidPrice {
price,
reason: format!(
"target {target_price} is outside the no-arbitrage band [{lower}, {upper}], so no implied volatility exists"
),
},
Err(e) => VolatilityError::PositiveError(e),
}
}
#[cfg(test)]
mod tests_option_pricing_trait {
use super::*;
use optionstratlib_core::model::types::{OptionStyle, Side};
use optionstratlib_core::model::utils::create_sample_option_simplest;
use optionstratlib_core::model::{ExpirationDate, OptionType};
use optionstratlib_core::pos_or_panic;
use rust_decimal_macros::dec;
fn sample() -> Options {
Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
pos_or_panic!(100.0),
ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2),
Positive::ONE,
pos_or_panic!(100.0),
dec!(0.05),
OptionStyle::Call,
Positive::ZERO,
None,
)
}
#[test]
fn test_option_pricing_trait_matches_inherent_black_scholes() {
let option = sample();
let via_trait = OptionPricing::calculate_price_black_scholes(&option);
let via_inherent = Options::calculate_price_black_scholes(&option);
assert_eq!(via_trait.ok(), via_inherent.ok());
}
#[test]
fn test_option_pricing_trait_matches_inherent_binomial() {
let option = sample();
let steps = NonZeroUsize::new(50).expect("non-zero");
let via_trait = OptionPricing::calculate_price_binomial(&option, steps);
let via_inherent = Options::calculate_price_binomial(&option, steps);
assert_eq!(via_trait.ok(), via_inherent.ok());
}
#[test]
fn test_option_pricing_trait_matches_inherent_time_value() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let via_trait = OptionPricing::time_value(&option);
let via_inherent = Options::time_value(&option);
assert_eq!(via_trait.ok(), via_inherent.ok());
}
#[test]
fn test_option_pricing_trait_matches_inherent_implied_volatility() {
let option = sample();
let market_price = dec!(3.0);
let via_trait = OptionPricing::calculate_implied_volatility(&option, market_price);
let via_inherent = Options::calculate_implied_volatility(&option, market_price);
assert_eq!(via_trait.ok(), via_inherent.ok());
}
}
#[cfg(test)]
mod tests_options_pricing {
use super::*;
use optionstratlib_core::model::types::{OptionStyle, OptionType, Side};
use optionstratlib_core::model::utils::create_sample_option_simplest;
use optionstratlib_core::model::{ExpirationDate, Options};
use optionstratlib_core::{model::Positive, pos_or_panic};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
#[test]
fn test_calculate_price_binomial() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let price = option
.calculate_price_binomial(optionstratlib_core::nz!(100))
.unwrap();
assert!(price > Decimal::ZERO);
}
#[test]
fn test_calculate_price_binomial_tree() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let (price, asset_tree, option_tree) = option
.calculate_price_binomial_tree(optionstratlib_core::nz!(5))
.unwrap();
assert!(price > Decimal::ZERO);
assert_eq!(asset_tree.len(), 6);
assert_eq!(option_tree.len(), 6);
}
#[test]
fn test_calculate_price_binomial_tree_short() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Short);
let (price, asset_tree, option_tree) = option
.calculate_price_binomial_tree(optionstratlib_core::nz!(5))
.unwrap();
let long = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let (long_price, _, _) = long
.calculate_price_binomial_tree(optionstratlib_core::nz!(5))
.unwrap();
assert!(price < Decimal::ZERO);
assert_eq!(price, -long_price);
assert_eq!(asset_tree.len(), 6);
assert_eq!(option_tree.len(), 6);
}
#[test]
fn test_calculate_price_black_scholes() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let price = option.calculate_price_black_scholes().unwrap();
assert!(price > Decimal::ZERO);
}
#[test]
fn test_calculate_time_value() {
let option = Options::new(
OptionType::European,
Side::Long,
"AAPL".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2),
Positive::ONE,
pos_or_panic!(105.0),
dec!(0.05),
OptionStyle::Call,
Positive::ZERO,
None,
);
let time_value = option.time_value().unwrap();
assert!(time_value > Decimal::ZERO);
assert!(time_value < option.calculate_price_black_scholes().unwrap());
}
}
#[cfg(test)]
mod tests_time_value {
use super::*;
use optionstratlib_core::model::types::{OptionStyle, Side};
use optionstratlib_core::model::utils::create_sample_option_simplest_strike;
use optionstratlib_core::{model::Positive, pos_or_panic};
use rust_decimal::Decimal;
use optionstratlib_core::assert_decimal_eq;
use rust_decimal_macros::dec;
use tracing::debug;
#[test]
fn test_calculate_time_value_long_call() {
let option = create_sample_option_simplest_strike(
Side::Long,
OptionStyle::Call,
pos_or_panic!(105.0),
);
let time_value = option.time_value().unwrap();
assert!(time_value > Decimal::ZERO);
assert!(time_value <= option.calculate_price_black_scholes().unwrap());
}
#[test]
fn test_calculate_time_value_short_call() {
let option = create_sample_option_simplest_strike(
Side::Short,
OptionStyle::Call,
pos_or_panic!(105.0),
);
let time_value = option.time_value().unwrap();
assert!(time_value > Decimal::ZERO);
assert!(time_value <= option.calculate_price_black_scholes().unwrap().abs());
}
#[test]
fn test_calculate_time_value_long_put() {
let option =
create_sample_option_simplest_strike(Side::Long, OptionStyle::Put, pos_or_panic!(95.0));
let time_value = option.time_value().unwrap();
assert!(time_value > Decimal::ZERO);
assert!(time_value <= option.calculate_price_black_scholes().unwrap());
}
#[test]
fn test_calculate_time_value_short_put() {
let option = create_sample_option_simplest_strike(
Side::Short,
OptionStyle::Put,
pos_or_panic!(95.0),
);
let time_value = option.time_value().unwrap();
assert!(time_value > Decimal::ZERO);
assert!(time_value <= option.calculate_price_black_scholes().unwrap().abs());
}
#[test]
fn test_calculate_time_value_at_the_money() {
let call =
create_sample_option_simplest_strike(Side::Long, OptionStyle::Call, Positive::HUNDRED);
let put =
create_sample_option_simplest_strike(Side::Long, OptionStyle::Put, Positive::HUNDRED);
let call_time_value = call.time_value().unwrap();
let put_time_value = put.time_value().unwrap();
assert!(call_time_value > Decimal::ZERO);
assert!(put_time_value > Decimal::ZERO);
assert_eq!(
call_time_value,
call.calculate_price_black_scholes().unwrap()
);
assert_eq!(put_time_value, put.calculate_price_black_scholes().unwrap());
}
#[test]
fn test_calculate_time_value_deep_in_the_money() {
let call = create_sample_option_simplest_strike(
Side::Long,
OptionStyle::Call,
pos_or_panic!(150.0),
);
let put =
create_sample_option_simplest_strike(Side::Long, OptionStyle::Put, pos_or_panic!(50.0));
let call_time_value = call.time_value().unwrap();
let put_time_value = put.time_value().unwrap();
let call_price = call.calculate_price_black_scholes().unwrap();
let put_price = put.calculate_price_black_scholes().unwrap();
assert_decimal_eq!(call_time_value, call_price, dec!(0.01));
assert_decimal_eq!(put_time_value, put_price, dec!(0.01));
debug!("Call time value: {}", call_time_value);
debug!("Call BS price: {}", call_price);
debug!("Put time value: {}", put_time_value);
debug!("Put BS price: {}", put_price);
assert!(call_time_value <= call_price);
assert!(put_time_value <= put_price);
}
}
#[cfg(test)]
mod tests_calculate_price_binomial {
use super::*;
use optionstratlib_core::model::types::{OptionStyle, OptionType, Side};
use optionstratlib_core::model::utils::{
create_sample_option, create_sample_option_simplest, create_sample_option_with_date,
};
use optionstratlib_core::model::{ExpirationDate, Options};
use optionstratlib_core::{model::Positive, pos_or_panic};
use rust_decimal::Decimal;
use chrono::Utc;
use rust_decimal_macros::dec;
use std::str::FromStr;
#[test]
fn test_european_call_option_basic() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let result = option.calculate_price_binomial(optionstratlib_core::nz!(100));
assert!(result.is_ok());
let price = result.unwrap();
assert!(price > Decimal::ZERO);
}
#[test]
fn test_american_put_option() {
let option = Options::new(
OptionType::American,
Side::Long,
"TEST".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2), Positive::ONE, pos_or_panic!(95.0), dec!(0.05), OptionStyle::Put,
Positive::ZERO, None,
);
let result = option.calculate_price_binomial(optionstratlib_core::nz!(100));
assert!(result.is_ok());
let price = result.unwrap();
assert!(price > Decimal::ZERO);
}
#[test]
fn test_zero_volatility() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.implied_volatility = Positive::ZERO;
let result = option.calculate_price_binomial(optionstratlib_core::nz!(100));
assert!(result.is_ok());
}
#[test]
fn test_zero_time_to_expiry() {
let now = Utc::now().naive_utc();
let option = create_sample_option_with_date(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
pos_or_panic!(95.0),
pos_or_panic!(0.2),
now,
);
let result = option.calculate_price_binomial(optionstratlib_core::nz!(100));
assert!(result.is_ok());
let price = result.unwrap();
assert_eq!(price, Decimal::from(5));
}
#[test]
fn test_deep_itm_call() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(150.0), Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
);
let result = option.calculate_price_binomial(optionstratlib_core::nz!(100));
assert!(result.is_ok());
let price = result.unwrap();
assert!(price > Decimal::from(45)); }
#[test]
fn test_deep_otm_put() {
let option = create_sample_option(
OptionStyle::Put,
Side::Long,
pos_or_panic!(150.0), Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
);
let result = option.calculate_price_binomial(optionstratlib_core::nz!(100));
assert!(result.is_ok());
let price = result.unwrap();
assert!(price < Decimal::from(1));
}
#[test]
fn test_convergence() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let price_100 = option
.calculate_price_binomial(optionstratlib_core::nz!(100))
.unwrap();
let price_1000 = option
.calculate_price_binomial(optionstratlib_core::nz!(1000))
.unwrap();
let diff = (price_1000 - price_100).abs();
assert!(diff < Decimal::from_str("0.1").unwrap());
}
#[test]
fn test_short_position() {
let long_call_option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let mut short_call_option = long_call_option.clone();
short_call_option.side = Side::Short;
let mut short_put_option = short_call_option.clone();
short_put_option.option_style = OptionStyle::Put;
let mut long_put_option = short_put_option.clone();
long_put_option.side = Side::Long;
let long_call_price = long_call_option
.calculate_price_binomial(optionstratlib_core::nz!(100))
.unwrap();
let short_call_price = short_call_option
.calculate_price_binomial(optionstratlib_core::nz!(100))
.unwrap();
let long_put_price = long_put_option
.calculate_price_binomial(optionstratlib_core::nz!(100))
.unwrap();
let short_put_price = short_put_option
.calculate_price_binomial(optionstratlib_core::nz!(100))
.unwrap();
assert_eq!(long_call_price, -short_call_price);
assert_eq!(long_put_price, -short_put_price);
}
}
#[cfg(test)]
mod tests_options_black_scholes {
use super::*;
use optionstratlib_core::assert_decimal_eq;
use optionstratlib_core::model::types::{OptionStyle, OptionType, Side};
use optionstratlib_core::model::{ExpirationDate, Options};
use optionstratlib_core::{model::Positive, pos_or_panic};
use rust_decimal_macros::dec;
#[test]
fn test_new_option_call() {
let option = Options::new(
OptionType::European,
Side::Long,
"SP500".to_string(),
pos_or_panic!(5790.0),
ExpirationDate::Days(pos_or_panic!(18.0)),
pos_or_panic!(0.1117),
Positive::ONE,
pos_or_panic!(5781.88),
dec!(0.05),
OptionStyle::Call,
Positive::ZERO,
None,
);
assert_decimal_eq!(
option.calculate_price_black_scholes().unwrap(),
pos_or_panic!(60.306_765_882_668_3),
dec!(1e-8)
);
}
#[test]
fn test_new_option_call_bis() {
let option = Options::new(
OptionType::European,
Side::Long,
"SP500".to_string(),
pos_or_panic!(6050.0),
ExpirationDate::Days(pos_or_panic!(61.2)),
pos_or_panic!(0.12594),
Positive::ONE,
pos_or_panic!(6032.18),
dec!(0.0),
OptionStyle::Call,
Positive::ZERO,
None,
);
assert_decimal_eq!(
option.calculate_price_black_scholes().unwrap(),
pos_or_panic!(115.56),
dec!(1e-2)
);
}
#[test]
fn test_new_option_put() {
let option = Options::new(
OptionType::European,
Side::Long,
"SP500".to_string(),
pos_or_panic!(6050.0),
ExpirationDate::Days(pos_or_panic!(61.2)),
pos_or_panic!(0.1258),
Positive::ONE,
pos_or_panic!(6032.18),
dec!(0.0),
OptionStyle::Put,
Positive::ZERO,
None,
);
assert_decimal_eq!(
option.calculate_price_black_scholes().unwrap(),
pos_or_panic!(133.25),
dec!(1e-2)
);
}
#[test]
fn test_new_option_call_short() {
let option = Options::new(
OptionType::European,
Side::Short,
"SP500".to_string(),
pos_or_panic!(6050.0),
ExpirationDate::Days(pos_or_panic!(60.0)),
pos_or_panic!(0.12594),
Positive::ONE,
pos_or_panic!(6032.18),
dec!(0.0),
OptionStyle::Call,
Positive::ZERO,
None,
);
assert_decimal_eq!(
option.calculate_price_black_scholes().unwrap(),
dec!(-114.34),
dec!(1e-2)
);
}
#[test]
fn test_new_option_put_short() {
let option = Options::new(
OptionType::European,
Side::Short,
"SP500".to_string(),
pos_or_panic!(6050.0),
ExpirationDate::Days(pos_or_panic!(60.0)),
pos_or_panic!(0.12594),
Positive::ONE,
pos_or_panic!(6032.18),
dec!(0.0),
OptionStyle::Put,
Positive::ZERO,
None,
);
assert_decimal_eq!(
option.calculate_price_black_scholes().unwrap(),
dec!(-132.16),
dec!(1e-2)
);
}
}
#[cfg(test)]
mod tests_calculate_implied_volatility {
use super::*;
use crate::error::VolatilityError;
use optionstratlib_core::assert_pos_relative_eq;
use optionstratlib_core::model::types::{OptionStyle, OptionType, Side};
use optionstratlib_core::model::{ExpirationDate, Options};
use optionstratlib_core::{model::Positive, pos_or_panic};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
fn assert_reprices(option: &Options, iv: Positive, market_price: Decimal) {
let mut repriced = option.clone();
repriced.implied_volatility = iv;
let price = repriced.calculate_price_black_scholes().unwrap();
let error = ((price - market_price) / market_price).abs();
assert!(
error <= dec!(1e-12),
"repriced {price} vs {market_price}: {error}"
);
}
#[test]
fn test_implied_volatility_call() {
let option = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
pos_or_panic!(5790.0), ExpirationDate::Days(pos_or_panic!(18.0)),
pos_or_panic!(0.1), Positive::ONE, pos_or_panic!(5781.88), dec!(0.05), OptionStyle::Call,
Positive::ZERO, None,
);
let market_price = dec!(60.30);
let iv = option.calculate_implied_volatility(market_price).unwrap();
assert_reprices(&option, iv, market_price);
assert_pos_relative_eq!(iv, pos_or_panic!(0.111618041), pos_or_panic!(1e-3));
}
#[test]
fn test_implied_volatility_put() {
let option = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
pos_or_panic!(6050.0), ExpirationDate::Days(pos_or_panic!(60.0)),
pos_or_panic!(0.1), Positive::ONE, pos_or_panic!(6032.18), dec!(0.0), OptionStyle::Put,
Positive::ZERO, None,
);
let market_price = dec!(132.16);
let iv = option.calculate_implied_volatility(market_price).unwrap();
assert_reprices(&option, iv, market_price);
assert_pos_relative_eq!(iv, pos_or_panic!(0.125961), pos_or_panic!(1e-3));
}
#[test]
fn test_implied_volatility_call_short() {
let option = Options::new(
OptionType::European,
Side::Short,
"TEST".to_string(),
pos_or_panic!(6050.0), ExpirationDate::Days(pos_or_panic!(60.0)),
pos_or_panic!(0.1), Positive::ONE, pos_or_panic!(6032.18), dec!(0.0), OptionStyle::Call,
Positive::ZERO, None,
);
let market_price = dec!(-114.16);
let iv = option.calculate_implied_volatility(market_price).unwrap();
assert_reprices(&option, iv, market_price);
assert_pos_relative_eq!(iv, pos_or_panic!(0.1258087), pos_or_panic!(1e-3));
}
#[test]
fn test_implied_volatility_put_short() {
let option = Options::new(
OptionType::European,
Side::Short,
"TEST".to_string(),
pos_or_panic!(6050.0), ExpirationDate::Days(pos_or_panic!(60.0)),
pos_or_panic!(0.1), Positive::ONE, pos_or_panic!(6032.18), dec!(0.0), OptionStyle::Put,
Positive::ZERO, None,
);
let market_price = dec!(-132.27);
let iv = option.calculate_implied_volatility(market_price).unwrap();
assert_reprices(&option, iv, market_price);
assert_pos_relative_eq!(iv, pos_or_panic!(0.12611389), pos_or_panic!(1e-3));
}
#[test]
fn test_invalid_market_price() {
let option = Options::default();
let result = option.calculate_implied_volatility(Decimal::ZERO);
assert!(matches!(result, Err(VolatilityError::Options(_))));
}
#[test]
fn test_expired_option() {
let option = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(Positive::ZERO),
pos_or_panic!(0.2),
Positive::ONE,
Positive::HUNDRED,
dec!(0.05),
OptionStyle::Call,
Positive::ZERO,
None,
);
let result = option.calculate_implied_volatility(dec!(2.5));
assert!(matches!(result, Err(VolatilityError::Options(_))));
}
#[test]
fn test_convergence_edge_cases() {
let option = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
pos_or_panic!(5790.0), ExpirationDate::Days(pos_or_panic!(18.0)),
pos_or_panic!(0.1), Positive::ONE, pos_or_panic!(5781.88), dec!(0.05), OptionStyle::Call,
Positive::ZERO, None,
);
let iv = option.calculate_implied_volatility(dec!(60.30)).unwrap();
assert_pos_relative_eq!(iv, pos_or_panic!(0.111328125), pos_or_panic!(0.01));
let iv = option.calculate_implied_volatility(dec!(60.30)).unwrap();
assert_pos_relative_eq!(iv, pos_or_panic!(0.111328125), pos_or_panic!(0.01));
}
}