pub use financial_types::{Action, OptionStyle, Side, UnderlyingAssetType};
pub use option_type::{
AsianAveragingType, BarrierType, BinaryType, LookbackType, OptionBasicType, OptionType,
RainbowType,
};
use crate::constants::ZERO;
use crate::pricing::payoff::{Payoff, PayoffInfo, standard_payoff};
use chrono::{DateTime, Utc};
use positive::Positive;
use rust_decimal::Decimal;
mod datetime_format {
use super::*;
use serde::{self, Deserialize, Deserializer, Serializer};
#[allow(dead_code)]
pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = date.to_rfc3339();
serializer.serialize_str(&s)
}
#[allow(dead_code)]
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
DateTime::parse_from_rfc3339(&s)
.map(|dt| dt.with_timezone(&Utc))
.map_err(serde::de::Error::custom)
}
}
impl Payoff for OptionType {
fn payoff(&self, info: &PayoffInfo) -> f64 {
match self {
OptionType::European | OptionType::American => standard_payoff(info),
OptionType::Bermuda { .. } => standard_payoff(info),
OptionType::Asian { averaging_type } => calculate_asian_payoff(averaging_type, info),
OptionType::Barrier {
barrier_type,
barrier_level,
rebate,
} => calculate_barrier_payoff(barrier_type, barrier_level, rebate, info),
OptionType::Binary { binary_type } => calculate_binary_payoff(binary_type, info),
OptionType::Lookback { lookback_type } => match lookback_type {
LookbackType::FixedStrike => standard_payoff(info),
LookbackType::FloatingStrike => calculate_floating_strike_payoff(info),
},
OptionType::Compound { underlying_option } => underlying_option.payoff(info),
OptionType::Chooser { .. } => (info.spot - info.strike)
.max(Positive::ZERO)
.max(
Positive::new_decimal(
(info.strike.to_dec() - info.spot.to_dec()).max(Decimal::ZERO),
)
.unwrap_or(Positive::ZERO),
)
.to_f64(),
OptionType::Cliquet { .. } => standard_payoff(info),
OptionType::Rainbow { .. }
| OptionType::Spread { .. }
| OptionType::Exchange { .. } => standard_payoff(info),
OptionType::Quanto { exchange_rate } => standard_payoff(info) * exchange_rate,
OptionType::Power { exponent } => match info.style {
OptionStyle::Call => (info.spot.to_f64().powf(*exponent) - info.strike).max(ZERO),
OptionStyle::Put => (info.strike - info.spot.to_f64().powf(*exponent))
.max(Positive::ZERO)
.to_f64(),
},
}
}
}
fn calculate_asian_payoff(averaging_type: &AsianAveragingType, info: &PayoffInfo) -> f64 {
let average = match (&info.spot_prices, info.spot_prices_len()) {
(Some(spot_prices), Some(len)) if len > 0 => match averaging_type {
AsianAveragingType::Arithmetic => spot_prices.iter().sum::<f64>() / len as f64,
AsianAveragingType::Geometric => {
let product = spot_prices.iter().fold(1.0, |acc, &x| acc * x);
product.powf(1.0 / len as f64)
}
},
_ => return ZERO,
};
match info.style {
OptionStyle::Call => (average - info.strike).max(ZERO),
OptionStyle::Put => (info.strike - average).max(Positive::ZERO).into(),
}
}
fn calculate_barrier_payoff(
barrier_type: &BarrierType,
barrier_level: &f64,
rebate: &Option<f64>,
info: &PayoffInfo,
) -> f64 {
let barrier_condition = match barrier_type {
BarrierType::UpAndIn | BarrierType::UpAndOut => {
info.spot_max.unwrap_or(info.spot.to_f64()) >= *barrier_level
}
BarrierType::DownAndIn | BarrierType::DownAndOut => {
info.spot_min.unwrap_or(info.spot.to_f64()) <= *barrier_level
}
};
let std_payoff = standard_payoff(info);
match barrier_type {
BarrierType::UpAndIn | BarrierType::DownAndIn => {
if barrier_condition {
std_payoff
} else {
0.0
}
}
BarrierType::UpAndOut | BarrierType::DownAndOut => {
if barrier_condition {
rebate.unwrap_or(0.0)
} else {
std_payoff
}
}
}
}
fn calculate_binary_payoff(binary_type: &BinaryType, info: &PayoffInfo) -> f64 {
let is_in_the_money = match info.style {
OptionStyle::Call => info.spot > info.strike,
OptionStyle::Put => info.spot < info.strike,
};
match binary_type {
BinaryType::CashOrNothing => {
if is_in_the_money {
1.0
} else {
0.0
}
}
BinaryType::AssetOrNothing => {
if is_in_the_money {
info.spot.to_f64()
} else {
0.0
}
}
BinaryType::Gap => {
if is_in_the_money {
(info.spot.to_f64() - info.strike.to_f64()).abs()
} else {
0.0
}
}
}
}
fn calculate_floating_strike_payoff(info: &PayoffInfo) -> f64 {
let extremum = match info.style {
OptionStyle::Call => info.spot_min,
OptionStyle::Put => info.spot_max,
};
match info.style {
OptionStyle::Call => info.spot.to_f64() - extremum.unwrap_or(ZERO),
OptionStyle::Put => extremum.unwrap_or(ZERO) - info.spot.to_f64(),
}
}
#[cfg(test)]
mod tests_payoff {
use super::*;
use positive::{Positive, pos_or_panic};
#[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), 10.0);
}
#[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), 10.0);
}
#[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![90.0, 100.0, 110.0]),
..Default::default()
};
assert_eq!(option.payoff(&info), ZERO);
}
#[test]
fn test_barrier_up_and_in_call() {
let option = OptionType::Barrier {
barrier_type: BarrierType::UpAndIn,
barrier_level: 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), 30.0);
}
#[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), 1.0);
}
#[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), 10.0);
}
#[test]
fn test_quanto_call() {
let option = OptionType::Quanto { exchange_rate: 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), 15.0);
}
#[test]
fn test_power_call() {
let option = OptionType::Power { exponent: 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), 10.0);
}
}
#[cfg(test)]
mod tests_calculate_floating_strike_payoff {
use super::*;
#[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(80.0),
spot_max: None,
};
assert_eq!(calculate_floating_strike_payoff(&info), 20.0);
}
#[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,
};
assert_eq!(calculate_floating_strike_payoff(&info), 100.0);
}
#[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(120.0),
};
assert_eq!(calculate_floating_strike_payoff(&info), 20.0);
}
#[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,
};
assert_eq!(calculate_floating_strike_payoff(&info), -100.0);
}
#[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(100.0),
spot_max: None,
};
assert_eq!(calculate_floating_strike_payoff(&info), 0.0);
}
#[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(100.0),
};
assert_eq!(calculate_floating_strike_payoff(&info), 0.0);
}
}
#[cfg(test)]
mod tests_option_type {
use super::*;
use positive::pos_or_panic;
#[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![90.0, 100.0, 110.0]),
..Default::default()
};
assert_eq!(option.payoff(&info), 0.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![90.0, 100.0, 110.0]),
..Default::default()
};
let expected_payoff = 4.67;
assert!((option.payoff(&info) - expected_payoff).abs() < 0.01);
}
#[test]
fn test_barrier_down_and_out_put() {
let option = OptionType::Barrier {
barrier_type: BarrierType::DownAndOut,
barrier_level: 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), 5.0);
}
#[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), 90.0);
}
#[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), 10.0);
}
#[test]
fn test_chooser_option() {
let option = OptionType::Chooser { choice_date: 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), 10.0);
}
#[test]
fn test_power_put() {
let option = OptionType::Power { exponent: 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), 36.0);
}
}
#[cfg(test)]
mod tests_vec_collection {
use positive::{Positive, pos_or_panic};
#[test]
fn test_collect_empty_iterator() {
let empty_vec: Vec<Positive> = Vec::new();
let collected: Vec<Positive> = empty_vec.into_iter().collect();
assert!(collected.is_empty());
}
#[test]
fn test_collect_single_value() {
let values = vec![Positive::ONE];
let collected: Vec<Positive> = values.into_iter().collect();
assert_eq!(collected.len(), 1);
assert_eq!(collected[0], Positive::ONE);
}
#[test]
fn test_collect_multiple_values() {
let values = vec![Positive::ONE, Positive::TWO, pos_or_panic!(3.0)];
let collected: Vec<Positive> = values.into_iter().collect();
assert_eq!(collected.len(), 3);
assert_eq!(collected[0], Positive::ONE);
assert_eq!(collected[1], Positive::TWO);
assert_eq!(collected[2], pos_or_panic!(3.0));
}
#[test]
fn test_collect_from_filter() {
let values = vec![
Positive::ONE,
Positive::TWO,
pos_or_panic!(3.0),
pos_or_panic!(4.0),
];
let collected: Vec<Positive> = values.into_iter().filter(|x| x.to_f64() > 2.0).collect();
assert_eq!(collected.len(), 2);
assert_eq!(collected[0], pos_or_panic!(3.0));
assert_eq!(collected[1], pos_or_panic!(4.0));
}
#[test]
fn test_collect_from_map() {
let values = vec![Positive::ONE, Positive::TWO, pos_or_panic!(3.0)];
let collected: Vec<Positive> = values
.into_iter()
.map(|x| pos_or_panic!(x.to_f64() * 2.0))
.collect();
assert_eq!(collected.len(), 3);
assert_eq!(collected[0], Positive::TWO);
assert_eq!(collected[1], pos_or_panic!(4.0));
assert_eq!(collected[2], pos_or_panic!(6.0));
}
#[test]
fn test_collect_from_chain() {
let values1 = vec![Positive::ONE, Positive::TWO];
let values2 = vec![pos_or_panic!(3.0), pos_or_panic!(4.0)];
let collected: Vec<Positive> = values1.into_iter().chain(values2).collect();
assert_eq!(collected.len(), 4);
assert_eq!(collected[0], Positive::ONE);
assert_eq!(collected[1], Positive::TWO);
assert_eq!(collected[2], pos_or_panic!(3.0));
assert_eq!(collected[3], pos_or_panic!(4.0));
}
}
#[cfg(test)]
mod test_asian_options {
use crate::model::types::AsianAveragingType;
use crate::model::{OptionStyle, OptionType, Side};
use positive::{Positive, pos_or_panic};
use crate::pricing::{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![85.0, 90.0, 95.0]),
..Default::default()
};
assert_eq!(option.payoff(&info), 10.0);
}
#[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), 0.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 crate::pricing::{Payoff, PayoffInfo};
#[test]
fn test_barrier_down_and_in_put() {
let option = OptionType::Barrier {
barrier_type: BarrierType::DownAndIn,
barrier_level: 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), 0.0);
}
#[test]
fn test_barrier_up_and_out_call() {
let option = OptionType::Barrier {
barrier_type: BarrierType::UpAndOut,
barrier_level: 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), 0.0);
}
}
#[cfg(test)]
mod test_cliquet_options {
use crate::model::{OptionStyle, OptionType, Side};
use positive::{Positive, pos_or_panic};
use crate::pricing::{Payoff, PayoffInfo};
#[test]
fn test_cliquet_option_with_resets() {
let option = OptionType::Cliquet {
reset_dates: vec![30.0, 60.0, 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), 20.0);
}
}
#[cfg(test)]
mod test_rainbow_options {
use crate::model::{OptionStyle, OptionType, RainbowType, Side};
use positive::{Positive, pos_or_panic};
use crate::pricing::{Payoff, PayoffInfo};
#[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,
..Default::default()
};
assert_eq!(option.payoff(&info), 20.0);
}
#[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,
..Default::default()
};
assert_eq!(option.payoff(&info), 20.0);
}
}
#[cfg(test)]
mod test_exchange_options {
use crate::model::{OptionStyle, OptionType, Side};
use positive::{Positive, pos_or_panic};
use crate::pricing::{Payoff, PayoffInfo};
#[test]
fn test_exchange_option_positive_diff() {
let option = OptionType::Exchange { second_asset: 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), 20.0);
}
#[test]
fn test_exchange_option_negative_diff() {
let option = OptionType::Exchange {
second_asset: 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), 10.0);
}
}