use super::adjustment::{AdjustmentConfig, AdjustmentPlan};
use super::optimizer::AdjustmentOptimizer;
use super::portfolio::{AdjustmentTarget, PortfolioGreeks};
use crate::error::position::PositionValidationErrorKind;
use crate::error::{GreeksError, PositionError, StrategyError};
use crate::greeks::Greeks;
use crate::greeks::calculate_delta_neutral_sizes;
use crate::model::types::{Action, OptionStyle};
use crate::model::{Trade, TradeStatusAble};
use crate::prelude::OperationErrorKind;
use crate::strategies::Strategies;
use crate::strategies::base::Positionable;
use crate::{Options, Side};
use positive::Positive;
use pretty_simple_display::{DebugPretty, DisplaySimple};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::fmt;
use tracing::{debug, warn};
use utoipa::ToSchema;
pub const DELTA_THRESHOLD: Decimal = dec!(0.0001);
#[derive(Debug, PartialEq, Serialize, Deserialize, ToSchema)]
pub enum DeltaAdjustment {
BuyOptions {
quantity: Positive,
strike: Positive,
option_style: OptionStyle,
side: Side,
},
SellOptions {
quantity: Positive,
strike: Positive,
option_style: OptionStyle,
side: Side,
},
BuyUnderlying(Positive),
SellUnderlying(Positive),
NoAdjustmentNeeded,
SameSize(DeltaAdjustmentSameSize),
}
#[derive(DebugPretty, DisplaySimple, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct DeltaAdjustmentSameSize {
pub first: Box<DeltaAdjustment>,
pub second: Box<DeltaAdjustment>,
}
impl fmt::Display for DeltaAdjustment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DeltaAdjustment::BuyOptions {
quantity,
strike,
option_style,
side,
} => {
write!(
f,
"Buy {quantity} {side} {option_style} options at strike {strike}"
)
}
DeltaAdjustment::SellOptions {
quantity,
strike,
option_style,
side,
} => {
write!(
f,
"Sell {quantity} {side} {option_style} options at strike {strike}"
)
}
DeltaAdjustment::BuyUnderlying(quantity) => {
write!(f, "Buy {quantity} units of the underlying asset")
}
DeltaAdjustment::SellUnderlying(quantity) => {
write!(f, "Sell {quantity} units of the underlying asset")
}
DeltaAdjustment::NoAdjustmentNeeded => {
write!(f, "No adjustment needed")
}
DeltaAdjustment::SameSize(adj) => {
write!(
f,
"Same size adjustments: [{}] and [{}]",
adj.first, adj.second
)
}
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema)]
pub struct DeltaPositionInfo {
pub delta: Decimal,
pub delta_per_contract: Decimal,
pub quantity: Positive,
pub strike: Positive,
pub option_style: OptionStyle,
pub side: Side,
}
impl fmt::Display for DeltaPositionInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Delta: {:.4}", self.delta)?;
writeln!(f, " Delta per Contract: {:.4}", self.delta_per_contract)?;
writeln!(f, " Quantity: {:.4}", self.quantity)?;
writeln!(f, " Strike: {}", self.strike)?;
writeln!(f, " Option Style: {:?}", self.option_style)?;
writeln!(f, " Side: {:?}", self.side)?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct DeltaInfo {
pub net_delta: Decimal,
pub individual_deltas: Vec<DeltaPositionInfo>,
pub is_neutral: bool,
pub neutrality_threshold: Decimal,
pub underlying_price: Positive,
}
impl fmt::Display for DeltaInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Delta Analysis:")?;
writeln!(f, " Net Delta: {:.4}", self.net_delta)?;
writeln!(f, " Is Neutral: {}", self.is_neutral)?;
writeln!(
f,
" Neutrality Threshold: {:.4}",
self.neutrality_threshold
)?;
writeln!(f, " Underlying Price: {}", self.underlying_price)?;
writeln!(f, " Individual Deltas:")?;
for (i, delta) in self.individual_deltas.iter().enumerate() {
writeln!(f, " Position {}: {:.4}", i + 1, delta)?;
}
Ok(())
}
}
pub trait DeltaNeutrality: Greeks + Positionable + Strategies {
fn delta_neutrality(&self) -> Result<DeltaInfo, GreeksError> {
let options = self.get_options()?;
if options.is_empty() {
return Err(GreeksError::StdError("No options found".to_string()));
}
let underlying_price = *self.get_underlying_price();
let individual_deltas: Vec<DeltaPositionInfo> = options
.iter()
.map(|option| DeltaPositionInfo {
delta: option.delta().unwrap(),
delta_per_contract: option.delta().unwrap() / option.quantity,
quantity: option.quantity,
strike: option.strike_price,
option_style: option.option_style,
side: option.side,
})
.collect();
Ok(DeltaInfo {
net_delta: self.delta()?,
individual_deltas,
is_neutral: self.is_delta_neutral(),
underlying_price,
neutrality_threshold: DELTA_THRESHOLD,
})
}
fn is_delta_neutral(&self) -> bool {
match self.delta() {
Ok(delta) => delta.abs() <= DELTA_THRESHOLD,
Err(_) => false,
}
}
fn get_atm_strike(&self) -> Result<Positive, StrategyError> {
Ok(*self.get_underlying_price())
}
fn generate_delta_adjustments(
&self,
net_delta: Decimal,
option_delta_per_contract: Decimal,
option: &Options,
) -> Result<DeltaAdjustment, GreeksError> {
if net_delta.is_zero() {
return Ok(DeltaAdjustment::NoAdjustmentNeeded);
}
if option_delta_per_contract.is_zero() {
return Err(GreeksError::StdError(
"Option delta per contract cannot be zero".to_string(),
));
}
let total_contracts_needed = (net_delta / option_delta_per_contract).abs();
if total_contracts_needed == option.quantity.to_dec() {
return Ok(DeltaAdjustment::NoAdjustmentNeeded);
}
let adjustment = match (
net_delta.is_sign_positive(),
option_delta_per_contract.is_sign_positive(),
option.quantity.to_dec() > total_contracts_needed,
) {
(true, true, true) => {
DeltaAdjustment::SellOptions {
quantity: Positive::new_decimal(total_contracts_needed)?,
strike: option.strike_price,
option_style: option.option_style,
side: option.side,
}
}
(true, true, false) => {
return Err(GreeksError::StdError(
"we can't sell what we don't have, so no adjustment is possible".to_string(),
));
}
(true, false, true) => {
DeltaAdjustment::BuyOptions {
quantity: Positive::new_decimal(total_contracts_needed.abs())
.unwrap_or(Positive::ZERO),
strike: option.strike_price,
option_style: option.option_style,
side: option.side,
}
}
(true, false, false) => DeltaAdjustment::BuyOptions {
quantity: Positive::new_decimal(total_contracts_needed.abs())
.unwrap_or(Positive::ZERO),
strike: option.strike_price,
option_style: option.option_style,
side: option.side,
},
(false, true, true) => {
DeltaAdjustment::BuyOptions {
quantity: Positive::new_decimal(total_contracts_needed.abs())
.unwrap_or(Positive::ZERO),
strike: option.strike_price,
option_style: option.option_style,
side: option.side,
}
}
(false, true, false) => DeltaAdjustment::BuyOptions {
quantity: Positive::new_decimal(total_contracts_needed.abs())
.unwrap_or(Positive::ZERO),
strike: option.strike_price,
option_style: option.option_style,
side: option.side,
},
(false, false, true) => {
DeltaAdjustment::SellOptions {
quantity: Positive::new_decimal(total_contracts_needed.abs())
.unwrap_or(Positive::ZERO),
strike: option.strike_price,
option_style: option.option_style,
side: option.side,
}
}
(false, false, false) => {
return Err(GreeksError::StdError(
"we can't sell what we don't have, so no adjustment is possible".to_string(),
));
}
};
Ok(adjustment)
}
fn delta_adjustments(&self) -> Result<Vec<DeltaAdjustment>, GreeksError> {
let net_delta = self.delta()?;
if net_delta.abs() <= DELTA_THRESHOLD {
return Ok(vec![DeltaAdjustment::NoAdjustmentNeeded]);
}
let options = self.get_options()?;
let mut adjustments = Vec::with_capacity(options.len());
let mut total_size: Positive = Positive::ZERO;
for option in &options {
let option_delta_per_contract = option.delta()? / option.quantity.to_dec();
total_size += option.quantity;
if option_delta_per_contract.abs() > DELTA_THRESHOLD / dec!(10.0) {
match self.generate_delta_adjustments(net_delta, option_delta_per_contract, option)
{
Ok(adjustment) => adjustments.push(adjustment),
Err(_) => {
warn!("We might not be able to sell options if we don't have enough");
}
}
}
}
if options.len() == 2 {
let (delta_neutral_size1, delta_neutral_size2) = calculate_delta_neutral_sizes(
options[0].delta()?,
options[1].delta()?,
total_size,
)?;
let size_diff1: Decimal = delta_neutral_size1.to_dec() - options[0].quantity.to_dec();
let size_diff2: Decimal = delta_neutral_size2.to_dec() - options[1].quantity.to_dec();
let adjustment1 = if size_diff1.is_sign_positive() {
DeltaAdjustment::BuyOptions {
quantity: Positive::new_decimal(size_diff1.abs()).unwrap_or(Positive::ZERO),
strike: options[0].strike_price,
option_style: options[0].option_style,
side: options[0].side,
}
} else if !size_diff1.is_zero() {
DeltaAdjustment::SellOptions {
quantity: Positive::new_decimal(size_diff1.abs()).unwrap_or(Positive::ZERO),
strike: options[0].strike_price,
option_style: options[0].option_style,
side: options[0].side,
}
} else {
DeltaAdjustment::NoAdjustmentNeeded
};
let adjustment2 = if size_diff2.is_sign_positive() {
DeltaAdjustment::BuyOptions {
quantity: Positive::new_decimal(size_diff2.abs()).unwrap_or(Positive::ZERO),
strike: options[1].strike_price,
option_style: options[1].option_style,
side: options[1].side,
}
} else if !size_diff2.is_zero() {
DeltaAdjustment::SellOptions {
quantity: Positive::new_decimal(size_diff2.abs()).unwrap_or(Positive::ZERO),
strike: options[1].strike_price,
option_style: options[1].option_style,
side: options[1].side,
}
} else {
DeltaAdjustment::NoAdjustmentNeeded
};
match (&adjustment1, &adjustment2) {
(DeltaAdjustment::NoAdjustmentNeeded, DeltaAdjustment::NoAdjustmentNeeded) => {
}
_ => {
adjustments.push(DeltaAdjustment::SameSize(DeltaAdjustmentSameSize {
first: Box::new(adjustment1),
second: Box::new(adjustment2),
}));
}
}
}
Ok(adjustments)
}
fn apply_delta_adjustments(&mut self, action: Option<Action>) -> Result<(), StrategyError> {
let delta_info = self.delta_neutrality()?;
if delta_info.is_neutral {
return Ok(());
}
for adjustment in self.delta_adjustments()? {
match (action, adjustment) {
(
Some(Action::Buy),
DeltaAdjustment::BuyOptions {
quantity,
strike,
option_style,
side,
},
) => {
self.adjust_option_position(quantity.to_dec(), &strike, &option_style, &side)?;
}
(
Some(Action::Sell),
DeltaAdjustment::SellOptions {
quantity,
strike,
option_style,
side,
},
) => {
self.adjust_option_position(-quantity.to_dec(), &strike, &option_style, &side)?;
}
(None, DeltaAdjustment::SameSize(adjustment)) => {
self.apply_single_adjustment(&adjustment.first)?;
self.apply_single_adjustment(&adjustment.second)?;
}
_ => {
debug!("Skipping adjustment - incompatible with requested action");
}
}
}
Ok(())
}
fn apply_single_adjustment(
&mut self,
adjustment: &DeltaAdjustment,
) -> Result<(), StrategyError> {
match adjustment {
DeltaAdjustment::BuyOptions {
quantity,
strike,
option_style,
side,
} => {
debug!("Applying BuyOptions adjustment");
self.adjust_option_position(quantity.to_dec(), strike, option_style, side)
}
DeltaAdjustment::SellOptions {
quantity,
strike,
option_style,
side,
} => {
debug!("Applying SellOptions adjustment");
self.adjust_option_position(-quantity.to_dec(), strike, option_style, side)
}
DeltaAdjustment::SameSize(_) => {
debug!("Nested SameSize adjustment not supported");
Ok(())
}
_ => {
debug!("Unknown adjustment type");
Ok(())
}
}
}
fn adjust_option_position(
&mut self,
quantity: Decimal,
strike: &Positive,
option_type: &OptionStyle,
side: &Side,
) -> Result<(), StrategyError> {
let mut binding = self.get_position(option_type, side, strike)?;
if let Some(current_position) = binding.first_mut() {
let mut updated_position = (*current_position).clone();
updated_position.option.quantity += quantity;
self.modify_position(&updated_position)?;
} else {
return Err(PositionError::ValidationError(
PositionValidationErrorKind::InvalidPosition {
reason: "Position not found".to_string(),
},
)
.into());
}
Ok(())
}
fn trade_from_delta_adjustment(&mut self, action: Action) -> Result<Vec<Trade>, StrategyError> {
let adjustments = self.delta_adjustments()?;
let mut trades = Vec::new();
let mut process_single_adjustment =
|adj: &DeltaAdjustment| -> Result<Option<Trade>, StrategyError> {
match adj {
DeltaAdjustment::BuyOptions {
quantity,
strike,
option_style,
side,
} => {
if quantity.is_zero() {
return Ok(None);
}
let positions = self.get_position(option_style, side, strike)?;
if let Some(position) = positions.first() {
let mut position_clone = (*position).clone();
position_clone.option.quantity = *quantity;
Ok(Some(position_clone.open()?))
} else {
Ok(None)
}
}
DeltaAdjustment::SellOptions {
quantity,
strike,
option_style,
side,
} => {
if quantity.is_zero() {
return Ok(None);
}
let positions = self.get_position(option_style, side, strike)?;
if let Some(position) = positions.first() {
let mut position_clone = (*position).clone();
position_clone.option.quantity = *quantity;
Ok(Some(position_clone.close()?))
} else {
Ok(None)
}
}
_ => Ok(None),
}
};
for adjustment in adjustments {
match (&action, adjustment) {
(Action::Buy, adj @ DeltaAdjustment::BuyOptions { .. }) => {
if let Some(trade) = process_single_adjustment(&adj)? {
trades.push(trade);
}
}
(Action::Sell, adj @ DeltaAdjustment::SellOptions { .. }) => {
if let Some(trade) = process_single_adjustment(&adj)? {
trades.push(trade);
}
}
(Action::Other, DeltaAdjustment::SameSize(adjustment)) => {
if let Some(trade) = process_single_adjustment(&adjustment.first)? {
trades.push(trade);
}
if let Some(trade) = process_single_adjustment(&adjustment.second)? {
trades.push(trade);
}
}
_ => {}
}
}
Ok(trades)
}
fn portfolio_greeks(&self) -> Result<PortfolioGreeks, GreeksError> {
let positions: Vec<_> = self
.get_positions()
.map_err(|e| GreeksError::StdError(e.to_string()))?
.into_iter()
.cloned()
.collect();
PortfolioGreeks::from_positions(&positions)
.map_err(|e| GreeksError::StdError(e.to_string()))
}
fn optimized_adjustment_plan(
&self,
config: AdjustmentConfig,
target: AdjustmentTarget,
) -> Result<AdjustmentPlan, StrategyError> {
let positions: Vec<_> = self.get_positions()?.into_iter().cloned().collect();
let optimizer = AdjustmentOptimizer::new(&positions, config, target);
optimizer.optimize().map_err(|e| {
StrategyError::OperationError(OperationErrorKind::InvalidParameters {
operation: "optimized_adjustment_plan".to_string(),
reason: e.to_string(),
})
})
}
fn optimized_adjustment_plan_with_chain(
&self,
chain: &crate::chains::chain::OptionChain,
config: AdjustmentConfig,
target: AdjustmentTarget,
) -> Result<AdjustmentPlan, StrategyError> {
let positions: Vec<_> = self.get_positions()?.into_iter().cloned().collect();
let optimizer = AdjustmentOptimizer::with_chain(&positions, chain, config, target);
optimizer.optimize().map_err(|e| {
StrategyError::OperationError(OperationErrorKind::InvalidParameters {
operation: "optimized_adjustment_plan_with_chain".to_string(),
reason: e.to_string(),
})
})
}
fn meets_greek_targets(&self, target: &AdjustmentTarget, tolerance: Decimal) -> bool {
match self.portfolio_greeks() {
Ok(greeks) => target.is_satisfied(&greeks, tolerance),
Err(_) => false,
}
}
fn delta_gap(&self, target_delta: Decimal) -> Result<Decimal, GreeksError> {
let current_delta = self.delta()?;
Ok(target_delta - current_delta)
}
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct DeltaNeutralResponse {
pub delta_info: DeltaInfo,
pub adjustments: Vec<DeltaAdjustment>,
}
#[cfg(test)]
mod tests_display_implementations {
use super::*;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
#[test]
fn test_delta_adjustment_display() {
let buy_options = DeltaAdjustment::BuyOptions {
quantity: pos_or_panic!(3.0),
strike: pos_or_panic!(105.0),
option_style: OptionStyle::Call,
side: Side::Long,
};
assert_eq!(
buy_options.to_string(),
"Buy 3 Long Call options at strike 105"
);
let sell_options = DeltaAdjustment::SellOptions {
quantity: pos_or_panic!(2.5),
strike: pos_or_panic!(95.0),
option_style: OptionStyle::Put,
side: Side::Short,
};
assert_eq!(
sell_options.to_string(),
"Sell 2.5 Short Put options at strike 95"
);
let buy_underlying = DeltaAdjustment::BuyUnderlying(pos_or_panic!(10.0));
assert_eq!(
buy_underlying.to_string(),
"Buy 10 units of the underlying asset"
);
let sell_underlying = DeltaAdjustment::SellUnderlying(pos_or_panic!(5.0));
assert_eq!(
sell_underlying.to_string(),
"Sell 5 units of the underlying asset"
);
let no_adjustment = DeltaAdjustment::NoAdjustmentNeeded;
assert_eq!(no_adjustment.to_string(), "No adjustment needed");
let same_size = DeltaAdjustmentSameSize {
first: Box::new(DeltaAdjustment::BuyOptions {
quantity: Positive::ONE,
strike: Positive::HUNDRED,
option_style: OptionStyle::Call,
side: Side::Long,
}),
second: Box::new(DeltaAdjustment::SellOptions {
quantity: Positive::ONE,
strike: pos_or_panic!(110.0),
option_style: OptionStyle::Call,
side: Side::Short,
}),
};
let same_size = DeltaAdjustment::SameSize(same_size);
assert_eq!(
same_size.to_string(),
"Same size adjustments: [Buy 1 Long Call options at strike 100] and [Sell 1 Short Call options at strike 110]"
);
}
#[test]
fn test_delta_position_info_display() {
let position_info = DeltaPositionInfo {
delta: dec!(0.5),
delta_per_contract: dec!(0.25),
quantity: Positive::TWO,
strike: Positive::HUNDRED,
option_style: OptionStyle::Call,
side: Side::Long,
};
let display_str = position_info.to_string();
assert!(display_str.contains("Delta: 0.5000"));
assert!(display_str.contains("Quantity: 2"));
assert!(display_str.contains("Strike: 100"));
assert!(display_str.contains("OptionStyle::Call"));
assert!(display_str.contains("Side::Long"));
}
#[test]
fn test_delta_info_display() {
let delta_info = DeltaInfo {
net_delta: dec!(-0.25),
individual_deltas: vec![
DeltaPositionInfo {
delta: dec!(0.5),
delta_per_contract: dec!(0.25),
quantity: Positive::ONE,
strike: Positive::HUNDRED,
option_style: OptionStyle::Call,
side: Side::Long,
},
DeltaPositionInfo {
delta: dec!(-0.75),
delta_per_contract: dec!(-0.375),
quantity: Positive::TWO,
strike: pos_or_panic!(95.0),
option_style: OptionStyle::Put,
side: Side::Short,
},
],
is_neutral: false,
neutrality_threshold: dec!(0.1),
underlying_price: pos_or_panic!(102.5),
};
let display_str = delta_info.to_string();
assert!(display_str.contains("Delta Analysis:"));
assert!(display_str.contains("Net Delta: -0.2500"));
assert!(display_str.contains("Is Neutral: false"));
assert!(display_str.contains("Neutrality Threshold: 0.1000"));
assert!(display_str.contains("Underlying Price: 102.5"));
assert!(display_str.contains("Individual Deltas:"));
assert!(display_str.contains("Position 1:"));
assert!(display_str.contains("Delta: 0.5000"));
assert!(display_str.contains("Position 2:"));
assert!(display_str.contains("Delta: -0.7500"));
}
}
#[cfg(test)]
mod tests_serialization {
use super::*;
use crate::ExpirationDate;
use crate::strategies::ShortStrangle;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
use serde_json;
use tracing::info;
#[test]
fn test_delta_adjustment_serialization() {
let buy_options = DeltaAdjustment::BuyOptions {
quantity: pos_or_panic!(3.0),
strike: pos_or_panic!(105.0),
option_style: OptionStyle::Call,
side: Side::Long,
};
let serialized = serde_json::to_string(&buy_options).unwrap();
let deserialized: DeltaAdjustment = serde_json::from_str(&serialized).unwrap();
assert_eq!(buy_options, deserialized);
let sell_options = DeltaAdjustment::SellOptions {
quantity: pos_or_panic!(2.5),
strike: pos_or_panic!(95.0),
option_style: OptionStyle::Put,
side: Side::Short,
};
let serialized = serde_json::to_string(&sell_options).unwrap();
let deserialized: DeltaAdjustment = serde_json::from_str(&serialized).unwrap();
assert_eq!(sell_options, deserialized);
let buy_underlying = DeltaAdjustment::BuyUnderlying(pos_or_panic!(10.0));
let serialized = serde_json::to_string(&buy_underlying).unwrap();
let deserialized: DeltaAdjustment = serde_json::from_str(&serialized).unwrap();
assert_eq!(buy_underlying, deserialized);
let sell_underlying = DeltaAdjustment::SellUnderlying(pos_or_panic!(5.0));
let serialized = serde_json::to_string(&sell_underlying).unwrap();
let deserialized: DeltaAdjustment = serde_json::from_str(&serialized).unwrap();
assert_eq!(sell_underlying, deserialized);
let no_adjustment = DeltaAdjustment::NoAdjustmentNeeded;
let serialized = serde_json::to_string(&no_adjustment).unwrap();
let deserialized: DeltaAdjustment = serde_json::from_str(&serialized).unwrap();
assert_eq!(no_adjustment, deserialized);
let same_size = DeltaAdjustmentSameSize {
first: Box::new(DeltaAdjustment::BuyOptions {
quantity: Positive::ONE,
strike: Positive::HUNDRED,
option_style: OptionStyle::Call,
side: Side::Long,
}),
second: Box::new(DeltaAdjustment::SellOptions {
quantity: Positive::ONE,
strike: pos_or_panic!(110.0),
option_style: OptionStyle::Call,
side: Side::Short,
}),
};
let same_size = DeltaAdjustment::SameSize(same_size);
let serialized = serde_json::to_string(&same_size).unwrap();
let deserialized: DeltaAdjustment = serde_json::from_str(&serialized).unwrap();
assert_eq!(same_size, deserialized);
}
#[test]
fn test_delta_position_info_serialization() {
let position_info = DeltaPositionInfo {
delta: dec!(0.5),
delta_per_contract: dec!(0.25),
quantity: Positive::TWO,
strike: Positive::HUNDRED,
option_style: OptionStyle::Call,
side: Side::Long,
};
let serialized = serde_json::to_string(&position_info).unwrap();
let deserialized: DeltaPositionInfo = serde_json::from_str(&serialized).unwrap();
assert_eq!(position_info.delta, deserialized.delta);
assert_eq!(position_info.quantity, deserialized.quantity);
assert_eq!(position_info.strike, deserialized.strike);
assert_eq!(position_info.option_style, deserialized.option_style);
assert_eq!(position_info.side, deserialized.side);
}
#[test]
fn test_delta_info_serialization() {
let delta_info = DeltaInfo {
net_delta: dec!(-0.25),
individual_deltas: vec![
DeltaPositionInfo {
delta: dec!(0.5),
delta_per_contract: dec!(0.25),
quantity: Positive::ONE,
strike: Positive::HUNDRED,
option_style: OptionStyle::Call,
side: Side::Long,
},
DeltaPositionInfo {
delta: dec!(-0.75),
delta_per_contract: dec!(-0.375),
quantity: Positive::TWO,
strike: pos_or_panic!(95.0),
option_style: OptionStyle::Put,
side: Side::Short,
},
],
is_neutral: false,
neutrality_threshold: dec!(0.1),
underlying_price: pos_or_panic!(102.5),
};
let serialized = serde_json::to_string(&delta_info).unwrap();
let deserialized: DeltaInfo = serde_json::from_str(&serialized).unwrap();
assert_eq!(delta_info.net_delta, deserialized.net_delta);
assert_eq!(delta_info.is_neutral, deserialized.is_neutral);
assert_eq!(
delta_info.neutrality_threshold,
deserialized.neutrality_threshold
);
assert_eq!(delta_info.underlying_price, deserialized.underlying_price);
assert_eq!(
delta_info.individual_deltas.len(),
deserialized.individual_deltas.len()
);
for (original, deserialized_delta) in delta_info
.individual_deltas
.iter()
.zip(deserialized.individual_deltas.iter())
{
assert_eq!(original.delta, deserialized_delta.delta);
assert_eq!(original.quantity, deserialized_delta.quantity);
assert_eq!(original.strike, deserialized_delta.strike);
assert_eq!(original.option_style, deserialized_delta.option_style);
assert_eq!(original.side, deserialized_delta.side);
}
}
#[test]
fn test_specific_json_formats() {
let buy_options = DeltaAdjustment::BuyOptions {
quantity: pos_or_panic!(3.0),
strike: pos_or_panic!(105.0),
option_style: OptionStyle::Call,
side: Side::Long,
};
let serialized = serde_json::to_string(&buy_options).unwrap();
assert!(serialized.contains("\"BuyOptions\""));
assert!(serialized.contains("\"quantity\""));
assert!(serialized.contains("\"strike\""));
assert!(serialized.contains("\"option_style\""));
assert!(serialized.contains("\"side\""));
let json_str =
r#"{"BuyOptions":{"quantity":3.0,"strike":105.0,"option_style":"Call","side":"Long"}}"#;
let deserialized: DeltaAdjustment = serde_json::from_str(json_str).unwrap();
match deserialized {
DeltaAdjustment::BuyOptions {
quantity,
strike,
option_style,
side,
} => {
assert_eq!(quantity, pos_or_panic!(3.0));
assert_eq!(strike, pos_or_panic!(105.0));
assert_eq!(option_style, OptionStyle::Call);
assert_eq!(side, Side::Long);
}
_ => panic!("Deserialized to wrong variant"),
}
}
#[test]
fn test_delta_response_serialization() {
let strategy = ShortStrangle::new(
"CL".to_string(),
pos_or_panic!(7250.0), pos_or_panic!(7450.0), pos_or_panic!(7050.0), ExpirationDate::Days(pos_or_panic!(45.0)),
pos_or_panic!(0.3745), 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), );
let delta_info = strategy.delta_neutrality().unwrap();
let adjustments = strategy.delta_adjustments().unwrap();
let response = DeltaNeutralResponse {
delta_info,
adjustments,
};
let serialized = serde_json::to_string_pretty(&response).unwrap();
info!("{}", serialized);
}
}
#[cfg(test)]
mod tests_generate_delta_adjustments {
use super::*;
use crate::ExpirationDate;
use crate::strategies::base::BreakEvenable;
use crate::strategies::{BasicAble, Validable};
use positive::pos_or_panic;
struct MockDeltaNeutral;
impl Greeks for MockDeltaNeutral {
fn get_options(&self) -> Result<Vec<&Options>, GreeksError> {
Ok(Vec::new())
}
}
impl Positionable for MockDeltaNeutral {}
impl Strategies for MockDeltaNeutral {}
impl Validable for MockDeltaNeutral {}
impl BreakEvenable for MockDeltaNeutral {}
impl BasicAble for MockDeltaNeutral {}
impl DeltaNeutrality for MockDeltaNeutral {}
fn create_test_option(option_style: OptionStyle, side: Side, size: Positive) -> Options {
Options {
option_type: crate::model::types::OptionType::European,
side,
underlying_symbol: "TEST".to_string(),
strike_price: Positive::HUNDRED,
expiration_date: ExpirationDate::Days(pos_or_panic!(30.0)),
implied_volatility: pos_or_panic!(0.2),
quantity: size,
underlying_price: Positive::HUNDRED,
risk_free_rate: dec!(0.05),
option_style,
dividend_yield: pos_or_panic!(0.01),
exotic_params: None,
}
}
#[test]
fn test_generate_delta_adjustments_positive_net_delta_positive_option_delta() {
let delta_neutral = MockDeltaNeutral;
let net_delta = dec!(0.5); let option_delta_per_contract = dec!(0.25); let option = create_test_option(OptionStyle::Call, Side::Long, pos_or_panic!(2.7));
let adjustment = delta_neutral
.generate_delta_adjustments(net_delta, option_delta_per_contract, &option)
.unwrap();
match adjustment {
DeltaAdjustment::SellOptions {
quantity,
strike,
option_style,
side,
} => {
assert_eq!(quantity, Positive::new(2.0).unwrap()); assert_eq!(strike, option.strike_price);
assert_eq!(option_style, option.option_style);
assert_eq!(side, option.side);
}
_ => panic!("Expected SellOptions adjustment"),
}
}
#[test]
fn test_generate_delta_adjustments_positive_net_delta_negative_option_delta() {
let delta_neutral = MockDeltaNeutral;
let net_delta = dec!(0.5); let option_delta_per_contract = dec!(-0.25); let option = create_test_option(OptionStyle::Put, Side::Long, pos_or_panic!(3.7));
let adjustment = delta_neutral
.generate_delta_adjustments(net_delta, option_delta_per_contract, &option)
.unwrap();
match adjustment {
DeltaAdjustment::BuyOptions {
quantity,
strike,
option_style,
side,
} => {
assert_eq!(quantity, Positive::new(2.0).unwrap()); assert_eq!(strike, option.strike_price);
assert_eq!(option_style, option.option_style);
assert_eq!(side, option.side);
}
_ => panic!("Expected BuyOptions adjustment"),
}
}
#[test]
fn test_generate_delta_adjustments_positive_net_delta_negative_option_delta_bis() {
let delta_neutral = MockDeltaNeutral;
let net_delta = dec!(0.5); let option_delta_per_contract = dec!(-0.25); let option = create_test_option(OptionStyle::Put, Side::Long, pos_or_panic!(1.7));
let adjustment = delta_neutral
.generate_delta_adjustments(net_delta, option_delta_per_contract, &option)
.unwrap();
match adjustment {
DeltaAdjustment::BuyOptions {
quantity,
strike,
option_style,
side,
} => {
assert_eq!(quantity, Positive::new(2.0).unwrap());
assert_eq!(strike, option.strike_price);
assert_eq!(option_style, option.option_style);
assert_eq!(side, option.side);
}
_ => panic!("Expected BuyOptions adjustment"),
}
}
#[test]
fn test_generate_delta_adjustments_negative_net_delta_positive_option_delta() {
let delta_neutral = MockDeltaNeutral;
let net_delta = dec!(-0.5); let option_delta_per_contract = dec!(0.25); let option = create_test_option(OptionStyle::Call, Side::Long, pos_or_panic!(21.7));
let adjustment = delta_neutral
.generate_delta_adjustments(net_delta, option_delta_per_contract, &option)
.unwrap();
match adjustment {
DeltaAdjustment::BuyOptions {
quantity,
strike,
option_style,
side,
} => {
assert_eq!(quantity, Positive::new(2.0).unwrap()); assert_eq!(strike, option.strike_price);
assert_eq!(option_style, option.option_style);
assert_eq!(side, option.side);
}
_ => panic!("Expected BuyOptions adjustment"),
}
}
#[test]
fn test_generate_delta_adjustments_negative_net_delta_positive_option_delta_bis() {
let delta_neutral = MockDeltaNeutral;
let net_delta = dec!(-0.5); let option_delta_per_contract = dec!(0.25); let option = create_test_option(OptionStyle::Call, Side::Long, pos_or_panic!(1.7));
let adjustment = delta_neutral
.generate_delta_adjustments(net_delta, option_delta_per_contract, &option)
.unwrap();
match adjustment {
DeltaAdjustment::BuyOptions {
quantity,
strike,
option_style,
side,
} => {
assert_eq!(quantity, Positive::new(2.0).unwrap()); assert_eq!(strike, option.strike_price);
assert_eq!(option_style, option.option_style);
assert_eq!(side, option.side);
}
_ => panic!("Expected BuyOptions adjustment"),
}
}
#[test]
fn test_generate_delta_adjustments_negative_net_delta_negative_option_delta_error() {
let delta_neutral = MockDeltaNeutral;
let net_delta = dec!(-0.5); let option_delta_per_contract = dec!(-0.25); let option = create_test_option(OptionStyle::Put, Side::Long, pos_or_panic!(1.5));
let adjustment =
delta_neutral.generate_delta_adjustments(net_delta, option_delta_per_contract, &option);
assert!(adjustment.is_err());
}
#[test]
fn test_generate_delta_adjustments_negative_net_delta_negative_option_delta() {
let delta_neutral = MockDeltaNeutral;
let net_delta = dec!(-0.5); let option_delta_per_contract = dec!(-0.25); let option = create_test_option(OptionStyle::Put, Side::Long, pos_or_panic!(10.5));
let adjustment = delta_neutral
.generate_delta_adjustments(net_delta, option_delta_per_contract, &option)
.unwrap();
match adjustment {
DeltaAdjustment::SellOptions {
quantity,
strike,
option_style,
side,
} => {
assert_eq!(quantity, Positive::TWO);
assert_eq!(strike, option.strike_price);
assert_eq!(option_style, option.option_style);
assert_eq!(side, option.side);
}
_ => panic!("Expected SellOptions adjustment"),
}
}
#[test]
fn test_generate_delta_adjustments_zero_option_delta() {
let delta_neutral = MockDeltaNeutral;
let net_delta = dec!(0.5); let option_delta_per_contract = dec!(0.0); let option = create_test_option(OptionStyle::Call, Side::Long, pos_or_panic!(7.0));
let result =
delta_neutral.generate_delta_adjustments(net_delta, option_delta_per_contract, &option);
assert!(result.is_err());
}
#[test]
fn test_generate_delta_adjustments_zero_net_delta() {
let delta_neutral = MockDeltaNeutral;
let net_delta = dec!(0.0); let option_delta_per_contract = dec!(0.25); let option = create_test_option(OptionStyle::Call, Side::Long, pos_or_panic!(2.7223423));
let adjustment = delta_neutral
.generate_delta_adjustments(net_delta, option_delta_per_contract, &option)
.unwrap();
match adjustment {
DeltaAdjustment::NoAdjustmentNeeded => {
}
_ => panic!("Expected Buy/SellOptions adjustment with zero quantity"),
}
}
#[test]
fn test_generate_delta_adjustments_with_different_option_styles() {
let delta_neutral = MockDeltaNeutral;
let net_delta = dec!(0.5);
let option_delta_per_contract = dec!(0.25);
let put_option =
create_test_option(OptionStyle::Put, Side::Long, pos_or_panic!(5.7534523452435));
let adjustment = delta_neutral
.generate_delta_adjustments(net_delta, option_delta_per_contract, &put_option)
.unwrap();
match adjustment {
DeltaAdjustment::SellOptions {
option_style,
quantity,
..
} => {
assert_eq!(option_style, OptionStyle::Put);
let expected = Positive::TWO;
assert_eq!(quantity, expected);
}
_ => panic!("Expected SellOptions adjustment"),
}
}
#[test]
fn test_generate_delta_adjustments_with_different_sides() {
let delta_neutral = MockDeltaNeutral;
let net_delta = dec!(0.5);
let option_delta_per_contract = dec!(0.25);
let short_option = create_test_option(OptionStyle::Call, Side::Short, Positive::ONE);
let adjustment = delta_neutral.generate_delta_adjustments(
net_delta,
option_delta_per_contract,
&short_option,
);
assert!(adjustment.is_err());
}
#[test]
fn test_generate_delta_adjustments_negative_net_delta_negative_option_delta_with_excess() {
let delta_neutral = MockDeltaNeutral;
let net_delta = dec!(-0.5); let option_delta_per_contract = dec!(-0.25); let option = create_test_option(OptionStyle::Put, Side::Long, pos_or_panic!(3.0));
let adjustment = delta_neutral
.generate_delta_adjustments(net_delta, option_delta_per_contract, &option)
.unwrap();
match adjustment {
DeltaAdjustment::SellOptions {
quantity,
strike,
option_style,
side,
} => {
assert_eq!(quantity, Positive::new(2.0).unwrap());
assert_eq!(strike, option.strike_price);
assert_eq!(option_style, option.option_style);
assert_eq!(side, option.side);
}
_ => panic!("Expected SellOptions adjustment"),
}
}
#[test]
fn test_generate_delta_adjustments_large_values() {
let delta_neutral = MockDeltaNeutral;
let net_delta = dec!(1000.0);
let option_delta_per_contract = dec!(0.01);
let option = create_test_option(OptionStyle::Call, Side::Long, pos_or_panic!(1.7));
let adjustment =
delta_neutral.generate_delta_adjustments(net_delta, option_delta_per_contract, &option);
assert!(adjustment.is_err());
}
}