use crate::chains::utils::{OptionDataPriceParams, default_empty_string, empty_string_round_to_2};
use crate::chains::{DeltasInStrike, OptionsInStrike};
use crate::error::ChainError;
use crate::error::chains::OptionDataErrorKind;
use crate::greeks::{delta, gamma};
use crate::model::Position;
use crate::strategies::{BasicAble, FindOptimalSide};
use crate::{ExpirationDate, OptionStyle, Options, Side};
use chrono::{DateTime, Utc};
use positive::Positive;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::cmp::Ordering;
use std::fmt;
use tracing::{debug, error, trace};
use utoipa::ToSchema;
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
pub struct OptionData {
#[serde(rename = "strike_price")]
pub strike_price: Positive,
#[serde(skip_serializing_if = "Option::is_none")]
pub call_bid: Option<Positive>,
#[serde(skip_serializing_if = "Option::is_none")]
pub call_ask: Option<Positive>,
#[serde(skip_serializing_if = "Option::is_none")]
pub put_bid: Option<Positive>,
#[serde(skip_serializing_if = "Option::is_none")]
pub put_ask: Option<Positive>,
#[serde(skip_serializing_if = "Option::is_none")]
pub call_middle: Option<Positive>,
#[serde(skip_serializing_if = "Option::is_none")]
pub put_middle: Option<Positive>,
#[serde(default)]
pub implied_volatility: Positive,
#[serde(skip_serializing_if = "Option::is_none")]
pub delta_call: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delta_put: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gamma: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume: Option<Positive>,
#[serde(skip_serializing_if = "Option::is_none")]
pub open_interest: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expiration_date: Option<ExpirationDate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub underlying_price: Option<Box<Positive>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub risk_free_rate: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dividend_yield: Option<Positive>,
#[serde(skip_serializing_if = "Option::is_none")]
pub epic: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extra_fields: Option<Value>,
}
impl OptionData {
#[allow(clippy::too_many_arguments)]
pub fn new(
strike_price: Positive,
call_bid: Option<Positive>,
call_ask: Option<Positive>,
put_bid: Option<Positive>,
put_ask: Option<Positive>,
implied_volatility: Positive,
delta_call: Option<Decimal>,
delta_put: Option<Decimal>,
gamma: Option<Decimal>,
volume: Option<Positive>,
open_interest: Option<u64>,
symbol: Option<String>,
expiration_date: Option<ExpirationDate>,
underlying_price: Option<Box<Positive>>,
risk_free_rate: Option<Decimal>,
dividend_yield: Option<Positive>,
epic: Option<String>,
extra_fields: Option<Value>,
) -> Self {
OptionData {
strike_price,
call_bid,
call_ask,
put_bid,
put_ask,
call_middle: None,
put_middle: None,
implied_volatility,
delta_call,
delta_put,
gamma,
volume,
open_interest,
symbol,
expiration_date,
underlying_price,
risk_free_rate,
dividend_yield,
epic,
extra_fields,
}
}
pub fn get_call_spread(&self) -> Option<Positive> {
match (self.call_bid, self.call_ask) {
(Some(call_bid), Some(call_ask)) => {
let spread = (call_ask.to_dec() - call_bid.to_dec()).abs();
Positive::new_decimal(spread).ok()
}
_ => None,
}
}
pub fn get_call_spread_per(&self) -> Option<Positive> {
match (self.call_bid, self.call_ask) {
(Some(call_bid), Some(call_ask)) => {
let spread = (call_ask.to_dec() - call_bid.to_dec()).abs();
let mid_price = (call_ask + call_bid) / 2.0;
Positive::new_decimal(spread).ok().map(|s| s / mid_price)
}
_ => None,
}
}
pub fn get_put_spread(&self) -> Option<Positive> {
match (self.put_bid, self.put_ask) {
(Some(put_bid), Some(put_ask)) => {
let spread = (put_ask.to_dec() - put_bid.to_dec()).abs();
Positive::new_decimal(spread).ok()
}
_ => None,
}
}
pub fn get_put_spread_per(&self) -> Option<Positive> {
match (self.put_bid, self.put_ask) {
(Some(put_bid), Some(put_ask)) => {
let spread = (put_ask.to_dec() - put_bid.to_dec()).abs();
let mid_price = (put_ask + put_bid) / 2.0;
Positive::new_decimal(spread).ok().map(|s| s / mid_price)
}
_ => None,
}
}
pub fn get_volatility(&self) -> Positive {
self.implied_volatility
}
pub fn set_volatility(&mut self, volatility: &Positive) {
self.implied_volatility = *volatility;
}
pub fn set_extra_params(&mut self, params: OptionDataPriceParams) {
if let Some(symbol) = params.underlying_symbol {
self.symbol = Some(symbol);
};
if let Some(expiration_date) = params.expiration_date {
self.expiration_date = Some(expiration_date);
};
if let Some(underlying_price) = params.underlying_price {
self.underlying_price = Some(underlying_price);
};
if let Some(risk_free_rate) = params.risk_free_rate {
self.risk_free_rate = Some(risk_free_rate);
};
if let Some(dividend_yield) = params.dividend_yield {
self.dividend_yield = Some(dividend_yield);
};
}
pub fn validate(&self) -> bool {
if self.strike_price == Positive::ZERO {
error!("Error: Strike price cannot be zero");
return false;
}
if !self.valid_call() || !self.valid_put() {
error!(
"Error: No valid prices for call or put options {} Deltas C {:?} P {:?}",
self.strike_price, self.delta_call, self.delta_put
);
return false;
}
true
}
pub fn strike(&self) -> Positive {
self.strike_price
}
pub(crate) fn valid_call(&self) -> bool {
self.strike_price > Positive::ZERO && self.call_bid.is_some() && self.call_ask.is_some()
}
pub(crate) fn valid_put(&self) -> bool {
self.strike_price > Positive::ZERO && self.put_bid.is_some() && self.put_ask.is_some()
}
pub fn get_call_buy_price(&self) -> Option<Positive> {
self.call_ask
}
pub fn get_call_sell_price(&self) -> Option<Positive> {
self.call_bid
}
pub fn get_put_buy_price(&self) -> Option<Positive> {
self.put_ask
}
pub fn get_put_sell_price(&self) -> Option<Positive> {
self.put_bid
}
pub fn some_price_is_none(&self) -> bool {
self.call_bid.is_none()
|| self.call_ask.is_none()
|| self.put_bid.is_none()
|| self.put_ask.is_none()
}
pub(super) fn get_option(
&self,
side: Side,
option_style: OptionStyle,
) -> Result<Options, ChainError> {
let mut option = Options::try_from(self)
.map_err(|e| ChainError::OptionDataError(OptionDataErrorKind::Other(e.to_string())))?;
option.side = side;
option.option_style = option_style;
Ok(option)
}
#[allow(dead_code)]
fn get_option_for_iv(
&self,
side: Side,
option_style: OptionStyle,
initial_iv: Positive,
) -> Result<Options, ChainError> {
let mut option = self.get_option(side, option_style)?;
let _ = option.set_implied_volatility(&initial_iv);
Ok(option)
}
pub fn get_position(
&self,
side: Side,
option_style: OptionStyle,
date: Option<DateTime<Utc>>,
open_fee: Option<Positive>,
close_fee: Option<Positive>,
) -> Result<Position, ChainError> {
let option = self.get_option(side, option_style)?;
let premium = match (side, option_style) {
(Side::Long, OptionStyle::Call) => self.get_call_buy_price(),
(Side::Short, OptionStyle::Call) => self.get_call_sell_price(),
(Side::Long, OptionStyle::Put) => self.get_put_buy_price(),
(Side::Short, OptionStyle::Put) => self.get_put_sell_price(),
};
let premium = match premium {
Some(premium) => premium,
None => {
let premium_dec = option.calculate_price_black_scholes()?.abs();
Positive::new_decimal(premium_dec)?
}
};
let date = if let Some(date) = date {
date
} else {
Utc::now()
};
let open_fee = if let Some(open_fee) = open_fee {
open_fee
} else {
Positive::ZERO
};
let close_fee = if let Some(close_fee) = close_fee {
close_fee
} else {
Positive::ZERO
};
Ok(Position::new(
option,
premium,
date,
open_fee,
close_fee,
self.epic.clone(),
self.extra_fields.clone(),
))
}
pub(super) fn get_options_in_strike(&self) -> Result<OptionsInStrike, ChainError> {
let mut option: Options = self.get_option(Side::Long, OptionStyle::Call)?;
option.option_style = OptionStyle::Call;
option.side = Side::Long;
let long_call = option.clone();
option.side = Side::Short;
let short_call = option.clone();
option.option_style = OptionStyle::Put;
let short_put = option.clone();
option.side = Side::Long;
let long_put = option.clone();
Ok(OptionsInStrike {
long_call,
short_call,
long_put,
short_put,
})
}
pub fn calculate_prices(&mut self, spread: Option<Positive>) -> Result<(), ChainError> {
let call_option = self.get_option(Side::Long, OptionStyle::Call)?;
match (
call_option.calculate_price_black_scholes(),
spread.is_some(),
) {
(Ok(price), true) => {
if price.is_sign_positive() {
self.call_middle = Positive::new_decimal(price).ok();
self.apply_spread(spread.unwrap(), 2);
}
}
(Ok(price), false) => {
self.call_middle = Positive::new_decimal(price).ok();
self.call_ask = self.call_middle;
self.call_bid = self.call_middle;
}
_ => {
debug!("calculate_prices: Failed to calculate call option price");
self.call_middle = None;
self.call_ask = None;
self.call_bid = None;
}
};
let put_option = self.get_option(Side::Long, OptionStyle::Put)?;
match (put_option.calculate_price_black_scholes(), spread.is_some()) {
(Ok(price), true) => {
if price.is_sign_positive() {
self.put_middle = Positive::new_decimal(price).ok();
self.apply_spread(spread.unwrap(), 2);
}
}
(Ok(price), false) => {
self.put_middle = Positive::new_decimal(price).ok();
self.put_ask = self.put_middle;
self.put_bid = self.put_middle;
}
_ => {
debug!("calculate_prices: Failed to calculate put option price");
self.put_middle = None;
self.put_ask = None;
self.put_bid = None;
}
};
Ok(())
}
pub fn apply_spread(&mut self, spread: Positive, decimal_places: u32) {
let half_spread: Decimal = (spread / Positive::TWO).into();
match (self.call_ask, self.call_bid, self.call_middle) {
(_, _, Some(call_middle)) => {
if self.call_middle.unwrap() > spread {
self.call_ask = Some((call_middle + half_spread).round_to(decimal_places));
self.call_bid = Some(
call_middle
.sub_or_zero(&half_spread)
.round_to(decimal_places),
);
} else {
trace!(
"apply_spread: Call middle price is not greater than spread, cannot apply spread"
);
self.call_ask = None;
self.call_bid = None;
self.call_middle = None;
}
}
(Some(call_ask), Some(call_bid), None) => {
trace!("apply_spread: Call middle price is None, cannot apply spread");
self.call_ask = Some((call_ask + half_spread).round_to(decimal_places));
self.call_bid = Some(call_bid.sub_or_zero(&half_spread).round_to(decimal_places));
self.call_middle = Some(
((self.call_ask.unwrap() + self.call_bid.unwrap()) / Positive::TWO)
.round_to(decimal_places),
);
}
_ => {
trace!("apply_spread: Missing call ask or bid prices, cannot apply spread");
self.call_ask = None;
self.call_bid = None;
}
}
match (self.put_ask, self.put_bid, self.put_middle) {
(_, _, Some(put_middle)) => {
if self.put_middle.unwrap() > spread {
self.put_ask = Some((put_middle + half_spread).round_to(decimal_places));
self.put_bid = Some(
put_middle
.sub_or_zero(&half_spread)
.round_to(decimal_places),
);
} else {
trace!(
"apply_spread: Put middle price is not greater than spread, cannot apply spread"
);
self.put_ask = None;
self.put_bid = None;
self.put_middle = None;
}
}
(Some(put_ask), Some(put_bid), None) => {
trace!("apply_spread: Put middle price is None, cannot apply spread");
self.put_ask = Some((put_ask + half_spread).round_to(decimal_places));
self.put_bid = Some(put_bid.sub_or_zero(&half_spread).round_to(decimal_places));
self.put_middle = Some(
((self.put_ask.unwrap() + self.put_bid.unwrap()) / Positive::TWO)
.round_to(decimal_places),
);
}
_ => {
trace!("apply_spread: Missing put ask or bid prices, cannot apply spread");
self.put_ask = None;
self.put_bid = None;
}
}
}
pub fn calculate_delta(&mut self) {
let option: Options = match self.get_option(Side::Long, OptionStyle::Call) {
Ok(option) => option,
Err(e) => {
debug!("Failed to get option for delta calculation: {}", e);
return;
}
};
match delta(&option) {
Ok(d) => self.delta_call = Some(d),
Err(e) => {
debug!("Delta calculation failed: {}", e);
self.delta_call = None;
}
}
let option: Options = match self.get_option(Side::Long, OptionStyle::Put) {
Ok(option) => option,
Err(e) => {
debug!("Failed to get option for delta calculation: {}", e);
return;
}
};
match delta(&option) {
Ok(d) => self.delta_put = Some(d),
Err(e) => {
debug!("Delta calculation failed: {}", e);
self.delta_put = None;
}
}
}
pub fn calculate_gamma(&mut self) {
let option: Options = match self.get_option(Side::Long, OptionStyle::Call) {
Ok(option) => option,
Err(e) => {
debug!("Failed to get option for delta calculation: {}", e);
return;
}
};
match gamma(&option) {
Ok(d) => self.gamma = Some(d),
Err(e) => {
debug!("Gamma calculation failed: {}", e);
self.gamma = None;
}
}
}
pub fn get_deltas(&self) -> Result<DeltasInStrike, ChainError> {
let options_in_strike = self.get_options_in_strike()?;
Ok(options_in_strike.deltas()?)
}
pub fn is_valid_optimal_side(
&self,
underlying_price: &Positive,
side: &FindOptimalSide,
) -> bool {
match side {
FindOptimalSide::Upper => &self.strike_price >= underlying_price,
FindOptimalSide::Lower => &self.strike_price <= underlying_price,
FindOptimalSide::All => true,
FindOptimalSide::Range(start, end) => {
self.strike_price >= *start && self.strike_price <= *end
}
FindOptimalSide::Deltable(_threshold) => true,
FindOptimalSide::Center => {
panic!("Center should be managed by the strategy");
}
FindOptimalSide::DeltaRange(min, max) => {
(self.delta_put.is_some()
&& self.delta_put.unwrap() >= *min
&& self.delta_put.unwrap() <= *max)
|| (self.delta_call.is_some()
&& self.delta_call.unwrap() >= *min
&& self.delta_call.unwrap() <= *max)
}
}
}
pub fn set_mid_prices(&mut self) {
self.call_middle = match (self.call_bid, self.call_ask) {
(Some(bid), Some(ask)) => Some(((bid + ask) / Positive::TWO).round_to(4)),
_ => None,
};
self.put_middle = match (self.put_bid, self.put_ask) {
(Some(bid), Some(ask)) => Some(((bid + ask) / Positive::TWO).round_to(4)),
_ => None,
};
}
pub fn get_mid_prices(&self) -> (Option<Positive>, Option<Positive>) {
(self.call_middle, self.put_middle)
}
pub(super) fn check_and_convert_implied_volatility(&mut self) {
if self.implied_volatility > Positive::ONE {
self.implied_volatility = self.implied_volatility / Positive::HUNDRED;
}
}
pub fn current_deltas(&self) -> (Option<Decimal>, Option<Decimal>) {
(self.delta_call, self.delta_put)
}
pub fn current_gamma(&self) -> Option<Decimal> {
self.gamma
}
}
impl Default for OptionData {
fn default() -> Self {
OptionData {
strike_price: Positive::ZERO,
call_bid: None,
call_ask: None,
put_bid: None,
put_ask: None,
call_middle: None,
put_middle: None,
implied_volatility: Positive::ZERO,
delta_call: None,
delta_put: None,
gamma: None,
volume: None,
open_interest: None,
symbol: None,
expiration_date: None,
underlying_price: None,
risk_free_rate: None,
dividend_yield: None,
epic: None,
extra_fields: None,
}
}
}
impl PartialOrd for OptionData {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Eq for OptionData {}
impl Ord for OptionData {
fn cmp(&self, other: &Self) -> Ordering {
self.strike_price.cmp(&other.strike_price)
}
}
impl fmt::Display for OptionData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:<10} {:<10} {:<10} {:<10} {:<10} {:<10} {:<10} {:<6}{:<7} {:.3}{:<4} {:.3}{:<5} {:.4}{:<8} {:<10} {:<10}",
self.strike_price.to_string(),
empty_string_round_to_2(self.call_bid),
empty_string_round_to_2(self.call_ask),
empty_string_round_to_2(self.call_middle),
empty_string_round_to_2(self.put_bid),
empty_string_round_to_2(self.put_ask),
empty_string_round_to_2(self.put_middle),
self.implied_volatility.format_fixed_places(3),
" ".to_string(),
self.delta_call.unwrap_or(Decimal::ZERO),
" ".to_string(),
self.delta_put.unwrap_or(Decimal::ZERO),
" ".to_string(),
self.gamma.unwrap_or(Decimal::ZERO) * Decimal::ONE_HUNDRED,
" ".to_string(),
default_empty_string(self.volume),
default_empty_string(self.open_interest),
)?;
Ok(())
}
}
#[cfg(test)]
mod optiondata_coverage_tests {
use super::*;
use positive::{pos_or_panic, spos};
use rust_decimal_macros::dec;
fn create_test_option_data() -> OptionData {
OptionData::new(
Positive::HUNDRED,
spos!(9.5),
spos!(10.0),
spos!(8.5),
spos!(9.0),
pos_or_panic!(0.2),
Some(dec!(-0.3)),
Some(dec!(0.7)),
Some(dec!(0.5)),
spos!(1000.0),
Some(500),
Some("TEST".to_string()), Some(ExpirationDate::Days(pos_or_panic!(30.0))), Some(Box::new(Positive::HUNDRED)), Some(dec!(0.05)), Some(pos_or_panic!(0.02)), None,
None,
)
}
#[test]
fn test_current_deltas() {
let option_data = create_test_option_data();
let (call_delta, put_delta) = option_data.current_deltas();
assert!(call_delta.is_some());
assert!(put_delta.is_some());
assert_eq!(call_delta.unwrap(), dec!(-0.3));
assert_eq!(put_delta.unwrap(), dec!(0.7));
}
#[test]
fn test_calculate_prices_with_refresh() {
let mut option_data = create_test_option_data();
option_data.set_volatility(&pos_or_panic!(0.25));
let result = option_data.calculate_prices(None);
assert!(result.is_ok());
assert!(option_data.call_bid.is_some());
assert!(option_data.call_ask.is_some());
assert!(option_data.put_bid.is_some());
assert!(option_data.put_ask.is_some());
assert!(option_data.call_middle.is_some());
assert!(option_data.put_middle.is_some());
}
#[test]
fn test_apply_spread_call() {
let mut option_data = create_test_option_data();
let original_call_bid = option_data.call_bid;
let original_call_ask = option_data.call_ask;
option_data.apply_spread(pos_or_panic!(0.6), 2);
assert_ne!(option_data.call_bid, original_call_bid);
assert_ne!(option_data.call_ask, original_call_ask);
let mut option_data = create_test_option_data();
option_data.call_bid = spos!(0.1);
option_data.apply_spread(Positive::ONE, 2);
assert_eq!(option_data.call_bid, Some(Positive::ZERO));
}
#[test]
fn test_apply_spread_put() {
let mut option_data = create_test_option_data();
let original_put_bid = option_data.put_bid;
let original_put_ask = option_data.put_ask;
option_data.apply_spread(pos_or_panic!(0.6), 2);
assert_ne!(option_data.put_bid, original_put_bid);
assert_ne!(option_data.put_ask, original_put_ask);
let mut option_data = create_test_option_data();
option_data.put_bid = spos!(0.1);
option_data.apply_spread(Positive::ONE, 2);
assert_eq!(option_data.put_bid, Some(Positive::ZERO));
}
#[test]
fn test_calculate_gamma_no_implied_volatility() {
let mut option_data = create_test_option_data();
option_data.set_volatility(&pos_or_panic!(0.2));
option_data.calculate_gamma();
assert!(option_data.gamma.is_some());
}
#[test]
fn test_get_deltas() {
let option_data = create_test_option_data();
let result = option_data.get_deltas();
assert!(result.is_ok());
let deltas = result.unwrap();
assert!(deltas.long_call != dec!(0.0));
assert!(deltas.short_call != dec!(0.0));
assert!(deltas.long_put != dec!(0.0));
assert!(deltas.short_put != dec!(0.0));
}
}
#[cfg(test)]
mod tests_get_position {
use super::*;
use crate::model::ExpirationDate;
use chrono::{Duration, Utc};
use positive::{assert_pos_relative_eq, pos_or_panic, spos};
use rust_decimal_macros::dec;
fn create_test_option_data() -> OptionData {
OptionData::new(
Positive::HUNDRED, spos!(9.5), spos!(10.0), spos!(8.5), spos!(9.0), pos_or_panic!(0.2), Some(dec!(-0.3)), Some(dec!(0.7)), Some(dec!(0.5)), spos!(1000.0), Some(500), Some("TEST".to_string()), Some(ExpirationDate::Days(pos_or_panic!(30.0))), Some(Box::new(Positive::HUNDRED)), Some(dec!(0.05)), Some(pos_or_panic!(0.02)), None,
None,
)
}
fn create_test_price_params() -> OptionDataPriceParams {
OptionDataPriceParams::new(
Some(Box::new(Positive::HUNDRED)),
Some(ExpirationDate::Days(pos_or_panic!(30.0))),
Some(dec!(0.05)),
spos!(0.02),
Some("AAPL".to_string()),
)
}
#[test]
fn test_get_position_long_call() {
let option_data = create_test_option_data();
let result = option_data.get_position(
Side::Long,
OptionStyle::Call,
None, None, None, );
assert!(result.is_ok(), "Should successfully create position");
let position = result.unwrap();
assert_eq!(position.option.side, Side::Long);
assert_eq!(position.option.option_style, OptionStyle::Call);
assert_eq!(position.option.strike_price, Positive::HUNDRED);
assert!(
position.premium > Positive::ZERO,
"Premium should be positive"
);
assert_eq!(position.open_fee, Positive::ZERO);
assert_eq!(position.close_fee, Positive::ZERO);
}
#[test]
fn test_get_position_short_put() {
let option_data = create_test_option_data();
let result = option_data.get_position(
Side::Short,
OptionStyle::Put,
None, None, None, );
assert!(result.is_ok(), "Should successfully create position");
let position = result.unwrap();
assert_eq!(position.option.side, Side::Short);
assert_eq!(position.option.option_style, OptionStyle::Put);
assert_eq!(position.option.strike_price, Positive::HUNDRED);
assert!(
position.premium > Positive::ZERO,
"Premium should be positive"
);
}
#[test]
fn test_get_position_with_custom_date() {
let option_data = create_test_option_data();
let custom_date = Utc::now() - Duration::days(7);
let result =
option_data.get_position(Side::Long, OptionStyle::Call, Some(custom_date), None, None);
assert!(result.is_ok());
let position = result.unwrap();
assert_eq!(position.date, custom_date);
}
#[test]
fn test_get_position_with_fees() {
let option_data = create_test_option_data();
let open_fee = pos_or_panic!(1.5);
let close_fee = Positive::TWO;
let result = option_data.get_position(
Side::Long,
OptionStyle::Put,
None,
Some(open_fee),
Some(close_fee),
);
assert!(result.is_ok());
let position = result.unwrap();
assert_eq!(position.open_fee, open_fee);
assert_eq!(position.close_fee, close_fee);
}
#[test]
fn test_get_position_in_the_money_call() {
let mut option_data = create_test_option_data();
let mut price_params = create_test_price_params();
price_params.underlying_price = Some(Box::new(pos_or_panic!(120.0)));
option_data.set_extra_params(price_params);
let result = option_data.get_position(Side::Long, OptionStyle::Call, None, None, None);
assert!(result.is_ok());
let position = result.unwrap();
assert!(
position.premium >= pos_or_panic!(10.0),
"ITM call premium should be significant"
);
}
#[test]
fn test_get_position_all_combinations() {
let option_data = create_test_option_data();
let combinations = vec![
(Side::Long, OptionStyle::Call),
(Side::Long, OptionStyle::Put),
(Side::Short, OptionStyle::Call),
(Side::Short, OptionStyle::Put),
];
for (side, style) in combinations {
let result = option_data.get_position(side, style, None, None, None);
assert!(
result.is_ok(),
"Failed to create position: {side:?}, {style:?}"
);
let position = result.unwrap();
assert_eq!(position.option.side, side);
assert_eq!(position.option.option_style, style);
assert!(position.premium > Positive::ZERO);
}
}
#[test]
fn test_get_position_with_custom_all_params() {
let option_data = create_test_option_data();
let custom_date = Utc::now() - Duration::days(14);
let open_fee = pos_or_panic!(2.5);
let close_fee = pos_or_panic!(1.75);
let result = option_data.get_position(
Side::Short,
OptionStyle::Put,
Some(custom_date),
Some(open_fee),
Some(close_fee),
);
assert!(result.is_ok());
let position = result.unwrap();
assert_eq!(position.option.side, Side::Short);
assert_eq!(position.option.option_style, OptionStyle::Put);
assert_eq!(position.date, custom_date);
assert_eq!(position.open_fee, open_fee);
assert_eq!(position.close_fee, close_fee);
}
#[test]
fn test_get_position_uses_market_price_long_call() {
let option_data = create_test_option_data();
let result = option_data.get_position(
Side::Long,
OptionStyle::Call,
None, None, None, );
assert!(result.is_ok(), "Should successfully create position");
let position = result.unwrap();
assert_eq!(
position.premium,
pos_or_panic!(10.0),
"Should use call_ask price for long call"
);
}
#[test]
fn test_get_position_uses_market_price_short_call() {
let option_data = create_test_option_data();
let result = option_data.get_position(
Side::Short,
OptionStyle::Call,
None, None, None, );
assert!(result.is_ok(), "Should successfully create position");
let position = result.unwrap();
assert_eq!(
position.premium,
pos_or_panic!(9.5),
"Should use call_bid price for short call"
);
}
#[test]
fn test_get_position_uses_market_price_long_put() {
let option_data = create_test_option_data();
let result = option_data.get_position(
Side::Long,
OptionStyle::Put,
None, None, None, );
assert!(result.is_ok(), "Should successfully create position");
let position = result.unwrap();
assert_eq!(
position.premium,
pos_or_panic!(9.0),
"Should use put_ask price for long put"
);
}
#[test]
fn test_get_position_uses_market_price_short_put() {
let option_data = create_test_option_data();
let result = option_data.get_position(
Side::Short,
OptionStyle::Put,
None, None, None, );
assert!(result.is_ok(), "Should successfully create position");
let position = result.unwrap();
assert_eq!(
position.premium,
pos_or_panic!(8.5),
"Should use put_bid price for short put"
);
}
#[test]
fn test_get_position_fallback_to_black_scholes() {
let option_data = OptionData::new(
Positive::HUNDRED, None, None, None, None, pos_or_panic!(0.2), Some(dec!(-0.3)), Some(dec!(0.7)), Some(dec!(0.5)), spos!(1000.0), Some(500), Some("TEST".to_string()), Some(ExpirationDate::Days(pos_or_panic!(30.0))), Some(Box::new(Positive::HUNDRED)), Some(dec!(0.05)), Some(pos_or_panic!(0.02)), None,
None,
);
let result = option_data.get_position(Side::Long, OptionStyle::Call, None, None, None);
assert!(
result.is_ok(),
"Should successfully create position using Black-Scholes"
);
let position = result.unwrap();
assert!(
position.premium > Positive::ZERO,
"Should calculate premium using Black-Scholes"
);
let option = option_data
.get_option(Side::Long, OptionStyle::Call)
.unwrap();
let bs_price = option.calculate_price_black_scholes().unwrap().abs();
let bs_price_positive = Positive::new_decimal(bs_price).unwrap();
assert_pos_relative_eq!(position.premium, bs_price_positive, pos_or_panic!(0.00001));
}
#[test]
fn test_get_position_with_custom_date_uses_market_price() {
let option_data = create_test_option_data();
let custom_date = Utc::now() - Duration::days(7);
let result =
option_data.get_position(Side::Long, OptionStyle::Call, Some(custom_date), None, None);
assert!(result.is_ok());
let position = result.unwrap();
assert_eq!(position.date, custom_date);
assert_eq!(
position.premium,
pos_or_panic!(10.0),
"Should use call_ask even with custom date"
);
}
#[test]
fn test_get_position_with_fees_uses_market_price() {
let option_data = create_test_option_data();
let open_fee = pos_or_panic!(1.5);
let close_fee = Positive::TWO;
let result = option_data.get_position(
Side::Short,
OptionStyle::Put,
None,
Some(open_fee),
Some(close_fee),
);
assert!(result.is_ok());
let position = result.unwrap();
assert_eq!(position.open_fee, open_fee);
assert_eq!(position.close_fee, close_fee);
assert_eq!(
position.premium,
pos_or_panic!(8.5),
"Should use put_bid even with custom fees"
);
}
#[test]
fn test_get_position_missing_specific_price() {
let mut option_data = create_test_option_data();
option_data.call_ask = None;
let result = option_data.get_position(Side::Long, OptionStyle::Call, None, None, None);
assert!(
result.is_ok(),
"Should fall back to Black-Scholes when specific price is missing"
);
let position = result.unwrap();
let option = option_data
.get_option(Side::Long, OptionStyle::Call)
.unwrap();
let bs_price = option.calculate_price_black_scholes().unwrap().abs();
let bs_price_positive = Positive::new_decimal(bs_price).unwrap();
assert_pos_relative_eq!(position.premium, bs_price_positive, pos_or_panic!(0.00001));
}
}
#[cfg(test)]
mod tests_check_convert_implied_volatility {
use super::*;
use positive::pos_or_panic;
#[test]
fn test_check_and_convert_implied_volatility_over_one() {
let mut option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
None,
pos_or_panic!(20.0), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
option_data.check_and_convert_implied_volatility();
assert_eq!(option_data.implied_volatility, pos_or_panic!(0.2));
}
#[test]
fn test_check_and_convert_implied_volatility_under_one() {
let mut option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
None,
pos_or_panic!(0.15), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let original_iv = option_data.implied_volatility;
option_data.check_and_convert_implied_volatility();
assert_eq!(option_data.implied_volatility, original_iv);
}
}
#[cfg(test)]
mod tests_get_option_for_iv {
use super::*;
use crate::OptionType;
use crate::model::ExpirationDate;
use positive::{pos_or_panic, spos};
use rust_decimal_macros::dec;
fn create_test_price_params() -> OptionDataPriceParams {
OptionDataPriceParams::new(
Some(Box::new(Positive::HUNDRED)),
Some(ExpirationDate::Days(pos_or_panic!(30.0))),
Some(dec!(0.05)),
spos!(0.02),
Some("AAPL".to_string()),
)
}
#[test]
fn test_get_option_for_iv_success() {
let mut option_data = OptionData::new(
Positive::HUNDRED,
spos!(5.0),
spos!(5.5),
spos!(4.5),
spos!(5.0),
pos_or_panic!(0.2),
None,
None,
None,
None,
None,
Some("TEST".to_string()), Some(ExpirationDate::Days(pos_or_panic!(30.0))), Some(Box::new(Positive::HUNDRED)), Some(dec!(0.05)), Some(pos_or_panic!(0.02)), None,
None,
);
let params = create_test_price_params();
option_data.set_extra_params(params.clone());
let initial_iv = pos_or_panic!(0.25);
let result = option_data.get_option_for_iv(Side::Long, OptionStyle::Call, initial_iv);
assert!(result.is_ok());
let option = result.unwrap();
assert_eq!(option.option_type, OptionType::European);
assert_eq!(option.side, Side::Long);
assert_eq!(option.strike_price, Positive::HUNDRED);
assert_eq!(option.expiration_date, params.expiration_date.unwrap());
assert_eq!(option.implied_volatility, initial_iv.to_f64()); assert_eq!(option.quantity, Positive::ONE);
assert_eq!(option.underlying_price, *params.underlying_price.unwrap());
assert_eq!(option.risk_free_rate, params.risk_free_rate.unwrap());
assert_eq!(option.option_style, OptionStyle::Call);
assert_eq!(option.dividend_yield, params.dividend_yield.unwrap());
}
#[test]
fn test_get_option_for_iv_put() {
let option_data = OptionData::new(
Positive::HUNDRED,
spos!(5.0),
spos!(5.5),
spos!(4.5),
spos!(5.0),
pos_or_panic!(0.2),
None,
None,
None,
None,
None,
Some("TEST".to_string()), Some(ExpirationDate::Days(pos_or_panic!(30.0))), Some(Box::new(Positive::HUNDRED)), Some(dec!(0.05)), Some(pos_or_panic!(0.02)), None,
None,
);
let initial_iv = pos_or_panic!(0.3);
let result = option_data.get_option_for_iv(Side::Long, OptionStyle::Put, initial_iv);
assert!(result.is_ok());
let option = result.unwrap();
assert_eq!(option.option_style, OptionStyle::Put);
}
#[test]
fn test_get_option_for_iv_short() {
let option_data = OptionData::new(
Positive::HUNDRED,
spos!(5.0),
spos!(5.5),
spos!(4.5),
spos!(5.0),
pos_or_panic!(0.2),
None,
None,
None,
None,
None,
Some("TEST".to_string()), Some(ExpirationDate::Days(pos_or_panic!(30.0))), Some(Box::new(Positive::HUNDRED)), Some(dec!(0.05)), Some(pos_or_panic!(0.02)), None,
None,
);
let initial_iv = pos_or_panic!(0.2);
let result = option_data.get_option_for_iv(Side::Short, OptionStyle::Call, initial_iv);
assert!(result.is_ok());
let option = result.unwrap();
assert_eq!(option.side, Side::Short);
}
}
#[cfg(test)]
mod tests_some_price_is_none {
use super::*;
use positive::{pos_or_panic, spos};
#[test]
fn test_some_price_is_none_all_prices_present() {
let option_data = OptionData::new(
Positive::HUNDRED,
spos!(5.0), spos!(5.5), spos!(4.5), spos!(5.0), pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert!(!option_data.some_price_is_none());
}
#[test]
fn test_some_price_is_none_with_missing_call_bid() {
let option_data = OptionData::new(
Positive::HUNDRED,
None, spos!(5.5), spos!(4.5), spos!(5.0), pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert!(option_data.some_price_is_none());
}
#[test]
fn test_some_price_is_none_with_missing_call_ask() {
let option_data = OptionData::new(
Positive::HUNDRED,
spos!(5.0), None, spos!(4.5), spos!(5.0), pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert!(option_data.some_price_is_none());
}
#[test]
fn test_some_price_is_none_with_missing_put_bid() {
let option_data = OptionData::new(
Positive::HUNDRED,
spos!(5.0), spos!(5.5), None, spos!(5.0), pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert!(option_data.some_price_is_none());
}
#[test]
fn test_some_price_is_none_with_missing_put_ask() {
let option_data = OptionData::new(
Positive::HUNDRED,
spos!(5.0), spos!(5.5), spos!(4.5), None, pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert!(option_data.some_price_is_none());
}
#[test]
fn test_some_price_is_none_with_all_prices_missing() {
let option_data = OptionData::new(
Positive::HUNDRED,
None, None, None, None, pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert!(option_data.some_price_is_none());
}
}
#[cfg(test)]
mod tests_is_valid_optimal_side_deltable {
use super::*;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
#[test]
fn test_is_valid_optimal_side_deltable() {
let option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
None,
pos_or_panic!(0.2), Some(dec!(0.3)), Some(dec!(-0.3)), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let result = option_data.is_valid_optimal_side(
&Positive::HUNDRED,
&FindOptimalSide::Deltable(pos_or_panic!(0.5)),
);
assert!(result);
}
#[test]
fn test_is_valid_optimal_side_center_panics() {
let option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
None,
pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let result = std::panic::catch_unwind(|| {
option_data.is_valid_optimal_side(&Positive::HUNDRED, &FindOptimalSide::Center);
});
assert!(result.is_err());
}
#[test]
fn test_is_valid_optimal_side_delta_range_valid_call() {
let option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
None,
pos_or_panic!(0.2), Some(dec!(0.3)), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let result = option_data.is_valid_optimal_side(
&Positive::HUNDRED,
&FindOptimalSide::DeltaRange(dec!(0.2), dec!(0.4)),
);
assert!(result);
}
#[test]
fn test_is_valid_optimal_side_delta_range_valid_put() {
let option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
None,
pos_or_panic!(0.2), None,
Some(dec!(0.3)), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let result = option_data.is_valid_optimal_side(
&Positive::HUNDRED,
&FindOptimalSide::DeltaRange(dec!(0.2), dec!(0.4)),
);
assert!(result);
}
#[test]
fn test_is_valid_optimal_side_delta_range_invalid() {
let option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
None,
pos_or_panic!(0.2), Some(dec!(0.1)), Some(dec!(0.5)), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let result = option_data.is_valid_optimal_side(
&Positive::HUNDRED,
&FindOptimalSide::DeltaRange(dec!(0.2), dec!(0.4)),
);
assert!(!result);
}
#[test]
fn test_is_valid_optimal_side_delta_range_no_deltas() {
let option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
None,
pos_or_panic!(0.2), None, None, None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let result = option_data.is_valid_optimal_side(
&Positive::HUNDRED,
&FindOptimalSide::DeltaRange(dec!(0.2), dec!(0.4)),
);
assert!(!result);
}
}
#[cfg(test)]
mod tests_set_mid_prices {
use super::*;
use positive::{pos_or_panic, spos};
#[test]
fn test_set_mid_prices_with_both_call_prices() {
let mut option_data = OptionData::new(
Positive::HUNDRED,
spos!(9.0), spos!(11.0), None,
None,
pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
option_data.set_mid_prices();
assert_eq!(option_data.call_middle, spos!(10.0));
assert_eq!(option_data.put_middle, None);
}
#[test]
fn test_set_mid_prices_with_both_put_prices() {
let mut option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
spos!(8.0), spos!(12.0), pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
option_data.set_mid_prices();
assert_eq!(option_data.put_middle, spos!(10.0));
assert_eq!(option_data.call_middle, None);
}
#[test]
fn test_set_mid_prices_with_all_prices() {
let mut option_data = OptionData::new(
Positive::HUNDRED,
spos!(9.0), spos!(11.0), spos!(8.0), spos!(12.0), pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
option_data.set_mid_prices();
assert_eq!(option_data.call_middle, spos!(10.0));
assert_eq!(option_data.put_middle, spos!(10.0));
}
#[test]
fn test_set_mid_prices_with_missing_call_bid() {
let mut option_data = OptionData::new(
Positive::HUNDRED,
None, spos!(11.0), spos!(8.0), spos!(12.0), pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
option_data.set_mid_prices();
assert_eq!(option_data.call_middle, None);
assert_eq!(option_data.put_middle, spos!(10.0));
}
#[test]
fn test_set_mid_prices_with_missing_call_ask() {
let mut option_data = OptionData::new(
Positive::HUNDRED,
spos!(9.0), None, spos!(8.0), spos!(12.0), pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
option_data.set_mid_prices();
assert_eq!(option_data.call_middle, None);
assert_eq!(option_data.put_middle, spos!(10.0));
}
}
#[cfg(test)]
mod tests_get_mid_prices {
use super::*;
use positive::{pos_or_panic, spos};
#[test]
fn test_get_mid_prices_with_both_mid_prices() {
let mut option_data = OptionData::new(
Positive::HUNDRED,
spos!(9.0),
spos!(11.0),
spos!(8.0),
spos!(12.0),
pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
option_data.set_mid_prices();
let (call_mid, put_mid) = option_data.get_mid_prices();
assert_eq!(call_mid, spos!(10.0));
assert_eq!(put_mid, spos!(10.0));
}
#[test]
fn test_get_mid_prices_with_only_call_mid() {
let mut option_data = OptionData::new(
Positive::HUNDRED,
spos!(9.0),
spos!(11.0),
None, spos!(12.0),
pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
option_data.set_mid_prices();
let (call_mid, put_mid) = option_data.get_mid_prices();
assert_eq!(call_mid, spos!(10.0));
assert_eq!(put_mid, None);
}
#[test]
fn test_get_mid_prices_with_only_put_mid() {
let mut option_data = OptionData::new(
Positive::HUNDRED,
None, spos!(11.0),
spos!(8.0),
spos!(12.0),
pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
option_data.set_mid_prices();
let (call_mid, put_mid) = option_data.get_mid_prices();
assert_eq!(call_mid, None);
assert_eq!(put_mid, spos!(10.0));
}
#[test]
fn test_get_mid_prices_with_no_mid_prices() {
let option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
None,
pos_or_panic!(0.2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let (call_mid, put_mid) = option_data.get_mid_prices();
assert_eq!(call_mid, None);
assert_eq!(put_mid, None);
}
}
#[cfg(test)]
mod tests_current_deltas {
use super::*;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
#[test]
fn test_current_deltas_with_both_deltas() {
let option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
None,
pos_or_panic!(0.2), Some(dec!(0.5)), Some(dec!(-0.5)), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let (call_delta, put_delta) = option_data.current_deltas();
assert_eq!(call_delta, Some(dec!(0.5)));
assert_eq!(put_delta, Some(dec!(-0.5)));
}
#[test]
fn test_current_deltas_with_only_call_delta() {
let option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
None,
pos_or_panic!(0.2), Some(dec!(0.5)), None, None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let (call_delta, put_delta) = option_data.current_deltas();
assert_eq!(call_delta, Some(dec!(0.5)));
assert_eq!(put_delta, None);
}
#[test]
fn test_current_deltas_with_only_put_delta() {
let option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
None,
pos_or_panic!(0.2), None, Some(dec!(-0.5)), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let (call_delta, put_delta) = option_data.current_deltas();
assert_eq!(call_delta, None);
assert_eq!(put_delta, Some(dec!(-0.5)));
}
#[test]
fn test_current_deltas_with_no_deltas() {
let option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
None,
pos_or_panic!(0.2), None, None, None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let (call_delta, put_delta) = option_data.current_deltas();
assert_eq!(call_delta, None);
assert_eq!(put_delta, None);
}
}
#[cfg(test)]
mod tests_spreads {
use super::*;
use positive::{pos_or_panic, spos};
#[test]
fn test_get_call_spread_some() {
let option_data = OptionData::new(
Positive::HUNDRED,
spos!(100.0),
spos!(110.0),
spos!(8.5),
spos!(9.0),
pos_or_panic!(0.2),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert_eq!(option_data.get_call_spread(), Some(pos_or_panic!(10.0)));
}
#[test]
fn test_get_call_spread_none_when_missing_prices() {
let od1 = OptionData::new(
Positive::HUNDRED,
None,
spos!(10.0),
None,
None,
pos_or_panic!(0.2),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert_eq!(od1.get_call_spread(), None);
let od2 = OptionData::new(
Positive::HUNDRED,
spos!(9.5),
None,
None,
None,
pos_or_panic!(0.2),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert_eq!(od2.get_call_spread(), None);
}
#[test]
fn test_get_call_spread_per_some() {
let option_data = OptionData::new(
pos_or_panic!(95.0),
spos!(95.0),
spos!(105.0),
None,
None,
pos_or_panic!(0.2),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let spread_per = option_data.get_call_spread_per().unwrap();
let got = spread_per.to_f64();
let expected = 0.1;
assert!((got - expected).abs() < 1e-12);
}
#[test]
fn test_get_call_spread_per_none_when_missing_prices() {
let od = OptionData::new(
Positive::HUNDRED,
None,
spos!(10.0),
None,
None,
pos_or_panic!(0.2),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert_eq!(od.get_call_spread_per(), None);
}
#[test]
fn test_get_put_spread_some() {
let option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
spos!(8.5),
spos!(9.0),
pos_or_panic!(0.2),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert_eq!(option_data.get_put_spread(), Some(pos_or_panic!(0.5)));
}
#[test]
fn test_get_put_spread_none_when_missing_prices() {
let od1 = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
spos!(9.0),
pos_or_panic!(0.2),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert_eq!(od1.get_put_spread(), None);
let od2 = OptionData::new(
Positive::HUNDRED,
None,
None,
spos!(8.5),
None,
pos_or_panic!(0.2),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert_eq!(od2.get_put_spread(), None);
}
#[test]
fn test_get_put_spread_per_some() {
let option_data = OptionData::new(
Positive::HUNDRED,
None,
None,
spos!(95.0),
spos!(105.0),
pos_or_panic!(0.2),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
let spread_per = option_data.get_put_spread_per().unwrap();
let got = spread_per.to_f64();
let expected = 0.1;
assert!((got - expected).abs() < 1e-12);
}
#[test]
fn test_get_put_spread_per_none_when_missing_prices() {
let od = OptionData::new(
Positive::HUNDRED,
None,
None,
None,
spos!(9.0),
pos_or_panic!(0.2),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert_eq!(od.get_put_spread_per(), None);
}
}
#[cfg(test)]
mod tests_validate_option_data {
use super::*;
use positive::pos_or_panic;
use positive::spos;
use rust_decimal_macros::dec;
#[test]
fn test_validate_option_data_missing_strike_price() {
let option_data = OptionData::new(
Positive::ZERO, spos!(9.5), spos!(10.0), spos!(8.5), spos!(9.0), pos_or_panic!(0.2), Some(dec!(-0.3)),
Some(dec!(0.7)),
Some(dec!(0.5)),
spos!(1000.0),
Some(500),
Some("TEST".to_string()),
Some(ExpirationDate::Days(pos_or_panic!(30.0))),
Some(Box::new(Positive::HUNDRED)),
Some(dec!(0.05)),
Some(pos_or_panic!(0.02)),
None,
None,
);
let is_valid = option_data.validate();
assert!(!is_valid);
}
#[test]
fn test_validate_option_data_missing_call_bid() {
let option_data = OptionData::new(
Positive::HUNDRED, None, spos!(10.0), spos!(8.5), spos!(9.0), pos_or_panic!(0.2), Some(dec!(-0.3)),
Some(dec!(0.7)),
Some(dec!(0.5)),
spos!(1000.0),
Some(500),
Some("TEST".to_string()),
Some(ExpirationDate::Days(pos_or_panic!(30.0))),
Some(Box::new(Positive::HUNDRED)),
Some(dec!(0.05)),
Some(pos_or_panic!(0.02)),
None,
None,
);
let is_valid = option_data.validate();
assert!(!is_valid);
}
#[test]
fn test_validate_option_data_missing_call_ask() {
let option_data = OptionData::new(
Positive::HUNDRED, spos!(9.5), None, spos!(8.5), spos!(9.0), pos_or_panic!(0.2), Some(dec!(-0.3)),
Some(dec!(0.7)),
Some(dec!(0.5)),
spos!(1000.0),
Some(500),
Some("TEST".to_string()),
Some(ExpirationDate::Days(pos_or_panic!(30.0))),
Some(Box::new(Positive::HUNDRED)),
Some(dec!(0.05)),
Some(pos_or_panic!(0.02)),
None,
None,
);
let is_valid = option_data.validate();
assert!(!is_valid);
}
#[test]
fn test_validate_option_data_missing_put_bid() {
let option_data = OptionData::new(
Positive::HUNDRED, spos!(9.5), spos!(10.0), None, spos!(9.0), pos_or_panic!(0.2), Some(dec!(-0.3)),
Some(dec!(0.7)),
Some(dec!(0.5)),
spos!(1000.0),
Some(500),
Some("TEST".to_string()),
Some(ExpirationDate::Days(pos_or_panic!(30.0))),
Some(Box::new(Positive::HUNDRED)),
Some(dec!(0.05)),
Some(pos_or_panic!(0.02)),
None,
None,
);
let is_valid = option_data.validate();
assert!(!is_valid);
}
#[test]
fn test_validate_option_data_missing_put_ask() {
let option_data = OptionData::new(
Positive::HUNDRED, spos!(9.5), spos!(10.0), spos!(8.5), None, pos_or_panic!(0.2), Some(dec!(-0.3)),
Some(dec!(0.7)),
Some(dec!(0.5)),
spos!(1000.0),
Some(500),
Some("TEST".to_string()),
Some(ExpirationDate::Days(pos_or_panic!(30.0))),
Some(Box::new(Positive::HUNDRED)),
Some(dec!(0.05)),
Some(pos_or_panic!(0.02)),
None,
None,
);
let is_valid = option_data.validate();
assert!(!is_valid);
}
}