nautilus_bybit/http/
error.rs1use nautilus_network::http::HttpClientError;
24use serde::{Deserialize, Serialize};
25use thiserror::Error;
26
27#[derive(Debug, Clone, Error)]
29pub enum BybitBuildError {
30 #[error("Missing required category")]
32 MissingCategory,
33 #[error("Missing required symbol")]
35 MissingSymbol,
36 #[error("Missing required interval")]
38 MissingInterval,
39 #[error("Invalid limit: must be between 1 and 1000")]
41 InvalidLimit,
42 #[error("Invalid time range: start ({start}) must be less than end ({end})")]
44 InvalidTimeRange { start: i64, end: i64 },
45 #[error("Cannot specify both 'orderId' and 'orderLinkId'")]
47 BothOrderIds,
48 #[error("Missing required order identifier (orderId or orderLinkId)")]
50 MissingOrderId,
51}
52
53#[derive(Clone, Debug, Deserialize, Serialize)]
58#[serde(rename_all = "camelCase")]
59pub struct BybitErrorResponse {
60 pub ret_code: i32,
62 pub ret_msg: String,
64 #[serde(default)]
66 pub ret_ext_info: Option<serde_json::Value>,
67}
68
69#[derive(Debug, Clone, Error)]
71pub enum BybitHttpError {
72 #[error("Missing credentials for authenticated request")]
74 MissingCredentials,
75 #[error("Bybit error {error_code}: {message}")]
77 BybitError { error_code: i32, message: String },
78 #[error("JSON error: {0}")]
80 JsonError(String),
81 #[error("Parameter validation error: {0}")]
83 ValidationError(String),
84 #[error("Build error: {0}")]
86 BuildError(#[from] BybitBuildError),
87 #[error("Request canceled: {0}")]
89 Canceled(String),
90 #[error("Network error: {0}")]
92 NetworkError(String),
93 #[error("Unexpected HTTP status code {status}: {body}")]
95 UnexpectedStatus { status: u16, body: String },
96}
97
98#[derive(Debug, Error)]
100pub enum BybitSubmitOrderError {
101 #[error("No order_id in response")]
103 MissingOrderId,
104 #[error("Order rejected: {reason}")]
106 Rejected {
107 reason: String,
109 },
110 #[error("Order lookup failed after submission: {source}")]
112 PostSubmitLookup {
113 #[source]
115 source: anyhow::Error,
116 },
117}
118
119#[derive(Debug, Error)]
121pub enum BybitCancelOrderError {
122 #[error("No order_id in cancel response")]
124 MissingOrderId,
125 #[error("Order lookup failed after cancellation: {source}")]
127 PostCancelLookup {
128 #[source]
130 source: anyhow::Error,
131 },
132}
133
134#[derive(Debug, Error)]
136pub enum BybitModifyOrderError {
137 #[error("No order_id in amend response")]
139 MissingOrderId,
140 #[error("Order lookup failed after amendment: {source}")]
142 PostModifyLookup {
143 #[source]
145 source: anyhow::Error,
146 },
147}
148
149impl From<HttpClientError> for BybitHttpError {
150 fn from(error: HttpClientError) -> Self {
151 Self::NetworkError(error.to_string())
152 }
153}
154
155impl From<String> for BybitHttpError {
156 fn from(error: String) -> Self {
157 Self::ValidationError(error)
158 }
159}
160
161impl From<serde_json::Error> for BybitHttpError {
164 fn from(error: serde_json::Error) -> Self {
165 Self::JsonError(error.to_string())
166 }
167}
168
169impl From<BybitErrorResponse> for BybitHttpError {
170 fn from(error: BybitErrorResponse) -> Self {
171 Self::BybitError {
172 error_code: error.ret_code,
173 message: error.ret_msg,
174 }
175 }
176}
177
178pub(crate) fn is_bybit_ambiguous_order_error_code(code: i64) -> bool {
179 matches!(
180 code,
181 10000 | 10016 | 10019 | 170001 | 170007 | 170032 | 20006 | 500000
182 )
183}
184
185#[cfg(test)]
186mod tests {
187 use rstest::rstest;
188
189 use super::*;
190
191 #[rstest]
192 fn test_bybit_build_error_display() {
193 let error = BybitBuildError::MissingSymbol;
194 assert_eq!(error.to_string(), "Missing required symbol");
195
196 let error = BybitBuildError::InvalidLimit;
197 assert_eq!(
198 error.to_string(),
199 "Invalid limit: must be between 1 and 1000"
200 );
201
202 let error = BybitBuildError::InvalidTimeRange {
203 start: 100,
204 end: 50,
205 };
206 assert_eq!(
207 error.to_string(),
208 "Invalid time range: start (100) must be less than end (50)"
209 );
210 }
211
212 #[rstest]
213 fn test_bybit_http_error_from_error_response() {
214 let error_response = BybitErrorResponse {
215 ret_code: 10001,
216 ret_msg: "Parameter error".to_string(),
217 ret_ext_info: None,
218 };
219
220 let http_error: BybitHttpError = error_response.into();
221 assert_eq!(http_error.to_string(), "Bybit error 10001: Parameter error");
222 }
223
224 #[rstest]
225 fn test_bybit_http_error_from_json_error() {
226 let json_err = serde_json::from_str::<BybitErrorResponse>("invalid json").unwrap_err();
227 let http_error: BybitHttpError = json_err.into();
228 assert!(http_error.to_string().contains("JSON error"));
229 }
230
231 #[rstest]
232 fn test_bybit_http_error_from_string() {
233 let error_msg = "Invalid parameter value".to_string();
234 let http_error: BybitHttpError = error_msg.into();
235 assert_eq!(
236 http_error.to_string(),
237 "Parameter validation error: Invalid parameter value"
238 );
239 }
240
241 #[rstest]
242 #[case(10000, true)]
243 #[case(10016, true)]
244 #[case(10019, true)]
245 #[case(170001, true)]
246 #[case(170007, true)]
247 #[case(170032, true)]
248 #[case(20006, true)]
249 #[case(500000, true)]
250 #[case(429, false)]
251 #[case(10006, false)]
252 #[case(10403, false)]
253 #[case(10429, false)]
254 #[case(170005, false)]
255 #[case(20003, false)]
256 fn test_order_error_ambiguity(#[case] code: i64, #[case] expected: bool) {
257 assert_eq!(is_bybit_ambiguous_order_error_code(code), expected);
258 }
259
260 #[rstest]
261 fn test_unexpected_status_error() {
262 let error = BybitHttpError::UnexpectedStatus {
263 status: 502,
264 body: "Server error".to_string(),
265 };
266 assert_eq!(
267 error.to_string(),
268 "Unexpected HTTP status code 502: Server error"
269 );
270 }
271
272 #[rstest]
273 fn test_bybit_submit_order_error_display() {
274 let missing_order_id = BybitSubmitOrderError::MissingOrderId;
275 let rejected = BybitSubmitOrderError::Rejected {
276 reason: "EC_PostOnlyWillTakeLiquidity".to_string(),
277 };
278 let post_submit_lookup = BybitSubmitOrderError::PostSubmitLookup {
279 source: anyhow::anyhow!("No order returned after submission"),
280 };
281
282 assert_eq!(missing_order_id.to_string(), "No order_id in response");
283 assert_eq!(
284 rejected.to_string(),
285 "Order rejected: EC_PostOnlyWillTakeLiquidity"
286 );
287 assert_eq!(
288 post_submit_lookup.to_string(),
289 "Order lookup failed after submission: No order returned after submission"
290 );
291 }
292}