use crate::errors::PricingError;
use crate::math::{ncdf, npdf};
use crate::types::OptionType;
#[derive(Debug, Clone, Copy)]
pub struct DisplacedParams {
pub option_type: OptionType,
pub forward: f64,
pub strike: f64,
pub rate: f64,
pub vol: f64,
pub time: f64,
pub displacement: f64,
}
#[inline]
pub fn price(params: &DisplacedParams) -> Result<f64, PricingError> {
if params.vol < 0.0_f64 {
return Err(PricingError::NegativeVolatility);
}
if params.time < 0.0_f64 {
return Err(PricingError::NegativeTime);
}
let shifted_forward = params.forward + params.displacement;
let shifted_strike = params.strike + params.displacement;
if shifted_forward <= 0.0_f64 {
return Err(PricingError::NegativeSpot);
}
if shifted_strike <= 0.0_f64 {
return Err(PricingError::NegativeStrike);
}
let discount = (-params.rate * params.time).exp();
if params.time == 0.0_f64 {
let intrinsic = match params.option_type {
OptionType::Call => {
if params.forward > params.strike {
discount * (params.forward - params.strike)
} else {
0.0_f64
}
}
OptionType::Put => {
if params.strike > params.forward {
discount * (params.strike - params.forward)
} else {
0.0_f64
}
}
};
return Err(PricingError::IntrinsicOnly { intrinsic });
}
if params.vol == 0.0_f64 {
let intrinsic = match params.option_type {
OptionType::Call => {
if params.forward > params.strike {
discount * (params.forward - params.strike)
} else {
0.0_f64
}
}
OptionType::Put => {
if params.strike > params.forward {
discount * (params.strike - params.forward)
} else {
0.0_f64
}
}
};
return Ok(intrinsic);
}
let vol_sqrt_t = params.vol * params.time.sqrt();
let d1 = ((shifted_forward / shifted_strike).ln()
+ 0.5_f64 * params.vol * params.vol * params.time)
/ vol_sqrt_t;
let d2 = d1 - vol_sqrt_t;
let price = match params.option_type {
OptionType::Call => {
discount * shifted_forward.mul_add(ncdf(d1), -shifted_strike * ncdf(d2))
}
OptionType::Put => {
discount * shifted_strike.mul_add(ncdf(-d2), -shifted_forward * ncdf(-d1))
}
};
Ok(price)
}
#[allow(dead_code)]
#[inline]
fn shifted_npdf(d: f64) -> f64 {
npdf(d)
}
#[cfg(test)]
mod tests {
use super::*;
const LOOSE: f64 = 1e-4_f64;
const TIGHT: f64 = 1e-10_f64;
fn black76_price(
option_type: OptionType,
forward: f64,
strike: f64,
rate: f64,
vol: f64,
time: f64,
) -> f64 {
let discount = (-rate * time).exp();
let vol_sqrt_t = vol * time.sqrt();
let d1 = ((forward / strike).ln() + 0.5_f64 * vol * vol * time) / vol_sqrt_t;
let d2 = d1 - vol_sqrt_t;
match option_type {
OptionType::Call => discount * forward.mul_add(ncdf(d1), -strike * ncdf(d2)),
OptionType::Put => discount * strike.mul_add(ncdf(-d2), -forward * ncdf(-d1)),
}
}
#[test]
fn test_beta_zero_call_matches_black76() {
let params = DisplacedParams {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
displacement: 0.0_f64,
};
let displaced = price(¶ms).unwrap();
let reference = black76_price(
OptionType::Call,
100.0_f64,
100.0_f64,
0.05_f64,
0.20_f64,
1.0_f64,
);
assert!(
(displaced - reference).abs() < TIGHT,
"displaced={displaced}, reference={reference}"
);
}
#[test]
fn test_beta_zero_put_matches_black76() {
let params = DisplacedParams {
option_type: OptionType::Put,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
displacement: 0.0_f64,
};
let displaced = price(¶ms).unwrap();
let reference = black76_price(
OptionType::Put,
100.0_f64,
100.0_f64,
0.05_f64,
0.20_f64,
1.0_f64,
);
assert!(
(displaced - reference).abs() < TIGHT,
"displaced={displaced}, reference={reference}"
);
}
#[test]
fn test_beta_zero_itm_call() {
let params = DisplacedParams {
option_type: OptionType::Call,
forward: 120.0_f64,
strike: 100.0_f64,
rate: 0.03_f64,
vol: 0.25_f64,
time: 0.5_f64,
displacement: 0.0_f64,
};
let displaced = price(¶ms).unwrap();
let reference = black76_price(
OptionType::Call,
120.0_f64,
100.0_f64,
0.03_f64,
0.25_f64,
0.5_f64,
);
assert!(
(displaced - reference).abs() < TIGHT,
"displaced={displaced}, reference={reference}"
);
}
#[test]
fn test_positive_displacement_call() {
let params = DisplacedParams {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
displacement: 50.0_f64,
};
let p = price(¶ms).unwrap();
assert!(p > 0.0_f64, "price should be positive, got {p}");
let p_zero = price(&DisplacedParams {
displacement: 0.0_f64,
..params
})
.unwrap();
assert!(
(p - p_zero).abs() > LOOSE,
"displacement should change price"
);
}
#[test]
fn test_positive_displacement_put() {
let params = DisplacedParams {
option_type: OptionType::Put,
forward: 100.0_f64,
strike: 105.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
displacement: 50.0_f64,
};
let p = price(¶ms).unwrap();
assert!(p > 0.0_f64, "put price should be positive, got {p}");
}
#[test]
fn test_put_call_parity_displaced() {
let call_params = DisplacedParams {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 95.0_f64,
rate: 0.05_f64,
vol: 0.25_f64,
time: 1.0_f64,
displacement: 30.0_f64,
};
let put_params = DisplacedParams {
option_type: OptionType::Put,
..call_params
};
let c = price(&call_params).unwrap();
let p = price(&put_params).unwrap();
let discount = (-0.05_f64 * 1.0_f64).exp();
let parity = discount * (100.0_f64 - 95.0_f64);
assert!(
(c - p - parity).abs() < LOOSE,
"put-call parity violated: C-P={}, parity={parity}",
c - p
);
}
#[test]
fn test_negative_vol_error() {
let params = DisplacedParams {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: -0.20_f64,
time: 1.0_f64,
displacement: 0.0_f64,
};
assert_eq!(price(¶ms), Err(PricingError::NegativeVolatility));
}
#[test]
fn test_negative_time_error() {
let params = DisplacedParams {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: -1.0_f64,
displacement: 0.0_f64,
};
assert_eq!(price(¶ms), Err(PricingError::NegativeTime));
}
#[test]
fn test_negative_shifted_forward_error() {
let params = DisplacedParams {
option_type: OptionType::Call,
forward: 10.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
displacement: -20.0_f64,
};
assert_eq!(price(¶ms), Err(PricingError::NegativeSpot));
}
#[test]
fn test_negative_shifted_strike_error() {
let params = DisplacedParams {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 10.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
displacement: -20.0_f64,
};
assert_eq!(price(¶ms), Err(PricingError::NegativeStrike));
}
#[test]
fn test_zero_time_intrinsic_call_itm() {
let params = DisplacedParams {
option_type: OptionType::Call,
forward: 110.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 0.0_f64,
displacement: 10.0_f64,
};
match price(¶ms) {
Err(PricingError::IntrinsicOnly { intrinsic }) => {
assert!(
(intrinsic - 10.0_f64).abs() < LOOSE,
"expected intrinsic ~10.0, got {intrinsic}"
);
}
other => panic!("expected IntrinsicOnly, got {other:?}"),
}
}
#[test]
fn test_zero_time_intrinsic_put_otm() {
let params = DisplacedParams {
option_type: OptionType::Put,
forward: 110.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 0.0_f64,
displacement: 10.0_f64,
};
match price(¶ms) {
Err(PricingError::IntrinsicOnly { intrinsic }) => {
assert!(
intrinsic.abs() < LOOSE,
"expected intrinsic ~0.0, got {intrinsic}"
);
}
other => panic!("expected IntrinsicOnly, got {other:?}"),
}
}
#[test]
fn test_zero_vol_intrinsic() {
let params = DisplacedParams {
option_type: OptionType::Call,
forward: 110.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.0_f64,
time: 1.0_f64,
displacement: 10.0_f64,
};
match price(¶ms) {
Ok(p) => {
let discount = (-0.05_f64).exp();
let expected = discount * 10.0_f64;
assert!(
(p - expected).abs() < LOOSE,
"expected intrinsic ~{expected}, got {p}"
);
}
other => panic!("expected Ok(intrinsic), got {other:?}"),
}
}
#[test]
fn test_large_displacement() {
let params = DisplacedParams {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
displacement: 1000.0_f64,
};
let p = price(¶ms).unwrap();
assert!(p.is_finite(), "price should be finite, got {p}");
assert!(p > 0.0_f64, "price should be positive, got {p}");
}
#[test]
fn test_negative_rate() {
let params = DisplacedParams {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: -0.01_f64,
vol: 0.20_f64,
time: 1.0_f64,
displacement: 20.0_f64,
};
let p = price(¶ms).unwrap();
assert!(p.is_finite(), "price should be finite with negative rate");
assert!(p > 0.0_f64, "ATM call should be positive");
}
}