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 if timestamp > now {
215 Some(Duration::from_secs(timestamp - now))
216 } else {
217 Some(Duration::from_secs(0))
218 }
219 })
220 });
221
222 let retry_duration = retry_after
224 .and_then(|s| s.parse::<u64>().ok().map(Duration::from_secs))
225 .or(reset_at);
226
227 Self::Retryable {
228 source: BitmexRetryableError::RateLimit {
229 remaining,
230 reset_at,
231 },
232 retry_after: retry_duration,
233 }
234 }
235
236 pub fn from_http_status(status: StatusCode, message: Option<String>) -> Self {
238 match status {
239 StatusCode::BAD_REQUEST => Self::NonRetryable {
240 source: BitmexNonRetryableError::BadRequest {
241 message: message.unwrap_or_else(|| "Bad request".to_string()),
242 },
243 },
244 StatusCode::UNAUTHORIZED => Self::Fatal {
245 source: BitmexFatalError::AuthenticationFailed {
246 message: message.unwrap_or_else(|| "Unauthorized".to_string()),
247 },
248 },
249 StatusCode::FORBIDDEN => Self::Fatal {
250 source: BitmexFatalError::Forbidden {
251 message: message.unwrap_or_else(|| "Forbidden".to_string()),
252 },
253 },
254 StatusCode::NOT_FOUND => Self::NonRetryable {
255 source: BitmexNonRetryableError::NotFound {
256 resource: message.unwrap_or_else(|| "Resource".to_string()),
257 },
258 },
259 StatusCode::METHOD_NOT_ALLOWED => Self::NonRetryable {
260 source: BitmexNonRetryableError::MethodNotAllowed {
261 method: message.unwrap_or_else(|| "Method".to_string()),
262 },
263 },
264 StatusCode::TOO_MANY_REQUESTS => Self::from_rate_limit_headers(None, None, None),
265 StatusCode::SERVICE_UNAVAILABLE => Self::Retryable {
266 source: BitmexRetryableError::ServiceUnavailable,
267 retry_after: None,
268 },
269 StatusCode::GATEWAY_TIMEOUT => Self::Retryable {
270 source: BitmexRetryableError::GatewayTimeout,
271 retry_after: None,
272 },
273 s if s.is_server_error() => Self::Retryable {
274 source: BitmexRetryableError::ServerError { status },
275 retry_after: None,
276 },
277 _ => Self::NonRetryable {
278 source: BitmexNonRetryableError::InvalidRequest {
279 message: format!("Unexpected status: {status}"),
280 },
281 },
282 }
283 }
284
285 #[must_use]
287 pub fn is_retryable(&self) -> bool {
288 matches!(self, Self::Retryable { .. })
289 }
290
291 #[must_use]
293 pub fn is_fatal(&self) -> bool {
294 matches!(self, Self::Fatal { .. })
295 }
296
297 #[must_use]
299 pub fn retry_after(&self) -> Option<Duration> {
300 match self {
301 Self::Retryable { retry_after, .. } => *retry_after,
302 _ => None,
303 }
304 }
305}
306
307impl From<serde_json::Error> for BitmexError {
308 fn from(error: serde_json::Error) -> Self {
309 Self::Json {
310 message: error.to_string(),
311 raw: None,
312 }
313 }
314}
315
316impl From<BitmexBuildError> for BitmexError {
317 fn from(error: BitmexBuildError) -> Self {
318 Self::NonRetryable {
319 source: BitmexNonRetryableError::Validation {
320 field: "parameters".to_string(),
321 message: error.to_string(),
322 },
323 }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use rstest::rstest;
330
331 use super::*;
332
333 #[rstest]
334 fn test_error_classification() {
335 let err = BitmexError::from_http_status(StatusCode::TOO_MANY_REQUESTS, None);
336 assert!(err.is_retryable());
337 assert!(!err.is_fatal());
338
339 let err = BitmexError::from_http_status(StatusCode::UNAUTHORIZED, None);
340 assert!(!err.is_retryable());
341 assert!(err.is_fatal());
342
343 let err = BitmexError::from_http_status(StatusCode::BAD_REQUEST, None);
344 assert!(!err.is_retryable());
345 assert!(!err.is_fatal());
346 }
347
348 #[rstest]
349 fn test_rate_limit_parsing() {
350 let future_timestamp = std::time::SystemTime::now()
352 .duration_since(std::time::UNIX_EPOCH)
353 .unwrap()
354 .as_secs()
355 + 60;
356 let err = BitmexError::from_rate_limit_headers(
357 Some("10"),
358 Some(&future_timestamp.to_string()),
359 None,
360 );
361 match err {
362 BitmexError::Retryable {
363 source: BitmexRetryableError::RateLimit { remaining, .. },
364 retry_after,
365 ..
366 } => {
367 assert_eq!(remaining, Some(10));
368 assert!(retry_after.is_some());
369 let duration = retry_after.unwrap();
370 assert!(duration.as_secs() >= 59 && duration.as_secs() <= 61);
371 }
372 _ => panic!("Expected rate limit error"),
373 }
374 }
375
376 #[rstest]
377 fn test_rate_limit_with_retry_after() {
378 let err = BitmexError::from_rate_limit_headers(Some("0"), None, Some("30"));
379 match err {
380 BitmexError::Retryable {
381 source: BitmexRetryableError::RateLimit { remaining, .. },
382 retry_after,
383 ..
384 } => {
385 assert_eq!(remaining, Some(0));
386 assert_eq!(retry_after, Some(Duration::from_secs(30)));
387 }
388 _ => panic!("Expected rate limit error"),
389 }
390 }
391
392 #[rstest]
393 fn test_retry_after() {
394 let err = BitmexError::Retryable {
395 source: BitmexRetryableError::RateLimit {
396 remaining: Some(0),
397 reset_at: Some(Duration::from_secs(60)),
398 },
399 retry_after: Some(Duration::from_secs(60)),
400 };
401 assert_eq!(err.retry_after(), Some(Duration::from_secs(60)));
402 }
403}