use crate::error::common::OperationErrorKind;
use crate::error::{GreeksError, OptionsError, PositionError, TradeError};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum StrategyError {
#[error("Price error: {0}")]
PriceError(PriceErrorKind),
#[error("Break-even error: {0}")]
BreakEvenError(BreakEvenErrorKind),
#[error("Profit/loss error: {0}")]
ProfitLossError(ProfitLossErrorKind),
#[error("Operation error: {0}")]
OperationError(OperationErrorKind),
#[error("Not implemented")]
NotImplemented,
#[error(transparent)]
Simulation(Box<crate::error::SimulationError>),
#[error(transparent)]
GreeksError(#[from] GreeksError),
#[error(transparent)]
PositiveError(#[from] positive::PositiveError),
#[error("numeric conversion failed: {value} is not a finite Decimal")]
NumericConversion {
value: f64,
},
#[error("missing greek `{name}`: option not initialized for greek calculation")]
MissingGreek {
name: &'static str,
},
#[error("empty collection in strategy operation: {context}")]
EmptyCollection {
context: String,
},
}
#[derive(Error, Debug)]
pub enum PriceErrorKind {
#[error("Invalid underlying price: {reason}")]
InvalidUnderlyingPrice {
reason: String,
},
#[error("Invalid price range ({start} to {end}): {reason}")]
InvalidPriceRange {
start: f64,
end: f64,
reason: String,
},
}
#[derive(Error, Debug)]
pub enum BreakEvenErrorKind {
#[error("Break-even calculation error: {reason}")]
CalculationError {
reason: String,
},
#[error("No break-even points exist for this strategy")]
NoBreakEvenPoints,
}
#[derive(Error, Debug)]
pub enum ProfitLossErrorKind {
#[error("Maximum profit calculation error: {reason}")]
MaxProfitError {
reason: String,
},
#[error("Maximum loss calculation error: {reason}")]
MaxLossError {
reason: String,
},
#[error("Profit range calculation error: {reason}")]
ProfitRangeError {
reason: String,
},
}
pub type StrategyResult<T> = Result<T, StrategyError>;
impl StrategyError {
#[must_use]
#[cold]
#[inline(never)]
pub fn operation_not_supported(operation: &str, strategy_type: &str) -> Self {
StrategyError::OperationError(OperationErrorKind::NotSupported {
operation: operation.to_string(),
reason: strategy_type.to_string(),
})
}
#[must_use]
#[cold]
#[inline(never)]
pub fn invalid_parameters(operation: &str, reason: &str) -> Self {
StrategyError::OperationError(OperationErrorKind::InvalidParameters {
operation: operation.to_string(),
reason: reason.to_string(),
})
}
#[cold]
#[inline(never)]
#[must_use]
pub fn numeric_conversion(value: f64) -> Self {
StrategyError::NumericConversion { value }
}
#[cold]
#[inline(never)]
#[must_use]
pub fn missing_greek(name: &'static str) -> Self {
StrategyError::MissingGreek { name }
}
#[cold]
#[inline(never)]
#[must_use]
pub fn empty_collection(context: impl Into<String>) -> Self {
StrategyError::EmptyCollection {
context: context.into(),
}
}
}
impl From<PositionError> for StrategyError {
fn from(err: PositionError) -> Self {
StrategyError::OperationError(OperationErrorKind::InvalidParameters {
operation: "Position".to_string(),
reason: err.to_string(),
})
}
}
impl From<OptionsError> for StrategyError {
fn from(err: OptionsError) -> Self {
StrategyError::OperationError(OperationErrorKind::InvalidParameters {
operation: "Options".to_string(),
reason: err.to_string(),
})
}
}
impl From<crate::error::SimulationError> for StrategyError {
#[inline]
fn from(err: crate::error::SimulationError) -> Self {
StrategyError::Simulation(Box::new(err))
}
}
impl From<crate::error::PricingError> for StrategyError {
fn from(err: crate::error::PricingError) -> Self {
StrategyError::OperationError(OperationErrorKind::InvalidParameters {
operation: "Pricing".to_string(),
reason: err.to_string(),
})
}
}
impl From<TradeError> for StrategyError {
fn from(value: TradeError) -> Self {
StrategyError::OperationError(OperationErrorKind::InvalidParameters {
operation: "Trade".to_string(),
reason: value.to_string(),
})
}
}
#[cfg(test)]
mod tests_from_str {
use super::*;
use crate::error::ProbabilityError;
#[test]
fn test_strategy_to_probability_error_conversion() {
let strategy_error = StrategyError::operation_not_supported("max_profit", "TestStrategy");
let probability_error = ProbabilityError::from(strategy_error);
assert!(probability_error.to_string().contains("max_profit"));
assert!(probability_error.to_string().contains("TestStrategy"));
}
#[test]
fn test_profit_loss_error_conversion() {
let strategy_error = StrategyError::ProfitLossError(ProfitLossErrorKind::MaxProfitError {
reason: "Test error".to_string(),
});
let probability_error = ProbabilityError::from(strategy_error);
assert!(probability_error.to_string().contains("Test error"));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strategy_error_creation() {
let error = StrategyError::operation_not_supported("max_profit", "TestStrategy");
assert!(matches!(
error,
StrategyError::OperationError(OperationErrorKind::NotSupported { .. })
));
}
#[test]
fn test_error_messages() {
let error = StrategyError::operation_not_supported("max_profit", "TestStrategy");
let error_string = error.to_string();
assert!(error_string.contains("max_profit"));
assert!(error_string.contains("TestStrategy"));
}
}
#[cfg(test)]
mod tests_panic_free_variants {
use super::*;
#[test]
fn test_numeric_conversion_constructor_and_display() {
let err = StrategyError::numeric_conversion(f64::NAN);
assert!(matches!(err, StrategyError::NumericConversion { .. }));
assert!(err.to_string().contains("NaN"));
}
#[test]
fn test_numeric_conversion_inf_message_includes_value() {
let err = StrategyError::numeric_conversion(f64::INFINITY);
assert!(err.to_string().contains("inf"));
}
#[test]
fn test_missing_greek_constructor() {
let err = StrategyError::missing_greek("delta");
match err {
StrategyError::MissingGreek { name } => assert_eq!(name, "delta"),
other => panic!("expected MissingGreek, got {other:?}"),
}
}
#[test]
fn test_missing_greek_display() {
let err = StrategyError::missing_greek("vega");
assert!(err.to_string().contains("missing greek `vega`"));
}
#[test]
fn test_empty_collection_constructor_accepts_str_and_string() {
let from_str = StrategyError::empty_collection("option chain");
let from_string = StrategyError::empty_collection(String::from("break-even points"));
assert!(matches!(from_str, StrategyError::EmptyCollection { .. }));
assert!(matches!(from_string, StrategyError::EmptyCollection { .. }));
assert!(from_str.to_string().contains("option chain"));
assert!(from_string.to_string().contains("break-even points"));
}
#[test]
fn test_panic_free_variants_route_through_probability_error() {
use crate::error::ProbabilityError;
let nc = ProbabilityError::from(StrategyError::numeric_conversion(f64::NAN));
let mg = ProbabilityError::from(StrategyError::missing_greek("delta"));
let ec = ProbabilityError::from(StrategyError::empty_collection("chain"));
assert!(nc.to_string().contains("numeric conversion"));
assert!(mg.to_string().contains("missing greek"));
assert!(ec.to_string().contains("empty collection"));
}
}
#[cfg(test)]
mod tests_display {
use super::*;
#[test]
fn test_price_error_display() {
let error = StrategyError::PriceError(PriceErrorKind::InvalidUnderlyingPrice {
reason: "Price cannot be negative".to_string(),
});
assert!(error.to_string().contains("Price cannot be negative"));
}
#[test]
fn test_break_even_error_display() {
let error = StrategyError::BreakEvenError(BreakEvenErrorKind::CalculationError {
reason: "Invalid input parameters".to_string(),
});
assert!(error.to_string().contains("Invalid input parameters"));
}
#[test]
fn test_profit_loss_error_display() {
let error = StrategyError::ProfitLossError(ProfitLossErrorKind::MaxProfitError {
reason: "Cannot calculate maximum profit".to_string(),
});
assert!(
error
.to_string()
.contains("Cannot calculate maximum profit")
);
}
#[test]
fn test_operation_error_display() {
let error = StrategyError::operation_not_supported("max_profit", "TestStrategy");
assert!(error.to_string().contains("max_profit"));
assert!(error.to_string().contains("TestStrategy"));
}
}
#[cfg(test)]
mod tests_extended {
use super::*;
#[test]
fn test_strategy_error_simulation() {
let sim_error = crate::error::SimulationError::walk_error("simulation failure");
let error = StrategyError::from(sim_error);
assert!(matches!(error, StrategyError::Simulation(_)));
}
#[test]
fn test_price_error_invalid_price_range() {
let error = PriceErrorKind::InvalidPriceRange {
start: 10.0,
end: 50.0,
reason: "Start price must be less than end price".to_string(),
};
assert_eq!(
format!("{error}"),
"Invalid price range (10 to 50): Start price must be less than end price"
);
}
#[test]
fn test_break_even_error_no_points() {
let error = BreakEvenErrorKind::NoBreakEvenPoints;
assert_eq!(
format!("{error}"),
"No break-even points exist for this strategy"
);
}
#[test]
fn test_profit_loss_error_max_loss_error() {
let error = ProfitLossErrorKind::MaxLossError {
reason: "Loss exceeds margin requirements".to_string(),
};
assert_eq!(
format!("{error}"),
"Maximum loss calculation error: Loss exceeds margin requirements"
);
}
#[test]
fn test_profit_loss_error_profit_range_error() {
let error = ProfitLossErrorKind::ProfitRangeError {
reason: "Profit calculation failed".to_string(),
};
assert_eq!(
format!("{error}"),
"Profit range calculation error: Profit calculation failed"
);
}
#[test]
fn test_strategy_error_invalid_parameters_constructor() {
let error = StrategyError::invalid_parameters("Open position", "Margin insufficient");
assert_eq!(
format!("{error}"),
"Operation error: Invalid parameters for operation 'Open position': Margin insufficient"
);
}
#[test]
fn test_strategy_error_from_trade_error_invalid_trade() {
let trade_error = TradeError::invalid_trade("Trade failure");
let error: StrategyError = trade_error.into();
assert_eq!(
format!("{error}"),
"Operation error: Invalid parameters for operation 'Trade': Invalid trade: Trade failure"
);
}
#[test]
fn test_strategy_error_from_trade_error_invalid_trade_status() {
let trade_error = TradeError::invalid_trade_status("Trade status failure");
let error: StrategyError = trade_error.into();
assert_eq!(
format!("{error}"),
"Operation error: Invalid parameters for operation 'Trade': Invalid trade status: Trade status failure"
);
}
}