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