Skip to main content

optionstratlib_backtest/error/
backtesting.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 21/9/25
5******************************************************************************/
6//! Target crate (ADR-0001 D2): **backtest**. Owns `BacktestError`.
7//!
8//! A backtest drives a strategy through a simulation, so it can fail for a
9//! strategy reason or a simulation reason. Backtesting is the layer that
10//! composes both, so it owns the error and keeps each cause typed instead of
11//! flattening it into a message (#511). Neither `StrategyError` nor
12//! `SimulationError` may name the other.
13
14use optionstratlib_core::error::{DecimalError, OptionsError};
15use optionstratlib_pricing::error::PricingError;
16use optionstratlib_simulation::error::SimulationError;
17use optionstratlib_strategies::error::StrategyError;
18use thiserror::Error;
19
20/// Failure of a backtest run.
21#[derive(Debug, Error)]
22pub enum BacktestError {
23    /// The strategy under test failed, for example while reading its legs or
24    /// its fees.
25    #[error(transparent)]
26    Strategy(Box<StrategyError>),
27
28    /// The simulation driving the backtest failed.
29    #[error(transparent)]
30    Simulation(Box<SimulationError>),
31
32    /// Pricing the leg at a step failed.
33    #[error(transparent)]
34    Pricing(Box<PricingError>),
35
36    /// The option being priced is invalid.
37    #[error(transparent)]
38    Options(Box<OptionsError>),
39
40    /// Decimal arithmetic on a premium or a statistic overflowed.
41    #[error(transparent)]
42    Decimal(Box<DecimalError>),
43
44    /// A value that must stay strictly positive left its range.
45    #[error(transparent)]
46    Positive(#[from] optionstratlib_core::model::PositiveError),
47
48    /// A run counter kept by the backtest reached the top of its range.
49    #[error("backtest counter `{counter}` overflowed")]
50    CounterOverflow {
51        /// Name of the counter that could not advance.
52        counter: &'static str,
53    },
54}
55
56impl BacktestError {
57    /// Creates a [`BacktestError::CounterOverflow`] for `counter`.
58    #[cold]
59    #[inline(never)]
60    #[must_use]
61    pub fn counter_overflow(counter: &'static str) -> Self {
62        BacktestError::CounterOverflow { counter }
63    }
64}
65
66impl From<PricingError> for BacktestError {
67    #[inline]
68    fn from(error: PricingError) -> Self {
69        BacktestError::Pricing(Box::new(error))
70    }
71}
72
73impl From<OptionsError> for BacktestError {
74    #[inline]
75    fn from(error: OptionsError) -> Self {
76        BacktestError::Options(Box::new(error))
77    }
78}
79
80impl From<DecimalError> for BacktestError {
81    #[inline]
82    fn from(error: DecimalError) -> Self {
83        BacktestError::Decimal(Box::new(error))
84    }
85}
86
87impl From<StrategyError> for BacktestError {
88    #[inline]
89    fn from(error: StrategyError) -> Self {
90        BacktestError::Strategy(Box::new(error))
91    }
92}
93
94impl From<SimulationError> for BacktestError {
95    #[inline]
96    fn from(error: SimulationError) -> Self {
97        BacktestError::Simulation(Box::new(error))
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_counter_overflow_names_the_counter() {
107        let error = BacktestError::counter_overflow("total_simulations");
108        assert!(matches!(
109            error,
110            BacktestError::CounterOverflow {
111                counter: "total_simulations"
112            }
113        ));
114        assert_eq!(
115            error.to_string(),
116            "backtest counter `total_simulations` overflowed"
117        );
118    }
119}