use super::normal::cdf;
use super::OptionKind;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DigitalKind {
CashOrNothing,
AssetOrNothing,
}
fn invalid_inputs(spot: f64, strike: f64, time_to_expiry: f64, volatility: f64) -> bool {
!spot.is_finite()
|| !strike.is_finite()
|| !time_to_expiry.is_finite()
|| !volatility.is_finite()
|| spot <= 0.0
|| strike <= 0.0
|| time_to_expiry < 0.0
|| volatility < 0.0
}
#[allow(clippy::too_many_arguments)]
pub fn digital_price(
spot: f64,
strike: f64,
rate: f64,
carry: f64,
time_to_expiry: f64,
volatility: f64,
option_kind: OptionKind,
digital_kind: DigitalKind,
) -> f64 {
if invalid_inputs(spot, strike, time_to_expiry, volatility)
|| !rate.is_finite()
|| !carry.is_finite()
{
return f64::NAN;
}
if time_to_expiry == 0.0 {
let itm = match option_kind {
OptionKind::Call => spot > strike,
OptionKind::Put => spot < strike,
};
return if itm {
match digital_kind {
DigitalKind::CashOrNothing => 1.0,
DigitalKind::AssetOrNothing => spot,
}
} else {
0.0
};
}
let discount = (-rate * time_to_expiry).exp();
let carry_discount = (-carry * time_to_expiry).exp();
if volatility == 0.0 {
let forward = spot * (carry_discount / discount); let itm = match option_kind {
OptionKind::Call => spot * carry_discount > strike * discount,
OptionKind::Put => spot * carry_discount < strike * discount,
};
let _ = forward; return if itm {
match digital_kind {
DigitalKind::CashOrNothing => discount,
DigitalKind::AssetOrNothing => spot * carry_discount,
}
} else {
0.0
};
}
let sqrt_t = time_to_expiry.sqrt();
let sigma_sqrt_t = volatility * sqrt_t;
let d1 = ((spot / strike).ln()
+ (rate - carry + 0.5 * volatility * volatility) * time_to_expiry)
/ sigma_sqrt_t;
let d2 = d1 - sigma_sqrt_t;
match digital_kind {
DigitalKind::CashOrNothing => match option_kind {
OptionKind::Call => discount * cdf(d2),
OptionKind::Put => discount * cdf(-d2),
},
DigitalKind::AssetOrNothing => match option_kind {
OptionKind::Call => spot * carry_discount * cdf(d1),
OptionKind::Put => spot * carry_discount * cdf(-d1),
},
}
}
#[allow(clippy::too_many_arguments)]
pub fn digital_greeks(
spot: f64,
strike: f64,
rate: f64,
carry: f64,
time_to_expiry: f64,
volatility: f64,
option_kind: OptionKind,
digital_kind: DigitalKind,
) -> (f64, f64, f64) {
let eps = spot * 1e-3;
if eps <= 0.0 {
return (f64::NAN, f64::NAN, f64::NAN);
}
let price_mid = digital_price(
spot,
strike,
rate,
carry,
time_to_expiry,
volatility,
option_kind,
digital_kind,
);
let price_up = digital_price(
spot + eps,
strike,
rate,
carry,
time_to_expiry,
volatility,
option_kind,
digital_kind,
);
let price_dn = digital_price(
spot - eps,
strike,
rate,
carry,
time_to_expiry,
volatility,
option_kind,
digital_kind,
);
let delta = (price_up - price_dn) / (2.0 * eps);
let gamma = (price_up - 2.0 * price_mid + price_dn) / (eps * eps);
let vol_bump = 1e-3;
let vega = if volatility + vol_bump > 0.0 && volatility - vol_bump > 0.0 {
let price_vup = digital_price(
spot,
strike,
rate,
carry,
time_to_expiry,
volatility + vol_bump,
option_kind,
digital_kind,
);
let price_vdn = digital_price(
spot,
strike,
rate,
carry,
time_to_expiry,
volatility - vol_bump,
option_kind,
digital_kind,
);
(price_vup - price_vdn) / (2.0 * vol_bump)
} else {
let price_vup = digital_price(
spot,
strike,
rate,
carry,
time_to_expiry,
volatility + vol_bump,
option_kind,
digital_kind,
);
(price_vup - price_mid) / vol_bump
};
(delta, gamma, vega)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::options::OptionKind;
#[test]
fn cash_or_nothing_call_atm() {
let price = digital_price(
100.0,
100.0,
0.05,
0.0,
1.0,
0.2,
OptionKind::Call,
DigitalKind::CashOrNothing,
);
assert!(
price > 0.0 && price < 1.0,
"price should be between 0 and 1"
);
assert!((price - 0.532).abs() < 0.01, "price ≈ 0.532, got {price}");
}
#[test]
fn asset_or_nothing_call_at_zero_vol() {
let price = digital_price(
110.0,
100.0,
0.05,
0.0,
1.0,
0.0,
OptionKind::Call,
DigitalKind::AssetOrNothing,
);
assert!((price - 110.0).abs() < 1e-6);
}
#[test]
fn digital_price_returns_nan_for_invalid() {
let price = digital_price(
-1.0,
100.0,
0.05,
0.0,
1.0,
0.2,
OptionKind::Call,
DigitalKind::CashOrNothing,
);
assert!(price.is_nan());
}
#[test]
fn cash_or_nothing_put_call_parity() {
let call = digital_price(
100.0,
100.0,
0.05,
0.02,
1.0,
0.25,
OptionKind::Call,
DigitalKind::CashOrNothing,
);
let put = digital_price(
100.0,
100.0,
0.05,
0.02,
1.0,
0.25,
OptionKind::Put,
DigitalKind::CashOrNothing,
);
let discount = (-0.05_f64).exp();
assert!((call + put - discount).abs() < 1e-10);
}
#[test]
fn asset_or_nothing_put_call_parity() {
let s = 100.0_f64;
let q = 0.02_f64;
let call = digital_price(
s,
100.0,
0.05,
q,
1.0,
0.25,
OptionKind::Call,
DigitalKind::AssetOrNothing,
);
let put = digital_price(
s,
100.0,
0.05,
q,
1.0,
0.25,
OptionKind::Put,
DigitalKind::AssetOrNothing,
);
let expected = s * (-q).exp();
assert!((call + put - expected).abs() < 1e-10);
}
#[test]
fn digital_greeks_are_finite_for_valid_inputs() {
let (delta, gamma, vega) = digital_greeks(
100.0,
100.0,
0.05,
0.0,
1.0,
0.2,
OptionKind::Call,
DigitalKind::CashOrNothing,
);
assert!(delta.is_finite());
assert!(gamma.is_finite());
assert!(vega.is_finite());
}
#[test]
fn digital_at_expiry_itm_returns_intrinsic() {
let price = digital_price(
110.0,
100.0,
0.05,
0.0,
0.0,
0.2,
OptionKind::Call,
DigitalKind::CashOrNothing,
);
assert!((price - 1.0).abs() < 1e-10);
let price2 = digital_price(
110.0,
100.0,
0.05,
0.0,
0.0,
0.2,
OptionKind::Call,
DigitalKind::AssetOrNothing,
);
assert!((price2 - 110.0).abs() < 1e-10);
}
#[test]
fn digital_at_expiry_otm_returns_zero() {
let price = digital_price(
90.0,
100.0,
0.05,
0.0,
0.0,
0.2,
OptionKind::Call,
DigitalKind::CashOrNothing,
);
assert!((price - 0.0).abs() < 1e-10);
}
}