1use crate::error::EMSError;
2use rusty_common::SmartString;
3use simd_json::prelude::{ValueAsScalar, ValueObjectAccess};
4use simd_json::value::owned::Value as JsonValue;
5
6#[must_use]
8pub fn parse_binance_error(json: &JsonValue, exchange: &str) -> Option<EMSError> {
9 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#[must_use]
186pub fn parse_bybit_error(json: &JsonValue, exchange: &str) -> Option<EMSError> {
187 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; }
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#[must_use]
318pub fn parse_coinbase_error(json: &JsonValue, exchange: &str) -> Option<EMSError> {
319 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#[derive(Debug, Clone, Default)]
361pub struct RateLimitInfo {
362 pub retry_after_ms: Option<u64>,
364
365 pub limit: Option<u64>,
368 pub remaining: Option<u64>,
370 pub reset_epoch: Option<u64>,
372
373 pub binance_order_count_1m: Option<u64>,
376 pub binance_order_count_1s: Option<u64>,
378 pub binance_used_weight_1m: Option<u64>,
380 pub binance_used_weight_1s: Option<u64>,
382
383 pub coinbase_remaining: Option<u64>,
386 pub coinbase_limit: Option<u64>,
388 pub coinbase_reset: Option<u64>,
390}
391
392impl RateLimitInfo {
393 #[must_use]
395 pub const fn get_retry_after_ms(&self) -> Option<u64> {
396 self.retry_after_ms
397 }
398
399 #[must_use]
401 pub fn is_approaching_limit(&self) -> bool {
402 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 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 #[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#[must_use]
452pub fn extract_rate_limit_info_detailed(headers: &reqwest::header::HeaderMap) -> RateLimitInfo {
453 let mut info = RateLimitInfo::default();
454
455 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); }
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); }
475 }
476
477 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 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 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 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); }
549 }
550
551 info
552}
553
554#[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#[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}