use crate::Options;
use crate::error::PricingError;
use crate::greeks::{big_n, d1, d2};
use crate::model::decimal::{d_exp, d_mul, d_sub};
use crate::model::types::{BinaryType, OptionStyle, OptionType};
use positive::Positive;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
const DEFAULT_CASH_PAYOUT: Decimal = dec!(1.0);
pub fn binary_black_scholes(option: &Options) -> Result<Decimal, PricingError> {
match &option.option_type {
OptionType::Binary { binary_type } => match binary_type {
BinaryType::CashOrNothing => cash_or_nothing_price(option, DEFAULT_CASH_PAYOUT),
BinaryType::AssetOrNothing => asset_or_nothing_price(option),
BinaryType::Gap => gap_binary_price(option),
_ => Err(PricingError::other(
"binary_black_scholes: unsupported BinaryType",
)),
},
_ => Err(PricingError::other(
"binary_black_scholes requires OptionType::Binary",
)),
}
}
fn cash_or_nothing_price(option: &Options, payout: Decimal) -> 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 Ok(intrinsic_cash_or_nothing(option, payout));
}
let t_dec = t.to_dec();
let b = d_sub(r, q, "pricing::binary::cash::carry")?;
let discount = d_exp(
d_mul(-r, t_dec, "pricing::binary::cash::neg_rt")?,
"pricing::binary::cash::discount",
)?;
if sigma == Positive::ZERO {
let forward = d_mul(
s.to_dec(),
d_exp(
d_mul(b, t_dec, "pricing::binary::cash::zero_vol::carry_t")?,
"pricing::binary::cash::zero_vol::growth",
)?,
"pricing::binary::cash::zero_vol::forward",
)?;
let itm = match option.option_style {
OptionStyle::Call => forward > k.to_dec(),
OptionStyle::Put => k.to_dec() > forward,
};
let value = if itm {
d_mul(payout, discount, "pricing::binary::cash::zero_vol")?
} else {
Decimal::ZERO
};
return Ok(apply_side(value, option));
}
let d2_val = d2(s, k, b, t, sigma)
.map_err(|e: crate::error::GreeksError| PricingError::other(&e.to_string()))?;
let price = match option.option_style {
OptionStyle::Call => {
let n_d2 = big_n(d2_val).unwrap_or(Decimal::ZERO);
let payout_disc = d_mul(payout, discount, "pricing::binary::cash::call::payout")?;
d_mul(payout_disc, n_d2, "pricing::binary::cash::call::price")?
}
OptionStyle::Put => {
let n_neg_d2 = big_n(-d2_val).unwrap_or(Decimal::ZERO);
let payout_disc = d_mul(payout, discount, "pricing::binary::cash::put::payout")?;
d_mul(payout_disc, n_neg_d2, "pricing::binary::cash::put::price")?
}
};
Ok(apply_side(price, option))
}
fn asset_or_nothing_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 Ok(intrinsic_asset_or_nothing(option));
}
let t_dec = t.to_dec();
let b = d_sub(r, q, "pricing::binary::asset::carry")?;
let dividend_discount = d_exp(
d_mul(-q, t_dec, "pricing::binary::asset::neg_qt")?,
"pricing::binary::asset::dividend_discount",
)?;
if sigma == Positive::ZERO {
let forward = d_mul(
s.to_dec(),
d_exp(
d_mul(b, t_dec, "pricing::binary::asset::zero_vol::carry_t")?,
"pricing::binary::asset::zero_vol::growth",
)?,
"pricing::binary::asset::zero_vol::forward",
)?;
let itm = match option.option_style {
OptionStyle::Call => forward > k.to_dec(),
OptionStyle::Put => k.to_dec() > forward,
};
let value = if itm {
d_mul(
s.to_dec(),
dividend_discount,
"pricing::binary::asset::zero_vol",
)?
} else {
Decimal::ZERO
};
return Ok(apply_side(value, option));
}
let d1_val = d1(s, k, b, t, sigma)
.map_err(|e: crate::error::GreeksError| PricingError::other(&e.to_string()))?;
let price = match option.option_style {
OptionStyle::Call => {
let n_d1 = big_n(d1_val).unwrap_or(Decimal::ZERO);
let s_disc = d_mul(
s.to_dec(),
dividend_discount,
"pricing::binary::asset::call::s_disc",
)?;
d_mul(s_disc, n_d1, "pricing::binary::asset::call::price")?
}
OptionStyle::Put => {
let n_neg_d1 = big_n(-d1_val).unwrap_or(Decimal::ZERO);
let s_disc = d_mul(
s.to_dec(),
dividend_discount,
"pricing::binary::asset::put::s_disc",
)?;
d_mul(s_disc, n_neg_d1, "pricing::binary::asset::put::price")?
}
};
Ok(apply_side(price, option))
}
fn gap_binary_price(option: &Options) -> Result<Decimal, PricingError> {
let asset_price = asset_or_nothing_price(option)?;
let cash_price = cash_or_nothing_price(option, option.strike_price.to_dec())?;
let unit_cash = cash_or_nothing_price(option, dec!(1.0))?;
let side_multiplier = match option.side {
crate::model::types::Side::Long => dec!(1),
crate::model::types::Side::Short => dec!(-1),
};
let asset_unsigned = d_mul(
asset_price,
side_multiplier,
"pricing::binary::gap::asset_unsigned",
)?;
let unit_cash_unsigned = d_mul(
unit_cash,
side_multiplier,
"pricing::binary::gap::unit_cash_unsigned",
)?;
let strike_cash = d_mul(
option.strike_price.to_dec(),
unit_cash_unsigned,
"pricing::binary::gap::strike_cash",
)?;
let gap_unsigned = d_sub(asset_unsigned, strike_cash, "pricing::binary::gap::price")?;
let _ = cash_price;
Ok(d_mul(
gap_unsigned,
side_multiplier,
"pricing::binary::gap::signed",
)?)
}
fn intrinsic_cash_or_nothing(option: &Options, payout: Decimal) -> Decimal {
let s = option.underlying_price;
let k = option.strike_price;
let itm = match option.option_style {
OptionStyle::Call => s > k,
OptionStyle::Put => k > s,
};
let value = if itm { payout } else { Decimal::ZERO };
apply_side(value, option)
}
fn intrinsic_asset_or_nothing(option: &Options) -> Decimal {
let s = option.underlying_price;
let k = option.strike_price;
let itm = match option.option_style {
OptionStyle::Call => s > k,
OptionStyle::Put => k > s,
};
let value = if itm { s.to_dec() } else { Decimal::ZERO };
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::MathematicalOps;
use rust_decimal_macros::dec;
fn create_binary_option(style: OptionStyle, binary_type: BinaryType) -> Options {
Options::new(
OptionType::Binary { binary_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,
)
}
#[test]
fn test_cash_or_nothing_call() {
let option = create_binary_option(OptionStyle::Call, BinaryType::CashOrNothing);
let price = binary_black_scholes(&option).unwrap();
assert!(
price > Decimal::ZERO,
"Cash-or-nothing call should be positive: {}",
price
);
assert!(
price < dec!(1.0),
"Cash-or-nothing call payout=1 should be < 1: {}",
price
);
}
#[test]
fn test_cash_or_nothing_put() {
let option = create_binary_option(OptionStyle::Put, BinaryType::CashOrNothing);
let price = binary_black_scholes(&option).unwrap();
assert!(
price > Decimal::ZERO,
"Cash-or-nothing put should be positive: {}",
price
);
assert!(
price < dec!(1.0),
"Cash-or-nothing put payout=1 should be < 1: {}",
price
);
}
#[test]
fn test_asset_or_nothing_call() {
let option = create_binary_option(OptionStyle::Call, BinaryType::AssetOrNothing);
let price = binary_black_scholes(&option).unwrap();
assert!(
price > dec!(30.0),
"Asset-or-nothing call should have substantial value: {}",
price
);
assert!(
price < dec!(100.0),
"Asset-or-nothing call should be < S: {}",
price
);
}
#[test]
fn test_asset_or_nothing_put() {
let option = create_binary_option(OptionStyle::Put, BinaryType::AssetOrNothing);
let price = binary_black_scholes(&option).unwrap();
assert!(
price > dec!(30.0),
"Asset-or-nothing put should have substantial value: {}",
price
);
}
#[test]
fn test_gap_binary_call() {
let option = create_binary_option(OptionStyle::Call, BinaryType::Gap);
let price = binary_black_scholes(&option).unwrap();
assert!(
price > dec!(-10.0),
"Gap binary call should be reasonable: {}",
price
);
}
#[test]
fn test_call_put_parity_cash_or_nothing() {
let call = create_binary_option(OptionStyle::Call, BinaryType::CashOrNothing);
let put = create_binary_option(OptionStyle::Put, BinaryType::CashOrNothing);
let call_price = binary_black_scholes(&call).unwrap();
let put_price = binary_black_scholes(&put).unwrap();
let t = call.expiration_date.get_years().unwrap();
let r = call.risk_free_rate;
let discounted_payout = DEFAULT_CASH_PAYOUT * (-r * t).exp();
assert_decimal_eq!(call_price + put_price, discounted_payout, dec!(0.01));
}
#[test]
fn test_short_binary_option() {
let mut option = create_binary_option(OptionStyle::Call, BinaryType::CashOrNothing);
let long_price = binary_black_scholes(&option).unwrap();
option.side = Side::Short;
let short_price = binary_black_scholes(&option).unwrap();
assert_decimal_eq!(long_price, -short_price, dec!(1e-10));
}
#[test]
fn test_zero_time_to_expiry_itm() {
let mut option = create_binary_option(OptionStyle::Call, BinaryType::CashOrNothing);
option.underlying_price = pos_or_panic!(110.0); option.expiration_date = ExpirationDate::Days(Positive::ZERO);
let price = binary_black_scholes(&option).unwrap();
assert_decimal_eq!(price, DEFAULT_CASH_PAYOUT, dec!(1e-10));
}
#[test]
fn test_zero_time_to_expiry_otm() {
let mut option = create_binary_option(OptionStyle::Call, BinaryType::CashOrNothing);
option.underlying_price = pos_or_panic!(90.0); option.expiration_date = ExpirationDate::Days(Positive::ZERO);
let price = binary_black_scholes(&option).unwrap();
assert_decimal_eq!(price, Decimal::ZERO, dec!(1e-10));
}
#[test]
fn test_deep_itm_cash_or_nothing() {
let mut option = create_binary_option(OptionStyle::Call, BinaryType::CashOrNothing);
option.underlying_price = pos_or_panic!(150.0); let price = binary_black_scholes(&option).unwrap();
let t = option.expiration_date.get_years().unwrap();
let r = option.risk_free_rate;
let discounted = DEFAULT_CASH_PAYOUT * (-r * t).exp();
assert!(
price > discounted * dec!(0.9),
"Deep ITM should be close to discounted payout: {}",
price
);
}
#[test]
fn test_deep_otm_cash_or_nothing() {
let mut option = create_binary_option(OptionStyle::Call, BinaryType::CashOrNothing);
option.underlying_price = pos_or_panic!(50.0); let price = binary_black_scholes(&option).unwrap();
assert!(
price < dec!(0.1),
"Deep OTM should be nearly zero: {}",
price
);
}
}