use super::base::{
BreakEvenable, Optimizable, Positionable, Strategable, StrategyBasics, StrategyType, Validable,
};
use super::shared::StraddleStrategy;
use crate::{
ExpirationDate, Options,
chains::{StrategyLegs, chain::OptionChain, utils::OptionDataGroup},
constants::ZERO,
error::{
GreeksError, OperationErrorKind, PricingError,
position::{PositionError, PositionValidationErrorKind},
probability::ProbabilityError,
strategies::StrategyError,
},
greeks::Greeks,
model::{
ProfitLossRange,
position::Position,
types::{OptionBasicType, OptionStyle, OptionType, Side},
utils::mean_and_std,
},
pnl::{PnLCalculator, utils::PnL},
pricing::payoff::Profit,
strategies::{
BasicAble, Strategies, StrategyConstructor,
delta_neutral::DeltaNeutrality,
probabilities::{core::ProbabilityAnalysis, utils::VolatilityAdjustment},
utils::{FindOptimalSide, OptimizationCriteria},
},
test_strategy_traits,
};
use chrono::Utc;
use num_traits::FromPrimitive;
use positive::Positive;
use pretty_simple_display::{DebugPretty, DisplaySimple};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use tracing::info;
use utoipa::ToSchema;
pub const LONG_STRADDLE_DESCRIPTION: &str = "Long Straddle strategy involves simultaneously \
buying a put and a call option with identical strike prices and expiration dates. \
Profits from increased volatility and significant price movements in either direction. \
Maximum loss limited to premium paid with unlimited profit potential. Most effective \
when expecting large price movements but uncertain about direction.";
#[derive(Clone, DebugPretty, DisplaySimple, Serialize, Deserialize, ToSchema)]
pub struct LongStraddle {
pub name: String,
pub kind: StrategyType,
pub description: String,
pub break_even_points: Vec<Positive>,
pub long_call: Position,
pub long_put: Position,
}
impl LongStraddle {
#[allow(clippy::too_many_arguments)]
pub fn new(
underlying_symbol: String,
underlying_price: Positive,
mut strike: Positive,
expiration: ExpirationDate,
implied_volatility: Positive,
risk_free_rate: Decimal,
dividend_yield: Positive,
quantity: Positive,
premium_long_call: Positive,
premium_long_put: Positive,
open_fee_long_call: Positive,
close_fee_long_call: Positive,
open_fee_long_put: Positive,
close_fee_long_put: Positive,
) -> Self {
if strike == Positive::ZERO {
strike = underlying_price;
}
let mut strategy = LongStraddle {
name: "Long Straddle".to_string(),
kind: StrategyType::LongStraddle,
description: LONG_STRADDLE_DESCRIPTION.to_string(),
break_even_points: Vec::new(),
long_call: Position::default(),
long_put: Position::default(),
};
let long_call_option = Options::new(
OptionType::European,
Side::Long,
underlying_symbol.clone(),
strike,
expiration,
implied_volatility,
quantity,
underlying_price,
risk_free_rate,
OptionStyle::Call,
dividend_yield,
None,
);
let long_call = Position::new(
long_call_option,
premium_long_call,
Utc::now(),
open_fee_long_call,
close_fee_long_call,
None,
None,
);
strategy
.add_position(&long_call)
.expect("Invalid long call");
let long_put_option = Options::new(
OptionType::European,
Side::Long,
underlying_symbol,
strike,
expiration,
implied_volatility,
quantity,
underlying_price,
risk_free_rate,
OptionStyle::Put,
dividend_yield,
None,
);
let long_put = Position::new(
long_put_option,
premium_long_put,
Utc::now(),
open_fee_long_put,
close_fee_long_put,
None,
None,
);
strategy.add_position(&long_put).expect("Invalid long put");
strategy
.update_break_even_points()
.expect("Unable to update break even points");
strategy
}
}
impl StrategyConstructor for LongStraddle {
fn get_strategy(vec_positions: &[Position]) -> Result<Self, StrategyError> {
if vec_positions.len() != 2 {
return Err(StrategyError::OperationError(
OperationErrorKind::InvalidParameters {
operation: "Long Straddle get_strategy".to_string(),
reason: "Must have exactly 2 options".to_string(),
},
));
}
let mut call_position = None;
let mut put_position = None;
for position in vec_positions {
match position.option.option_style {
OptionStyle::Call => call_position = Some(position),
OptionStyle::Put => put_position = Some(position),
}
}
let (call_position, put_position) = match (call_position, put_position) {
(Some(call), Some(put)) => (call, put),
_ => {
return Err(StrategyError::OperationError(
OperationErrorKind::InvalidParameters {
operation: "Long Straddle get_strategy".to_string(),
reason: "Must have one call and one put option".to_string(),
},
));
}
};
if call_position.option.strike_price != put_position.option.strike_price {
return Err(StrategyError::OperationError(
OperationErrorKind::InvalidParameters {
operation: "Long Straddle get_strategy".to_string(),
reason: "Options must have the same strike price".to_string(),
},
));
}
if call_position.option.side != Side::Long || put_position.option.side != Side::Long {
return Err(StrategyError::OperationError(
OperationErrorKind::InvalidParameters {
operation: "Long Straddle get_strategy".to_string(),
reason: "Both options must be long positions".to_string(),
},
));
}
if call_position.option.expiration_date != put_position.option.expiration_date {
return Err(StrategyError::OperationError(
OperationErrorKind::InvalidParameters {
operation: "Long Straddle get_strategy".to_string(),
reason: "Options must have the same expiration date".to_string(),
},
));
}
let long_call = Position::new(
call_position.option.clone(),
call_position.premium,
Utc::now(),
call_position.open_fee,
call_position.close_fee,
call_position.epic.clone(),
call_position.extra_fields.clone(),
);
let long_put = Position::new(
put_position.option.clone(),
put_position.premium,
Utc::now(),
put_position.open_fee,
put_position.close_fee,
put_position.epic.clone(),
put_position.extra_fields.clone(),
);
let mut strategy = LongStraddle {
name: "Long Straddle".to_string(),
kind: StrategyType::LongStraddle,
description: LONG_STRADDLE_DESCRIPTION.to_string(),
break_even_points: Vec::new(),
long_call,
long_put,
};
strategy.validate();
strategy.update_break_even_points()?;
Ok(strategy)
}
}
impl BreakEvenable for LongStraddle {
fn get_break_even_points(&self) -> Result<&Vec<Positive>, StrategyError> {
Ok(&self.break_even_points)
}
fn update_break_even_points(&mut self) -> Result<(), StrategyError> {
self.break_even_points = Vec::new();
let total_cost = self.get_total_cost()?;
self.break_even_points.push(
(self.long_put.option.strike_price - (total_cost / self.long_put.option.quantity))
.round_to(2),
);
self.break_even_points.push(
(self.long_call.option.strike_price + (total_cost / self.long_call.option.quantity))
.round_to(2),
);
self.break_even_points.sort();
Ok(())
}
}
impl Positionable for LongStraddle {
fn add_position(&mut self, position: &Position) -> Result<(), PositionError> {
match position.option.option_style {
OptionStyle::Call => {
self.long_call = position.clone();
Ok(())
}
OptionStyle::Put => {
self.long_put = position.clone();
Ok(())
}
}
}
fn get_positions(&self) -> Result<Vec<&Position>, PositionError> {
Ok(vec![&self.long_call, &self.long_put])
}
fn get_position(
&mut self,
option_style: &OptionStyle,
side: &Side,
strike: &Positive,
) -> Result<Vec<&mut Position>, PositionError> {
match (side, option_style, strike) {
(Side::Short, _, _) => Err(PositionError::invalid_position_type(
*side,
"Position side is Short, it is not valid for LongStraddle".to_string(),
)),
(Side::Long, OptionStyle::Call, strike)
if *strike == self.long_call.option.strike_price =>
{
Ok(vec![&mut self.long_call])
}
(Side::Long, OptionStyle::Put, strike)
if *strike == self.long_put.option.strike_price =>
{
Ok(vec![&mut self.long_put])
}
_ => Err(PositionError::invalid_position_type(
*side,
"Strike not found in positions".to_string(),
)),
}
}
fn modify_position(&mut self, position: &Position) -> Result<(), PositionError> {
if !position.validate() {
return Err(PositionError::ValidationError(
PositionValidationErrorKind::InvalidPosition {
reason: "Invalid position data".to_string(),
},
));
}
if position.option.side == Side::Short {
return Err(PositionError::invalid_position_type(
position.option.side,
"Position side is Short, it is not valid for LongStraddle".to_string(),
));
}
if position.option.strike_price != self.long_call.option.strike_price
&& position.option.strike_price != self.long_put.option.strike_price
{
return Err(PositionError::invalid_position_type(
position.option.side,
"Strike not found in positions".to_string(),
));
}
if position.option.option_style == OptionStyle::Call {
self.long_call = position.clone();
}
if position.option.option_style == OptionStyle::Put {
self.long_put = position.clone();
}
Ok(())
}
}
impl Strategable for LongStraddle {
fn info(&self) -> Result<StrategyBasics, StrategyError> {
Ok(StrategyBasics {
name: self.name.clone(),
kind: self.kind.clone(),
description: self.description.clone(),
})
}
}
impl BasicAble for LongStraddle {
fn get_title(&self) -> String {
let strategy_title = format!("{:?} Strategy: ", self.kind);
let leg_titles: Vec<String> = [self.long_call.get_title(), self.long_put.get_title()]
.iter()
.map(|leg| leg.to_string())
.collect();
if leg_titles.is_empty() {
strategy_title
} else {
format!("{}\n\t{}", strategy_title, leg_titles.join("\n\t"))
}
}
fn get_option_basic_type(&self) -> HashSet<OptionBasicType<'_>> {
let mut hash_set = HashSet::new();
let long_call = &self.long_call.option;
let long_put = &self.long_put.option;
hash_set.insert(OptionBasicType {
option_style: &long_call.option_style,
side: &long_call.side,
strike_price: &long_call.strike_price,
expiration_date: &long_call.expiration_date,
});
hash_set.insert(OptionBasicType {
option_style: &long_put.option_style,
side: &long_put.side,
strike_price: &long_put.strike_price,
expiration_date: &long_put.expiration_date,
});
hash_set
}
fn get_implied_volatility(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
let options = [
(
&self.long_call.option,
&self.long_call.option.implied_volatility,
),
(
&self.long_put.option,
&self.long_put.option.implied_volatility,
),
];
options
.into_iter()
.map(|(option, iv)| {
(
OptionBasicType {
option_style: &option.option_style,
side: &option.side,
strike_price: &option.strike_price,
expiration_date: &option.expiration_date,
},
iv,
)
})
.collect()
}
fn get_quantity(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
let options = [
(&self.long_call.option, &self.long_call.option.quantity),
(&self.long_put.option, &self.long_put.option.quantity),
];
options
.into_iter()
.map(|(option, quantity)| {
(
OptionBasicType {
option_style: &option.option_style,
side: &option.side,
strike_price: &option.strike_price,
expiration_date: &option.expiration_date,
},
quantity,
)
})
.collect()
}
fn one_option(&self) -> &Options {
self.long_call.one_option()
}
fn one_option_mut(&mut self) -> &mut Options {
self.long_call.one_option_mut()
}
fn set_expiration_date(
&mut self,
expiration_date: ExpirationDate,
) -> Result<(), StrategyError> {
self.long_call.option.expiration_date = expiration_date;
self.long_put.option.expiration_date = expiration_date;
Ok(())
}
fn set_underlying_price(&mut self, price: &Positive) -> Result<(), StrategyError> {
self.long_call.option.underlying_price = *price;
self.long_call.premium =
Positive::new_decimal(self.long_call.option.calculate_price_black_scholes()?.abs())
.unwrap_or(Positive::ZERO);
self.long_put.option.underlying_price = *price;
self.long_put.premium =
Positive::new_decimal(self.long_put.option.calculate_price_black_scholes()?.abs())
.unwrap_or(Positive::ZERO);
Ok(())
}
fn set_implied_volatility(&mut self, volatility: &Positive) -> Result<(), StrategyError> {
self.long_call.option.implied_volatility = *volatility;
self.long_put.option.implied_volatility = *volatility;
self.long_call.premium =
Positive::new_decimal(self.long_call.option.calculate_price_black_scholes()?.abs())
.unwrap_or(Positive::ZERO);
self.long_put.premium =
Positive::new_decimal(self.long_put.option.calculate_price_black_scholes()?.abs())
.unwrap_or(Positive::ZERO);
Ok(())
}
}
impl Strategies for LongStraddle {
fn get_max_profit(&self) -> Result<Positive, StrategyError> {
Ok(Positive::INFINITY) }
fn get_max_loss(&self) -> Result<Positive, StrategyError> {
Ok(self.get_total_cost()?)
}
fn get_profit_area(&self) -> Result<Decimal, StrategyError> {
let strike_diff = self.break_even_points[1] - self.break_even_points[0];
let cat = (strike_diff / 2.0_f64.sqrt()).to_f64();
let loss_area = (cat.powf(2.0)) / (2.0 * 10.0_f64.powf(cat.log10().ceil()));
let result = (1.0 / loss_area) * 10000.0; Ok(Decimal::from_f64(result).unwrap())
}
fn get_profit_ratio(&self) -> Result<Decimal, StrategyError> {
let break_even_diff = self.break_even_points[1] - self.break_even_points[0];
let result = match self.get_max_loss() {
Ok(max_loss) => ((break_even_diff / max_loss) * 100.0).to_f64(),
Err(_) => ZERO,
};
Ok(Decimal::from_f64(result).unwrap())
}
}
impl Validable for LongStraddle {
fn validate(&self) -> bool {
self.long_call.validate()
&& self.long_put.validate()
&& self.long_call.option.strike_price == self.long_put.option.strike_price
}
}
impl Optimizable for LongStraddle {
type Strategy = LongStraddle;
fn filter_combinations<'a>(
&'a self,
option_chain: &'a OptionChain,
side: FindOptimalSide,
) -> impl Iterator<Item = OptionDataGroup<'a>> {
let underlying_price = self.get_underlying_price();
let strategy = self.clone();
option_chain
.get_single_iter()
.filter(move |both| {
if side == FindOptimalSide::Center {
let atm_strike = match option_chain.atm_strike() {
Ok(atm_strike) => atm_strike,
Err(_) => return false,
};
both.is_valid_optimal_side(
underlying_price,
&FindOptimalSide::Range(*atm_strike, *atm_strike),
)
} else {
both.is_valid_optimal_side(underlying_price, &side)
}
})
.filter(|both| {
both.call_ask.unwrap_or(Positive::ZERO) > Positive::ZERO
&& both.call_bid.unwrap_or(Positive::ZERO) > Positive::ZERO
})
.filter(move |both| {
let legs = StrategyLegs::TwoLegs {
first: both,
second: both,
};
let strategy = strategy.create_strategy(option_chain, &legs);
strategy.validate()
&& strategy.get_max_profit().is_ok()
&& strategy.get_max_loss().is_ok()
})
.map(OptionDataGroup::One)
}
fn find_optimal(
&mut self,
option_chain: &OptionChain,
side: FindOptimalSide,
criteria: OptimizationCriteria,
) {
let mut best_value = Decimal::MIN;
let strategy_clone = self.clone();
let options_iter = strategy_clone.filter_combinations(option_chain, side);
for option_data_group in options_iter {
let both = match option_data_group {
OptionDataGroup::One(first) => first,
_ => panic!("Invalid OptionDataGroup"),
};
let legs = StrategyLegs::TwoLegs {
first: both,
second: both,
};
let strategy = self.create_strategy(option_chain, &legs);
let current_value = match criteria {
OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(),
OptimizationCriteria::Area => strategy.get_profit_area().unwrap(),
};
if current_value > best_value {
info!("Found better value: {}", current_value);
best_value = current_value;
*self = strategy.clone();
}
}
}
fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy {
let (call, put) = match legs {
StrategyLegs::TwoLegs { first, second } => (first, second),
_ => panic!("Invalid number of legs for this strategy"),
};
let implied_volatility = call.implied_volatility;
assert!(implied_volatility <= Positive::ONE);
LongStraddle::new(
chain.symbol.clone(),
chain.underlying_price,
call.strike_price,
self.long_call.option.expiration_date,
implied_volatility,
self.long_call.option.risk_free_rate,
self.long_call.option.dividend_yield,
self.long_call.option.quantity,
call.call_ask.unwrap(),
put.put_ask.unwrap(),
self.long_call.open_fee,
self.long_call.close_fee,
self.long_put.open_fee,
self.long_put.close_fee,
)
}
}
impl Profit for LongStraddle {
fn calculate_profit_at(&self, price: &Positive) -> Result<Decimal, PricingError> {
let price = Some(price);
Ok(self.long_call.pnl_at_expiration(&price)? + self.long_put.pnl_at_expiration(&price)?)
}
}
impl ProbabilityAnalysis for LongStraddle {
fn get_profit_ranges(&self) -> Result<Vec<ProfitLossRange>, ProbabilityError> {
let break_even_points = self.get_break_even_points()?;
let option = &self.long_call.option;
let expiration_date = &option.expiration_date;
let risk_free_rate = option.risk_free_rate;
let (mean_volatility, std_dev) = mean_and_std(vec![
option.implied_volatility,
self.long_put.option.implied_volatility,
]);
let mut lower_profit_range =
ProfitLossRange::new(None, Some(break_even_points[0]), Positive::ZERO)?;
lower_profit_range.calculate_probability(
self.get_underlying_price(),
Some(VolatilityAdjustment {
base_volatility: mean_volatility,
std_dev_adjustment: std_dev,
}),
None,
expiration_date,
Some(risk_free_rate),
)?;
let mut upper_profit_range =
ProfitLossRange::new(Some(break_even_points[1]), None, Positive::ZERO)?;
upper_profit_range.calculate_probability(
self.get_underlying_price(),
Some(VolatilityAdjustment {
base_volatility: mean_volatility,
std_dev_adjustment: std_dev,
}),
None,
expiration_date,
Some(risk_free_rate),
)?;
Ok(vec![lower_profit_range, upper_profit_range])
}
fn get_loss_ranges(&self) -> Result<Vec<ProfitLossRange>, ProbabilityError> {
let break_even_points = &self.get_break_even_points()?;
let option = &self.long_call.option;
let expiration_date = &option.expiration_date;
let risk_free_rate = option.risk_free_rate;
let (mean_volatility, std_dev) = mean_and_std(vec![
option.implied_volatility,
self.long_call.option.implied_volatility,
]);
let mut loss_range = ProfitLossRange::new(
Some(break_even_points[0]),
Some(break_even_points[1]),
Positive::ZERO,
)?;
loss_range.calculate_probability(
self.get_underlying_price(),
Some(VolatilityAdjustment {
base_volatility: mean_volatility,
std_dev_adjustment: std_dev,
}),
None,
expiration_date,
Some(risk_free_rate),
)?;
Ok(vec![loss_range])
}
}
impl Greeks for LongStraddle {
fn get_options(&self) -> Result<Vec<&Options>, GreeksError> {
Ok(vec![&self.long_call.option, &self.long_put.option])
}
}
impl DeltaNeutrality for LongStraddle {}
impl StraddleStrategy for LongStraddle {
fn strike(&self) -> Positive {
self.long_call.option.strike_price
}
fn call_position(&self) -> &Position {
&self.long_call
}
fn put_position(&self) -> &Position {
&self.long_put
}
fn is_long(&self) -> bool {
true
}
}
impl PnLCalculator for LongStraddle {
fn calculate_pnl(
&self,
market_price: &Positive,
expiration_date: ExpirationDate,
implied_volatility: &Positive,
) -> Result<PnL, PricingError> {
Ok(self
.long_call
.calculate_pnl(market_price, expiration_date, implied_volatility)?
+ self
.long_put
.calculate_pnl(market_price, expiration_date, implied_volatility)?)
}
fn calculate_pnl_at_expiration(
&self,
underlying_price: &Positive,
) -> Result<PnL, PricingError> {
Ok(self
.long_call
.calculate_pnl_at_expiration(underlying_price)?
+ self
.long_put
.calculate_pnl_at_expiration(underlying_price)?)
}
}
test_strategy_traits!(LongStraddle, test_short_call_implementations);
#[cfg(test)]
mod tests_long_straddle_probability {
use super::*;
use crate::model::ExpirationDate;
use positive::pos_or_panic;
use crate::strategies::probabilities::utils::PriceTrend;
use rust_decimal_macros::dec;
fn create_test_long_straddle() -> LongStraddle {
LongStraddle::new(
"TEST".to_string(),
Positive::HUNDRED, pos_or_panic!(110.0), ExpirationDate::Days(pos_or_panic!(30.0)), pos_or_panic!(0.2), dec!(0.05), Positive::ZERO, Positive::ONE, Positive::TWO, Positive::TWO, Positive::ZERO, Positive::ZERO, Positive::ZERO, Positive::ZERO, )
}
#[test]
fn test_get_expiration() {
let straddle = create_test_long_straddle();
let expiration_date = *straddle.get_expiration().values().next().unwrap();
assert_eq!(expiration_date, &ExpirationDate::Days(pos_or_panic!(30.0)));
}
#[test]
fn test_get_risk_free_rate() {
let straddle = create_test_long_straddle();
assert_eq!(
**straddle.get_risk_free_rate().values().next().unwrap(),
dec!(0.05)
);
}
#[test]
fn test_get_profit_ranges() {
let straddle = create_test_long_straddle();
let result = straddle.get_profit_ranges();
assert!(result.is_ok());
let ranges = result.unwrap();
assert_eq!(ranges.len(), 2);
assert!(ranges[0].upper_bound.is_some());
assert!(ranges[1].lower_bound.is_some());
}
#[test]
fn test_get_loss_ranges() {
let straddle = create_test_long_straddle();
let result = straddle.get_loss_ranges();
assert!(result.is_ok());
let ranges = result.unwrap();
assert_eq!(ranges.len(), 1); assert!(ranges[0].lower_bound.is_some());
assert!(ranges[0].upper_bound.is_some());
}
#[test]
fn test_probability_of_profit() {
let straddle = create_test_long_straddle();
let result = straddle.probability_of_profit(None, None);
assert!(result.is_ok());
let prob = result.unwrap();
assert!(prob > Positive::ZERO);
assert!(prob <= Positive::ONE);
}
#[test]
fn test_probability_with_volatility_adjustment() {
let straddle = create_test_long_straddle();
let vol_adj = Some(VolatilityAdjustment {
base_volatility: pos_or_panic!(0.25),
std_dev_adjustment: pos_or_panic!(0.1),
});
let result = straddle.probability_of_profit(vol_adj, None);
assert!(result.is_ok());
let prob = result.unwrap();
assert!(prob > Positive::ZERO);
assert!(prob <= Positive::ONE);
}
#[test]
fn test_probability_with_trend() {
let straddle = create_test_long_straddle();
let trend = Some(PriceTrend {
drift_rate: 0.1,
confidence: 0.95,
});
let result = straddle.probability_of_profit(None, trend);
assert!(result.is_ok());
let prob = result.unwrap();
assert!(prob > Positive::ZERO);
assert!(prob <= Positive::ONE);
}
#[test]
fn test_expected_value_calculation() {
let straddle = create_test_long_straddle();
let result = straddle.expected_value(None, None);
assert!(result.is_ok());
let ev = result.unwrap();
assert!(
ev >= Positive::ZERO,
"Expected value should be non-negative"
);
let vol_adj = Some(VolatilityAdjustment {
base_volatility: pos_or_panic!(0.25),
std_dev_adjustment: pos_or_panic!(0.1),
});
let result_with_vol = straddle.expected_value(vol_adj, None);
assert!(result_with_vol.is_ok());
assert!(result_with_vol.unwrap() >= Positive::ZERO);
}
#[test]
fn test_calculate_extreme_probabilities() {
let straddle = create_test_long_straddle();
let result = straddle.calculate_extreme_probabilities(None, None);
assert!(result.is_ok());
let (max_profit_prob, max_loss_prob) = result.unwrap();
assert!(max_profit_prob >= Positive::ZERO);
assert!(max_loss_prob >= Positive::ZERO);
assert!(max_profit_prob + max_loss_prob <= Positive::ONE);
}
}
#[cfg(test)]
mod tests_long_straddle_delta {
use super::*;
use crate::assert_decimal_eq;
use crate::greeks::Greeks;
use crate::model::types::OptionStyle;
use crate::strategies::delta_neutral::DELTA_THRESHOLD;
use crate::strategies::delta_neutral::{DeltaAdjustment, DeltaNeutrality};
use crate::strategies::long_straddle::{LongStraddle, Positive};
use positive::{assert_pos_relative_eq, pos_or_panic};
use rust_decimal_macros::dec;
fn get_strategy(strike: Positive) -> LongStraddle {
let underlying_price = pos_or_panic!(7138.5);
LongStraddle::new(
"CL".to_string(),
underlying_price, strike, ExpirationDate::Days(pos_or_panic!(45.0)),
pos_or_panic!(0.3745), dec!(0.05), Positive::ZERO, Positive::ONE, pos_or_panic!(84.2), pos_or_panic!(353.2), pos_or_panic!(7.01), pos_or_panic!(7.01), pos_or_panic!(7.01), pos_or_panic!(7.01), )
}
#[test]
fn create_test_short_straddle_reducing_adjustments() {
let strike = pos_or_panic!(7450.0);
let strategy = get_strategy(strike);
let size = dec!(-0.168);
let delta = pos_or_panic!(0.4039537995372765);
let k = pos_or_panic!(7450.0);
assert_decimal_eq!(
strategy.delta_neutrality().unwrap().net_delta,
size,
DELTA_THRESHOLD
);
assert!(!strategy.is_delta_neutral());
let binding = strategy.delta_adjustments().unwrap();
let suggestion = binding.first().unwrap();
match suggestion {
DeltaAdjustment::BuyOptions {
quantity,
strike,
option_style,
side,
} => {
assert_pos_relative_eq!(
*quantity,
delta,
Positive::new_decimal(DELTA_THRESHOLD).unwrap()
);
assert_pos_relative_eq!(
*strike,
k,
Positive::new_decimal(DELTA_THRESHOLD).unwrap()
);
assert_eq!(*option_style, OptionStyle::Call);
assert_eq!(*side, Side::Long);
}
_ => panic!("Invalid suggestion"),
}
let mut option = strategy.long_call.option.clone();
option.quantity = delta;
let delta = option.delta().unwrap();
assert_decimal_eq!(delta, -size, DELTA_THRESHOLD);
assert_decimal_eq!(
delta + strategy.delta_neutrality().unwrap().net_delta,
Decimal::ZERO,
DELTA_THRESHOLD
);
}
#[test]
fn create_test_short_straddle_increasing_adjustments() {
let strategy = get_strategy(pos_or_panic!(7150.0));
let size = dec!(0.079961694);
let delta = pos_or_panic!(0.17382253382440663);
let k = pos_or_panic!(7150.0);
assert_decimal_eq!(
strategy.delta_neutrality().unwrap().net_delta,
size,
DELTA_THRESHOLD
);
assert!(!strategy.is_delta_neutral());
let binding = strategy.delta_adjustments().unwrap();
match &binding[1] {
DeltaAdjustment::BuyOptions {
quantity,
strike,
option_style,
side,
} => {
assert_pos_relative_eq!(
*quantity,
delta,
Positive::new_decimal(DELTA_THRESHOLD).unwrap()
);
assert_pos_relative_eq!(
*strike,
k,
Positive::new_decimal(DELTA_THRESHOLD).unwrap()
);
assert_eq!(*option_style, OptionStyle::Put);
assert_eq!(*side, Side::Long);
}
_ => panic!("Invalid suggestion"),
}
let mut option = strategy.long_put.option.clone();
option.quantity = delta;
let delta = option.delta().unwrap();
assert_decimal_eq!(delta, -size, DELTA_THRESHOLD);
assert_decimal_eq!(
delta + strategy.delta_neutrality().unwrap().net_delta,
Decimal::ZERO,
DELTA_THRESHOLD
);
}
#[test]
fn create_test_short_straddle_no_adjustments() {
let strategy = get_strategy(pos_or_panic!(7245.0));
assert_decimal_eq!(
strategy.delta_neutrality().unwrap().net_delta,
Decimal::ZERO,
DELTA_THRESHOLD
);
assert!(strategy.is_delta_neutral());
let suggestion = strategy.delta_adjustments().unwrap();
assert_eq!(suggestion[0], DeltaAdjustment::NoAdjustmentNeeded);
}
}
#[cfg(test)]
mod tests_long_straddle_delta_size {
use crate::greeks::Greeks;
use crate::model::types::OptionStyle;
use crate::strategies::delta_neutral::DELTA_THRESHOLD;
use crate::strategies::delta_neutral::{DeltaAdjustment, DeltaNeutrality};
use crate::strategies::long_straddle::{LongStraddle, Positive};
use crate::{ExpirationDate, Side, assert_decimal_eq};
use positive::{assert_pos_relative_eq, pos_or_panic};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::str::FromStr;
fn get_strategy(strike: Positive) -> LongStraddle {
let underlying_price = pos_or_panic!(7138.5);
LongStraddle::new(
"CL".to_string(),
underlying_price, strike, ExpirationDate::Days(pos_or_panic!(45.0)),
pos_or_panic!(0.3745), dec!(0.05), Positive::ZERO, Positive::TWO, pos_or_panic!(84.2), pos_or_panic!(353.2), pos_or_panic!(7.01), pos_or_panic!(7.01), pos_or_panic!(7.01), pos_or_panic!(7.01), )
}
#[test]
fn create_test_short_straddle_reducing_adjustments() {
let strike = pos_or_panic!(7450.0);
let strategy = get_strategy(strike);
let size = dec!(-0.3360);
let delta = pos_or_panic!(0.807_907_599_074_553);
let k = pos_or_panic!(7450.0);
assert_decimal_eq!(
strategy.delta_neutrality().unwrap().net_delta,
size,
DELTA_THRESHOLD
);
assert!(!strategy.is_delta_neutral());
let binding = strategy.delta_adjustments().unwrap();
let suggestion = binding.first().unwrap();
match suggestion {
DeltaAdjustment::BuyOptions {
quantity,
strike,
option_style,
side,
} => {
assert_pos_relative_eq!(
*quantity,
delta,
Positive::new_decimal(DELTA_THRESHOLD).unwrap()
);
assert_pos_relative_eq!(
*strike,
k,
Positive::new_decimal(DELTA_THRESHOLD).unwrap()
);
assert_eq!(*option_style, OptionStyle::Call);
assert_eq!(*side, Side::Long);
}
_ => panic!("Invalid suggestion"),
}
let mut option = strategy.long_call.option.clone();
option.quantity = delta;
let delta = option.delta().unwrap();
assert_decimal_eq!(delta, -size, DELTA_THRESHOLD);
assert_decimal_eq!(
delta + strategy.delta_neutrality().unwrap().net_delta,
Decimal::ZERO,
DELTA_THRESHOLD
);
}
#[test]
fn create_test_short_straddle_increasing_adjustments() {
let strategy = get_strategy(pos_or_panic!(7150.0));
let size = dec!(0.1599);
let delta =
Positive::new_decimal(Decimal::from_str("0.3476450676488132").unwrap()).unwrap();
let k = pos_or_panic!(7150.0);
assert_decimal_eq!(
strategy.delta_neutrality().unwrap().net_delta,
size,
DELTA_THRESHOLD
);
assert!(!strategy.is_delta_neutral());
let binding = strategy.delta_adjustments().unwrap();
match &binding[1] {
DeltaAdjustment::BuyOptions {
quantity,
strike,
option_style,
side,
} => {
assert_pos_relative_eq!(
*quantity,
delta,
Positive::new_decimal(DELTA_THRESHOLD).unwrap()
);
assert_pos_relative_eq!(
*strike,
k,
Positive::new_decimal(DELTA_THRESHOLD).unwrap()
);
assert_eq!(*option_style, OptionStyle::Put);
assert_eq!(*side, Side::Long);
}
_ => panic!("Invalid suggestion"),
}
let mut option = strategy.long_put.option.clone();
option.quantity = delta;
let delta = option.delta().unwrap();
assert_decimal_eq!(delta, -size, DELTA_THRESHOLD);
assert_decimal_eq!(
delta + strategy.delta_neutrality().unwrap().net_delta,
Decimal::ZERO,
DELTA_THRESHOLD
);
}
#[test]
fn create_test_short_straddle_no_adjustments() {
let strategy = get_strategy(pos_or_panic!(7245.0));
assert_decimal_eq!(
strategy.delta_neutrality().unwrap().net_delta,
Decimal::ZERO,
DELTA_THRESHOLD
);
assert!(strategy.is_delta_neutral());
let suggestion = strategy.delta_adjustments().unwrap();
assert_eq!(suggestion[0], DeltaAdjustment::NoAdjustmentNeeded);
}
}
#[cfg(test)]
mod tests_straddle_position_management {
use super::*;
use crate::error::position::PositionValidationErrorKind;
use crate::model::types::{OptionStyle, Side};
use positive::pos_or_panic;
use rust_decimal_macros::dec;
use tracing::error;
fn create_test_long_straddle() -> LongStraddle {
LongStraddle::new(
"TEST".to_string(),
Positive::HUNDRED, pos_or_panic!(110.0), ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2), dec!(0.05), Positive::ZERO, Positive::ONE, Positive::TWO, Positive::TWO, pos_or_panic!(0.1), pos_or_panic!(0.1), pos_or_panic!(0.1), pos_or_panic!(0.1), )
}
#[test]
fn test_long_straddle_get_position() {
let mut straddle = create_test_long_straddle();
let call_position =
straddle.get_position(&OptionStyle::Call, &Side::Long, &pos_or_panic!(110.0));
assert!(call_position.is_ok());
let positions = call_position.unwrap();
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].option.strike_price, pos_or_panic!(110.0));
assert_eq!(positions[0].option.option_style, OptionStyle::Call);
assert_eq!(positions[0].option.side, Side::Long);
let put_position =
straddle.get_position(&OptionStyle::Put, &Side::Long, &pos_or_panic!(110.0));
assert!(put_position.is_ok());
let positions = put_position.unwrap();
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].option.strike_price, pos_or_panic!(110.0));
assert_eq!(positions[0].option.option_style, OptionStyle::Put);
assert_eq!(positions[0].option.side, Side::Long);
let invalid_position =
straddle.get_position(&OptionStyle::Call, &Side::Long, &Positive::HUNDRED);
assert!(invalid_position.is_err());
match invalid_position {
Err(PositionError::ValidationError(
PositionValidationErrorKind::IncompatibleSide {
position_side: _,
reason,
},
)) => {
assert_eq!(reason, "Strike not found in positions");
}
_ => {
error!("Unexpected error: {:?}", invalid_position);
panic!()
}
}
}
#[test]
fn test_long_straddle_modify_position() {
let mut straddle = create_test_long_straddle();
let mut modified_call = straddle.long_call.clone();
modified_call.option.quantity = Positive::TWO;
let result = straddle.modify_position(&modified_call);
assert!(result.is_ok());
assert_eq!(straddle.long_call.option.quantity, Positive::TWO);
let mut modified_put = straddle.long_put.clone();
modified_put.option.quantity = Positive::TWO;
let result = straddle.modify_position(&modified_put);
assert!(result.is_ok());
assert_eq!(straddle.long_put.option.quantity, Positive::TWO);
let mut invalid_position = straddle.long_call.clone();
invalid_position.option.strike_price = pos_or_panic!(95.0);
let result = straddle.modify_position(&invalid_position);
assert!(result.is_err());
match result {
Err(PositionError::ValidationError(kind)) => match kind {
PositionValidationErrorKind::IncompatibleSide {
position_side: _,
reason,
} => {
assert_eq!(reason, "Strike not found in positions");
}
_ => panic!("Expected ValidationError::InvalidPosition"),
},
_ => panic!("Expected ValidationError"),
}
}
}
#[cfg(test)]
mod tests_adjust_option_position {
use super::*;
use crate::model::types::{OptionStyle, Side};
use positive::pos_or_panic;
use rust_decimal_macros::dec;
fn create_test_long_straddle() -> LongStraddle {
LongStraddle::new(
"TEST".to_string(),
Positive::HUNDRED, pos_or_panic!(110.0), ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2), dec!(0.05), Positive::ZERO, Positive::ONE, Positive::TWO, Positive::TWO, pos_or_panic!(0.1), pos_or_panic!(0.1), pos_or_panic!(0.1), pos_or_panic!(0.1), )
}
#[test]
fn test_adjust_existing_call_position_long() {
let mut strategy = create_test_long_straddle();
let initial_quantity = strategy.long_call.option.quantity;
let adjustment = Positive::ONE;
let result = strategy.adjust_option_position(
adjustment.to_dec(),
&pos_or_panic!(110.0),
&OptionStyle::Call,
&Side::Long,
);
assert!(result.is_ok());
assert_eq!(
strategy.long_call.option.quantity,
initial_quantity + adjustment
);
}
#[test]
fn test_adjust_existing_put_position_long() {
let mut strategy = create_test_long_straddle();
let initial_quantity = strategy.long_put.option.quantity;
let adjustment = Positive::ONE;
let result = strategy.adjust_option_position(
adjustment.to_dec(),
&pos_or_panic!(110.0),
&OptionStyle::Put,
&Side::Long,
);
assert!(result.is_ok());
assert_eq!(
strategy.long_put.option.quantity,
initial_quantity + adjustment
);
}
#[test]
fn test_adjust_nonexistent_position_long() {
let mut strategy = create_test_long_straddle();
let result = strategy.adjust_option_position(
Decimal::ONE,
&Positive::HUNDRED,
&OptionStyle::Call,
&Side::Short,
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("Position side is Short, it is not valid for LongStraddle")
);
}
#[test]
fn test_adjust_with_invalid_strike_long() {
let mut strategy = create_test_long_straddle();
let result = strategy.adjust_option_position(
Decimal::ONE,
&Positive::HUNDRED, &OptionStyle::Call,
&Side::Short,
);
assert!(result.is_err());
}
#[test]
fn test_zero_quantity_adjustment_long() {
let mut strategy = create_test_long_straddle();
let initial_quantity = strategy.long_call.option.quantity;
let result = strategy.adjust_option_position(
Decimal::ZERO,
&pos_or_panic!(110.0),
&OptionStyle::Call,
&Side::Long,
);
assert!(result.is_ok());
assert_eq!(strategy.long_call.option.quantity, initial_quantity);
}
}
#[cfg(test)]
mod tests_long_strategy_constructor {
use super::*;
use crate::model::utils::create_sample_position;
use positive::pos_or_panic;
#[test]
fn test_get_strategy_valid() {
let options = vec![
create_sample_position(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
),
create_sample_position(
OptionStyle::Put,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
),
];
let result = LongStraddle::get_strategy(&options);
assert!(result.is_ok());
let strategy = result.unwrap();
assert_eq!(strategy.long_call.option.strike_price, Positive::HUNDRED);
assert_eq!(strategy.long_put.option.strike_price, Positive::HUNDRED);
}
#[test]
fn test_get_strategy_wrong_number_of_options() {
let options = vec![create_sample_position(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
)];
let result = LongStraddle::get_strategy(&options);
assert!(matches!(
result,
Err(StrategyError::OperationError(OperationErrorKind::InvalidParameters { operation, reason }))
if operation == "Long Straddle get_strategy" && reason == "Must have exactly 2 options"
));
}
#[test]
fn test_get_strategy_missing_put_option() {
let options = vec![
create_sample_position(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
),
create_sample_position(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
),
];
let result = LongStraddle::get_strategy(&options);
assert!(matches!(
result,
Err(StrategyError::OperationError(OperationErrorKind::InvalidParameters { operation, reason }))
if operation == "Long Straddle get_strategy" && reason == "Must have one call and one put option"
));
}
#[test]
fn test_get_strategy_different_strikes() {
let options = vec![
create_sample_position(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
),
create_sample_position(
OptionStyle::Put,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
pos_or_panic!(95.0),
pos_or_panic!(0.2),
),
];
let result = LongStraddle::get_strategy(&options);
assert!(matches!(
result,
Err(StrategyError::OperationError(OperationErrorKind::InvalidParameters { operation, reason }))
if operation == "Long Straddle get_strategy" && reason == "Options must have the same strike price"
));
}
#[test]
fn test_get_strategy_wrong_sides() {
let options = vec![
create_sample_position(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
),
create_sample_position(
OptionStyle::Put,
Side::Short,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
),
];
let result = LongStraddle::get_strategy(&options);
assert!(matches!(
result,
Err(StrategyError::OperationError(OperationErrorKind::InvalidParameters { operation, reason }))
if operation == "Long Straddle get_strategy" && reason == "Both options must be long positions"
));
}
#[test]
fn test_get_strategy_different_expiration_dates() {
let mut option1 = create_sample_position(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
);
let mut option2 = create_sample_position(
OptionStyle::Put,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
);
option1.option.expiration_date = ExpirationDate::Days(pos_or_panic!(30.0));
option2.option.expiration_date = ExpirationDate::Days(pos_or_panic!(60.0));
let options = vec![option1, option2];
let result = LongStraddle::get_strategy(&options);
assert!(matches!(
result,
Err(StrategyError::OperationError(OperationErrorKind::InvalidParameters { operation, reason }))
if operation == "Long Straddle get_strategy" && reason == "Options must have the same expiration date"
));
}
}
#[cfg(test)]
mod tests_long_straddle_pnl {
use super::*;
use crate::assert_decimal_eq;
use crate::model::utils::create_sample_position;
use positive::{assert_pos_relative_eq, pos_or_panic};
use rust_decimal_macros::dec;
fn create_test_long_straddle() -> Result<LongStraddle, StrategyError> {
let long_call = create_sample_position(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED, Positive::ONE, Positive::HUNDRED, pos_or_panic!(0.2), );
let long_put = create_sample_position(
OptionStyle::Put,
Side::Long,
Positive::HUNDRED, Positive::ONE, Positive::HUNDRED, pos_or_panic!(0.2), );
LongStraddle::get_strategy(&[long_call, long_put])
}
#[test]
fn test_calculate_pnl_at_strike() {
let straddle = create_test_long_straddle().unwrap();
let market_price = Positive::HUNDRED; let expiration_date = ExpirationDate::Days(pos_or_panic!(20.0));
let implied_volatility = pos_or_panic!(0.2);
let result = straddle.calculate_pnl(&market_price, expiration_date, &implied_volatility);
assert!(result.is_ok());
let pnl = result.unwrap();
assert!(pnl.unrealized.is_some());
assert_pos_relative_eq!(pnl.initial_income, Positive::ZERO, pos_or_panic!(1e-6)); assert_pos_relative_eq!(pnl.initial_costs, pos_or_panic!(12.0), pos_or_panic!(1e-6)); }
#[test]
fn test_calculate_pnl_below_strike() {
let straddle = create_test_long_straddle().unwrap();
let market_price = Positive::HUNDRED; let expiration_date = ExpirationDate::Days(pos_or_panic!(20.0));
let implied_volatility = pos_or_panic!(0.2);
let result = straddle.calculate_pnl(&market_price, expiration_date, &implied_volatility);
assert!(result.is_ok());
let pnl = result.unwrap();
assert!(pnl.unrealized.is_some());
assert!(pnl.unrealized.unwrap() < dec!(0.0)); }
#[test]
fn test_calculate_pnl_above_strike() {
let straddle = create_test_long_straddle().unwrap();
let market_price = pos_or_panic!(110.0); let expiration_date = ExpirationDate::Days(pos_or_panic!(20.0));
let implied_volatility = pos_or_panic!(0.2);
let result = straddle.calculate_pnl(&market_price, expiration_date, &implied_volatility);
assert!(result.is_ok());
let pnl = result.unwrap();
assert!(pnl.unrealized.is_some());
assert!(pnl.unrealized.unwrap() > dec!(0.0)); }
#[test]
fn test_calculate_pnl_with_higher_volatility() {
let straddle = create_test_long_straddle().unwrap();
let market_price = Positive::HUNDRED;
let expiration_date = ExpirationDate::Days(pos_or_panic!(20.0));
let implied_volatility = pos_or_panic!(0.4);
let result = straddle.calculate_pnl(&market_price, expiration_date, &implied_volatility);
assert!(result.is_ok());
let pnl = result.unwrap();
assert!(pnl.unrealized.is_some());
assert!(pnl.unrealized.unwrap() < dec!(3.0));
}
#[test]
fn test_calculate_pnl_at_expiration_max_profit() {
let straddle = create_test_long_straddle().unwrap();
let underlying_price = Positive::HUNDRED;
let result = straddle.calculate_pnl_at_expiration(&underlying_price);
assert!(result.is_ok());
let pnl = result.unwrap();
assert!(pnl.realized.is_some());
assert_decimal_eq!(pnl.realized.unwrap(), dec!(-12.0), dec!(1e-6)); assert_eq!(pnl.initial_income, Positive::ZERO);
assert_eq!(pnl.initial_costs, pos_or_panic!(12.0));
}
}