rusty_ems/error/
exchange_errors.rs

1use crate::error::EMSError;
2use rusty_common::SmartString;
3use simd_json::prelude::{ValueAsScalar, ValueObjectAccess};
4use simd_json::value::owned::Value as JsonValue;
5
6/// Parse Binance error response
7#[must_use]
8pub fn parse_binance_error(json: &JsonValue, exchange: &str) -> Option<EMSError> {
9    // Use "Binance" as default exchange name if empty string is passed
10    let exchange_name = if exchange.is_empty() {
11        "Binance"
12    } else {
13        exchange
14    };
15    let code = json
16        .get("code")
17        .and_then(simd_json::prelude::ValueAsScalar::as_i64)
18        .map(|x| x as i32)
19        .unwrap_or(-1);
20    let msg = json
21        .get("msg")
22        .and_then(|v| v.as_str())
23        .unwrap_or("Unknown error");
24
25    match code {
26        -1000 => Some(EMSError::UnknownError(
27            "An unknown error occurred while processing the request".into(),
28        )),
29        -1001 => Some(EMSError::ConnectionError(
30            "Internal error; unable to process your request".into(),
31        )),
32        -1002 => Some(EMSError::AuthenticationError(
33            "You are not authorized to execute this request".into(),
34        )),
35        -1003 => Some(EMSError::RateLimitExceeded {
36            message: EMSError::rate_limit_with_exchange(exchange_name, msg),
37            retry_after_ms: None,
38        }),
39        -1004 => Some(EMSError::ConnectionError(
40            "Server is busy, please wait and try again".into(),
41        )),
42        -1005 => Some(EMSError::AuthenticationError(
43            "No such IP has been white listed".into(),
44        )),
45        -1006 => Some(EMSError::UnknownError(
46            "An unexpected response was received from the message bus".into(),
47        )),
48        -1007 => Some(EMSError::Timeout {
49            duration_ms: 30000,
50            context: "Request timeout".into(),
51        }),
52        -1010 => Some(EMSError::UnknownError("ERROR_MSG_RECEIVED".into())),
53        -1011 => Some(EMSError::UnknownError(
54            "This IP cannot access this route".into(),
55        )),
56        -1013 => Some(EMSError::InvalidOrderParameters("Invalid quantity".into())),
57        -1014 => Some(EMSError::UnknownError(
58            "Unsupported order combination".into(),
59        )),
60        -1015 => Some(EMSError::UnknownError("Too many new orders".into())),
61        -1016 => Some(EMSError::UnknownError(
62            "This service is no longer available".into(),
63        )),
64        -1020 => Some(EMSError::UnknownError(
65            "This operation is not supported".into(),
66        )),
67        -1021 => Some(EMSError::InvalidOrderParameters("Invalid timestamp".into())),
68        -1022 => Some(EMSError::AuthenticationError("Invalid signature".into())),
69        -1100 => Some(EMSError::InvalidOrderParameters(
70            "Illegal characters found in a parameter".into(),
71        )),
72        -1101 => Some(EMSError::InvalidOrderParameters(
73            "Too many parameters sent for this endpoint".into(),
74        )),
75        -1102 => Some(EMSError::InvalidOrderParameters(
76            "Mandatory parameter was not sent, was empty/null, or malformed".into(),
77        )),
78        -1103 => Some(EMSError::InvalidOrderParameters(
79            "Unknown parameters sent".into(),
80        )),
81        -1104 => Some(EMSError::InvalidOrderParameters(
82            "Not all sent parameters were read".into(),
83        )),
84        -1105 => Some(EMSError::InvalidOrderParameters(
85            "Parameter was empty".into(),
86        )),
87        -1106 => Some(EMSError::InvalidOrderParameters(
88            "Parameter was not required and cannot be used in combination with other parameters"
89                .into(),
90        )),
91        -1111 => Some(EMSError::InvalidOrderParameters("Bad precision".into())),
92        -1112 => Some(EMSError::OrderSubmissionError(
93            "No orders on book for symbol".into(),
94        )),
95        -1114 => Some(EMSError::InvalidOrderParameters(
96            "TimeInForce parameter sent when not required".into(),
97        )),
98        -1115 => Some(EMSError::InvalidOrderParameters(
99            "Invalid timeInForce".into(),
100        )),
101        -1116 => Some(EMSError::InvalidOrderParameters("Invalid orderType".into())),
102        -1117 => Some(EMSError::InvalidOrderParameters("Invalid side".into())),
103        -1118 => Some(EMSError::InvalidOrderParameters(
104            "Empty New Client Order ID".into(),
105        )),
106        -1119 => Some(EMSError::InvalidOrderParameters(
107            "Empty Orig Client Order ID".into(),
108        )),
109        -1120 => Some(EMSError::InvalidOrderParameters("Invalid interval".into())),
110        -1121 => Some(EMSError::InvalidOrderParameters("Invalid symbol".into())),
111        -1125 => Some(EMSError::InvalidOrderParameters(
112            "This listenKey does not exist".into(),
113        )),
114        -1127 => Some(EMSError::InvalidOrderParameters(
115            "Lookup interval is too big".into(),
116        )),
117        -1128 => Some(EMSError::InvalidOrderParameters(
118            "Combination of optional parameters invalid".into(),
119        )),
120        -1130 => Some(EMSError::InvalidOrderParameters(
121            "Invalid data sent for a parameter".into(),
122        )),
123        -1131 => Some(EMSError::OrderSubmissionError(
124            "recvWindow must be less than 60000".into(),
125        )),
126        -2010 => Some(EMSError::InsufficientBalance(
127            "Account has insufficient balance for requested action".into(),
128        )),
129        -2011 => Some(EMSError::OrderCancellationError(
130            "Order does not exist".into(),
131        )),
132        -2013 => Some(EMSError::OrderCancellationError(
133            "Order does not exist".into(),
134        )),
135        -2014 => Some(EMSError::AuthenticationError(
136            "API-key format invalid".into(),
137        )),
138        -2015 => Some(EMSError::AuthenticationError(
139            "Invalid API-key, IP, or permissions for action".into(),
140        )),
141        -2016 => Some(EMSError::OrderSubmissionError(
142            "No trading window could be found for the symbol".into(),
143        )),
144        -2018 => Some(EMSError::InsufficientBalance(
145            "Balance is insufficient".into(),
146        )),
147        -2019 => Some(EMSError::InsufficientBalance(
148            "Margin is insufficient".into(),
149        )),
150        -2020 => Some(EMSError::OrderSubmissionError("Unable to fill".into())),
151        -2021 => Some(EMSError::OrderSubmissionError(
152            "Order would immediately trigger".into(),
153        )),
154        -2022 => Some(EMSError::OrderSubmissionError(
155            "Order would immediately match and take".into(),
156        )),
157        -2023 => Some(EMSError::OrderSubmissionError(
158            "Cannot be placed as Post Only order".into(),
159        )),
160        -2024 => Some(EMSError::InsufficientBalance(
161            "Balance is insufficient".into(),
162        )),
163        -2025 => Some(EMSError::OrderSubmissionError(
164            "Reach max open order limit".into(),
165        )),
166        -2026 => Some(EMSError::OrderSubmissionError(
167            "This order type cannot be used with SPOT trading".into(),
168        )),
169        -2027 => Some(EMSError::InsufficientBalance(
170            "Account has insufficient balance for requested action".into(),
171        )),
172        -2028 => Some(EMSError::OrderSubmissionError(
173            "Your request to cancel all open orders has been accepted".into(),
174        )),
175        _ => Some(EMSError::ExchangeApiError {
176            exchange: exchange_name.into(),
177            code,
178            message: msg.into(),
179            details: None,
180        }),
181    }
182}
183
184/// Parse Bybit error response
185#[must_use]
186pub fn parse_bybit_error(json: &JsonValue, exchange: &str) -> Option<EMSError> {
187    // Use "Bybit" as default exchange name if empty string is passed
188    let exchange_name = if exchange.is_empty() {
189        "Bybit"
190    } else {
191        exchange
192    };
193    let ret_code = json
194        .get("retCode")
195        .and_then(simd_json::prelude::ValueAsScalar::as_i64)
196        .map(|x| x as i32)
197        .unwrap_or(0);
198    let ret_msg = json
199        .get("retMsg")
200        .and_then(|v| v.as_str())
201        .unwrap_or("Unknown error");
202
203    if ret_code == 0 {
204        return None; // Success
205    }
206
207    match ret_code {
208        10001 => Some(EMSError::InvalidOrderParameters(
209            "Parameter validation failed".into(),
210        )),
211        10002 => Some(EMSError::InvalidOrderParameters("Request failed".into())),
212        10003 => Some(EMSError::AuthenticationError("Invalid API key".into())),
213        10004 => Some(EMSError::AuthenticationError("Invalid sign".into())),
214        10005 => Some(EMSError::AuthenticationError("Permission denied".into())),
215        10006 => Some(EMSError::RateLimitExceeded {
216            message: EMSError::rate_limit_with_exchange(exchange_name, "Too many requests"),
217            retry_after_ms: None,
218        }),
219        10007 => Some(EMSError::AuthenticationError(
220            "Authentication failed".into(),
221        )),
222        10010 => Some(EMSError::ConnectionError("IP banned".into())),
223        10014 => Some(EMSError::InvalidOrderParameters(
224            "Invalid parameters".into(),
225        )),
226        10016 => Some(EMSError::ConnectionError("Server error".into())),
227        10017 => Some(EMSError::InvalidOrderParameters("Path not found".into())),
228        10018 => Some(EMSError::RateLimitExceeded {
229            message: EMSError::rate_limit_with_exchange(exchange_name, "Too many visits"),
230            retry_after_ms: None,
231        }),
232        110001 => Some(EMSError::OrderSubmissionError(
233            "Order does not exist".into(),
234        )),
235        110003 => Some(EMSError::OrderSubmissionError(
236            "Quantity exceeded lower limit".into(),
237        )),
238        110004 => Some(EMSError::OrderSubmissionError(
239            "Quantity exceeded upper limit".into(),
240        )),
241        110005 => Some(EMSError::InvalidOrderParameters(
242            "Price exceeded lower limit".into(),
243        )),
244        110006 => Some(EMSError::InvalidOrderParameters(
245            "Price exceeded upper limit".into(),
246        )),
247        110007 => Some(EMSError::InsufficientBalance("Insufficient balance".into())),
248        110008 => Some(EMSError::OrderSubmissionError(
249            "Order has been completed".into(),
250        )),
251        110009 => Some(EMSError::OrderSubmissionError(
252            "Order has been cancelled".into(),
253        )),
254        110010 => Some(EMSError::OrderSubmissionError(
255            "Order does not exist".into(),
256        )),
257        110011 => Some(EMSError::InvalidOrderParameters(
258            "Any adjustments made will not take effect. Incorrect quantity".into(),
259        )),
260        110012 => Some(EMSError::InsufficientBalance("Insufficient balance".into())),
261        110013 => Some(EMSError::OrderSubmissionError(
262            "Risk control triggered".into(),
263        )),
264        110014 => Some(EMSError::InsufficientBalance(
265            "Insufficient available balance".into(),
266        )),
267        110015 => Some(EMSError::OrderSubmissionError(
268            "Market order cannot be post only".into(),
269        )),
270        110016 => Some(EMSError::InvalidOrderParameters("Invalid qty".into())),
271        110017 => Some(EMSError::InvalidOrderParameters(
272            "Buy order price exceeded lower limit".into(),
273        )),
274        110018 => Some(EMSError::InvalidOrderParameters(
275            "Sell order price exceeded upper limit".into(),
276        )),
277        110019 => Some(EMSError::InvalidOrderParameters(
278            "Your order quantity to buy is too large".into(),
279        )),
280        110020 => Some(EMSError::InvalidOrderParameters(
281            "Your order quantity to sell is too large".into(),
282        )),
283        110021 => Some(EMSError::InvalidOrderParameters(
284            "Your order amount is too large".into(),
285        )),
286        110022 => Some(EMSError::OrderSubmissionError(
287            "Your order quantity is lower than minimum".into(),
288        )),
289        110023 => Some(EMSError::InvalidOrderParameters(
290            "Price precision doesn't match tick size".into(),
291        )),
292        110024 => Some(EMSError::InvalidOrderParameters(
293            "Quantity precision doesn't match step size".into(),
294        )),
295        110025 => Some(EMSError::OrderSubmissionError(
296            "Market order price protection triggered".into(),
297        )),
298        110026 => Some(EMSError::InvalidOrderParameters(
299            "Cross margin trading not supported for this coin".into(),
300        )),
301        110027 => Some(EMSError::InsufficientBalance(
302            "Insufficient available balance".into(),
303        )),
304        110028 => Some(EMSError::OrderSubmissionError(
305            "The order value should be greater than 1 USDT".into(),
306        )),
307        _ => Some(EMSError::ExchangeApiError {
308            exchange: exchange_name.into(),
309            code: ret_code,
310            message: ret_msg.into(),
311            details: None,
312        }),
313    }
314}
315
316/// Parse Coinbase error response
317#[must_use]
318pub fn parse_coinbase_error(json: &JsonValue, exchange: &str) -> Option<EMSError> {
319    // Use "Coinbase" as default exchange name if empty string is passed
320    let exchange_name = if exchange.is_empty() {
321        "Coinbase"
322    } else {
323        exchange
324    };
325    let error = json.get("error").and_then(|v| v.as_str()).unwrap_or("");
326    let message = json.get("message").and_then(|v| v.as_str()).unwrap_or("");
327    let error_details = json.get("error_details").and_then(|v| v.as_str());
328
329    match error {
330        "invalid_request" => Some(EMSError::InvalidOrderParameters(message.into())),
331        "unauthorized" => Some(EMSError::AuthenticationError(message.into())),
332        "forbidden" => Some(EMSError::AuthenticationError("Forbidden access".into())),
333        "invalid_scope" => Some(EMSError::AuthenticationError("Invalid API scope".into())),
334        "not_found" => Some(EMSError::OrderCancellationError("Order not found".into())),
335        "method_not_allowed" => Some(EMSError::OperationNotSupported {
336            exchange: exchange_name.into(),
337            operation: "HTTP method not allowed".into(),
338        }),
339        "conflict" => Some(EMSError::OrderSubmissionError(
340            "Request conflicts with current state".into(),
341        )),
342        "too_many_requests" => Some(EMSError::RateLimitExceeded {
343            message: EMSError::rate_limit_with_exchange(exchange_name, message),
344            retry_after_ms: None,
345        }),
346        "internal_server_error" => Some(EMSError::ConnectionError("Internal server error".into())),
347        "service_unavailable" => Some(EMSError::ConnectionError(
348            "Service temporarily unavailable".into(),
349        )),
350        _ => Some(EMSError::ExchangeApiError {
351            exchange: exchange_name.into(),
352            code: -1,
353            message: message.into(),
354            details: error_details.map(std::convert::Into::into),
355        }),
356    }
357}
358
359/// Comprehensive rate limit information from exchange headers
360#[derive(Debug, Clone, Default)]
361pub struct RateLimitInfo {
362    /// Retry after in milliseconds (for backward compatibility)
363    pub retry_after_ms: Option<u64>,
364
365    /// Generic rate limit information
366    /// Maximum number of requests allowed
367    pub limit: Option<u64>,
368    /// Number of requests remaining in the current window
369    pub remaining: Option<u64>,
370    /// Epoch timestamp when the rate limit resets
371    pub reset_epoch: Option<u64>,
372
373    /// Binance specific rate limits
374    /// Orders placed in the last 1 minute
375    pub binance_order_count_1m: Option<u64>,
376    /// Orders placed in the last 1 second
377    pub binance_order_count_1s: Option<u64>,
378    /// Request weight used in the last 1 minute
379    pub binance_used_weight_1m: Option<u64>,
380    /// Request weight used in the last 1 second
381    pub binance_used_weight_1s: Option<u64>,
382
383    /// Coinbase specific rate limits
384    /// Remaining requests for Coinbase API
385    pub coinbase_remaining: Option<u64>,
386    /// Maximum requests allowed for Coinbase API
387    pub coinbase_limit: Option<u64>,
388    /// Epoch timestamp when Coinbase rate limit resets
389    pub coinbase_reset: Option<u64>,
390}
391
392impl RateLimitInfo {
393    /// Get retry after milliseconds for backward compatibility
394    #[must_use]
395    pub const fn get_retry_after_ms(&self) -> Option<u64> {
396        self.retry_after_ms
397    }
398
399    /// Check if any rate limits are approaching the limit (> 80% usage)
400    #[must_use]
401    pub fn is_approaching_limit(&self) -> bool {
402        // Check generic rate limits
403        if let (Some(remaining), Some(limit)) = (self.remaining, self.limit)
404            && limit > 0
405            && (remaining as f64 / limit as f64) < 0.2
406        {
407            return true;
408        }
409
410        // Check Coinbase rate limits
411        if let (Some(remaining), Some(limit)) = (self.coinbase_remaining, self.coinbase_limit)
412            && limit > 0
413            && (remaining as f64 / limit as f64) < 0.2
414        {
415            return true;
416        }
417
418        false
419    }
420
421    /// Get a summary string for logging
422    #[must_use]
423    pub fn summary(&self) -> String {
424        let mut parts = Vec::new();
425
426        if let (Some(remaining), Some(limit)) = (self.remaining, self.limit) {
427            parts.push(format!("generic:{remaining}/{limit}"));
428        }
429
430        if let Some(count) = self.binance_order_count_1m {
431            parts.push(format!("binance_orders_1m:{count}"));
432        }
433
434        if let Some(weight) = self.binance_used_weight_1m {
435            parts.push(format!("binance_weight_1m:{weight}"));
436        }
437
438        if let (Some(remaining), Some(limit)) = (self.coinbase_remaining, self.coinbase_limit) {
439            parts.push(format!("coinbase:{remaining}/{limit}"));
440        }
441
442        if parts.is_empty() {
443            "no_rate_limit_info".to_string()
444        } else {
445            parts.join(", ")
446        }
447    }
448}
449
450/// Extract comprehensive rate limit information from headers
451#[must_use]
452pub fn extract_rate_limit_info_detailed(headers: &reqwest::header::HeaderMap) -> RateLimitInfo {
453    let mut info = RateLimitInfo::default();
454
455    // Parse common rate limit headers
456    if let Some(retry_after) = headers.get("retry-after")
457        && let Ok(seconds) = retry_after.to_str()
458        && let Ok(secs) = seconds.parse::<u64>()
459    {
460        info.retry_after_ms = Some(secs * 1000); // Convert to milliseconds
461    }
462
463    if let Some(rate_limit_reset) = headers.get("x-ratelimit-reset")
464        && let Ok(reset_time) = rate_limit_reset.to_str()
465        && let Ok(reset_epoch) = reset_time.parse::<u64>()
466    {
467        info.reset_epoch = Some(reset_epoch);
468        let now = std::time::SystemTime::now()
469            .duration_since(std::time::UNIX_EPOCH)
470            .expect("System time should be after UNIX epoch")
471            .as_secs();
472        if reset_epoch > now {
473            info.retry_after_ms = Some((reset_epoch - now) * 1000); // Convert to milliseconds
474        }
475    }
476
477    // Parse generic X-RateLimit headers
478    if let Some(limit) = headers.get("x-ratelimit-limit")
479        && let Ok(limit_str) = limit.to_str()
480        && let Ok(limit_val) = limit_str.parse::<u64>()
481    {
482        info.limit = Some(limit_val);
483    }
484
485    if let Some(remaining) = headers.get("x-ratelimit-remaining")
486        && let Ok(remaining_str) = remaining.to_str()
487        && let Ok(remaining_val) = remaining_str.parse::<u64>()
488    {
489        info.remaining = Some(remaining_val);
490    }
491
492    // Parse Binance specific headers
493    if let Some(order_count_1m) = headers.get("x-mbx-order-count-1m")
494        && let Ok(count_str) = order_count_1m.to_str()
495        && let Ok(count_val) = count_str.parse::<u64>()
496    {
497        info.binance_order_count_1m = Some(count_val);
498    }
499
500    if let Some(order_count_1s) = headers.get("x-mbx-order-count-1s")
501        && let Ok(count_str) = order_count_1s.to_str()
502        && let Ok(count_val) = count_str.parse::<u64>()
503    {
504        info.binance_order_count_1s = Some(count_val);
505    }
506
507    if let Some(used_weight_1m) = headers.get("x-mbx-used-weight-1m")
508        && let Ok(weight_str) = used_weight_1m.to_str()
509        && let Ok(weight_val) = weight_str.parse::<u64>()
510    {
511        info.binance_used_weight_1m = Some(weight_val);
512    }
513
514    if let Some(used_weight_1s) = headers.get("x-mbx-used-weight-1s")
515        && let Ok(weight_str) = used_weight_1s.to_str()
516        && let Ok(weight_val) = weight_str.parse::<u64>()
517    {
518        info.binance_used_weight_1s = Some(weight_val);
519    }
520
521    // Parse Coinbase specific headers
522    if let Some(cb_remaining) = headers.get("cb-ratelimit-remaining")
523        && let Ok(remaining_str) = cb_remaining.to_str()
524        && let Ok(remaining_val) = remaining_str.parse::<u64>()
525    {
526        info.coinbase_remaining = Some(remaining_val);
527    }
528
529    if let Some(cb_limit) = headers.get("cb-ratelimit-limit")
530        && let Ok(limit_str) = cb_limit.to_str()
531        && let Ok(limit_val) = limit_str.parse::<u64>()
532    {
533        info.coinbase_limit = Some(limit_val);
534    }
535
536    if let Some(cb_reset) = headers.get("cb-ratelimit-reset")
537        && let Ok(reset_str) = cb_reset.to_str()
538        && let Ok(reset_val) = reset_str.parse::<u64>()
539    {
540        info.coinbase_reset = Some(reset_val);
541        // Calculate retry time if reset is in the future
542        let now = std::time::SystemTime::now()
543            .duration_since(std::time::UNIX_EPOCH)
544            .expect("System time should be after UNIX epoch")
545            .as_secs();
546        if reset_val > now {
547            info.retry_after_ms = Some((reset_val - now) * 1000); // Convert to milliseconds
548        }
549    }
550
551    info
552}
553
554/// Extract rate limit information from headers (backward compatibility)
555#[must_use]
556pub fn extract_rate_limit_info(headers: &reqwest::header::HeaderMap) -> Option<u64> {
557    extract_rate_limit_info_detailed(headers).get_retry_after_ms()
558}
559
560/// Helper function to convert HTTP status codes to `EMSError`
561#[must_use]
562pub fn http_status_to_error(
563    status: reqwest::StatusCode,
564    exchange: &str,
565    body: Option<&str>,
566) -> EMSError {
567    match status {
568        reqwest::StatusCode::UNAUTHORIZED => EMSError::auth_failed_error(exchange),
569        reqwest::StatusCode::FORBIDDEN => EMSError::access_forbidden_error(exchange),
570        reqwest::StatusCode::NOT_FOUND => {
571            let mut message = SmartString::new();
572            message.push_str(exchange);
573            message.push_str(": Resource not found");
574            if let Some(body) = body
575                && !body.is_empty()
576            {
577                message.push_str(" - ");
578                message.push_str(body);
579            }
580            EMSError::OrderCancellationError(message)
581        }
582        reqwest::StatusCode::TOO_MANY_REQUESTS => EMSError::RateLimitExceeded {
583            message: EMSError::rate_limit_exceeded_error(exchange),
584            retry_after_ms: None,
585        },
586        reqwest::StatusCode::INTERNAL_SERVER_ERROR => EMSError::internal_server_error(exchange),
587        reqwest::StatusCode::SERVICE_UNAVAILABLE => EMSError::service_unavailable_error(exchange),
588        reqwest::StatusCode::GATEWAY_TIMEOUT => EMSError::Timeout {
589            duration_ms: 30000,
590            context: EMSError::gateway_timeout_error(exchange),
591        },
592        _ => {
593            let mut message = SmartString::new();
594            message.push_str(exchange);
595            message.push_str(": HTTP ");
596            message.push_str(&status.as_u16().to_string());
597            message.push(' ');
598            message.push_str(status.canonical_reason().unwrap_or("Unknown Status"));
599            EMSError::ExchangeApiError {
600                exchange: exchange.into(),
601                code: i32::from(status.as_u16()),
602                message,
603                details: body.map(std::convert::Into::into),
604            }
605        }
606    }
607}