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