use crate::error::{DecimalError, GreeksError, OptionsError};
use positive::Positive;
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ChainError {
#[error("Option data error: {0}")]
OptionDataError(OptionDataErrorKind),
#[error("Chain build error: {0}")]
ChainBuildError(ChainBuildErrorKind),
#[error("File error: {0}")]
FileError(FileErrorKind),
#[error("Strategy error: {0}")]
StrategyError(StrategyErrorKind),
#[error("option chain is empty: {symbol}")]
EmptyChain {
symbol: String,
},
#[error("cannot find ATM option for empty option chain: {symbol}")]
EmptyChainAtm {
symbol: String,
},
#[error("failed to find ATM option for option chain: {symbol}")]
AtmNotFound {
symbol: String,
},
#[error("failed to calculate any valid risk-neutral density value")]
EmptyDensities,
#[error("no valid data points available for volatility skew calculation")]
EmptySkewData,
#[error("strike {strike} not found in option chain")]
StrikeNotFound {
strike: Positive,
},
#[error(transparent)]
Curve(Box<crate::error::CurveError>),
#[error(transparent)]
Volatility(Box<crate::error::VolatilityError>),
#[error(transparent)]
Simulation(Box<crate::error::SimulationError>),
#[error(transparent)]
ExpirationDate(#[from] expiration_date::error::ExpirationDateError),
#[error(transparent)]
PositiveError(#[from] positive::PositiveError),
}
#[derive(Error, Debug)]
pub enum OptionDataErrorKind {
#[error("Invalid strike price {strike}: {reason}")]
InvalidStrike {
strike: f64,
reason: String,
},
#[error("Invalid volatility {volatility:?}: {reason}")]
InvalidVolatility {
volatility: Option<f64>,
reason: String,
},
#[error("Invalid prices (bid: {bid:?}, ask: {ask:?}): {reason}")]
InvalidPrices {
bid: Option<f64>,
ask: Option<f64>,
reason: String,
},
#[error("Invalid delta {delta:?}: {reason}")]
InvalidDelta {
delta: Option<f64>,
reason: String,
},
#[error("Price calculation error: {0}")]
PriceCalculationError(String),
#[error("Option data error: {0}")]
Other(String),
}
#[derive(Error, Debug)]
pub enum ChainBuildErrorKind {
#[error("Invalid parameter '{parameter}': {reason}")]
InvalidParameters {
parameter: String,
reason: String,
},
#[error("Volatility adjustment error (smile curve: {smile_curve}): {reason}")]
VolatilityAdjustmentError {
smile_curve: f64,
reason: String,
},
#[error(
"Strike generation error (ref price: {reference_price}, interval: {interval}): {reason}"
)]
StrikeGenerationError {
reference_price: f64,
interval: f64,
reason: String,
},
}
#[derive(Error, Debug)]
pub enum FileErrorKind {
#[error("IO error: {0}")]
IOError(#[from] io::Error),
#[error("Invalid file format '{format}': {reason}")]
InvalidFormat {
format: String,
reason: String,
},
#[error("Parse error at line {line} ('{content}'): {reason}")]
ParseError {
line: usize,
content: String,
reason: String,
},
}
#[derive(Error, Debug, PartialEq)]
pub enum StrategyErrorKind {
#[error("Invalid number of legs: expected {expected}, found {found} - {reason}")]
InvalidLegs {
expected: usize,
found: usize,
reason: String,
},
#[error("Invalid combination for strategy '{strategy_type}': {reason}")]
InvalidCombination {
strategy_type: String,
reason: String,
},
}
impl From<OptionsError> for ChainError {
fn from(error: OptionsError) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::PriceCalculationError(
error.to_string(),
))
}
}
impl From<io::Error> for ChainError {
fn from(error: io::Error) -> Self {
ChainError::FileError(FileErrorKind::IOError(error))
}
}
impl From<DecimalError> for ChainError {
fn from(error: DecimalError) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::PriceCalculationError(
error.to_string(),
))
}
}
impl ChainError {
#[must_use]
#[cold]
#[inline(never)]
pub fn invalid_strike(strike: f64, reason: &str) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::InvalidStrike {
strike,
reason: reason.to_string(),
})
}
#[must_use]
#[cold]
#[inline(never)]
pub fn invalid_volatility(volatility: Option<f64>, reason: &str) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::InvalidVolatility {
volatility,
reason: reason.to_string(),
})
}
#[must_use]
#[cold]
#[inline(never)]
pub fn invalid_prices(bid: Option<f64>, ask: Option<f64>, reason: &str) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::InvalidPrices {
bid,
ask,
reason: reason.to_string(),
})
}
#[must_use]
#[cold]
#[inline(never)]
pub fn invalid_legs(expected: usize, found: usize, reason: &str) -> Self {
ChainError::StrategyError(StrategyErrorKind::InvalidLegs {
expected,
found,
reason: reason.to_string(),
})
}
#[must_use]
#[cold]
#[inline(never)]
pub fn invalid_parameters(parameter: &str, reason: &str) -> Self {
ChainError::ChainBuildError(ChainBuildErrorKind::InvalidParameters {
parameter: parameter.to_string(),
reason: reason.to_string(),
})
}
#[must_use]
#[cold]
#[inline(never)]
pub fn invalid_price_calculation(reason: &str) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::PriceCalculationError(
reason.to_string(),
))
}
}
impl From<GreeksError> for ChainError {
#[inline]
fn from(err: GreeksError) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::Other(err.to_string()))
}
}
impl From<csv::Error> for ChainError {
fn from(err: csv::Error) -> Self {
ChainError::FileError(FileErrorKind::ParseError {
line: 0,
content: String::new(),
reason: err.to_string(),
})
}
}
impl From<serde_json::Error> for ChainError {
fn from(err: serde_json::Error) -> Self {
ChainError::FileError(FileErrorKind::ParseError {
line: 0,
content: String::new(),
reason: err.to_string(),
})
}
}
impl From<chrono::ParseError> for ChainError {
fn from(err: chrono::ParseError) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::Other(err.to_string()))
}
}
impl From<std::num::ParseIntError> for ChainError {
fn from(err: std::num::ParseIntError) -> Self {
ChainError::OptionDataError(OptionDataErrorKind::Other(err.to_string()))
}
}
impl From<crate::error::CurveError> for ChainError {
#[inline]
fn from(err: crate::error::CurveError) -> Self {
ChainError::Curve(Box::new(err))
}
}
impl From<crate::error::SimulationError> for ChainError {
#[inline]
fn from(err: crate::error::SimulationError) -> Self {
ChainError::Simulation(Box::new(err))
}
}
impl From<crate::error::VolatilityError> for ChainError {
#[inline]
fn from(err: crate::error::VolatilityError) -> Self {
ChainError::Volatility(Box::new(err))
}
}
#[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 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(_))
));
}
#[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_invalid_price_calculation_helper_method() {
let error = ChainError::invalid_price_calculation("Division by zero");
assert_eq!(
format!("{error}"),
"Option data error: Price calculation error: Division by zero"
);
}
#[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_empty_chain_atm() {
let error = ChainError::EmptyChainAtm {
symbol: "SPX".to_string(),
};
assert_eq!(
format!("{error}"),
"cannot find ATM option for empty option chain: SPX"
);
}
#[test]
fn test_chain_error_atm_not_found() {
let error = ChainError::AtmNotFound {
symbol: "SPX".to_string(),
};
assert_eq!(
format!("{error}"),
"failed to find ATM option for option chain: SPX"
);
}
#[test]
fn test_chain_error_empty_densities() {
let error = ChainError::EmptyDensities;
assert_eq!(
format!("{error}"),
"failed to calculate any valid risk-neutral density value"
);
}
#[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 Some(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 (ref price: 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"
);
}
}