use crate::error::strategies::{BreakEvenErrorKind, ProfitLossErrorKind};
use crate::error::{GreeksError, OperationErrorKind, StrategyError};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ProbabilityError {
#[error("Probability calculation error: {0}")]
CalculationError(ProbabilityCalculationErrorKind),
#[error("Profit/loss range error: {0}")]
RangeError(ProfitLossRangeErrorKind),
#[error("Expiration error: {0}")]
ExpirationError(ExpirationErrorKind),
#[error("Price error: {0}")]
PriceError(PriceErrorKind),
#[error("No positions available: {0}")]
NoPositions(String),
#[error("missing metric `{metric}` for probability analysis")]
MissingMetric {
metric: &'static str,
},
#[error(transparent)]
PositiveError(#[from] positive::PositiveError),
}
#[derive(Error, Debug)]
pub enum ProbabilityCalculationErrorKind {
#[error("Invalid probability value {value}: {reason}")]
InvalidProbability {
value: f64,
reason: String,
},
#[error("Expected value calculation error: {reason}")]
ExpectedValueError {
reason: String,
},
#[error("Volatility adjustment error: {reason}")]
VolatilityAdjustmentError {
reason: String,
},
#[error("Price trend error: {reason}")]
TrendError {
reason: String,
},
}
#[derive(Error, Debug)]
pub enum ProfitLossRangeErrorKind {
#[error("Invalid profit range '{range}': {reason}")]
InvalidProfitRange {
range: String,
reason: String,
},
#[error("Invalid loss range '{range}': {reason}")]
InvalidLossRange {
range: String,
reason: String,
},
#[error("Invalid break-even points: {reason}")]
InvalidBreakEvenPoints {
reason: String,
},
}
#[derive(Error, Debug)]
pub enum ExpirationErrorKind {
#[error("Invalid expiration: {reason}")]
InvalidExpiration {
reason: String,
},
#[error("Invalid risk-free rate {rate:?}: {reason}")]
InvalidRiskFreeRate {
rate: Option<f64>,
reason: String,
},
}
#[derive(Error, Debug)]
pub enum PriceErrorKind {
#[error("Invalid underlying price {price}: {reason}")]
InvalidUnderlyingPrice {
price: f64,
reason: String,
},
#[error("Invalid price range '{range}': {reason}")]
InvalidPriceRange {
range: String,
reason: String,
},
}
impl From<GreeksError> for ProbabilityError {
#[inline]
fn from(error: GreeksError) -> Self {
ProbabilityError::CalculationError(ProbabilityCalculationErrorKind::ExpectedValueError {
reason: error.to_string(),
})
}
}
impl From<crate::error::PricingError> for ProbabilityError {
#[inline]
fn from(error: crate::error::PricingError) -> Self {
ProbabilityError::CalculationError(ProbabilityCalculationErrorKind::ExpectedValueError {
reason: error.to_string(),
})
}
}
impl From<crate::error::DecimalError> for ProbabilityError {
#[inline]
fn from(error: crate::error::DecimalError) -> Self {
ProbabilityError::CalculationError(ProbabilityCalculationErrorKind::ExpectedValueError {
reason: error.to_string(),
})
}
}
pub type ProbabilityResult<T> = Result<T, ProbabilityError>;
impl From<StrategyError> for ProbabilityError {
fn from(error: StrategyError) -> Self {
let reason = |r: String| {
ProbabilityError::CalculationError(
ProbabilityCalculationErrorKind::ExpectedValueError { reason: r },
)
};
match error {
StrategyError::ProfitLossError(kind) => match kind {
ProfitLossErrorKind::MaxProfitError { reason: r }
| ProfitLossErrorKind::MaxLossError { reason: r }
| ProfitLossErrorKind::ProfitRangeError { reason: r } => reason(r),
},
StrategyError::PriceError(kind) => match kind {
crate::error::strategies::PriceErrorKind::InvalidUnderlyingPrice { reason: r }
| crate::error::strategies::PriceErrorKind::InvalidPriceRange {
start: _,
end: _,
reason: r,
} => reason(r),
},
StrategyError::BreakEvenError(kind) => match kind {
BreakEvenErrorKind::CalculationError { reason: r } => reason(r),
BreakEvenErrorKind::NoBreakEvenPoints => {
reason("No break-even points found".to_string())
}
},
StrategyError::OperationError(kind) => match kind {
OperationErrorKind::NotSupported {
operation,
reason: strategy_type,
} => reason(format!(
"Operation '{operation}' not supported for strategy '{strategy_type}'"
)),
OperationErrorKind::InvalidParameters {
operation,
reason: r,
} => reason(format!(
"Invalid parameters for operation '{operation}': {r}"
)),
},
StrategyError::NotImplemented => reason("Strategy not implemented".to_string()),
StrategyError::GreeksError(err) => reason(err.to_string()),
StrategyError::PositiveError(err) => reason(err.to_string()),
StrategyError::Simulation(err) => reason(err.to_string()),
StrategyError::NumericConversion { value } => reason(format!(
"numeric conversion failed: {value} is not a finite Decimal"
)),
StrategyError::MissingGreek { name } => reason(format!("missing greek `{name}`")),
StrategyError::EmptyCollection { context } => {
reason(format!("empty collection: {context}"))
}
}
}
}
impl From<expiration_date::error::ExpirationDateError> for ProbabilityError {
#[inline]
fn from(err: expiration_date::error::ExpirationDateError) -> Self {
ProbabilityError::CalculationError(ProbabilityCalculationErrorKind::ExpectedValueError {
reason: err.to_string(),
})
}
}
impl From<OperationErrorKind> for ProbabilityError {
fn from(error: OperationErrorKind) -> Self {
match error {
OperationErrorKind::InvalidParameters { operation, reason } => {
ProbabilityError::CalculationError(
ProbabilityCalculationErrorKind::ExpectedValueError {
reason: format!("Invalid parameters for operation '{operation}': {reason}"),
},
)
}
OperationErrorKind::NotSupported { operation, reason } => {
ProbabilityError::CalculationError(
ProbabilityCalculationErrorKind::ExpectedValueError {
reason: format!("Operation '{operation}' not supported: {reason}"),
},
)
}
}
}
}
impl ProbabilityError {
#[must_use]
#[cold]
#[inline(never)]
pub fn invalid_probability(value: f64, reason: &str) -> Self {
ProbabilityError::CalculationError(ProbabilityCalculationErrorKind::InvalidProbability {
value,
reason: reason.to_string(),
})
}
#[must_use]
#[cold]
#[inline(never)]
pub fn invalid_profit_range(range: &str, reason: &str) -> Self {
ProbabilityError::RangeError(ProfitLossRangeErrorKind::InvalidProfitRange {
range: range.to_string(),
reason: reason.to_string(),
})
}
#[must_use]
#[cold]
#[inline(never)]
pub fn invalid_expiration(reason: &str) -> Self {
ProbabilityError::ExpirationError(ExpirationErrorKind::InvalidExpiration {
reason: reason.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_invalid_probability_error() {
let error = ProbabilityError::invalid_probability(1.2, "Probability cannot exceed 1.0");
assert!(matches!(
error,
ProbabilityError::CalculationError(
ProbabilityCalculationErrorKind::InvalidProbability { .. }
)
));
}
#[test]
fn test_missing_metric_variant() {
let error = ProbabilityError::MissingMetric {
metric: "max_profit",
};
assert_eq!(
error.to_string(),
"missing metric `max_profit` for probability analysis"
);
}
#[test]
fn test_error_formatting() {
let error = ProbabilityError::invalid_probability(1.2, "Probability cannot exceed 1.0");
let error_string = error.to_string();
assert!(error_string.contains("Invalid probability"));
assert!(error_string.contains("1.2"));
assert!(error_string.contains("Probability cannot exceed 1.0"));
}
#[test]
fn test_expiration_error_display() {
let error = ProbabilityError::ExpirationError(ExpirationErrorKind::InvalidExpiration {
reason: "Cannot be in the past".to_string(),
});
assert!(error.to_string().contains("Cannot be in the past"));
}
#[test]
fn test_price_error_display() {
let error = ProbabilityError::PriceError(PriceErrorKind::InvalidUnderlyingPrice {
price: -10.0,
reason: "Price cannot be negative".to_string(),
});
assert!(error.to_string().contains("Price cannot be negative"));
assert!(error.to_string().contains("-10"));
}
}
#[cfg(test)]
mod tests_extended {
use super::*;
use crate::error::strategies;
#[test]
fn test_invalid_probability_error() {
let error = ProbabilityError::invalid_probability(1.2, "Probability cannot exceed 1.0");
assert!(matches!(
error,
ProbabilityError::CalculationError(
ProbabilityCalculationErrorKind::InvalidProbability { .. }
)
));
}
#[test]
fn test_error_formatting() {
let error = ProbabilityError::invalid_probability(1.2, "Probability cannot exceed 1.0");
let error_string = error.to_string();
assert!(error_string.contains("Invalid probability"));
assert!(error_string.contains("1.2"));
assert!(error_string.contains("Probability cannot exceed 1.0"));
}
#[test]
fn test_profit_loss_range_error_display() {
let error = ProbabilityError::RangeError(ProfitLossRangeErrorKind::InvalidProfitRange {
range: "100-200".to_string(),
reason: "Invalid range".to_string(),
});
assert!(error.to_string().contains("100-200"));
assert!(error.to_string().contains("Invalid range"));
let error =
ProbabilityError::RangeError(ProfitLossRangeErrorKind::InvalidBreakEvenPoints {
reason: "No break-even points found".to_string(),
});
assert!(error.to_string().contains("No break-even points found"));
}
#[test]
fn test_calculation_error_display() {
let error = ProbabilityError::CalculationError(
ProbabilityCalculationErrorKind::VolatilityAdjustmentError {
reason: "Invalid volatility adjustment".to_string(),
},
);
assert!(error.to_string().contains("Invalid volatility adjustment"));
let error =
ProbabilityError::CalculationError(ProbabilityCalculationErrorKind::TrendError {
reason: "Invalid trend".to_string(),
});
assert!(error.to_string().contains("Invalid trend"));
}
#[test]
fn test_expiration_error() {
let error = ProbabilityError::ExpirationError(ExpirationErrorKind::InvalidRiskFreeRate {
rate: Some(0.05),
reason: "Rate out of bounds".to_string(),
});
assert!(error.to_string().contains("0.05"));
assert!(error.to_string().contains("Rate out of bounds"));
}
#[test]
fn test_strategy_error_conversion() {
let strategy_error = StrategyError::ProfitLossError(ProfitLossErrorKind::MaxProfitError {
reason: "Invalid max profit".to_string(),
});
let prob_error: ProbabilityError = strategy_error.into();
assert!(matches!(
prob_error,
ProbabilityError::CalculationError(
ProbabilityCalculationErrorKind::ExpectedValueError { .. }
)
));
}
#[test]
fn test_strategy_break_even_error_conversion() {
let strategy_error = StrategyError::BreakEvenError(BreakEvenErrorKind::NoBreakEvenPoints);
let prob_error: ProbabilityError = strategy_error.into();
assert!(matches!(
prob_error,
ProbabilityError::CalculationError(
ProbabilityCalculationErrorKind::ExpectedValueError { .. }
)
));
}
#[test]
fn test_strategy_operation_error_conversion() {
let strategy_error = StrategyError::OperationError(OperationErrorKind::NotSupported {
operation: "test".to_string(),
reason: "TestStrategy".to_string(),
});
let prob_error: ProbabilityError = strategy_error.into();
assert!(matches!(
prob_error,
ProbabilityError::CalculationError(
ProbabilityCalculationErrorKind::ExpectedValueError { .. }
)
));
}
#[test]
fn test_greeks_error_conversion() {
let greeks_error = GreeksError::invalid_volatility(-0.5, "negative");
let prob_error: ProbabilityError = greeks_error.into();
assert!(matches!(
prob_error,
ProbabilityError::CalculationError(
ProbabilityCalculationErrorKind::ExpectedValueError { .. }
)
));
}
#[test]
fn test_no_positions_error() {
let error = ProbabilityError::NoPositions("No positions found".to_string());
assert!(error.to_string().contains("No positions found"));
}
#[test]
fn test_probability_result_type() {
let success: ProbabilityResult<f64> = Ok(0.5);
let failure: ProbabilityResult<f64> =
Err(ProbabilityError::invalid_probability(1.5, "Value too high"));
assert!(success.is_ok());
assert!(failure.is_err());
}
#[test]
fn test_probability_error_missing_metric() {
let error = ProbabilityError::MissingMetric {
metric: "max_profit",
};
assert_eq!(
format!("{error}"),
"missing metric `max_profit` for probability analysis"
);
}
#[test]
fn test_price_error_invalid_price_range() {
let error = PriceErrorKind::InvalidPriceRange {
range: "0-100".to_string(),
reason: "Negative values are not allowed".to_string(),
};
assert_eq!(
format!("{error}"),
"Invalid price range '0-100': Negative values are not allowed"
);
}
#[test]
fn test_profit_loss_range_error_invalid_loss_range() {
let error = ProfitLossRangeErrorKind::InvalidLossRange {
range: "-50-0".to_string(),
reason: "Range must be positive".to_string(),
};
assert_eq!(
format!("{error}"),
"Invalid loss range '-50-0': Range must be positive"
);
}
#[test]
fn test_profit_loss_error_max_loss_error() {
let error = ProfitLossErrorKind::MaxLossError {
reason: "Maximum loss exceeded".to_string(),
};
assert_eq!(
format!("{error}"),
"Maximum loss calculation error: Maximum loss exceeded"
);
}
#[test]
fn test_strategy_error_price_error_invalid_price_range() {
let error = StrategyError::PriceError(strategies::PriceErrorKind::InvalidPriceRange {
start: 0.0,
end: 100.0,
reason: "Out of bounds".to_string(),
});
assert!(matches!(error, StrategyError::PriceError(_)));
}
#[test]
fn test_break_even_error_calculation_error() {
let error = StrategyError::BreakEvenError(BreakEvenErrorKind::CalculationError {
reason: "Failed to calculate break-even point".to_string(),
});
let converted_error: ProbabilityError = error.into();
assert_eq!(
format!("{converted_error}"),
"Probability calculation error: Expected value calculation error: Failed to calculate break-even point"
);
}
#[test]
fn test_operation_error_invalid_parameters() {
let error = OperationErrorKind::InvalidParameters {
operation: "Calculate P/L".to_string(),
reason: "Invalid input values".to_string(),
};
let converted_error: ProbabilityError = error.into();
assert_eq!(
format!("{converted_error}"),
"Probability calculation error: Expected value calculation error: Invalid parameters for operation 'Calculate P/L': Invalid input values"
);
}
#[test]
fn test_strategy_error_simulation_conversion() {
let strategy_error = StrategyError::Simulation(Box::new(
crate::error::SimulationError::walk_error("simulation failed"),
));
let converted_error: ProbabilityError = strategy_error.into();
assert!(matches!(
converted_error,
ProbabilityError::CalculationError(
ProbabilityCalculationErrorKind::ExpectedValueError { .. }
)
));
}
#[test]
fn test_invalid_profit_range_constructor() {
let error = ProbabilityError::invalid_profit_range("0-100", "Range mismatch");
assert_eq!(
format!("{error}"),
"Profit/loss range error: Invalid profit range '0-100': Range mismatch"
);
}
#[test]
fn test_invalid_expiration_constructor() {
let error = ProbabilityError::invalid_expiration("Expiration date invalid");
assert_eq!(
format!("{error}"),
"Expiration error: Invalid expiration: Expiration date invalid"
);
}
#[test]
fn test_profit_loss_error_max_loss_error_bis() {
let error = ProfitLossErrorKind::MaxLossError {
reason: "Exceeded allowed loss".to_string(),
};
assert_eq!(
format!("{error}"),
"Maximum loss calculation error: Exceeded allowed loss"
);
}
#[test]
fn test_profit_loss_error_profit_range_error() {
let error = ProfitLossErrorKind::ProfitRangeError {
reason: "Profit range mismatch".to_string(),
};
assert_eq!(
format!("{error}"),
"Profit range calculation error: Profit range mismatch"
);
}
#[test]
fn test_strategy_error_price_error_invalid_underlying_price() {
let error = StrategyError::PriceError(
crate::error::strategies::PriceErrorKind::InvalidUnderlyingPrice {
reason: "Underlying price is negative".to_string(),
},
);
let converted_error: ProbabilityError = ProbabilityError::from(error);
assert_eq!(
format!("{converted_error}"),
"Probability calculation error: Expected value calculation error: Underlying price is negative"
);
}
#[test]
fn test_strategy_error_price_error_invalid_price_range_bis() {
let error = StrategyError::PriceError(
crate::error::strategies::PriceErrorKind::InvalidPriceRange {
start: 0.0,
end: 50.0,
reason: "Start price is greater than end price".to_string(),
},
);
let converted_error: ProbabilityError = ProbabilityError::from(error);
assert_eq!(
format!("{converted_error}"),
"Probability calculation error: Expected value calculation error: Start price is greater than end price"
);
}
#[test]
fn test_operation_error_invalid_parameters_bis() {
let error = OperationErrorKind::InvalidParameters {
operation: "Calculate P/L".to_string(),
reason: "Invalid input values".to_string(),
};
let converted_error: ProbabilityError = error.into();
assert!(matches!(
converted_error,
ProbabilityError::CalculationError(
ProbabilityCalculationErrorKind::ExpectedValueError { .. }
)
));
}
#[test]
fn test_operation_error_not_supported() {
let error = OperationErrorKind::NotSupported {
operation: "Hedging".to_string(),
reason: "Operation not implemented".to_string(),
};
let converted_error = ProbabilityError::CalculationError(
ProbabilityCalculationErrorKind::ExpectedValueError {
reason: format!("Operation {error}"),
},
);
assert_eq!(
format!("{converted_error}"),
"Probability calculation error: Expected value calculation error: Operation Operation 'Hedging' is not supported for strategy 'Operation not implemented'"
);
}
}