bts_rs/
errors.rs

1//! Error types for the BTS library.
2
3use chrono::{DateTime, Utc};
4
5/// Enum representing possible errors in the crate.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Custom error types for the `bts` library.
9#[derive(thiserror::Error, Debug)]
10pub enum Error {
11    /// The candle data provided is empty.
12    ///
13    /// Backtesting requires at least one candle to execute.
14    #[error("Candle data is empty: backtesting requires at least one candle")]
15    CandleDataEmpty,
16
17    /// The requested candle was not found in the dataset.
18    #[error("Candle not found")]
19    CandleNotFound,
20
21    /// The Aggregator factor is invalid.
22    #[error("The Aggregator factor is invalid")]
23    InvalidFactor,
24
25    /// A required field is missing.
26    #[error("Missing required field: {0}")]
27    MissingField(&'static str),
28
29    /// Prices are not in valid order (open ≤ low ≤ high ≤ close).
30    #[error("Invalid price order: open={0}, low={1}, high={2}, close={3}")]
31    InvalidPriceOrder(f64, f64, f64, f64),
32
33    /// Volume cannot be negative.
34    #[error("Volume cannot be negative (got: {0})")]
35    NegativeVolume(f64),
36
37    /// Open time and close time are not in valid order (open time < close time).
38    #[error("Invalid time order: open={0}, close={1}")]
39    InvalideTimes(DateTime<Utc>, DateTime<Utc>),
40
41    /// The initial or current balance is not positive.
42    ///
43    /// ### Arguments
44    /// * `0` - The invalid balance value.
45    #[error("Balance must be positive (got: {0})")]
46    NegZeroBalance(f64),
47
48    /// The wallet does not have enough funds to place the order.
49    ///
50    /// ### Arguments
51    /// * `0` - The required amount.
52    /// * `1` - The available amount.
53    #[error("Insufficient funds: required {0}, available {1}")]
54    InsufficientFunds(f64, f64),
55
56    /// The free balance is negative.
57    ///
58    /// ### Arguments
59    /// * `0` - The current balance.
60    /// * `1` - The locked funds.
61    #[error("Negative free balance: balance={0}, locked={1}")]
62    NegFreeBalance(f64, f64),
63
64    /// The fees are negative.
65    #[error("Negative fees")]
66    NegZeroFees,
67
68    /// The locked funds are insufficient for the requested amount.
69    ///
70    /// ### Arguments
71    /// * `0` - The currently locked funds.
72    /// * `1` - The requested amount to unlock.
73    #[error("Locked funds {0} are insufficient for amount {1}")]
74    UnlockBalance(f64, f64),
75
76    /// The requested order was not found.
77    #[error("Order not found")]
78    OrderNotFound,
79
80    /// Failed to remove an order.
81    #[error("Failed to remove order")]
82    RemoveOrder,
83
84    /// The requested position was not found.
85    #[error("Position not found")]
86    PositionNotFound,
87
88    /// Failed to remove a position.
89    #[error("Failed to remove position")]
90    RemovePosition,
91
92    /// The exit price is invalid.
93    #[error("Invalid exit price {0}")]
94    ExitPrice(f64),
95
96    /// A generic error with a custom message.
97    ///
98    /// ### Arguments
99    /// * `0` - The error message.
100    #[error("{0}")]
101    Msg(String),
102
103    /// Take profit or stop loss values must be positive.
104    #[error("TakeProfit or StopLoss must be positive")]
105    NegTakeProfitAndStopLoss,
106
107    /// Trailing stop values must be positive.
108    #[error("TrailingStop must be positive and greater than 0")]
109    NegZeroTrailingStop,
110
111    /// The order type is not compatible with the operation.
112    ///
113    /// Use market or limit orders to open a position, and take profit, stop loss, or trailing stop to close a position.
114    #[error("Try another order type")]
115    MismatchedOrderType,
116
117    /// An error with plotters crate.
118    ///
119    /// ### Arguments
120    /// * `0` - The underlying plotters error.
121    #[cfg(feature = "draws")]
122    #[error("{0}")]
123    Plotters(String),
124
125    /// An error with charming crate.
126    ///
127    /// ### Arguments
128    /// * `0` - The underlying charming error.
129    #[cfg(feature = "draws")]
130    #[error("{0}")]
131    Charming(#[from] charming::EchartsError),
132}
133
134#[cfg(feature = "serde")]
135impl serde::Serialize for Error {
136    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
137    where
138        S: serde::Serializer,
139    {
140        serializer.serialize_str(self.to_string().as_ref())
141    }
142}
143
144#[cfg(feature = "serde")]
145impl<'de> serde::Deserialize<'de> for Error {
146    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
147    where
148        D: serde::Deserializer<'de>,
149    {
150        // Error proxy
151        #[derive(serde::Deserialize)]
152        #[serde(tag = "type", content = "data")]
153        enum ErrorWrapper {
154            CandleDataEmpty,
155            CandleNotFound,
156            InvalidFactor,
157            MissingField {
158                field: String,
159            },
160            InvalidPriceOrder {
161                open: f64,
162                low: f64,
163                high: f64,
164                close: f64,
165            },
166            NegativeVolume {
167                volume: f64,
168            },
169            InvalideTimes {
170                open: i64,
171                close: i64,
172            },
173            NegZeroBalance {
174                balance: f64,
175            },
176            InsufficientFunds {
177                required: f64,
178                available: f64,
179            },
180            NegFreeBalance {
181                balance: f64,
182                locked: f64,
183            },
184            NegZeroFees,
185            UnlockBalance {
186                locked: f64,
187                amount: f64,
188            },
189            OrderNotFound,
190            RemoveOrder,
191            PositionNotFound,
192            RemovePosition,
193            ExitPrice {
194                price: f64,
195            },
196            Msg {
197                message: String,
198            },
199            NegTakeProfitAndStopLoss,
200            NegZeroTrailingStop,
201            MismatchedOrderType,
202            #[cfg(feature = "draws")]
203            Plotters {
204                error: String,
205            },
206            #[cfg(feature = "draws")]
207            Charming {
208                error: String,
209            },
210        }
211
212        // Désérialiser en utilisant la structure intermédiaire
213        let wrapper = ErrorWrapper::deserialize(deserializer)?;
214
215        // Convertir en Error
216        Ok(match wrapper {
217            ErrorWrapper::CandleDataEmpty => Error::CandleDataEmpty,
218            ErrorWrapper::CandleNotFound => Error::CandleNotFound,
219            ErrorWrapper::InvalidFactor => Error::InvalidFactor,
220            ErrorWrapper::MissingField { field } => Error::MissingField(Box::leak(field.into_boxed_str())),
221            ErrorWrapper::InvalidPriceOrder { open, low, high, close } => {
222                Error::InvalidPriceOrder(open, low, high, close)
223            }
224            ErrorWrapper::NegativeVolume { volume } => Error::NegativeVolume(volume),
225            ErrorWrapper::InvalideTimes { open, close } => {
226                let open_dt = DateTime::from_timestamp_millis(open).unwrap_or(Utc::now());
227                let close_dt = DateTime::from_timestamp_millis(close).unwrap_or(Utc::now());
228                Error::InvalideTimes(open_dt, close_dt)
229            }
230            ErrorWrapper::NegZeroBalance { balance } => Error::NegZeroBalance(balance),
231            ErrorWrapper::InsufficientFunds { required, available } => Error::InsufficientFunds(required, available),
232            ErrorWrapper::NegFreeBalance { balance, locked } => Error::NegFreeBalance(balance, locked),
233            ErrorWrapper::NegZeroFees => Error::NegZeroFees,
234            ErrorWrapper::UnlockBalance { locked, amount } => Error::UnlockBalance(locked, amount),
235            ErrorWrapper::OrderNotFound => Error::OrderNotFound,
236            ErrorWrapper::RemoveOrder => Error::RemoveOrder,
237            ErrorWrapper::PositionNotFound => Error::PositionNotFound,
238            ErrorWrapper::RemovePosition => Error::RemovePosition,
239            ErrorWrapper::ExitPrice { price } => Error::ExitPrice(price),
240            ErrorWrapper::Msg { message } => Error::Msg(message),
241            ErrorWrapper::NegTakeProfitAndStopLoss => Error::NegTakeProfitAndStopLoss,
242            ErrorWrapper::NegZeroTrailingStop => Error::NegZeroTrailingStop,
243            ErrorWrapper::MismatchedOrderType => Error::MismatchedOrderType,
244            #[cfg(feature = "draws")]
245            ErrorWrapper::Plotters { error } => Error::Plotters(error),
246            #[cfg(feature = "draws")]
247            ErrorWrapper::Charming { error } => Error::Charming(charming::EchartsError::HtmlRenderingError(error)),
248        })
249    }
250}