#![allow(clippy::indexing_slicing)]
use crate::Options;
use crate::error::PricingError;
use crate::greeks::big_n;
use crate::model::decimal::{d_add, d_mul, d_sub, finite_decimal};
use crate::model::types::OptionType;
use num_traits::Inv;
use rust_decimal::Decimal;
use rust_decimal::prelude::*;
use rust_decimal_macros::dec;
pub fn cliquet_black_scholes(option: &Options) -> Result<Decimal, PricingError> {
match &option.option_type {
OptionType::Cliquet { reset_dates } => price_cliquet(option, reset_dates),
_ => Err(PricingError::other(
"cliquet_black_scholes requires OptionType::Cliquet",
)),
}
}
fn price_cliquet(option: &Options, reset_dates: &[f64]) -> Result<Decimal, PricingError> {
let (local_cap, local_floor) = if let Some(ref params) = option.exotic_params {
(
params.cliquet_local_cap.unwrap_or(dec!(0.1)), params.cliquet_local_floor.unwrap_or(dec!(0.0)), )
} else {
(dec!(0.1), dec!(0.0))
};
if reset_dates.iter().any(|d| d.is_nan()) {
return Err(PricingError::method_error(
"cliquet_black_scholes",
"reset_dates contains NaN",
));
}
let mut dates = reset_dates.to_vec();
dates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let t_total = option
.expiration_date
.get_years()
.map_err(|e| PricingError::other(&e.to_string()))?;
let t_total_f = t_total.to_f64();
let mut reset_times_years = vec![0.0]; for &d in &dates {
let t = d / 365.0;
if t > 0.0 && t < t_total_f {
reset_times_years.push(t);
}
}
reset_times_years.push(t_total_f);
reset_times_years.dedup();
let mut total_price = dec!(0.0);
for i in 1..reset_times_years.len() {
let t_prev = reset_times_years[i - 1];
let t_curr = reset_times_years[i];
let dt = t_curr - t_prev;
let delta_price = price_period(option, t_prev, dt, local_cap, local_floor)?;
total_price = d_add(total_price, delta_price, "pricing::cliquet::total")?;
}
if let Some(ref params) = option.exotic_params {
if let Some(g_cap) = params.cliquet_global_cap {
total_price = total_price.min(g_cap);
}
if let Some(g_floor) = params.cliquet_global_floor {
total_price = total_price.max(g_floor);
}
}
Ok(apply_side(total_price, option))
}
fn price_period(
option: &Options,
t_start: f64,
dt: f64,
cap: Decimal,
floor: Decimal,
) -> Result<Decimal, PricingError> {
if dt <= 0.0 {
return Ok(dec!(0.0));
}
let s0 = option.underlying_price.to_dec();
let r = option.risk_free_rate;
let q = option.dividend_yield.to_dec();
let sigma = option.implied_volatility.to_dec();
let dt_dec = finite_decimal(dt)
.ok_or_else(|| PricingError::non_finite("pricing::cliquet::price_period::dt", dt))?;
let t_start_dec = finite_decimal(t_start).ok_or_else(|| {
PricingError::non_finite("pricing::cliquet::price_period::t_start", t_start)
})?;
let s_prev_pv = s0 * (-q * t_start_dec).exp();
let call_f = call_price_on_unit(r, q, sigma, dt_dec, dec!(1.0) + floor)?;
let call_c = call_price_on_unit(r, q, sigma, dt_dec, dec!(1.0) + cap)?;
let floor_part = floor * (-r * dt_dec).exp();
let step = d_add(
floor_part,
call_f,
"pricing::cliquet::period::floor_plus_call_f",
)?;
let period_val_at_t_prev = d_sub(step, call_c, "pricing::cliquet::period::value")?;
d_mul(
s_prev_pv,
period_val_at_t_prev,
"pricing::cliquet::period::price",
)
.map_err(PricingError::from)
}
fn call_price_on_unit(
r: Decimal,
q: Decimal,
sigma: Decimal,
t: Decimal,
k: Decimal,
) -> Result<Decimal, PricingError> {
if k <= dec!(0.0) {
let s_pv = (-q * t).exp();
let k_pv = d_mul(k, (-r * t).exp(), "pricing::cliquet::unit_call::itm::k_pv")?;
return d_sub(s_pv, k_pv, "pricing::cliquet::unit_call::itm::price")
.map_err(PricingError::from);
}
if sigma == dec!(0.0) || t == dec!(0.0) {
let forward = ((r - q) * t).exp();
let intrinsic = d_sub(
forward,
k,
"pricing::cliquet::unit_call::zero_vol::intrinsic",
)?
.max(dec!(0.0));
return d_mul(
intrinsic,
(-r * t).exp(),
"pricing::cliquet::unit_call::zero_vol::discounted",
)
.map_err(PricingError::from);
}
let sqrt_t = t.sqrt().unwrap_or(dec!(0.0));
let b = r - q;
let d1 = (k.inv().ln() + (b + sigma * sigma / dec!(2.0)) * t) / (sigma * sqrt_t);
let d2 = d1 - sigma * sqrt_t;
let n1 = big_n(d1).unwrap_or(dec!(0.0));
let n2 = big_n(d2).unwrap_or(dec!(0.0));
let s_leg = d_mul((-q * t).exp(), n1, "pricing::cliquet::unit_call::s_leg")?;
let discounted_k = d_mul(
k,
(-r * t).exp(),
"pricing::cliquet::unit_call::discounted_k",
)?;
let k_leg = d_mul(discounted_k, n2, "pricing::cliquet::unit_call::k_leg")?;
d_sub(s_leg, k_leg, "pricing::cliquet::unit_call::price").map_err(PricingError::from)
}
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::model::option::ExoticParams;
use crate::model::types::{OptionStyle, Side};
use positive::{Positive, pos_or_panic};
use rust_decimal_macros::dec;
fn create_cliquet_option() -> Options {
Options::new(
OptionType::Cliquet {
reset_dates: vec![90.0, 180.0],
},
Side::Long,
"TEST".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(pos_or_panic!(270.0)),
pos_or_panic!(0.2),
Positive::ONE,
Positive::HUNDRED,
dec!(0.05),
OptionStyle::Call,
Positive::ZERO,
Some(ExoticParams {
spot_prices: None,
spot_min: None,
spot_max: None,
cliquet_local_cap: Some(dec!(0.05)),
cliquet_local_floor: Some(dec!(0.0)),
cliquet_global_cap: None,
cliquet_global_floor: None,
rainbow_second_asset_price: None,
rainbow_second_asset_volatility: None,
rainbow_second_asset_dividend: None,
rainbow_correlation: None,
spread_second_asset_volatility: None,
spread_second_asset_dividend: None,
spread_correlation: None,
quanto_fx_volatility: None,
quanto_fx_correlation: None,
quanto_foreign_rate: None,
exchange_second_asset_volatility: None,
exchange_second_asset_dividend: None,
exchange_correlation: None,
}),
)
}
#[test]
fn test_cliquet_pricing() {
let option = create_cliquet_option();
let price = cliquet_black_scholes(&option).unwrap();
assert!(price > dec!(0.0));
}
#[test]
fn test_cliquet_zero_vol() {
let mut option = create_cliquet_option();
option.implied_volatility = Positive::ZERO;
let price = cliquet_black_scholes(&option).unwrap();
assert!(price > dec!(0.0));
}
#[test]
fn test_cliquet_high_cap() {
let mut option = create_cliquet_option();
if let Some(ref mut params) = option.exotic_params {
params.cliquet_local_cap = Some(dec!(1.0)); }
let price_high = cliquet_black_scholes(&option).unwrap();
if let Some(ref mut params) = option.exotic_params {
params.cliquet_local_cap = Some(dec!(0.01)); }
let price_low = cliquet_black_scholes(&option).unwrap();
assert!(price_high > price_low);
}
}