1use std::time::Duration;
23
24use nautilus_network::http::{HttpClientError, HttpResponse};
25use thiserror::Error;
26use tokio_tungstenite::tungstenite;
27
28#[derive(Debug, Error)]
30pub enum BybitError {
31 #[error("Retryable error: {source}")]
33 Retryable {
34 #[source]
35 source: BybitRetryableError,
36 retry_after: Option<Duration>,
38 },
39
40 #[error("Non-retryable error: {source}")]
42 NonRetryable {
43 #[source]
44 source: BybitNonRetryableError,
45 },
46
47 #[error("Fatal error: {source}")]
49 Fatal {
50 #[source]
51 source: BybitFatalError,
52 },
53
54 #[error("Network error: {0}")]
56 Network(#[from] HttpClientError),
57
58 #[error("WebSocket error: {0}")]
60 WebSocket(String),
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 BybitRetryableError {
78 #[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: u16 },
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 BybitNonRetryableError {
119 #[error("Bad request: {message} (retCode: {ret_code:?})")]
123 BadRequest {
124 message: String,
125 ret_code: Option<i32>,
126 },
127
128 #[error("Resource not found: {resource}")]
130 NotFound { resource: String },
131
132 #[error("Method not allowed: {method}")]
134 MethodNotAllowed { method: String },
135
136 #[error("Validation error: {field}: {message}")]
138 Validation { field: String, message: String },
139
140 #[error("Invalid order: {message} (retCode: {ret_code:?})")]
142 InvalidOrder {
143 message: String,
144 ret_code: Option<i32>,
145 },
146
147 #[error("Insufficient balance: {message}")]
149 InsufficientBalance { message: String },
150
151 #[error("Invalid symbol: {symbol}")]
153 InvalidSymbol { symbol: String },
154
155 #[error("Invalid request format: {message}")]
157 InvalidRequest { message: String },
158
159 #[error("Missing required parameter: {param}")]
161 MissingParameter { param: String },
162
163 #[error("Order not found: {order_id}")]
165 OrderNotFound { order_id: String },
166
167 #[error("Position not found: {symbol}")]
169 PositionNotFound { symbol: String },
170
171 #[error("Bybit error (retCode: {ret_code}): {message}")]
175 BybitApiError { ret_code: i32, message: String },
176}
177
178#[derive(Debug, Error)]
180pub enum BybitFatalError {
181 #[error("Authentication failed: {message}")]
183 AuthenticationFailed { message: String },
184
185 #[error("Forbidden: {message}")]
187 Forbidden { message: String },
188
189 #[error("Account suspended: {reason}")]
191 AccountSuspended { reason: String },
192
193 #[error("Invalid API credentials")]
195 InvalidCredentials,
196
197 #[error("API version no longer supported")]
199 ApiVersionDeprecated,
200
201 #[error("Critical invariant violation: {invariant}")]
203 InvariantViolation { invariant: String },
204
205 #[error("Permission denied: {endpoint}")]
207 PermissionDenied { endpoint: String },
208}
209
210impl BybitError {
211 pub fn from_rate_limit_headers(
224 limit_status: Option<&str>,
225 limit: Option<&str>,
226 reset_timestamp: Option<&str>,
227 ) -> Self {
228 let current = limit_status.and_then(|s| s.parse::<u32>().ok());
229 let max_limit = limit.and_then(|s| s.parse::<u32>().ok());
230
231 let remaining = if let (Some(current), Some(max)) = (current, max_limit) {
232 Some(max.saturating_sub(current))
233 } else {
234 None
235 };
236
237 let reset_at = reset_timestamp.and_then(|s| {
239 s.parse::<u64>().ok().and_then(|timestamp_ms| {
240 let now_ms = std::time::SystemTime::now()
241 .duration_since(std::time::UNIX_EPOCH)
242 .ok()?
243 .as_millis() as u64;
244 if timestamp_ms > now_ms {
245 Some(Duration::from_millis(timestamp_ms - now_ms))
246 } else {
247 Some(Duration::from_secs(0))
248 }
249 })
250 });
251
252 Self::Retryable {
253 source: BybitRetryableError::RateLimit {
254 remaining,
255 reset_at,
256 },
257 retry_after: reset_at,
258 }
259 }
260
261 pub fn from_http_status(status: u16, message: Option<String>) -> Self {
263 match status {
264 400 => Self::NonRetryable {
265 source: BybitNonRetryableError::BadRequest {
266 message: message.unwrap_or_else(|| "Bad request".to_string()),
267 ret_code: None,
268 },
269 },
270 401 => Self::Fatal {
271 source: BybitFatalError::AuthenticationFailed {
272 message: message.unwrap_or_else(|| "Unauthorized".to_string()),
273 },
274 },
275 403 => Self::Fatal {
276 source: BybitFatalError::Forbidden {
277 message: message.unwrap_or_else(|| "Forbidden".to_string()),
278 },
279 },
280 404 => Self::NonRetryable {
281 source: BybitNonRetryableError::NotFound {
282 resource: message.unwrap_or_else(|| "Resource".to_string()),
283 },
284 },
285 405 => Self::NonRetryable {
286 source: BybitNonRetryableError::MethodNotAllowed {
287 method: message.unwrap_or_else(|| "Method".to_string()),
288 },
289 },
290 429 => Self::from_rate_limit_headers(None, None, None),
291 503 => Self::Retryable {
292 source: BybitRetryableError::ServiceUnavailable,
293 retry_after: None,
294 },
295 504 => Self::Retryable {
296 source: BybitRetryableError::GatewayTimeout,
297 retry_after: None,
298 },
299 s if (500..600).contains(&s) => Self::Retryable {
300 source: BybitRetryableError::ServerError { status: s },
301 retry_after: None,
302 },
303 _ => Self::NonRetryable {
304 source: BybitNonRetryableError::InvalidRequest {
305 message: format!("Unexpected status: {status}"),
306 },
307 },
308 }
309 }
310
311 pub fn from_http_response(response: &HttpResponse) -> Self {
313 let status = response.status.as_u16();
314 let message = String::from_utf8_lossy(&response.body).to_string();
315 Self::from_http_status(status, Some(message))
316 }
317
318 pub fn from_bybit_ret_code(ret_code: i32, message: String) -> Self {
323 match ret_code {
324 0 => Self::Config("Success code received as error".to_string()),
325 10001 => Self::NonRetryable {
326 source: BybitNonRetryableError::BadRequest {
327 message,
328 ret_code: Some(ret_code),
329 },
330 },
331 10002 | 10003 | 10004 | 33004 => Self::Fatal {
332 source: BybitFatalError::AuthenticationFailed { message },
333 },
334 10005 => Self::Fatal {
335 source: BybitFatalError::PermissionDenied { endpoint: message },
336 },
337 10006 => Self::from_rate_limit_headers(None, None, None),
338 110001 | 110003 | 110004 => Self::NonRetryable {
339 source: BybitNonRetryableError::InvalidOrder {
340 message,
341 ret_code: Some(ret_code),
342 },
343 },
344 110007 => Self::NonRetryable {
345 source: BybitNonRetryableError::InsufficientBalance { message },
346 },
347 110025 | 110026 => Self::NonRetryable {
348 source: BybitNonRetryableError::OrderNotFound { order_id: message },
349 },
350 110043 => Self::NonRetryable {
351 source: BybitNonRetryableError::InvalidSymbol { symbol: message },
352 },
353 _ if (10000..20000).contains(&ret_code) => Self::NonRetryable {
354 source: BybitNonRetryableError::BybitApiError { ret_code, message },
355 },
356 10429 | 131230 | 148019 => Self::Retryable {
358 source: BybitRetryableError::TemporaryNetwork { message },
359 retry_after: None,
360 },
361 _ if (30000..40000).contains(&ret_code) => Self::NonRetryable {
363 source: BybitNonRetryableError::BybitApiError { ret_code, message },
364 },
365 _ if (130000..140000).contains(&ret_code) => Self::NonRetryable {
367 source: BybitNonRetryableError::BybitApiError { ret_code, message },
368 },
369 _ if (170000..180000).contains(&ret_code) => Self::NonRetryable {
371 source: BybitNonRetryableError::BybitApiError { ret_code, message },
372 },
373 _ if (180000..190000).contains(&ret_code) => Self::NonRetryable {
375 source: BybitNonRetryableError::BybitApiError { ret_code, message },
376 },
377 _ => Self::NonRetryable {
379 source: BybitNonRetryableError::BybitApiError { ret_code, message },
380 },
381 }
382 }
383
384 #[must_use]
386 pub fn is_retryable(&self) -> bool {
387 matches!(self, Self::Retryable { .. })
388 }
389
390 #[must_use]
392 pub fn is_fatal(&self) -> bool {
393 matches!(self, Self::Fatal { .. })
394 }
395
396 #[must_use]
398 pub fn retry_after(&self) -> Option<Duration> {
399 match self {
400 Self::Retryable { retry_after, .. } => *retry_after,
401 _ => None,
402 }
403 }
404}
405
406pub use crate::{
408 http::error::BybitHttpError,
409 websocket::error::{BybitWsError, BybitWsResult},
410};
411
412impl From<serde_json::Error> for BybitError {
413 fn from(error: serde_json::Error) -> Self {
414 Self::Json {
415 message: error.to_string(),
416 raw: None,
417 }
418 }
419}
420
421impl From<tungstenite::Error> for BybitError {
422 fn from(error: tungstenite::Error) -> Self {
423 Self::WebSocket(error.to_string())
424 }
425}
426
427impl From<BybitHttpError> for BybitError {
428 fn from(error: BybitHttpError) -> Self {
429 match error {
430 BybitHttpError::MissingCredentials => {
431 Self::Config("API credentials not configured".to_string())
432 }
433 BybitHttpError::BybitError {
434 error_code,
435 message,
436 } => Self::Config(format!("Bybit error {error_code}: {message}")),
437 BybitHttpError::JsonError(msg) => Self::Json {
438 message: msg,
439 raw: None,
440 },
441 BybitHttpError::ValidationError(msg) => {
442 Self::Config(format!("Validation error: {msg}"))
443 }
444 BybitHttpError::BuildError(e) => Self::Config(format!("Build error: {e}")),
445 BybitHttpError::NetworkError(msg) => Self::Config(format!("Network error: {msg}")),
446 BybitHttpError::UnexpectedStatus { status, body } => Self::Json {
447 message: format!("HTTP {status}: {body}"),
448 raw: Some(body),
449 },
450 }
451 }
452}
453
454impl From<BybitWsError> for BybitError {
455 fn from(error: BybitWsError) -> Self {
456 Self::WebSocket(error.to_string())
457 }
458}
459
460#[cfg(test)]
465mod tests {
466 use rstest::rstest;
467
468 use super::*;
469
470 #[rstest]
471 fn test_error_classification() {
472 let err = BybitError::from_http_status(429, None);
473 assert!(err.is_retryable());
474 assert!(!err.is_fatal());
475
476 let err = BybitError::from_http_status(401, None);
477 assert!(!err.is_retryable());
478 assert!(err.is_fatal());
479
480 let err = BybitError::from_http_status(400, None);
481 assert!(!err.is_retryable());
482 assert!(!err.is_fatal());
483 }
484
485 #[rstest]
486 fn test_rate_limit_parsing() {
487 let future_timestamp_ms = std::time::SystemTime::now()
489 .duration_since(std::time::UNIX_EPOCH)
490 .unwrap()
491 .as_millis() as u64
492 + 60_000; let err = BybitError::from_rate_limit_headers(
495 Some("148"),
496 Some("150"),
497 Some(&future_timestamp_ms.to_string()),
498 );
499
500 match err {
501 BybitError::Retryable {
502 source: BybitRetryableError::RateLimit { remaining, .. },
503 retry_after,
504 ..
505 } => {
506 assert_eq!(remaining, Some(2)); assert!(retry_after.is_some());
508 let duration = retry_after.unwrap();
509 assert!(duration.as_secs() >= 59 && duration.as_secs() <= 61);
510 }
511 _ => panic!("Expected rate limit error"),
512 }
513 }
514
515 #[rstest]
516 fn test_bybit_ret_codes() {
517 let err = BybitError::from_bybit_ret_code(10003, "Invalid API key".to_string());
519 assert!(err.is_fatal());
520
521 let err = BybitError::from_bybit_ret_code(10001, "Invalid parameter".to_string());
523 assert!(!err.is_retryable());
524 assert!(!err.is_fatal());
525
526 let err = BybitError::from_bybit_ret_code(10006, "Rate limit exceeded".to_string());
528 assert!(err.is_retryable());
529
530 let err = BybitError::from_bybit_ret_code(110007, "Not enough balance".to_string());
532 assert!(!err.is_retryable());
533 }
534
535 #[rstest]
536 fn test_retry_after() {
537 let err = BybitError::Retryable {
538 source: BybitRetryableError::RateLimit {
539 remaining: Some(0),
540 reset_at: Some(Duration::from_secs(60)),
541 },
542 retry_after: Some(Duration::from_secs(60)),
543 };
544 assert_eq!(err.retry_after(), Some(Duration::from_secs(60)));
545 }
546
547 #[rstest]
548 fn test_invalid_order_errors() {
549 let err = BybitError::from_bybit_ret_code(110001, "Invalid order".to_string());
550 match err {
551 BybitError::NonRetryable {
552 source: BybitNonRetryableError::InvalidOrder { ret_code, .. },
553 } => {
554 assert_eq!(ret_code, Some(110001));
555 }
556 _ => panic!("Expected invalid order error"),
557 }
558 }
559}