use crate::constants::ZERO;
use crate::error::{OptionsError, OptionsResult};
use crate::model::decimal::{decimal_to_f64, finite_decimal};
use crate::model::option::ExoticParams;
use crate::model::types::{
AsianAveragingType, BarrierType, BinaryType, LookbackType, OptionStyle, OptionType,
RainbowType, Side,
};
use positive::Positive;
use rust_decimal::Decimal;
pub trait Payoff {
fn payoff(&self, info: &PayoffInfo) -> OptionsResult<Decimal>;
}
#[derive(Debug, Clone)]
pub struct PayoffInfo {
pub spot: Positive,
pub strike: Positive,
pub style: OptionStyle,
pub side: Side,
pub spot_prices: Option<Vec<Positive>>, pub spot_min: Option<Positive>, pub spot_max: Option<Positive>, pub exotic_params: Option<ExoticParams>,
}
impl Default for PayoffInfo {
fn default() -> Self {
PayoffInfo {
spot: Positive::ZERO,
strike: Positive::ZERO,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: None,
}
}
}
impl PayoffInfo {
#[inline]
#[must_use]
pub fn spot_prices_len(&self) -> Option<usize> {
self.spot_prices.as_ref().map(|vec| vec.len())
}
}
#[inline]
fn intrinsic(spot: Decimal, strike: Decimal, style: OptionStyle) -> Decimal {
let moneyness = match style {
OptionStyle::Call => spot.checked_sub(strike),
OptionStyle::Put => strike.checked_sub(spot),
};
moneyness.unwrap_or(Decimal::ZERO).max(Decimal::ZERO)
}
#[inline]
fn vanilla(info: &PayoffInfo) -> Decimal {
intrinsic(info.spot.to_dec(), info.strike.to_dec(), info.style)
}
#[inline]
fn signed(value: Decimal, side: Side) -> Decimal {
match side {
Side::Short if !value.is_zero() => -value,
_ => value,
}
}
impl Payoff for OptionType {
fn payoff(&self, info: &PayoffInfo) -> OptionsResult<Decimal> {
Ok(signed(long_payoff(self, info)?, info.side).normalize())
}
}
#[cold]
#[inline(never)]
fn payoff_not_representable(value: f64) -> OptionsError {
OptionsError::PayoffError {
reason: format!(
"payoff {value} is not representable as a Decimal (non-finite or out of range)"
),
}
}
#[cold]
#[inline(never)]
fn payoff_missing(reason: &str) -> OptionsError {
OptionsError::PayoffError {
reason: reason.to_string(),
}
}
#[cold]
#[inline(never)]
fn payoff_overflow(operation: &str) -> OptionsError {
OptionsError::PayoffError {
reason: format!("{operation} left the Decimal range"),
}
}
fn from_kernel(value: f64) -> OptionsResult<Decimal> {
finite_decimal(value).ok_or_else(|| payoff_not_representable(value))
}
fn long_payoff(option_type: &OptionType, info: &PayoffInfo) -> OptionsResult<Decimal> {
match option_type {
OptionType::European | OptionType::American | OptionType::Bermuda { .. } => {
Ok(vanilla(info))
}
OptionType::Asian { averaging_type } => asian_payoff(averaging_type, info),
OptionType::Barrier {
barrier_type,
barrier_level,
rebate,
} => Ok(barrier_payoff(barrier_type, barrier_level, rebate, info)),
OptionType::Binary { binary_type } => Ok(binary_payoff(binary_type, info)),
OptionType::Lookback {
lookback_type: LookbackType::FloatingStrike,
} => Ok(floating_strike_payoff(info)),
OptionType::Lookback { .. } => Ok(vanilla(info)),
OptionType::Compound { underlying_option } => {
let underlying = long_payoff(underlying_option, info)?;
Ok(intrinsic(underlying, info.strike.to_dec(), info.style))
}
OptionType::Chooser { .. } => {
Ok(
intrinsic(info.spot.to_dec(), info.strike.to_dec(), OptionStyle::Call).max(
intrinsic(info.spot.to_dec(), info.strike.to_dec(), OptionStyle::Put),
),
)
}
OptionType::Cliquet { .. } => Ok(cliquet_payoff(info)),
OptionType::Rainbow {
num_assets,
rainbow_type,
} => rainbow_payoff(*num_assets, rainbow_type, info),
OptionType::Spread { second_asset } => {
let spread = info
.spot
.to_dec()
.checked_sub(second_asset.to_dec())
.ok_or_else(|| payoff_overflow("spread S1 - S2"))?;
let strike = info.strike.to_dec();
let value = match info.style {
OptionStyle::Call => spread.checked_sub(strike),
OptionStyle::Put => strike.checked_sub(spread),
}
.ok_or_else(|| payoff_overflow("spread payoff"))?;
Ok(value.max(Decimal::ZERO))
}
OptionType::Exchange { second_asset } => Ok(intrinsic(
info.spot.to_dec(),
second_asset.to_dec(),
OptionStyle::Call,
)),
OptionType::Quanto { exchange_rate } => vanilla(info)
.checked_mul(exchange_rate.to_dec())
.ok_or_else(|| payoff_overflow("quanto payoff times the exchange rate")),
OptionType::Power { exponent } => power_payoff(*exponent, info),
_ => Ok(vanilla(info)),
}
}
fn asian_payoff(averaging_type: &AsianAveragingType, info: &PayoffInfo) -> OptionsResult<Decimal> {
let fixings = match info.spot_prices.as_deref() {
Some(fixings) if !fixings.is_empty() => fixings,
_ => return Ok(vanilla(info)),
};
let average = match averaging_type {
AsianAveragingType::Geometric => geometric_mean(fixings)?,
_ => {
let mut total = ZERO;
for fixing in fixings {
total += decimal_to_f64(fixing.to_dec())?;
}
total / fixings.len() as f64
}
};
match info.style {
OptionStyle::Call => {
from_kernel((average - decimal_to_f64(info.strike.to_dec())?).max(ZERO))
}
OptionStyle::Put => {
let average = from_kernel(average)?;
Ok(intrinsic(average, info.strike.to_dec(), OptionStyle::Put))
}
}
}
fn geometric_mean(fixings: &[Positive]) -> OptionsResult<f64> {
let Some(first) = fixings.first() else {
return Ok(ZERO);
};
if fixings.contains(&Positive::ZERO) {
return Ok(ZERO);
}
let centre = decimal_to_f64(first.to_dec())?;
let mut log_sum = ZERO;
for fixing in fixings {
log_sum += (decimal_to_f64(fixing.to_dec())? / centre).ln(); }
Ok(centre * (log_sum / fixings.len() as f64).exp()) }
fn barrier_payoff(
barrier_type: &BarrierType,
barrier_level: &Positive,
rebate: &Option<Positive>,
info: &PayoffInfo,
) -> Decimal {
let hit = match barrier_type {
BarrierType::UpAndIn | BarrierType::UpAndOut => {
info.spot_max.unwrap_or(info.spot) >= *barrier_level
}
BarrierType::DownAndIn | BarrierType::DownAndOut => {
info.spot_min.unwrap_or(info.spot) <= *barrier_level
}
_ => false,
};
let rebate = rebate.map_or(Decimal::ZERO, |amount| amount.to_dec());
match barrier_type {
BarrierType::UpAndIn | BarrierType::DownAndIn if !hit => rebate,
BarrierType::UpAndOut | BarrierType::DownAndOut if hit => rebate,
_ => vanilla(info),
}
}
fn binary_payoff(binary_type: &BinaryType, info: &PayoffInfo) -> Decimal {
let in_the_money = match info.style {
OptionStyle::Call => info.spot > info.strike,
OptionStyle::Put => info.spot < info.strike,
};
if !in_the_money {
return Decimal::ZERO;
}
match binary_type {
BinaryType::AssetOrNothing => info.spot.to_dec(),
BinaryType::Gap => vanilla(info),
_ => Decimal::ONE,
}
}
fn floating_strike_payoff(info: &PayoffInfo) -> Decimal {
let spot = info.spot.to_dec();
match info.style {
OptionStyle::Call => {
let low = info.spot_min.map_or(spot, |low| low.to_dec().min(spot));
spot.checked_sub(low).unwrap_or(Decimal::ZERO)
}
OptionStyle::Put => {
let high = info.spot_max.map_or(spot, |high| high.to_dec().max(spot));
high.checked_sub(spot).unwrap_or(Decimal::ZERO)
}
}
}
fn cliquet_payoff(info: &PayoffInfo) -> Decimal {
let mut total = Decimal::ZERO;
if let Some(params) = &info.exotic_params {
if let Some(cap) = params.cliquet_global_cap {
total = total.min(cap);
}
if let Some(floor) = params.cliquet_global_floor {
total = total.max(floor);
}
}
total
}
fn rainbow_payoff(
num_assets: usize,
rainbow_type: &RainbowType,
info: &PayoffInfo,
) -> OptionsResult<Decimal> {
if num_assets != 2 {
return Err(payoff_missing(
"a rainbow payoff is defined on two assets only",
));
}
let second = info
.exotic_params
.as_ref()
.and_then(|params| params.rainbow_second_asset_price)
.ok_or_else(|| {
payoff_missing("a rainbow payoff needs exotic_params.rainbow_second_asset_price")
})?
.to_dec();
let first = info.spot.to_dec();
let reference = match rainbow_type {
RainbowType::BestOf => first.max(second),
RainbowType::WorstOf => first.min(second),
_ => return Err(payoff_missing("unsupported rainbow type")),
};
Ok(intrinsic(reference, info.strike.to_dec(), info.style))
}
fn power_payoff(exponent: Positive, info: &PayoffInfo) -> OptionsResult<Decimal> {
let powered = decimal_to_f64(info.spot.to_dec())?.powf(decimal_to_f64(exponent.to_dec())?);
match (finite_decimal(powered), info.style) {
(Some(powered), style) => Ok(intrinsic(powered, info.strike.to_dec(), style)),
(None, OptionStyle::Put) => Ok(Decimal::ZERO),
(None, OptionStyle::Call) => Err(payoff_not_representable(powered)),
}
}
#[cfg(test)]
mod tests_payoff {
use super::*;
use positive::{Positive, pos_or_panic};
use rust_decimal_macros::dec;
#[test]
fn test_european_call() {
let option = OptionType::European;
let info = PayoffInfo {
spot: pos_or_panic!(110.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(10));
}
#[test]
fn test_european_put() {
let option = OptionType::European;
let info = PayoffInfo {
spot: pos_or_panic!(90.0),
strike: Positive::HUNDRED,
style: OptionStyle::Put,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(10));
}
#[test]
fn test_asian_arithmetic_call() {
let option = OptionType::Asian {
averaging_type: AsianAveragingType::Arithmetic,
};
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: Some(vec![
pos_or_panic!(90.0),
pos_or_panic!(100.0),
pos_or_panic!(110.0),
]),
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), Decimal::ZERO);
}
#[test]
fn test_barrier_up_and_in_call() {
let option = OptionType::Barrier {
barrier_type: BarrierType::UpAndIn,
barrier_level: pos_or_panic!(120.0),
rebate: None,
};
let info = PayoffInfo {
spot: pos_or_panic!(130.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(30));
}
#[test]
fn test_binary_cash_or_nothing_call() {
let option = OptionType::Binary {
binary_type: BinaryType::CashOrNothing,
};
let info = PayoffInfo {
spot: pos_or_panic!(110.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(1));
}
#[test]
fn test_lookback_fixed_strike_put() {
let option = OptionType::Lookback {
lookback_type: LookbackType::FixedStrike,
};
let info = PayoffInfo {
spot: pos_or_panic!(90.0),
strike: Positive::HUNDRED,
style: OptionStyle::Put,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(10));
}
#[test]
fn test_quanto_call() {
let option = OptionType::Quanto {
exchange_rate: pos_or_panic!(1.5),
};
let info = PayoffInfo {
spot: pos_or_panic!(110.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(15));
}
#[test]
fn test_power_call() {
let option = OptionType::Power {
exponent: pos_or_panic!(2.0),
};
let info = PayoffInfo {
spot: pos_or_panic!(10.0),
strike: pos_or_panic!(90.0),
style: OptionStyle::Call,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(10));
}
#[test]
fn test_chooser_out_of_the_money_call_returns_put_intrinsic() {
let option = OptionType::Chooser {
choice_date: Positive::ONE,
};
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: pos_or_panic!(110.0),
style: OptionStyle::Call,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(10));
}
#[test]
fn test_chooser_in_the_money_call_keeps_its_value() {
let option = OptionType::Chooser {
choice_date: Positive::ONE,
};
let info = PayoffInfo {
spot: pos_or_panic!(110.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(10));
}
#[test]
fn test_chooser_at_the_money_is_zero() {
let option = OptionType::Chooser {
choice_date: Positive::ONE,
};
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), Decimal::ZERO);
}
#[test]
fn test_power_put_out_of_the_money_is_zero() {
let option = OptionType::Power {
exponent: pos_or_panic!(2.0),
};
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: pos_or_panic!(110.0),
style: OptionStyle::Put,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), Decimal::ZERO);
}
#[test]
fn test_power_put_in_the_money_keeps_its_value() {
let option = OptionType::Power {
exponent: pos_or_panic!(2.0),
};
let info = PayoffInfo {
spot: pos_or_panic!(10.0),
strike: pos_or_panic!(110.0),
style: OptionStyle::Put,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(10));
}
#[test]
fn test_power_put_at_the_money_is_zero() {
let option = OptionType::Power {
exponent: pos_or_panic!(2.0),
};
let info = PayoffInfo {
spot: pos_or_panic!(10.0),
strike: Positive::HUNDRED,
style: OptionStyle::Put,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), Decimal::ZERO);
}
#[test]
fn test_power_put_unrepresentable_power_is_zero() {
let option = OptionType::Power {
exponent: Positive::MAX,
};
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::HUNDRED,
style: OptionStyle::Put,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), Decimal::ZERO);
}
}
#[cfg(test)]
mod tests_calculate_floating_strike_payoff {
use super::*;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
#[test]
fn test_call_option_with_spot_min() {
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::ZERO, style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
spot_min: Some(pos_or_panic!(80.0)),
spot_max: None,
exotic_params: None,
};
assert_eq!(floating_strike_payoff(&info), dec!(20));
}
#[test]
fn test_call_option_without_spot_min() {
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::ZERO,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: None,
};
assert_eq!(floating_strike_payoff(&info), Decimal::ZERO);
}
#[test]
fn test_put_option_with_spot_max() {
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::ZERO,
style: OptionStyle::Put,
side: Side::Long,
spot_prices: None,
spot_min: None,
spot_max: Some(pos_or_panic!(120.0)),
exotic_params: None,
};
assert_eq!(floating_strike_payoff(&info), dec!(20));
}
#[test]
fn test_put_option_without_spot_max() {
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::ZERO,
style: OptionStyle::Put,
side: Side::Long,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: None,
};
assert_eq!(floating_strike_payoff(&info), Decimal::ZERO);
}
#[test]
fn test_call_option_spot_equals_min() {
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::ZERO,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
spot_min: Some(pos_or_panic!(100.0)),
spot_max: None,
exotic_params: None,
};
assert_eq!(floating_strike_payoff(&info), Decimal::ZERO);
}
#[test]
fn test_put_option_spot_equals_max() {
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::ZERO,
style: OptionStyle::Put,
side: Side::Long,
spot_prices: None,
spot_min: None,
spot_max: Some(pos_or_panic!(100.0)),
exotic_params: None,
};
assert_eq!(floating_strike_payoff(&info), Decimal::ZERO);
}
}
#[cfg(test)]
mod test_asian_options {
use crate::model::types::AsianAveragingType;
use crate::model::{OptionStyle, OptionType, Side};
use positive::{Positive, pos_or_panic};
use rust_decimal_macros::dec;
use crate::model::payoff::{Payoff, PayoffInfo};
#[test]
fn test_asian_arithmetic_put() {
let option = OptionType::Asian {
averaging_type: AsianAveragingType::Arithmetic,
};
let info = PayoffInfo {
spot: pos_or_panic!(90.0),
strike: Positive::HUNDRED,
style: OptionStyle::Put,
side: Side::Long,
spot_prices: Some(vec![
pos_or_panic!(85.0),
pos_or_panic!(90.0),
pos_or_panic!(95.0),
]),
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(10));
}
#[test]
fn test_asian_no_spot_prices() {
let option = OptionType::Asian {
averaging_type: AsianAveragingType::Arithmetic,
};
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(0));
}
}
#[cfg(test)]
mod test_barrier_options {
use crate::model::types::BarrierType;
use crate::model::{OptionStyle, OptionType, Side};
use positive::{Positive, pos_or_panic};
use rust_decimal_macros::dec;
use crate::model::payoff::{Payoff, PayoffInfo};
#[test]
fn test_barrier_down_and_in_put() {
let option = OptionType::Barrier {
barrier_type: BarrierType::DownAndIn,
barrier_level: pos_or_panic!(110.0),
rebate: None,
};
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(0));
}
#[test]
fn test_barrier_up_and_out_call() {
let option = OptionType::Barrier {
barrier_type: BarrierType::UpAndOut,
barrier_level: pos_or_panic!(110.0),
rebate: None,
};
let info = PayoffInfo {
spot: pos_or_panic!(120.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(0));
}
}
#[cfg(test)]
mod test_cliquet_options {
use crate::model::{OptionStyle, OptionType, Side};
use positive::{Positive, pos_or_panic};
use rust_decimal_macros::dec;
use crate::model::option::ExoticParams;
use crate::model::payoff::{Payoff, PayoffInfo};
use rust_decimal::Decimal;
#[test]
fn test_cliquet_option_with_resets() {
let option = OptionType::Cliquet {
reset_dates: vec![
pos_or_panic!(30.0),
pos_or_panic!(60.0),
pos_or_panic!(90.0),
],
};
let info = PayoffInfo {
spot: pos_or_panic!(120.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), Decimal::ZERO);
let floored = PayoffInfo {
exotic_params: Some(ExoticParams {
cliquet_global_floor: Some(dec!(2)),
..ExoticParams::default()
}),
..info.clone()
};
assert_eq!(option.payoff(&floored).unwrap(), dec!(2));
let capped = PayoffInfo {
exotic_params: Some(ExoticParams {
cliquet_global_cap: Some(dec!(-1)),
..ExoticParams::default()
}),
..info
};
assert_eq!(option.payoff(&capped).unwrap(), dec!(-1));
}
}
#[cfg(test)]
mod test_rainbow_options {
use crate::error::OptionsError;
use crate::model::option::ExoticParams;
use crate::model::{OptionStyle, OptionType, RainbowType, Side};
use positive::{Positive, pos_or_panic};
use rust_decimal_macros::dec;
use crate::model::payoff::{Payoff, PayoffInfo};
fn second_asset(price: f64) -> Option<ExoticParams> {
Some(ExoticParams {
rainbow_second_asset_price: Some(pos_or_panic!(price)),
..ExoticParams::default()
})
}
#[test]
fn test_rainbow_option_best_of() {
let option = OptionType::Rainbow {
num_assets: 2,
rainbow_type: RainbowType::BestOf,
};
let info = PayoffInfo {
spot: pos_or_panic!(120.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
exotic_params: second_asset(130.0),
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(30));
let missing = PayoffInfo {
exotic_params: None,
..info
};
assert!(matches!(
option.payoff(&missing),
Err(OptionsError::PayoffError { .. })
));
}
#[test]
fn test_rainbow_option_worst_of() {
let option = OptionType::Rainbow {
num_assets: 2,
rainbow_type: RainbowType::WorstOf,
};
let info = PayoffInfo {
spot: pos_or_panic!(80.0),
strike: Positive::HUNDRED,
style: OptionStyle::Put,
side: Side::Long,
spot_prices: None,
exotic_params: second_asset(70.0),
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(30));
}
}
#[cfg(test)]
mod test_exchange_options {
use crate::model::{OptionStyle, OptionType, Side};
use positive::{Positive, pos_or_panic};
use rust_decimal_macros::dec;
use crate::model::payoff::{Payoff, PayoffInfo};
#[test]
fn test_exchange_option_positive_diff() {
let option = OptionType::Exchange {
second_asset: pos_or_panic!(90.0),
};
let info = PayoffInfo {
spot: pos_or_panic!(120.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(30));
}
#[test]
fn test_exchange_option_negative_diff() {
let option = OptionType::Exchange {
second_asset: pos_or_panic!(110.0),
};
let info = PayoffInfo {
spot: pos_or_panic!(110.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(0));
}
}
#[cfg(test)]
mod tests_option_type {
use super::*;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
#[test]
fn test_asian_geometric_call() {
let option = OptionType::Asian {
averaging_type: AsianAveragingType::Geometric,
};
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: Some(vec![
pos_or_panic!(90.0),
pos_or_panic!(100.0),
pos_or_panic!(110.0),
]),
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(0));
}
#[test]
fn test_asian_geometric_call_positive_payoff() {
let option = OptionType::Asian {
averaging_type: AsianAveragingType::Geometric,
};
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: pos_or_panic!(95.0),
style: OptionStyle::Call,
side: Side::Long,
spot_prices: Some(vec![
pos_or_panic!(90.0),
pos_or_panic!(100.0),
pos_or_panic!(110.0),
]),
..Default::default()
};
let expected_payoff = dec!(4.67);
assert!((option.payoff(&info).unwrap() - expected_payoff).abs() < dec!(0.01));
}
#[test]
fn test_barrier_down_and_out_put() {
let option = OptionType::Barrier {
barrier_type: BarrierType::DownAndOut,
barrier_level: pos_or_panic!(90.0),
rebate: None,
};
let info = PayoffInfo {
spot: pos_or_panic!(95.0),
strike: Positive::HUNDRED,
style: OptionStyle::Put,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(5));
}
#[test]
fn test_binary_asset_or_nothing_put() {
let option = OptionType::Binary {
binary_type: BinaryType::AssetOrNothing,
};
let info = PayoffInfo {
spot: pos_or_panic!(90.0),
strike: Positive::HUNDRED,
style: OptionStyle::Put,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(90));
}
#[test]
fn test_compound_option() {
let inner_option = OptionType::European;
let option = OptionType::Compound {
underlying_option: Box::new(inner_option),
};
let info = PayoffInfo {
spot: pos_or_panic!(110.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(0));
let cheap = PayoffInfo {
spot: pos_or_panic!(110.0),
strike: pos_or_panic!(4.0),
style: OptionStyle::Call,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&cheap).unwrap(), dec!(102));
}
#[test]
fn test_chooser_option() {
let option = OptionType::Chooser {
choice_date: pos_or_panic!(30.0),
};
let info = PayoffInfo {
spot: pos_or_panic!(110.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(10));
}
#[test]
fn test_power_put() {
let option = OptionType::Power {
exponent: pos_or_panic!(2.0),
};
let info = PayoffInfo {
spot: pos_or_panic!(8.0),
strike: Positive::HUNDRED,
style: OptionStyle::Put,
side: Side::Long,
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), dec!(36));
}
}
#[cfg(test)]
mod tests_standard_payoff {
use super::*;
use crate::model::types::OptionType;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
#[test]
fn test_call_option_in_the_money() {
let option_type = OptionType::European;
let info = PayoffInfo {
spot: pos_or_panic!(110.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: None,
};
assert_eq!(option_type.payoff(&info).unwrap(), dec!(10));
}
#[test]
fn test_call_option_at_the_money() {
let option_type = OptionType::European;
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: None,
};
assert_eq!(option_type.payoff(&info).unwrap(), dec!(0));
}
#[test]
fn test_call_option_out_of_the_money() {
let option_type = OptionType::European;
let info = PayoffInfo {
spot: pos_or_panic!(90.0),
strike: Positive::HUNDRED,
style: OptionStyle::Call,
side: Side::Long,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: None,
};
assert_eq!(option_type.payoff(&info).unwrap(), dec!(0));
}
#[test]
fn test_put_option_in_the_money() {
let option_type = OptionType::European;
let info = PayoffInfo {
spot: pos_or_panic!(90.0),
strike: Positive::HUNDRED,
style: OptionStyle::Put,
side: Side::Long,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: None,
};
assert_eq!(option_type.payoff(&info).unwrap(), dec!(10));
}
#[test]
fn test_put_option_at_the_money() {
let option_type = OptionType::European;
let info = PayoffInfo {
spot: Positive::HUNDRED,
strike: Positive::HUNDRED,
style: OptionStyle::Put,
side: Side::Long,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: None,
};
assert_eq!(option_type.payoff(&info).unwrap(), dec!(0));
}
#[test]
fn test_put_option_out_of_the_money() {
let option_type = OptionType::European;
let info = PayoffInfo {
spot: pos_or_panic!(110.0),
strike: Positive::HUNDRED,
style: OptionStyle::Put,
side: Side::Long,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: None,
};
assert_eq!(option_type.payoff(&info).unwrap(), dec!(0));
}
}
#[cfg(test)]
mod tests_decimal_boundary_equivalence {
use super::*;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
fn info(spot: Positive, strike: Positive, style: OptionStyle, side: Side) -> PayoffInfo {
PayoffInfo {
spot,
strike,
style,
side,
..Default::default()
}
}
#[test]
fn test_payoff_asian_geometric_call_pins_legacy_f64_digits() {
let option = OptionType::Asian {
averaging_type: AsianAveragingType::Geometric,
};
let info = PayoffInfo {
spot_prices: Some(vec![
pos_or_panic!(90.0),
pos_or_panic!(100.0),
pos_or_panic!(110.0),
]),
..info(
Positive::HUNDRED,
pos_or_panic!(95.0),
OptionStyle::Call,
Side::Long,
)
};
assert_eq!(option.payoff(&info).unwrap(), dec!(4.66554934125965));
}
#[test]
fn test_payoff_short_side_negates_long_payoff() {
let option = OptionType::European;
let long = info(
pos_or_panic!(110.0),
Positive::HUNDRED,
OptionStyle::Call,
Side::Long,
);
let short = PayoffInfo {
side: Side::Short,
..long.clone()
};
assert_eq!(option.payoff(&long).unwrap(), dec!(10));
assert_eq!(option.payoff(&short).unwrap(), dec!(-10));
}
#[test]
fn test_payoff_fractional_strike_is_exact() {
let option = OptionType::European;
let info = info(
pos_or_panic!(101.25),
pos_or_panic!(100.5),
OptionStyle::Call,
Side::Long,
);
assert_eq!(option.payoff(&info).unwrap(), dec!(0.75));
}
#[test]
fn test_payoff_up_and_in_barrier_uses_spot_max() {
let option = OptionType::Barrier {
barrier_type: BarrierType::UpAndIn,
barrier_level: pos_or_panic!(120.0),
rebate: None,
};
let touched = PayoffInfo {
spot_max: Some(pos_or_panic!(125.0)),
..info(
pos_or_panic!(110.0),
Positive::HUNDRED,
OptionStyle::Call,
Side::Long,
)
};
let untouched = PayoffInfo {
spot_max: Some(pos_or_panic!(115.0)),
..touched.clone()
};
assert_eq!(option.payoff(&touched).unwrap(), dec!(10));
assert_eq!(option.payoff(&untouched).unwrap(), Decimal::ZERO);
}
#[test]
fn test_payoff_down_and_out_barrier_uses_spot_min_and_pays_rebate() {
let option = OptionType::Barrier {
barrier_type: BarrierType::DownAndOut,
barrier_level: pos_or_panic!(90.0),
rebate: Some(pos_or_panic!(2.5)),
};
let knocked_out = PayoffInfo {
spot_min: Some(pos_or_panic!(85.0)),
..info(
pos_or_panic!(95.0),
Positive::HUNDRED,
OptionStyle::Put,
Side::Long,
)
};
assert_eq!(option.payoff(&knocked_out).unwrap(), dec!(2.5));
}
fn barrier_with_rebate(barrier_type: BarrierType, level: f64) -> OptionType {
OptionType::Barrier {
barrier_type,
barrier_level: pos_or_panic!(level),
rebate: Some(pos_or_panic!(3.0)),
}
}
#[test]
fn test_payoff_unhit_knock_in_pays_the_rebate() {
let spot = Positive::HUNDRED;
for (barrier_type, level) in [
(BarrierType::DownAndIn, 95.0),
(BarrierType::UpAndIn, 105.0),
] {
let option = barrier_with_rebate(barrier_type, level);
for style in [OptionStyle::Call, OptionStyle::Put] {
let long = info(spot, pos_or_panic!(90.0), style, Side::Long);
let short = info(spot, pos_or_panic!(90.0), style, Side::Short);
assert_eq!(
option.payoff(&long).unwrap(),
dec!(3),
"{barrier_type:?} {style:?}"
);
assert_eq!(
option.payoff(&short).unwrap(),
dec!(-3),
"{barrier_type:?} {style:?}"
);
}
}
}
#[test]
fn test_payoff_hit_knock_in_is_the_vanilla_payoff() {
let option = barrier_with_rebate(BarrierType::DownAndIn, 95.0);
let hit = PayoffInfo {
spot_min: Some(pos_or_panic!(94.0)),
..info(
Positive::HUNDRED,
pos_or_panic!(90.0),
OptionStyle::Call,
Side::Long,
)
};
assert_eq!(option.payoff(&hit).unwrap(), dec!(10));
}
#[test]
fn test_payoff_hit_knock_out_rebate_is_signed_by_side() {
let option = barrier_with_rebate(BarrierType::UpAndOut, 105.0);
let long = info(
pos_or_panic!(106.0),
Positive::HUNDRED,
OptionStyle::Call,
Side::Long,
);
let short = info(
pos_or_panic!(106.0),
Positive::HUNDRED,
OptionStyle::Call,
Side::Short,
);
assert_eq!(option.payoff(&long).unwrap(), dec!(3));
assert_eq!(option.payoff(&short).unwrap(), dec!(-3));
}
#[test]
fn test_payoff_unhit_knock_in_without_rebate_is_zero() {
let option = OptionType::Barrier {
barrier_type: BarrierType::DownAndIn,
barrier_level: pos_or_panic!(95.0),
rebate: Some(Positive::ZERO),
};
let short = info(
Positive::HUNDRED,
pos_or_panic!(90.0),
OptionStyle::Call,
Side::Short,
);
let payoff = option.payoff(&short).unwrap();
assert_eq!(payoff, Decimal::ZERO);
assert!(!payoff.is_sign_negative());
}
#[test]
fn test_options_payoff_judges_the_barrier_from_the_spot() {
let mut option =
crate::model::utils::create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.option_type = barrier_with_rebate(BarrierType::DownAndIn, 95.0);
option.underlying_price = Positive::HUNDRED;
option.quantity = Positive::TWO;
assert_eq!(option.payoff().unwrap(), dec!(6));
option.underlying_price = pos_or_panic!(95.0);
let vanilla_at_95 = OptionType::European
.payoff(&info(
pos_or_panic!(95.0),
option.strike_price,
OptionStyle::Call,
Side::Long,
))
.unwrap();
assert_eq!(option.payoff().unwrap(), vanilla_at_95 * dec!(2));
}
#[test]
fn test_payoff_floating_strike_lookback_through_trait() {
let option = OptionType::Lookback {
lookback_type: LookbackType::FloatingStrike,
};
let call = PayoffInfo {
spot_min: Some(pos_or_panic!(80.0)),
..info(
Positive::HUNDRED,
Positive::ZERO,
OptionStyle::Call,
Side::Long,
)
};
let put = PayoffInfo {
spot_max: Some(pos_or_panic!(120.0)),
..info(
Positive::HUNDRED,
Positive::ZERO,
OptionStyle::Put,
Side::Long,
)
};
assert_eq!(option.payoff(&call).unwrap(), dec!(20));
assert_eq!(option.payoff(&put).unwrap(), dec!(20));
}
#[test]
fn test_payoff_binary_gap_and_asset_or_nothing() {
let gap = OptionType::Binary {
binary_type: BinaryType::Gap,
};
let asset = OptionType::Binary {
binary_type: BinaryType::AssetOrNothing,
};
let itm_call = info(
pos_or_panic!(112.5),
Positive::HUNDRED,
OptionStyle::Call,
Side::Long,
);
assert_eq!(gap.payoff(&itm_call).unwrap(), dec!(12.5));
assert_eq!(asset.payoff(&itm_call).unwrap(), dec!(112.5));
}
#[test]
fn test_payoff_unrepresentable_power_call_is_error() {
let option = OptionType::Power {
exponent: Positive::MAX,
};
let info = info(
Positive::HUNDRED,
Positive::HUNDRED,
OptionStyle::Call,
Side::Long,
);
assert!(matches!(
option.payoff(&info),
Err(OptionsError::PayoffError { .. })
));
}
#[test]
fn test_payoff_at_decimal_max_is_exact() {
let option = OptionType::European;
let info = info(Positive::MAX, Positive::ZERO, OptionStyle::Call, Side::Long);
assert_eq!(option.payoff(&info).unwrap(), Decimal::MAX);
}
}
#[cfg(test)]
mod tests_geometric_mean {
use super::*;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
fn ulps(a: f64, b: f64) -> u64 {
(a.to_bits() as i64 - b.to_bits() as i64).unsigned_abs()
}
fn fixings(values: impl IntoIterator<Item = Decimal>) -> Vec<Positive> {
values
.into_iter()
.map(|value| Positive::new_decimal(value).unwrap())
.collect()
}
const REFERENCE_ULPS: u64 = 3;
#[test]
fn test_geometric_mean_matches_the_reference() {
let cases: [(Vec<Positive>, f64); 4] = [
(fixings([dec!(90), dec!(100), dec!(110)]), 99.66554934125963),
(fixings(std::iter::repeat_n(dec!(10000), 100)), 10000.0),
(
fixings((0..252).map(|i| dec!(10000) + Decimal::from(10 * i))),
11231.426621138467,
),
(
fixings([dec!(0.01), dec!(1000000), dec!(37.5)]),
72.11247851537043,
),
];
for (values, reference) in cases {
let mean = match geometric_mean(&values) {
Ok(mean) => mean,
Err(e) => panic!("geometric mean must convert: {e:?}"),
};
assert!(
ulps(mean, reference) <= REFERENCE_ULPS,
"{} fixings: {mean} against {reference}",
values.len()
);
}
}
#[test]
fn test_geometric_asian_with_100_fixings_at_1e4_prices() {
let option = OptionType::Asian {
averaging_type: AsianAveragingType::Geometric,
};
for (style, strike, expected) in [
(OptionStyle::Call, dec!(9000), dec!(1000)),
(OptionStyle::Put, dec!(10500), dec!(500)),
] {
let info = PayoffInfo {
spot: pos_or_panic!(10000.0),
strike: Positive::new_decimal(strike).unwrap(),
style,
side: Side::Long,
spot_prices: Some(vec![pos_or_panic!(10000.0); 100]),
..Default::default()
};
assert_eq!(option.payoff(&info).unwrap(), expected, "{style:?}");
}
}
#[test]
fn test_geometric_mean_with_a_zero_fixing_is_zero() {
for values in [
fixings([dec!(0), dec!(100), dec!(110)]),
fixings([dec!(90), dec!(100), dec!(0)]),
] {
assert!(matches!(geometric_mean(&values), Ok(mean) if mean == 0.0));
}
assert!(matches!(geometric_mean(&[]), Ok(mean) if mean == 0.0));
}
}
#[cfg(test)]
mod tests_terminal_payoff {
use super::*;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
fn info(spot: f64, style: OptionStyle, side: Side) -> PayoffInfo {
PayoffInfo {
spot: pos_or_panic!(spot),
strike: Positive::HUNDRED,
style,
side,
..Default::default()
}
}
#[test]
fn test_short_binary_chooser_and_power_are_signed() {
let short_call = info(105.0, OptionStyle::Call, Side::Short);
let binary = OptionType::Binary {
binary_type: BinaryType::CashOrNothing,
};
assert_eq!(binary.payoff(&short_call).unwrap(), dec!(-1));
let chooser = OptionType::Chooser {
choice_date: pos_or_panic!(30.0),
};
assert_eq!(chooser.payoff(&short_call).unwrap(), dec!(-5));
let power = OptionType::Power {
exponent: Positive::ONE,
};
assert_eq!(power.payoff(&short_call).unwrap(), dec!(-5));
let otm = info(95.0, OptionStyle::Call, Side::Short);
let zero = binary.payoff(&otm).unwrap();
assert!(zero.is_zero() && !zero.is_sign_negative());
}
#[test]
fn test_floating_strike_lookback_without_extremes_is_zero() {
let lookback = OptionType::Lookback {
lookback_type: LookbackType::FloatingStrike,
};
for style in [OptionStyle::Call, OptionStyle::Put] {
assert_eq!(
lookback.payoff(&info(105.0, style, Side::Long)).unwrap(),
Decimal::ZERO
);
}
let call = PayoffInfo {
spot_min: Some(pos_or_panic!(90.0)),
..info(105.0, OptionStyle::Call, Side::Short)
};
assert_eq!(lookback.payoff(&call).unwrap(), dec!(-15));
let stale = PayoffInfo {
spot_min: Some(pos_or_panic!(110.0)),
..info(105.0, OptionStyle::Call, Side::Long)
};
assert_eq!(lookback.payoff(&stale).unwrap(), Decimal::ZERO);
}
#[test]
fn test_asian_payoff_without_fixings_is_intrinsic_and_signed() {
let asian = OptionType::Asian {
averaging_type: AsianAveragingType::Arithmetic,
};
assert_eq!(
asian
.payoff(&info(105.0, OptionStyle::Call, Side::Long))
.unwrap(),
dec!(5)
);
let fixed = PayoffInfo {
spot_prices: Some(vec![pos_or_panic!(104.0), pos_or_panic!(108.0)]),
..info(105.0, OptionStyle::Call, Side::Short)
};
assert_eq!(asian.payoff(&fixed).unwrap(), dec!(-6));
}
#[test]
fn test_compound_pays_on_its_strike() {
let compound = OptionType::Compound {
underlying_option: Box::new(OptionType::European),
};
assert_eq!(
compound
.payoff(&info(105.0, OptionStyle::Call, Side::Long))
.unwrap(),
Decimal::ZERO
);
assert_eq!(
compound
.payoff(&info(105.0, OptionStyle::Put, Side::Long))
.unwrap(),
dec!(100)
);
}
#[test]
fn test_vanilla_payoff_is_exact() {
let call = PayoffInfo {
spot: Positive::new_decimal(dec!(50.07)).unwrap(),
strike: pos_or_panic!(50.0),
style: OptionStyle::Call,
side: Side::Long,
..Default::default()
};
assert_eq!(OptionType::European.payoff(&call).unwrap(), dec!(0.07));
}
}