use crate::error::GreeksError;
use crate::model::types::{OptionStyle, Side};
use crate::model::{ExpirationDate, Options};
use positive::Positive;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::fmt;
use utoipa::ToSchema;
use super::portfolio::PortfolioGreeks;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub enum AdjustmentAction {
ModifyQuantity {
leg_index: usize,
new_quantity: Positive,
},
AddLeg {
option: Box<Options>,
side: Side,
quantity: Positive,
},
CloseLeg {
leg_index: usize,
},
RollStrike {
leg_index: usize,
new_strike: Positive,
quantity: Positive,
},
RollExpiration {
leg_index: usize,
new_expiration: ExpirationDate,
quantity: Positive,
},
AddUnderlying {
quantity: Decimal,
},
}
impl fmt::Display for AdjustmentAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AdjustmentAction::ModifyQuantity {
leg_index,
new_quantity,
} => {
write!(f, "Modify leg {} to quantity {}", leg_index, new_quantity)
}
AdjustmentAction::AddLeg {
option,
side,
quantity,
} => {
write!(
f,
"Add {} {} {} at strike {} (qty: {})",
side, option.option_style, option.option_type, option.strike_price, quantity
)
}
AdjustmentAction::CloseLeg { leg_index } => {
write!(f, "Close leg {}", leg_index)
}
AdjustmentAction::RollStrike {
leg_index,
new_strike,
quantity,
} => {
write!(
f,
"Roll leg {} to strike {} (qty: {})",
leg_index, new_strike, quantity
)
}
AdjustmentAction::RollExpiration {
leg_index,
new_expiration,
quantity,
} => {
write!(
f,
"Roll leg {} to expiration {} (qty: {})",
leg_index, new_expiration, quantity
)
}
AdjustmentAction::AddUnderlying { quantity } => {
if quantity.is_sign_positive() {
write!(f, "Buy {} shares of underlying", quantity)
} else {
write!(f, "Sell {} shares of underlying", quantity.abs())
}
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct AdjustmentConfig {
pub allow_new_legs: bool,
pub allow_underlying: bool,
pub max_new_legs: Option<usize>,
pub allowed_styles: Vec<OptionStyle>,
pub strike_range: Option<(Positive, Positive)>,
pub max_cost: Option<Positive>,
pub min_liquidity: Option<u64>,
pub delta_tolerance: Decimal,
pub prefer_existing_legs: bool,
}
impl Default for AdjustmentConfig {
fn default() -> Self {
Self {
allow_new_legs: true,
allow_underlying: false,
max_new_legs: Some(2),
allowed_styles: vec![OptionStyle::Call, OptionStyle::Put],
strike_range: None,
max_cost: None,
min_liquidity: None,
delta_tolerance: dec!(0.01),
prefer_existing_legs: true,
}
}
}
impl AdjustmentConfig {
#[inline]
#[must_use]
pub fn existing_legs_only() -> Self {
Self {
allow_new_legs: false,
allow_underlying: false,
..Default::default()
}
}
#[inline]
#[must_use]
pub fn with_underlying() -> Self {
Self {
allow_underlying: true,
..Default::default()
}
}
#[inline]
#[must_use]
pub fn aggressive() -> Self {
Self {
allow_new_legs: true,
allow_underlying: true,
max_new_legs: Some(4),
prefer_existing_legs: false,
..Default::default()
}
}
#[inline]
#[must_use]
pub fn with_max_cost(mut self, max_cost: Positive) -> Self {
self.max_cost = Some(max_cost);
self
}
#[inline]
#[must_use]
pub fn with_delta_tolerance(mut self, tolerance: Decimal) -> Self {
self.delta_tolerance = tolerance;
self
}
#[inline]
#[must_use]
pub fn with_strike_range(mut self, min: Positive, max: Positive) -> Self {
self.strike_range = Some((min, max));
self
}
#[inline]
#[must_use]
pub fn with_allow_new_legs(mut self, allow: bool) -> Self {
self.allow_new_legs = allow;
self
}
#[inline]
#[must_use]
pub fn with_allow_underlying(mut self, allow: bool) -> Self {
self.allow_underlying = allow;
self
}
#[inline]
#[must_use]
pub fn with_max_new_legs(mut self, max: usize) -> Self {
self.max_new_legs = Some(max);
self
}
#[inline]
#[must_use]
pub fn with_min_liquidity(mut self, min: u64) -> Self {
self.min_liquidity = Some(min);
self
}
#[inline]
#[must_use]
pub fn with_prefer_existing_legs(mut self, prefer: bool) -> Self {
self.prefer_existing_legs = prefer;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct AdjustmentPlan {
pub actions: Vec<AdjustmentAction>,
pub estimated_cost: Decimal,
pub resulting_greeks: PortfolioGreeks,
pub residual_delta: Decimal,
pub quality_score: Decimal,
}
impl AdjustmentPlan {
#[inline]
#[must_use]
pub fn new(
actions: Vec<AdjustmentAction>,
estimated_cost: Decimal,
resulting_greeks: PortfolioGreeks,
residual_delta: Decimal,
) -> Self {
let quality_score = residual_delta.abs() + estimated_cost * dec!(0.01);
Self {
actions,
estimated_cost,
resulting_greeks,
residual_delta,
quality_score,
}
}
#[inline]
#[must_use]
pub fn is_delta_neutral(&self, tolerance: Decimal) -> bool {
self.residual_delta.abs() <= tolerance
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.actions.is_empty()
}
#[inline]
#[must_use]
pub fn action_count(&self) -> usize {
self.actions.len()
}
}
impl fmt::Display for AdjustmentPlan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Adjustment Plan:")?;
writeln!(f, " Actions: {}", self.actions.len())?;
for (i, action) in self.actions.iter().enumerate() {
writeln!(f, " {}: {}", i + 1, action)?;
}
writeln!(f, " Estimated Cost: {:.2}", self.estimated_cost)?;
writeln!(f, " Residual Delta: {:.4}", self.residual_delta)?;
writeln!(f, " Quality Score: {:.4}", self.quality_score)?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AdjustmentError {
NoViablePlan,
CostExceeded,
NoPositions,
InvalidLegIndex(usize),
GreeksError(String),
ConfigurationViolation(String),
}
impl fmt::Display for AdjustmentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AdjustmentError::NoViablePlan => write!(f, "No viable adjustment plan found"),
AdjustmentError::CostExceeded => write!(f, "Adjustment cost exceeds maximum"),
AdjustmentError::NoPositions => write!(f, "No positions to adjust"),
AdjustmentError::InvalidLegIndex(idx) => write!(f, "Invalid leg index: {}", idx),
AdjustmentError::GreeksError(msg) => write!(f, "Greeks calculation error: {}", msg),
AdjustmentError::ConfigurationViolation(msg) => {
write!(f, "Configuration violation: {}", msg)
}
}
}
}
impl std::error::Error for AdjustmentError {}
impl From<GreeksError> for AdjustmentError {
fn from(err: GreeksError) -> Self {
AdjustmentError::GreeksError(err.to_string())
}
}
#[cfg(test)]
mod tests_adjustment {
use super::*;
use positive::pos_or_panic;
#[test]
fn test_adjustment_config_default() {
let config = AdjustmentConfig::default();
assert!(config.allow_new_legs);
assert!(!config.allow_underlying);
assert_eq!(config.max_new_legs, Some(2));
assert!(config.prefer_existing_legs);
}
#[test]
fn test_adjustment_config_existing_legs_only() {
let config = AdjustmentConfig::existing_legs_only();
assert!(!config.allow_new_legs);
assert!(!config.allow_underlying);
}
#[test]
fn test_adjustment_config_with_underlying() {
let config = AdjustmentConfig::with_underlying();
assert!(config.allow_underlying);
}
#[test]
fn test_adjustment_config_aggressive() {
let config = AdjustmentConfig::aggressive();
assert!(config.allow_new_legs);
assert!(config.allow_underlying);
assert_eq!(config.max_new_legs, Some(4));
assert!(!config.prefer_existing_legs);
}
#[test]
fn test_adjustment_action_display() {
let modify = AdjustmentAction::ModifyQuantity {
leg_index: 0,
new_quantity: pos_or_panic!(5.0),
};
assert!(modify.to_string().contains("Modify leg 0"));
let close = AdjustmentAction::CloseLeg { leg_index: 1 };
assert!(close.to_string().contains("Close leg 1"));
let underlying = AdjustmentAction::AddUnderlying {
quantity: dec!(100.0),
};
assert!(underlying.to_string().contains("Buy 100"));
let short_underlying = AdjustmentAction::AddUnderlying {
quantity: dec!(-50.0),
};
assert!(short_underlying.to_string().contains("Sell 50"));
}
#[test]
fn test_adjustment_plan_is_delta_neutral() {
let plan = AdjustmentPlan::new(
vec![],
Decimal::ZERO,
PortfolioGreeks::default(),
dec!(0.005),
);
assert!(plan.is_delta_neutral(dec!(0.01)));
assert!(!plan.is_delta_neutral(dec!(0.001)));
}
#[test]
fn test_adjustment_error_display() {
let err = AdjustmentError::NoViablePlan;
assert!(err.to_string().contains("No viable"));
let err = AdjustmentError::InvalidLegIndex(5);
assert!(err.to_string().contains("5"));
}
}