use crate::Options;
use crate::error::PricingError;
use crate::greeks::{big_n, d1, d2};
use crate::model::decimal::{d_add, d_div, d_exp, d_ln, d_mul, d_powd, d_sqrt, d_sub};
use crate::model::types::{AsianAveragingType, OptionStyle, OptionType};
use positive::Positive;
use rust_decimal::Decimal;
use rust_decimal::prelude::*;
use rust_decimal_macros::dec;
const M2_SERIES_TERMS: u32 = 6;
const M2_SERIES_THRESHOLD: Decimal = dec!(1e-3);
const M2_MIDPOINT_THRESHOLD: Decimal = dec!(1e-8);
pub fn asian_black_scholes(option: &Options) -> Result<Decimal, PricingError> {
match &option.option_type {
OptionType::Asian { averaging_type } => match averaging_type {
AsianAveragingType::Geometric => geometric_asian_price(option),
AsianAveragingType::Arithmetic => arithmetic_asian_price(option),
_ => Err(PricingError::other(
"asian_black_scholes: unsupported AsianAveragingType",
)),
},
_ => Err(PricingError::other(
"asian_black_scholes requires OptionType::Asian",
)),
}
}
fn geometric_asian_price(option: &Options) -> Result<Decimal, PricingError> {
let s = option.underlying_price;
let k = option.strike_price;
let r = option.risk_free_rate;
let q = option.dividend_yield.to_dec();
let sigma = option.implied_volatility;
let t = option
.expiration_date
.get_years()
.map_err(|e| PricingError::other(&e.to_string()))?;
if t == Positive::ZERO {
return intrinsic_value(option);
}
if sigma == Positive::ZERO {
let t_dec = t.to_dec();
let discount = d_exp(
d_mul(-r, t_dec, "pricing::asian::geometric::det::neg_rt")?,
"pricing::asian::geometric::det::discount",
)?;
let carry = d_sub(r, q, "pricing::asian::geometric::det::carry")?;
let average = d_mul(
s.to_dec(),
d_exp(
d_div(
d_mul(carry, t_dec, "pricing::asian::geometric::det::carry_t")?,
dec!(2),
"pricing::asian::geometric::det::half_carry_t",
)?,
"pricing::asian::geometric::det::growth",
)?,
"pricing::asian::geometric::det::average",
)?;
return deterministic_price(average, k.to_dec(), discount, option);
}
let sigma_sq = d_mul(
sigma.to_dec(),
sigma.to_dec(),
"pricing::asian::geometric::sigma_sq",
)?;
let sqrt_three = Positive::new(3.0_f64.sqrt())
.map_err(|e| PricingError::method_error("geometric_asian_price", &e.to_string()))?;
let sigma_adj = Positive::new_decimal(d_div(
sigma.to_dec(),
sqrt_three.to_dec(),
"pricing::asian::geometric::sigma_adj",
)?)?;
let b_adj = d_div(
d_sub(
d_sub(r, q, "pricing::asian::geometric::carry")?,
d_div(
sigma_sq,
dec!(6),
"pricing::asian::geometric::variance_drag",
)?,
"pricing::asian::geometric::b_numerator",
)?,
dec!(2),
"pricing::asian::geometric::b_adj",
)?;
let d1_val = d1(s, k, b_adj, t, sigma_adj)
.map_err(|e: crate::error::GreeksError| PricingError::other(&e.to_string()))?;
let d2_val = d2(s, k, b_adj, t, sigma_adj)
.map_err(|e: crate::error::GreeksError| PricingError::other(&e.to_string()))?;
let t_dec = t.to_dec();
let discount = d_exp(
d_mul(-r, t_dec, "pricing::asian::geometric::neg_rt")?,
"pricing::asian::geometric::discount",
)?;
let carry_discount = d_exp(
d_mul(
d_sub(b_adj, r, "pricing::asian::geometric::carry_spread")?,
t_dec,
"pricing::asian::geometric::carry_spread_t",
)?,
"pricing::asian::geometric::carry_discount",
)?;
let s_leg = d_mul(
s.to_dec(),
carry_discount,
"pricing::asian::geometric::spot_leg",
)?;
let k_leg = d_mul(
k.to_dec(),
discount,
"pricing::asian::geometric::strike_leg",
)?;
let price = match option.option_style {
OptionStyle::Call => {
let n_d1 = big_n(d1_val).unwrap_or(Decimal::ZERO);
let n_d2 = big_n(d2_val).unwrap_or(Decimal::ZERO);
d_sub(
d_mul(s_leg, n_d1, "pricing::asian::geometric::call::spot")?,
d_mul(k_leg, n_d2, "pricing::asian::geometric::call::strike")?,
"pricing::asian::geometric::call",
)?
}
OptionStyle::Put => {
let n_neg_d1 = big_n(-d1_val).unwrap_or(Decimal::ZERO);
let n_neg_d2 = big_n(-d2_val).unwrap_or(Decimal::ZERO);
d_sub(
d_mul(k_leg, n_neg_d2, "pricing::asian::geometric::put::strike")?,
d_mul(s_leg, n_neg_d1, "pricing::asian::geometric::put::spot")?,
"pricing::asian::geometric::put",
)?
}
};
Ok(apply_side(price, option))
}
fn arithmetic_asian_price(option: &Options) -> Result<Decimal, PricingError> {
let s = option.underlying_price;
let k = option.strike_price;
let r = option.risk_free_rate;
let q = option.dividend_yield.to_dec();
let sigma = option.implied_volatility;
let t = option
.expiration_date
.get_years()
.map_err(|e| PricingError::other(&e.to_string()))?;
if t == Positive::ZERO {
return intrinsic_value(option);
}
let t_dec = t.to_dec();
let discount = d_exp(
d_mul(-r, t_dec, "pricing::asian::arithmetic::neg_rt")?,
"pricing::asian::arithmetic::discount",
)?;
if sigma == Positive::ZERO {
let carry = d_sub(r, q, "pricing::asian::arithmetic::det::carry")?;
let average = arithmetic_average_forward(s.to_dec(), carry, t_dec)?;
return deterministic_price(average, k.to_dec(), discount, option);
}
let b = d_sub(r, q, "pricing::asian::arithmetic::carry")?; let sigma_dec = sigma.to_dec();
let sigma_sq = d_mul(sigma_dec, sigma_dec, "pricing::asian::arithmetic::sigma_sq")?;
let s_dec = s.to_dec();
let s_sq = d_powd(s_dec, Decimal::TWO, "pricing::asian::arithmetic::s_sq")?;
let two_b_plus_var = d_add(
d_mul(dec!(2), b, "pricing::asian::arithmetic::two_b")?,
sigma_sq,
"pricing::asian::arithmetic::two_b_plus_var",
)?;
let b_plus_var = d_add(b, sigma_sq, "pricing::asian::arithmetic::b_plus_var")?;
let m1 = arithmetic_average_forward(s_dec, b, t_dec)?;
let x = d_mul(b, t_dec, "pricing::asian::arithmetic::m2::outer_rate")?;
let z = d_mul(
two_b_plus_var,
t_dec,
"pricing::asian::arithmetic::m2::total_rate",
)?;
let y = d_mul(
b_plus_var,
t_dec,
"pricing::asian::arithmetic::m2::inner_rate",
)?;
let m2 = d_mul(
s_sq,
scaled_second_moment(x, z, y)?,
"pricing::asian::arithmetic::m2",
)?;
let f_adj = m1;
if f_adj <= Decimal::ZERO {
return deterministic_price(f_adj, k.to_dec(), discount, option);
}
let m1_sq = d_powd(m1, Decimal::TWO, "pricing::asian::arithmetic::m1_sq")?;
let moment_ratio = d_div(m2, m1_sq, "pricing::asian::arithmetic::moment_ratio")?;
let variance = if moment_ratio > Decimal::ZERO {
d_div(
d_ln(moment_ratio, "pricing::asian::arithmetic::log_moment_ratio")?,
t_dec,
"pricing::asian::arithmetic::variance",
)?
} else {
Decimal::ZERO
};
let sigma_adj = variance.sqrt().unwrap_or(sigma_dec);
let sqrt_t = d_sqrt(t_dec, "pricing::asian::arithmetic::sqrt_t")?;
let denominator = d_mul(sigma_adj, sqrt_t, "pricing::asian::arithmetic::denominator")?;
if denominator.is_zero() {
return deterministic_price(f_adj, k.to_dec(), discount, option);
}
let moneyness = d_div(f_adj, k.to_dec(), "pricing::asian::arithmetic::moneyness")?;
let log_moneyness = if moneyness.is_zero() {
Decimal::MIN
} else {
d_ln(moneyness, "pricing::asian::arithmetic::log_moneyness")?
};
let half_variance_t = d_div(
d_mul(
d_mul(
sigma_adj,
sigma_adj,
"pricing::asian::arithmetic::sigma_adj_sq",
)?,
t_dec,
"pricing::asian::arithmetic::variance_t",
)?,
dec!(2),
"pricing::asian::arithmetic::half_variance_t",
)?;
let d1_val = d_div(
d_add(
log_moneyness,
half_variance_t,
"pricing::asian::arithmetic::d1_numerator",
)?,
denominator,
"pricing::asian::arithmetic::d1",
)?;
let d2_val = d_sub(d1_val, denominator, "pricing::asian::arithmetic::d2")?;
let price = match option.option_style {
OptionStyle::Call => {
let n_d1 = big_n(d1_val).unwrap_or(Decimal::ZERO);
let n_d2 = big_n(d2_val).unwrap_or(Decimal::ZERO);
d_mul(
discount,
d_sub(
d_mul(f_adj, n_d1, "pricing::asian::arithmetic::call::forward")?,
d_mul(k.to_dec(), n_d2, "pricing::asian::arithmetic::call::strike")?,
"pricing::asian::arithmetic::call::intrinsic",
)?,
"pricing::asian::arithmetic::call",
)?
}
OptionStyle::Put => {
let n_neg_d1 = big_n(-d1_val).unwrap_or(Decimal::ZERO);
let n_neg_d2 = big_n(-d2_val).unwrap_or(Decimal::ZERO);
d_mul(
discount,
d_sub(
d_mul(
k.to_dec(),
n_neg_d2,
"pricing::asian::arithmetic::put::strike",
)?,
d_mul(f_adj, n_neg_d1, "pricing::asian::arithmetic::put::forward")?,
"pricing::asian::arithmetic::put::intrinsic",
)?,
"pricing::asian::arithmetic::put",
)?
}
};
Ok(apply_side(price, option))
}
fn growth_average(w: Decimal) -> Result<Decimal, PricingError> {
if w.abs() < M2_SERIES_THRESHOLD {
return growth_average_series(w);
}
Ok(d_div(
d_sub(
d_exp(w, "pricing::asian::growth_average::exp")?,
Decimal::ONE,
"pricing::asian::growth_average::numerator",
)?,
w,
"pricing::asian::growth_average",
)?)
}
fn growth_average_series(w: Decimal) -> Result<Decimal, PricingError> {
let mut w_power = Decimal::ONE;
let mut factorial = Decimal::ONE;
let mut correction = Decimal::ZERO;
for n in 1..M2_SERIES_TERMS {
w_power = d_mul(w_power, w, "pricing::asian::phi::series::power")?;
factorial = d_mul(
factorial,
Decimal::from(n + 1),
"pricing::asian::phi::series::factorial",
)?;
correction = d_add(
correction,
d_div(w_power, factorial, "pricing::asian::phi::series::term")?,
"pricing::asian::phi::series::correction",
)?;
}
Ok(d_add(
Decimal::ONE,
correction,
"pricing::asian::phi::series",
)?)
}
fn scaled_second_moment(x: Decimal, z: Decimal, y: Decimal) -> Result<Decimal, PricingError> {
if x.abs().max(z.abs()) < M2_SERIES_THRESHOLD {
return second_moment_series(x, z);
}
if y.abs() < M2_MIDPOINT_THRESHOLD {
let m = d_div(
d_add(x, z, "pricing::asian::m2::midpoint::sum")?,
dec!(2),
"pricing::asian::m2::midpoint",
)?;
debug_assert!(!m.is_zero(), "midpoint branch reached with x = -z");
let exp_m = d_exp(m, "pricing::asian::m2::midpoint::exp")?;
return Ok(d_div(
d_mul(
dec!(2),
d_add(
d_mul(
exp_m,
d_sub(m, Decimal::ONE, "pricing::asian::m2::midpoint::m_less_one")?,
"pricing::asian::m2::midpoint::exp_term",
)?,
Decimal::ONE,
"pricing::asian::m2::midpoint::bracket",
)?,
"pricing::asian::m2::midpoint::numerator",
)?,
d_mul(m, m, "pricing::asian::m2::midpoint::m_sq")?,
"pricing::asian::m2::midpoint::derivative",
)?);
}
Ok(d_div(
d_mul(
dec!(2),
d_sub(
growth_average(z)?,
growth_average(x)?,
"pricing::asian::m2::divided_difference::numerator",
)?,
"pricing::asian::m2::divided_difference::scaled",
)?,
y,
"pricing::asian::m2::divided_difference",
)?)
}
fn second_moment_series(x: Decimal, z: Decimal) -> Result<Decimal, PricingError> {
let mut h = Decimal::ONE;
let mut z_power = Decimal::ONE;
let mut factorial = dec!(2);
let mut correction = Decimal::ZERO;
for k in 1..M2_SERIES_TERMS {
z_power = d_mul(z_power, z, "pricing::asian::m2::series::z_power")?;
h = d_add(
d_mul(x, h, "pricing::asian::m2::series::shift")?,
z_power,
"pricing::asian::m2::series::homogeneous",
)?;
factorial = d_mul(
factorial,
Decimal::from(k + 2),
"pricing::asian::m2::series::factorial",
)?;
correction = d_add(
correction,
d_div(
d_mul(dec!(2), h, "pricing::asian::m2::series::twice")?,
factorial,
"pricing::asian::m2::series::term",
)?,
"pricing::asian::m2::series::correction",
)?;
}
Ok(d_add(
Decimal::ONE,
correction,
"pricing::asian::m2::series",
)?)
}
fn arithmetic_average_forward(s: Decimal, b: Decimal, t: Decimal) -> Result<Decimal, PricingError> {
let bt = d_mul(b, t, "pricing::asian::average_forward::bt")?;
Ok(d_mul(
s,
growth_average(bt)?,
"pricing::asian::average_forward",
)?)
}
fn deterministic_price(
forward: Decimal,
strike: Decimal,
discount: Decimal,
option: &Options,
) -> Result<Decimal, PricingError> {
let intrinsic = match option.option_style {
OptionStyle::Call => {
d_sub(forward, strike, "pricing::asian::deterministic::call")?.max(Decimal::ZERO)
}
OptionStyle::Put => {
d_sub(strike, forward, "pricing::asian::deterministic::put")?.max(Decimal::ZERO)
}
};
let price = d_mul(intrinsic, discount, "pricing::asian::deterministic::price")?;
Ok(apply_side(price, option))
}
fn intrinsic_value(option: &Options) -> Result<Decimal, PricingError> {
let s = option.underlying_price.to_dec();
let k = option.strike_price.to_dec();
let value = match option.option_style {
OptionStyle::Call => d_sub(s, k, "pricing::asian::intrinsic::call")?.max(Decimal::ZERO),
OptionStyle::Put => d_sub(k, s, "pricing::asian::intrinsic::put")?.max(Decimal::ZERO),
};
Ok(apply_side(value, option))
}
fn apply_side(price: Decimal, option: &Options) -> Decimal {
match option.side {
crate::model::types::Side::Long => price,
crate::model::types::Side::Short => -price,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ExpirationDate;
use crate::assert_decimal_eq;
use crate::model::types::{OptionStyle, OptionType, Side};
use positive::pos_or_panic;
use rust_decimal_macros::dec;
fn create_asian_option(style: OptionStyle, averaging_type: AsianAveragingType) -> Options {
Options::new(
OptionType::Asian { averaging_type },
Side::Long,
"TEST".to_string(),
Positive::HUNDRED, ExpirationDate::Days(pos_or_panic!(182.5)), pos_or_panic!(0.25), Positive::ONE, Positive::HUNDRED, dec!(0.05), style,
Positive::ZERO, None,
)
}
fn create_turnbull_wakeman_boundary_option(dividend_yield: Decimal) -> Options {
Options::new(
OptionType::Asian {
averaging_type: AsianAveragingType::Arithmetic,
},
Side::Long,
"TEST".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(pos_or_panic!(365.0)),
Positive::new_decimal(dec!(0.2)).unwrap(),
Positive::ONE,
Positive::HUNDRED,
Decimal::ZERO,
OptionStyle::Call,
Positive::new_decimal(dividend_yield).unwrap(),
None,
)
}
#[test]
fn test_arithmetic_asian_carry_at_negative_variance_prices() {
let option = create_turnbull_wakeman_boundary_option(dec!(0.04));
let price = asian_black_scholes(&option).unwrap();
assert_decimal_eq!(price, dec!(3.6245129), dec!(1e-6));
let mut geometric = option.clone();
geometric.option_type = OptionType::Asian {
averaging_type: AsianAveragingType::Geometric,
};
let geometric_price = asian_black_scholes(&geometric).unwrap();
assert!(
geometric_price < price,
"geometric {} should sit below arithmetic {}",
geometric_price,
price
);
}
#[test]
fn test_arithmetic_asian_carry_at_half_negative_variance_prices() {
let option = create_turnbull_wakeman_boundary_option(dec!(0.02));
let price = asian_black_scholes(&option).unwrap();
assert_decimal_eq!(price, dec!(4.0977699), dec!(1e-6));
let mut geometric = option.clone();
geometric.option_type = OptionType::Asian {
averaging_type: AsianAveragingType::Geometric,
};
let geometric_price = asian_black_scholes(&geometric).unwrap();
assert!(
geometric_price < price,
"geometric {} should sit below arithmetic {}",
geometric_price,
price
);
}
#[test]
fn test_arithmetic_asian_negative_variance_boundary_is_continuous() {
let at = asian_black_scholes(&create_turnbull_wakeman_boundary_option(dec!(0.04))).unwrap();
let below =
asian_black_scholes(&create_turnbull_wakeman_boundary_option(dec!(0.039999999)))
.unwrap();
let above =
asian_black_scholes(&create_turnbull_wakeman_boundary_option(dec!(0.040000001)))
.unwrap();
assert_decimal_eq!(at, below, dec!(1e-7));
assert_decimal_eq!(at, above, dec!(1e-7));
assert!(
above < at && at < below,
"the price must stay monotone in q across the boundary: {} {} {}",
below,
at,
above
);
}
#[test]
fn test_arithmetic_asian_half_negative_variance_boundary_is_continuous() {
let at = asian_black_scholes(&create_turnbull_wakeman_boundary_option(dec!(0.02))).unwrap();
let below =
asian_black_scholes(&create_turnbull_wakeman_boundary_option(dec!(0.019999999)))
.unwrap();
let above =
asian_black_scholes(&create_turnbull_wakeman_boundary_option(dec!(0.020000001)))
.unwrap();
assert_decimal_eq!(at, below, dec!(1e-7));
assert_decimal_eq!(at, above, dec!(1e-7));
assert!(
above < at && at < below,
"the price must stay monotone in q across the boundary: {} {} {}",
below,
at,
above
);
}
fn create_zero_carry_option(risk_free_rate: Decimal) -> Options {
Options::new(
OptionType::Asian {
averaging_type: AsianAveragingType::Arithmetic,
},
Side::Long,
"TEST".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(pos_or_panic!(365.0)),
Positive::new_decimal(dec!(0.2)).unwrap(),
Positive::ONE,
Positive::HUNDRED,
risk_free_rate,
OptionStyle::Call,
Positive::new_decimal(dec!(0.04)).unwrap(),
None,
)
}
fn create_deterministic_option(
averaging_type: AsianAveragingType,
implied_volatility: Positive,
) -> Options {
Options::new(
OptionType::Asian { averaging_type },
Side::Long,
"TEST".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(pos_or_panic!(365.0)),
implied_volatility,
Positive::ONE,
Positive::HUNDRED,
dec!(0.05),
OptionStyle::Call,
Positive::ZERO,
None,
)
}
fn low_vol_option(risk_free_rate: Decimal) -> Options {
Options::new(
OptionType::Asian {
averaging_type: AsianAveragingType::Arithmetic,
},
Side::Long,
"TEST".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(pos_or_panic!(365.0)),
Positive::new_decimal(dec!(1e-5)).unwrap(),
Positive::ONE,
Positive::HUNDRED,
risk_free_rate,
OptionStyle::Call,
Positive::ZERO,
None,
)
}
#[test]
fn test_arithmetic_asian_low_volatility_carry_boundary_is_continuous() {
let price_at = |rate: Decimal| asian_black_scholes(&low_vol_option(rate)).unwrap();
let below = price_at(dec!(9.99999e-11));
let at = price_at(dec!(1e-10));
let above = price_at(dec!(1.00001e-10));
assert_decimal_eq!(below, at, dec!(1e-13));
assert_decimal_eq!(above, at, dec!(1e-13));
assert!(
below < at && at < above,
"the price must stay monotone in the carry across the old cutoff: {below} {at} {above}"
);
assert!(
price_at(dec!(1e-7)) > at + dec!(1e-13),
"a real carry must price above the vanishing one"
);
}
#[test]
fn test_arithmetic_asian_zero_carry_prices_the_average() {
let option = create_zero_carry_option(dec!(0.04));
let price = asian_black_scholes(&option).unwrap();
assert_decimal_eq!(price, dec!(4.4308753050), dec!(1e-6));
let mut geometric = option.clone();
geometric.option_type = OptionType::Asian {
averaging_type: AsianAveragingType::Geometric,
};
let geometric_price = asian_black_scholes(&geometric).unwrap();
assert_decimal_eq!(geometric_price, dec!(4.2581167661), dec!(1e-6));
assert!(
geometric_price < price && price < geometric_price + dec!(0.25),
"arithmetic {} must sit just above geometric {}",
price,
geometric_price
);
}
#[test]
fn test_arithmetic_asian_zero_carry_boundary_is_continuous() {
let at = asian_black_scholes(&create_zero_carry_option(dec!(0.04))).unwrap();
let below = asian_black_scholes(&create_zero_carry_option(dec!(0.039999999))).unwrap();
let above = asian_black_scholes(&create_zero_carry_option(dec!(0.040000001))).unwrap();
assert_decimal_eq!(at, below, dec!(1e-7));
assert_decimal_eq!(at, above, dec!(1e-7));
assert!(
below < at && at < above,
"the price must stay monotone in r across the boundary: {} {} {}",
below,
at,
above
);
}
#[test]
fn test_arithmetic_asian_zero_carry_underflowed_volatility_prices() {
let mut option = create_zero_carry_option(dec!(0.04));
option.underlying_price = pos_or_panic!(110.0);
option.implied_volatility = Positive::new_decimal(dec!(1e-15)).unwrap();
let price = asian_black_scholes(&option).unwrap();
assert_decimal_eq!(price, dec!(9.6078943915), dec!(1e-9));
}
#[test]
fn test_arithmetic_asian_zero_carry_small_variance_series_matches_closed_form() {
let mut option = create_zero_carry_option(dec!(0.04));
option.implied_volatility = Positive::new_decimal(dec!(0.000999999)).unwrap();
let series = asian_black_scholes(&option).unwrap();
option.implied_volatility = Positive::new_decimal(dec!(0.001)).unwrap();
let at = asian_black_scholes(&option).unwrap();
option.implied_volatility = Positive::new_decimal(dec!(0.001000001)).unwrap();
let closed_form = asian_black_scholes(&option).unwrap();
assert_decimal_eq!(series, at, dec!(1e-7));
assert_decimal_eq!(closed_form, at, dec!(1e-7));
assert!(
series < at && at < closed_form,
"the price must stay monotone in σ across the threshold: {} {} {}",
series,
at,
closed_form
);
}
#[test]
fn test_arithmetic_asian_zero_carry_tiny_volatility_stays_proportional() {
let mut option = create_zero_carry_option(dec!(0.04));
option.implied_volatility = Positive::new_decimal(dec!(1e-7)).unwrap();
let price = asian_black_scholes(&option).unwrap();
assert_decimal_eq!(price, dec!(0.0000022129811), dec!(1e-11));
}
#[test]
fn test_geometric_asian_zero_volatility_averages_the_path() {
let option = create_deterministic_option(AsianAveragingType::Geometric, Positive::ZERO);
let price = asian_black_scholes(&option).unwrap();
assert_decimal_eq!(price, dec!(2.4080487528), dec!(1e-9));
let near = create_deterministic_option(
AsianAveragingType::Geometric,
Positive::new_decimal(dec!(1e-6)).unwrap(),
);
assert_decimal_eq!(price, asian_black_scholes(&near).unwrap(), dec!(1e-9));
}
#[test]
fn test_arithmetic_asian_zero_volatility_averages_the_path() {
let option = create_deterministic_option(AsianAveragingType::Arithmetic, Positive::ZERO);
let price = asian_black_scholes(&option).unwrap();
assert_decimal_eq!(price, dec!(2.4182085485), dec!(1e-9));
let near = create_deterministic_option(
AsianAveragingType::Arithmetic,
Positive::new_decimal(dec!(1e-6)).unwrap(),
);
assert_decimal_eq!(price, asian_black_scholes(&near).unwrap(), dec!(1e-9));
}
#[test]
fn test_geometric_asian_call() {
let option = create_asian_option(OptionStyle::Call, AsianAveragingType::Geometric);
let price = asian_black_scholes(&option).unwrap();
assert!(
price > Decimal::ZERO,
"Geometric Asian call should be positive: {}",
price
);
assert!(
price < dec!(15.0),
"Geometric Asian call should be less than vanilla"
);
}
#[test]
fn test_geometric_asian_put() {
let option = create_asian_option(OptionStyle::Put, AsianAveragingType::Geometric);
let price = asian_black_scholes(&option).unwrap();
assert!(
price > Decimal::ZERO,
"Geometric Asian put should be positive: {}",
price
);
}
#[test]
fn test_arithmetic_asian_call() {
let option = create_asian_option(OptionStyle::Call, AsianAveragingType::Arithmetic);
let price = asian_black_scholes(&option).unwrap();
assert!(
price > Decimal::ZERO,
"Arithmetic Asian call should be positive: {}",
price
);
}
#[test]
fn test_arithmetic_asian_put() {
let option = create_asian_option(OptionStyle::Put, AsianAveragingType::Arithmetic);
let price = asian_black_scholes(&option).unwrap();
assert!(
price > Decimal::ZERO,
"Arithmetic Asian put should be positive: {}",
price
);
}
#[test]
fn test_geometric_less_than_arithmetic() {
let geometric = create_asian_option(OptionStyle::Call, AsianAveragingType::Geometric);
let arithmetic = create_asian_option(OptionStyle::Call, AsianAveragingType::Arithmetic);
let geo_price = asian_black_scholes(&geometric).unwrap();
let arith_price = asian_black_scholes(&arithmetic).unwrap();
assert!(
geo_price <= arith_price + dec!(0.5),
"Geometric {} should be <= Arithmetic {}",
geo_price,
arith_price
);
}
#[test]
fn test_short_asian_option() {
let mut option = create_asian_option(OptionStyle::Call, AsianAveragingType::Geometric);
let long_price = asian_black_scholes(&option).unwrap();
option.side = Side::Short;
let short_price = asian_black_scholes(&option).unwrap();
assert_decimal_eq!(long_price, -short_price, dec!(1e-10));
}
#[test]
fn test_zero_time_to_expiry() {
let mut option = create_asian_option(OptionStyle::Call, AsianAveragingType::Geometric);
option.expiration_date = ExpirationDate::Days(Positive::ZERO);
let price = asian_black_scholes(&option).unwrap();
assert_decimal_eq!(price, Decimal::ZERO, dec!(1e-10));
}
#[test]
fn test_itm_asian_call() {
let mut option = create_asian_option(OptionStyle::Call, AsianAveragingType::Geometric);
option.underlying_price = pos_or_panic!(120.0); let price = asian_black_scholes(&option).unwrap();
assert!(
price > dec!(10.0),
"ITM Asian call should have significant value: {}",
price
);
}
#[test]
fn test_otm_asian_call() {
let mut option = create_asian_option(OptionStyle::Call, AsianAveragingType::Geometric);
option.underlying_price = pos_or_panic!(80.0); let price = asian_black_scholes(&option).unwrap();
assert!(
price < dec!(5.0),
"OTM Asian call should have low value: {}",
price
);
}
fn create_short_maturity_option(days: Decimal, style: OptionStyle) -> Options {
Options::new(
OptionType::Asian {
averaging_type: AsianAveragingType::Arithmetic,
},
Side::Long,
"TEST".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(Positive::new_decimal(days).unwrap()),
Positive::new_decimal(dec!(0.2)).unwrap(),
Positive::ONE,
Positive::HUNDRED,
dec!(0.08),
style,
Positive::new_decimal(dec!(0.04)).unwrap(),
None,
)
}
fn create_boundary_option_at(dividend_yield: Decimal, days: Decimal) -> Options {
Options::new(
OptionType::Asian {
averaging_type: AsianAveragingType::Arithmetic,
},
Side::Long,
"TEST".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(Positive::new_decimal(days).unwrap()),
Positive::new_decimal(dec!(0.2)).unwrap(),
Positive::ONE,
Positive::HUNDRED,
Decimal::ZERO,
OptionStyle::Call,
Positive::new_decimal(dividend_yield).unwrap(),
None,
)
}
fn relative_error(value: Decimal, reference: Decimal) -> Decimal {
((value - reference) / reference).abs()
}
#[test]
fn test_arithmetic_asian_short_maturity_matches_quadrature() {
let reference = [
(dec!(0.5), dec!(0.17185581541141069723)),
(dec!(0.2), dec!(0.10837712084561396701)),
(dec!(0.1), dec!(0.076521744332875558055)),
(dec!(0.05), dec!(0.054052666108035095143)),
(dec!(0.02), dec!(0.034154202088962484802)),
(dec!(0.01), dec!(0.024139351973902131194)),
(dec!(0.005), dec!(0.017063436757072324427)),
(dec!(0.002), dec!(0.010788684989048071953)),
(dec!(0.001), dec!(0.0076276185063713378631)),
];
for (days, expected) in reference {
let option = create_short_maturity_option(days, OptionStyle::Call);
let price = asian_black_scholes(&option).unwrap();
let error = relative_error(price, expected);
assert!(
error < dec!(1e-9),
"{days} days priced {price} against {expected}, a relative {error}"
);
}
}
#[test]
fn test_arithmetic_asian_short_maturity_holds_put_call_parity() {
let call = asian_black_scholes(&create_short_maturity_option(
dec!(0.001),
OptionStyle::Call,
))
.unwrap();
let put = asian_black_scholes(&create_short_maturity_option(dec!(0.001), OptionStyle::Put))
.unwrap();
assert_decimal_eq!(call - put, dec!(0.0000054794510539814525), dec!(1e-18));
}
#[test]
fn test_arithmetic_asian_series_threshold_is_continuous() {
let summed = asian_black_scholes(&create_short_maturity_option(
dec!(3.04166666),
OptionStyle::Call,
))
.unwrap();
let closed_form = asian_black_scholes(&create_short_maturity_option(
dec!(3.0416667),
OptionStyle::Call,
))
.unwrap();
assert_decimal_eq!(summed, closed_form, dec!(1e-7));
assert!(
summed < closed_form,
"the price must stay monotone in T across the threshold: {summed} {closed_form}"
);
}
#[test]
fn test_arithmetic_asian_midpoint_threshold_is_continuous() {
let closed_form =
asian_black_scholes(&create_boundary_option_at(dec!(0.0399999899), dec!(365.0)))
.unwrap();
let midpoint =
asian_black_scholes(&create_boundary_option_at(dec!(0.0399999901), dec!(365.0)))
.unwrap();
let at = asian_black_scholes(&create_boundary_option_at(dec!(0.04), dec!(365.0))).unwrap();
assert_decimal_eq!(closed_form, midpoint, dec!(1e-7));
assert!(
at < midpoint && midpoint < closed_form,
"the price must stay monotone in q across the threshold: {closed_form} {midpoint} {at}"
);
}
#[test]
fn test_arithmetic_asian_removable_boundaries_hold_at_short_maturity() {
let reference = [
(dec!(0.04), dec!(0.01), dec!(0.024084590543502699977)),
(dec!(0.04), dec!(0.001), dec!(0.0076221400994210520537)),
(dec!(0.02), dec!(0.01), dec!(0.024098286688876302321)),
(dec!(0.02), dec!(0.001), dec!(0.0076235098840218887521)),
];
for (dividend_yield, days, expected) in reference {
let option = create_boundary_option_at(dividend_yield, days);
let price = asian_black_scholes(&option).unwrap();
let error = relative_error(price, expected);
assert!(
error < dec!(1e-9),
"q = {dividend_yield} at {days} days priced {price} against {expected}, \
a relative {error}"
);
}
}
}