use crate::Options;
use crate::error::PricingError;
use crate::greeks::{big_n, d1, d2};
use crate::model::decimal::{d_add, d_mul, d_sub};
use crate::model::types::{LookbackType, OptionStyle, OptionType};
use positive::Positive;
use rust_decimal::Decimal;
use rust_decimal::prelude::*;
use rust_decimal_macros::dec;
pub fn lookback_black_scholes(option: &Options) -> Result<Decimal, PricingError> {
match &option.option_type {
OptionType::Lookback { lookback_type } => match lookback_type {
LookbackType::FloatingStrike => floating_strike_lookback(option),
LookbackType::FixedStrike => fixed_strike_lookback(option),
},
_ => Err(PricingError::other(
"lookback_black_scholes requires OptionType::Lookback",
)),
}
}
fn floating_strike_lookback(option: &Options) -> Result<Decimal, PricingError> {
let s = option.underlying_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(Decimal::ZERO);
}
if sigma == Positive::ZERO {
let discount = (-r * t).exp();
let forward = s * ((r - q) * t).exp();
let value = match option.option_style {
OptionStyle::Call => (forward - s).max(Positive::ZERO).to_dec() * discount,
OptionStyle::Put => (s - forward).max(Positive::ZERO).to_dec() * discount,
};
return Ok(apply_side(value, option));
}
let b = r - q; let sigma_sq = sigma * sigma;
let t_dec = t.to_dec();
let sqrt_t = t_dec.sqrt().unwrap_or(Decimal::ZERO);
let price = match option.option_style {
OptionStyle::Call => {
if b.abs() < dec!(1e-10) {
let a1 = sigma.to_dec() * sqrt_t / dec!(2);
let n_a1 = big_n(a1).unwrap_or(Decimal::ZERO);
let n_neg_a1 = big_n(-a1).unwrap_or(Decimal::ZERO);
s.to_dec() * (dec!(2) * n_a1 - dec!(1))
+ s.to_dec()
* sigma.to_dec()
* sqrt_t
* (dec!(2) * n_a1 - dec!(1)
+ dec!(2) / (dec!(2.506628274631) * dec!(1))
* (a1 * n_neg_a1).exp().min(dec!(10)))
.min(s.to_dec())
} else {
let a1 = ((b + sigma_sq / dec!(2)) * t_dec) / (sigma.to_dec() * sqrt_t);
let a2 = a1 - sigma.to_dec() * sqrt_t;
let n_a1 = big_n(a1).unwrap_or(Decimal::ZERO);
let n_a2 = big_n(a2).unwrap_or(Decimal::ZERO);
let n_neg_a1 = big_n(-a1).unwrap_or(Decimal::ZERO);
let dividend_discount = (-q * t).exp();
let discount = (-r * t).exp();
let term1 = s.to_dec() * dividend_discount * n_a1;
let term2 = s.to_dec() * discount * n_a2;
let term3 = s.to_dec()
* discount
* (sigma_sq / (dec!(2) * b))
* (n_a2 - (b * t_dec).exp() * n_neg_a1);
let diff = d_sub(term1, term2, "pricing::lookback::floating::call::diff")?;
d_add(diff, term3, "pricing::lookback::floating::call::price")?
}
}
OptionStyle::Put => {
if b.abs() < dec!(1e-10) {
let a1 = sigma.to_dec() * sqrt_t / dec!(2);
let n_neg_a1 = big_n(-a1).unwrap_or(Decimal::ZERO);
s.to_dec() * (dec!(1) - dec!(2) * n_neg_a1)
+ s.to_dec() * sigma.to_dec() * sqrt_t * dec!(0.5)
} else {
let a1 = ((b + sigma_sq / dec!(2)) * t_dec) / (sigma.to_dec() * sqrt_t);
let a2 = a1 - sigma.to_dec() * sqrt_t;
let n_neg_a1 = big_n(-a1).unwrap_or(Decimal::ZERO);
let n_neg_a2 = big_n(-a2).unwrap_or(Decimal::ZERO);
let n_a1 = big_n(a1).unwrap_or(Decimal::ZERO);
let n_a2 = big_n(a2).unwrap_or(Decimal::ZERO);
let dividend_discount = (-q * t).exp();
let discount = (-r * t).exp();
let term1 = s.to_dec() * discount * n_neg_a2;
let term2 = s.to_dec() * dividend_discount * n_neg_a1;
let term3 = s.to_dec()
* discount
* (sigma_sq / (dec!(2) * b))
* ((b * t_dec).exp() * n_a1 - n_a2);
let diff = d_sub(term1, term2, "pricing::lookback::floating::put::diff")?;
d_add(diff, term3, "pricing::lookback::floating::put::price")?
}
}
};
Ok(apply_side(price.max(Decimal::ZERO), option))
}
fn fixed_strike_lookback(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 intrinsic = match option.option_style {
OptionStyle::Call => (s - k).max(Positive::ZERO).to_dec(),
OptionStyle::Put => (k - s).max(Positive::ZERO).to_dec(),
};
return Ok(apply_side(intrinsic, option));
}
if sigma == Positive::ZERO {
let discount = (-r * t).exp();
let forward = s * ((r - q) * t).exp();
let intrinsic = match option.option_style {
OptionStyle::Call => (forward - k).max(Positive::ZERO).to_dec() * discount,
OptionStyle::Put => (k - forward).max(Positive::ZERO).to_dec() * discount,
};
return Ok(apply_side(intrinsic, option));
}
let b = r - q;
let sigma_sq = sigma * sigma;
let t_dec = t.to_dec();
let sqrt_t = t_dec.sqrt().unwrap_or(Decimal::ZERO);
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 discount = (-r * t).exp();
let dividend_discount = (-q * t).exp();
let price = match option.option_style {
OptionStyle::Call => {
let n_d1 = big_n(d1_val).unwrap_or(Decimal::ZERO);
let n_d2 = big_n(d2_val).unwrap_or(Decimal::ZERO);
let s_leg = d_mul(
s.to_dec() * dividend_discount,
n_d1,
"pricing::lookback::fixed::call::s_leg",
)?;
let k_leg = d_mul(
k.to_dec() * discount,
n_d2,
"pricing::lookback::fixed::call::k_leg",
)?;
let bs_call = d_sub(s_leg, k_leg, "pricing::lookback::fixed::call::bs")?;
let lambda = if b.abs() < dec!(1e-10) {
dec!(1) + sigma_sq * t_dec / dec!(2)
} else {
(b + sigma_sq / dec!(2)) * t_dec / (sigma.to_dec() * sqrt_t)
};
let n_lambda = big_n(lambda).unwrap_or(dec!(0.5));
let lookback_premium =
s.to_dec() * sigma.to_dec() * sqrt_t * (n_lambda - dec!(0.5)) * dec!(0.5);
d_add(
bs_call,
lookback_premium,
"pricing::lookback::fixed::call::price",
)?
.max(Decimal::ZERO)
}
OptionStyle::Put => {
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 k_discounted = d_mul(
k.to_dec(),
discount,
"pricing::lookback::fixed::put::k_discounted",
)?;
let k_leg = d_mul(
k_discounted,
n_neg_d2,
"pricing::lookback::fixed::put::k_leg",
)?;
let s_discounted = d_mul(
s.to_dec(),
dividend_discount,
"pricing::lookback::fixed::put::s_discounted",
)?;
let s_leg = d_mul(
s_discounted,
n_neg_d1,
"pricing::lookback::fixed::put::s_leg",
)?;
let bs_put = d_sub(k_leg, s_leg, "pricing::lookback::fixed::put::bs")?;
let lambda = if b.abs() < dec!(1e-10) {
dec!(1) + sigma_sq * t_dec / dec!(2)
} else {
(b + sigma_sq / dec!(2)) * t_dec / (sigma.to_dec() * sqrt_t)
};
let n_lambda = big_n(lambda).unwrap_or(dec!(0.5));
let lookback_premium =
s.to_dec() * sigma.to_dec() * sqrt_t * (n_lambda - dec!(0.5)) * dec!(0.5);
d_add(
bs_put,
lookback_premium,
"pricing::lookback::fixed::put::price",
)?
.max(Decimal::ZERO)
}
};
Ok(apply_side(price, 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_lookback_option(style: OptionStyle, lookback_type: LookbackType) -> Options {
Options::new(
OptionType::Lookback { lookback_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_floating_strike_call() {
let option = create_lookback_option(OptionStyle::Call, LookbackType::FloatingStrike);
let price = lookback_black_scholes(&option).unwrap();
assert!(
price > Decimal::ZERO,
"Floating strike call should be positive: {}",
price
);
}
#[test]
fn test_floating_strike_put() {
let option = create_lookback_option(OptionStyle::Put, LookbackType::FloatingStrike);
let price = lookback_black_scholes(&option).unwrap();
assert!(
price > Decimal::ZERO,
"Floating strike put should be positive: {}",
price
);
}
#[test]
fn test_fixed_strike_call() {
let option = create_lookback_option(OptionStyle::Call, LookbackType::FixedStrike);
let price = lookback_black_scholes(&option).unwrap();
assert!(
price > Decimal::ZERO,
"Fixed strike call should be positive: {}",
price
);
}
#[test]
fn test_fixed_strike_put() {
let option = create_lookback_option(OptionStyle::Put, LookbackType::FixedStrike);
let price = lookback_black_scholes(&option).unwrap();
assert!(
price > Decimal::ZERO,
"Fixed strike put should be positive: {}",
price
);
}
#[test]
fn test_lookback_more_expensive_than_vanilla() {
let lookback = create_lookback_option(OptionStyle::Call, LookbackType::FixedStrike);
let lookback_price = lookback_black_scholes(&lookback).unwrap();
assert!(
lookback_price > dec!(7.0),
"Lookback should be at least as expensive as vanilla: {}",
lookback_price
);
}
#[test]
fn test_short_lookback_option() {
let mut option = create_lookback_option(OptionStyle::Call, LookbackType::FloatingStrike);
let long_price = lookback_black_scholes(&option).unwrap();
option.side = Side::Short;
let short_price = lookback_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_lookback_option(OptionStyle::Call, LookbackType::FloatingStrike);
option.expiration_date = ExpirationDate::Days(Positive::ZERO);
let price = lookback_black_scholes(&option).unwrap();
assert_decimal_eq!(price, Decimal::ZERO, dec!(1e-10));
}
#[test]
fn test_fixed_strike_itm_at_expiry() {
let mut option = create_lookback_option(OptionStyle::Call, LookbackType::FixedStrike);
option.underlying_price = pos_or_panic!(110.0); option.expiration_date = ExpirationDate::Days(Positive::ZERO);
let price = lookback_black_scholes(&option).unwrap();
assert_decimal_eq!(price, dec!(10.0), dec!(1e-10));
}
#[test]
fn test_higher_vol_means_higher_lookback_value() {
let low_vol = create_lookback_option(OptionStyle::Call, LookbackType::FloatingStrike);
let low_vol_price = lookback_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 = lookback_black_scholes(&high_vol).unwrap();
assert!(
high_vol_price > low_vol_price,
"Higher vol should mean higher lookback value: {} vs {}",
high_vol_price,
low_vol_price
);
}
#[test]
fn test_floating_strike_symmetry() {
let call = create_lookback_option(OptionStyle::Call, LookbackType::FloatingStrike);
let put = create_lookback_option(OptionStyle::Put, LookbackType::FloatingStrike);
let call_price = lookback_black_scholes(&call).unwrap();
let put_price = lookback_black_scholes(&put).unwrap();
let ratio = if call_price > put_price {
call_price / put_price
} else {
put_price / call_price
};
assert!(
ratio < dec!(2.0),
"Call and put should be similar for ATM: call={}, put={}",
call_price,
put_price
);
}
}