use crate::constants::ZERO;
use crate::error::{OptionsError, OptionsResult};
use crate::model::ExpirationDate;
use crate::model::payoff::{Payoff, PayoffInfo};
use crate::model::types::{OptionStyle, OptionType, Side};
use positive::Positive;
#[cfg(test)]
use positive::pos_or_panic;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use tracing::error;
#[cold]
#[inline(never)]
fn payoff_out_of_range(context: &'static str, payoff: Decimal, size: Positive) -> OptionsError {
OptionsError::PayoffError {
reason: format!(
"{context}: payoff {payoff} times position size {size} is not representable as a Decimal (out of range)"
),
}
}
#[cold]
#[inline(never)]
fn position_size_out_of_range(quantity: Positive, contract_size: Positive) -> OptionsError {
OptionsError::ValidationError {
field: "contract_size".to_string(),
reason: format!(
"quantity {quantity} times contract size {contract_size} is not representable as a Positive (out of range)"
),
}
}
#[inline]
#[must_use]
fn default_contract_size() -> Positive {
Positive::ONE
}
#[derive(Clone, Default, PartialEq, Serialize, Deserialize, Debug)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct ExoticParams {
pub spot_prices: Option<Vec<Positive>>,
pub spot_min: Option<Decimal>,
pub spot_max: Option<Decimal>,
pub cliquet_local_cap: Option<Decimal>,
pub cliquet_local_floor: Option<Decimal>,
pub cliquet_global_cap: Option<Decimal>,
pub cliquet_global_floor: Option<Decimal>,
pub rainbow_second_asset_price: Option<Positive>,
pub rainbow_second_asset_volatility: Option<Positive>,
pub rainbow_second_asset_dividend: Option<Positive>,
pub rainbow_correlation: Option<Decimal>,
pub spread_second_asset_volatility: Option<Positive>,
pub spread_second_asset_dividend: Option<Positive>,
pub spread_correlation: Option<Decimal>,
pub quanto_fx_volatility: Option<Positive>,
pub quanto_fx_correlation: Option<Decimal>,
pub quanto_foreign_rate: Option<Decimal>,
pub foreign_rate: Option<Decimal>,
pub exchange_second_asset_volatility: Option<Positive>,
pub exchange_second_asset_dividend: Option<Positive>,
pub exchange_correlation: Option<Decimal>, }
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct Options {
pub option_type: OptionType,
pub side: Side,
pub underlying_symbol: String,
pub strike_price: Positive,
pub expiration_date: ExpirationDate,
pub implied_volatility: Positive,
pub quantity: Positive,
#[serde(default = "default_contract_size")]
pub contract_size: Positive,
pub underlying_price: Positive,
pub risk_free_rate: Decimal,
pub option_style: OptionStyle,
pub dividend_yield: Positive,
pub exotic_params: Option<ExoticParams>,
}
impl Options {
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn new(
option_type: OptionType,
side: Side,
underlying_symbol: String,
strike_price: Positive,
expiration_date: ExpirationDate,
implied_volatility: Positive,
quantity: Positive,
underlying_price: Positive,
risk_free_rate: Decimal,
option_style: OptionStyle,
dividend_yield: Positive,
exotic_params: Option<ExoticParams>,
) -> Self {
Options {
option_type,
side,
underlying_symbol,
strike_price,
expiration_date,
implied_volatility,
quantity,
contract_size: default_contract_size(),
underlying_price,
risk_free_rate,
option_style,
dividend_yield,
exotic_params,
}
}
#[must_use = "with_contract_size returns the updated option and leaves the original untouched"]
#[inline]
pub fn with_contract_size(mut self, contract_size: Positive) -> Self {
self.contract_size = contract_size;
self
}
#[inline]
pub fn position_size(&self) -> OptionsResult<Positive> {
self.quantity
.checked_mul(&self.contract_size)
.map_err(|_| position_size_out_of_range(self.quantity, self.contract_size))
}
pub fn time_to_expiration(&self) -> OptionsResult<Positive> {
Ok(self.expiration_date.get_years()?)
}
#[must_use]
pub fn is_long(&self) -> bool {
matches!(self.side, Side::Long)
}
#[must_use]
pub fn is_short(&self) -> bool {
matches!(self.side, Side::Short)
}
pub fn payoff(&self) -> OptionsResult<Decimal> {
let payoff_info = PayoffInfo {
spot: self.underlying_price,
strike: self.strike_price,
style: self.option_style,
side: self.side,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: self.exotic_params.clone(),
};
let payoff = self.option_type.payoff(&payoff_info)?;
let size = self.position_size()?;
payoff
.checked_mul(size.to_dec())
.ok_or_else(|| payoff_out_of_range("Options::payoff", payoff, size))
}
pub fn payoff_at_price(&self, price: &Positive) -> OptionsResult<Decimal> {
let payoff_info = PayoffInfo {
spot: *price,
strike: self.strike_price,
style: self.option_style,
side: self.side,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: self.exotic_params.clone(),
};
let payoff = self.option_type.payoff(&payoff_info)?;
let size = self.position_size()?;
payoff
.checked_mul(size.to_dec())
.ok_or_else(|| payoff_out_of_range("Options::payoff_at_price", payoff, size))
}
pub fn intrinsic_value(&self, underlying_price: Positive) -> OptionsResult<Decimal> {
let payoff_info = PayoffInfo {
spot: underlying_price,
strike: self.strike_price,
style: self.option_style,
side: self.side,
spot_prices: None,
spot_min: None,
spot_max: None,
exotic_params: self.exotic_params.clone(),
};
let payoff = self.option_type.payoff(&payoff_info)?;
let size = self.position_size()?;
payoff
.checked_mul(size.to_dec())
.ok_or_else(|| payoff_out_of_range("Options::intrinsic_value", payoff, size))
}
#[must_use]
pub fn is_in_the_money(&self) -> bool {
match self.option_style {
OptionStyle::Call => self.underlying_price >= self.strike_price,
OptionStyle::Put => self.underlying_price <= self.strike_price,
}
}
pub(crate) fn validate(&self) -> bool {
if self.underlying_symbol == *"" {
error!("Underlying symbol is empty");
return false;
}
if self.implied_volatility < ZERO {
error!("Implied volatility is less than zero");
return false;
}
if self.quantity == ZERO {
error!("Quantity is equal to zero");
return false;
}
if self.contract_size == Positive::ZERO {
error!("Contract size is equal to zero");
return false;
}
if self.strike_price == Positive::ZERO {
error!("Strike is zero");
return false;
}
if self.underlying_price == Positive::ZERO {
error!("Underlying price is zero");
return false;
}
true
}
}
impl Default for Options {
fn default() -> Self {
Options {
option_type: OptionType::European,
side: Side::Long,
underlying_symbol: "".to_string(),
strike_price: Positive::ZERO,
expiration_date: ExpirationDate::Days(Positive::ZERO),
implied_volatility: Positive::ZERO,
quantity: Positive::ZERO,
contract_size: default_contract_size(),
underlying_price: Positive::ZERO,
risk_free_rate: Decimal::ZERO,
option_style: OptionStyle::Call,
dividend_yield: Positive::ZERO,
exotic_params: None,
}
}
}
#[cfg(test)]
mod tests_options {
use super::*;
use crate::model::utils::create_sample_option_simplest;
use approx::assert_relative_eq;
use chrono::{Duration, Utc};
use num_traits::ToPrimitive;
use rust_decimal_macros::dec;
#[test]
fn test_new_option() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
assert_eq!(option.underlying_symbol, "AAPL");
assert_eq!(option.strike_price, 100.0);
assert_eq!(option.implied_volatility, 0.2);
}
#[test]
fn test_time_to_expiration() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
assert_relative_eq!(
option.time_to_expiration().unwrap().to_f64(),
30.0 / 365.0,
epsilon = 0.0001
);
let future_date = Utc::now() + Duration::days(60);
let option_with_datetime = Options::new(
OptionType::European,
Side::Long,
"AAPL".to_string(),
Positive::HUNDRED,
ExpirationDate::DateTime(future_date),
pos_or_panic!(0.2),
Positive::ONE,
pos_or_panic!(105.0),
dec!(0.05),
OptionStyle::Call,
pos_or_panic!(0.01),
None,
);
assert!(option_with_datetime.time_to_expiration().unwrap() >= 59.0 / 365.0);
assert!(option_with_datetime.time_to_expiration().unwrap() < 61.0 / 365.0);
}
#[test]
fn test_is_long_and_short() {
let long_option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
assert!(long_option.is_long());
assert!(!long_option.is_short());
let short_option = Options::new(
OptionType::European,
Side::Short,
"AAPL".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2),
Positive::ONE,
pos_or_panic!(105.0),
dec!(0.05),
OptionStyle::Call,
pos_or_panic!(0.01),
None,
);
assert!(!short_option.is_long());
assert!(short_option.is_short());
}
#[test]
fn test_payoff_european_call_long() {
let call_option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let call_payoff = call_option.payoff().unwrap();
assert_eq!(call_payoff, Decimal::ZERO);
let put_option = Options::new(
OptionType::European,
Side::Long,
"AAPL".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2),
Positive::ONE,
pos_or_panic!(95.0),
dec!(0.05),
OptionStyle::Put,
pos_or_panic!(0.01),
None,
);
let put_payoff = put_option.payoff().unwrap();
assert_eq!(put_payoff.to_f64().unwrap(), 5.0); }
}
#[cfg(test)]
mod tests_valid_option {
use super::*;
use rust_decimal_macros::dec;
fn create_valid_option() -> Options {
Options {
option_type: OptionType::European,
side: Side::Long,
underlying_symbol: "AAPL".to_string(),
strike_price: Positive::HUNDRED,
expiration_date: ExpirationDate::Days(pos_or_panic!(30.0)),
implied_volatility: pos_or_panic!(0.2),
quantity: Positive::ONE,
contract_size: Positive::ONE,
underlying_price: pos_or_panic!(105.0),
risk_free_rate: dec!(0.05),
option_style: OptionStyle::Call,
dividend_yield: pos_or_panic!(0.01),
exotic_params: None,
}
}
#[test]
fn test_valid_option() {
let option = create_valid_option();
assert!(option.validate());
}
#[test]
fn test_empty_underlying_symbol() {
let mut option = create_valid_option();
option.underlying_symbol = "".to_string();
assert!(!option.validate());
}
#[test]
fn test_zero_strike_price() {
let mut option = create_valid_option();
option.strike_price = Positive::ZERO;
assert!(!option.validate());
}
#[test]
fn test_zero_quantity() {
let mut option = create_valid_option();
option.quantity = Positive::ZERO;
assert!(!option.validate());
}
#[test]
fn test_zero_underlying_price() {
let mut option = create_valid_option();
option.underlying_price = Positive::ZERO;
assert!(!option.validate());
}
#[test]
fn test_options_validate_negative_risk_free_rate_is_valid() {
for rate in [dec!(-0.01), Decimal::ZERO, dec!(-0.0075), dec!(-0.25)] {
let mut option = create_valid_option();
option.risk_free_rate = rate;
assert!(option.validate(), "r = {rate}");
}
}
#[test]
fn test_options_validate_negative_rate_keeps_other_checks() {
let mut option = create_valid_option();
option.risk_free_rate = dec!(-0.01);
option.quantity = Positive::ZERO;
assert!(!option.validate());
let mut option = create_valid_option();
option.risk_free_rate = dec!(-0.01);
option.underlying_symbol = String::new();
assert!(!option.validate());
let mut option = create_valid_option();
option.risk_free_rate = dec!(-0.01);
option.underlying_price = Positive::ZERO;
assert!(!option.validate());
}
}
#[cfg(test)]
mod tests_options_payoffs {
use super::*;
use crate::model::utils::create_sample_option_simplest_strike;
use rust_decimal_macros::dec;
#[test]
fn test_payoff_european_call_long() {
let call_option = create_sample_option_simplest_strike(
Side::Long,
OptionStyle::Call,
pos_or_panic!(95.0),
);
let call_payoff = call_option.payoff().unwrap();
assert_eq!(call_payoff, dec!(5.0));
let call_option_otm = create_sample_option_simplest_strike(
Side::Long,
OptionStyle::Call,
pos_or_panic!(105.0),
);
let call_payoff_otm = call_option_otm.payoff().unwrap();
assert_eq!(call_payoff_otm, Decimal::ZERO); }
#[test]
fn test_payoff_european_call_short() {
let call_option = create_sample_option_simplest_strike(
Side::Short,
OptionStyle::Call,
pos_or_panic!(95.0),
);
let call_payoff = call_option.payoff().unwrap();
assert_eq!(call_payoff, dec!(-5.0));
let call_option_otm = create_sample_option_simplest_strike(
Side::Short,
OptionStyle::Call,
pos_or_panic!(105.0),
);
let call_payoff_otm = call_option_otm.payoff().unwrap();
assert_eq!(call_payoff_otm, Decimal::ZERO); }
#[test]
fn test_payoff_european_put_long() {
let put_option = create_sample_option_simplest_strike(
Side::Long,
OptionStyle::Put,
pos_or_panic!(105.0),
);
let put_payoff = put_option.payoff().unwrap();
assert_eq!(put_payoff, dec!(5.0));
let put_option_otm =
create_sample_option_simplest_strike(Side::Long, OptionStyle::Put, pos_or_panic!(95.0));
let put_payoff_otm = put_option_otm.payoff().unwrap();
assert_eq!(put_payoff_otm, Decimal::ZERO); }
#[test]
fn test_payoff_european_put_short() {
let put_option = create_sample_option_simplest_strike(
Side::Short,
OptionStyle::Put,
pos_or_panic!(105.0),
);
let put_payoff = put_option.payoff().unwrap();
assert_eq!(put_payoff, dec!(-5.0));
let put_option_otm = create_sample_option_simplest_strike(
Side::Short,
OptionStyle::Put,
pos_or_panic!(95.0),
);
let put_payoff_otm = put_option_otm.payoff().unwrap();
assert_eq!(put_payoff_otm, Decimal::ZERO); }
#[test]
fn test_payoff_out_of_range_reports_an_error_instead_of_zero() {
let mut option = create_sample_option_simplest_strike(
Side::Long,
OptionStyle::Call,
pos_or_panic!(95.0),
);
option.strike_price = Positive::ZERO;
option.underlying_price = Positive::MAX;
option.quantity = Positive::TWO;
assert!(matches!(
option.payoff(),
Err(OptionsError::PayoffError { .. })
));
assert!(matches!(
option.payoff_at_price(&Positive::MAX),
Err(OptionsError::PayoffError { .. })
));
assert!(matches!(
option.intrinsic_value(Positive::MAX),
Err(OptionsError::PayoffError { .. })
));
}
#[test]
fn test_payoff_out_of_the_money_still_returns_zero() {
let mut option =
create_sample_option_simplest_strike(Side::Long, OptionStyle::Put, pos_or_panic!(95.0));
option.strike_price = Positive::ZERO;
option.underlying_price = Positive::MAX;
option.quantity = Positive::ONE;
assert_eq!(option.payoff().unwrap(), Decimal::ZERO);
assert_eq!(
option.payoff_at_price(&Positive::MAX).unwrap(),
Decimal::ZERO
);
assert_eq!(
option.intrinsic_value(Positive::MAX).unwrap(),
Decimal::ZERO
);
}
}
#[cfg(test)]
mod tests_options_payoff_at_price {
use super::*;
use crate::model::utils::create_sample_option_simplest;
use rust_decimal_macros::dec;
#[test]
fn test_payoff_european_call_long() {
let call_option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let call_payoff = call_option.payoff_at_price(&pos_or_panic!(105.0)).unwrap();
assert_eq!(call_payoff, dec!(5.0));
let call_option_otm = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let call_payoff_otm = call_option_otm
.payoff_at_price(&pos_or_panic!(95.0))
.unwrap();
assert_eq!(call_payoff_otm, Decimal::ZERO); }
#[test]
fn test_payoff_european_call_short() {
let call_option = create_sample_option_simplest(OptionStyle::Call, Side::Short);
let call_payoff = call_option.payoff_at_price(&pos_or_panic!(105.0)).unwrap();
assert_eq!(call_payoff, dec!(-5.0));
let call_option_otm = create_sample_option_simplest(OptionStyle::Call, Side::Short);
let call_payoff_otm = call_option_otm
.payoff_at_price(&pos_or_panic!(95.0))
.unwrap();
assert_eq!(call_payoff_otm, Decimal::ZERO); }
#[test]
fn test_payoff_european_put_long() {
let put_option = create_sample_option_simplest(OptionStyle::Put, Side::Long);
let put_payoff = put_option.payoff_at_price(&pos_or_panic!(95.0)).unwrap();
assert_eq!(put_payoff, dec!(5.0));
let put_option_otm = create_sample_option_simplest(OptionStyle::Put, Side::Long);
let put_payoff_otm = put_option_otm
.payoff_at_price(&pos_or_panic!(105.0))
.unwrap();
assert_eq!(put_payoff_otm, Decimal::ZERO); }
#[test]
fn test_payoff_european_put_short() {
let put_option = create_sample_option_simplest(OptionStyle::Put, Side::Short);
let put_payoff = put_option.payoff_at_price(&pos_or_panic!(95.0)).unwrap();
assert_eq!(put_payoff, dec!(-5.0));
let put_option_otm = create_sample_option_simplest(OptionStyle::Put, Side::Short);
let put_payoff_otm = put_option_otm
.payoff_at_price(&pos_or_panic!(105.0))
.unwrap();
assert_eq!(put_payoff_otm, Decimal::ZERO); }
}
#[cfg(test)]
mod tests_options_payoffs_with_quantity {
use super::*;
use crate::model::utils::create_sample_option;
use num_traits::ToPrimitive;
use rust_decimal_macros::dec;
#[test]
fn test_payoff_call_long() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(105.0),
pos_or_panic!(10.0),
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(option.payoff().unwrap().to_f64().unwrap(), 50.0);
let option_otm = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(95.0),
pos_or_panic!(4.0),
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(option_otm.payoff().unwrap(), Decimal::ZERO);
}
#[test]
fn test_payoff_call_short() {
let option = create_sample_option(
OptionStyle::Call,
Side::Short,
pos_or_panic!(105.0),
pos_or_panic!(3.0),
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(option.payoff().unwrap().to_f64().unwrap(), -15.0);
let option_otm = create_sample_option(
OptionStyle::Call,
Side::Short,
pos_or_panic!(95.0),
pos_or_panic!(7.0),
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(option_otm.payoff().unwrap(), Decimal::ZERO);
}
#[test]
fn test_payoff_put_long() {
let option = create_sample_option(
OptionStyle::Put,
Side::Long,
pos_or_panic!(95.0),
Positive::TWO,
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(option.payoff().unwrap().to_f64().unwrap(), 10.0);
let option_otm = create_sample_option(
OptionStyle::Put,
Side::Long,
pos_or_panic!(105.0),
pos_or_panic!(7.0),
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(option_otm.payoff().unwrap(), Decimal::ZERO);
}
#[test]
fn test_payoff_put_short() {
let option = create_sample_option(
OptionStyle::Put,
Side::Short,
pos_or_panic!(95.0),
pos_or_panic!(3.0),
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(option.payoff().unwrap().to_f64().unwrap(), -15.0);
let option_otm = create_sample_option(
OptionStyle::Put,
Side::Short,
pos_or_panic!(105.0),
pos_or_panic!(3.0),
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(option_otm.payoff().unwrap(), Decimal::ZERO);
}
#[test]
fn test_payoff_with_quantity() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(110.0),
pos_or_panic!(3.0),
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(option.payoff().unwrap().to_f64().unwrap(), 30.0); }
#[test]
fn test_intrinsic_value_call_long() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
pos_or_panic!(11.0),
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(
option.intrinsic_value(pos_or_panic!(105.0)).unwrap(),
dec!(55.0)
);
assert_eq!(
option.intrinsic_value(pos_or_panic!(95.0)).unwrap(),
Decimal::ZERO
);
}
#[test]
fn test_intrinsic_value_call_short() {
let option = create_sample_option(
OptionStyle::Call,
Side::Short,
Positive::HUNDRED,
pos_or_panic!(13.0),
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(
option.intrinsic_value(pos_or_panic!(105.0)).unwrap(),
dec!(-65.0)
);
assert_eq!(
option.intrinsic_value(pos_or_panic!(95.0)).unwrap(),
Decimal::ZERO
);
}
#[test]
fn test_intrinsic_value_put_long() {
let option = create_sample_option(
OptionStyle::Put,
Side::Long,
Positive::HUNDRED,
pos_or_panic!(17.0),
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(
option.intrinsic_value(pos_or_panic!(95.0)).unwrap(),
dec!(85.0)
);
assert_eq!(
option.intrinsic_value(pos_or_panic!(105.0)).unwrap(),
Decimal::ZERO
);
}
#[test]
fn test_intrinsic_value_put_short() {
let option = create_sample_option(
OptionStyle::Put,
Side::Short,
Positive::HUNDRED,
pos_or_panic!(19.0),
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(
option.intrinsic_value(pos_or_panic!(95.0)).unwrap(),
dec!(-95.0)
);
assert_eq!(
option.intrinsic_value(pos_or_panic!(105.0)).unwrap(),
Decimal::ZERO
);
}
#[test]
fn test_intrinsic_value_with_quantity() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
pos_or_panic!(23.0),
Positive::HUNDRED,
pos_or_panic!(0.02),
);
assert_eq!(
option.intrinsic_value(pos_or_panic!(110.0)).unwrap(),
dec!(230.0)
); }
}
#[cfg(test)]
mod tests_in_the_money {
use super::*;
use crate::model::utils::create_sample_option;
#[test]
fn test_call_in_the_money() {
let mut option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(110.0),
Positive::ONE,
pos_or_panic!(110.0),
pos_or_panic!(0.02),
);
option.strike_price = Positive::HUNDRED;
assert!(option.is_in_the_money());
}
#[test]
fn test_call_at_the_money() {
let mut option = create_sample_option(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
pos_or_panic!(110.0),
pos_or_panic!(0.02),
);
option.strike_price = Positive::HUNDRED;
assert!(option.is_in_the_money());
}
#[test]
fn test_call_out_of_the_money() {
let mut option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(90.0),
Positive::ONE,
pos_or_panic!(110.0),
pos_or_panic!(0.02),
);
option.strike_price = Positive::HUNDRED;
assert!(!option.is_in_the_money());
}
#[test]
fn test_put_in_the_money() {
let mut option = create_sample_option(
OptionStyle::Put,
Side::Long,
pos_or_panic!(90.0),
Positive::ONE,
pos_or_panic!(110.0),
pos_or_panic!(0.02),
);
option.strike_price = Positive::HUNDRED;
assert!(option.is_in_the_money());
}
#[test]
fn test_put_at_the_money() {
let mut option = create_sample_option(
OptionStyle::Put,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
pos_or_panic!(110.0),
pos_or_panic!(0.02),
);
option.strike_price = Positive::HUNDRED;
assert!(option.is_in_the_money());
}
#[test]
fn test_put_out_of_the_money() {
let mut option = create_sample_option(
OptionStyle::Put,
Side::Long,
pos_or_panic!(110.0),
Positive::ONE,
pos_or_panic!(110.0),
pos_or_panic!(0.02),
);
option.strike_price = Positive::HUNDRED;
assert!(!option.is_in_the_money());
}
}
#[cfg(test)]
mod tests_serialize_deserialize {
use super::*;
use crate::model::utils::create_sample_option_simplest_strike;
#[test]
fn test_serialize_deserialize_options() {
let options = create_sample_option_simplest_strike(
Side::Long,
OptionStyle::Call,
pos_or_panic!(95.0),
);
let serialized = serde_json::to_string(&options).expect("Failed to serialize");
let deserialized: Options =
serde_json::from_str(&serialized).expect("Failed to deserialize");
assert_eq!(options, deserialized);
}
}
#[cfg(test)]
mod tests_contract_size {
use super::*;
use crate::model::utils::create_sample_option;
use rust_decimal_macros::dec;
fn call(side: Side, spot: Positive, quantity: Positive) -> Options {
create_sample_option(
OptionStyle::Call,
side,
spot,
quantity,
Positive::HUNDRED,
pos_or_panic!(0.2),
)
}
#[test]
fn test_options_contract_size_defaults_to_one() {
let option = call(Side::Long, pos_or_panic!(110.0), Positive::ONE);
assert_eq!(option.contract_size, Positive::ONE);
assert_eq!(Options::default().contract_size, Positive::ONE);
assert_eq!(option.position_size().ok(), Some(Positive::ONE));
}
#[test]
fn test_options_with_contract_size_sets_the_multiplier() {
let option = call(Side::Long, pos_or_panic!(110.0), Positive::TWO)
.with_contract_size(Positive::HUNDRED);
assert_eq!(option.contract_size, Positive::HUNDRED);
assert_eq!(option.position_size().ok(), Some(pos_or_panic!(200.0)));
}
#[test]
fn test_options_position_size_overflow_is_an_error() {
let option = call(Side::Long, pos_or_panic!(110.0), Positive::MAX)
.with_contract_size(Positive::HUNDRED);
assert!(matches!(
option.position_size(),
Err(OptionsError::ValidationError { .. })
));
assert!(option.payoff().is_err());
}
#[test]
fn test_options_payoff_scales_by_contract_size() {
let one = call(Side::Long, pos_or_panic!(110.0), Positive::TWO);
let sized = one.clone().with_contract_size(Positive::HUNDRED);
assert_eq!(one.payoff().ok(), Some(dec!(20)));
assert_eq!(sized.payoff().ok(), Some(dec!(2000)));
assert_eq!(
sized.payoff_at_price(&pos_or_panic!(120.0)).ok(),
Some(dec!(4000))
);
assert_eq!(
sized.intrinsic_value(pos_or_panic!(105.0)).ok(),
Some(dec!(1000))
);
assert_eq!(
sized.intrinsic_value(pos_or_panic!(90.0)).ok(),
Some(Decimal::ZERO)
);
}
#[test]
fn test_options_short_payoff_scales_by_contract_size() {
let sized = call(Side::Short, pos_or_panic!(110.0), Positive::ONE)
.with_contract_size(Positive::HUNDRED);
assert_eq!(sized.payoff().ok(), Some(dec!(-1000)));
}
#[test]
fn test_options_validate_rejects_zero_contract_size() {
let option = call(Side::Long, pos_or_panic!(110.0), Positive::ONE);
assert!(option.validate());
assert!(!option.with_contract_size(Positive::ZERO).validate());
}
#[test]
fn test_options_deserialize_without_contract_size_defaults_to_one() {
let option = call(Side::Long, pos_or_panic!(110.0), Positive::ONE);
let mut value = serde_json::to_value(&option).expect("serialize");
let removed = value
.as_object_mut()
.and_then(|map| map.remove("contract_size"));
assert!(removed.is_some(), "contract_size is serialized");
let restored: Options = serde_json::from_value(value).expect("deserialize");
assert_eq!(restored.contract_size, Positive::ONE);
assert_eq!(restored, option);
}
#[test]
fn test_options_contract_size_round_trips_through_serde() {
let option = call(Side::Long, pos_or_panic!(110.0), Positive::ONE)
.with_contract_size(Positive::HUNDRED);
let json = serde_json::to_string(&option).expect("serialize");
let restored: Options = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored.contract_size, Positive::HUNDRED);
}
#[test]
fn test_options_format_shows_contract_size_only_when_not_one() {
let one = call(Side::Long, pos_or_panic!(110.0), Positive::ONE);
assert!(!format!("{one}").contains("Contract Size"));
assert!(!format!("{one:?}").contains("contract_size"));
let sized = one.with_contract_size(Positive::HUNDRED);
assert!(format!("{sized}").contains("Contract Size: 100"));
assert!(format!("{sized:?}").contains("contract_size: 100"));
}
}