Skip to main content

brokapi_bybit/
error.rs

1//! Mapping of the errors of the protocol layer to the broker-independent [`Error`].
2//!
3//! Two functions: [`map_err`] for reads and for everything that changes nothing, [`map_command_err`] for a command on
4//! an order. They differ on purpose: a command that got no answer may have been done, so it is [`Error::Unknown`].
5
6use std::time::Duration;
7
8use brokapi_bybit_protocol::error::BybitApiError;
9use brokapi_core::{ConfigError, Error, TransportError};
10
11/// How long to wait after the exchange said "too many requests".
12const RATE_LIMIT_PAUSE: Duration = Duration::from_secs(1);
13
14/// The `retCode`s after which an order command may or may not have been done: a timeout and a restart of the service.
15fn outcome_unknown(code: i64) -> bool {
16    matches!(code, 10000 | 10016 | 429)
17}
18
19/// Whether a refusal says the symbol is not one the exchange has.
20fn symbol_unknown(code: i64, message: &str) -> bool {
21    code == 10001 && message.to_ascii_lowercase().contains("symbol invalid")
22}
23
24/// Maps an error of a read.
25pub 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
62/// Maps an error of the construction of the client.
63pub fn map_config_err(err: BybitApiError) -> Error {
64    Error::Config(ConfigError::Message(err.to_string()))
65}
66
67/// Maps an error of a command that changes an order.
68pub 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        // The exchange answered and said no (a 4xx, a refusal code): nothing was done.
74        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        // An order that is not there (already gone, or never was).
82        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}