use crate::Options;
use crate::error::PricingError;
use crate::greeks::{big_n, d1, d2};
use crate::model::types::OptionType;
use positive::Positive;
use rust_decimal::Decimal;
use rust_decimal::prelude::*;
use rust_decimal_macros::dec;
pub fn chooser_black_scholes(option: &Options) -> Result<Decimal, PricingError> {
match &option.option_type {
OptionType::Chooser { choice_date } => simple_chooser_price(option, *choice_date),
_ => Err(PricingError::other(
"chooser_black_scholes requires OptionType::Chooser",
)),
}
}
fn simple_chooser_price(option: &Options, choice_date_days: f64) -> 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_big = option
.expiration_date
.get_years()
.map_err(|e| PricingError::other(&e.to_string()))?;
let t_choice = Positive::new(choice_date_days / 365.0).unwrap_or(Positive::ZERO);
if t_choice >= t_big {
return price_at_choice_equals_expiry(option);
}
if t_big == Positive::ZERO {
let call_intrinsic = (s.to_dec() - k.to_dec()).max(Decimal::ZERO);
let put_intrinsic = (k.to_dec() - s.to_dec()).max(Decimal::ZERO);
return Ok(apply_side(call_intrinsic.max(put_intrinsic), option));
}
if sigma == Positive::ZERO {
let discount_t = (-r * t_big).exp();
let forward = s.to_dec() * ((r - q) * t_big.to_dec()).exp();
let call_val = (forward - k.to_dec()).max(Decimal::ZERO) * discount_t;
let put_val = (k.to_dec() - forward).max(Decimal::ZERO) * discount_t;
return Ok(apply_side(call_val.max(put_val), option));
}
let b = r - q;
let t_big_dec = t_big.to_dec();
let t_choice_dec = t_choice.to_dec();
let _sqrt_t_big = t_big_dec.sqrt().unwrap_or(Decimal::ZERO);
let sqrt_t_choice = t_choice_dec.sqrt().unwrap_or(dec!(0.001));
let d1_val = d1(s, k, b, t_big, sigma)
.map_err(|e: crate::error::GreeksError| PricingError::other(&e.to_string()))?;
let d2_val = d2(s, k, b, t_big, sigma)
.map_err(|e: crate::error::GreeksError| PricingError::other(&e.to_string()))?;
let y1 = ((s.to_dec() / k.to_dec()).ln() + (b + sigma * sigma / dec!(2)) * t_choice_dec)
/ (sigma.to_dec() * sqrt_t_choice);
let y2 = y1 - sigma.to_dec() * sqrt_t_choice;
let n_d1 = big_n(d1_val).unwrap_or(Decimal::ZERO);
let n_d2 = big_n(d2_val).unwrap_or(Decimal::ZERO);
let n_neg_y1 = big_n(-y1).unwrap_or(Decimal::ZERO);
let n_neg_y2 = big_n(-y2).unwrap_or(Decimal::ZERO);
let dividend_discount_t = (-q * t_big_dec).exp();
let discount_t = (-r * t_big_dec).exp();
let dividend_discount_choice = (-q * t_choice_dec).exp();
let discount_choice = (-r * t_choice_dec).exp();
let price = s.to_dec() * dividend_discount_t * n_d1 - k.to_dec() * discount_t * n_d2
+ k.to_dec() * discount_choice * n_neg_y2
- s.to_dec() * dividend_discount_choice * n_neg_y1;
Ok(apply_side(price.max(Decimal::ZERO), option))
}
fn price_at_choice_equals_expiry(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 {
let call_intrinsic = (s.to_dec() - k.to_dec()).max(Decimal::ZERO);
let put_intrinsic = (k.to_dec() - s.to_dec()).max(Decimal::ZERO);
return Ok(apply_side(call_intrinsic.max(put_intrinsic), option));
}
let b = r - q;
let d1_val = d1(s, k, b, t, sigma)
.map_err(|e: crate::error::GreeksError| PricingError::other(&e.to_string()))?;
let d2_val = d2(s, k, b, t, sigma)
.map_err(|e: crate::error::GreeksError| PricingError::other(&e.to_string()))?;
let n_d1 = big_n(d1_val).unwrap_or(Decimal::ZERO);
let n_d2 = big_n(d2_val).unwrap_or(Decimal::ZERO);
let n_neg_d1 = big_n(-d1_val).unwrap_or(Decimal::ZERO);
let n_neg_d2 = big_n(-d2_val).unwrap_or(Decimal::ZERO);
let dividend_discount = (-q * t).exp();
let discount = (-r * t).exp();
let call_price = s.to_dec() * dividend_discount * n_d1 - k.to_dec() * discount * n_d2;
let put_price = k.to_dec() * discount * n_neg_d2 - s.to_dec() * dividend_discount * n_neg_d1;
Ok(apply_side(
(call_price + put_price).max(Decimal::ZERO),
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_chooser_option(choice_date_days: f64) -> Options {
Options::new(
OptionType::Chooser {
choice_date: choice_date_days,
},
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), OptionStyle::Call, Positive::ZERO, None,
)
}
#[test]
fn test_simple_chooser() {
let option = create_chooser_option(45.0); let price = chooser_black_scholes(&option).unwrap();
assert!(
price > Decimal::ZERO,
"Chooser should be positive: {}",
price
);
}
#[test]
fn test_chooser_more_valuable_than_call() {
let chooser = create_chooser_option(45.0);
let chooser_price = chooser_black_scholes(&chooser).unwrap();
let call = Options::new(
OptionType::European,
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),
OptionStyle::Call,
Positive::ZERO,
None,
);
let call_price = crate::pricing::black_scholes_model::black_scholes(&call).unwrap();
assert!(
chooser_price >= call_price,
"Chooser {} should be >= call {}",
chooser_price,
call_price
);
}
#[test]
fn test_chooser_more_valuable_than_put() {
let chooser = create_chooser_option(45.0);
let chooser_price = chooser_black_scholes(&chooser).unwrap();
let put = Options::new(
OptionType::European,
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),
OptionStyle::Put,
Positive::ZERO,
None,
);
let put_price = crate::pricing::black_scholes_model::black_scholes(&put).unwrap();
assert!(
chooser_price >= put_price,
"Chooser {} should be >= put {}",
chooser_price,
put_price
);
}
#[test]
fn test_early_choice_date() {
let option = create_chooser_option(1.0);
let price = chooser_black_scholes(&option).unwrap();
assert!(price > Decimal::ZERO, "Early choice date price: {}", price);
}
#[test]
fn test_late_choice_date() {
let option = create_chooser_option(180.0);
let price = chooser_black_scholes(&option).unwrap();
assert!(price > Decimal::ZERO, "Late choice date price: {}", price);
}
#[test]
fn test_choice_at_expiry() {
let option = create_chooser_option(182.5);
let price = chooser_black_scholes(&option).unwrap();
assert!(price > Decimal::ZERO, "Choice at expiry price: {}", price);
}
#[test]
fn test_short_chooser_option() {
let mut option = create_chooser_option(45.0);
let long_price = chooser_black_scholes(&option).unwrap();
option.side = Side::Short;
let short_price = chooser_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_chooser_option(0.0);
option.expiration_date = ExpirationDate::Days(Positive::ZERO);
let price = chooser_black_scholes(&option).unwrap();
assert_decimal_eq!(price, Decimal::ZERO, dec!(0.01));
}
#[test]
fn test_itm_call_at_expiry() {
let mut option = create_chooser_option(0.0);
option.underlying_price = pos_or_panic!(110.0);
option.expiration_date = ExpirationDate::Days(Positive::ZERO);
let price = chooser_black_scholes(&option).unwrap();
assert_decimal_eq!(price, dec!(10.0), dec!(0.01));
}
#[test]
fn test_itm_put_at_expiry() {
let mut option = create_chooser_option(0.0);
option.underlying_price = pos_or_panic!(90.0);
option.expiration_date = ExpirationDate::Days(Positive::ZERO);
let price = chooser_black_scholes(&option).unwrap();
assert_decimal_eq!(price, dec!(10.0), dec!(0.01));
}
#[test]
fn test_higher_vol_means_higher_chooser_value() {
let low_vol = create_chooser_option(45.0);
let low_vol_price = chooser_black_scholes(&low_vol).unwrap();
let mut high_vol = low_vol.clone();
high_vol.implied_volatility = pos_or_panic!(0.4);
let high_vol_price = chooser_black_scholes(&high_vol).unwrap();
assert!(
high_vol_price > low_vol_price,
"Higher vol {} should mean higher chooser value: {}",
high_vol_price,
low_vol_price
);
}
}