use crate::error::PricingError;
use crate::kernels::{big_n, discount_factor};
use optionstratlib_core::model::Positive;
use optionstratlib_core::model::decimal::{d_add, d_div, d_ln, d_mul, d_powd, d_sqrt, d_sub};
use optionstratlib_core::model::types::OptionStyle;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
const MAX_ITERATIONS: usize = 100;
const TOLERANCE: Decimal = dec!(0.000001);
pub fn barone_adesi_whaley(
spot: Positive,
strike: Positive,
time_to_expiry: Positive,
risk_free_rate: Decimal,
dividend_yield: Positive,
volatility: Positive,
option_style: &OptionStyle,
) -> Result<Decimal, PricingError> {
let approximation = baw_approximation(
spot,
strike,
time_to_expiry,
risk_free_rate,
dividend_yield,
volatility,
option_style,
)?;
Ok(approximation.max(intrinsic_value(
spot.to_dec(),
strike.to_dec(),
option_style,
)?))
}
#[inline]
fn intrinsic_value(
s: Decimal,
k: Decimal,
option_style: &OptionStyle,
) -> Result<Decimal, PricingError> {
Ok(match option_style {
OptionStyle::Call => d_sub(s, k, "pricing::american::intrinsic::call")?,
OptionStyle::Put => d_sub(k, s, "pricing::american::intrinsic::put")?,
}
.max(Decimal::ZERO))
}
fn zero_volatility_american(
s: Decimal,
k: Decimal,
t: Decimal,
r: Decimal,
q: Decimal,
option_style: &OptionStyle,
) -> Result<Decimal, PricingError> {
let exercise_at = |tau: Decimal| -> Result<Decimal, PricingError> {
let k_pv = d_mul(
k,
discount_factor(
r,
tau,
"pricing::american::zero_vol::rt",
"pricing::american::zero_vol::discount_r",
)?,
"pricing::american::zero_vol::k_pv",
)?;
let s_pv = d_mul(
s,
discount_factor(
q,
tau,
"pricing::american::zero_vol::qt",
"pricing::american::zero_vol::discount_q",
)?,
"pricing::american::zero_vol::s_pv",
)?;
Ok(match option_style {
OptionStyle::Call => d_sub(s_pv, k_pv, "pricing::american::zero_vol::call")?,
OptionStyle::Put => d_sub(k_pv, s_pv, "pricing::american::zero_vol::put")?,
})
};
let mut best = exercise_at(Decimal::ZERO)?
.max(exercise_at(t)?)
.max(Decimal::ZERO);
if r > Decimal::ZERO && q > Decimal::ZERO && r != q {
let ratio = d_div(
d_mul(r, k, "pricing::american::zero_vol::rk")?,
d_mul(q, s, "pricing::american::zero_vol::qs")?,
"pricing::american::zero_vol::ratio",
)?;
let tau_star = d_div(
d_ln(ratio, "pricing::american::zero_vol::ln_ratio")?,
d_sub(r, q, "pricing::american::zero_vol::carry")?,
"pricing::american::zero_vol::tau_star",
)?;
if tau_star > Decimal::ZERO && tau_star < t {
best = best.max(exercise_at(tau_star)?);
}
}
Ok(best)
}
fn baw_approximation(
spot: Positive,
strike: Positive,
time_to_expiry: Positive,
risk_free_rate: Decimal,
dividend_yield: Positive,
volatility: Positive,
option_style: &OptionStyle,
) -> Result<Decimal, PricingError> {
let s = spot.to_dec();
let k = strike.to_dec();
let t = time_to_expiry.to_dec();
let r = risk_free_rate;
let q = dividend_yield.to_dec();
let sigma = volatility.to_dec();
if t <= Decimal::ZERO {
return intrinsic_value(s, k, option_style);
}
if sigma <= Decimal::ZERO {
return zero_volatility_american(s, k, t, r, q, option_style);
}
let european_price = black_scholes_european(s, k, t, r, q, sigma, option_style)?;
if matches!(option_style, OptionStyle::Call) && q <= Decimal::ZERO {
return Ok(european_price);
}
if matches!(option_style, OptionStyle::Put) && r < Decimal::ZERO {
return Ok(european_price);
}
let sigma_sq = d_mul(sigma, sigma, "pricing::american::sigma_sq")?;
let m = d_div(
d_mul(dec!(2), r, "pricing::american::two_r")?,
sigma_sq,
"pricing::american::m",
)?;
let n = d_div(
d_mul(
dec!(2),
d_sub(r, q, "pricing::american::carry")?,
"pricing::american::two_carry",
)?,
sigma_sq,
"pricing::american::n",
)?;
let k_factor = d_sub(
dec!(1),
discount_factor(
r,
t,
"pricing::american::neg_rt",
"pricing::american::discount",
)?,
"pricing::american::k_factor",
)?;
let four_m_over_k = if k_factor.is_zero() {
d_div(
dec!(8),
d_mul(sigma_sq, t, "pricing::american::variance_time")?,
"pricing::american::four_m_over_k_limit",
)?
} else {
d_div(
d_mul(dec!(4), m, "pricing::american::four_m")?,
k_factor,
"pricing::american::four_m_over_k",
)?
};
let n_minus_one = d_sub(n, dec!(1), "pricing::american::n_minus_one")?;
let discriminant = d_add(
d_powd(n_minus_one, Decimal::TWO, "pricing::american::n_squared")?,
four_m_over_k,
"pricing::american::discriminant",
)?;
let sqrt_disc = d_sqrt(discriminant, "pricing::american::sqrt_disc").map_err(|_| {
PricingError::method_error(
"baw",
"cannot calculate square root of negative discriminant",
)
})?;
match option_style {
OptionStyle::Call => {
let q2 = d_div(
d_add(-n_minus_one, sqrt_disc, "pricing::american::q2_numerator")?,
dec!(2),
"pricing::american::q2",
)?;
let s_star = find_critical_price_call(s, k, t, r, q, sigma, q2)?;
if s >= s_star {
d_sub(s, k, "pricing::american::call::immediate_exercise")
.map_err(PricingError::from)
} else {
let d1_val = d1(s_star, k, t, r, q, sigma)?;
let n_d1 = big_n(d1_val)?;
let a2 = d_mul(
d_div(s_star, q2, "pricing::american::call::a2_scale")?,
d_sub(
dec!(1),
d_mul(
discount_factor(
q,
t,
"pricing::american::call::neg_qt",
"pricing::american::call::dividend_discount",
)?,
n_d1,
"pricing::american::call::a2_weight",
)?,
"pricing::american::call::a2_bracket",
)?,
"pricing::american::call::a2",
)?;
let early_exercise_premium = d_mul(
a2,
d_powd(
d_div(s, s_star, "pricing::american::call::moneyness")?,
q2,
"pricing::american::call::power",
)?,
"pricing::american::call::premium",
)?;
d_add(
european_price,
early_exercise_premium,
"pricing::american::call::price",
)
.map_err(PricingError::from)
}
}
OptionStyle::Put => {
let q1 = d_div(
d_sub(-n_minus_one, sqrt_disc, "pricing::american::q1_numerator")?,
dec!(2),
"pricing::american::q1",
)?;
let s_star_star = find_critical_price_put(s, k, t, r, q, sigma, q1)?;
if s <= s_star_star {
d_sub(k, s, "pricing::american::put::immediate_exercise")
.map_err(PricingError::from)
} else {
let d1_val = d1(s_star_star, k, t, r, q, sigma)?;
let n_minus_d1 = big_n(-d1_val)?;
let a1 = d_mul(
-d_div(s_star_star, q1, "pricing::american::put::a1_scale")?,
d_sub(
dec!(1),
d_mul(
discount_factor(
q,
t,
"pricing::american::put::neg_qt",
"pricing::american::put::dividend_discount",
)?,
n_minus_d1,
"pricing::american::put::a1_weight",
)?,
"pricing::american::put::a1_bracket",
)?,
"pricing::american::put::a1",
)?;
let early_exercise_premium = d_mul(
a1,
d_powd(
d_div(s, s_star_star, "pricing::american::put::moneyness")?,
q1,
"pricing::american::put::power",
)?,
"pricing::american::put::premium",
)?;
d_add(
european_price,
early_exercise_premium,
"pricing::american::put::price",
)
.map_err(PricingError::from)
}
}
}
}
fn black_scholes_european(
s: Decimal,
k: Decimal,
t: Decimal,
r: Decimal,
q: Decimal,
sigma: Decimal,
option_style: &OptionStyle,
) -> Result<Decimal, PricingError> {
let d1_val = d1(s, k, t, r, q, sigma)?;
let sqrt_t = d_sqrt(t, "pricing::american::european::sqrt_t")?;
let d2_val = d_sub(
d1_val,
d_mul(sigma, sqrt_t, "pricing::american::european::sigma_sqrt_t")?,
"pricing::american::european::d2",
)?;
let discount = discount_factor(
r,
t,
"pricing::american::european::neg_rt",
"pricing::american::european::discount",
)?;
let forward_factor = discount_factor(
q,
t,
"pricing::american::european::neg_qt",
"pricing::american::european::forward_factor",
)?;
let n_d1 = big_n(d1_val)?;
let n_d2 = big_n(d2_val)?;
let n_minus_d1 = big_n(-d1_val)?;
let n_minus_d2 = big_n(-d2_val)?;
let s_pv = d_mul(s, forward_factor, "pricing::american::european::s_pv")?;
let k_pv = d_mul(k, discount, "pricing::american::european::k_pv")?;
match option_style {
OptionStyle::Call => Ok(d_sub(
d_mul(s_pv, n_d1, "pricing::american::european::call::spot")?,
d_mul(k_pv, n_d2, "pricing::american::european::call::strike")?,
"pricing::american::european::call",
)?),
OptionStyle::Put => Ok(d_sub(
d_mul(k_pv, n_minus_d2, "pricing::american::european::put::strike")?,
d_mul(s_pv, n_minus_d1, "pricing::american::european::put::spot")?,
"pricing::american::european::put",
)?),
}
}
fn d1(
s: Decimal,
k: Decimal,
t: Decimal,
r: Decimal,
q: Decimal,
sigma: Decimal,
) -> Result<Decimal, PricingError> {
if t <= Decimal::ZERO || sigma <= Decimal::ZERO {
return Err(PricingError::method_error(
"d1",
"time and volatility must be positive",
));
}
if s <= Decimal::ZERO || k <= Decimal::ZERO {
return Err(PricingError::method_error(
"d1",
"underlying price and strike must be positive",
));
}
let sqrt_t = d_sqrt(t, "pricing::american::d1::sqrt_t")?;
let ln_s_k = d_ln(
d_div(s, k, "pricing::american::d1::moneyness")?,
"pricing::american::d1::log_moneyness",
)?;
Ok(d_div(
d_add(
ln_s_k,
d_mul(
d_add(
d_sub(r, q, "pricing::american::d1::carry")?,
d_div(
d_mul(sigma, sigma, "pricing::american::d1::variance")?,
dec!(2),
"pricing::american::d1::half_variance",
)?,
"pricing::american::d1::drift_rate",
)?,
t,
"pricing::american::d1::drift",
)?,
"pricing::american::d1::numerator",
)?,
d_mul(sigma, sqrt_t, "pricing::american::d1::denominator")?,
"pricing::american::d1",
)?)
}
fn find_critical_price_call(
_spot: Decimal,
strike: Decimal,
t: Decimal,
r: Decimal,
q: Decimal,
sigma: Decimal,
q2: Decimal,
) -> Result<Decimal, PricingError> {
let mut s_star = d_mul(strike, dec!(1.1), "pricing::american::call::s_star_seed")?;
let exp_qt = discount_factor(
q,
t,
"pricing::american::call::neg_qt_seed",
"pricing::american::call::dividend_discount_seed",
)?;
let tolerance = TOLERANCE;
let boundary_rhs = |spot: Decimal| -> Result<Decimal, PricingError> {
let d1_val = d1(spot, strike, t, r, q, sigma)?;
let n_d1 = big_n(d1_val)?;
let c_euro = black_scholes_european(spot, strike, t, r, q, sigma, &OptionStyle::Call)?;
Ok(d_add(
c_euro,
d_mul(
d_div(spot, q2, "pricing::american::call::boundary_scale")?,
d_sub(
dec!(1),
d_mul(exp_qt, n_d1, "pricing::american::call::boundary_weight")?,
"pricing::american::call::boundary_bracket",
)?,
"pricing::american::call::boundary_premium",
)?,
"pricing::american::call::boundary_rhs",
)?)
};
for _ in 0..MAX_ITERATIONS {
let f = d_sub(
d_sub(s_star, strike, "pricing::american::call::lhs")?,
boundary_rhs(s_star)?,
"pricing::american::call::f",
)?;
let delta_s = d_mul(s_star, dec!(0.0001), "pricing::american::call::delta_s")?;
if delta_s.is_zero() {
break;
}
let bumped = d_add(s_star, delta_s, "pricing::american::call::bumped")?;
let f_plus = d_sub(
d_sub(bumped, strike, "pricing::american::call::lhs_plus")?,
boundary_rhs(bumped)?,
"pricing::american::call::f_plus",
)?;
let f_prime: Decimal = d_div(
d_sub(f_plus, f, "pricing::american::call::df")?,
delta_s,
"pricing::american::call::f_prime",
)?;
if f_prime.abs() < dec!(1e-10) {
break;
}
let s_star_new = d_sub(
s_star,
d_div(f, f_prime, "pricing::american::call::newton_step")?,
"pricing::american::call::s_star_new",
)?;
if d_sub(s_star_new, s_star, "pricing::american::call::convergence")?.abs() < tolerance {
return Ok(s_star_new.max(strike)); }
s_star = s_star_new.max(d_mul(strike, dec!(0.5), "pricing::american::call::floor")?);
}
Ok(s_star.max(strike))
}
fn find_critical_price_put(
_spot: Decimal,
strike: Decimal,
t: Decimal,
r: Decimal,
q: Decimal,
sigma: Decimal,
q1: Decimal,
) -> Result<Decimal, PricingError> {
let mut s_star = d_mul(strike, dec!(0.9), "pricing::american::put::s_star_seed")?;
let exp_qt = discount_factor(
q,
t,
"pricing::american::put::neg_qt_seed",
"pricing::american::put::dividend_discount_seed",
)?;
let tolerance = TOLERANCE;
let boundary_rhs = |spot: Decimal| -> Result<Decimal, PricingError> {
let d1_val = d1(spot, strike, t, r, q, sigma)?;
let n_minus_d1 = big_n(-d1_val)?;
let p_euro = black_scholes_european(spot, strike, t, r, q, sigma, &OptionStyle::Put)?;
Ok(d_sub(
p_euro,
d_mul(
d_div(spot, q1, "pricing::american::put::boundary_scale")?,
d_sub(
dec!(1),
d_mul(
exp_qt,
n_minus_d1,
"pricing::american::put::boundary_weight",
)?,
"pricing::american::put::boundary_bracket",
)?,
"pricing::american::put::boundary_premium",
)?,
"pricing::american::put::boundary_rhs",
)?)
};
for _ in 0..MAX_ITERATIONS {
let f = d_sub(
d_sub(strike, s_star, "pricing::american::put::lhs")?,
boundary_rhs(s_star)?,
"pricing::american::put::f",
)?;
let delta_s =
d_mul(s_star, dec!(0.0001), "pricing::american::put::delta_s")?.max(dec!(0.01)); let bumped = d_add(s_star, delta_s, "pricing::american::put::bumped")?;
let f_plus = d_sub(
d_sub(strike, bumped, "pricing::american::put::lhs_plus")?,
boundary_rhs(bumped)?,
"pricing::american::put::f_plus",
)?;
let f_prime: Decimal = d_div(
d_sub(f_plus, f, "pricing::american::put::df")?,
delta_s,
"pricing::american::put::f_prime",
)?;
if f_prime.abs() < dec!(1e-10) {
break;
}
let s_star_new = d_sub(
s_star,
d_div(f, f_prime, "pricing::american::put::newton_step")?,
"pricing::american::put::s_star_new",
)?;
if d_sub(s_star_new, s_star, "pricing::american::put::convergence")?.abs() < tolerance {
return Ok(s_star_new.max(dec!(0.01)).min(strike)); }
s_star = s_star_new.max(dec!(0.01)).min(d_mul(
strike,
dec!(1.5),
"pricing::american::put::ceiling",
)?);
}
Ok(s_star.max(dec!(0.01)).min(strike))
}
#[cfg(test)]
mod tests_american_pricing {
use super::*;
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
use optionstratlib_core::pos_or_panic;
#[test]
fn test_baw_call_at_expiry() {
let price = barone_adesi_whaley(
pos_or_panic!(110.0),
Positive::HUNDRED,
Positive::ZERO,
dec!(0.05),
Positive::ZERO,
pos_or_panic!(0.2),
&OptionStyle::Call,
)
.unwrap();
assert_relative_eq!(price.to_f64().unwrap(), 10.0, epsilon = 0.01);
}
#[test]
fn test_baw_put_at_expiry() {
let price = barone_adesi_whaley(
pos_or_panic!(90.0),
Positive::HUNDRED,
Positive::ZERO,
dec!(0.05),
Positive::ZERO,
pos_or_panic!(0.2),
&OptionStyle::Put,
)
.unwrap();
assert_relative_eq!(price.to_f64().unwrap(), 10.0, epsilon = 0.01);
}
#[test]
fn test_baw_call_no_dividend() {
let price = barone_adesi_whaley(
Positive::HUNDRED,
Positive::HUNDRED,
Positive::ONE,
dec!(0.05),
Positive::ZERO,
pos_or_panic!(0.2),
&OptionStyle::Call,
)
.unwrap();
assert!(price.to_f64().unwrap() > 9.0);
assert!(price.to_f64().unwrap() < 12.0);
}
#[test]
fn test_baw_put_has_early_exercise_premium() {
let american_put = barone_adesi_whaley(
Positive::HUNDRED,
Positive::HUNDRED,
Positive::ONE,
dec!(0.05),
Positive::ZERO,
pos_or_panic!(0.2),
&OptionStyle::Put,
)
.unwrap();
let european_put = black_scholes_european(
dec!(100),
dec!(100),
dec!(1),
dec!(0.05),
dec!(0),
dec!(0.2),
&OptionStyle::Put,
)
.unwrap();
assert!(american_put >= european_put);
}
#[test]
fn test_baw_deep_itm_put() {
let price = barone_adesi_whaley(
pos_or_panic!(50.0),
Positive::HUNDRED,
Positive::ONE,
dec!(0.05),
Positive::ZERO,
pos_or_panic!(0.2),
&OptionStyle::Put,
)
.unwrap();
assert!(price.to_f64().unwrap() >= 49.0);
}
#[test]
fn test_baw_call_with_dividend() {
let american_call = barone_adesi_whaley(
Positive::HUNDRED,
Positive::HUNDRED,
Positive::ONE,
dec!(0.05),
pos_or_panic!(0.03), pos_or_panic!(0.2),
&OptionStyle::Call,
)
.unwrap();
assert!(american_call.to_f64().unwrap() > 0.0);
}
#[test]
fn test_baw_zero_volatility() {
let price = barone_adesi_whaley(
pos_or_panic!(110.0),
Positive::HUNDRED,
Positive::ONE,
dec!(0.05),
Positive::ZERO,
Positive::ZERO,
&OptionStyle::Call,
)
.unwrap();
assert!(price.to_f64().unwrap() > 0.0);
}
#[test]
fn test_baw_otm_call() {
let price = barone_adesi_whaley(
pos_or_panic!(90.0),
Positive::HUNDRED,
Positive::ONE,
dec!(0.05),
Positive::ZERO,
pos_or_panic!(0.2),
&OptionStyle::Call,
)
.unwrap();
assert!(price.to_f64().unwrap() > 0.0);
assert!(price.to_f64().unwrap() < 10.0);
}
#[test]
fn test_baw_otm_put() {
let price = barone_adesi_whaley(
pos_or_panic!(110.0),
Positive::HUNDRED,
Positive::ONE,
dec!(0.05),
Positive::ZERO,
pos_or_panic!(0.2),
&OptionStyle::Put,
)
.unwrap();
assert!(price.to_f64().unwrap() > 0.0);
}
}