use crate::Options;
use crate::error::PricingError;
use crate::greeks::big_n;
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))
};
let mut dates = reset_dates.to_vec();
dates.sort_by(|a, b| a.partial_cmp(b).unwrap());
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 += delta_price;
}
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 = Decimal::from_f64(dt).unwrap();
let t_start_dec = Decimal::from_f64(t_start).unwrap();
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 period_val_at_t_prev = floor_part + call_f - call_c;
Ok(s_prev_pv * period_val_at_t_prev)
}
fn call_price_on_unit(
r: Decimal,
q: Decimal,
sigma: Decimal,
t: Decimal,
k: Decimal,
) -> Result<Decimal, PricingError> {
if k <= dec!(0.0) {
return Ok((-q * t).exp() - k * (-r * t).exp());
}
if sigma == dec!(0.0) || t == dec!(0.0) {
let forward = ((r - q) * t).exp();
return Ok((forward - k).max(dec!(0.0)) * (-r * t).exp());
}
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));
Ok((-q * t).exp() * n1 - k * (-r * t).exp() * n2)
}
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);
}
}