use crate::error::{GreeksError, OptionsError};
use std::error::Error;
use std::fmt;
use std::io;
#[derive(Debug)]
pub enum ChainError {
OptionDataError(OptionDataErrorKind),
ChainBuildError(ChainBuildErrorKind),
FileError(FileErrorKind),
StrategyError(StrategyErrorKind),
DynError {
message: String,
},
}
#[derive(Debug)]
pub enum OptionDataErrorKind {
InvalidStrike {
strike: f64,
reason: String,
},
InvalidVolatility {
volatility: Option<f64>,
reason: String,
},
InvalidPrices {
bid: Option<f64>,
ask: Option<f64>,
reason: String,
},
InvalidDelta {
delta: Option<f64>,
reason: String,
},
PriceCalculationError(String),
OtherError(String),
}
#[derive(Debug)]
pub enum ChainBuildErrorKind {
InvalidParameters {
parameter: String,
reason: String,
},
VolatilityAdjustmentError {
smile_curve: f64,
reason: String,
},
StrikeGenerationError {
reference_price: f64,
interval: f64,
reason: String,
},
}
#[derive(Debug)]
pub enum FileErrorKind {
IOError(io::Error),
InvalidFormat {
format: String,
reason: String,
},
ParseError {
line: usize,
content: String,
reason: String,
},
}
#[derive(Debug, PartialEq)]
pub enum StrategyErrorKind {
InvalidLegs {
expected: usize,
found: usize,
reason: String,
},
InvalidCombination {
strategy_type: String,
reason: String,
},
}
impl fmt::Display for ChainError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ChainError::OptionDataError(err) => write!(f, "Option data error: {err}"),
ChainError::ChainBuildError(err) => write!(f, "Chain build error: {err}"),
ChainError::FileError(err) => write!(f, "File error: {err}"),
ChainError::StrategyError(err) => write!(f, "Strategy error: {err}"),
ChainError::DynError { message } => write!(f, "Error: {message}"),
}
}
}
impl fmt::Display for OptionDataErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OptionDataErrorKind::InvalidStrike { strike, reason } => {
write!(f, "Invalid strike price {strike}: {reason}")
}
OptionDataErrorKind::InvalidVolatility { volatility, reason } => {
write!(
f,
"Invalid volatility {:?}: {}",
volatility.unwrap_or(0.0),
reason
)
}
OptionDataErrorKind::InvalidPrices { bid, ask, reason } => {
write!(f, "Invalid prices (bid: {bid:?}, ask: {ask:?}): {reason}")
}
OptionDataErrorKind::InvalidDelta { delta, reason } => {
write!(f, "Invalid delta {delta:?}: {reason}")
}
OptionDataErrorKind::PriceCalculationError(msg) => {
write!(f, "Price calculation error: {msg}")
}
OptionDataErrorKind::OtherError(msg) => write!(f, "{msg}"),
}
}
}
impl fmt::Display for ChainBuildErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ChainBuildErrorKind::InvalidParameters { parameter, reason } => {
write!(f, "Invalid parameter '{parameter}': {reason}")
}
ChainBuildErrorKind::VolatilityAdjustmentError {
smile_curve,
reason,
} => {
write!(
f,
"Volatility adjustment error (skew factor: {smile_curve}): {reason}"
)
}
ChainBuildErrorKind::StrikeGenerationError {
reference_price,
interval,
reason,
} => {
write!(
f,
"Strike generation error (reference: {reference_price}, interval: {interval}): {reason}"
)
}
}
}
}
impl fmt::Display for FileErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileErrorKind::IOError(err) => write!(f, "IO error: {err}"),
FileErrorKind::InvalidFormat { format, reason } => {
write!(f, "Invalid {format} format: {reason}")
}
FileErrorKind::ParseError {
line,
content,
reason,
} => {
write!(
f,
"Parse error at line {line}, content '{content}': {reason}"
)
}
}
}
}
impl fmt::Display for StrategyErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StrategyErrorKind::InvalidLegs {
expected,
found,
reason,
} => {
write!(
f,
"Invalid number of legs (expected: {expected}, found: {found}): {reason}"
)
}
StrategyErrorKind::InvalidCombination {
strategy_type,
reason,
} => {
write!(
f,
"Invalid combination for strategy '{strategy_type}': {reason}"
)
}
}
}
}
impl Error for ChainError {}
impl From<io::Error> for ChainError {
fn from(error: io::Error) -> Self {
ChainError::FileError(FileErrorKind::IOError(error))
}
}
impl From<OptionsError> for ChainError {
fn from(error: OptionsError) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::PriceCalculationError(
error.to_string(),
))
}
}
impl ChainError {
pub fn invalid_strike(strike: f64, reason: &str) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::InvalidStrike {
strike,
reason: reason.to_string(),
})
}
pub fn invalid_volatility(volatility: Option<f64>, reason: &str) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::InvalidVolatility {
volatility,
reason: reason.to_string(),
})
}
pub fn invalid_prices(bid: Option<f64>, ask: Option<f64>, reason: &str) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::InvalidPrices {
bid,
ask,
reason: reason.to_string(),
})
}
pub fn invalid_legs(expected: usize, found: usize, reason: &str) -> Self {
ChainError::StrategyError(StrategyErrorKind::InvalidLegs {
expected,
found,
reason: reason.to_string(),
})
}
pub fn invalid_parameters(parameter: &str, reason: &str) -> Self {
ChainError::ChainBuildError(ChainBuildErrorKind::InvalidParameters {
parameter: parameter.to_string(),
reason: reason.to_string(),
})
}
}
impl From<String> for ChainError {
fn from(msg: String) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::PriceCalculationError(msg))
}
}
impl From<GreeksError> for ChainError {
fn from(err: GreeksError) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::OtherError(err.to_string()))
}
}
impl From<Box<dyn Error>> for ChainError {
fn from(error: Box<dyn Error>) -> Self {
ChainError::DynError {
message: error.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_option_data_errors() {
let error = ChainError::invalid_strike(-10.0, "Strike cannot be negative");
assert!(matches!(
error,
ChainError::OptionDataError(OptionDataErrorKind::InvalidStrike { .. })
));
let error = ChainError::invalid_volatility(Some(-0.5), "Volatility must be positive");
assert!(matches!(
error,
ChainError::OptionDataError(OptionDataErrorKind::InvalidVolatility { .. })
));
}
#[test]
fn test_error_messages() {
let error = ChainError::invalid_strike(0.0, "Strike must be positive");
assert!(error.to_string().contains("Strike must be positive"));
}
#[test]
fn test_chain_build_errors() {
let error = ChainError::ChainBuildError(ChainBuildErrorKind::InvalidParameters {
parameter: "chain_size".to_string(),
reason: "Must be greater than 0".to_string(),
});
assert!(error.to_string().contains("chain_size"));
assert!(error.to_string().contains("Must be greater than 0"));
}
#[test]
fn test_strategy_errors() {
let error = ChainError::invalid_legs(4, 3, "Iron Condor requires exactly 4 legs");
assert!(error.to_string().contains("4"));
assert!(error.to_string().contains("3"));
assert!(error.to_string().contains("Iron Condor"));
}
#[test]
fn test_file_errors() {
let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
let error = ChainError::from(io_error);
assert!(matches!(
error,
ChainError::FileError(FileErrorKind::IOError(..))
));
}
}
#[cfg(test)]
mod tests_extended {
use super::*;
#[test]
fn test_chain_build_error_display() {
let error = ChainBuildErrorKind::InvalidParameters {
parameter: "size".to_string(),
reason: "must be positive".to_string(),
};
assert!(error.to_string().contains("size"));
assert!(error.to_string().contains("must be positive"));
let error = ChainBuildErrorKind::VolatilityAdjustmentError {
smile_curve: 0.5,
reason: "invalid adjustment".to_string(),
};
assert!(error.to_string().contains("0.5"));
assert!(error.to_string().contains("invalid adjustment"));
}
#[test]
fn test_file_error_display() {
let error = FileErrorKind::InvalidFormat {
format: "CSV".to_string(),
reason: "invalid header".to_string(),
};
assert!(error.to_string().contains("CSV"));
assert!(error.to_string().contains("invalid header"));
let error = FileErrorKind::ParseError {
line: 42,
content: "bad data".to_string(),
reason: "invalid number".to_string(),
};
assert!(error.to_string().contains("42"));
assert!(error.to_string().contains("bad data"));
}
#[test]
fn test_option_data_error_display() {
let error = OptionDataErrorKind::InvalidDelta {
delta: Some(1.5),
reason: "delta cannot exceed 1".to_string(),
};
assert!(error.to_string().contains("1.5"));
assert!(error.to_string().contains("delta cannot exceed 1"));
}
#[test]
fn test_strategy_error_equality() {
let error1 = StrategyErrorKind::InvalidLegs {
expected: 4,
found: 3,
reason: "Iron Condor needs 4 legs".to_string(),
};
let error2 = StrategyErrorKind::InvalidLegs {
expected: 4,
found: 3,
reason: "Iron Condor needs 4 legs".to_string(),
};
assert_eq!(error1, error2);
}
#[test]
fn test_error_conversions() {
let string_error: ChainError = "test error".to_string().into();
assert!(matches!(
string_error,
ChainError::OptionDataError(OptionDataErrorKind::PriceCalculationError(_))
));
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let chain_error = ChainError::from(io_error);
assert!(matches!(
chain_error,
ChainError::FileError(FileErrorKind::IOError(_))
));
let std_error: Box<dyn Error> = Box::new(std::io::Error::other("dynamic error"));
let chain_error = ChainError::from(std_error);
assert!(matches!(chain_error, ChainError::DynError { .. }));
}
#[test]
fn test_helper_methods() {
let error = ChainError::invalid_strike(-10.0, "Strike must be positive");
assert!(matches!(
error,
ChainError::OptionDataError(OptionDataErrorKind::InvalidStrike { .. })
));
let error = ChainError::invalid_volatility(None, "Volatility missing");
assert!(matches!(
error,
ChainError::OptionDataError(OptionDataErrorKind::InvalidVolatility { .. })
));
}
#[test]
fn test_chain_error_file_error() {
let error = ChainError::FileError(FileErrorKind::IOError(io::Error::new(
io::ErrorKind::NotFound,
"File not found",
)));
assert_eq!(format!("{error}"), "File error: IO error: File not found");
}
#[test]
fn test_chain_error_dyn_error() {
let error = ChainError::DynError {
message: "Dynamic error occurred".to_string(),
};
assert_eq!(format!("{error}"), "Error: Dynamic error occurred");
}
#[test]
fn test_option_data_error_invalid_volatility() {
let error = OptionDataErrorKind::InvalidVolatility {
volatility: Some(0.25),
reason: "Out of bounds".to_string(),
};
assert_eq!(format!("{error}"), "Invalid volatility 0.25: Out of bounds");
}
#[test]
fn test_option_data_error_invalid_prices() {
let error = OptionDataErrorKind::InvalidPrices {
bid: Some(1.0),
ask: Some(2.0),
reason: "Bid-ask spread too wide".to_string(),
};
assert_eq!(
format!("{error}"),
"Invalid prices (bid: Some(1.0), ask: Some(2.0)): Bid-ask spread too wide"
);
}
#[test]
fn test_option_data_error_price_calculation_error() {
let error = OptionDataErrorKind::PriceCalculationError("Division by zero".to_string());
assert_eq!(
format!("{error}"),
"Price calculation error: Division by zero"
);
}
#[test]
fn test_chain_build_error_strike_generation_error() {
let error = ChainBuildErrorKind::StrikeGenerationError {
reference_price: 100.0,
interval: 5.0,
reason: "Invalid strike intervals".to_string(),
};
assert_eq!(
format!("{error}"),
"Strike generation error (reference: 100, interval: 5): Invalid strike intervals"
);
}
#[test]
fn test_file_error_io_error() {
let error = FileErrorKind::IOError(io::Error::new(
io::ErrorKind::PermissionDenied,
"Permission denied",
));
assert_eq!(format!("{error}"), "IO error: Permission denied");
}
#[test]
fn test_strategy_error_invalid_combination() {
let error = StrategyErrorKind::InvalidCombination {
strategy_type: "Straddle".to_string(),
reason: "Conflicting legs".to_string(),
};
assert_eq!(
format!("{error}"),
"Invalid combination for strategy 'Straddle': Conflicting legs"
);
}
#[test]
fn test_chain_error_invalid_prices_constructor() {
let error = ChainError::invalid_prices(Some(1.0), Some(2.0), "Spread too wide");
assert_eq!(
format!("{error}"),
"Option data error: Invalid prices (bid: Some(1.0), ask: Some(2.0)): Spread too wide"
);
}
}