use crate::ExpirationDate;
use crate::chains::OptionData;
use crate::constants::{IV_TOLERANCE, MAX_ITERATIONS_IV, ZERO};
use crate::error::{
GreeksError, OptionsError, OptionsResult, PricingError, StrategyError, VolatilityError,
};
use crate::greeks::Greeks;
use crate::model::types::{OptionBasicType, OptionStyle, OptionType, Side};
use crate::model::utils::calculate_optimal_price_range;
use crate::pnl::utils::{PnL, PnLCalculator};
use crate::pricing::monte_carlo::price_option_monte_carlo;
use crate::pricing::{
BinomialPricingParams, Payoff, PayoffInfo, Profit, black_scholes, generate_binomial_tree,
price_binomial, telegraph,
};
use crate::strategies::base::BasicAble;
use crate::visualization::{
ColorScheme, Graph, GraphConfig, GraphData, LineStyle, Series2D, TraceMode,
};
use num_traits::FromPrimitive;
use positive::{Positive, pos_or_panic};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use tracing::{error, trace};
use utoipa::ToSchema;
type PriceBinomialTree = OptionsResult<(Decimal, Vec<Vec<Decimal>>, Vec<Vec<Decimal>>)>;
#[derive(Clone, Default, PartialEq, Serialize, Deserialize, Debug, 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 exchange_second_asset_volatility: Option<Positive>,
pub exchange_second_asset_dividend: Option<Positive>,
pub exchange_correlation: Option<Decimal>, }
#[derive(Clone, PartialEq, Serialize, Deserialize, 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,
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)]
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,
underlying_price,
risk_free_rate,
option_style,
dividend_yield,
exotic_params,
}
}
pub(crate) fn update_from_option_data(&mut self, option_data: &OptionData) {
self.strike_price = option_data.strike_price;
self.implied_volatility = option_data.implied_volatility;
trace!("Updated Option: {:#?}", self);
}
pub fn time_to_expiration(&self) -> OptionsResult<Positive> {
Ok(self.expiration_date.get_years()?)
}
pub fn is_long(&self) -> bool {
matches!(self.side, Side::Long)
}
pub fn is_short(&self) -> bool {
matches!(self.side, Side::Short)
}
pub fn calculate_price_binomial(&self, no_steps: usize) -> OptionsResult<Decimal> {
if no_steps == 0 {
return Err(OptionsError::OtherError {
reason: "Number of steps cannot be zero".to_string(),
});
}
let expiry = self.time_to_expiration()?;
let cpb = price_binomial(BinomialPricingParams {
asset: self.underlying_price,
volatility: self.implied_volatility,
int_rate: self.risk_free_rate,
strike: self.strike_price,
expiry,
no_steps,
option_type: &self.option_type,
option_style: &self.option_style,
side: &self.side,
})?;
Ok(cpb)
}
pub fn calculate_price_binomial_tree(&self, no_steps: usize) -> PriceBinomialTree {
let expiry = self.time_to_expiration()?;
let params = BinomialPricingParams {
asset: self.underlying_price,
volatility: self.implied_volatility,
int_rate: self.risk_free_rate,
strike: self.strike_price,
expiry,
no_steps,
option_type: &self.option_type,
option_style: &self.option_style,
side: &self.side,
};
let (asset_tree, option_tree) = generate_binomial_tree(¶ms)?;
let price = match self.side {
Side::Long => option_tree[0][0],
Side::Short => -option_tree[0][0],
};
Ok((price, asset_tree, option_tree))
}
pub fn calculate_price_black_scholes(&self) -> OptionsResult<Decimal> {
Ok(black_scholes(self)?)
}
pub fn calculate_price_montecarlo(&self, prices: &[Positive]) -> OptionsResult<Positive> {
Ok(price_option_monte_carlo(self, prices)?)
}
pub fn calculate_price_telegraph(&self, no_steps: usize) -> OptionsResult<Decimal> {
Ok(telegraph(self, no_steps, None, None)?)
}
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,
};
let payoff = self.option_type.payoff(&payoff_info) * self.quantity.to_f64();
Ok(Decimal::from_f64(payoff).unwrap_or_default())
}
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,
};
let price = self.option_type.payoff(&payoff_info) * self.quantity.to_f64();
Ok(Decimal::from_f64(price).unwrap_or_default())
}
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,
};
let iv = self.option_type.payoff(&payoff_info) * self.quantity.to_f64();
Ok(Decimal::from_f64(iv).unwrap_or_default())
}
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 fn time_value(&self) -> OptionsResult<Decimal> {
let option_price = self.calculate_price_black_scholes()?.abs();
let intrinsic_value = self.intrinsic_value(self.underlying_price)?;
Ok((option_price - intrinsic_value).max(Decimal::ZERO))
}
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.risk_free_rate < Decimal::ZERO {
error!("Risk free rate is less than 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
}
pub fn calculate_implied_volatility(
&self,
market_price: Decimal,
) -> Result<Positive, VolatilityError> {
let is_short = self.is_short();
let target_price = if is_short {
-market_price
} else {
market_price
};
let mut high = pos_or_panic!(5.0); let mut low = Positive::ZERO;
for _ in 0..MAX_ITERATIONS_IV {
let mid_vol = (high.to_dec() + low.to_dec()) / Decimal::TWO;
let volatility = Positive::new_decimal(mid_vol)
.expect("mid_vol derived from Positive bounds is non-negative");
let mut option_copy = self.clone();
option_copy.implied_volatility = volatility;
let price = option_copy.calculate_price_black_scholes()?;
let actual_price = if is_short { -price } else { price };
if (actual_price - target_price).abs() < IV_TOLERANCE {
return Ok(volatility);
}
if actual_price > target_price {
high = volatility;
} else {
low = volatility;
}
if (high - low).to_dec() < dec!(0.0001) {
return Ok(volatility);
}
}
Err(VolatilityError::NoConvergence {
iterations: MAX_ITERATIONS_IV,
last_volatility: (high + low) / Positive::TWO,
})
}
}
impl TryFrom<&OptionData> for Options {
type Error = OptionsError;
fn try_from(option_data: &OptionData) -> Result<Self, Self::Error> {
let underlying_symbol =
option_data
.symbol
.clone()
.ok_or_else(|| OptionsError::ValidationError {
field: "symbol".to_string(),
reason: "OptionData must have a valid symbol".to_string(),
})?;
let expiration_date =
option_data
.expiration_date
.ok_or_else(|| OptionsError::ValidationError {
field: "expiration_date".to_string(),
reason: "OptionData must have a valid expiration date".to_string(),
})?;
let underlying_price = option_data
.underlying_price
.as_ref()
.map(|p| **p)
.ok_or_else(|| OptionsError::ValidationError {
field: "underlying_price".to_string(),
reason: "OptionData must have a valid underlying price".to_string(),
})?;
Ok(Options {
option_type: OptionType::European,
side: Side::Long,
underlying_symbol,
strike_price: option_data.strike_price,
expiration_date,
implied_volatility: option_data.implied_volatility,
quantity: Positive::ONE,
underlying_price,
risk_free_rate: option_data.risk_free_rate.unwrap_or(Decimal::ZERO),
option_style: OptionStyle::Call,
dividend_yield: option_data.dividend_yield.unwrap_or(Positive::ZERO),
exotic_params: None,
})
}
}
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,
underlying_price: Positive::ZERO,
risk_free_rate: Decimal::ZERO,
option_style: OptionStyle::Call,
dividend_yield: Positive::ZERO,
exotic_params: None,
}
}
}
impl Greeks for Options {
fn get_options(&self) -> Result<Vec<&Options>, GreeksError> {
Ok(vec![self])
}
}
impl PnLCalculator for Options {
fn calculate_pnl(
&self,
market_price: &Positive,
expiration_date: ExpirationDate,
implied_volatility: &Positive,
) -> Result<PnL, PricingError> {
let mut current_option = self.clone();
current_option.underlying_price = *market_price;
current_option.expiration_date = expiration_date;
current_option.implied_volatility = *implied_volatility;
let current_price = current_option.calculate_price_black_scholes()?;
let initial_price = self.calculate_price_black_scholes()?;
let (initial_costs, initial_income) = match self.side {
Side::Long => (initial_price * self.quantity, Decimal::ZERO),
Side::Short => (Decimal::ZERO, -initial_price * self.quantity),
};
let unrealized = Some((current_price - initial_price) * self.quantity);
Ok(PnL::new(
None, unrealized,
Positive::new_decimal(initial_costs)?,
Positive::new_decimal(initial_income)?,
current_option.expiration_date.get_date()?,
))
}
fn calculate_pnl_at_expiration(
&self,
underlying_price: &Positive,
) -> Result<PnL, PricingError> {
let realized = Some(self.payoff_at_price(underlying_price)?);
let initial_price = self.calculate_price_black_scholes()?;
let (initial_costs, initial_income) = match self.side {
Side::Long => (initial_price * self.quantity, Decimal::ZERO),
Side::Short => (Decimal::ZERO, initial_price * self.quantity),
};
Ok(PnL::new(
realized, None,
Positive::new_decimal(initial_costs)?,
Positive::new_decimal(initial_income)?,
self.expiration_date.get_date()?,
))
}
}
impl Profit for Options {
fn calculate_profit_at(&self, price: &Positive) -> Result<Decimal, PricingError> {
Ok(self.payoff_at_price(price)?)
}
}
impl BasicAble for Options {
fn get_title(&self) -> String {
format!(
"Underlying: {} @ ${:.0} {} {} {}",
self.underlying_symbol,
self.strike_price,
self.side,
self.option_style,
self.option_type
)
}
fn get_option_basic_type(&self) -> HashSet<OptionBasicType<'_>> {
let mut hash_set = HashSet::new();
hash_set.insert(OptionBasicType {
option_style: &self.option_style,
side: &self.side,
strike_price: &self.strike_price,
expiration_date: &self.expiration_date,
});
hash_set
}
fn get_symbol(&self) -> &str {
self.underlying_symbol.as_str()
}
fn get_strike(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
let option_basic_type = match self.get_option_basic_type().iter().next().copied() {
Some(option_basic_type) => option_basic_type,
None => return HashMap::new(),
};
HashMap::from([(option_basic_type, &self.strike_price)])
}
fn get_side(&self) -> HashMap<OptionBasicType<'_>, &Side> {
let option_basic_type = match self.get_option_basic_type().iter().next().copied() {
Some(option_basic_type) => option_basic_type,
None => return HashMap::new(),
};
HashMap::from([(option_basic_type, &self.side)])
}
fn get_type(&self) -> &OptionType {
&self.option_type
}
fn get_style(&self) -> HashMap<OptionBasicType<'_>, &OptionStyle> {
let option_basic_type = match self.get_option_basic_type().iter().next().copied() {
Some(option_basic_type) => option_basic_type,
None => return HashMap::new(),
};
HashMap::from([(option_basic_type, &self.option_style)])
}
fn get_expiration(&self) -> HashMap<OptionBasicType<'_>, &ExpirationDate> {
let option_basic_type = match self.get_option_basic_type().iter().next().copied() {
Some(option_basic_type) => option_basic_type,
None => return HashMap::new(),
};
HashMap::from([(option_basic_type, &self.expiration_date)])
}
fn get_implied_volatility(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
let option_basic_type = match self.get_option_basic_type().iter().next().copied() {
Some(option_basic_type) => option_basic_type,
None => return HashMap::new(),
};
HashMap::from([(option_basic_type, &self.implied_volatility)])
}
fn get_quantity(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
let option_basic_type = match self.get_option_basic_type().iter().next().copied() {
Some(option_basic_type) => option_basic_type,
None => return HashMap::new(),
};
HashMap::from([(option_basic_type, &self.quantity)])
}
fn get_underlying_price(&self) -> &Positive {
&self.underlying_price
}
fn get_risk_free_rate(&self) -> HashMap<OptionBasicType<'_>, &Decimal> {
let option_basic_type = match self.get_option_basic_type().iter().next().copied() {
Some(option_basic_type) => option_basic_type,
None => return HashMap::new(),
};
HashMap::from([(option_basic_type, &self.risk_free_rate)])
}
fn get_dividend_yield(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
let option_basic_type = match self.get_option_basic_type().iter().next().copied() {
Some(option_basic_type) => option_basic_type,
None => return HashMap::new(),
};
HashMap::from([(option_basic_type, &self.dividend_yield)])
}
fn one_option(&self) -> &Options {
self
}
fn one_option_mut(&mut self) -> &mut Options {
self
}
fn set_implied_volatility(&mut self, volatility: &Positive) -> Result<(), StrategyError> {
self.implied_volatility = *volatility;
Ok(())
}
fn set_underlying_price(&mut self, price: &Positive) -> Result<(), StrategyError> {
self.underlying_price = *price;
Ok(())
}
fn set_expiration_date(
&mut self,
expiration_date: ExpirationDate,
) -> Result<(), StrategyError> {
self.expiration_date = expiration_date;
Ok(())
}
}
impl Graph for Options {
fn graph_data(&self) -> GraphData {
let range = calculate_optimal_price_range(
self.underlying_price,
self.strike_price,
self.implied_volatility,
self.expiration_date,
)
.unwrap_or_else(|_| {
let lower = self.strike_price * Positive::new(0.5).unwrap_or(Positive::ONE);
let upper = self.strike_price * Positive::new(1.5).unwrap_or(Positive::ONE);
(lower, upper)
});
let mut positive_series = Series2D {
x: vec![],
y: vec![],
name: "Positive Payoff".to_string(),
mode: TraceMode::Lines,
line_color: Some("#2ca02c".to_string()),
line_width: Some(2.0),
};
let mut negative_series = Series2D {
x: vec![],
y: vec![],
name: "Negative Payoff".to_string(),
mode: TraceMode::Lines,
line_color: Some("#FF0000".to_string()),
line_width: Some(2.0),
};
for i in range.0.to_u64()..range.1.to_u64() {
let profit = self
.payoff_at_price(&Positive::new(i as f64).unwrap_or(Positive::ONE))
.unwrap_or_default();
match profit {
p if p == Decimal::ZERO => {
positive_series
.x
.push(Decimal::from_u64(i).unwrap_or_default());
positive_series.y.push(profit);
negative_series
.x
.push(Decimal::from_u64(i).unwrap_or_default());
negative_series.y.push(profit);
}
p if p > Decimal::ZERO => {
positive_series
.x
.push(Decimal::from_u64(i).unwrap_or_default());
positive_series.y.push(profit);
}
_ => {
negative_series
.x
.push(Decimal::from_u64(i).unwrap_or_default());
negative_series.y.push(profit);
}
}
}
let multi_series_2d = vec![positive_series, negative_series];
GraphData::MultiSeries(multi_series_2d)
}
fn graph_config(&self) -> GraphConfig {
let title = self.get_title();
let legend = Some(vec![title.clone()]);
GraphConfig {
title,
width: 1600,
height: 900,
x_label: Some("Underlying Price".to_string()),
y_label: Some("Profit/Loss".to_string()),
z_label: None,
line_style: LineStyle::Solid,
color_scheme: ColorScheme::Default,
legend,
show_legend: false,
}
}
}
#[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_calculate_price_binomial() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let price = option.calculate_price_binomial(100).unwrap();
assert!(price > Decimal::ZERO);
}
#[test]
fn test_calculate_price_binomial_tree() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let (price, asset_tree, option_tree) = option.calculate_price_binomial_tree(5).unwrap();
assert!(price > Decimal::ZERO);
assert_eq!(asset_tree.len(), 6);
assert_eq!(option_tree.len(), 6);
}
#[test]
fn test_calculate_price_binomial_tree_short() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Short);
let (price, asset_tree, option_tree) = option.calculate_price_binomial_tree(5).unwrap();
assert!(price > Decimal::ZERO);
assert_eq!(asset_tree.len(), 6);
assert_eq!(option_tree.len(), 6);
}
#[test]
fn test_calculate_price_black_scholes() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let price = option.calculate_price_black_scholes().unwrap();
assert!(price > Decimal::ZERO);
}
#[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); }
#[test]
fn test_calculate_time_value() {
let 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!(105.0),
dec!(0.05),
OptionStyle::Call,
Positive::ZERO,
None,
);
let time_value = option.time_value().unwrap();
assert!(time_value > Decimal::ZERO);
assert!(time_value < option.calculate_price_black_scholes().unwrap());
}
}
#[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,
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());
}
}
#[cfg(test)]
mod tests_time_value {
use super::*;
use crate::model::utils::create_sample_option_simplest_strike;
use crate::assert_decimal_eq;
use rust_decimal_macros::dec;
use tracing::debug;
#[test]
fn test_calculate_time_value_long_call() {
let option = create_sample_option_simplest_strike(
Side::Long,
OptionStyle::Call,
pos_or_panic!(105.0),
);
let time_value = option.time_value().unwrap();
assert!(time_value > Decimal::ZERO);
assert!(time_value <= option.calculate_price_black_scholes().unwrap());
}
#[test]
fn test_calculate_time_value_short_call() {
let option = create_sample_option_simplest_strike(
Side::Short,
OptionStyle::Call,
pos_or_panic!(105.0),
);
let time_value = option.time_value().unwrap();
assert!(time_value > Decimal::ZERO);
assert!(time_value <= option.calculate_price_black_scholes().unwrap().abs());
}
#[test]
fn test_calculate_time_value_long_put() {
let option =
create_sample_option_simplest_strike(Side::Long, OptionStyle::Put, pos_or_panic!(95.0));
let time_value = option.time_value().unwrap();
assert!(time_value > Decimal::ZERO);
assert!(time_value <= option.calculate_price_black_scholes().unwrap());
}
#[test]
fn test_calculate_time_value_short_put() {
let option = create_sample_option_simplest_strike(
Side::Short,
OptionStyle::Put,
pos_or_panic!(95.0),
);
let time_value = option.time_value().unwrap();
assert!(time_value > Decimal::ZERO);
assert!(time_value <= option.calculate_price_black_scholes().unwrap().abs());
}
#[test]
fn test_calculate_time_value_at_the_money() {
let call =
create_sample_option_simplest_strike(Side::Long, OptionStyle::Call, Positive::HUNDRED);
let put =
create_sample_option_simplest_strike(Side::Long, OptionStyle::Put, Positive::HUNDRED);
let call_time_value = call.time_value().unwrap();
let put_time_value = put.time_value().unwrap();
assert!(call_time_value > Decimal::ZERO);
assert!(put_time_value > Decimal::ZERO);
assert_eq!(
call_time_value,
call.calculate_price_black_scholes().unwrap()
);
assert_eq!(put_time_value, put.calculate_price_black_scholes().unwrap());
}
#[test]
fn test_calculate_time_value_deep_in_the_money() {
let call = create_sample_option_simplest_strike(
Side::Long,
OptionStyle::Call,
pos_or_panic!(150.0),
);
let put =
create_sample_option_simplest_strike(Side::Long, OptionStyle::Put, pos_or_panic!(50.0));
let call_time_value = call.time_value().unwrap();
let put_time_value = put.time_value().unwrap();
let call_price = call.calculate_price_black_scholes().unwrap();
let put_price = put.calculate_price_black_scholes().unwrap();
assert_decimal_eq!(call_time_value, call_price, dec!(0.01));
assert_decimal_eq!(put_time_value, put_price, dec!(0.01));
debug!("Call time value: {}", call_time_value);
debug!("Call BS price: {}", call_price);
debug!("Put time value: {}", put_time_value);
debug!("Put BS price: {}", put_price);
assert!(call_time_value <= call_price);
assert!(put_time_value <= put_price);
}
}
#[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); }
}
#[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_greeks {
use super::*;
use crate::assert_decimal_eq;
use crate::model::utils::create_sample_option_simplest;
use rust_decimal_macros::dec;
const EPSILON: Decimal = dec!(1e-6);
#[test]
fn test_delta() {
let delta = create_sample_option_simplest(OptionStyle::Call, Side::Long)
.delta()
.unwrap();
assert_decimal_eq!(delta, dec!(0.539519922), EPSILON);
}
#[test]
fn test_delta_size() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.quantity = Positive::TWO;
assert_decimal_eq!(option.delta().unwrap(), dec!(1.0790398), EPSILON);
}
#[test]
fn test_gamma() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
assert_decimal_eq!(option.gamma().unwrap(), dec!(0.0691707), EPSILON);
}
#[test]
fn test_gamma_size() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.quantity = Positive::TWO;
assert_decimal_eq!(option.gamma().unwrap(), dec!(0.1383415), EPSILON);
}
#[test]
fn test_theta() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
assert_decimal_eq!(option.theta().unwrap(), dec!(-0.043510019), EPSILON);
}
#[test]
fn test_theta_size() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.quantity = Positive::TWO;
assert_decimal_eq!(option.theta().unwrap(), dec!(-0.0870200), EPSILON);
}
#[test]
fn test_vega() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
assert_decimal_eq!(option.vega().unwrap(), dec!(0.113705366), EPSILON);
}
#[test]
fn test_vega_size() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.quantity = Positive::TWO;
assert_decimal_eq!(option.vega().unwrap(), dec!(0.2274107), EPSILON);
}
#[test]
fn test_rho() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
assert_decimal_eq!(option.rho().unwrap(), dec!(0.0423312145), EPSILON);
}
#[test]
fn test_rho_size() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.quantity = Positive::TWO;
assert_decimal_eq!(option.rho().unwrap(), dec!(0.08466242), EPSILON);
}
#[test]
fn test_rho_d() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
assert_decimal_eq!(option.rho_d().unwrap(), dec!(-0.04434410320), EPSILON);
}
#[test]
fn test_rho_d_size() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.quantity = Positive::TWO;
assert_decimal_eq!(option.rho_d().unwrap(), dec!(-0.0886882064063), EPSILON);
}
#[test]
fn test_vanna() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
assert_decimal_eq!(option.vanna().unwrap(), dec!(-0.085279024623), EPSILON);
}
#[test]
fn test_vanna_size() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.quantity = Positive::TWO;
assert_decimal_eq!(option.vanna().unwrap(), dec!(-0.170558049246), EPSILON);
}
#[test]
fn test_vomma() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
assert_decimal_eq!(option.vomma().unwrap(), dec!(0.002453232215), EPSILON);
}
#[test]
fn test_vomma_size() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.quantity = Positive::TWO;
assert_decimal_eq!(option.vomma().unwrap(), dec!(0.009812928860), EPSILON);
}
#[test]
fn test_veta() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
assert_decimal_eq!(option.veta().unwrap(), dec!(0.000027206189), EPSILON);
}
#[test]
fn test_veta_size() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.quantity = Positive::TWO;
assert_decimal_eq!(option.veta().unwrap(), dec!(0.000108824758), EPSILON);
}
#[test]
fn test_charm() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
assert_decimal_eq!(option.charm().unwrap(), dec!(-0.000458), EPSILON);
}
#[test]
fn test_charm_size() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.quantity = Positive::TWO;
assert_decimal_eq!(option.charm().unwrap(), dec!(-0.000917), EPSILON);
}
#[test]
fn test_color() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
assert_decimal_eq!(option.color().unwrap(), dec!(-0.001163), EPSILON);
}
#[test]
fn test_color_size() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.quantity = Positive::TWO;
assert_decimal_eq!(option.color().unwrap(), dec!(-0.002326), EPSILON);
}
}
#[cfg(test)]
mod tests_greek_trait {
use super::*;
use crate::assert_decimal_eq;
use crate::model::utils::create_sample_option_simplest;
use rust_decimal_macros::dec;
const EPSILON: Decimal = dec!(1e-6);
#[test]
fn test_greeks_implementation() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let greeks = option.greeks().unwrap();
assert_decimal_eq!(greeks.delta, option.delta().unwrap(), EPSILON);
assert_decimal_eq!(greeks.gamma, option.gamma().unwrap(), EPSILON);
assert_decimal_eq!(greeks.theta, option.theta().unwrap(), EPSILON);
assert_decimal_eq!(greeks.vega, option.vega().unwrap(), EPSILON);
assert_decimal_eq!(greeks.rho, option.rho().unwrap(), EPSILON);
assert_decimal_eq!(greeks.rho_d, option.rho_d().unwrap(), EPSILON);
assert_decimal_eq!(greeks.vanna, option.vanna().unwrap(), EPSILON);
assert_decimal_eq!(greeks.vomma, option.vomma().unwrap(), EPSILON);
assert_decimal_eq!(greeks.veta, option.veta().unwrap(), EPSILON);
assert_decimal_eq!(greeks.charm, option.charm().unwrap(), EPSILON);
assert_decimal_eq!(greeks.color, option.color().unwrap(), EPSILON);
}
#[test]
fn test_greeks_consistency() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let greeks = option.greeks().unwrap();
assert!(
greeks.delta >= Decimal::NEGATIVE_ONE && greeks.delta <= Decimal::ONE,
"Delta should be between -1 and 1"
);
assert!(
greeks.gamma >= Decimal::ZERO,
"Gamma should be non-negative"
);
assert!(greeks.vega >= Decimal::ZERO, "Vega should be non-negative");
}
#[test]
fn test_greeks_for_different_options() {
let call_option = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
pos_or_panic!(5790.0), ExpirationDate::Days(pos_or_panic!(18.0)),
pos_or_panic!(0.1), Positive::ONE, pos_or_panic!(5781.88), dec!(0.05), OptionStyle::Call,
Positive::ZERO, None,
);
let mut put_option = call_option.clone();
put_option.option_style = OptionStyle::Put;
let call_greeks = call_option.greeks().unwrap();
let put_greeks = put_option.greeks().unwrap();
assert_decimal_eq!(
call_greeks.delta + put_greeks.delta.abs(),
Decimal::ONE,
EPSILON
);
assert_decimal_eq!(call_greeks.gamma, put_greeks.gamma, EPSILON);
assert_decimal_eq!(call_greeks.vega, put_greeks.vega, EPSILON);
}
}
#[cfg(test)]
mod tests_calculate_price_binomial {
use super::*;
use crate::model::utils::{
create_sample_option, create_sample_option_simplest, create_sample_option_with_date,
};
use chrono::Utc;
use rust_decimal_macros::dec;
use std::str::FromStr;
#[test]
fn test_european_call_option_basic() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let result = option.calculate_price_binomial(100);
assert!(result.is_ok());
let price = result.unwrap();
assert!(price > Decimal::ZERO);
}
#[test]
fn test_american_put_option() {
let option = Options::new(
OptionType::American,
Side::Long,
"TEST".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,
Positive::ZERO, None,
);
let result = option.calculate_price_binomial(100);
assert!(result.is_ok());
let price = result.unwrap();
assert!(price > Decimal::ZERO);
}
#[test]
fn test_zero_volatility() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.implied_volatility = Positive::ZERO;
let result = option.calculate_price_binomial(100);
assert!(result.is_ok());
}
#[test]
fn test_zero_time_to_expiry() {
let now = Utc::now().naive_utc();
let option = create_sample_option_with_date(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
pos_or_panic!(95.0),
pos_or_panic!(0.2),
now,
);
let result = option.calculate_price_binomial(100);
assert!(result.is_ok());
let price = result.unwrap();
assert_eq!(price, Decimal::from(5));
}
#[test]
fn test_invalid_steps() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let result = option.calculate_price_binomial(0);
assert!(result.is_err());
}
#[test]
fn test_deep_itm_call() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(150.0), Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
);
let result = option.calculate_price_binomial(100);
assert!(result.is_ok());
let price = result.unwrap();
assert!(price > Decimal::from(45)); }
#[test]
fn test_deep_otm_put() {
let option = create_sample_option(
OptionStyle::Put,
Side::Long,
pos_or_panic!(150.0), Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
);
let result = option.calculate_price_binomial(100);
assert!(result.is_ok());
let price = result.unwrap();
assert!(price < Decimal::from(1));
}
#[test]
fn test_convergence() {
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let price_100 = option.calculate_price_binomial(100).unwrap();
let price_1000 = option.calculate_price_binomial(1000).unwrap();
let diff = (price_1000 - price_100).abs();
assert!(diff < Decimal::from_str("0.1").unwrap());
}
#[test]
fn test_short_position() {
let long_call_option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let mut short_call_option = long_call_option.clone();
short_call_option.side = Side::Short;
let mut short_put_option = short_call_option.clone();
short_put_option.option_style = OptionStyle::Put;
let mut long_put_option = short_put_option.clone();
long_put_option.side = Side::Long;
let long_call_price = long_call_option.calculate_price_binomial(100).unwrap();
let short_call_price = short_call_option.calculate_price_binomial(100).unwrap();
let long_put_price = long_put_option.calculate_price_binomial(100).unwrap();
let short_put_price = short_put_option.calculate_price_binomial(100).unwrap();
assert_eq!(long_call_price, -short_call_price);
assert_eq!(long_put_price, -short_put_price);
}
}
#[cfg(test)]
mod tests_options_black_scholes {
use super::*;
use crate::assert_decimal_eq;
use rust_decimal_macros::dec;
#[test]
fn test_new_option_call() {
let option = Options::new(
OptionType::European,
Side::Long,
"SP500".to_string(),
pos_or_panic!(5790.0),
ExpirationDate::Days(pos_or_panic!(18.0)),
pos_or_panic!(0.1117),
Positive::ONE,
pos_or_panic!(5781.88),
dec!(0.05),
OptionStyle::Call,
Positive::ZERO,
None,
);
assert_decimal_eq!(
option.calculate_price_black_scholes().unwrap(),
pos_or_panic!(60.306_765_882_668_3),
dec!(1e-8)
);
}
#[test]
fn test_new_option_call_bis() {
let option = Options::new(
OptionType::European,
Side::Long,
"SP500".to_string(),
pos_or_panic!(6050.0),
ExpirationDate::Days(pos_or_panic!(61.2)),
pos_or_panic!(0.12594),
Positive::ONE,
pos_or_panic!(6032.18),
dec!(0.0),
OptionStyle::Call,
Positive::ZERO,
None,
);
assert_decimal_eq!(
option.calculate_price_black_scholes().unwrap(),
pos_or_panic!(115.56),
dec!(1e-2)
);
}
#[test]
fn test_new_option_put() {
let option = Options::new(
OptionType::European,
Side::Long,
"SP500".to_string(),
pos_or_panic!(6050.0),
ExpirationDate::Days(pos_or_panic!(61.2)),
pos_or_panic!(0.1258),
Positive::ONE,
pos_or_panic!(6032.18),
dec!(0.0),
OptionStyle::Put,
Positive::ZERO,
None,
);
assert_decimal_eq!(
option.calculate_price_black_scholes().unwrap(),
pos_or_panic!(133.25),
dec!(1e-2)
);
}
#[test]
fn test_new_option_call_short() {
let option = Options::new(
OptionType::European,
Side::Short,
"SP500".to_string(),
pos_or_panic!(6050.0),
ExpirationDate::Days(pos_or_panic!(60.0)),
pos_or_panic!(0.12594),
Positive::ONE,
pos_or_panic!(6032.18),
dec!(0.0),
OptionStyle::Call,
Positive::ZERO,
None,
);
assert_decimal_eq!(
option.calculate_price_black_scholes().unwrap(),
dec!(-114.34),
dec!(1e-2)
);
}
#[test]
fn test_new_option_put_short() {
let option = Options::new(
OptionType::European,
Side::Short,
"SP500".to_string(),
pos_or_panic!(6050.0),
ExpirationDate::Days(pos_or_panic!(60.0)),
pos_or_panic!(0.12594),
Positive::ONE,
pos_or_panic!(6032.18),
dec!(0.0),
OptionStyle::Put,
Positive::ZERO,
None,
);
assert_decimal_eq!(
option.calculate_price_black_scholes().unwrap(),
dec!(-132.16),
dec!(1e-2)
);
}
}
#[cfg(test)]
mod tests_calculate_implied_volatility {
use super::*;
use crate::error::VolatilityError;
use positive::assert_pos_relative_eq;
use rust_decimal_macros::dec;
#[test]
fn test_implied_volatility_call() {
let option = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
pos_or_panic!(5790.0), ExpirationDate::Days(pos_or_panic!(18.0)),
pos_or_panic!(0.1), Positive::ONE, pos_or_panic!(5781.88), dec!(0.05), OptionStyle::Call,
Positive::ZERO, None,
);
let market_price = dec!(60.30);
let iv = option.calculate_implied_volatility(market_price).unwrap();
assert_pos_relative_eq!(
iv,
pos_or_panic!(0.111618041),
Positive::new_decimal(IV_TOLERANCE).unwrap()
);
}
#[test]
fn test_implied_volatility_put() {
let option = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
pos_or_panic!(6050.0), ExpirationDate::Days(pos_or_panic!(60.0)),
pos_or_panic!(0.1), Positive::ONE, pos_or_panic!(6032.18), dec!(0.0), OptionStyle::Put,
Positive::ZERO, None,
);
let market_price = dec!(132.16);
let iv = option.calculate_implied_volatility(market_price).unwrap();
assert_pos_relative_eq!(
iv,
pos_or_panic!(0.125961),
Positive::new_decimal(IV_TOLERANCE).unwrap()
);
}
#[test]
fn test_implied_volatility_call_short() {
let option = Options::new(
OptionType::European,
Side::Short,
"TEST".to_string(),
pos_or_panic!(6050.0), ExpirationDate::Days(pos_or_panic!(60.0)),
pos_or_panic!(0.1), Positive::ONE, pos_or_panic!(6032.18), dec!(0.0), OptionStyle::Call,
Positive::ZERO, None,
);
let market_price = dec!(-114.16);
let iv = option.calculate_implied_volatility(market_price).unwrap();
assert_pos_relative_eq!(
iv,
pos_or_panic!(0.1258087),
Positive::new_decimal(IV_TOLERANCE).unwrap()
);
}
#[test]
fn test_implied_volatility_put_short() {
let option = Options::new(
OptionType::European,
Side::Short,
"TEST".to_string(),
pos_or_panic!(6050.0), ExpirationDate::Days(pos_or_panic!(60.0)),
pos_or_panic!(0.1), Positive::ONE, pos_or_panic!(6032.18), dec!(0.0), OptionStyle::Put,
Positive::ZERO, None,
);
let market_price = dec!(-132.27);
let iv = option.calculate_implied_volatility(market_price).unwrap();
assert_pos_relative_eq!(
iv,
pos_or_panic!(0.12611389),
Positive::new_decimal(IV_TOLERANCE).unwrap()
);
}
#[test]
fn test_invalid_market_price() {
let option = Options::default();
let result = option.calculate_implied_volatility(Decimal::ZERO);
assert!(matches!(result, Err(VolatilityError::Options(_))));
}
#[test]
fn test_expired_option() {
let option = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(Positive::ZERO),
pos_or_panic!(0.2),
Positive::ONE,
Positive::HUNDRED,
dec!(0.05),
OptionStyle::Call,
Positive::ZERO,
None,
);
let result = option.calculate_implied_volatility(dec!(2.5));
assert!(matches!(result, Err(VolatilityError::Options(_))));
}
#[test]
fn test_convergence_edge_cases() {
let option = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
pos_or_panic!(5790.0), ExpirationDate::Days(pos_or_panic!(18.0)),
pos_or_panic!(0.1), Positive::ONE, pos_or_panic!(5781.88), dec!(0.05), OptionStyle::Call,
Positive::ZERO, None,
);
let iv = option.calculate_implied_volatility(dec!(60.30)).unwrap();
assert_pos_relative_eq!(iv, pos_or_panic!(0.111328125), pos_or_panic!(0.01));
let iv = option.calculate_implied_volatility(dec!(60.30)).unwrap();
assert_pos_relative_eq!(iv, pos_or_panic!(0.111328125), pos_or_panic!(0.01));
}
}
#[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);
}
}