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