nautilus_okx/common/
failure.rs1use nautilus_live::execution::failure::CommandFailure;
19use nautilus_network::{error::SendError, http::HttpClientError};
20
21use crate::{
22 common::consts::{OKX_ORDER_REQUEST_TIMEOUT_CODE, should_retry_error_code},
23 http::error::OKXHttpError,
24 websocket::error::OKXWsError,
25};
26
27#[must_use]
32pub fn classify_okx_venue_code(error_code: &str, reason: impl Into<String>) -> CommandFailure {
33 let reason = reason.into();
34
35 if error_code.is_empty()
36 || should_retry_error_code(error_code)
37 || error_code == OKX_ORDER_REQUEST_TIMEOUT_CODE
38 {
39 CommandFailure::Ambiguous(reason)
40 } else {
41 CommandFailure::VenueRejected(reason)
42 }
43}
44
45#[must_use]
47pub fn classify_okx_http_failure(error: &OKXHttpError) -> CommandFailure {
48 let reason = error.to_string();
49 match error {
50 OKXHttpError::MissingCredentials
51 | OKXHttpError::ValidationError(_)
52 | OKXHttpError::RequestSerialization(_)
53 | OKXHttpError::HttpClientError(
54 HttpClientError::InvalidProxy(_) | HttpClientError::ClientBuildError(_),
55 ) => CommandFailure::NotSent(reason),
56 OKXHttpError::OkxError { error_code, .. }
57 | OKXHttpError::RetryableOkxError { error_code, .. } => {
58 classify_okx_venue_code(error_code, reason)
59 }
60 OKXHttpError::MalformedResponse(_)
61 | OKXHttpError::ResponseDecoding(_)
62 | OKXHttpError::Canceled(_)
63 | OKXHttpError::HttpClientError(_)
64 | OKXHttpError::RetryableStatus { .. }
65 | OKXHttpError::UnexpectedStatus { .. }
66 | OKXHttpError::OperationTimeout { .. }
67 | OKXHttpError::RetryBudgetExceeded(_)
68 | OKXHttpError::EmptyResponse => CommandFailure::Ambiguous(reason),
69 }
70}
71
72#[must_use]
74pub fn classify_okx_ws_failure(error: &OKXWsError) -> CommandFailure {
75 let reason = error.to_string();
76 match error {
77 OKXWsError::ClientError(_)
78 | OKXWsError::JsonError(_)
79 | OKXWsError::NoActiveClient
80 | OKXWsError::HandlerUnavailable(_)
81 | OKXWsError::TransportSend(
82 SendError::InvalidInput(_)
83 | SendError::BufferFull
84 | SendError::Closed
85 | SendError::Timeout
86 | SendError::ConnectionChanged,
87 ) => CommandFailure::NotSent(reason),
88 OKXWsError::OkxError { error_code, .. } => classify_okx_venue_code(error_code, reason),
89 OKXWsError::ParsingError(_)
90 | OKXWsError::AuthenticationError(_)
91 | OKXWsError::TungsteniteError(_)
92 | OKXWsError::TransportSend(SendError::WriteTimeout | SendError::BrokenPipe(_))
93 | OKXWsError::SendFailed(_)
94 | OKXWsError::OperationTimeout { .. } => CommandFailure::Ambiguous(reason),
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use nautilus_network::http::{HttpClientError, StatusCode};
101 use rstest::rstest;
102
103 use super::*;
104
105 #[rstest]
106 #[case::parameter_reject("51000", "Parameter state error", true)]
107 #[case::system_busy("50013", "System busy, please retry later", false)]
108 #[case::request_timeout("50004", "API endpoint request timeout", false)]
109 #[case::order_timeout("51149", "Order timed out. Please try again.", false)]
110 #[case::rate_limit("50011", "Request too frequent", false)]
111 #[case::ws_rate_limit("60014", "WebSocket requests too frequent", true)]
112 #[case::ws_internal_error("64007", "WebSocket internal error", true)]
113 #[case::invalid_signature("50113", "Invalid signature", true)]
114 #[case::missing_code("", "All operations failed", false)]
115 fn test_classify_okx_venue_code(
116 #[case] error_code: &str,
117 #[case] message: &str,
118 #[case] venue_rejected: bool,
119 ) {
120 let failure = classify_okx_venue_code(error_code, message);
121
122 if venue_rejected {
123 assert_eq!(failure, CommandFailure::VenueRejected(message.to_string()));
124 } else {
125 assert_eq!(failure, CommandFailure::Ambiguous(message.to_string()));
126 }
127 }
128
129 #[rstest]
130 fn test_classify_okx_http_permanent_venue_error_is_rejected() {
131 let error = OKXHttpError::OkxError {
132 error_code: "51000".to_string(),
133 message: "Parameter state error".to_string(),
134 };
135 let reason = error.to_string();
136
137 assert_eq!(
138 classify_okx_http_failure(&error),
139 CommandFailure::VenueRejected(reason)
140 );
141 }
142
143 #[rstest]
144 fn test_classify_okx_http_retryable_venue_error_is_ambiguous() {
145 let error = OKXHttpError::RetryableOkxError {
146 error_code: "50013".to_string(),
147 message: "System busy, please try again later".to_string(),
148 retry_after: None,
149 };
150 let reason = error.to_string();
151
152 assert_eq!(
153 classify_okx_http_failure(&error),
154 CommandFailure::Ambiguous(reason)
155 );
156 }
157
158 #[rstest]
159 fn test_classify_okx_http_missing_credentials_is_not_sent() {
160 let error = OKXHttpError::MissingCredentials;
161
162 assert_eq!(
163 classify_okx_http_failure(&error),
164 CommandFailure::NotSent(error.to_string())
165 );
166 }
167
168 #[rstest]
169 fn test_classify_okx_http_validation_is_not_sent() {
170 let error = OKXHttpError::ValidationError("invalid quantity".to_string());
171
172 assert_eq!(
173 classify_okx_http_failure(&error),
174 CommandFailure::NotSent(error.to_string())
175 );
176 }
177
178 #[rstest]
179 fn test_classify_okx_http_invalid_retry_config_is_not_sent() {
180 let error = OKXHttpError::ValidationError("invalid retry configuration".to_string());
181
182 assert_eq!(
183 classify_okx_http_failure(&error),
184 CommandFailure::NotSent(error.to_string())
185 );
186 }
187
188 #[rstest]
189 fn test_classify_okx_http_timeout_is_ambiguous() {
190 let error = OKXHttpError::OperationTimeout { timeout_ms: 1_000 };
191
192 assert_eq!(
193 classify_okx_http_failure(&error),
194 CommandFailure::Ambiguous(error.to_string())
195 );
196 }
197
198 #[rstest]
199 fn test_classify_okx_http_budget_is_ambiguous() {
200 let error = OKXHttpError::RetryBudgetExceeded("budget exceeded".to_string());
201
202 assert_eq!(
203 classify_okx_http_failure(&error),
204 CommandFailure::Ambiguous(error.to_string())
205 );
206 }
207
208 #[rstest]
209 fn test_classify_okx_http_network_is_ambiguous() {
210 let error = OKXHttpError::HttpClientError(HttpClientError::TransportError(
211 "connection reset".to_string(),
212 ));
213
214 assert_eq!(
215 classify_okx_http_failure(&error),
216 CommandFailure::Ambiguous(error.to_string())
217 );
218 }
219
220 #[rstest]
221 fn test_classify_okx_http_permanent_client_error_is_ambiguous() {
222 let error = OKXHttpError::HttpClientError(HttpClientError::Error(
223 "response body exceeds maximum".to_string(),
224 ));
225
226 assert_eq!(
227 classify_okx_http_failure(&error),
228 CommandFailure::Ambiguous(error.to_string())
229 );
230 }
231
232 #[rstest]
233 fn test_classify_okx_http_unexpected_status_is_ambiguous() {
234 let error = OKXHttpError::UnexpectedStatus {
235 status: StatusCode::INTERNAL_SERVER_ERROR,
236 body: String::new(),
237 };
238
239 assert_eq!(
240 classify_okx_http_failure(&error),
241 CommandFailure::Ambiguous(error.to_string())
242 );
243 }
244
245 #[rstest]
246 fn test_classify_okx_http_response_decoding_is_ambiguous() {
247 let error = OKXHttpError::ResponseDecoding("failed to deserialize".to_string());
248
249 assert_eq!(
250 classify_okx_http_failure(&error),
251 CommandFailure::Ambiguous(error.to_string())
252 );
253 }
254
255 #[rstest]
256 fn test_classify_okx_http_request_serialization_is_not_sent() {
257 let error = OKXHttpError::RequestSerialization("failed to serialize".to_string());
258
259 assert_eq!(
260 classify_okx_http_failure(&error),
261 CommandFailure::NotSent(error.to_string())
262 );
263 }
264
265 #[rstest]
266 fn test_classify_okx_http_empty_response_is_ambiguous() {
267 let error = OKXHttpError::EmptyResponse;
268
269 assert_eq!(
270 classify_okx_http_failure(&error),
271 CommandFailure::Ambiguous(error.to_string())
272 );
273 }
274
275 #[rstest]
276 fn test_classify_okx_ws_handler_unavailable_is_not_sent() {
277 let error = OKXWsError::HandlerUnavailable("channel closed".to_string());
278
279 assert_eq!(
280 classify_okx_ws_failure(&error),
281 CommandFailure::NotSent(error.to_string())
282 );
283 }
284
285 #[rstest]
286 fn test_classify_okx_ws_no_active_client_is_not_sent() {
287 let error = OKXWsError::NoActiveClient;
288
289 assert_eq!(
290 classify_okx_ws_failure(&error),
291 CommandFailure::NotSent(error.to_string())
292 );
293 }
294
295 #[rstest]
296 fn test_classify_okx_ws_json_encode_is_not_sent() {
297 let error = OKXWsError::JsonError("Failed to serialize order: eof".to_string());
298
299 assert_eq!(
300 classify_okx_ws_failure(&error),
301 CommandFailure::NotSent(error.to_string())
302 );
303 }
304
305 #[rstest]
306 fn test_classify_okx_ws_send_failed_is_ambiguous() {
307 let error = OKXWsError::SendFailed("connection reset".to_string());
308
309 assert_eq!(
310 classify_okx_ws_failure(&error),
311 CommandFailure::Ambiguous(error.to_string())
312 );
313 }
314
315 #[rstest]
316 #[case(SendError::Timeout)]
317 #[case(SendError::BufferFull)]
318 fn test_classify_okx_ws_pre_write_failure_is_not_sent(#[case] error: SendError) {
319 let error = OKXWsError::TransportSend(error);
320
321 assert_eq!(
322 classify_okx_ws_failure(&error),
323 CommandFailure::NotSent(error.to_string())
324 );
325 }
326
327 #[rstest]
328 fn test_classify_okx_ws_write_timeout_is_ambiguous() {
329 let error = OKXWsError::TransportSend(SendError::WriteTimeout);
330
331 assert_eq!(
332 classify_okx_ws_failure(&error),
333 CommandFailure::Ambiguous(error.to_string())
334 );
335 }
336
337 #[rstest]
338 fn test_classify_okx_ws_timeout_is_ambiguous() {
339 let error = OKXWsError::OperationTimeout { timeout_ms: 2_000 };
340
341 assert_eq!(
342 classify_okx_ws_failure(&error),
343 CommandFailure::Ambiguous(error.to_string())
344 );
345 }
346}