use positive::{Positive, pos_or_panic};
use crate::chains::OptionData;
use crate::chains::chain::{SKEW_SLOPE, SKEW_SMILE_CURVE};
use crate::error::chains::ChainError;
use crate::model::ExpirationDate;
use crate::model::utils::ToRound;
use num_traits::ToPrimitive;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fmt::Display;
#[derive(Debug)]
pub enum OptionDataGroup<'a> {
One(&'a OptionData),
Two(&'a OptionData, &'a OptionData),
Three(&'a OptionData, &'a OptionData, &'a OptionData),
Four(
&'a OptionData,
&'a OptionData,
&'a OptionData,
&'a OptionData,
),
Any(Vec<&'a OptionData>),
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct OptionChainBuildParams {
pub(crate) symbol: String,
pub(crate) volume: Option<Positive>,
pub(crate) chain_size: usize,
pub(crate) strike_interval: Option<Positive>,
pub(crate) skew_slope: Decimal,
pub(crate) smile_curve: Decimal,
pub(crate) spread: Positive,
pub(crate) decimal_places: u32,
pub(crate) price_params: OptionDataPriceParams,
pub(crate) implied_volatility: Positive,
}
#[allow(clippy::too_many_arguments)]
impl OptionChainBuildParams {
pub fn new(
symbol: String,
volume: Option<Positive>,
chain_size: usize,
strike_interval: Option<Positive>,
skew_slope: Decimal,
smile_curve: Decimal,
spread: Positive,
decimal_places: u32,
price_params: OptionDataPriceParams,
implied_volatility: Positive,
) -> Self {
Self {
symbol,
volume,
chain_size,
strike_interval,
skew_slope,
smile_curve,
spread,
decimal_places,
price_params,
implied_volatility,
}
}
pub fn set_underlying_price(&mut self, price: Option<Box<Positive>>) {
self.price_params.underlying_price = price;
}
pub fn set_implied_volatility(&mut self, implied_vol: Positive) {
self.implied_volatility = implied_vol;
}
pub fn get_implied_volatility(&self) -> Positive {
self.implied_volatility
}
}
impl Display for OptionChainBuildParams {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match serde_json::to_string(self) {
Ok(pretty_json) => write!(f, "{pretty_json}"),
Err(e) => write!(f, "Error serializing to JSON: {e}"),
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
pub struct OptionDataPriceParams {
pub(crate) underlying_price: Option<Box<Positive>>,
pub(crate) expiration_date: Option<ExpirationDate>,
pub(crate) risk_free_rate: Option<Decimal>,
pub(crate) dividend_yield: Option<Positive>,
pub(crate) underlying_symbol: Option<String>,
}
impl OptionDataPriceParams {
pub fn new(
underlying_price: Option<Box<Positive>>,
expiration_date: Option<ExpirationDate>,
risk_free_rate: Option<Decimal>,
dividend_yield: Option<Positive>,
underlying_symbol: Option<String>,
) -> Self {
Self {
underlying_price,
expiration_date,
risk_free_rate,
dividend_yield,
underlying_symbol,
}
}
pub fn get_underlying_price(&self) -> Option<Box<Positive>> {
self.underlying_price.clone()
}
pub fn get_expiration_date(&self) -> Option<ExpirationDate> {
self.expiration_date
}
pub fn get_risk_free_rate(&self) -> Option<Decimal> {
self.risk_free_rate
}
pub fn get_dividend_yield(&self) -> Option<Positive> {
self.dividend_yield
}
pub fn get_symbol(&self) -> Option<String> {
self.underlying_symbol.clone()
}
}
impl Display for OptionDataPriceParams {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Underlying Price: {:.3}, Expiration: {:.4} Years, Risk-Free Rate: {:.2}%, Dividend Yield: {:.2}%, Symbol: {}",
self.underlying_price
.as_ref()
.map_or_else(|| "None".to_string(), |p| p.value().to_string()),
self.expiration_date.map_or_else(
|| "None".to_string(),
|d| d.get_years().unwrap().to_string()
),
self.risk_free_rate
.map_or_else(|| "None".to_string(), |r| (r * dec!(100.0)).to_string()),
self.dividend_yield.map_or_else(
|| "None".to_string(),
|d| (d.value() * dec!(100.0)).to_string()
),
self.underlying_symbol
.as_ref()
.map_or_else(|| "None".to_string(), |s| s.to_string()),
)
}
}
pub trait OptionChainParams {
fn get_params(&self, strike_price: Positive) -> Result<OptionDataPriceParams, ChainError>;
}
#[derive(Clone, Debug)]
pub struct RandomPositionsParams {
pub qty_puts_long: Option<usize>,
pub qty_puts_short: Option<usize>,
pub qty_calls_long: Option<usize>,
pub qty_calls_short: Option<usize>,
pub expiration_date: ExpirationDate,
pub option_qty: Positive,
pub risk_free_rate: Decimal,
pub dividend_yield: Positive,
pub open_put_fee: Positive,
pub open_call_fee: Positive,
pub close_put_fee: Positive,
pub close_call_fee: Positive,
pub epic: Option<String>,
pub extra_fields: Option<serde_json::Value>,
}
impl RandomPositionsParams {
#[allow(clippy::too_many_arguments)]
pub fn new(
qty_puts_long: Option<usize>,
qty_puts_short: Option<usize>,
qty_calls_long: Option<usize>,
qty_calls_short: Option<usize>,
expiration_date: ExpirationDate,
option_qty: Positive,
risk_free_rate: Decimal,
dividend_yield: Positive,
open_put_fee: Positive,
open_call_fee: Positive,
close_put_fee: Positive,
close_call_fee: Positive,
epic: Option<String>,
extra_fields: Option<serde_json::Value>,
) -> Self {
Self {
qty_puts_long,
qty_puts_short,
qty_calls_long,
qty_calls_short,
expiration_date,
option_qty,
risk_free_rate,
dividend_yield,
open_put_fee,
open_call_fee,
close_put_fee,
close_call_fee,
epic,
extra_fields,
}
}
pub fn total_positions(&self) -> usize {
self.qty_puts_long.unwrap_or(0)
+ self.qty_puts_short.unwrap_or(0)
+ self.qty_calls_long.unwrap_or(0)
+ self.qty_calls_short.unwrap_or(0)
}
}
pub fn adjust_volatility(
base_vol: &Option<Positive>, skew_slope: &Option<Decimal>, smile_curve: &Option<Decimal>, strike: &Positive,
underlying_price: &Positive, ) -> Option<Positive> {
if base_vol.is_none() {
return None;
}
if strike.is_zero() {
return None;
}
let base_vol = base_vol.unwrap();
let skew_slope = skew_slope.unwrap_or(SKEW_SLOPE).to_f64().unwrap();
let smile_curve = smile_curve.unwrap_or(SKEW_SMILE_CURVE).to_f64().unwrap();
let m = (strike / underlying_price.to_f64()).ln();
let factor: f64 = 1.0 + skew_slope * m + smile_curve * m * m;
let clamped = factor.clamp(0.01, 3.0);
(base_vol * clamped)
.clamp(Positive::ZERO, Positive::ONE)
.into()
}
pub(crate) fn parse<T: std::str::FromStr>(s: &str) -> Option<T> {
let trimmed = s.trim();
let input: Result<T, <T as std::str::FromStr>::Err> = match trimmed.parse::<T>() {
Ok(value) => Ok(value),
Err(_) => {
return None;
}
};
input.ok()
}
pub(crate) fn empty_string_round_to_2<T: ToString + ToRound>(input: Option<T>) -> String {
input.map_or_else(|| "".to_string(), |v| v.round_to(2).to_string())
}
pub(crate) fn empty_string_round_to_3<T: ToString + ToRound>(input: Option<T>) -> String {
input.map_or_else(|| "".to_string(), |v| v.round_to(3).to_string())
}
pub(crate) fn default_empty_string<T: ToString>(input: Option<T>) -> String {
input.map_or_else(|| "".to_string(), |v| v.to_string())
}
pub(crate) fn rounder(reference_price: Positive, strike_interval: Positive) -> Positive {
if strike_interval == Positive::ZERO {
return reference_price;
}
let price = reference_price.value();
let interval = strike_interval.value();
let remainder = price % interval;
let base = price - remainder;
let rounded = if remainder >= interval / Decimal::TWO {
base + interval
} else {
base
};
Positive::new_decimal(rounded).unwrap_or(reference_price)
}
#[allow(dead_code)]
fn round_to_clean_interval(interval: Positive, price: Positive) -> Positive {
let v = interval.to_f64();
if price < pos_or_panic!(25.0) {
if v <= 0.25 {
pos_or_panic!(0.25)
} else if v <= 0.5 {
pos_or_panic!(0.5)
} else if v <= 1.0 {
Positive::ONE
} else if v <= 2.5 {
pos_or_panic!(2.5)
} else {
pos_or_panic!(5.0)
}
} else if price < Positive::HUNDRED {
if v <= 1.0 {
Positive::ONE
} else if v <= 2.5 {
pos_or_panic!(2.5)
} else if v <= 5.0 {
pos_or_panic!(5.0)
} else {
pos_or_panic!(10.0)
}
} else if v <= 5.0 {
Positive::ONE
} else if v <= 8.0 {
Positive::TWO
} else if v <= 12.5 {
pos_or_panic!(5.0)
} else if v <= 15.0 {
pos_or_panic!(10.0)
} else if v <= 20.0 {
pos_or_panic!(15.0)
} else if v <= 25.0 {
pos_or_panic!(20.0)
} else if v <= 35.0 {
pos_or_panic!(25.0)
} else if v <= 50.0 {
pos_or_panic!(50.0)
} else {
Positive::HUNDRED
}
}
pub fn strike_step(
underlying_price: Positive,
implied_vol: Positive, days_to_exp: Positive,
size: usize, k: Option<Positive>, ) -> Positive {
let k = k.unwrap_or_else(|| pos_or_panic!(4.0));
assert!(size > 1, "need at least two strikes");
let t = days_to_exp / 365.0;
let sigma = underlying_price * implied_vol * t.sqrt();
let raw_step = Positive::TWO * k * sigma / (size as f64 - 1.0);
let bins: &[Positive] = &[
pos_or_panic!(0.01),
pos_or_panic!(0.05),
pos_or_panic!(0.10),
pos_or_panic!(0.25),
pos_or_panic!(0.5),
Positive::ONE,
pos_or_panic!(2.5),
pos_or_panic!(5.0),
pos_or_panic!(10.0),
pos_or_panic!(25.0),
pos_or_panic!(50.0),
Positive::HUNDRED,
pos_or_panic!(150.0),
pos_or_panic!(200.0),
pos_or_panic!(250.0),
];
bins.iter()
.copied()
.min_by(|a, b| {
((a.to_dec() - raw_step.to_dec()).abs())
.partial_cmp(&(b.to_dec() - raw_step.to_dec()).abs())
.unwrap()
})
.unwrap_or(raw_step)
}
#[cfg(test)]
mod tests_strike_step {
use super::*;
use positive::spos;
use crate::chains::OptionChain;
use crate::utils::Len;
#[test]
fn basic() {
let step = strike_step(
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(30.0),
11,
None,
);
assert_eq!(step, 5.0);
}
#[test]
fn long_days() {
let step = strike_step(
pos_or_panic!(150.0),
pos_or_panic!(0.5),
pos_or_panic!(120.0),
30,
spos!(3.0),
);
assert_eq!(step, 10.0);
}
#[test]
fn long_discrepancy() {
let symbol = "AAPL".to_string();
let risk_free_rate = dec!(0.02);
let dividend_yield = Positive::ZERO;
let volume = Some(Positive::ONE);
let spread = pos_or_panic!(0.01);
let decimal_places = 2;
let skew_slope = dec!(-0.2);
let smile_curve = dec!(0.1);
let underlying_price = Some(Box::new(pos_or_panic!(1547.0)));
let days = pos_or_panic!(45.0);
let implied_volatility = pos_or_panic!(0.17);
let chain_size = 28;
let strike_interval = strike_step(
*underlying_price.clone().unwrap(),
implied_volatility,
days,
chain_size,
spos!(3.0),
);
assert_eq!(strike_interval, 25.0);
let price_params = OptionDataPriceParams::new(
underlying_price,
Some(ExpirationDate::Days(days)),
Some(risk_free_rate),
Some(dividend_yield),
Some(symbol.clone()),
);
let build_params = OptionChainBuildParams::new(
symbol,
volume,
chain_size,
Some(strike_interval),
skew_slope,
smile_curve,
spread,
decimal_places,
price_params,
implied_volatility,
);
let initial_chain = OptionChain::build_chain(&build_params).unwrap();
assert_eq!(initial_chain.len() - 1, chain_size);
}
}
#[cfg(test)]
mod tests_rounder {
use super::*;
#[test]
fn test_rounder() {
assert_eq!(
rounder(pos_or_panic!(151.0), pos_or_panic!(5.0)),
pos_or_panic!(150.0)
);
assert_eq!(
rounder(pos_or_panic!(154.0), pos_or_panic!(5.0)),
pos_or_panic!(155.0)
);
assert_eq!(
rounder(pos_or_panic!(152.5), pos_or_panic!(5.0)),
pos_or_panic!(155.0)
);
assert_eq!(
rounder(pos_or_panic!(152.4), pos_or_panic!(5.0)),
pos_or_panic!(150.0)
);
assert_eq!(
rounder(pos_or_panic!(151.0), pos_or_panic!(10.0)),
pos_or_panic!(150.0)
);
assert_eq!(
rounder(pos_or_panic!(156.0), pos_or_panic!(10.0)),
pos_or_panic!(160.0)
);
assert_eq!(
rounder(pos_or_panic!(155.0), pos_or_panic!(10.0)),
pos_or_panic!(160.0)
);
assert_eq!(
rounder(pos_or_panic!(154.9), pos_or_panic!(10.0)),
pos_or_panic!(150.0)
);
assert_eq!(
rounder(pos_or_panic!(17.0), pos_or_panic!(15.0)),
pos_or_panic!(15.0)
);
assert_eq!(
rounder(pos_or_panic!(43.0), pos_or_panic!(15.0)),
pos_or_panic!(45.0)
);
assert_eq!(
rounder(pos_or_panic!(37.5), pos_or_panic!(15.0)),
pos_or_panic!(45.0)
);
assert_eq!(
rounder(pos_or_panic!(37.4), pos_or_panic!(15.0)),
pos_or_panic!(30.0)
);
}
}
#[cfg(test)]
mod tests_parse {
use super::*;
use positive::spos;
use std::f64::consts::PI;
#[test]
fn test_parse_valid_integer() {
let input = "42";
let result: Option<i32> = parse(input);
assert_eq!(result, Some(42));
}
#[test]
fn test_parse_invalid_integer() {
let input = "not_a_number";
let result: Option<i32> = parse(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_valid_float() {
let input = &*PI.to_string();
let result: Option<f64> = parse(input);
assert_eq!(result, Some(PI));
}
#[test]
fn test_positive_f64() {
let input = "42.01";
let result: Option<Positive> = parse(input);
assert_eq!(result, spos!(42.01));
}
}
#[cfg(test)]
mod tests_parse_bis {
use super::*;
use positive::spos;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
#[test]
fn test_parse_decimal() {
let input = "42.5";
let result: Option<Decimal> = parse(input);
assert_eq!(result, Some(dec!(42.5)));
let invalid = "not_a_decimal";
let result: Option<Decimal> = parse(invalid);
assert_eq!(result, None);
}
#[test]
fn test_parse_empty_string() {
let input = "";
let result: Option<i32> = parse(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_whitespace() {
let input = " ";
let result: Option<i32> = parse(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_bool() {
let input = "true";
let result: Option<bool> = parse(input);
assert_eq!(result, Some(true));
let input = "false";
let result: Option<bool> = parse(input);
assert_eq!(result, Some(false));
let input = "not_a_bool";
let result: Option<bool> = parse(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_positive() {
let input = "42.5";
let result: Option<Positive> = parse(input);
assert_eq!(result, spos!(42.5));
let input = "-42.5";
let result: Option<Positive> = parse(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_different_number_formats() {
let result: Option<i32> = parse("123");
assert_eq!(result, Some(123));
let result: Option<f64> = parse("123.456");
assert_eq!(result, Some(123.456));
let result: Option<f64> = parse("1.23e2");
assert_eq!(result, Some(123.0));
}
#[test]
fn test_parse_with_leading_trailing_spaces() {
let input = " 42 ";
let result: Option<i32> = parse(input);
assert_eq!(result, Some(42));
let input = " 42.5 ";
let result: Option<f64> = parse(input);
assert_eq!(result, Some(42.5));
}
#[test]
fn test_parse_invalid_formats() {
let result: Option<i32> = parse("42abc");
assert_eq!(result, None);
let result: Option<f64> = parse("42.3.4");
assert_eq!(result, None);
let result: Option<f64> = parse("1.23e");
assert_eq!(result, None);
}
}
#[cfg(test)]
mod tests_default_empty_string {
use super::*;
#[test]
fn test_default_empty_string_with_some_value() {
let input = Some(42);
let result = default_empty_string(input);
assert_eq!(result, "42");
}
#[test]
fn test_default_empty_string_with_float() {
let input = Some(42.01223);
let result = default_empty_string(input);
assert_eq!(result, "42.01223");
}
#[test]
fn test_default_empty_string_with_none() {
let input: Option<i32> = None;
let result = default_empty_string(input);
assert_eq!(result, "");
}
#[test]
fn test_default_empty_string_with_string_value() {
let input = Some("Hello");
let result = default_empty_string(input);
assert_eq!(result, "Hello");
}
}
#[cfg(test)]
mod tests_random_positions_params {
use super::*;
use num_traits::ToPrimitive;
use rust_decimal_macros::dec;
fn create_test_params() -> RandomPositionsParams {
RandomPositionsParams::new(
Some(1),
Some(1),
Some(1),
Some(1),
ExpirationDate::Days(pos_or_panic!(30.0)),
Positive::ONE,
dec!(0.05),
pos_or_panic!(0.02),
Positive::ONE,
Positive::ONE,
Positive::ONE,
Positive::ONE,
Some("Epic".to_string()),
None,
)
}
#[test]
fn test_new_params() {
let params = create_test_params();
assert_eq!(params.qty_puts_long, Some(1));
assert_eq!(params.qty_puts_short, Some(1));
assert_eq!(params.qty_calls_long, Some(1));
assert_eq!(params.qty_calls_short, Some(1));
assert_eq!(params.option_qty, 1.0);
assert_eq!(params.risk_free_rate.to_f64().unwrap(), 0.05);
assert_eq!(params.dividend_yield.to_f64(), 0.02);
assert_eq!(params.open_put_fee, 1.0);
assert_eq!(params.close_put_fee, 1.0);
assert_eq!(params.open_call_fee, 1.0);
assert_eq!(params.close_call_fee, 1.0);
}
#[test]
fn test_total_positions() {
let params = create_test_params();
assert_eq!(params.total_positions(), 4);
let params = RandomPositionsParams::new(
Some(2),
None,
Some(3),
None,
ExpirationDate::Days(pos_or_panic!(30.0)),
Positive::ONE,
dec!(0.05),
pos_or_panic!(0.02),
Positive::ONE,
Positive::ONE,
Positive::ONE,
Positive::ONE,
Some("Epic".to_string()),
None,
);
assert_eq!(params.total_positions(), 5);
let params = RandomPositionsParams::new(
None,
None,
None,
None,
ExpirationDate::Days(pos_or_panic!(30.0)),
Positive::ONE,
dec!(0.05),
pos_or_panic!(0.02),
Positive::ONE,
Positive::ONE,
Positive::ONE,
Positive::ONE,
Some("Epic".to_string()),
None,
);
assert_eq!(params.total_positions(), 0);
}
#[test]
fn test_clone() {
let params = create_test_params();
let cloned = params.clone();
assert_eq!(params.total_positions(), cloned.total_positions());
}
#[test]
fn test_debug() {
let params = create_test_params();
let debug_output = format!("{params:?}");
assert!(debug_output.contains("RandomPositionsParams"));
}
}
#[cfg(test)]
mod tests_adjust_volatility {
use super::*;
use approx::assert_relative_eq;
use rust_decimal_macros::dec;
#[test]
fn returns_none_when_base_is_none() {
let strike = Positive::HUNDRED;
let spot = Positive::HUNDRED;
let out = adjust_volatility(
&None, &None, &None, &strike, &spot,
);
assert!(out.is_none());
}
#[test]
fn atm_unchanged_with_defaults() {
let base = pos_or_panic!(0.17);
let strike = pos_or_panic!(1500.0);
let spot = pos_or_panic!(1500.0);
let out = adjust_volatility(
&Some(base),
&None,
&None, &strike,
&spot,
)
.unwrap();
assert_eq!(out.to_dec(), base.to_dec());
}
#[test]
fn huge_positive_smile_clamps_upper() {
let base = pos_or_panic!(0.20);
let strike = pos_or_panic!(3000.0);
let spot = pos_or_panic!(1000.0);
let smile = dec!(5.0);
let out = adjust_volatility(&Some(base), &None, &Some(smile), &strike, &spot).unwrap();
assert_eq!(out, base + 0.4);
}
#[test]
fn extreme_moneyness_clamps_lower() {
let base = pos_or_panic!(0.30);
let strike = pos_or_panic!(10.0);
let spot = pos_or_panic!(1000.0);
let skew = dec!(10.0);
let out = adjust_volatility(
&Some(base),
&Some(skew),
&None, &strike,
&spot,
)
.unwrap();
let expected = base * pos_or_panic!(0.01); assert_relative_eq!(
out.to_dec().to_f64().unwrap(),
expected.to_dec().to_f64().unwrap(),
epsilon = 1e-12
);
}
#[test]
fn negative_skew_increases_vol_below_atm() {
let base = pos_or_panic!(0.20);
let strike = pos_or_panic!(1000.0);
let spot = pos_or_panic!(1500.0);
let skew = dec!(-1.0);
let out = adjust_volatility(&Some(base), &Some(skew), &None, &strike, &spot).unwrap();
assert!(out > base);
}
}
#[cfg(test)]
mod tests_option_data_price_params {
use super::*;
use num_traits::ToPrimitive;
use positive::spos;
use rust_decimal_macros::dec;
fn get_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_new_price_params() {
let params = get_params();
assert_eq!(*params.underlying_price.unwrap(), Positive::HUNDRED);
assert_eq!(
params.expiration_date.unwrap().get_days().unwrap(),
pos_or_panic!(30.0)
);
assert_eq!(params.risk_free_rate.unwrap().to_f64().unwrap(), 0.05);
assert_eq!(params.dividend_yield.unwrap().to_f64(), 0.02);
assert_eq!(params.underlying_symbol.unwrap(), "AAPL");
}
#[test]
fn test_default_price_params() {
let params = OptionDataPriceParams::default();
assert_eq!(params.underlying_price, None);
assert_eq!(params.risk_free_rate, None);
assert_eq!(params.dividend_yield, None);
assert_eq!(params.underlying_symbol, None);
}
#[test]
fn test_display_price_params() {
let params = get_params();
let display_string = format!("{params}");
assert!(display_string.contains("Underlying Price: 100"));
assert!(display_string.contains("Risk-Free Rate: 5"));
assert!(display_string.contains("Dividend Yield: 2"));
assert!(display_string.contains("Symbol: AAPL"));
assert!(display_string.contains("Expiration: 0.08 Years"));
}
#[test]
fn test_option_data_price_params_getters() {
let underlying_price = Some(Box::new(Positive::HUNDRED));
let expiration_date = Some(ExpirationDate::Days(pos_or_panic!(30.0)));
let risk_free_rate = Some(dec!(0.05));
let dividend_yield = spos!(0.02);
let underlying_symbol = Some("AAPL".to_string());
let params = OptionDataPriceParams {
underlying_price: underlying_price.clone(),
expiration_date,
risk_free_rate,
dividend_yield,
underlying_symbol: underlying_symbol.clone(),
};
assert_eq!(params.get_underlying_price(), underlying_price);
assert_eq!(params.get_expiration_date(), expiration_date);
assert_eq!(params.get_risk_free_rate(), risk_free_rate);
assert_eq!(params.get_dividend_yield(), dividend_yield);
}
#[test]
fn test_option_data_price_params_getters_with_datetime_expiration() {
use chrono::{Duration, Utc};
let future_date = Utc::now() + Duration::days(30);
let expiration_date = Some(ExpirationDate::DateTime(future_date));
let mut params = get_params();
params.expiration_date = expiration_date;
assert_eq!(params.get_expiration_date(), expiration_date);
}
#[test]
fn test_option_data_price_params_getters_zero_values() {
let mut params = get_params();
params.underlying_price = Some(Box::new(Positive::ZERO));
params.expiration_date = Some(ExpirationDate::Days(Positive::ZERO));
params.risk_free_rate = Some(Decimal::ZERO);
params.dividend_yield = Some(Positive::ZERO);
assert_eq!(*params.get_underlying_price().unwrap(), Positive::ZERO);
assert_eq!(
params.get_expiration_date().unwrap(),
ExpirationDate::Days(Positive::ZERO)
);
assert_eq!(params.get_risk_free_rate().unwrap(), Decimal::ZERO);
assert_eq!(params.get_dividend_yield().unwrap(), Positive::ZERO);
}
}
#[cfg(test)]
mod tests_option_chain_build_params {
use super::*;
use positive::spos;
use rust_decimal_macros::dec;
fn get_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_new_chain_build_params() {
let price_params = get_params();
let params = OptionChainBuildParams::new(
"TEST".to_string(),
spos!(1000.0),
10,
spos!(5.0),
dec!(-0.2),
dec!(0.1),
pos_or_panic!(0.02),
2,
price_params,
pos_or_panic!(0.25),
);
assert_eq!(params.symbol, "TEST");
assert_eq!(params.volume, spos!(1000.0));
assert_eq!(params.chain_size, 10);
assert_eq!(params.strike_interval, spos!(5.0));
assert_eq!(params.smile_curve, dec!(0.1));
assert_eq!(params.spread, pos_or_panic!(0.02));
assert_eq!(params.decimal_places, 2);
let display = format!("{params}");
assert_eq!(
display,
r#"{"symbol":"TEST","volume":1000,"chain_size":10,"strike_interval":5,"skew_slope":"-0.2","smile_curve":"0.1","spread":0.02,"decimal_places":2,"price_params":{"underlying_price":100,"expiration_date":{"days":30.0},"risk_free_rate":"0.05","dividend_yield":0.02,"underlying_symbol":"AAPL"},"implied_volatility":0.25}"#
);
}
#[test]
fn test_chain_build_params_without_volume() {
let price_params = OptionDataPriceParams::default();
let params = OptionChainBuildParams::new(
"TEST".to_string(),
None,
10,
spos!(5.0),
dec!(-0.2),
dec!(0.1),
pos_or_panic!(0.02),
2,
price_params,
pos_or_panic!(0.25),
);
assert_eq!(params.volume, None);
}
}
#[cfg(test)]
mod tests_random_positions_params_extended {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_partial_positions() {
let params = RandomPositionsParams::new(
Some(2),
None,
Some(1),
None,
ExpirationDate::Days(pos_or_panic!(30.0)),
Positive::ONE,
dec!(0.05),
pos_or_panic!(0.02),
Positive::ONE,
Positive::ONE,
Positive::ONE,
Positive::ONE,
Some("Epic".to_string()),
None,
);
assert_eq!(params.qty_puts_long, Some(2));
assert_eq!(params.qty_puts_short, None);
assert_eq!(params.qty_calls_long, Some(1));
assert_eq!(params.qty_calls_short, None);
assert_eq!(params.total_positions(), 3);
}
#[test]
fn test_no_positions() {
let params = RandomPositionsParams::new(
None,
None,
None,
None,
ExpirationDate::Days(pos_or_panic!(30.0)),
Positive::ONE,
dec!(0.05),
pos_or_panic!(0.02),
Positive::ONE,
Positive::ONE,
Positive::ONE,
Positive::ONE,
Some("Epic".to_string()),
None,
);
assert_eq!(params.total_positions(), 0);
}
#[test]
fn test_expiration_date() {
let params = RandomPositionsParams::new(
None,
None,
None,
None,
ExpirationDate::Days(pos_or_panic!(30.0)),
Positive::ONE,
dec!(0.05),
pos_or_panic!(0.02),
Positive::ONE,
Positive::ONE,
Positive::ONE,
Positive::ONE,
Some("Epic".to_string()),
None,
);
match params.expiration_date {
ExpirationDate::Days(days) => assert_eq!(days, 30.0),
_ => panic!("Expected ExpirationDate::Days"),
}
}
}
#[cfg(test)]
mod tests_sample {
use super::*;
use positive::spos;
use crate::chains::chain::OptionChain;
use rust_decimal_macros::dec;
#[test]
fn test_chain() {
let chain = 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()),
);
let params = OptionChainBuildParams::new(
"AAPL".to_string(),
Some(Positive::ONE),
5,
Some(Positive::ONE),
dec!(-0.2),
dec!(0.0001),
Positive::new(0.02).unwrap(),
2,
chain,
pos_or_panic!(0.25),
);
let built_chain = OptionChain::build_chain(¶ms).unwrap();
assert_eq!(built_chain.symbol, "AAPL");
assert_eq!(built_chain.underlying_price, Positive::new(100.0).unwrap());
}
#[test]
fn test_empty_string_round_to_2() {
let value = spos!(123.456);
let result = empty_string_round_to_2(value);
assert_eq!(result, "123.46");
let value: Option<Positive> = None;
let result = empty_string_round_to_2(value);
assert_eq!(result, "");
}
}