1use std::time::Duration;
23
24use nautilus_network::http::{HttpClientError, StatusCode};
25use thiserror::Error;
26use tokio_tungstenite::tungstenite;
27
28use crate::http::error::BitmexBuildError;
29
30#[derive(Debug, Error)]
32pub enum BitmexError {
33 #[error("Retryable error: {source}")]
35 Retryable {
36 #[source]
37 source: BitmexRetryableError,
38 retry_after: Option<Duration>,
40 },
41
42 #[error("Non-retryable error: {source}")]
44 NonRetryable {
45 #[source]
46 source: BitmexNonRetryableError,
47 },
48
49 #[error("Fatal error: {source}")]
51 Fatal {
52 #[source]
53 source: BitmexFatalError,
54 },
55
56 #[error("Network error: {0}")]
58 Network(#[from] HttpClientError),
59
60 #[error("WebSocket error: {0}")]
62 WebSocket(#[from] tungstenite::Error),
63
64 #[error("JSON error: {message}")]
66 Json {
67 message: String,
68 raw: Option<String>,
70 },
71
72 #[error("Configuration error: {0}")]
74 Config(String),
75}
76
77#[derive(Debug, Error)]
79pub enum BitmexRetryableError {
80 #[error("Rate limit exceeded (remaining: {remaining:?}, reset: {reset_at:?})")]
82 RateLimit {
83 remaining: Option<u32>,
84 reset_at: Option<Duration>,
85 },
86
87 #[error("Service temporarily unavailable")]
89 ServiceUnavailable,
90
91 #[error("Gateway timeout")]
93 GatewayTimeout,
94
95 #[error("Server error (status: {status})")]
97 ServerError { status: StatusCode },
98
99 #[error("Request timed out after {duration:?}")]
101 Timeout { duration: Duration },
102
103 #[error("Temporary network error: {message}")]
105 TemporaryNetwork { message: String },
106
107 #[error("WebSocket connection lost")]
109 ConnectionLost,
110
111 #[error("Order book resync required for {symbol}")]
113 OrderBookResync { symbol: String },
114}
115
116#[derive(Debug, Error)]
118pub enum BitmexNonRetryableError {
119 #[error("Bad request: {message}")]
121 BadRequest { message: String },
122
123 #[error("Resource not found: {resource}")]
125 NotFound { resource: String },
126
127 #[error("Method not allowed: {method}")]
129 MethodNotAllowed { method: String },
130
131 #[error("Validation error: {field}: {message}")]
133 Validation { field: String, message: String },
134
135 #[error("Invalid order: {message}")]
137 InvalidOrder { message: String },
138
139 #[error("Insufficient balance: {available} < {required}")]
141 InsufficientBalance { available: String, required: String },
142
143 #[error("Invalid symbol: {symbol}")]
145 InvalidSymbol { symbol: String },
146
147 #[error("Invalid request format: {message}")]
149 InvalidRequest { message: String },
150
151 #[error("Missing required parameter: {param}")]
153 MissingParameter { param: String },
154
155 #[error("Order not found: {order_id}")]
157 OrderNotFound { order_id: String },
158
159 #[error("Position not found: {symbol}")]
161 PositionNotFound { symbol: String },
162}
163
164#[derive(Debug, Error)]
166pub enum BitmexFatalError {
167 #[error("Authentication failed: {message}")]
169 AuthenticationFailed { message: String },
170
171 #[error("Forbidden: {message}")]
173 Forbidden { message: String },
174
175 #[error("Account suspended: {reason}")]
177 AccountSuspended { reason: String },
178
179 #[error("Invalid API credentials")]
181 InvalidCredentials,
182
183 #[error("API version no longer supported")]
185 ApiVersionDeprecated,
186
187 #[error("Critical invariant violation: {invariant}")]
189 InvariantViolation { invariant: String },
190}
191
192impl BitmexError {
193 pub fn from_rate_limit_headers(
201 remaining: Option<&str>,
202 reset: Option<&str>,
203 retry_after: Option<&str>,
204 ) -> Self {
205 let remaining = remaining.and_then(|s| s.parse().ok());
206
207 let reset_at = reset.and_then(|s| {
209 s.parse::<u64>().ok().and_then(|timestamp| {
210 let now = std::time::SystemTime::now()
211 .duration_since(std::time::UNIX_EPOCH)
212 .ok()?
213 .as_secs();
214
215 if timestamp > now {
216 Some(Duration::from_secs(timestamp - now))
217 } else {
218 Some(Duration::from_secs(0))
219 }
220 })
221 });
222
223 let retry_duration = retry_after
225 .and_then(|s| s.parse::<u64>().ok().map(Duration::from_secs))
226 .or(reset_at);
227
228 Self::Retryable {
229 source: BitmexRetryableError::RateLimit {
230 remaining,
231 reset_at,
232 },
233 retry_after: retry_duration,
234 }
235 }
236
237 pub fn from_http_status(status: StatusCode, message: Option<String>) -> Self {
239 match status {
240 StatusCode::BAD_REQUEST => Self::NonRetryable {
241 source: BitmexNonRetryableError::BadRequest {
242 message: message.unwrap_or_else(|| "Bad request".to_string()),
243 },
244 },
245 StatusCode::UNAUTHORIZED => Self::Fatal {
246 source: BitmexFatalError::AuthenticationFailed {
247 message: message.unwrap_or_else(|| "Unauthorized".to_string()),
248 },
249 },
250 StatusCode::FORBIDDEN => Self::Fatal {
251 source: BitmexFatalError::Forbidden {
252 message: message.unwrap_or_else(|| "Forbidden".to_string()),
253 },
254 },
255 StatusCode::NOT_FOUND => Self::NonRetryable {
256 source: BitmexNonRetryableError::NotFound {
257 resource: message.unwrap_or_else(|| "Resource".to_string()),
258 },
259 },
260 StatusCode::METHOD_NOT_ALLOWED => Self::NonRetryable {
261 source: BitmexNonRetryableError::MethodNotAllowed {
262 method: message.unwrap_or_else(|| "Method".to_string()),
263 },
264 },
265 StatusCode::TOO_MANY_REQUESTS => Self::from_rate_limit_headers(None, None, None),
266 StatusCode::SERVICE_UNAVAILABLE => Self::Retryable {
267 source: BitmexRetryableError::ServiceUnavailable,
268 retry_after: None,
269 },
270 StatusCode::GATEWAY_TIMEOUT => Self::Retryable {
271 source: BitmexRetryableError::GatewayTimeout,
272 retry_after: None,
273 },
274 s if s.is_server_error() => Self::Retryable {
275 source: BitmexRetryableError::ServerError { status },
276 retry_after: None,
277 },
278 _ => Self::NonRetryable {
279 source: BitmexNonRetryableError::InvalidRequest {
280 message: format!("Unexpected status: {status}"),
281 },
282 },
283 }
284 }
285
286 #[must_use]
288 pub fn is_retryable(&self) -> bool {
289 matches!(self, Self::Retryable { .. })
290 }
291
292 #[must_use]
294 pub fn is_fatal(&self) -> bool {
295 matches!(self, Self::Fatal { .. })
296 }
297
298 #[must_use]
300 pub fn retry_after(&self) -> Option<Duration> {
301 match self {
302 Self::Retryable { retry_after, .. } => *retry_after,
303 _ => None,
304 }
305 }
306}
307
308impl From<serde_json::Error> for BitmexError {
309 fn from(error: serde_json::Error) -> Self {
310 Self::Json {
311 message: error.to_string(),
312 raw: None,
313 }
314 }
315}
316
317impl From<BitmexBuildError> for BitmexError {
318 fn from(error: BitmexBuildError) -> Self {
319 Self::NonRetryable {
320 source: BitmexNonRetryableError::Validation {
321 field: "parameters".to_string(),
322 message: error.to_string(),
323 },
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use rstest::rstest;
331
332 use super::*;
333
334 #[rstest]
335 fn test_error_classification() {
336 let err = BitmexError::from_http_status(StatusCode::TOO_MANY_REQUESTS, None);
337 assert!(err.is_retryable());
338 assert!(!err.is_fatal());
339
340 let err = BitmexError::from_http_status(StatusCode::UNAUTHORIZED, None);
341 assert!(!err.is_retryable());
342 assert!(err.is_fatal());
343
344 let err = BitmexError::from_http_status(StatusCode::BAD_REQUEST, None);
345 assert!(!err.is_retryable());
346 assert!(!err.is_fatal());
347 }
348
349 #[rstest]
350 fn test_rate_limit_parsing() {
351 let future_timestamp = std::time::SystemTime::now()
353 .duration_since(std::time::UNIX_EPOCH)
354 .unwrap()
355 .as_secs()
356 + 60;
357 let err = BitmexError::from_rate_limit_headers(
358 Some("10"),
359 Some(&future_timestamp.to_string()),
360 None,
361 );
362 match err {
363 BitmexError::Retryable {
364 source: BitmexRetryableError::RateLimit { remaining, .. },
365 retry_after,
366 ..
367 } => {
368 assert_eq!(remaining, Some(10));
369 assert!(retry_after.is_some());
370 let duration = retry_after.unwrap();
371 assert!(duration.as_secs() >= 59 && duration.as_secs() <= 61);
372 }
373 _ => panic!("Expected rate limit error"),
374 }
375 }
376
377 #[rstest]
378 fn test_rate_limit_with_retry_after() {
379 let err = BitmexError::from_rate_limit_headers(Some("0"), None, Some("30"));
380 match err {
381 BitmexError::Retryable {
382 source: BitmexRetryableError::RateLimit { remaining, .. },
383 retry_after,
384 ..
385 } => {
386 assert_eq!(remaining, Some(0));
387 assert_eq!(retry_after, Some(Duration::from_secs(30)));
388 }
389 _ => panic!("Expected rate limit error"),
390 }
391 }
392
393 #[rstest]
394 fn test_retry_after() {
395 let err = BitmexError::Retryable {
396 source: BitmexRetryableError::RateLimit {
397 remaining: Some(0),
398 reset_at: Some(Duration::from_secs(60)),
399 },
400 retry_after: Some(Duration::from_secs(60)),
401 };
402 assert_eq!(err.retry_after(), Some(Duration::from_secs(60)));
403 }
404}