#![allow(clippy::indexing_slicing)]
use crate::Options;
use crate::error::PricingError;
use crate::greeks::{big_n, d1, d2};
use crate::model::decimal::{d_add, d_mul, d_sub, finite_decimal};
use crate::model::types::{OptionStyle, OptionType};
use positive::Positive;
use rust_decimal::Decimal;
use rust_decimal::prelude::*;
use rust_decimal_macros::dec;
use std::f64::consts::PI;
fn bivariate_normal_cdf(a: Decimal, b: Decimal, rho: Decimal) -> Result<Decimal, PricingError> {
let a_f = a.to_f64().unwrap_or(0.0);
let b_f = b.to_f64().unwrap_or(0.0);
let rho_f = rho.to_f64().unwrap_or(0.0);
if rho_f.abs() < 1e-10 {
let n_a = big_n(a).unwrap_or(Decimal::ZERO);
let n_b = big_n(b).unwrap_or(Decimal::ZERO);
return Ok(n_a * n_b);
}
if rho_f >= 1.0 - 1e-10 {
let min_ab = a.min(b);
return Ok(big_n(min_ab).unwrap_or(Decimal::ZERO));
}
if rho_f <= -1.0 + 1e-10 {
if a + b >= Decimal::ZERO {
return Ok(big_n(a).unwrap_or(Decimal::ZERO));
} else {
return Ok(Decimal::ZERO);
}
}
let result = drezner_bivariate_normal(a_f, b_f, rho_f);
let result_dec = finite_decimal(result).ok_or_else(|| {
PricingError::non_finite("pricing::compound::bivariate_normal_cdf", result)
})?;
Ok(result_dec.max(Decimal::ZERO).min(Decimal::ONE))
}
fn drezner_bivariate_normal(a: f64, b: f64, rho: f64) -> f64 {
let x: [f64; 5] = [0.04691008, 0.23076534, 0.5, 0.76923466, 0.95308992];
let w: [f64; 5] = [
0.018854042,
0.038088059,
0.0452707394,
0.038088059,
0.018854042,
];
let h = -a;
let k = -b;
let hk = h * k;
let mut bvn = 0.0;
if rho.abs() < 0.925 {
let hs = (h * h + k * k) / 2.0;
let asr = rho.asin();
for i in 0..5 {
let sn = (asr * (1.0 - x[i]) / 2.0).sin();
bvn += w[i] * (sn * hk / (1.0 - sn * sn)).exp() * (-hs / (1.0 - sn * sn)).exp();
let sn = (asr * (1.0 + x[i]) / 2.0).sin();
bvn += w[i] * (sn * hk / (1.0 - sn * sn)).exp() * (-hs / (1.0 - sn * sn)).exp();
}
bvn *= asr / (4.0 * PI);
bvn += standard_normal_cdf(-h) * standard_normal_cdf(-k);
} else {
if rho < 0.0 {
let k_tmp = -k;
let hk_tmp = -hk;
bvn = high_correlation_bvn(h, k_tmp, hk_tmp, rho, &x, &w);
} else {
bvn = high_correlation_bvn(h, k, hk, rho, &x, &w);
}
}
bvn.clamp(0.0, 1.0)
}
fn high_correlation_bvn(h: f64, k: f64, hk: f64, rho: f64, x: &[f64; 5], w: &[f64; 5]) -> f64 {
let mut bvn;
if rho.abs() < 1.0 {
let ass = (1.0 - rho) * (1.0 + rho);
let a = (ass).sqrt();
let bs = (h - k).powi(2);
let c = (4.0 - hk) / 8.0;
let d = (12.0 - hk) / 16.0;
let asr = -(bs / ass + hk) / 2.0;
if asr > -100.0 {
bvn = a
* asr.exp()
* (1.0 - c * (bs - ass) * (1.0 - d * bs / 5.0) / 3.0 + c * d * ass * ass / 5.0);
} else {
bvn = 0.0;
}
if -hk < 100.0 {
let b = ass.sqrt();
bvn -= (-hk / 2.0).exp()
* (2.0 * PI).sqrt()
* standard_normal_cdf(-h / b)
* b
* (1.0 - c * bs * (1.0 - d * bs / 5.0) / 3.0);
}
let xs = (a / 2.0) * (h - k);
for i in 0..5 {
let xs_tmp = xs * (1.0 - x[i]);
let rs = xs_tmp.powi(2);
let asr_tmp = -(bs / rs + hk) / 2.0;
if asr_tmp > -100.0 {
bvn += a
* w[i]
* asr_tmp.exp()
* ((-hk * (1.0 - rs) / (2.0 * (1.0 + (1.0 - rs).sqrt()))).exp()
/ (1.0 + (1.0 - rs).sqrt())
- (1.0 + c * rs * (1.0 + d * rs)));
}
let xs_tmp = xs * (1.0 + x[i]);
let rs = xs_tmp.powi(2);
let asr_tmp = -(bs / rs + hk) / 2.0;
if asr_tmp > -100.0 {
bvn += a
* w[i]
* asr_tmp.exp()
* ((-hk * (1.0 - rs) / (2.0 * (1.0 + (1.0 - rs).sqrt()))).exp()
/ (1.0 + (1.0 - rs).sqrt())
- (1.0 + c * rs * (1.0 + d * rs)));
}
}
bvn /= -2.0 * PI;
} else {
bvn = 0.0;
}
if rho > 0.0 {
bvn += standard_normal_cdf(-h.max(k));
} else {
bvn = -bvn;
if k > h {
bvn += standard_normal_cdf(k) - standard_normal_cdf(h);
}
}
bvn
}
fn standard_normal_cdf(x: f64) -> f64 {
big_n(Decimal::from_f64(x).unwrap_or(Decimal::ZERO))
.unwrap_or(Decimal::ZERO)
.to_f64()
.unwrap_or(0.5)
}
pub fn compound_black_scholes(option: &Options) -> Result<Decimal, PricingError> {
match &option.option_type {
OptionType::Compound { underlying_option } => price_compound(option, underlying_option),
_ => Err(PricingError::other(
"compound_black_scholes requires OptionType::Compound",
)),
}
}
fn price_compound(
compound: &Options,
underlying_type: &OptionType,
) -> Result<Decimal, PricingError> {
let s = compound.underlying_price;
let k1 = compound.strike_price; let r = compound.risk_free_rate;
let q = compound.dividend_yield.to_dec();
let sigma = compound.implied_volatility;
let t1 = compound
.expiration_date
.get_years()
.map_err(|e| PricingError::other(&e.to_string()))?;
if t1 == Positive::ZERO {
let underlying_value =
value_underlying_option(compound, underlying_type).unwrap_or(Decimal::ZERO);
let intrinsic = match compound.option_style {
OptionStyle::Call => d_sub(
underlying_value,
k1.to_dec(),
"pricing::compound::intrinsic::call",
)?
.max(Decimal::ZERO),
OptionStyle::Put => d_sub(
k1.to_dec(),
underlying_value,
"pricing::compound::intrinsic::put",
)?
.max(Decimal::ZERO),
};
return Ok(apply_side(intrinsic, compound));
}
if sigma == Positive::ZERO {
let discount = (-r * t1).exp();
let forward_value =
value_underlying_option(compound, underlying_type)? * ((r - q) * t1).exp();
let intrinsic = match compound.option_style {
OptionStyle::Call => d_mul(
d_sub(
forward_value,
k1.to_dec(),
"pricing::compound::zero_vol::call::intrinsic",
)?
.max(Decimal::ZERO),
discount,
"pricing::compound::zero_vol::call::discounted",
)?,
OptionStyle::Put => d_mul(
d_sub(
k1.to_dec(),
forward_value,
"pricing::compound::zero_vol::put::intrinsic",
)?
.max(Decimal::ZERO),
discount,
"pricing::compound::zero_vol::put::discounted",
)?,
};
return Ok(apply_side(intrinsic, compound));
}
let two = Positive::new(2.0)
.map_err(|e| PricingError::method_error("price_compound", &e.to_string()))?;
let t2 = t1 * two;
let b = r - q;
let t1_dec = t1.to_dec();
let t2_dec = t2.to_dec();
let sqrt_t1 = t1_dec.sqrt().unwrap_or(Decimal::ZERO);
let _sqrt_t2 = t2_dec.sqrt().unwrap_or(Decimal::ZERO);
let rho = (t1_dec / t2_dec).sqrt().unwrap_or(dec!(0.5));
let s_star = find_critical_price(s, k1, underlying_type, t1, sigma, r, q)?;
let d1_t1 = ((s.to_dec() / s_star).ln() + (b + sigma * sigma / dec!(2)) * t1_dec)
/ (sigma.to_dec() * sqrt_t1);
let d2_t1 = d1_t1 - sigma.to_dec() * sqrt_t1;
let k2 = get_underlying_strike(underlying_type, compound.strike_price);
let d1_t2 = d1(s, k2, b, t2, sigma)
.map_err(|e: crate::error::GreeksError| PricingError::other(&e.to_string()))?;
let d2_t2 = d2(s, k2, b, t2, sigma)
.map_err(|e: crate::error::GreeksError| PricingError::other(&e.to_string()))?;
let discount_t1 = (-r * t1).exp();
let discount_t2 = (-r * t2).exp();
let dividend_discount_t2 = (-q * t2).exp();
let is_compound_call = matches!(compound.option_style, OptionStyle::Call);
let is_underlying_call = is_underlying_option_call(underlying_type);
let build_leg = |base: Decimal,
discount: Decimal,
weight: Decimal,
op_base: &'static str,
op_leg: &'static str|
-> Result<Decimal, PricingError> {
let discounted = d_mul(base, discount, op_base)?;
Ok(d_mul(discounted, weight, op_leg)?)
};
let price = if is_compound_call && is_underlying_call {
let m1 = bivariate_normal_cdf(d1_t1, d1_t2, rho)?;
let m2 = bivariate_normal_cdf(d2_t1, d2_t2, rho)?;
let n_d2_t1 = big_n(d2_t1).unwrap_or(Decimal::ZERO);
let leg_s = build_leg(
s.to_dec(),
dividend_discount_t2,
m1,
"pricing::compound::call_call::leg_s_discounted",
"pricing::compound::call_call::leg_s",
)?;
let leg_k2 = build_leg(
k2.to_dec(),
discount_t2,
m2,
"pricing::compound::call_call::leg_k2_discounted",
"pricing::compound::call_call::leg_k2",
)?;
let leg_k1 = build_leg(
k1.to_dec(),
discount_t1,
n_d2_t1,
"pricing::compound::call_call::leg_k1_discounted",
"pricing::compound::call_call::leg_k1",
)?;
let step = d_sub(leg_s, leg_k2, "pricing::compound::call_call::step")?;
d_sub(step, leg_k1, "pricing::compound::call_call::price")?
} else if is_compound_call && !is_underlying_call {
let m1 = bivariate_normal_cdf(-d1_t1, -d1_t2, rho)?;
let m2 = bivariate_normal_cdf(-d2_t1, -d2_t2, rho)?;
let n_neg_d2_t1 = big_n(-d2_t1).unwrap_or(Decimal::ZERO);
let leg_k2 = build_leg(
k2.to_dec(),
discount_t2,
m2,
"pricing::compound::call_put::leg_k2_discounted",
"pricing::compound::call_put::leg_k2",
)?;
let leg_s = build_leg(
s.to_dec(),
dividend_discount_t2,
m1,
"pricing::compound::call_put::leg_s_discounted",
"pricing::compound::call_put::leg_s",
)?;
let leg_k1 = build_leg(
k1.to_dec(),
discount_t1,
n_neg_d2_t1,
"pricing::compound::call_put::leg_k1_discounted",
"pricing::compound::call_put::leg_k1",
)?;
let step = d_sub(leg_k2, leg_s, "pricing::compound::call_put::step")?;
d_sub(step, leg_k1, "pricing::compound::call_put::price")?
} else if !is_compound_call && is_underlying_call {
let m1 = bivariate_normal_cdf(-d1_t1, d1_t2, -rho)?;
let m2 = bivariate_normal_cdf(-d2_t1, d2_t2, -rho)?;
let n_neg_d2_t1 = big_n(-d2_t1).unwrap_or(Decimal::ZERO);
let leg_k1 = build_leg(
k1.to_dec(),
discount_t1,
n_neg_d2_t1,
"pricing::compound::put_call::leg_k1_discounted",
"pricing::compound::put_call::leg_k1",
)?;
let leg_s = build_leg(
s.to_dec(),
dividend_discount_t2,
m1,
"pricing::compound::put_call::leg_s_discounted",
"pricing::compound::put_call::leg_s",
)?;
let leg_k2 = build_leg(
k2.to_dec(),
discount_t2,
m2,
"pricing::compound::put_call::leg_k2_discounted",
"pricing::compound::put_call::leg_k2",
)?;
let step = d_sub(leg_k1, leg_s, "pricing::compound::put_call::step")?;
d_add(step, leg_k2, "pricing::compound::put_call::price")?
} else {
let m1 = bivariate_normal_cdf(d1_t1, -d1_t2, -rho)?;
let m2 = bivariate_normal_cdf(d2_t1, -d2_t2, -rho)?;
let n_d2_t1 = big_n(d2_t1).unwrap_or(Decimal::ZERO);
let leg_k1 = build_leg(
k1.to_dec(),
discount_t1,
n_d2_t1,
"pricing::compound::put_put::leg_k1_discounted",
"pricing::compound::put_put::leg_k1",
)?;
let leg_s = build_leg(
s.to_dec(),
dividend_discount_t2,
m1,
"pricing::compound::put_put::leg_s_discounted",
"pricing::compound::put_put::leg_s",
)?;
let leg_k2 = build_leg(
k2.to_dec(),
discount_t2,
m2,
"pricing::compound::put_put::leg_k2_discounted",
"pricing::compound::put_put::leg_k2",
)?;
let step = d_add(leg_k1, leg_s, "pricing::compound::put_put::step")?;
d_sub(step, leg_k2, "pricing::compound::put_put::price")?
};
Ok(apply_side(price.max(Decimal::ZERO), compound))
}
fn find_critical_price(
s: Positive,
k1: Positive,
underlying_type: &OptionType,
t: Positive,
sigma: Positive,
r: Decimal,
q: Decimal,
) -> Result<Decimal, PricingError> {
let _k2 = match underlying_type {
OptionType::European | OptionType::American => s,
_ => s,
};
let _is_call = is_underlying_option_call(underlying_type);
let b = r - q;
let t_dec = t.to_dec();
let sqrt_t = t_dec.sqrt().unwrap_or(Decimal::ZERO);
let forward = s.to_dec() * (b * t_dec).exp();
let vol_adjustment = sigma.to_dec() * sqrt_t * dec!(0.4);
let critical = if k1.to_dec() < forward * dec!(0.5) {
forward * (dec!(1) - vol_adjustment)
} else {
forward * (dec!(1) + vol_adjustment)
};
Ok(critical.max(dec!(0.01)))
}
fn get_underlying_strike(underlying_type: &OptionType, default_strike: Positive) -> Positive {
match underlying_type {
OptionType::European | OptionType::American => default_strike,
_ => default_strike,
}
}
fn is_underlying_option_call(underlying_type: &OptionType) -> bool {
match underlying_type {
OptionType::European | OptionType::American => true,
_ => true, }
}
fn value_underlying_option(
compound: &Options,
underlying_type: &OptionType,
) -> Result<Decimal, PricingError> {
let underlying = Options::new(
underlying_type.clone(),
compound.side,
compound.underlying_symbol.clone(),
compound.strike_price,
compound.expiration_date,
compound.implied_volatility,
compound.quantity,
compound.underlying_price,
compound.risk_free_rate,
compound.option_style, compound.dividend_yield,
compound.exotic_params.clone(),
);
crate::pricing::black_scholes_model::black_scholes(&underlying)
}
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_compound_option(style: OptionStyle, underlying_type: OptionType) -> Options {
Options::new(
OptionType::Compound {
underlying_option: Box::new(underlying_type),
},
Side::Long,
"TEST".to_string(),
pos_or_panic!(5.0), ExpirationDate::Days(pos_or_panic!(91.25)), pos_or_panic!(0.25), Positive::ONE, Positive::HUNDRED, dec!(0.05), style,
Positive::ZERO, None,
)
}
#[test]
fn test_bivariate_normal_independent() {
let a = dec!(0.0);
let b = dec!(0.0);
let rho = dec!(0.0);
let result = bivariate_normal_cdf(a, b, rho).expect("finite CDF");
assert!(
(result - dec!(0.25)).abs() < dec!(0.01),
"Independent result: {}",
result
);
}
#[test]
fn test_bivariate_normal_perfect_correlation() {
let a = dec!(1.0);
let b = dec!(0.5);
let rho = dec!(0.999);
let result = bivariate_normal_cdf(a, b, rho).expect("finite CDF");
let n_min = big_n(b).unwrap_or(Decimal::ZERO);
assert!(
(result - n_min).abs() < dec!(0.1),
"Perfect correlation result: {} vs N(0.5)={}",
result,
n_min
);
}
#[test]
fn test_call_on_call() {
let option = create_compound_option(OptionStyle::Call, OptionType::European);
let price = compound_black_scholes(&option).unwrap();
assert!(
price > Decimal::ZERO,
"Call-on-call should be positive: {}",
price
);
}
#[test]
fn test_call_on_put() {
let mut option = create_compound_option(OptionStyle::Call, OptionType::European);
option.option_style = OptionStyle::Put; let option = create_compound_option(OptionStyle::Call, OptionType::European);
let price = compound_black_scholes(&option).unwrap();
assert!(
price >= Decimal::ZERO,
"Call-on-put should be non-negative: {}",
price
);
}
#[test]
fn test_put_on_call() {
let option = create_compound_option(OptionStyle::Put, OptionType::European);
let price = compound_black_scholes(&option).unwrap();
assert!(
price >= Decimal::ZERO,
"Put-on-call should be non-negative: {}",
price
);
}
#[test]
fn test_put_on_put() {
let option = create_compound_option(OptionStyle::Put, OptionType::European);
let price = compound_black_scholes(&option).unwrap();
assert!(
price >= Decimal::ZERO,
"Put-on-put should be non-negative: {}",
price
);
}
#[test]
fn test_short_compound_option() {
let mut option = create_compound_option(OptionStyle::Call, OptionType::European);
let long_price = compound_black_scholes(&option).unwrap();
option.side = Side::Short;
let short_price = compound_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_compound_option(OptionStyle::Call, OptionType::European);
option.expiration_date = ExpirationDate::Days(Positive::ZERO);
let price = compound_black_scholes(&option).unwrap();
assert!(price >= Decimal::ZERO, "Zero time result: {}", price);
}
#[test]
fn test_compound_value_reasonable() {
let compound = create_compound_option(OptionStyle::Call, OptionType::European);
let compound_price = compound_black_scholes(&compound).unwrap();
assert!(
compound_price > Decimal::ZERO,
"Compound should be positive: {}",
compound_price
);
assert!(
compound_price < dec!(200.0),
"Compound price {} seems too high",
compound_price
);
}
#[test]
fn test_higher_compound_strike_means_lower_call_value() {
let low_strike = create_compound_option(OptionStyle::Call, OptionType::European);
let low_strike_price = compound_black_scholes(&low_strike).unwrap();
let mut high_strike = low_strike.clone();
high_strike.strike_price = pos_or_panic!(10.0);
let high_strike_price = compound_black_scholes(&high_strike).unwrap();
assert!(
low_strike_price >= high_strike_price,
"Lower compound strike should mean higher call value: {} vs {}",
low_strike_price,
high_strike_price
);
}
}