use crate::error::PricingError;
use crate::model::position::Position;
use positive::Positive;
use rust_decimal::Decimal;
#[derive(Debug, Clone)]
pub struct SPANMargin {
short_option_minimum: Decimal,
price_scan_range: Decimal,
volatility_scan_range: Decimal,
}
#[allow(dead_code)]
impl SPANMargin {
#[inline]
#[must_use]
pub fn new(
short_option_minimum: Decimal,
price_scan_range: Decimal,
volatility_scan_range: Decimal,
) -> Self {
SPANMargin {
short_option_minimum,
price_scan_range,
volatility_scan_range,
}
}
pub fn calculate_margin(&self, position: &Position) -> Result<Decimal, PricingError> {
let risk_array = self.calculate_risk_array(position)?;
let short_option_minimum = self.calculate_short_option_minimum(position);
let max_loss = risk_array
.into_iter()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or_else(|| {
tracing::warn!(
"calculate_margin: empty risk_array for position {}; using ZERO",
position.option.underlying_symbol
);
Decimal::ZERO
});
Ok(max_loss.max(short_option_minimum))
}
fn calculate_risk_array(&self, position: &Position) -> Result<Vec<Decimal>, PricingError> {
let mut risk_array = Vec::new();
let option = &position.option;
let price_scenarios = self.generate_price_scenarios(option.underlying_price);
let volatility_scenarios = self.generate_volatility_scenarios(option.implied_volatility);
for &price in &price_scenarios {
for &volatility in &volatility_scenarios {
let scenario_loss = self.calculate_scenario_loss(position, price, volatility)?;
risk_array.push(scenario_loss);
}
}
Ok(risk_array)
}
#[inline]
fn generate_price_scenarios(&self, underlying_price: Positive) -> Vec<Positive> {
vec![
underlying_price * (Decimal::ONE - self.price_scan_range),
underlying_price,
underlying_price * (Decimal::ONE + self.price_scan_range),
]
}
#[inline]
fn generate_volatility_scenarios(&self, implied_volatility: Positive) -> Vec<Positive> {
vec![
implied_volatility * (Decimal::ONE - self.volatility_scan_range),
implied_volatility,
implied_volatility * (Decimal::ONE + self.volatility_scan_range),
]
}
fn calculate_scenario_loss(
&self,
position: &Position,
scenario_price: Positive,
scenario_volatility: Positive,
) -> Result<Decimal, PricingError> {
let option = &position.option;
let current_price = option.calculate_price_black_scholes()?;
let mut scenario_option = option.clone();
scenario_option.underlying_price = scenario_price;
scenario_option.implied_volatility = scenario_volatility;
let scenario_price = scenario_option.calculate_price_black_scholes()?;
Ok((scenario_price - current_price)
* option.quantity
* if option.is_short() {
Decimal::NEGATIVE_ONE
} else {
Decimal::ONE
})
}
#[inline]
fn calculate_short_option_minimum(&self, position: &Position) -> Decimal {
let option = &position.option;
if option.is_short() {
self.short_option_minimum * option.underlying_price * option.quantity
} else {
Decimal::ZERO
}
}
}
#[cfg(test)]
mod tests_span {
use super::*;
use crate::model::types::{OptionStyle, Side};
use crate::model::utils::create_sample_option;
use chrono::Utc;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
use tracing::info;
#[test]
fn test_span_margin() -> Result<(), crate::error::Error> {
let option = create_sample_option(
OptionStyle::Call,
Side::Short,
pos_or_panic!(155.0),
Positive::ONE,
pos_or_panic!(150.0),
pos_or_panic!(0.2),
);
let position = Position {
option,
premium: pos_or_panic!(5.0),
date: Utc::now(),
open_fee: pos_or_panic!(0.5),
close_fee: pos_or_panic!(0.5),
epic: Some("Epic123".to_string()),
extra_fields: None,
};
let span = SPANMargin::new(
dec!(0.1), dec!(0.05), dec!(0.1), );
let margin = span.calculate_margin(&position)?;
assert!(margin > Decimal::ZERO, "Margin should be positive");
info!("Calculated margin: {}", margin);
Ok(())
}
}