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 429 | 10000
182 | 10006
183 | 10016
184 | 10019
185 | 10403
186 | 10429
187 | 170001
188 | 170005
189 | 170007
190 | 170032
191 | 20003
192 | 20006
193 | 500000
194 )
195}
196
197#[cfg(test)]
198mod tests {
199 use rstest::rstest;
200
201 use super::*;
202
203 #[rstest]
204 fn test_bybit_build_error_display() {
205 let error = BybitBuildError::MissingSymbol;
206 assert_eq!(error.to_string(), "Missing required symbol");
207
208 let error = BybitBuildError::InvalidLimit;
209 assert_eq!(
210 error.to_string(),
211 "Invalid limit: must be between 1 and 1000"
212 );
213
214 let error = BybitBuildError::InvalidTimeRange {
215 start: 100,
216 end: 50,
217 };
218 assert_eq!(
219 error.to_string(),
220 "Invalid time range: start (100) must be less than end (50)"
221 );
222 }
223
224 #[rstest]
225 fn test_bybit_http_error_from_error_response() {
226 let error_response = BybitErrorResponse {
227 ret_code: 10001,
228 ret_msg: "Parameter error".to_string(),
229 ret_ext_info: None,
230 };
231
232 let http_error: BybitHttpError = error_response.into();
233 assert_eq!(http_error.to_string(), "Bybit error 10001: Parameter error");
234 }
235
236 #[rstest]
237 fn test_bybit_http_error_from_json_error() {
238 let json_err = serde_json::from_str::<BybitErrorResponse>("invalid json").unwrap_err();
239 let http_error: BybitHttpError = json_err.into();
240 assert!(http_error.to_string().contains("JSON error"));
241 }
242
243 #[rstest]
244 fn test_bybit_http_error_from_string() {
245 let error_msg = "Invalid parameter value".to_string();
246 let http_error: BybitHttpError = error_msg.into();
247 assert_eq!(
248 http_error.to_string(),
249 "Parameter validation error: Invalid parameter value"
250 );
251 }
252
253 #[rstest]
254 fn test_unexpected_status_error() {
255 let error = BybitHttpError::UnexpectedStatus {
256 status: 502,
257 body: "Server error".to_string(),
258 };
259 assert_eq!(
260 error.to_string(),
261 "Unexpected HTTP status code 502: Server error"
262 );
263 }
264
265 #[rstest]
266 fn test_bybit_submit_order_error_display() {
267 let missing_order_id = BybitSubmitOrderError::MissingOrderId;
268 let rejected = BybitSubmitOrderError::Rejected {
269 reason: "EC_PostOnlyWillTakeLiquidity".to_string(),
270 };
271 let post_submit_lookup = BybitSubmitOrderError::PostSubmitLookup {
272 source: anyhow::anyhow!("No order returned after submission"),
273 };
274
275 assert_eq!(missing_order_id.to_string(), "No order_id in response");
276 assert_eq!(
277 rejected.to_string(),
278 "Order rejected: EC_PostOnlyWillTakeLiquidity"
279 );
280 assert_eq!(
281 post_submit_lookup.to_string(),
282 "Order lookup failed after submission: No order returned after submission"
283 );
284 }
285}