1use std::time::Duration;
7
8use brokapi_bybit_protocol::error::BybitApiError;
9use brokapi_core::{ConfigError, Error, TransportError};
10
11const RATE_LIMIT_PAUSE: Duration = Duration::from_secs(1);
13
14fn outcome_unknown(code: i64) -> bool {
16 matches!(code, 10000 | 10016 | 429)
17}
18
19fn symbol_unknown(code: i64, message: &str) -> bool {
21 code == 10001 && message.to_ascii_lowercase().contains("symbol invalid")
22}
23
24pub fn map_err(err: BybitApiError) -> Error {
26 match err {
27 BybitApiError::RateLimitExceeded => Error::RateLimited {
28 retry_after: RATE_LIMIT_PAUSE,
29 },
30 BybitApiError::Auth { message } => Error::Config(ConfigError::Message(format!(
31 "authentication refused: {message}"
32 ))),
33 BybitApiError::Http {
34 status: Some(429), ..
35 } => Error::RateLimited {
36 retry_after: RATE_LIMIT_PAUSE,
37 },
38 BybitApiError::Http {
39 status: Some(403),
40 message,
41 } => Error::Transport(TransportError::Message(format!(
42 "HTTP 403: the address is over the request limit (a ban of 10 minutes) or the region is not served: {message}"
43 ))),
44 BybitApiError::Api { code, message } if symbol_unknown(code, &message) => Error::not_found(
45 format!("the exchange does not know this instrument: {message}"),
46 ),
47 BybitApiError::Api {
48 code: 110001,
49 message,
50 } => Error::not_found(format!("the exchange does not know this order: {message}")),
51 BybitApiError::Api { code, message } => Error::Rejected {
52 code: format!("bybit_{code}"),
53 message,
54 },
55 BybitApiError::Http { message, .. }
56 | BybitApiError::Ws { message }
57 | BybitApiError::Subscription { message }
58 | BybitApiError::Parse(message) => Error::Transport(TransportError::Message(message)),
59 }
60}
61
62pub fn map_config_err(err: BybitApiError) -> Error {
64 Error::Config(ConfigError::Message(err.to_string()))
65}
66
67pub fn map_command_err(err: BybitApiError) -> Error {
69 match &err {
70 BybitApiError::Http { status, .. } if status.is_none_or(|s| s >= 500) => Error::Unknown,
71 BybitApiError::Parse(_) | BybitApiError::Ws { .. } => Error::Unknown,
72 BybitApiError::Api { code, .. } if outcome_unknown(*code) => Error::Unknown,
73 BybitApiError::Http {
75 status: Some(s),
76 message,
77 } if (400..500).contains(s) && *s != 429 => Error::Rejected {
78 code: format!("http_{s}"),
79 message: message.clone(),
80 },
81 BybitApiError::Api {
83 code: 110001,
84 message,
85 } => Error::Rejected {
86 code: brokapi_core::NOT_FOUND.into(),
87 message: message.clone(),
88 },
89 _ => map_err(err),
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 fn api(code: i64, message: &str) -> BybitApiError {
98 BybitApiError::Api {
99 code,
100 message: message.into(),
101 }
102 }
103
104 #[test]
105 fn lost_commands_are_unknown_refused_ones_are_not() {
106 let http = |status| BybitApiError::Http {
107 status,
108 message: "x".into(),
109 };
110 assert!(matches!(map_command_err(http(None)), Error::Unknown));
111 assert!(matches!(map_command_err(http(Some(503))), Error::Unknown));
112 assert!(matches!(
113 map_command_err(BybitApiError::Parse("x".into())),
114 Error::Unknown
115 ));
116 assert!(matches!(
117 map_command_err(api(10016, "restarting")),
118 Error::Unknown
119 ));
120 assert!(matches!(
121 map_command_err(api(10000, "timeout")),
122 Error::Unknown
123 ));
124 assert!(matches!(
125 map_command_err(http(Some(429))),
126 Error::RateLimited { .. }
127 ));
128 assert!(matches!(
129 map_command_err(http(Some(403))),
130 Error::Rejected { .. }
131 ));
132 assert!(matches!(
133 map_command_err(BybitApiError::RateLimitExceeded),
134 Error::RateLimited { .. }
135 ));
136 }
137
138 #[test]
139 fn a_refusal_keeps_its_code_and_an_unknown_order_is_not_found() {
140 match map_command_err(api(110072, "OrderLinkedID is duplicate")) {
141 Error::Rejected { code, .. } => assert_eq!(code, "bybit_110072"),
142 other => panic!("{other:?}"),
143 }
144 match map_command_err(api(110001, "Order does not exist")) {
145 Error::Rejected { code, .. } => assert_eq!(code, brokapi_core::NOT_FOUND),
146 other => panic!("{other:?}"),
147 }
148 assert!(map_err(api(110001, "Order does not exist")).is_not_found());
149 assert!(map_err(api(10001, "params error: symbol invalid")).is_not_found());
150 assert!(matches!(
151 map_err(api(10001, "params error: limit")),
152 Error::Rejected { .. }
153 ));
154 }
155
156 #[test]
157 fn a_refused_key_is_a_configuration_error() {
158 let e = map_err(BybitApiError::Auth {
159 message: "10003: API key is invalid".into(),
160 });
161 assert!(matches!(e, Error::Config(_)));
162 }
163}